資料結構和演算法——遞迴-八皇后問題(回溯演算法)

天然呆dull發表於2021-08-29

看完 資料結構與演算法——遞迴-迷宮問題 後,我們對遞迴和回溯演算法有了一個基本的認識,本篇將講解 一個著名的問題:八皇后問題,能讓我們對遞迴和回溯有一個更深刻的認識。

八皇后問題,是一個古老而著名的問題,是 回溯演算法 的典型案例。

該問題是國際西洋棋棋手馬克斯·貝瑟爾於 1848 年提出:在 8×8 格的國際象棋上擺放八個皇后,使其不能互相攻擊,即:任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。

高斯認為有 76 種方案。1854年在柏林的象棋雜誌上不同的作者發表了 40 種不同的解,後來有人用圖論的方法解出 92 種結果。計算機發明後,有多種計算機語言可以解決此問題。

可以去百度搜尋下這個小遊戲,自己玩幾下感受下,還是很難的

八皇后思路分析

用遞迴回溯演算法找出八皇后有多少種擺法,其實就是暴力窮舉,思路如下:

  1. 第一個皇后先放第一行第一列

  2. 第二個皇后放在第二行第一列,然後判斷是否符合規則,如果不符合規則,則繼續放在第 2 列,依次的測試下去,直到符合規則後,放第三個皇后。

  3. 繼續第 3 個皇后,直到 8 個皇后都放到了棋盤上,並且沒有違反規則,就算是一個解答

  4. 當得到一個正確解時,記錄下棋盤上的皇后的所有座標(在每一次放皇后符合規則後都會記錄本座標),方便我們列印檢視結果,在棧回退到上一個棧時,就會開始回溯,即將第一個皇后放在第一列的所有正確解全部拿到

  5. 然後回頭繼續第一個皇后放第二列,後面繼續迴圈執行前面 4 步,以此類推,當求完第一行的皇后在每個列上的所有答案,就是全部解了(一共92種)

    如果這裡讀不明白,就繼續往下看,結合後面的細節分析和程式碼註釋 看懂程式碼,就好理解了。

理論上應該建立一個二維陣列來表示棋盤,但是實際上可以通過演算法,用一個一維陣列即可解決問題. arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3},這個是儲存結果,陣列下標表示第幾行,對應的值則為在那一列上擺放著

/**
 * 八皇后問題:
 * 
 *     規則:在 8×8 格的國際象棋上擺放八個皇后,使其不能互相攻擊,
 *          即:任意 兩個皇后 都不能處於同一行、同一列或同一斜線上,問有多少種擺法。
 * 
 */
public class Queue8 {
    // 共有多少個皇后
    int max = 8;
    /**
     * 存放皇后位置的結果
     * 
     * 下標:表示棋盤中的某一行
     * 對應的值:表示在這一行上,該皇后擺放在哪一列
     * 比如:array[0] = 1,表示在第 1 行的第 2 列上擺放了一個皇后
     *
     * 由於規則,一行只能有一個皇后,所以可以使用一維陣列來代替二維陣列的棋盤結果
     * 
     */
    int[] array = new int[max];
    int count = 0; // 統計有多少個結果

    public static void main(String[] args) {
        Queue8 queue8 = new Queue8();
        queue8.check(0); // 從第 1 行開始放置
    }

    /**
     * 放置第 n 個(行)皇后
     *特別注意: check 是 每一次遞迴時,進入到check中都有  for(int i = 0; i < max; i++),因此會有回溯
     * @param n
     */
    private void check(int n) {
        // n = 8,那麼表示放第 9 個皇后,8 個皇后已經放完了(n 從0開始計數的)
        // 表示找到了一個正確的結果,列印這個結果,並返回
        if (n == max) {
            count++;
            print();
            return;
        }

        // 開始暴力對比,從該行的第一列開始嘗試放置皇后,直到與前面所放置的不衝突
        for (int i = 0; i < max; i++) {
            // 在該行的第 i 列上放置一個皇后
            array[n] = i;
            // 檢測與已經放置的是否衝突
            if (judge(n)) {
                // 如果不衝突,則表示該行的皇后放置沒有問題
                // 開始進入下一行的皇后放置
                check(n + 1);
            }
            // 如果衝突,這裡什麼也不做
            // 因為是從第一列開始放置,如果衝突,則嘗試放置到第 2 列上,直到放置成功
        }
    }

