回溯算法(Backtracking)说明与实例

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/leoleocs/article/details/47311283

定义

回溯算法(Backtracking)在很多场景中会使用,如N皇后,数迷,集合等,其是暴力求解的一种优化。参考https://en.wikipedia.org/wiki/Backtracking 中的说明,定义如下:

Backtracking is a general algorithm for finding all (or some) solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons each partial candidate c (“backtracks”) as soon as it determines that c cannot possibly be completed to a valid solution

从上文中可以得出, 核心的含义是 回溯算法是通过一步一步(通常是用递归)构建可能”解”,并且回溯不可能”解”来求所有或者部分解决方案的通用算法。其中“回溯”的具体意思就是将不可能解或者部分解的候选尽早的舍弃掉,“解”需要满足一定的限制条件(constraint satisfaction)

通用算法

这里介绍一下回溯算法的通用思想,一般来讲,会设置一个递归函数,函数的参数会携带一些当前的可能解的信息,根据这些参数得出可能解或者不可能而回溯。
参考 http://web.cse.ohio-state.edu/~gurari/course/cis680/cis680Ch19.html 这里面有一个通用的算法

ALGORITHM try(v1,...,vi)  // 这里的V1.....V2携带的参数说明 “可能解”  
   // 入口处验证是否是全局解,如果是,直接返回。 
   // 实际编程中也需要查看是否是无效解,如果是,也是直接返回
   IF (v1,...,vi) is a solution THEN RETURN (v1,...,vi)  
   FOR each v DO  // 对于每一个可能的解,进行查看
      // 下面的含义是形成一个可能解 进行递归
      IF (v1,...,vi,v) is acceptable vector  THEN 
        sol = try(v1,...,vi,v) 
        IF sol != () THEN RETURN sol 
        // 这个地方其实需要增加“回溯” 处理,实际编程中通常是函数参数的变化
      END 
   END 
   RETURN () 

经典算法

  • N皇后问题

这个是一个比较经典的问题, 意思是将N个“皇后”放在N*N的棋盘上,每个皇后不能再同一列,同一行,和斜对角 (这个就是限制条件constraint satisfaction)。 下面是回溯算法的需求所有的可能解。
算法基本的步骤思想为:
1)从第一行开始
2 )如果所有的皇后已经放置完成, 生成解,并且返回true
3)尝试当前行的所有列,如果当前行与列是合法的
3.1 修改棋盘让其成为部分解,
3.2 然后递归查看(主要是2, 3,4)该解是否合法
3.3 Backtrack 棋盘进行回溯
4) 如果上述所有的组合都为非法,返回false

    // 寻找N皇后问题的可能解, 我们用 '.' 表示不放置皇后,用'Q'表示放置皇后
    // 利用vector<string> 表示一个解决方案, vector<vector<string>> 表示                                 
    // 所有的解决方案。
    // 在递归初,我们可以生成一个待解的棋盘
    vector<vector<string>> solveNQueens(int n) {
        string tmp (n, '.');
        //生成一个N*N待解的棋盘,没有任何皇后
        vector<string> broad (n, tmp);
        nQueue = n;
        vector<vector<string>> ans;
        solveNQueensHelper (ans, broad, 0, nQueue );
        return ans;
    }
    // 回溯算法的递归函数,
    bool solveNQueensHelper (vector<vector<string>>& ans, vector<string> &broad, int row, int nQueue)
    {
         // 如果当前的行数大于或者等于皇后数,说明当前棋盘是一个解
         // 直接返回
        if(row >= nQueue){
            ans.push_back(broad);
            return true;
        }
        // 从当前行的列中选取一个可能解
        for (int column = 0; column < nQueue; column++){
            // 查看一下,当前可能解是否有效,只有有效,才可能继续递归
            if(isOk (broad, row, column))
            {
                // 有效,修改可能解的棋盘
                broad[row][column] = 'Q';
                // 递归调用是否可能解
                if (solveNQueensHelper (ans, broad, row + 1, nQueue)){
                   // return true;
                }
                // 回溯, 去生成其他解
                broad[row][column] = '.';
            }
        }

        return false;
    }
    // 查看当前部分解是否有效
    bool isOk (const vector<string> &broad, int row, int column){
        // 查看
        for (int i = 0; i <= row; i++)
        {
            if(broad [i] [column] == 'Q'){
                return false;
            }
        }

        int tmpRow = row;
        int tmpColumn = column;
        while (tmpRow >= 0 && tmpColumn >= 0)
        {
            if(broad [tmpRow] [tmpColumn] == 'Q'){
                return false;
            }
            tmpRow--,
            tmpColumn--;
        }

        tmpRow = row;
        tmpColumn = column;
        while (tmpRow >= 0 && tmpColumn < nQueue)
        {
            if(broad [tmpRow] [tmpColumn] == 'Q'){
                return false;
            }
            tmpRow--,
            tmpColumn++;
        }
        return true;    
    }
};

