【问题标题】:How to avoid stackoverflow error for n = 8 in N Rooks problem如何避免 N Rooks 问题中 n = 8 的 stackoverflow 错误
【发布时间】:2019-09-20 03:51:37
【问题描述】:

我想使用最大 N = 8 的递归解决 N x N 板中的 N Rooks 问题.我的代码适用于 N = 2, 3, 4, 5, 67。但是当 N = 8 它给出了很多可能的结果,从 1 0 0 0 0 0 0 0 的第一行开始然后给出 stackoverflow 错误,然后从 0 1 0 0 0 0 0 0 的第一行开始检查其他可能的结果。

我知道像 斐波那契数列阶乘等一般递归,我可以追踪它们。然后我遇到了一种新的递归形式,称为回溯递归。然后我开始学习这种递归形式背后的逻辑,并阅读了一些伪代码算法。实际上,这种形式的递归在我看来比普通递归更难构建。

public class NRooks {
/**
 * In this code r = which row, c = which column.
 * lastY method just returns column c of last placed rook in
 * a given row r in order to remove it.
 * row.length, col.length, board.length have no special meaning. They all
 * equal to the dimension of board N.
 * main() method always initiates first row(r = 0). Therefore in main()
 * method r remains 0 and c changes as you can see in putRook(0, i). 
 * So solve() method always begins from second row(r = 1).
 */

private static int found = 0;
private static int[][] board;
private static int[] row;
private static int[] col;

public static void putRook(int r, int c) {
    board[r][c] = 1;
    row[r]  = 1;
    col[c]  = 1;
}

public static void removeRook(int r, int c) {
    board[r][c] = 0;
    row[r]  = 0;
    col[c]  = 0;
}

public static boolean isValid(int r, int c) {
    if (row[r] == 0 && col[c] == 0) return true;
    return false;
}

public static void showBoard() {
    for (int r = 0; r < board.length; r++) {
        for (int c = 0; c < board.length; c++) {
            System.out.print(board[r][c] + " ");
        }
        System.out.println();
    }
    System.out.println();
}

public static int lastY(int r) {
    for (int j = 0; j < board.length; j++) {
        if (board[r][j] == 1) return j;
    }
    return -1;
}


public static boolean solve(int r, int c) {
    int last;

    if (r == 0) return false;

    if (r == col.length) {
        found++;
        /**
         * When I dont include below printline statement my code 
         * works fine until N = 7 then gives SO error.
         * But When I include this print statement in order
         * to print number of results my code works fine until
         * N = 6 then gives SO error
         */
        //System.out.println("Found: " + found);
        showBoard();
        r--;
        last = lastY(r);
        removeRook(r, last);
        c = last + 1;
    }

    for (int j = c; j < row.length; j++) {
        if (isValid(r, j)) {
            putRook(r, j);
            return solve(r + 1, 0);
        }
    }

    last = lastY(r - 1);
    removeRook(r - 1, last);
    return solve(r - 1, last + 1);
}

public static void main(String[] args) {
    int n = Integer.parseInt(args[0]);
    board = new int[n][n];
    row = new int[n];
    col = new int[n];

    for (int i = 0; i < row.length; i++) {
        boolean finished; // not important
        putRook(0, i);
        finished = solve(1, 0);
        if (finished) System.out.println("============"); // ignore this too
    }
}
}

Stackoverflow 指向包含对 solve() 方法的递归调用的行。

注意:我只知道 C 之类的 java 语法和基本数据抽象。我用我的这个级别的 Java 编写了这段代码。

我想自己解决这个问题和 N 个皇后问题。 因为这些问题有很多解决方案,无论是数学上还是算法上。而且我现在对高级 Java 数据抽象 东西不感兴趣。
我只想要一些关于我的代码 sn-p 的建议,例如

  • 您的回溯算法效率不高。 (很直接)
  • 您需要使用一些 Java 数据抽象的东西来有效地解决这个问题。
  • 您需要使用另一种形式的递归,例如 尾递归(我也听说过。)
  • ....