    /**
     * 按遊戲規則要求,判定要放置的這一個皇后,和前面已經擺放的位置是否衝突
     *
     * @param n 第 n 個皇后
     * @return
     */
    private boolean judge(int n) {
        for (int i = 0; i < n; i++) {
            if (
                    /*
                     如果他們的擺放位置一樣,說明是在同一列
                     注:圖是從下往上看
                      x .......
                      x .......
                     */
                    array[i] == array[n]
                            /*
                              檢測是否是同一斜列
                              array[下標] = 值
                              下標: 代表的是第幾行
                              值:代表的是第幾列
                              
                              注:圖是從下往上看
                              . x . . . . . . n = 1,value = 1
                              x . . . . . . . i = 0,value = 0
                              Math.abs(n - i) = 1
                              Math.abs(array[n] - array[i]) = 1

                              . . x . . . . . n = 1,value = 2
                              . x . . . . . . i = 0,value = 1
                              Math.abs(n - i) = 1
                              Math.abs(array[n] - array[i]) = 1
                              
                              注:這裡運用了角的斜率問題,如果斜率為1,則角就是45°,所以兩點在一斜列上。
                              想不明白可以畫圖看看,如果兩點在一斜列上的話,就會組成一個正方形。很巧妙
                             */
                            || Math.abs(n - i) == Math.abs(array[n] - array[i])
            ) {
                return false;
            }
        }
        return true;
    }

    /**
     * 列印皇后的位置
     */
    private void print() {
        System.out.printf("第 %02d 個結果 :", count);
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
        }
        System.out.println();
    }

    /**
     * 下面是對於判定是否是同行,同列、同一斜列的分步驟測試,比較好理解
     */
    @Test
    public void judgeTest() {
        //注:圖是從下往上看
        /*
         * . . . . . . . .
         * x . . . . . . .
         */
        Queue8 queue8 = new Queue8();
        queue8.array[0] = 0;

        //======== 放置第 1 個皇后
        // 判斷是否是同一列
        /*
         * x . . . . . . .  // 計劃放到這裡
         * x . . . . . . .
         */
        queue8.array[1] = 0; // 從第一列開始放置
        System.out.println("同一列,是否通過:" + queue8.judge(1));

        /*
         * . x . . . . . .  // 計劃放到這裡
         * x . . . . . . .
         */
        queue8.array[1] = 1;
        // 第一列不行,放置到第 2 列上
        System.out.println("同一斜列,是否通過:" + queue8.judge(1));

        /*
         * . . x . . . . .  // 計劃放到這裡
         * x . . . . . . .
         */
        queue8.array[1] = 2;
        // 第 2 列不行,放置到第 3 列上,這個肯定是可以的
        System.out.println("同一列或同一斜列,是否通過:" + queue8.judge(1));

        //======== 放置第 3 個皇后
        /*
         * x . . . . . . .  // 計劃放到這裡
         * . . x . . . . .
         * x . . . . . . .
         */
        queue8.array[2] = 0;
        // 與第一行的在同一列上了
        System.out.println("同一列,是否通過:" + queue8.judge(2));

        /*
         * . x . . . . . .  // 計劃放到這裡
         * . . x . . . . .
         * x . . . . . . .
         */
        queue8.array[2] = 1;
        // 第一列不行,放置到第 2 列
        // 這裡與第 2 行的同一斜列了,也是不行的
        System.out.println("同一斜列,是否通過:" + queue8.judge(2));
    }
}

輸出結果如下

