如果你在匆匆忙忙地学习回溯,此文不负您愿
快速认识回溯法
回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
生活思维想象:
- 假如我们在走一条三叉路口,我们去的目的地就在三条道路中的一条之中,我们试探性的选取
第一条道路进行行走,并寻找目的地,这叫(按选优条件向前搜索,以达到目标). - 后来我们发现,我们目的地不在第一条道路上,且第一条道路一直向前是找不到我们的目的地的,这叫做(发现原先选择并不优或达不到目标)
- 我们不得不退回三叉路口点,重新选择第二条路或者 第三条路,这叫做:(退回一步重新选择),我们的三叉路口点就叫做 “回溯点”。
多叉树分析回溯法:
回溯点
/ | \ 1 节点达不到目标,退回回溯点,重新选择其他节点,直到能 达到目标 结束回溯
1 2 3
解题分析,深入回溯
接下来我们来使用两个 leetcode中 比较中等的题目更加具体理解回溯的过程:
1. 矩阵中的路径 (难度:中等)
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[[“a”,“b”,“c”,“e”],
[“s”, “f”, “c”,“s”],
[“a”,“d”, “e”,“e”]]
注意不能重复访问:但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
来源:力扣(LeetCode)
让我们来分析一下这个问题:
- 简单明了的说就是 矩阵中连接起来的字符元素 能完全匹配 字符串
- 我们将问题分散思考为 (多个两个字符之间的连接, abc = a 连接 b ,b 再连接 c), 比如:字符串第一个字符与第二个字符的连接,当我们在矩阵中匹配到第一个字符后,将矩阵中该元素 作为 “回溯点”,之后 进一步探索 第二个字符在矩阵元素中的位置 (由回溯点 进行选择)
- 第一字符与第二字符配连接,未匹配成功,则退回回溯点,之后重新选择。这样多个两字符之间的连接过程使用递归实现,我们就能完成整个 匹配过程。
- 注意点:访问重复是不允许的,我们可以使用 bool 数组来做这个事情
问题已知条件:
矩阵: vector<vector> matrix
要匹配的字符串:string str
代码如下:
class Solution {
public:
/*思路: 查找是否存在不可逆的路径满足 字符串 的匹配。
* 1. 不可逆路径的实现:设置 矩阵bool数组 判断
* 2. 单个问题:字符串中的角标字符与矩阵中位置上的字符匹配,之后进行递增角标得到新字符,从回溯点转移状态,与新字符再次进行匹配
* 3. 字符匹配不成功,需要回归上一状态(回溯),再次进行其余状态的转移,直到完全匹配
*/
bool exist(vector<vector<char>>& board, string word);
private:
int rows,cols;
int wordLength;
bool hasPathCore(const vector<vector<char>>& board,int row,int col, const string& word,int &pathLength,bool* visited);
};
bool Solution::exist(vector<vector<char>> &board, string word) {
if(board.size() < 1 || word.size() < 1 ) return false;
rows = board.size(),cols = board.at(0).size(),wordLength = word.size();
bool visited[rows * cols];
memset(visited, 0, rows * cols);
int pathLength = 0;
for (int row = 0; row !=rows; ++row) {
for (int col = 0; col !=cols; ++col) {
if(hasPathCore(board,row,col,word,pathLength,visited))
return true;
}
}
return false;
}
bool Solution::hasPathCore(const vector<vector<char>> &board, int row, int col, const string &word, int &pathLength,
bool *visited) {
if(pathLength == wordLength)
return true;
bool hasPath = false;
if(row>=0 && row<rows && col>=0 && col < cols
&& board.at(row).at(col) == word.at(pathLength)&& !visited[row * cols+ col])
{
++ pathLength;
visited[row * cols+ col] = true;
hasPath = hasPathCore(board,row+1,col,word,pathLength,visited)
||hasPathCore(board,row-1,col,word,pathLength,visited)
||hasPathCore(board,row,col+1,word,pathLength,visited)
||hasPathCore(board,row,col-1,word,pathLength,visited);
if(!hasPath){
-- pathLength;
visited[row * cols+ col] = false;
}
}
return hasPath;
}
2. 机器人的运动返回 (难度:中等)
地上有一个 m 行 n 列的方格。 一个机器人从坐标(0,0)的格子进行移动,它每次可以向左,右,上,下,移动一格,但不能进入行坐标和列坐标 的数位之和 大于 k 的格子。 例如,当 k 为18 时,机器人能够进入方格 (二维数组元素坐标)(35,37),因为 3+5+3+7 = 18。但它不能进入方格 (二维数组元素坐标)(35,38),因为 3+5+3+8 = 19。请问该机器人能够到达多少个格子(二维数组元素)?
来源:剑指offer
让我们来分析一下这个问题:
机器人矩阵内移动问题,更简单的说就是走格子,走什么样子的格子,走符合条件的(行坐标和列坐标 的数位之和 小于等于 k 的格子),每走一个格子都是作为“回溯点进行选择的”,我们就是统计机器人一步一步走格子并回溯重新选择,到无符合条件的格子中能够走,所能达到的所有格子数。
- 走格子的条件判断(是否行坐标和列坐标 的数位之和 小于等于 k 的格子,且未被访问过)
- 回溯 累加 格子量,看下图,机器人能走 1 -2-4 ,1 -2 -5 , 1 - 3 ,这些需要回溯并累加可能情况
这样看来,这道题比第一道题还要更加简单一点.
比如: 1
/ \
2 3
/ \
4 5
分为四个函数:
- int getDigitSum(int number) :用来计算 格子横纵坐标值的 位数值相加数
- bool isValid(int rows,int cols,int row,int col,int k,bool* visited):判断格子是否有效,机器人能够行驶
- int movingCountCore(int rows,int cols,int row,int col,int k,bool* visited):核心函数,计算机器人在 指定格子点上能够继续行驶的格子数。
- int movingCount(int m, int n, int k) :主要函数, 用来 检测输入控制,和 创建变量,返回总移动格子数值。
代码如下:
class Solution {
public:
int movingCount(int m, int n, int k) {
if(m <=0 || n<=0 || k< 0)
return 0;
bool visited[m*n];
memset(visited,0,m*n);
return movingCountCore(m,n,0,0,k,visited);
}
private:
int getDigitSum(int number){
int sum = 0;
while(number > 0){
sum += number % 10;
number/=10;
}
return sum;
}
bool isValid(int rows,int cols,int row,int col,int k,bool* visited){
if(row >=0 && row< rows && col >=0 &&col< cols
&&getDigitSum(row)+getDigitSum(col)<=k
&&!visited[row*cols+col] ){
return true;
}
return false;
}
int movingCountCore(int rows,int cols,int row,int col,int k,bool* visited){
int count = 0;
if(isValid(rows,cols,row,col,k,visited)){
visited[row*cols+col] = true;
count = 1+ movingCountCore(rows,cols,row+1,col,k,visited)
+ movingCountCore(rows,cols,row-1,col,k,visited)
+ movingCountCore(rows,cols,row,col+1,k,visited)
+ movingCountCore(rows,cols,row,col-1,k,visited);
}
return count;
}
};
回溯法解题的关键要素
- 确定了问题的解空间结构后,回溯法将从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。开始结点成为活结点,同时也成为扩展结点。
- 在当前的扩展结点处,向纵深方向搜索并移至一个新结点,这个新结点就成为一个新的活结点,并成为当前的扩展结点。
- 如果在当前的扩展结点处不能再向纵深方向移动,则当前的扩展结点就成为死结点。此时应往回移动(回溯)至最近的一个活结点处,并使其成为当前的扩展结点。
- 回溯法以上述工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点时为止。
运用回溯法解题的关键要素有以下三点:
- (1) 针对给定的问题,定义问题的解空间;
- (2) 确定易于搜索的解空间结构;
- (3) 以深度优先方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。