如何解决Java中的“ N”皇后难题?

 

在此博客中,我们将解决Java中的“ N”皇后难题。我们将从问题的概述开始,继续了解解决方案所涉及的挑战,最后使用递归以Java编写代码。与往常一样,我将通过适当的推理来详细解释代码。在必要时,我还会提供一些图像,这些图像将帮助您可视化理论和解决方案。

在我以前的博客中,我们详细了解了4个皇后难题。通过使用大量的图形表示,我们了解了问题和解决方案,选择了一个单维数组而不是一个二维数组,最后,我们遍历了Java中的每一行代码。如果您完全不熟悉4/8 /'N'女王拼图,建议您先阅读该博客。

什么是“ N”皇后问题?

在4个皇后难题中,我们面临的问题是将4个皇后放置在4 * 4的国际象棋棋盘上,这样就不会有两个皇后互相攻击。在“ N”个皇后问题中,我们将得到一个N * N的棋盘,并尝试放置“ N”个皇后,这样就不会有2个皇后相互攻击。首先考虑N = 8。如果2个皇后不应该互相攻击,则意味着没有两个皇后共享:

  1. 同一行
  2. 同一列
  3. 对角线

“ N”皇后问题的约束条件与“ 4皇后”问题相同。从代码角度来看,我们实际上将重用相同的函数noKill来检查冲突。4皇后问题有满足这些约束的2个解。8个皇后问题有92个解决方案–我们可以以92种不同的方式将8个皇后放置在8 * 8板上,以满足上述约束。

8皇后问题的92个解决方案之一

难题的逻辑解决方案

将8个皇后放在板上的步骤与“ 4个皇后”拼图相似。我们采用相同的方法将皇后区逐列放置。上图显示了皇后区Q1至Q8是如何从左到右放置在列中的。

为了快速入门,为了不打冲突地放置8个皇后区,我们从左上角开始。第一个皇后区放置在该正方形中。我们移至第二列,并在该列中找到与第一个皇后区没有冲突的合适位置。然后我们进入第3个皇后,在这里我们将检查与前两个皇后的冲突。重复此过程,直到我们能够将所有8个皇后放在板上。如果在某个时候发现冲突,我们将回溯到上一列,并将女王/王后移动到该列中的下一个位置/行。

4皇后问题共有的技术零件

处理结果

在4 * 4 Queens场景中,我们初始化了一个大小为4的单维数组以保存结果。我们将使用相同的一维数组,但是由于我们要处理8个皇后,或者实际上我们继续将其推广为'N'个皇后,因此我们将动态初始化大小为'N'的电路板。

1个
int [] board = new int[noOfQueens];

数组的大小将是我们要放置在板上的皇后数量,让我们将其作为输入并将其分配给上面显示的参数。

'noKill'功能检查冲突

我们在4个皇后问题中为此函数编写了代码,并且对于“ N”个皇后问题,这部分代码也相同。我们检查函数中是否存在相同的行和对角线冲突。由于我们在每一列中放置一个皇后,因此无需对列进行冲突检查。

作为快速刷新,board数组中的值将表示特定列中每个皇后的行号,该列用作该数组的索引。

单维数组,其中包含8个皇后问题的解决方案之一

如上所示,板数组将包含[0、4、7、5、2、6、1、3]。它们代表放置女王的行号。列号将成为数组的索引。因此,1号皇后号位于{第0行,第0列},8号皇后号位于{第3行,第7列},依此类推。

1个
2个
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean noKill(int[] board, int currentColumnOfQueen) {
 
        for (int i = 0; i < currentColumnOfQueen; i++) {
            // same row
            if (board[i] == board[currentColumnOfQueen])
                return false;
 
            // Diagonal
            if ((currentColumnOfQueen - i) == Math.abs(board[currentColumnOfQueen] - board[i])) {
                return false;
            }
        }
        return true;
}

我以前的博客中已经详细解释了此功能中的代码。让我们总结一下此函数中的代码-

  1. “ for”循环检查当前要放置的皇后是否与所有先前放置的皇后有任何冲突。因此,循环从0开始,并向上移动到所考虑的当前列的上一列。
  2. 可以通过将所有先前索引(先前放置的皇后的位置)的板数组中的值与争用中当前位置的值进行比较来检查行冲突。如果它们相同,则意味着我们存在行冲突。
  3. 由正方形组成的棋盘,任何对角线冲突都可以通过2个正方形的行和列值之差来检测。如果差异产生相同的值,则我们有一个对角线冲突。如下所示,当我们要在第4列,第1行,{1、4}处放置皇后5时,for循环将检查与现有皇后的所有对角线冲突。当该循环运行且i = 1时,Q2为{4,1}。行和列值之差的绝对值得出3,因此检测到对角线。