第 01 個結果 :0 4 7 5 2 6 1 3 
第 02 個結果 :0 5 7 2 6 3 1 4 
第 03 個結果 :0 6 3 5 7 1 4 2 
第 04 個結果 :0 6 4 7 1 3 5 2 
第 05 個結果 :1 3 5 7 2 0 6 4 
第 06 個結果 :1 4 6 0 2 7 5 3 
第 07 個結果 :1 4 6 3 0 7 5 2 
第 08 個結果 :1 5 0 6 3 7 2 4 
第 09 個結果 :1 5 7 2 0 3 6 4 
第 10 個結果 :1 6 2 5 7 4 0 3 
第 11 個結果 :1 6 4 7 0 3 5 2 
第 12 個結果 :1 7 5 0 2 4 6 3 
第 13 個結果 :2 0 6 4 7 1 3 5 
第 14 個結果 :2 4 1 7 0 6 3 5 
第 15 個結果 :2 4 1 7 5 3 6 0 
第 16 個結果 :2 4 6 0 3 1 7 5 
第 17 個結果 :2 4 7 3 0 6 1 5 
第 18 個結果 :2 5 1 4 7 0 6 3 
第 19 個結果 :2 5 1 6 0 3 7 4 
第 20 個結果 :2 5 1 6 4 0 7 3 
第 21 個結果 :2 5 3 0 7 4 6 1 
第 22 個結果 :2 5 3 1 7 4 6 0 
第 23 個結果 :2 5 7 0 3 6 4 1 
第 24 個結果 :2 5 7 0 4 6 1 3 
第 25 個結果 :2 5 7 1 3 0 6 4 
第 26 個結果 :2 6 1 7 4 0 3 5 
第 27 個結果 :2 6 1 7 5 3 0 4 
第 28 個結果 :2 7 3 6 0 5 1 4 
第 29 個結果 :3 0 4 7 1 6 2 5 
第 30 個結果 :3 0 4 7 5 2 6 1 
第 31 個結果 :3 1 4 7 5 0 2 6 
第 32 個結果 :3 1 6 2 5 7 0 4 
第 33 個結果 :3 1 6 2 5 7 4 0 
第 34 個結果 :3 1 6 4 0 7 5 2 
第 35 個結果 :3 1 7 4 6 0 2 5 
第 36 個結果 :3 1 7 5 0 2 4 6 
第 37 個結果 :3 5 0 4 1 7 2 6 
第 38 個結果 :3 5 7 1 6 0 2 4 
第 39 個結果 :3 5 7 2 0 6 4 1 
第 40 個結果 :3 6 0 7 4 1 5 2 
第 41 個結果 :3 6 2 7 1 4 0 5 
第 42 個結果 :3 6 4 1 5 0 2 7 
第 43 個結果 :3 6 4 2 0 5 7 1 
第 44 個結果 :3 7 0 2 5 1 6 4 
第 45 個結果 :3 7 0 4 6 1 5 2 
第 46 個結果 :3 7 4 2 0 6 1 5 
第 47 個結果 :4 0 3 5 7 1 6 2 
第 48 個結果 :4 0 7 3 1 6 2 5 
第 49 個結果 :4 0 7 5 2 6 1 3 
第 50 個結果 :4 1 3 5 7 2 0 6 
第 51 個結果 :4 1 3 6 2 7 5 0 
第 52 個結果 :4 1 5 0 6 3 7 2 
第 53 個結果 :4 1 7 0 3 6 2 5 
第 54 個結果 :4 2 0 5 7 1 3 6 
第 55 個結果 :4 2 0 6 1 7 5 3 
第 56 個結果 :4 2 7 3 6 0 5 1 
第 57 個結果 :4 6 0 2 7 5 3 1 
第 58 個結果 :4 6 0 3 1 7 5 2 
第 59 個結果 :4 6 1 3 7 0 2 5 
第 60 個結果 :4 6 1 5 2 0 3 7 
第 61 個結果 :4 6 1 5 2 0 7 3 
第 62 個結果 :4 6 3 0 2 7 5 1 
第 63 個結果 :4 7 3 0 2 5 1 6 
第 64 個結果 :4 7 3 0 6 1 5 2 
第 65 個結果 :5 0 4 1 7 2 6 3 
第 66 個結果 :5 1 6 0 2 4 7 3 
第 67 個結果 :5 1 6 0 3 7 4 2 
第 68 個結果 :5 2 0 6 4 7 1 3 
第 69 個結果 :5 2 0 7 3 1 6 4 
第 70 個結果 :5 2 0 7 4 1 3 6 
第 71 個結果 :5 2 4 6 0 3 1 7 
第 72 個結果 :5 2 4 7 0 3 1 6 
第 73 個結果 :5 2 6 1 3 7 0 4 
第 74 個結果 :5 2 6 1 7 4 0 3 
第 75 個結果 :5 2 6 3 0 7 1 4 
第 76 個結果 :5 3 0 4 7 1 6 2 
第 77 個結果 :5 3 1 7 4 6 0 2 
第 78 個結果 :5 3 6 0 2 4 1 7 
第 79 個結果 :5 3 6 0 7 1 4 2 
第 80 個結果 :5 7 1 3 0 6 4 2 
第 81 個結果 :6 0 2 7 5 3 1 4 
第 82 個結果 :6 1 3 0 7 4 2 5 
第 83 個結果 :6 1 5 2 0 3 7 4 
第 84 個結果 :6 2 0 5 7 4 1 3 
第 85 個結果 :6 2 7 1 4 0 5 3 
第 86 個結果 :6 3 1 4 7 0 2 5 
第 87 個結果 :6 3 1 7 5 0 2 4 
第 88 個結果 :6 4 2 0 5 7 1 3 
第 89 個結果 :7 1 3 0 6 4 2 5 
第 90 個結果 :7 1 4 2 0 6 3 5 
第 91 個結果 :7 2 0 5 1 4 6 3 
第 92 個結果 :7 3 0 2 5 1 6 4 

