看遞歸的時候懂了,看for循環的時候也懂了,看到for循環和遞歸一起就蒙了,看了一個下午才看懂,通過LeetCode里面的幾道題目詳細記錄一下整體思路。
1、題目描述
給定一個無重復數字的整數數組,求其所有的排列方式。
輸入輸出樣例
輸入是一個一維整數數組,輸出是一個二維數組,表示輸入數組的所有排列方式
Input: [1, 2, 3]
Output: [1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]
思路:
怎樣輸出所有的排列方式呢?對於每一個當前位置 i,我們可以將其於之后的任意位置交換,然后繼續處理位置 i+1,直到處理到最后一位。
為了防止我們每此遍歷時都要新建一個子數組儲然后繼續處理位置 i+1,直到處理到最后一位。為了防止我們每此遍歷時都要新建一個子數組儲存位置 i 之前已經交換好的數字,我們可以利用回溯法,只對原數組進行修改,在遞歸完成后再修改回來。
可以將任務划分為多層的子任務,通過for循環實現,所有子任務的處理方式都是通過交換來兩個位置的元素,可以通過遞歸調用實現。
代碼:
#include<vector> #include<iostream> #include<algorithm> using namespace std; //輔助函數 void backtracking(vector<int>& nums, int level, vector<vector<int>>& ans) { if (level == nums.size() - 1) { //結束遞歸調用的條件:當任務不能繼續細分時 ans.push_back(nums); return; } for (int i = level; i < nums.size(); ++i) { //子任務(level, i) swap(nums[i], nums[level]); backtracking(nums, level + 1, ans);//在子任務的基礎上繼續划分子任務(level+1, i),i=level, level+1,...,nums.size()-1 swap(nums[i], nums[level]);//回改節點狀態 } } //主函數 vector<vector<int>> permute(vector<int>& nums) { vector<vector<int>> ans; backtracking(nums, 0, ans); return ans; } int main() { vector<int> nums = { 1,2,3 }; vector<vector<int>> result = permute(nums); //輸出結果 for (int i = 0; i < result.size(); ++i) { for (int j = 0; j < result[0].size(); ++j) { cout << result[i][j]; } cout << endl; } }
詳細的運行情況:
for循環+遞歸調用將任務先划分為了(level=0, i=0)、(level=0, i=1)、(level=0, i=2)三個子任務,然后對
- (level=0, i=0)繼續划分為(level=1, i=1)、(level=1, i=2)兩個子任務,
- (level=0, i=1)繼續划分為(level=1, i=1)、(level=1, i=2)兩個子任務,
- (level=0, i=2)繼續划分為(level=1, i=1)、(level=1, i=2)兩個子任務。
一直到任務不能再繼續划分,滿足if條件(level=2),輸出該子任務的排序結果,然后返回上一層子任務。(level=1, i=1)繼續划分子任務(level=2, i=2),滿足if條件,輸出排序結果:[1,2,3]。
2、題目描述
給定一個整數 n 和一個整數 k,求在 1 到 n 中選取 k 個數字的所有組合方法。
輸入輸出樣例
輸入是兩個正整數 n 和 k,輸出是一個二維數組,表示所有組合方式
Input: n = 4, k = 2
Output: [[2,4], [3,4], [2,3], [1,2], [1,3], [1,4]]
思路:
類似於排列問題,我們也可以進行回溯。排列回溯的是交換的位置,而組合回溯的是否把當前的數字加入結果中。
代碼:
#include<vector> #include<iostream> #include<algorithm> using namespace std; //輔助函數 void backtracking2(vector<vector<int>>& ans, vector<int>& comb, int count, int pos, int n, int k) { if (count == k) { //結束條件 ans.push_back(comb); return; } for (int i = pos; i <= n; ++i) { //for循環設定的子任務count=0賦值為i comb[count] = i; ++count; //遞歸調用對每個子任務執行同樣的操作 backtracking2(ans, comb, count, i + 1, n, k); /* 對於backtracking又有for循環設定的子任務:count=1賦值為i+1 */ --count;//將節點回溯(遞歸調用結束后自動將count-1,然后繼續下一個子任務count=0賦值為i+1) } } //主函數 vector<vector<int>> combine(int n, int k) { vector<vector<int>> ans; vector<int> comb(k, 0); int count = 0; backtracking2(ans, comb, count, 1, n, k); return ans; } int main() { int n = 4, k = 2; vector<vector<int>> result = combine(n, k); for (int i = 0; i < result.size(); ++i) { for (int j = 0; j < result[0].size(); ++j) { cout << result[i][j]; } cout << endl; } }
詳細的運行情況:
首先將任務通過for循環划分為四個子任務(count=0, i=1), (count=0, i=2), (count=0, i=3), (count=0, i=4),對於每個子任務,比如(count=0, i=1),使用for循環繼續划分為子任務(count=1, i=2),(count=1, i=3),(count=1, i=4)。當任務不能繼續划分時(count=2)返回comb,並返回上一個子任務,coun同時減一(回溯)。
可以看出組合問題使用回溯法回溯的是位置,而排序問題回溯的是交換位置。
3、題目描述
給定一個大小為 n 的正方形國際象棋棋盤,求有多少種方式可以放置 n 個皇后並使得她們互不攻擊,即每一行、列、左斜、右斜最多只有一個皇后。下圖為8皇后的一種解法,為了說明方便,在此只討論4皇后問題,可以類推到n=8
輸入輸出樣例
輸入是一個整數 n,輸出是一個二維字符串數組,表示所有的棋盤表示方法。
Input: n = 4
Output:
思路:
類似於在矩陣中尋找字符串,本題也是通過修改狀態矩陣來進行回溯。不同的是,我們需要對每一行、列、左斜、右斜建立訪問數組,來記錄它們是否存在皇后。
本題有一個隱藏的條件,即滿足條件的結果中每一行或列有且僅有一個皇后。這是因為我們一共只有 n 行和 n 列,所以如果我們通過對每一行遍歷來插入皇后。
代碼:
#include<vector> #include<iostream> #include<string> #include<algorithm> using namespace std; //輔助函數 void backtracking4(vector<vector<string>>& ans, vector<string>& board, vector<bool>& column, vector<bool>& ldiag, vector<bool>& rdiag, int row, int n) { if (row == n) { ans.push_back(board); return; } for (int i = 0; i < n; ++i) { //子任務(count=0, i=0) //board[0][0] = 'Q' if (column[i] || ldiag[n - row + i - 1] || rdiag[row + i]) { continue; } board[row][i] = 'Q'; column[i] = ldiag[n - row + i - 1] = rdiag[row + i] = true; //子任務:(count=0, i=0) backtracking4(ans, board, column, ldiag, rdiag, row + 1, n); //回溯:子任務結束后,將節點回調(嘗試下一種情況:board[0][1] = 'Q') board[row][i] = '.'; column[i] = ldiag[n - row + i - 1] = rdiag[row + i] = false; } } //主函數 vector<vector<string>> solveNQueens(int n) { vector<vector<string>> ans; if (n == 0) { return ans; } vector<string> board(n, string(n, '.')); //ldiag和rdiag用於初始化左、右斜線向量,n*n矩陣有2*n-1條左、右斜線,且每條斜線的差(左)、和(右)相同 vector<bool> column(n, false), ldiag(2 * n - 1, false), rdiag(2 * n - 1, false); backtracking4(ans, board, column, ldiag, rdiag, 0, n); return ans; } int main() { vector<vector<string>> result = solveNQueens(4); for (int i = 0; i < result.size(); ++i) { for (int j = 0; j < result[0].size(); ++j) { cout << result[i][j]; cout << endl; } cout << endl; } }
運行步驟:
backtracking4(ans, board, column, ldiag, rdiag, 0, n)將任務划分為(count=0, i=0)、(count=0, i=1)、(count=0, i=2)、(count=0, i=3)四個子任務,分別對應board[0][0]='Q'、board[0][1]='Q'、board[0][2]='Q'、board[0][3]='Q',然后將位置對應的所在的行、左斜線、右斜線修改為true,代表已存在Queen,其他的位置如果在這些行或者斜線上時,不能放置Queen。對於四個子任務,使用遞歸調用,繼續划分子任務,直到不能繼續划分為止,返回上一級子任務並回溯節點狀態。