[算法数据结构] 回溯算法的难题分析

257 阅读2分钟

这里记录二刷代码随想录的回溯部分,9.4号到9.8号,耗时5天。主要有以下几类问题:组合问题、分割问题、子集问题、排列问题和棋盘问题。其中基础的题型一般是不用去重的,在选择子节点上没有太多要求的,难题相反。

  • 行程问题 问题十分抽象。如何选择行程,如何转换成回溯问题?这是我没怎么想明白的地方。和之前题目不同的是,这里的待选项比较复杂,是一个字符串“出发地,目的地”。我们该怎么选择?首先是要解决的一个问题是,如何构建映射关系?(其实这一步一开始也没想到,这一步的目的是什么?)
unordered_map<string, map<string, int>> targets;

unordered_map<出发地,<到达地,航班次数>> 为什么这么构造?把出发地当成键值的原因在于,在每一次选择下一个目的地的时候,我们是根据当前的出发地来选择的。之所以要加上航班次数,是要确保一张票飞一次。 映射关系的构建:

for (const vector<string>& vec : tickets) {
    targets[vec[0]][vec[1]]++; // 记录映射关系
}

如何进行选择?和之前选择数不同,这里的选择比较复杂:

for (pair<const string, int>& target : targets[result[result.size() - 1]])

result[result.size() - 1] 表示的是结果的最后一个,也就是上一层的终点,作为起点。这里的选择的逻辑是,遍历键所对应的值,含义是对应当前这个地方所能去的目的地。没有这种思维,一开始遇到这个题还是有点儿难的,难在不晓得改用什么数据结构能解决这个问题。

unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result) {
    if (ticketNum + 1 == result.size()) {
        return true;
    }

    for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
        if (target.second > 0) {
            result.push_back(target.first);
            target.second--;
            if (backtracking(ticketNum, result)) return true;
            result.pop_back();
            target.second++;
        }
    }
    return false;
}
  • N皇后

N皇后问题可以将其规律总结为棋盘问题。但是相较于数独问题而言,其要判断的逻辑相对简单一些。 单层逻辑:

for (int col = 0; col < n; col++) {
    if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
        chessboard[row][col] = 'Q'; // 放置皇后
        backtracking(n, row + 1, chessboard);
        chessboard[row][col] = '.'; // 回溯,撤销皇后
    }
}

类似的,多了一个判断的语句,如果符合要求,就放皇后,不符合要求跳过。这里假如n=3时,返回的result是一个空的,因为row != n,无法取到n。因为只有在当前层(棋盘的一行)合理时,才会row+1传给回溯函数,然后再进行一个判断,判断row == n时,才会把chessboard放进result。

  • 数独

      bool backtracking(vector<vector<char>>& board) {
          for (int i = 0; i < board.size(); i++) {
              for (int j = 0; j < board.size(); j++) {
                  if (board[i][j] != '.') continue;
                  for (char k = '1'; k <= '9'; k++) {
                      if (isValid(i, j, k, board)) {
                          board[i][j] = k;
                          if (backtracking(board)) return true;
                          board[i][j] = '.';
                      }
                  }
                  return false;
              }
          }
          return true;
      }
    

数独问题难在需要判断行和列,因此需要双重遍历来进行判断,符合要求的对board进行赋值。和安排行程类似的是,回溯函数返回的bool类型的值,if(backtracking(board)) return true; // 行程问题中的语句是if (backtracking(ticketNum, result)) return true;