可以在百度找到 8皇后 這個遊戲,隨便選一兩個資料測試正確性,如果你想全部測試我沒意見,結果是對的,你開心就好。

細節分析

一定要理解它這個 回溯 機制,比如拿其中這兩個來分析(依據上面程式碼)

第 06 個結果 :1 4 6 0 2 7 5 3 
第 07 個結果 :1 4 6 3 0 7 5 2 
  1. 從第 1 行,到最後的第 8 行,這時 n+1 進入到第 9 行,發現已經放完 8 個皇后了,拿到一個完整的結果,
  2. 就返回到第 8 行的那個check()方法中的check(n+1)哪裡,因為第 8 行放置結果是 3 是一個正確解,那麼回到這裡來的時候,就會嘗試剩下沒有測試的列,比如第 4 列
  3. 從結果來看,肯定不滿足,就一直會嘗試直到,第 8 行的 3 變成 7(從0開始計數),這個 check 方法嘗試完了,都沒有出一個結果,就自動退出這個方法
  4. 回到了第 7 行的第 5 列這個結果上,又繼續嘗試,像第 2 步和第 3 步那樣
  5. 從結果上來看,直到回到了第 4 行上,將 0 變成了 3,才發現這個位置與之前的不衝突,然後往下,進入到第 5 個皇后的放置上,重複遞迴操作
  6. 依次類推

其實這裡遞迴的原理就是窮舉法一樣的思想,挨個的嘗試,直到把所有的可能都嘗試完。就像手機密碼忘了,你一個一個去嘗試,暴力破解。

*有一個 細節需要知道,這裡的 不同行、不同列、不同斜列,不要求非連續的,也就是說,即使不是連續的斜列也算

x . . . . . . .
. . . . . . . .
. . x . . . . .
這樣不連續的斜列,也算是在一條斜線上。一定要明白這個規則,才能使用那個判定斜列的演算法

小結

  • 重點 1 :判定是否在棋盤是符合規則:是否是同一列、同一行(同行實際上是不存在的,所以不用做判斷)、同一斜列

    這裡使用一維陣列,來儲存二維陣列中的點的位置,使用如下的演算法,來檢驗這個規則,這裡設定的很巧妙

                         /*
                         如果他們的擺放位置一樣,說明是在同一列
                         注:圖是從下往上看
                          x .......
                          x .......
                         */
                        array[i] == array[n]
                                /*
                                  檢測是否是同一斜列
                                  array[下標] = 值
                                  下標: 代表的是第幾行
                                  值:代表的是第幾列
                                  
                                  注:圖是從下往上看
                                  . x . . . . . . n = 1,value = 1
                                  x . . . . . . . i = 0,value = 0
                                  Math.abs(n - i) = 1
                                  Math.abs(array[n] - array[i]) = 1
    
                                  . . x . . . . . n = 1,value = 2
                                  . x . . . . . . i = 0,value = 1
                                  Math.abs(n - i) = 1
                                  Math.abs(array[n] - array[i]) = 1
                                  
                                  注:這裡運用了角的斜率問題,如果斜率為1,則角就是45°,所以兩點在一斜列上。
                                  想不明白可以畫圖看看,如果兩點在一斜列上的話,就會組成一個正方形。很巧妙
                                 */
                                || Math.abs(n - i) == Math.abs(array[n] - array[i])
    
  • 重點 2:遞迴的回溯流程,一定要明白是怎麼回溯的

相關文章