写给大忙人看的回溯法

快速认识回溯法

          回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

生活思维想象:

  1. 假如我们在走一条三叉路口,我们去的目的地就在三条道路中的一条之中,我们试探性的选取
    第一条道路进行行走,并寻找目的地,这叫(按选优条件向前搜索,以达到目标).
  2. 后来我们发现,我们目的地不在第一条道路上,且第一条道路一直向前是找不到我们的目的地的,这叫做(发现原先选择并不优或达不到目标)
  3. 我们不得不退回三叉路口点,重新选择第二条路或者 第三条路,这叫做:(退回一步重新选择),我们的三叉路口点就叫做 “回溯点”。

多叉树分析回溯法:

    回溯点
    /  |  \         1 节点达不到目标,退回回溯点,重新选择其他节点,直到能 达到目标 结束回溯
   1   2   3





解题分析,深入回溯

接下来我们来使用两个 leetcode中 比较中等的题目更加具体理解回溯的过程:

1. 矩阵中的路径 (难度:中等)

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。

[[“a”,“b”,“c”,“e”],
[“s”, “f”, “c”,“s”],
[“a”,“d”, “e”,“e”]]

注意不能重复访问:但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

来源:力扣(LeetCode)

让我们来分析一下这个问题:

  1. 简单明了的说就是 矩阵中连接起来的字符元素 能完全匹配 字符串
  2. 我们将问题分散思考为 (多个两个字符之间的连接, abc = a 连接 b ,b 再连接 c), 比如:字符串第一个字符与第二个字符的连接,当我们在矩阵中匹配到第一个字符后,将矩阵中该元素 作为 “回溯点”,之后 进一步探索 第二个字符在矩阵元素中的位置 (由回溯点 进行选择)
  3. 第一字符与第二字符配连接,未匹配成功,则退回回溯点,之后重新选择。这样多个两字符之间的连接过程使用递归实现,我们就能完成整个 匹配过程。
  4. 注意点:访问重复是不允许的,我们可以使用 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 的格子),每走一个格子都是作为“回溯点进行选择的”,我们就是统计机器人一步一步走格子并回溯重新选择,到无符合条件的格子中能够走,所能达到的所有格子数。

  1. 走格子的条件判断(是否行坐标和列坐标 的数位之和 小于等于 k 的格子,且未被访问过)
  2. 回溯 累加 格子量,看下图,机器人能走 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) 以深度优先方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。

猜你喜欢

转载自blog.csdn.net/chongzi_daima/article/details/105393856