本篇主要是介绍一下回溯算法:所谓回溯算法,又称为“试探法”。解决问题时,每进行一步,都是抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目标,立刻做回退操作重新选择。这种走不通就回退再走的方法就是回溯算法。
文章目录
- 一、回溯算法的定义
- 二、回溯算法的模型
- 深度优先搜索
- 广度优先搜索
- 三、有关回溯算法的题型
一、回溯算法的定义
回溯法:实际上回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足条件的某个状态的点称为“回溯点”。也可以称为剪枝点,所谓的剪枝,指的是把不会找到的目标,或者不必要的路径裁减掉。
在包含问题的所有解中空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当深度到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
如果使用回溯法求解问题的时候,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一解时,只要搜索到问题的一个解就可以结束。(除了深度优先搜索外,常见的还有广度优先搜索)。
二、回溯算法的模型
一、深度优先遍历(DFS)
DFS(当前这一步的处理逻辑)
{
1. 判断边界,是否已经一条道走到黑了:向上回退
2. 尝试当下的每一种可能
3. 确定一种可能之后,继续下一步 DFS(下一步)
}
二、广度优先遍历(BFS)
BFS()
{
1. 建立起始步骤,队列初始化
2. 遍历队列中的每一种可能,whlie(队列不为空)
{
通过队头元素带出下一步的所有可能,并且依次入队
{
判断当前情况是否达成目标:按照目标要求处理逻辑
}
继续遍历队列中的剩余情况
}
}
三、有关回溯算法的题型
一、有关深度优先遍历的题型
1.员工的重要性OJ链接
/*
// Definition for Employee.
class Employee {
public:
int id;
int importance;
vector<int> subordinates;
};
*/
class Solution {
public:
int DFS(unordered_map<int,Employee*>&info,int id)
{
int sumImportance=info[id]->importance;
//深度探索员工一的下属然后往下探索员工二的下属……
for(auto e:info[id]->subordinates)
{
sumImportance+=DFS(info,e);
}
//最后返回员工的数据结构中的重要度的和
return sumImportance;
}
int getImportance(vector<Employee*> employees, int id) {
//如果此时没有保存员工的数据结构则返回空的重要度
if(employees.empty())
return 0;
//为了使时间效率更高可以使用哈希的结构,因为哈希的查找效率比较高
unordered_map<int,Employee*> info;
//将员工的信息的数据结构放入哈希表中
for(auto e:employees)
{
info[e->id]=e;
}
return DFS(info,id);//将哈希表中的内容进行深度优先搜索操作
}
};
2.图像渲染OJ链接
int nextp[4][2]={{1,0},{0,1},{-1,0},{0,-1}};
class Solution {
public:
void DFS(vector<vector<int>>&image,vector<vector<int>>&book,int row,int col,int sr,int sc,int oldColor,int newColor)
{
image[sr][sc]=newColor;
book[sr][sc]=1;
for(int i=0;i<4;i++)
{
int newX=sr+nextp[i][0];
int newY=sc+nextp[i][1];
if(newX>=row||newX<0)
{
continue;
}
if(newY>=col||newY<0)
{
continue;
}
if(image[newX][newY]==oldColor&&book[newX][newY]==0)
{
DFS(image,book,row,col,newX,newY,oldColor,newColor);
}
}
}
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
if(image.empty())
{
return image;
}
vector<vector<int>> book(image.size(),vector<int>(image[0].size(),0));
int oldColor=image[sr][sc];
DFS(image,book,image.size(),image[0].size(),sr,sc,oldColor,color);
return image;
}
};
3.岛屿的周长OJ链接
int nextp[4][2]={{1,0},{0,1},{-1,0},{0,-1}};//给定方向矩阵
class Solution {
public:
int DFS(vector<vector<int>> &grid,int row,int col,int x,int y)
{
if(x>=row||x<0||y>=col||y<0||grid[x][y]==0) return 1;
if(grid[x][y]==2) return 0;
grid[x][y]=2;//标记此时位置已经搜索过
int s=0;
//对于此时的位置进行深度优先遍历操作
for(int k=0;k<4;k++)
{
int newX=x+nextp[k][0];
int newY=y+nextp[k][1];
//对于该位置进行深度优先遍历求出此时的位置有效的边界长度
s+=DFS(grid,row,col,newX,newY);
}
return s;
}
int islandPerimeter(vector<vector<int>>& grid) {
if(grid.empty())
return 0;
int row=grid.size();
int col=grid[0].size();
int c=0;
for(int i=0;i<row;i++)
{
for(int j=0;j<col;j++)
{
//对于陆地进行深度优先遍历操作
if(grid[i][j]==1)
c+=DFS(grid,row,col,i,j);
}
}
return c;
}
};
4.被围绕的区域OJ链接
int nextp[4][2]={{1,0},{0,1},{-1,0},{0,-1}};
class Solution {
public:
void DFS(vector<vector<char>>&board,int row,int col,int sr,int sc)
{
board[sr][sc]='*';//标记此时为‘O’深度探索是否被
for(int i=0;i<4;i++)
{
int newX=sr+nextp[i][0];//将此时的方向进行四个方向进行深度优先遍历操作
int newY=sc+nextp[i][1];
if(newX<0||newX>=row)//需要对此时的方向进行判断是否是越界行为,如果是越界行为则此时跳过去,进行下一个方向的深度优先遍历操作
{
continue;
}
if(newY<0||newY>=col)
{
continue;
}
if(board[newX][newY]!='*'&&board[newX][newY]!='X')//
//继续搜索‘O’
{
DFS(board,row,col,newX,newY);
}
}
}
void solve(vector<vector<char>>& board) {
if(board.empty())
return;
int row=board.size();
int col=board[0].size();
//第一行和最后一行进行深度优先遍历
for(int j=0;j<col;j++)
{
if(board[0][j]=='O')
DFS(board,row,col,0,j);
if(board[row-1][j]=='O')
DFS(board,row,col,row-1,j);
}
//第一列和最后一列进行深度优先遍历
for(int i=0;i<row;i++)
{
if(board[i][0]=='O')
DFS(board,row,col,i,0);
if(board[i][col-1]=='O')
DFS(board,row,col,i,col-1);
}
//最后将深度搜索的结果进行修改
for(int i=0;i<row;i++)
{
for(int j=0;j<col;j++)
{
if(board[i][j]=='*')//最后将所有的'*'改为‘O’
{
board[i][j]='O';
}
else if(board[i][j]=='O')//最后将所有的'O'改为‘X’
{
board[i][j]='X';
}
}
}
}
};
5.岛屿数量OJ链接
int nextp[4][2]={{1,0},{0,1},{-1,0},{0,-1}};//定义一个方向矩阵
class Solution {
public:
void DFS(vector<vector<char>>&grid,vector<vector<int>>&book,int row,int col,int x,int y)
{
book[x][y]=1;//标记此时的位置已经走过
for(int k=0;k<4;k++)//对于当前x,y的位置进行方向遍历操作
{
int newX=x+nextp[k][0];//对于x的横向坐标
int newY=y+nextp[k][1];//对于y的纵向坐标
if(newX>=row||newX<0||newY>=col||newY<0)//考虑一下边界的问题,如果此时走到了边界应该访问下一个位置
continue;
if(grid[newX][newY]=='1'&&book[newX][newY]==0)
DFS(grid,book,row,col,newX,newY);
}
}
int numIslands(vector<vector<char>>& grid) {
if(grid.empty())
return 0;
int row=grid.size();
int col=grid[0].size();
//标记矩阵
vector<vector<int>> book(row,vector<int>(col,0));
int amount=0;
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
if(grid[i][j]=='1'&&book[i][j]==0)
{
amount++;
DFS(grid,book,row,col,i,j);
}
return amount;
}
};
6.岛屿的最大面积OJ链接
int nextp[4][2]={{1,0},{0,1},{-1,0},{0,-1}};//给定方向矩阵
int s=0;//定义全局变量来求出每个岛屿的面积
class Solution {
public:
void DFS(vector<vector<int>>&grid,int row,int col,int x,int y)
{
//如果此时为陆地则进行S++
if(grid[x][y]==1)
s++;
grid[x][y]=2;//同时标记此时已经走过
for(int k=0;k<4;k++)//从当前位置进行方向遍历
{
int newX=x+nextp[k][0];
int newY=y+nextp[k][1];
if(newX<0||newX>=row||newY<0||newY>=col)
continue;
if(grid[newX][newY]==1)//从陆地开始进行深度遍历
DFS(grid,row,col,newX,newY);
}
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
if(grid.empty())
return 0;
int row=grid.size();
int col=grid[0].size();
int maxS=0;
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
if(grid[i][j]==1)//从陆地开始进行深度优先遍历操作
//将每一个位置进行比较,找出最大那个岛屿的面积
{
DFS(grid,row,col,i,j);
maxS=max(maxS,s);
s=0;
}
return maxS;
}
};
7.电话号码的字母组合OJ链接
map<char,string> hashTable={{'2',"abc"},{'3',"def"},{'4',"ghi"},{'5',"jkl"},{'6',"mno"},{'7',"pqrs"},{'8',"tuv"},{'9',"wxyz"}};
class Solution {
public:
void DFS(string& digits,vector<string>&resultString,string curStr,int curDepth)
{
//如果此时遍历到最后一个
if(curDepth==digits.size())
{
resultString.push_back(curStr);
return;
}
//利用哈希映射的方式进行
string strMap=hashTable[digits[curDepth]];
for(char ch:strMap)
{
DFS(digits,resultString,curStr+ch,curDepth+1);
}
}
vector<string> letterCombinations(string digits) {
//将所有的结果集保存到resultString
vector<string> resultString;
if(digits.empty())
return resultString;
//从第一个位置开始进行深度优先遍历深度优先遍历
DFS(digits,resultString,"",0);
return resultString;
}
};
8.组合总和OJ链接
class Solution {
public:
void DFS(vector<int>&candidates,vector<vector<int>>&solutions,vector<int>&solution,int curSum,int position,int target)
{
//如果当前遍历所得到的结果刚好为所给的target值
//那么将当前所遍历的结果集保留在solutions
if(curSum==target)
{
solutions.push_back(solution);
}
//如果遍历出的和是大于,则说明此时条件不满足
if(curSum>target)
{
return;
}
//从position位置开始进行深度优先遍历操作
for(int i=position;i<candidates.size();i++)
{
if(candidates[i]>target)//如果此时的所给出的candidats中
// //的值比所给的target大,那么直接结束当前所在位置的优先遍历
{
continue;
}
//不满足上述条件说明此时的curSum的值小于target
//可以将当前的candidates[i]放入到当前的结果集中
//然后接着往后进行深度优先遍历操作
solution.push_back(candidates[i]);
DFS(candidates,solutions,solution,curSum+candidates[i],i,target);
solution.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//定义一个二维数组保留所有结果集
vector<vector<int>> solutions;
//保留当前的结果集
vector<int> solution;
//记录当前结果集中的和
int curSum=0;
//进行深度优先遍历
DFS(candidates,solutions,solution,curSum,0,target);
return solutions;
}
};
9.活字印刷OJ链接
class Solution {
public:
void DFS(string &tiles,string curStr,vector<int>&useString,unordered_set<string>&hashTable)
{
//如果此时深度优先遍历的字符串不为空,则此时的所得到的的字符串放入当前的哈希表中,将自动进行去重的处理
if(!curStr.empty())
hashTable.insert(curStr);
//深度优先遍历每一个位置
for(int i=0;i<tiles.size();i++)
{
if(useString[i])//如果当前的位置已经被访问过,则说明此时当前位置不需要访问
continue;
useString[i]=1;//标记当前位置
DFS(tiles,curStr+tiles[i],useString,hashTable);//继续往下进行深度优先遍历的操作
//需要进行回退的操作
useString[i]=0;
}
}
int numTilePossibilities(string tiles) {
if(tiles.empty())
return 0;
//利用unoreded_set容器的特性,将容器内的内容自动进行去重操作
unordered_set<string> hashTable;
//先将当前的tiles进行初始化的操作
vector<int> useString(tiles.size(),0);
//进行深度优先遍历操作
DFS(tiles,"",useString,hashTable);
return hashTable.size();
}
};
10.N皇后OJ链接
class Solution {
public:
//深度优先遍历
void DFS(vector<vector<pair<int,int>>>&solutions,vector<pair<int,int>>&solution,int curRow,int n)
{
//如果当前行等于n则将一种solution方案放入solutions中
if(curRow==n)
{
solutions.push_back(solution);
}
for(int i=0;i<n;i++)
{
//判断当前皇后所在的位置是否能够满足条件
if(isValid(solution,curRow,i))
{
//如果满足条件则将此时的坐标记录下来
solution.push_back(make_pair(curRow,i));
//继续深度优先遍历下一行
DFS(solutions,solution,curRow+1,n);
//第一列遍历后回溯下来遍历第二行
solution.pop_back();
}
}
}
bool isValid(vector<pair<int,int>>&solution,int curRow,int col)
{
//遍历方案中的点
//如果此时该皇后中的纵坐标与方案中的纵坐标相同或者横坐标与纵坐标的和等于当前皇后的横纵坐标之和或者是方案中横纵坐标之差等于当前皇后的横纵坐标之差
for(auto i:solution)
{
if(i.second==col||i.first+i.second==curRow+col||i.first-i.second==curRow-col)
{
return false;
}
}
return true;
}
vector<vector<string>> transformString(vector<vector<pair<int,int>>>&solutions,int n)
{
vector<vector<string>> resultString;//保留最后方案中的字符串
for(vector<pair<int,int>>&solution:solutions)
{
//利用构造函数先进行所有的字符变为'.'最后再将皇后位置变为'Q'
vector<string> tmpString(n,string(n,'.'));
for(pair<int,int> &i:solution)
{
tmpString[i.first][i.second]='Q';
}
resultString.push_back(tmpString);
}
return resultString;
}
vector<vector<string>> solveNQueens(int n) {
vector<vector<pair<int,int>>> solutions;//将不同解法的方案存放
vector<pair<int,int>> solution;//存放一种方案中的n个皇后的摆放
//将n*n的棋盘从第一个位置开始进行深度优先遍历
DFS(solutions,solution,0,n);
//将深度优先遍历后的每一种方案都转化为字符型
return transformString(solutions,n);
}
};
11.N皇后IIOJ链接
class Solution {
public:
//深度优先遍历
int DFS(vector<pair<int,int>>&solution,int curRow,int n)
{
//如果当前行等于n则将一种solution
if(curRow==n)
{
return 1;
}
else
{
int count=0;
for(int i=0;i<n;i++)
{
//判断当前皇后所在的位置是否能够满足条件
if(isValid(solution,curRow,i))
{
//如果满足条件则将此时的坐标记录下来
solution.push_back(make_pair(curRow,i));
//继续深度优先遍历下一行
count+=DFS(solution,curRow+1,n);
//第一列遍历后回溯下来遍历第二行
solution.pop_back();
}
}
return count;
}
}
bool isValid(vector<pair<int,int>>&solution,int curRow,int col)
{
//遍历方案中的点
//如果此时该皇后中的纵坐标与方案中的纵坐标相同或者横坐标与纵坐标的和等于当前皇后的横纵坐标之和或者是方案中横纵坐标之差等于当前皇后的横纵坐标之差
for(auto i:solution)
{
if(i.second==col||i.first+i.second==curRow+col||i.first-i.second==curRow-col)
{
return false;
}
}
return true;
}
int totalNQueens(int n) {
vector<pair<int,int>> solution;
return DFS(solution,0,n);
}
};
二、有关广度优先遍历的题型
1.N叉树的层序遍历OJ链接
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
if(root==nullptr) return vector<vector<int>>();
vector<vector<int>> vv;
queue<Node*> q;//利用队列进行广度优先遍历
q.push(root);//首先将头结点先入队操作
while(!q.empty())//如果此时的队列不为空,则继续进行操作
{
vector<int> v;//利用一个一维数组来存储当前层的节点
int size=q.size();//每一层的大小需要保存下来,方便下一次的遍历操作
for(int i=0;i<size;i++)
{
Node*front=q.front();
q.pop();
v.push_back(front->val);
for(auto child:front->children)//遍历当前节点的孩子节点,如果不为空,则将此时的孩子节点入队列
if(child)
q.push(child);
}
vv.push_back(v);
}
return vv;
}
};
2.腐烂的橘子OJ链接
int nextp[4][2]={{1,0},{0,1},{0,-1},{-1,0}};
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
//利用pair来存储位置信息
queue<pair<int,int>> q;
int row=grid.size();
int col=grid[0].size();
//第一步将腐烂的橘子进行入队操作
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
if(grid[i][j]==2)
q.push(make_pair(i,j));
//进行蔓延的方向
int times=0;
while(!q.empty()){
int size=q.size();
int flag=0;//定义一个flag来判断是否是有新橘子被腐烂
for(int i=0;i<size;i++){
pair<int,int> Curpos=q.front();
q.pop();
for(int i=0;i<4;i++){
int newX=Curpos.first+nextp[i][0];
int newY=Curpos.second+nextp[i][1];
//如果此时的位置中存在越界或者是该位置上面的橘子已经被腐烂过则跳过此次
if(newX>=row||newY>=col||newX<0||newY<0||grid[newX][newY]!=1)
continue;
flag=1;
grid[newX][newY]=2;
q.push(make_pair(newX,newY));
}
}
if(flag)
++times;
}
//最后需要进行判断是否还存在无法腐烂的橘子
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
if(grid[i][j]==1)
return -1;
return times;
}
};
3.单词接龙OJ链接
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
//利用哈希表进行查找效率比较高
unordered_set<string> HashTable(wordList.begin(),wordList.end());
//定义一个标记的哈希表来标记已经查找过的单词
unordered_set<string> visitHashTable;
//讲起始的单词进行标记
visitHashTable.insert(beginWord);
//利用队列的特性进行广度优先遍历操作
queue<string> q;
q.push(beginWord);
int step=1;
//队列不为空,继续进行转换操作
while(!q.empty())
{
int size=q.size();
for(int i=0;i<size;i++)
{
//用来保存队列中第一个单词进行转换操作
string curWord=q.front();
q.pop();
for(int i=0;i<curWord.size();i++){
//对于当前单词进行转换操作
string newWord=curWord;
for(char ch='a';ch<='z';ch++)
{
//替换newWord中的字符来进行转换
newWord[i]=ch;
//如果此时的单词在哈希表中没有找到,或者该单词已经被标记过
if(!HashTable.count(newWord)||visitHashTable.count(newWord))
continue;
if(newWord==endWord)
return step+1;
//如果没有转换成功,需要进行入队操作和进行标记操作
q.push(newWord);
visitHashTable.insert(newWord);
}
}
}
step++;
}
return 0;
}
};
4.最小基因变化OJ链接
class Solution {
public:
int minMutation(string startGene, string endGene, vector<string>& bank) {
//利用哈希表查找
unordered_set<string> HashTable(bank.begin(),bank.end());
//标记
unordered_set<string> visitHashTable;
visitHashTable.insert(startGene);
//利用队列进行广度优先遍历操作
queue<string> q;
q.push(startGene);
//对于基因有四种可能
vector<char> Gene(4);
Gene[0]='A';
Gene[1]='C';
Gene[2]='G';
Gene[3]='T';
int step=0;
while(!q.empty())
{
int size=q.size();
for(int i=0;i<size;i++)
{
string curGene=q.front();
q.pop();
//对于当前基因的位置进行广度遍历
for(int j=0;j<curGene.size();j++)
{
string newGene=curGene;
for(auto ch:Gene)
{
newGene[j]=ch;
//如果已经出现了当前的基因或者是该基因不存在
if(!HashTable.count(newGene)||visitHashTable.count(newGene))
continue;
//如果此时的基因正好是等于当前的基因
if(newGene==endGene)
return step+1;
//否则讲当前额的基因入队列
visitHashTable.insert(newGene);
q.push(newGene);
}
}
}
step++;
}
return -1;
}
};
5.打开转盘锁 OJ链接
class Solution {
public:
int openLock(vector<string>& deadends, string target) {
//利用哈希表的查找
unordered_set<string> HashTable(deadends.begin(),deadends.end());
if(HashTable.find("0000")!=HashTable.end())
return -1;
//标记已经搜索过的字符串
unordered_set<string> visitHashTable;
visitHashTable.insert("0000");
//利用队列进行广度优先遍历
queue<string> q;
q.push("0000");
int step=0;
while(!q.empty())
{
int size=q.size();
for(int i=0;i<size;i++)
{
string curString=q.front();
q.pop();
//如果此时curStringw为目标则直接返回
if(curString==target)
return step;
for(int j=0;j<curString.size();j++)
{
string newString1=curString;
string newString2=curString;
//当前位置可以向前或者向后拨动
newString1[j]=newString1[j]=='9'?'0':newString1[j]+1;
newString2[j]=newString2[j]=='0'?'9':newString2[j]-1;
if(HashTable.find(newString1)==HashTable.end()&&visitHashTable.find(newString1)==visitHashTable.end())
{
q.push(newString1);
visitHashTable.insert(newString1);
}
if(HashTable.find(newString2)==HashTable.end()&&visitHashTable.find(newString2)==visitHashTable.end())
{
q.push(newString2);
visitHashTable.insert(newString2);
}
}
}
step++;
}
return -1;
}
};