經過n皇后問題搞明白回溯算法

前言

很久沒聊算法啦!此次咱們來聊聊n皇后問題。n 皇后問題,研究的是如何將 n 個皇后放置在 n×n 的棋盤上,而且使皇后彼此之間不能相互攻擊。好多同窗對這樣的問題都比較慌張,以爲規則多燒腦抗拒,祈禱面試中不要遇到,別急,咱們今天就來嘗試把這其中的邏輯給說道說道。面試

一個皇后能夠向水平、垂直以及向斜對角方向移動,若是一個皇后出如今另外一個皇后的同一行,同一列或者斜對角,那它就能夠被這個皇后攻擊。算法

那咱們直覺裏的一個比較粗暴的辦法,就是列舉出在棋盤上全部的狀況,而後咱們判斷每一個組合是否是符合咱們的條件。可是實際上咱們不須要嘗試全部的組合,咱們知道當咱們在某一列上放置了一個皇后以後,其它的皇后就不能放在這一列了,在它的同一個水平線上跟四個斜對角也放不了。這樣咱們能夠最先發現「此路不通」。數組

當咱們發現一條路行不通時,咱們趕忙回到前面一步,而後嘗試另一個不一樣的選擇,咱們稱之爲回溯安全

這個高大上的回溯是什麼

針對n皇后問題咱們把這個思路再展開一下:markdown

  1. 把一個皇后放在第一行的第一列
  2. 而後咱們在第二行找到一個位置,在這兒第二個皇后不會被第一行的皇后攻擊到
  3. 若是咱們找不到這樣的一個位置, 那咱們就回退到前一行,嘗試把這個皇后放到那一行的下一列
  4. 重複這個步驟,直到咱們在最後一行也找到一個合適的位置放置最後一個皇后,那這時咱們就找到了一種解決方案
  5. 找到一個解決方案以後,咱們會繼續回退到前一行,去嘗試找到下一個解決方案

是否是還以爲有點抽象?函數

那咱們拿其中一個場景來具體說說:優化

棋盤,X表明一個皇后

  1. 咱們從x=0,y=0開始,第一個皇后a放在這兒是安全的,
  2. 而後第二行的皇后b爲了不被攻擊,只能從第三列開始放
  3. 那此時第三行咱們發現就無法兒放皇后了,由於無論放哪兒都會被皇后a或者皇后b攻擊
  4. 那咱們只能回溯到第二行,繼續日後找一個合適的列來放置皇后b
  5. 當第二行找到最後一列也不知足的條件時,咱們只能回溯到第一行,繼續日後找能夠放置皇后a的列,重複這個過程

走兩步?

如今是否是以爲眼睛會了?🤔,接下來咱們可讓手來試試了。spa

首先咱們須要一個方法來判斷某一個位置能不能放皇后,這樣若是一個位置會被棋盤上已有的皇后攻擊的話,咱們能夠直接跳過這個位置:code

// 這個方法用來判斷咱們接下來要放置的皇后在不在某個已經放置的皇后的水平方向、垂直方向或者斜對角,
    // 若是都不,那咱們找到了一個合適的位置來放一個新皇后了
    static boolean isValidPosition(int proposedRow, int proposedCol, List<Integer> solution) {
        //對當前棋盤上的全部皇后,咱們都要作判斷
        for (int oldRow = 0; oldRow < proposedRow; ++oldRow) {
            int oldCol = solution.get(oldRow);
            int diagonalOffset = proposedRow - oldRow;
            if (oldCol == proposedCol ||
                    oldCol == proposedCol - diagonalOffset ||
                    oldCol == proposedCol + diagonalOffset) {
                return false;
            }
        }
        return true;
    }
複製代碼

有了這個方法以後,咱們就須要實現逐行搜索以及在全部路不通時回到前一行的搜索裏面來,繼續尋找其它可能性。每一行的搜索方式都一致,因此這邊適合使用遞歸來實現咱們的邏輯:orm

