1.分治
分治就是分而治之,即把一個問題分解成很多個子問題,且這些問題與原問題結構相似,然後遞迴解決子問題,最後合併子問題的結果,得到原來問題的結果。
分治解題三個步驟:
分解:將問題分解成與原問題結構相似的子問題
解決:遞迴求解各個子問題,當子問題足夠小,直接返回結果(問題分解到足夠小,分解終止條件)
合併:將子問題的結果合併,得到原問題的結果
分治演算法也是一種遞迴,之前有寫到過遞迴的模板,這裡也貼一下分治演算法的模板。
接下來看個問題,深入瞭解一下分治演算法,並套用一下模板。
Pow(x,n)
首先按照分治解題的三個步驟分析一下這個問題:
分解:x的n次冪可以分解為子問題:x^n/2 ……
解決:問題還沒有分解最小時,就進行遞迴,當問題分解到足夠小時,如n=0時,結果為1;n=1時,結果為x;n=-1時,結果為 1/x。
合併:將得到的結果合併
當n%2==0時,x^n = (x^n/2)*(x^n/2)
當n%2==1時,x^n = x* (x^n/2)*(x^n/2)
下面來看一下程式碼:
public double myPow(double x, int n) {
//當結果足夠小時,直接返回結果
if (n == 0) { return 1; }
if (n == 1) { return x; }
if (n == -1) { return 1 / x; }
//解決子問題,遞迴呼叫
double half = myPow(x, n / 2);
//這裡取模進行呼叫(考慮到n可能為負數)
double rest = myPow(x, n % 2);
//合併,返回結果
return rest * half * half;
}
這就是一個典型分治演算法的題目,先對原問題進行分解,在求解子問題(遞迴求解,當問題足夠小的時候,返回結果),然後對子問題的結果進行合併。
2.回溯
回溯利用了試錯的思想,它嘗試分步的去解決問題。當分步計算的時候,發現答案錯誤或者無效時,它會取消上一步,甚至上幾步的操作,然後再通過其他的分步解答去找到正確答案。
八皇后
來看看經典的八皇后問題
來說說解題思路,把這個題目放在這篇文章裡,當然就是用遞迴回溯法解決了...好了,接下來就該手撕八皇后了~
首先題目中要求每個皇后都不同行,不同列,也不在對角線上(其實就是題目給我們的過濾條件),不同行,不同列好理解,也好解決,我們就來看看不在對角線上(丿斜對角線、捺斜對角線)
丿對角線有個特點:行下標和列下標之和相等。(圖來自LeetCode)
捺斜對角線有個特點:行下標和列下標之差相等。(圖來自LeetCode)
好了,題目給我們的過濾條件就羅列出來了,我們需要去窮舉皇后出現的位置(不能被攻擊到),並記錄當前皇后位置可以攻擊到的位置。
很多分析都寫到程式碼的註釋裡了,直接看程式碼吧~
/**
* N皇后問題
* @param n n
* @return result
*/
public List<List<String>> solveNQueens(int n) {
//存放結果集
List<List<String>> solutions = new ArrayList<List<String>>();
//記錄皇后的位置
int[] queens = new int[n];
//賦予預設值
Arrays.fill(queens, -1);
//記錄皇后攻擊的列
Set<Integer> columns = new HashSet<Integer>();
//記錄皇后攻擊的丿斜線
Set<Integer> pie = new HashSet<Integer>();
//記錄皇后攻擊的捺斜線
Set<Integer> na = new HashSet<Integer>();
//遞迴呼叫
backtrack(solutions, queens, n, 0, columns, pie, na);
//返回結果
return solutions;
}
/**
* 遞迴函式,窮舉皇后的位置
* @param solutions 結果集
* @param queens 皇后可以放置的列(陣列記錄)
* @param n n個皇后
* @param row 當前遞迴是第 row 行
* @param columns 已有的皇后能攻擊到的列
* @param pie 已有的皇后能攻擊到的丿斜線
* @param na 已有的皇后能攻擊到的捺斜線
*/
public void backtrack(List<List<String>> solutions, int[] queens, int n, int row, Set<Integer> columns, Set<Integer> pie, Set<Integer> na) {
if (row == n) {
//當行 等於 n 時,說明已經窮舉完,記錄結果集
List<String> board = generateBoard(queens, n);
solutions.add(board);
} else {
//遍歷當前行的所有列
for (int i = 0; i < n; i++) {
//被攻擊的列包含了當前列,跳過當前列,不進行後面的邏輯
if (columns.contains(i)) {
continue;
}
//被攻擊的丿斜線包含了當前位置,跳過當前位置
if (pie.contains(row + i)) {
continue;
}
//被攻擊的捺斜線包含了當前位置,跳過當前位置
if (na.contains(row - i)) {
continue;
}
//記錄當前皇后存放的列
queens[row] = i;
//記錄當前皇后攻擊到的列
columns.add(i);
//記錄當前皇后攻擊到的丿斜線
pie.add(row+i);
//記錄當前皇后攻擊到的捺斜線
na.add(row-i);
//遞迴呼叫,窮舉下一行的皇后位置
backtrack(solutions, queens, n, row + 1, columns, pie, na);
/**
* 經過上面的遞迴,自頂向下遞迴完成時就已經窮舉出了一種皇后所有可能出現的位置
* 然後這裡需要把之前修改的資料進行清理,窮舉另一種皇后所有可能出現的位置,這裡是自底向上清理資料(即回溯)
*/
//清理皇后存放的列
queens[row] = -1;
//清理皇后攻擊的列
columns.remove(i);
//清理皇后攻擊的丿斜線
pie.remove(row+i);
//清理皇后攻擊的捺斜線
na.remove(row-i);
}
}
}
/**
*
* @param queens 皇后位置
* @param n n皇后
* @return 所有皇后的位置(用.和Q表示)
*/
public List<String> generateBoard(int[] queens, int n) {
List<String> board = new ArrayList<String>();
for (int i = 0; i < n; i++) {
char[] row = new char[n];
Arrays.fill(row, '.');
row[queens[i]] = 'Q';
board.add(new String(row));
}
return board;
}
看完程式碼你學廢了嗎?八皇后問題這裡主要是想來說明一下遞迴中的回溯,當我們列舉一種可能,到最後發現不正確時,需要清理之前的資料,即取消上一步,甚至上好多步的操作。遞迴函式呼叫(自頂向下)完成後去清理資料(自底向上),多理解一下這種歸去來兮的感覺。
總結
分治:遞迴呼叫後,去合併遞迴得到的結果。
回溯:遞迴呼叫後,去取消上一步,上好多步的操作。
分治和回溯本質上都是遞迴的一種,具體的問題,我們要具體分析,是需要分解後合併結果,還是不斷窮舉試錯,然後回溯。
歡迎關注微信公眾號「山主」,公眾號文章首發,解鎖更多幹貨~