其要求,1-9的数不能出现在同一列,同一行,或者同一个3*3的格子中。

算法思想如下:
1 )找到一个空格子,如果找不到, 说明已经填满,这是一个解,返回true
2 )从1 - 9 的数字中填上找到的空格子,如果合法
2.1 生成部分解,递归调用 1)2) 3)
2.2 如果递归调用返回true,说明合法
2.3 回溯部分解
3)上述都不成,直接返回false

c++的代码实现如下:

    // board 是一个9*9的格子,如果空格子,用'.' 表示,否则为1-9的数字
    void solveSudoku(vector<vector<char>>& board) {    
        // 递归调用该函数
        helperSolveSudoku (board);
    }

    bool helperSolveSudoku(vector<vector<char>>& board) {   
      int row = 0;
      int colum = 0;
      // step1, 获取一个空格, 如果没找到,说明已经填满。
      if ( !GetNextUnsignedOne (board, row, colum) )
         return true;
       // Step2: 从1-9 分别填上棋盘
      for (char num = '1'; num <= '9'; num++ ){
         // 查看当前的数字放入棋盘中是否合法
         if (isNumOk (board, row, colum, num) ){
            // step2.1 生成部分解,递归调用
            board[row][colum] = num;
            if (helperSolveSudoku (board) )
            {
               // 如果解合法,直接返回 
               return true;
            }
            // 回溯部分解
            board[row][colum] = '.';
         }
      }
      // step3 没有解,直接返回false
      return false;
    }
    // 帮助函数以获取一个可能的空位置 
    bool GetNextUnsignedOne(const vector<vector<char>>& board, int &row, int& column){
        for(int i = 0; i < 9; i++){
            for(int j = 0; j < 9; j++){
                if (board [i] [j] == '.'){
                    row = i;
                    column = j;
                    return true;
                }
            }
        }
        return false;
    }

    // 帮助函数以验证数字在当前是否合法
    bool isNumOk (const vector<vector<char>>& board, int row, int column, char num){
        // check the row is ok or not
        for(int i = 0; i < 9; i++){
            if(board [row] [i] == num){
                return false;
            }
        }
        // check the colum is ok or not
        for(int i = 0; i < 9; i++){
            if(board [i] [column] == num){
                return false;
            }
        }

        int startRow = row - row % 3;
        int startcolumn = column - column %3;     
        // check the 3*3 inbox is ok for not.
        for (int i = 0; i < 3; i++){
            for(int j = 0; j < 3; j++)
            {
                if(board [startRow + i] [startcolumn + j] == num){
                    return false;
                }
            }
        } 
        return true;   
    }
  • 集合问题
    有一个数组,产生一个集合,该集合能包含所有的元素,如 【1,2,3】数组可以满足的集合就是【1,2,3】【1,3,2】【2,1,3】【2,3,1】【3,1,2】【3,2,1】.
    这个也可以用回溯的方式实现
    基本算法思想为:
    1) 查看当前的集合,是否满足所有元素的集合,如果满足,返回
    2) 从数组挑选一个元素,查看当前的元素是否属于部分解
    2.1 如果不属于部分解,将元素加入到部分解
    2.2 递归调用 1, 2, 3
    2.3 回溯该元素
    3 数组元素完成,直接返回
    具体实现的C++代码如下:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> ans;
        vector<int> tmpRes;
        permuteHelper (ans, tmpRes, nums);
        return ans;

    }
    // 帮助函数以实现数据集合功能
    // ans 表示几个数组
    // tmpRes 表示部分解
    // nums 表示的当前数组元素个数
    void permuteHelper(vector<vector<int>>& ans, vector<int>&tmpRes, vector<int>& nums){
       // step1 如果部分解的元素个数和数字的元素相同,直接插入
       if(tmpRes.size () == nums.size ()){
           ans.push_back (tmpRes);
           return;
       }  
        //step2 从集合中选定一个元素     
        for(int i = 0; i < nums.size(); i++) 
        {
            if(std::find (tmpRes.begin(), tmpRes.end(), nums [i]) == tmpRes.end()){
                //step 2.1 如果部分解不包含该元素,
                // 其实也可以用一个set<int> 来判定是否含有该元素。
                tmpRes.push_back (nums [i]);
                //step 2.2 递归调用
                permuteHelper (ans, tmpRes, nums);
                // 回溯部分解
                tmpRes.pop_back();
            }
            else{
                continue;
            }
        }
        // Step3,完成,直接返回
        return;
    }
展开阅读全文

没有更多推荐了,返回首页