在計算機問題中,大量的問題都需要使用遞迴演算法,上一篇部落格我們介紹了一下二叉樹中的遞迴問題。現在我們來看遞迴演算法中非常經典的思想回溯法,這樣的演算法思想通常都應用在一類問題上,這類問題叫做樹型問題,這類問題他本身沒有定義在一顆二叉樹中,但我們具體分析這個問題時就會發現解決這個問題的思路本質是一顆樹的形狀。
樹形問題
現在我們來看遞迴演算法中非常經典的思想回溯法,這樣的演算法思想通常都應用在一類問題上,這類問題叫做樹型問題,這類問題他本身沒有定義在一顆二叉樹中,但我們具體分析這個問題時就會發現解決這個問題的思路本質是一顆樹的形狀。
leetcode 17. 電話號碼的字母組合
解題思路
比如我們輸入的digits=“23”,2能代表abc三個字母,當2代表a時,3代表def,同理我們就可以畫出一棵樹。
遞迴過程:
digits是數字字串
s(digits)是digits所能代表的字母字串
s(digits[0…n-1]) = letter(digits[0]) + s(digits[1…n-1]) = letter(digits[0]) + letter(digits[1]) + s(digits[2…n-1]) = …
程式碼實現
class Solution {
private:
const string letterMap[10] = {
" ", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz" //9
};
vector<string> res;
//index表示從該數字開始在字串,存於s中
// s中儲存了此時從digits[0...index-1]翻譯得到的一個字母字串
// 尋找和digits[index]匹配的字母, 獲得digits[0...index]翻譯得到的解
void findCombination(const string &digits, int index, const string &s){
if (index == digits.size()){
res.push_back(s);
return;
}
//獲得數字
char c = digits[index];
//對應的字母串
string letters = letterMap[c - '0'];
for (int i = 0; i < letters.size(); i++){
findCombination(digits, index + 1, s + letters[i]);
}
return ;
}
public:
vector<string> letterCombinations(string digits) {
res.clear();
if (digits.size() == 0){
return res;
}
findCombination(digits, 0, "");
return res;
}
};
複製程式碼
什麼是回溯
遞迴呼叫的一個重要特徵-要返回。回溯法是暴力解法的一個主要實現手段。
思考題
- leetcode 93
- leetcode 131
組合問題(Permutations)
leetcode 46. 全排列
解題思路
回溯演算法能處理一類重要的問題是排列問題,如果我們要用1,2,3進行排列,我們可以先抽出一個元素,比如我們現在抽出1,那麼我們下面要做的事就是使用2,3兩個元素構造排列。我們又需要抽出一個元素,如果我們抽出2,我們剩下唯一的元素就是3,我們通過這個路徑獲得排列123,用23排列如果選3,那麼就剩下2我們得到排列132。相應的我們考慮最開始選擇2或者選擇3。
這也是一個樹形問題
Perms(nums[0…n-1]) = {取出一個數字} + Perms(nums[{0…n-1} - 這個數字])
程式碼實現
class Solution {
private:
vector<vector<int>> res;
vector<bool> visitor;
//產生一個解
//p[0, index-1]已經是一個組合了
//要生成index大小的組合
// p中儲存了一個有index-1個元素的排列。
// 向這個排列的末尾新增第index個元素, 獲得一個有index個元素的排列
void generatePermute(const vector<int>& nums, int index, vector<int>& p){
if (index == nums.size()){
res.push_back(p);
return;
}
for (int i = 0; i < nums.size(); i++){
if (!visitor[i]){
p.push_back(nums[i]);
visitor[i] = true;
generatePermute(nums, index + 1, p);
//回溯
p.pop_back();
visitor[i] = false;
}
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
res.clear();
if (nums.size() == 0){
return res;
}
visitor = vector<bool>(nums.size(), false);
vector<int> p;
generatePermute(nums, 0, p);
return res;
}
};
複製程式碼
相似問題
- leetcode 47
組合問題 Combinations
回溯的意思就是要回去,遞迴函式自動保證了回去,但是我們設定的其他變數如果有必要的話也必須要回到原位。
leetcode 77. 組合
解題思路
我們在1,2,3,4中取出你兩個數。在第一步時如果我們取1,那麼接下來就在2,3,4中取一個數,我們可以得到組合12,13,14。如果第一步取2,那麼第二步在3,4中取一個數,可以得到組合23,24。如果我們第一步取3,那麼第二步只能取4,得到組合34。
程式碼實現
class Solution {
private:
vector<vector<int>> res;
//前start-1個組合已經完成
// 求解C(n,k), 當前已經找到的組合儲存在c中, 需要從start開始搜尋新的元素
void generateCombinations(int n, int k, int start, vector<int> &c){
if (c.size() == k){
res.push_back(c);
return;
}
for (int i = start; i <= n; i++){
c.push_back(i);
generateCombinations(n, k , i+1, c);
//回溯
c.pop_back();
}
return;
}
public:
vector<vector<int>> combine(int n, int k) {
res.clear();
if (n <= 0 || k <= 0){
return res;
}
vector<int> c;
generateCombinations(n, k, 1, c);
return res;
}
};
複製程式碼
回溯法解決組合問題的優化
這是我們對這道題遞迴樹建立的模型,在這個模型裡存在一個地方我們是明顯沒必要去走的,就是在於最後的地方,我們根本不需要去嘗試取4,這是因為我們取4之後無法再取任意一個數了。在我們上面的演算法中我們還是嘗試取了4,取完4之後當取第二個數時發現我們什麼都取不了了,所以只好再返回回去,對於這一部分我們完全可以把它剪掉。換句話說,我們只嘗試取1,2,3。
回溯法的剪枝
#include <iostream>
#include <vector>
using namespace std;
class Solution {
private:
vector<vector<int>> res;
// 求解C(n,k), 當前已經找到的組合儲存在c中, 需要從start開始搜尋新的元素
void generateCombinations(int n, int k, int start, vector<int> &c){
if( c.size() == k ){
res.push_back(c);
return;
}
// 還有k - c.size()個空位, 所以,[i...n]中至少要有k-c.size()個元素
// i最多為 n - (k-c.size()) + 1
for( int i = start ; i <= n - (k-c.size()) + 1 ; i ++ ){
c.push_back( i );
generateCombinations(n, k, i + 1 , c );
c.pop_back();
}
return;
}
public:
vector<vector<int>> combine(int n, int k) {
res.clear();
if( n <= 0 || k <= 0 || k > n )
return res;
vector<int> c;
generateCombinations(n, k, 1, c);
return res;
}
};
複製程式碼
相似問題
- leetcode 39
- leetcode 40
- leetcode 216
- leetcode 78
- leetcode 90
- leetcode 401
二維平面上的回溯法
leetcode79. 單詞搜尋
解題思路
對於每一個位置,我們按照上右下左從四個方向尋找,當選擇的方向匹配時,則選擇這個位置繼續進行上右下左尋找,如果四個方向都不匹配,則退回上一步的位置尋找下一個方向。
程式碼實現
class Solution {
//從board[startx][starty]開始, 尋找[index...word.size()]
private:
vector<vector<bool>> visited;
int m,n;//行與列
int d[4][2] = {{-1,0}, {0, 1}, {1, 0}, {0, -1}};
bool inArea(int x, int y){
return x >= 0 && x < m && y >= 0 && y < n;
}
bool searchWord(vector<vector<char>>& board, string word, int index, int startx, int starty){
//尋找到最後一個元素了
if (index == (word.size() -1)){
return board[startx][starty] == word[index];
}
if (board[startx][starty] == word[index]){
visited[startx][starty] = true;
// 從startx, starty出發,向四個方向尋
for (int i = 0; i < 4; i++){
int newx = startx + d[i][0];
int newy = starty + d[i][1];
if(inArea(newx, newy) && !visited[newx][newy]){
if (searchWord(board, word, index + 1, newx, newy)){
return true;
}
}
}
//回溯
visited[startx][starty] = false;
}
return false;
}
public:
bool exist(vector<vector<char>>& board, string word) {
m = board.size();
assert(m > 0);
n = board[0].size();
//初始化visitor
for (int i = 0; i < m ; i++){
visited.push_back(vector<bool>(n, false));
}
for (int i = 0; i < m; i++){
for (int j = 0; j < n; j++){
if (searchWord(board, word, 0, i, j)){
return true;
}
}
}
return false;
}
};
複製程式碼
floodfill演算法,一類經典問題
leetcode 200. 島嶼的個數
解題思路
首先我們從二維陣列最開始的地方(0,0)找起,這個地方是1,我們就找到了一個新的島嶼,但我們需要標記和這塊陸地同屬於一個島嶼的陸地,當我們尋找下一個島嶼的時候才不會重複。那麼這個過程就是floodfill過程。其實就是從初始點開始進行一次深度優先遍歷,和上面那道題的尋找很相似,對每一個島嶼進行四個方向尋找。
程式碼實現
class Solution {
private:
int d[4][2] = {{0,1}, {0, -1}, {1,0},{-1, 0}};
int m, n;
vector<vector<bool>> visited;
bool inArea(int x, int y){
return x >= 0 && x < m && y >= 0 && y < n;
}
void dfs(vector<vector<char>>& grid, int x, int y){
visited[x][y] = true;
for (int i = 0; i < 4; i++){
int newx = x + d[i][0];
int newy = y + d[i][1];
if (inArea(newx, newy) && !visited[newx][newy] && grid[newx][newy] == '1'){
dfs(grid, newx, newy);
}
}
return;
}
public:
int numIslands(vector<vector<char>>& grid) {
m = grid.size();
if (m == 0){
return 0;
}
n = grid[0].size();
if (n == 0){
return 0;
}
for (int i = 0; i < m; i++){
visited.push_back(vector<bool>(n, false));
}
int res = 0;
for (int i = 0; i < m; i++){
for (int j = 0; j < n; j++){
if (grid[i][j] == '1' && !visited[i][j]){
dfs(grid, i, j);
res++;
}
}
}
return res;
}
};
複製程式碼
在這裡,我們似乎沒有看見回溯的過程,也就是說我們不需要找到一個位置讓visited[x][y]為false,這是因為我們的目的就是要把和最初我們執行的(i,j)這個點相連線的島嶼全部標記上,而不是在其中找到某一個特定的序列或者一個具體的值,所以我們只標記true,不會把它倒著標記成false。所以對於這個問題是否叫做回溯法,這是一個見仁見智的問題。在搜尋的過程中一定會回去,這是遞迴的特性。但它沒有對資訊進行重置。不過它的解題思路是經典的floodfill。
相似問題
- leetcode 130
- leetcode 417
回溯法是經典人工智慧的基礎
回溯法師經典人工智慧的基礎
leetcode 51. N皇后
解題思路
**快速判斷合法的情況
- dia1: 橫縱座標相加相同
- dia2:橫座標-縱座標相同
對於四皇后為例看一下如何遞迴回溯。首先肯定每行都應該有一個皇后,否則就會有一行出現多個皇后。那麼第二行只能在第三個位置或第四個位置,考慮第三個位置。那麼第三行無論在哪都會有衝突。說明我們第二行的皇后不能放在第三個位置,我們回溯,在第四個位置放置皇后。
每一次在一行中嘗試擺放一個皇后,來看我們能不能擺下這個皇后,如果不能擺下,回去上一行重新擺放上一行皇后的位置,直到我們在四行都擺放皇后。
程式碼實現
class Solution {
private:
vector<bool> col, dia1, dia2;
vector<vector<string>> res;
//嘗試在一個n皇后問題中,擺放第index行的皇后位置
void putQueen(int n, int index, vector<int> &row){
if (index == n){
res.push_back(generateBoard(n, row));
return;
}
// 嘗試將第index行的皇后擺放在第i列
for (int i = 0; i < n; i++){
if (!col[i] && !dia1[index + i] && !dia2[index -i + n -1]){
row.push_back(i);
col[i] = true;
dia1[index + i] = true;
dia2[index -i + n -1] = true;
//遞迴,嘗試下一行
putQueen(n, index + 1, row);
//回溯,復原
col[i] = false;
dia1[index + i] = false;
dia2[index -i + n -1] = false;
row.pop_back();
}
}
return;
}
vector<string> generateBoard(int n, vector<int> &row){
assert(n == row.size());
vector<string> board(n, string(n, '.'));
for(int i = 0; i < n; i++){
board[i][row[i]] = 'Q';
}
return board;
}
public:
vector<vector<string>> solveNQueens(int n) {
res.clear();
col.clear();
dia1.clear();
dia2.clear();
col = vector<bool>(n, false);
dia1 = vector<bool>(2*n -1, false);
dia2 = vector<bool>(2*n -1, false);
vector<int> row;
putQueen(n, 0, row);
return res;
}
};
複製程式碼
相似問題
- leetcode 52
- leetcode 37
-------------------------華麗的分割線--------------------
看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。
想了解更多,歡迎關注我的微信公眾號:番茄技術小棧