在計算機問題中,大量的問題都須要使用遞歸算法,上一篇博客咱們介紹了一下二叉樹中的遞歸問題。如今咱們來看遞歸算法中很是經典的思想回溯法,這樣的算法思想一般都應用在一類問題上,這類問題叫作樹型問題,這類問題他自己沒有定義在一顆二叉樹中,但咱們具體分析這個問題時就會發現解決這個問題的思路本質是一顆樹的形狀。ios
如今咱們來看遞歸算法中很是經典的思想回溯法,這樣的算法思想一般都應用在一類問題上,這類問題叫作樹型問題,這類問題他自己沒有定義在一顆二叉樹中,但咱們具體分析這個問題時就會發現解決這個問題的思路本質是一顆樹的形狀。git
好比咱們輸入的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;
}
};
複製代碼
遞歸調用的一個重要特徵-要返回。回溯法是暴力解法的一個主要實現手段。數組
回溯算法能處理一類重要的問題是排列問題,若是咱們要用1,2,3進行排列,咱們能夠先抽出一個元素,好比咱們如今抽出1,那麼咱們下面要作的事就是使用2,3兩個元素構造排列。咱們又須要抽出一個元素,若是咱們抽出2,咱們剩下惟一的元素就是3,咱們經過這個路徑得到排列123,用23排列若是選3,那麼就剩下2咱們獲得排列132。相應的咱們考慮最開始選擇2或者選擇3。bash
這也是一個樹形問題
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;
}
};
複製代碼
回溯的意思就是要回去,遞歸函數自動保證了回去,可是咱們設置的其餘變量若是有必要的話也必需要回到原位。函數
咱們在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。ui
#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;
}
};
複製代碼
對於每個位置,咱們按照上右下左從四個方向尋找,當選擇的方向匹配時,則選擇這個位置繼續進行上右下左尋找,若是四個方向都不匹配,則退回上一步的位置尋找下一個方向。人工智能
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;
}
};
複製代碼
首先咱們從二維數組最開始的地方(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。
回溯法師經典人工智能的基礎
**快速判斷合法的狀況
對於四皇后爲例看一下如何遞歸回溯。首先確定每行都應該有一個皇后,不然就會有一行出現多個皇后。那麼第二行只能在第三個位置或第四個位置,考慮第三個位置。那麼第三行不管在哪都會有衝突。說明咱們第二行的皇后不能放在第三個位置,咱們回溯,在第四個位置放置皇后。
每一次在一行中嘗試擺放一個皇后,來看咱們能不能擺下這個皇后,若是不能擺下,回去上一行從新擺放上一行皇后的位置,直到咱們在四行都擺放皇后。
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;
}
};
複製代碼
-------------------------華麗的分割線--------------------
看完的朋友能夠點個喜歡/關注,您的支持是對我最大的鼓勵。
想了解更多,歡迎關注個人微信公衆號:番茄技術小棧