static void solveNQueensRec(int n, List<Integer> solution, int row, List<List<Integer>> results) {
        // 最後一行也找到了一個合適的位置,咱們成功找到了一種解決方案
        if (row == n) {
            results.add(new ArrayList<Integer>(solution));
            return;
        }

        // 從每一行的第一列開始嘗試
        for (int i = 0; i < n; ++i) {
            // 對於走到最後一列還沒都沒有找到合適的點的狀況, 當前遞歸結束,調用棧回到上一層的遞歸流程,會回去執行前面一行裏剩餘的狀況
            if (isValidPosition(row, i, solution)) {
                solution.set(row, i);
                solveNQueensRec(n, solution, row + 1, results);
            }
        }
    }
複製代碼

當有條路走不通時,調用棧裏當前遞歸就執行結束了,它會回到上一個遞歸的調用邏輯裏,也就實現了咱們的回溯。咱們的目的很簡單,這一行走到最後沒路走了,就繼續回到前一行繼續日後走,直到全部的路都嘗試過。

最後執行一下咱們的遞歸函數就好啦:

static int solveNQueens(int n, List<List<Integer>> results) {
        List<Integer> solution = new ArrayList<Integer>(n);
        for (int i = 0; i < n; ++i) {
            solution.add(-1);
        }
        solveNQueensRec(n, solution, 0, results);
        return results.size();
    }
複製代碼

這邊至關於從N×N的數組裏,選出N個數,時間複雜度是O(n^n),而空間複雜度是O(n!)

繼續發散

上面咱們搜索的過程當中,一行一行上升去尋找合適的位置,而後在某個條件下又回到前一行,有點像棧的入棧出棧操做,其實咱們也是能夠用棧來實現整個回溯過程的。咱們在某一行裏找到一個合適的位置時就把它的列push到棧中,回溯到前一行時再把它pop出來。

// 這邊棧裏咱們只存放列的值
    static int solveNQueens(int n, List<List<Integer>> results) {

        List<Integer> solution = new ArrayList<Integer>(n);
        Stack<Integer> solStack = new Stack<Integer>();

        for (int i = 0; i < n; ++i) {
            solution.add(-1);
        }

        int row = 0;
        int col = 0;

        while(row < n){
            while(col < n){
                // 當前能夠放置皇后,繼續下一行的尋找
                if(isValidPosition(row, col, solution)){
                    solStack.push(col);
                    solution.set(row, col);
                    row++;
                    col = 0;
                    break;
                }
                // 當前位置不行,嘗試在下一列放置皇后
                col++;
            }

            // 找到了當前行的最後一列
            if(col == n){
                // 說明前面一行還沒到最後一列,執行pop操做回到前一行尋找過程
                if(!solStack.empty()){
                    col = solStack.peek() + 1;
                    solStack.pop();
                    row--;
                }
                else{
                    // 只有第一行也走到了最後一列,而且全部路徑都嘗試過了,此時因爲上面if裏的邏輯棧空了,說明咱們的尋找過程該結束了
                    break;
                }
            }

            if(row == n){
                // 咱們找到一種符合條件的擺放位置
                results.add(new ArrayList<Integer>(solution));

                // 回溯到前一行
                row--;
                col = solStack.peek() + 1;
                solStack.pop();
            }
        }
        return results.size();
    }
複製代碼

時間複雜度是O(n^n),空間複雜度是O(n!)。這邊整個邏輯仍是比較直接的,咱們依舊須要isValidPosition這個輔助方法來判斷某個位置能不能放置皇后,而後對每一個位置逐一判斷,用棧來配合尋找過程當中的回溯操做,核心思想仍是不變的。

總結

咱們兩種辦法裏都把全部的符合規則的擺放記錄下來了,若是咱們只須要最後求得有多少種可能性,那咱們其實能夠把數組換成一個變量來計數,這樣咱們的空間複雜度能夠優化成O(n)

好啦,相信你們這會兒對回溯算法有了一個感性的認識,也能明白回溯只是咱們面對問題時常規的思路,並非什麼高大上的概念,咱們不用去畏懼它~

相關文章
相關標籤/搜索