回溯算法| 豆包MarsCode AI刷题

159 阅读5分钟

在豆包上刷完这么多题之后,我发现豆包没有特意为回溯算法分类,就也很少会想到使用回溯算法,所以我今天就来总结一下回溯算法的特性,以及剖析回溯中最经典的八皇问题。

回溯算法概述

回溯算法是一种通过尝试所有可能的解来解决问题的算法设计方法。它是一种系统性的搜索算法,适合用来解决组合优化问题、排列问题、子集问题等。回溯的核心思想是“探索-回退”,在搜索过程中记录路径,当发现当前路径无法满足条件时,撤销最后一步的选择,回退到上一步继续探索。

回溯算法的特点

  1. 穷举所有可能性: 回溯算法会遍历所有可能的解,确保不会遗漏任何一种情况。
  2. 剪枝优化: 在搜索的过程中,通过条件判断提前终止不符合要求的分支,减少不必要的计算。
  3. 状态恢复: 每次尝试一个可能的解后,在递归返回时撤销当前尝试(即“回退”),以保证其他分支的探索不受影响。
  4. 递归实现: 回溯一般通过递归实现,用递归栈保存搜索的状态和路径。

回溯算法的基本框架

void backtrack(参数列表) {
    // 1. 判断是否满足结束条件,若满足则保存解或返回
    if (满足结束条件) {
        保存解;
        return;
    }

    // 2. 遍历所有可能的选择
    for (选择 : 所有可能的选择) {
        // 3. 做出选择
        做出选择;

        // 4. 递归进入下一层
        backtrack(参数列表);

        // 5. 撤销选择(回溯)
        撤销选择;
    }
}

回溯算法的关键步骤

定义解空间: 确定问题的解空间,即所有可能的候选解的集合。例如:

  1. 子集问题: 解空间是所有子集。
  2. 排列问题: 解空间是数组的全排列。
  3. 图问题: 解空间是所有可能的路径。

递归实现:

按照当前状态递归地探索下一步的可能性,逐步缩小问题规模。

约束条件(剪枝):

在递归的过程中检查是否满足约束条件,不满足则提前返回。

结果收集:

如果当前路径满足问题的解约束,将其保存。

回退:

在返回上一步时撤销当前的选择,恢复状态。

回溯算法的优缺点

优点:

  1. 实现简单: 通过递归和状态恢复,算法逻辑清晰。
  2. 全面性: 可以保证找到所有可能的解。

缺点:

  1. 效率低: 在最坏情况下需要穷举所有解,时间复杂度通常是指数级。
  2. 依赖剪枝: 如果没有有效的剪枝策略,可能会导致不必要的计算,影响性能。

总结

回溯算法适用于问题规模较小、需要找到所有解的场景。为了提升效率,关键是设计有效的剪枝条件以及控制递归状态。熟练掌握回溯框架后,可以快速应用到各种组合、排列、路径等问题中。

八皇问题

概述

八皇后问题是经典的回溯算法问题之一。问题的描述是:在 8×8 的国际象棋棋盘上放置 8 个皇后,使得每个皇后都不能攻击其他任何皇后。皇后可以横向、纵向和对角线攻击,因此解需要满足:

  • 每行只能有一个皇后。
  • 每列只能有一个皇后。
  • 每条对角线只能有一个皇后。

解题思路

八皇后问题可以通过回溯算法来解决。解题的核心思想是:

  1. 按行进行递归,每次尝试在当前行放置一个皇后。
  2. 对于当前行的每个列,检查是否满足放置条件(不与已有皇后冲突)。
  3. 如果满足条件,放置皇后并递归处理下一行。
  4. 如果下一行无法放置,回溯到上一步,撤销皇后的位置,尝试其他可能。

代码实现

#include <vector>
#include <string>
#include <iostream>

using namespace std;

// 全局结果保存
vector<vector<string>> results;

void backtrack(int row, vector<string>& board, vector<bool>& cols, vector<bool>& diag1, vector<bool>& diag2) {
    // 如果放置完成,将棋盘加入结果
    if (row == board.size()) {
        results.push_back(board);
        return;
    }

    int n = board.size();
    for (int col = 0; col < n; col++) {
        // 检查列、主对角线、副对角线是否被占用
        if (cols[col] || diag1[row + col] || diag2[row - col + n - 1]) continue;

        // 做选择
        board[row][col] = 'Q';  // 放置皇后
        cols[col] = true;       // 标记列
        diag1[row + col] = true; // 标记主对角线
        diag2[row - col + n - 1] = true; // 标记副对角线

        // 递归处理下一行
        backtrack(row + 1, board, cols, diag1, diag2);

        // 撤销选择
        board[row][col] = '.';  // 移除皇后
        cols[col] = false;
        diag1[row + col] = false;
        diag2[row - col + n - 1] = false;
    }
}

vector<vector<string>> solveNQueens(int n) {
    // 初始化棋盘
    vector<string> board(n, string(n, '.'));
    vector<bool> cols(n, false);        // 标记列
    vector<bool> diag1(2 * n - 1, false); // 标记主对角线
    vector<bool> diag2(2 * n - 1, false); // 标记副对角线

    // 回溯求解
    backtrack(0, board, cols, diag1, diag2);
    return results;
}

int main() {
    int n = 8; // 八皇后
    vector<vector<string>> solutions = solveNQueens(n);

    // 打印结果
    for (const auto& solution : solutions) {
        for (const auto& row : solution) {
            cout << row << endl;
        }
        cout << endl;
    }

    return 0;
}

拓展

  1. 八皇后是 n=8n=8n=8 的特例,可以扩展到任意大小的棋盘。
  2. 如果只关心有多少种解法,可以省略棋盘保存逻辑,仅统计解的数量。
  3. 可以为问题添加其他约束(如对称性、特定位置)。