【问题讨论】:

  • 暂时离开“运行代码”,拿一张纸和一支笔,然后看看你的r在运行这个函数时会发生什么。 非常具体r == col.length 时会发生什么。 r 可以承担什么价值?这对那个终端条件意味着什么?它真的正确吗?
  • 我怀疑你的逻辑有缺陷。 N=8 不应该太大而导致溢出,因为如果实施正确,您应该只递归 8 次调用。阅读ericlippert.com/2014/03/05/how-to-debug-small-programs 了解调试代码以查找错误的提示。
  • 您的 for 循环在我看来很可疑。递归解决方案不需要任何 for 循环。我也同意 @Mike'Pomax'Kamermans 的观点,即您应该远离代码并手动解决问题。
  • 问题:这段代码的目的是什么?您想找到问题的一个解决方案吗?还是您想找到所有解决方案?
  • 代码完成后,我很乐意试一试。正如@Code-Apprentice 已经指出的那样,没有任何理由比 8 次调用更深:到那时,您会击中棋盘的另一边。通过手动和主要调试工具跟踪调用序列,跟踪输出。还要记录您的程序,以便我们了解您认为您在每个区块中所做的事情。

标签: java algorithm recursion backtracking


【解决方案1】:

出现 Stack Overflow 错误的主要问题是递归的结构方式。在main方法中调用solve的那一刻,它不断地递归越来越深;事实上,它的所有调用都形成了一个数千次调用的深度链。对于 n=7,有 3193 个嵌套调用(我添加了一个计数器来检查这个)。对于 n=8,它会在我的机器上溢出堆栈之前执行大约 5k 次递归调用 - 我猜默认情况下堆栈大小相当小。

因此,要使其适用于更高的 n 值,您需要以一种不会将所有递归调用作为单个链执行的方式来重构递归。我可以争辩说,您当前的解决方案并没有真正回溯,因为它从未真正回溯。让我来说明一下回溯对于一个更简单的问题意味着什么。假设您想以编程方式打印长度为 n=3(“000”到“111”)的所有二进制字符串,而不依赖于知道 n 的值。对此的实现可能是这样的:

def build_binary_string(current_prefix, chars_left):
  if chars_left == 0:
    print current_prefix
    return
  build_binary_string(current_prefix + 'a', chars_left - 1)
  build_binary_string(current_prefix + 'b', chars_left - 1)

build_binary_string("", 3)

有趣的事情(回溯!)发生在使用参数(“00”,1)调用build_binary_string的那一刻:

  • build_binary_string("000", 0) 被调用,打印“000”并立即返回
  • 我们回到了build_binary_string("00", 1)函数调用,马上就要执行build_binary_string(current_prefix + 'b', chars_left - 1)
  • build_binary_string("001", 0) 被调用,打印“001”并立即返回

控制流从build_binary_string("000", 0)返回到build_binary_string("00", 1)并选择进行另一个函数调用的那一点是回溯。请注意,递归深度从未超过 3。

【讨论】:

    【解决方案2】:

    我无法测试你的代码,因为我没有你的一些方法,但是 int j = c 应该是 int j = r 吗?

    for (int j = c; j < row.length; j++) {
        if (isValid(row, col, r, j)) {
            putRook(b, row, col, r, j);
            return solve(b, row, col, r + 1, 0);
        }
    }
    

    在这一行中,您将 0 传递给 c,然后在 for 循环条件中声明 j=c,因此 j

       return solve(b, row, col, r + 1, 0);
    

    编辑:我现在看到 c 正在上面的 if 块中声明,但如果该 if 块没有被执行,这应该是一个无限循环。也许检查一下 r == col.length 是否正确执行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-29
      • 2018-11-30
      相关资源
      最近更新 更多