回溯演算法
一、什麼是回溯演算法
回溯演算法實際上一個類似列舉的搜尋嘗試過程,主要是在搜尋嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。許多複雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。
回溯演算法實際上一個類似列舉的深度優先搜尋嘗試過程,主要是在搜尋嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回(也就是遞迴返回),嘗試別的路徑。
二、回溯演算法思想
回溯法一般都用在要給出多個可以實現最終條件的解的最終形式。回溯法要求對解要新增一些約束條件。總的來說,如果要解決一個回溯法的問題,通常要確定三個元素:
1、選擇。對於每個特定的解,肯定是由一步步構建而來的,而每一步怎麼構建,肯定都是有限個選擇,要怎麼選擇,這個要知道;同時,在程式設計時候要定下,優先或合法的每一步選擇的順序,一般是通過多個if或者for迴圈來排列。
2、條件。對於每個特定的解的某一步,他必然要符合某個解要求符合的條件,如果不符合條件,就要回溯,其實回溯也就是遞迴呼叫的返回。
3、結束。當到達一個特定結束條件時候,就認為這個一步步構建的解是符合要求的解了。把解存下來或者打印出來。對於這一步來說,有時候也可以另外寫一個issolution函式來進行判斷。注意,當到達第三步後,有時候還需要構建一個數據結構,把符合要求的解存起來,便於當得到所有解後,把解空間輸出來。這個資料結構必須是全域性的,作為引數之一傳遞給遞迴函式。
三、遞迴函式的引數的選擇,要遵循四個原則
1、必須要有一個臨時變數(可以就直接傳遞一個字面量或者常量進去)傳遞不完整的解,因為每一步選擇後,暫時還沒構成完整的解,這個時候這個選擇的不完整解,也要想辦法傳遞給遞迴函式。也就是,把每次遞迴的不同情況傳遞給遞迴呼叫的函式。
2、可以有一個全域性變數,用來儲存完整的每個解,一般是個集合容器(也不一定要有這樣一個變數,因為每次符合結束條件,不完整解就是完整解了,直接列印即可)。
3、最重要的一點,一定要在引數設計中,可以得到結束條件。一個選擇是可以傳遞一個量n,也許是陣列的長度,也許是數量,等等。
4、要保證遞迴函式返回後,狀態可以恢復到遞迴前,以此達到真正回溯。
四、例題
N皇后
難度:困難
n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。
上圖為 8 皇后問題的一種解法。
給定一個整數 n,返回所有不同的 n 皇后問題的解決方案。
每一種解法包含一個明確的 n 皇后問題的棋子放置方案,該方案中 ‘Q’ 和 ‘.’ 分別代表了皇后和空位。
示例:
輸入: 4
輸出:[
[".Q…", // 解法 1
“…Q”,
“Q…”,
“…Q.”],
["…Q.", // 解法 2
“Q…”,
“…Q”,
“.Q…”]
]
解釋: 4 皇后問題存在兩個不同的解法。
提示:
皇后,是國際象棋中的棋子,意味著國王的妻子。皇后只做一件事,那就是“吃子”。當她遇見可以吃的棋子時,就迅速衝上去吃掉棋子。
當然,她橫、豎、斜都可走一到七步,可進可退。(引用自 百度百科 - 皇后 )
來源:新增連結描述
程式碼思路:
這道題是經典的回溯題,開始我們先要考慮一下怎麼生成棋盤並且將“皇后”放入其中,我們可以用二維陣列來建立起一個棋盤mark,之後將陣列中的每個元素設定成為0來代表是空位,之後用1來代表放置的皇后和皇后所能吃子的範圍(就是不能再放入新皇后的位置)之後我們用方向陣列的方法來將棋盤上不能放皇后的位置改成1,這樣棋盤的函式就寫好了。
接下來我們再建立一個和棋盤mark類似的陣列location來儲存皇后的位置,建立方式和棋盤一樣,只不過是用字串的形式,在沒有皇后的位置用‘.'來表示,用’Q‘來表示皇后的位置。
最後開始建立遞迴函式,因為每一行最多隻能有一個皇后,那麼我們就每次遞迴一行棋盤來考慮,每一行只要mark位置上是0就可以放皇后,之後每次遞迴到下一行,在遞迴之前用一個臨時棋盤來儲存現在的棋盤用來之後的回溯用,當我們遞迴到某行的時候發現沒有皇后的位置可以放置了,那麼我就要回溯了,返回到沒有放皇后的時候,之後換成另一個位置來放置皇后的位置,經過這樣的遞迴一直到n行都放置了皇后就可以結束遞迴了。
#include<vector> #include<algorithm> #include<cstring> #include<string> #include<iostream> using namespace std; class Solution { public: vector<vector<string> >solveNQueens(int n) { vector<vector<string> >result; vector<vector<int> >mark; vector<string>location; for (int i = 0;i < n;++i) { mark.push_back((vector<int>())); for (int j = 0;j < n;++j) { mark[i].push_back(0); } location.push_back(""); location[i].append(n, '.'); } generate(0, n, location, result, mark); return result; } private: void put_down_the_queen(int x, int y,//棋盤函式 vector<vector<int> >& mark) { static const int dx[] = { -1,1,0,0,-1,-1,1,1 }; static const int dy[] = { 0,0,-1,1,-1,1,-1,1 }; mark[x][y] = 1; for (int i = 1;i < mark.size();++i) { for (int j = 0;j < 8;++j) { int new_x = x + i * dx[j]; int new_y = y + i * dy[j]; if (new_x >= 0 && new_x < mark.size() && new_y >= 0 && new_y < mark.size()) mark[new_x][new_y] = 1; } } } void generate(int k, int n, vector<string>& location,//遞迴函式 vector<vector<string> >& result, vector<vector<int> >& mark) { if (k == n) { result.push_back(location); return; } for (int i = 0;i < n;++i) { if (mark[k][i] == 0) { vector<vector<int> >tmp_mark = mark; location[k][i] = 'Q'; put_down_the_queen(k, i, mark); generate(k + 1, n, location, result, mark); mark = tmp_mark; location[k][i] = '.'; } } } }; int main() { //測試案例 vector<vector<string> >result; Solution solve; result = solve.solveNQueens(4); for (int i = 0;i < result.size();++i) { cout << "i = " << i<<endl; for (int j = 0;j < result[i].size();j++) { cout << result[i][j].c_str()<<endl; } cout << endl; } }
例二:
78. 子集
給定一組不含重複元素的整數陣列 nums,返回該陣列所有可能的子集(冪集)。
說明:解集不能包含重複的子集。
示例:
輸入:nums = [1,2,3]
輸出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
分析思路:
給定一個集合比如{1,2,3},求該集合的所有子集。
對於集合中的每一個元素,在某一子集中只有兩種狀態,要麼在子集中,要麼不在子集中。
因此對於一個含有n個元素的集合來說,對其中的某一個元素i,用xi來表示其在某一子集中的狀態,xi=1表示在子集中,xi=0表示不在子集中,因此,解可以表示為:
{x1,x2,x3,x4……xn};一共有2^n個向量。那麼可以寫程式碼如下.
#include<iostream> #include<vector> #include<algorithm> #include<cstring> #include<string> #include<queue> using namespace std; void function(int i, vector<int>& num, vector<int>& item, vector<vector<int> >& result) { if (i == num.size()) return; item.push_back(num[i]); result.push_back(item); function(i+1, num, item, result); item.pop_back(); function(i + 1, num, item, result); } int main() { vector<int>num; num.push_back(1); num.push_back(2); num.push_back(3); vector<int>item; vector<vector<int> >result; function(0, num, item, result); for (int i = 0;i < result.size();++i) { for (int j = 0;j < result[i].size();++j) { cout << "[" << result[i][j] << "]"; } cout << endl; } }
另外一種解題方法(位運算):
對於集合{A,B,C}來說,可將每元素轉還成二進位制的100、010、001三個數,之後對於只有三個元素的集合來說,他的子集個數是2^3個,轉化成二進位制就是000、001、010、011、100、101、110、111八個數,之後對於每個元素是否出行只要用&運算出是否為真便可。
#include<iostream> #include<vector> #include<algorithm> #include<cstring> #include<string> #include<queue> using namespace std; vector<vector<int> > function(vector<int>& v) { vector<vector<int> > result; int all = 1 << v.size(); for (int i = 0;i < all;++i) { vector<int> item; for (int j = 0;j < v.size();++j) { if (i & (1 << j)) item.push_back(v[j]); } result.push_back(item); } return result; } int main() { vector<int>num; num.push_back(1); num.push_back(2); num.push_back(3); vector<int>item; vector<vector<int> >result=function(num); for (int i = 0;i < result.size();++i) { for (int j = 0;j < result[i].size();++j) { cout << "[" << result[i][j] << "]"; } cout << endl; } }