Q5和Q2之间的对角线冲突

这些是4个皇后和'N'queens问题之间的共同部分。现在,让我们看一下如何改进4个皇后难题的代码,并使它更简洁通用,足以处理“ N”个皇后。

使用递归求解“ N”个皇后

我们在解决4个皇后问题的同时使用了嵌套的“ for”循环。实际上,我们为4个皇后使用了4个“ for”循环。由于我们要求解8个皇后或实际上是“ N”个皇后,因此多个“ for”循环不是可行的解决方案。我们需要提出另一个解决方案,递归将在这里为我们提供帮助。

当您想到递归时,请考虑将一个大问题分解为子问题。您有时也可以在生活中应用这种哲学!我们需要放置8个皇后,让我们分解一下,让我们尝试放置1个皇后。如果我们有一个函数可以解决在所有约束下放置1个皇后的问题,请移至下一个皇后,然后再次执行相同的步骤。我们可以考虑将1个皇后作为我们的子问题,而我们有8个或“ N”个子问题。当我们使用递归时,该函数会不断调用自身。

好吧,一个人不能不停地递归地调用该函数!我的意思是,想象一下无休止地解决生活中的问题,我的意思是,我们可以做到这一点,但我们绝对希望打破这种循环,对吧?

如果您考虑放置“ N”个皇后的过程,我们从左上角的第0列开始,并在每一列中全盘查找位置,以免发生冲突。我们什么时候停止?当我们将所有“ N”个皇后放在棋盘上时,我们停止。从代码角度来看,我们可以从0开始,然后在放置每个皇后之后,我们可以递增该计数器。当该计数器等于“ N”时,我们停止。

当特定正方形发生冲突时,由于当前冲突产生了冲突,因此我们回溯到上一列,然后移至该列中的下一行/位置。然后,相同的过程继续。

让我们将所有这些理论都放在一个函数中,并了解代码-

1个
2个
3
4
5
6
7
8
9
10
11
12
13
private static void placeQueen(int[] board, int current, int noOfQueens) {
        if (current == noOfQueens) {
            displayQueens(board);
            return;
        }
 
        for (int i = 0; i < noOfQueens; i++) {
            board[current] = i;
            if (noKill(board, current)) {
                placeQueen(board, current + 1, noOfQueens);
            }
        }
}

函数placeQueen具有3个参数–

  1. 板阵列将保持放置在板上的皇后区的位置。
  2. 一个counter,current,最初将为0。这将跟踪已经放置在板上的皇后数量。每次放置一个女王/王后时都不会发生冲突,该值将递增。
  3. 最后一个参数是我们要放置的皇后数量。

包含“ if”条件的代码部分仅检查我们是否放置了所有皇后。如果已放置所有皇后,则从函数中返回。这正是我们递归过程的退出条件。我希望很容易找到我们生活中所有问题的退出条件!

1个
2个
3
4
if (current == noOfQueens) {
    displayQueens(board);
    return;
}

“ for”循环涉及更多点。让我们看一下这段代码。

1个
2个
3
4
5
6
7
8
9
private static placeQueen(int[] board, int current, int noOfQueens) {
    ....
    for (int i = 0; i < noOfQueens; i++) {
        board[current] = i;
        if (noKill(board, current)) {
            placeQueen(board, current + 1, noOfQueens);
        }
    }
}

让我们详细了解此代码-

  • for循环迭代输入输入的皇后数N次,解决此难题的第一步是将第一个皇后放在左上角,然后将其他皇后放在其他列中。但是要记住的重要一点是,第一任皇后本身将担任8个不同的职位。

