[Leetcode] Backtracking回溯法(又稱DFS,遞歸)全解

回溯全集

回溯是啥

用登山來比喻回溯,比如從山腳下找一條爬上山頂的路,起初有好幾條道可走,當選擇一條道走到某處時,又有幾條岔道可供選擇,只能選擇其中一條道往前走,若能這樣子順利爬上山頂則罷了,不然走到一條絕路上時或者這條路上有一坨屎,咱們只好返回到最近的一個路口,從新選擇另外一條沒走過的道往前走。若是該路口的全部路都走不通,只得從該路口繼續回返。照此規則走下去,要麼找到一條到達山頂的路,要麼最終試過全部可能的道,沒法到達山頂。
回溯本質上是一種窮舉。html

還有一些愛混淆的概念:遞歸,回溯,DFS。這些都是一個事兒的不一樣方面。如下以回溯統稱,由於這個詞聽上去很文雅。node

識別回溯

判斷回溯很簡單,拿到一個問題,你感受若是不窮舉一下就無法知道答案,那就能夠開始回溯了。
通常回溯的問題有三種:express

  1. Find a path to success 有沒有解
  2. Find all paths to success 求全部解數組

    • 求全部解的個數
    • 求全部解的具體信息
  3. Find the best path to success 求最優解

理解回溯:給一堆選擇, 必須從裏面選一個. 選完以後我又有了新的一組選擇. This procedure is repeated over and over until you reach a final state. If you made a good sequence of choices, your final state is a goal state; if you didn't, it isn't.函數

回溯能夠抽象爲一棵樹,咱們的目標能夠是找這個樹有沒有good leaf,也能夠是問有多少個good leaf,也能夠是找這些good leaf都在哪,也能夠問哪一個good leaf最好,分別對應上面所說回溯的問題分類。
good leaf都在leaf上。good leaf是咱們的goal state,leaf node是final state,是解空間的邊界。優化

對於第一類問題(問有沒有解),基本都是長着個樣子的,理解了它,其餘類別迎刃而解:code

boolean solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, return true
        else return false
    } else {
        for each child c of n {
            if solve(c) succeeds, return true
        }
        return false
    }
}

請讀如下這段話以加深理解:
Notice that the algorithm is expressed as a boolean function. This is essential to understanding the algorithm. If solve(n) is true, that means node n is part of a solution--that is, node n is one of the nodes on a path from the root to some goal node. We say that n is solvable. If solve(n) is false, then there is no path that includes n to any goal node.htm

還不懂的話請通讀全文吧:Backtracking - David Matuszek遞歸

關於回溯的三種問題,模板略有不一樣,
第一種,返回值是true/false。
第二種,求個數,設全局counter,返回值是void;求全部解信息,設result,返回值void。
第三種,設個全局變量best,返回值是void。ci

第一種:

boolean solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, return true
        else return false
    } else {
        for each child c of n {
            if solve(c) succeeds, return true
        }
        return false
    }
}

第二種:

void solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, count++, return;
        else return
    } else {
        for each child c of n {
            solve(c)
        }
    }
}

第三種:

void solve(Node n) {
    if n is a leaf node {
        if the leaf is a goal node, update best result, return;
        else return
    } else {
        for each child c of n {
            solve(c)
        }
    }
}

題目

八皇后 N-Queens

問題

1.給個n,問有沒有解;
2.給個n,有幾種解;(Leetcode N-Queens II)
3.給個n,給出全部解;(Leetcode N-Queens I)

解答

1.有沒有解

怎麼作:一行一行的放queen,每行嘗試n個可能,有一個可達,返回true;都不可達,返回false.

邊界條件leaf:放完第n行 或者 該放第n+1行(出界,返回)

目標條件goal:n行放滿且isValid,即目標必定在leaf上

helper函數:
boolean solve(int i, int[][] matrix)
在進來的一瞬間,知足property:第i行尚未被放置,前i-1行放置完畢且valid
solve要在給定的matrix上試圖給第i行每一個位置放queen。

public static boolean solve1(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            return true;
        return false;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                if (solve1(i + 1, matrix, n)) 
                    return true;
            }
            matrix.remove(matrix.size() - 1);
        }
        return false;
    }
}
2.求解的個數

怎麼作:一行一行的放queen,每行嘗試n個可能。這回由於要找全部,返回值就沒有了意義,用void便可。在搜索時,若是有一個可達,仍要繼續嘗試;每一個子選項都試完了,返回.

邊界條件leaf:放完第n行 或者 該放第n+1行(出界,返回)

目標條件goal:n行放滿且isValid,即目標必定在leaf上

helper函數:
void solve(int i, int[][] matrix)
在進來的一瞬間,知足property:第i行尚未被放置,前i-1行放置完畢且valid
solve要在給定的matrix上試圖給第i行每一個位置放queen。
這裏爲了記錄解的個數,設置一個全局變量(static)int是比較efficient的作法。

public static void solve2(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            count++;
        return;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                solve2(i + 1, matrix, n); 
            }
            matrix.remove(matrix.size() - 1);
        }
    }
}
3.求全部解的具體信息

怎麼作:一行一行的放queen,每行嘗試n個可能。返回值一樣用void便可。在搜索時,若是有一個可達,仍要繼續嘗試;每一個子選項都試完了,返回.

邊界條件leaf:放完第n行 或者 該放第n+1行(出界,返回)

目標條件goal:n行放滿且isValid,即目標必定在leaf上

helper函數:
void solve(int i, int[][] matrix)
在進來的一瞬間,知足property:第i行尚未被放置,前i-1行放置完畢且valid
solve要在給定的matrix上試圖給第i行每一個位置放queen。
這裏爲了記錄解的具體狀況,設置一個全局變量(static)集合是比較efficient的作法。
固然也能夠把結果集合做爲參數傳來傳去。

public static void solve3(int i, List<Integer> matrix, int n) {
    if (i == n) {
        if (isValid(matrix))
            result.add(new ArrayList<Integer>(matrix));
        return;
    } else {
        for (int j = 0; j < n; j++) {
            matrix.add(j);
            if (isValid(matrix)) {    //剪枝
                solve3(i + 1, matrix, n); 
            }
            matrix.remove(matrix.size() - 1);
        }
    }
}

優化

上面的例子用了省空間的方法。
因爲每行只能放一個,一共n行的話,用一個大小爲n的數組,數組的第i個元素表示第i行放在了第幾列上。

Utility(給一個list判斷他的最後一行是否和前面衝突):

public static boolean isValid(List<Integer> list){
    int row = list.size() - 1;
    int col = list.get(row);
    for (int i = 0; i <= row - 1; i++) {
        int row1 = i;
        int col1 = list.get(i);
        if (col == col1)
            return false;
        if (row1 - row == col1 - col)
            return false;
        if (row1 - row == col - col1)
            return false;
    }
    return true;
    
}
相關文章
相關標籤/搜索