Q1将迭代8次,该解决方案可以在第一列中从0 -7的任何位置具有Q1

  • 代码的下一行board [current] = i固定女王/王后在左上角的位置。请记住,当我们第一次调用此函数时,current等于0。然后,对“ noKill”函数的调用将返回true,因为它是第一个皇后,并且显然不会有冲突。这意味着第一位女王已经被成功安置。成功放置第一个皇后之后,下一步是什么?我们移至第二列,并尝试确定第二个女王的位置。我们讨论了使用递归找到并固定每个女王的位置。让我们进行递归调用。
  • 递归调用是通过将当前计数器增加1来进行的。我们再次进入“ for”循环。应该执行几次?这是做什么的?它检查{0,1}处第二个女王的位置是否可以固定,从而调用'noKill'函数。在这种情况下,“ noKill”函数将返回false,因为与第一个女王/王后存在行冲突。该代码从“ if”循环中出来,并在“ for”循环中执行下一个迭代。现在,此循环将负责再次确定第二个皇后的位置,但是这次它将尝试在第二个行中进行。此列中有多少个可能的位置?好吧,这是8行,所以是8次。因此,要回答前面的问题,它可以执行8次,并且它的作用是固定第二个皇后的位置。
  • 直到我们能够为第一个皇后的第一个位置固定所有8个皇后的位置为止。那时,对于8号皇后,如果'noKill'函数返回true,我们将放置所有8号皇后。“当前”值将为8,如果“ if”条件将返回,则函数将返回。
  • 请记住,对于左上角的第一个位置,第二个皇后可以在第二列中占据8个不同的位置,对于第二个皇后的每个位置,第三个皇后可以占据8个不同的位置,依此类推。为什么递归过程是解决此问题的有效方法,无论是否有4、8或'N'个皇后。因此,实际上,正在形成一个完整的分支,既在深度方面又在宽度方面。

该递归过程的一个非常重要的部分是,仅当冲突不存在时才进行递归调用,因此存在修剪,并且仅在没有冲突时才形成分支。如果'noKill'函数返回false,则我们不会再次进行递归调用,这又避免了在递归树中创建另一个分支。

完整的代码

1个
2个
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18岁
19
20
21岁
22
23
24
25
26
27
28岁
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class NQueens {
 
    public static void main(String[] args) {
        int n = 8;
        nQueens(n);
    }
 
    private static void nQueens(int noOfQueens) {
        int [] board = new int[noOfQueens];
        placeQueen(board, 0, noOfQueens);
    }
 
    private static void placeQueen(int[] board, int current, int noOfQueens) {
        if (current == noOfQueens) {
            displayQueens(board);
            return;
        }
 
        for (int i = 0; i < noOfQueens; i++) {
            board[current] = i;
            if (noKill(board, current)) {
                placeQueen(board, current + 1, noOfQueens);
            }
        }
    }
 
    private static boolean noKill(int[] board, int currentColumnOfQueen) {
 
        for (int i = 0; i < currentColumnOfQueen; i++) {
            // same row
            if (board[i] == board[currentColumnOfQueen])
                return false;
 
            // Diagonal
            if ((currentColumnOfQueen - i) == Math.abs(board[currentColumnOfQueen] - board[i])) {
                return false;
            }
        }
        return true;
    }
 
    private static void displayQueens(int[] board) {
        System.out.print("\n");
 
        for (int value : board)
            System.out.printf(value + "%3s" ," ");
 
        System.out.print("\n\n");
 
        int n = board.length;
 
        for (int i = 0; i < n; i++) {
            for (int value : board) {
                if (value == i)
                    System.out.print("Q\t");
                else
                    System.out.print("*\t");
            }
            System.out.print("\n");
        }
    }
}

上面唯一的附加代码是显示功能。每次成功放置所有8个皇后后都将调用它。该功能具有带有皇后位置的电路​​板阵列。可以在下面看到输出的示例,此处的皇后数量为8。第一行显示了每一列中皇后的位置(行号)。

运行“ N = 8”的代码的输出快照

结论

使用递归,“ N”皇后问题的代码看起来很干净。递归绝对是解决某些类型问题的重要且有效的技术。但是,必须谨慎使用递归,如果未正确使用该技术,则可能会出现性能问题。

上面的代码肯定更简洁明了,但也涉及更多。我敦促您以较小的'N'值跟踪代码,以更好地理解代码以及解决问题所涉及的递归。

在结束本博客时,我将为您提供N = 4时的轨迹快照。一些要点将使您更轻松地跟踪轨迹-

  • 按照左侧从1开始的箭头上的数字,它们表示步骤编号。
  • 跟踪从以下3个输入开始:–初始化为-1的电路板阵列,指针,电流,最后是要放置在电路板上的皇后总数。
  • 通过红色箭头可以看到一些回溯。蓝色虚线箭头表示正在修剪。
  • 在每个框中,如果板阵列值显示为红色,则表示行或对角线冲突。
  • 绿色圆圈表示已找到女王的位置,此后,计数器的值current(2nd parameter)递增1。

N = 4时的跟踪快照

参考

  • 优良讲座上麻省理工学院开放式通道
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值