算法---回溯算法

一、回溯算法的核心概念

回溯算法(Backtracking)是一种通过深度优先搜索(DFS) 探索所有可能解的暴力搜索策略,其核心思想是:在探索过程中,当发现当前路径无法形成有效解时,撤销上一步的选择并回退到上一状态,继续尝试其他路径

这种思想类似"走迷宫":沿着一条路走到尽头,若不通则退回上一个岔路口,选择另一条路继续尝试。回溯算法通过"尝试-撤销-再尝试"的循环,遍历问题的解空间树(所有可能解构成的树结构),最终找到所有有效解或最优解。

二、回溯算法的基本原理
  1. 解空间树

    问题的所有可能解可抽象为一棵"解空间树":

    • 根节点:初始状态(无任何选择);
    • 中间节点:部分解(已做出部分选择);
    • 叶子节点:完整解(所有选择均已做出)。
      回溯算法通过DFS遍历这棵树,从根节点出发,逐层深入,直至叶子节点(得到解)或判定当前路径无效(剪枝)。
  2. 核心操作

    回溯算法的执行过程可概括为3步:

    • 选择:在当前状态下,从可选选项中选择一个,加入当前解;
    • 递归:基于选择的状态,进入下一层递归,探索更深的路径;
    • 撤销(回溯):递归返回后,撤销上一步的选择,恢复到上一状态,以便尝试其他选项。
三、基本框架与伪代码

回溯算法通常用递归实现,通用框架如下:

cpp 复制代码
// 全局变量:存储所有有效解
vector<vector<int>> result;  
// 全局变量:存储当前路径(部分解)
vector<int> path;  

void backtrack(选择列表, 路径状态) {
    // 终止条件:到达叶子节点(路径构成完整解)
    if (路径满足结束条件) {
        result.push_back(path);  // 记录解
        return;
    }
    // 遍历所有可选选项
    for (选择 : 选择列表) {
        // 剪枝:若当前选择无效,直接跳过(优化关键)
        if (选择不满足约束条件) continue;
        
        // 1. 做出选择
        path.push_back(选择);
        // 2. 递归探索下一层
        backtrack(更新后的选择列表, 更新后的状态);
        // 3. 撤销选择(回溯)
        path.pop_back();
    }
}

关键要点

  • 选择列表:随递归深度动态变化(例如组合问题中,后续选择需排除已选元素);
  • 剪枝条件:根据问题约束设计(例如N皇后中,当前位置与已放皇后冲突则剪枝);
  • 状态恢复:path.pop_back() 是回溯的核心,确保递归返回后状态与进入前一致。
四、适用场景

回溯算法适用于需枚举"所有可能解"或"满足约束条件的解"的问题,典型场景包括:

  1. 组合问题:从集合中选取k个元素(不考虑顺序),如"组合总和";
  2. 排列问题:对集合元素进行全排列(考虑顺序),如"全排列";
  3. 子集问题:求集合的所有子集,如"子集";
  4. 切割问题:将字符串/数组切割为满足条件的子部分,如"分割回文串";
  5. 棋盘问题:满足特定约束的布局,如"N皇后""数独"。
五、经典问题与C++实现示例
示例1:组合问题(LeetCode 77. 组合)

问题:给定两个整数n和k,返回1~n中所有可能的k个数的组合(如n=4, k=2时,解为[[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]])。

思路

  • 解空间树:每层选择一个数,且后选的数必须大于已选的数(避免重复组合,如[1,2]与[2,1]视为同一组合);
  • 剪枝:若剩余可选数字不足k - path.size()个,直接终止(例如当前path已有1个元素,剩余数字需至少1个才能凑齐k=2)。

代码

cpp 复制代码
#include <vector>
using namespace std;

vector<vector<int>> result;
vector<int> path;

void backtrack(int n, int k, int start) {
    // 终止条件:path长度为k
    if (path.size() == k) {
        result.push_back(path);
        return;
    }
    // 遍历可选数字(从start开始,避免重复)
    // 剪枝:i最多到n - (k - path.size()) + 1(剩余数字足够)
    for (int i = start; i <= n - (k - path.size()) + 1; i++) {
        path.push_back(i);       // 选择
        backtrack(n, k, i + 1); // 下一层从i+1开始
        path.pop_back();         // 撤销
    }
}

vector<vector<int>> combine(int n, int k) {
    backtrack(n, k, 1);
    return result;
}
示例2:全排列问题(LeetCode 46. 全排列)

问题:给定不含重复数字的数组,返回所有可能的全排列(如[1,2,3]的全排列为[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]])。

思路

  • 解空间树:每层选择一个未使用的数字(需标记已用元素);
  • 剪枝:若数字已被使用,直接跳过。

代码

cpp 复制代码
#include <vector>
using namespace std;

vector<vector<int>> result;
vector<int> path;

void backtrack(vector<int>& nums, vector<bool>& used) {
    // 终止条件:path长度等于数组长度
    if (path.size() == nums.size()) {
        result.push_back(path);
        return;
    }
    // 遍历所有数字
    for (int i = 0; i < nums.size(); i++) {
        if (used[i]) continue; // 剪枝:跳过已使用的数字
        
        used[i] = true;        // 标记为使用
        path.push_back(nums[i]);// 选择
        backtrack(nums, used); // 递归
        path.pop_back();        // 撤销选择
        used[i] = false;       // 恢复标记
    }
}

vector<vector<int>> permute(vector<int>& nums) {
    vector<bool> used(nums.size(), false);
    backtrack(nums, used);
    return result;
}
示例3:N皇后问题(LeetCode 51. N皇后)

问题:在n×n的棋盘上放置n个皇后,使它们不能互相攻击(同一行、列、对角线无多个皇后),返回所有可行布局。

思路

  • 解空间树:每行放一个皇后,列号作为选择(一行一个,避免行冲突);
  • 剪枝:检查当前列和对角线是否已有皇后(列冲突:同一列;对角线冲突:行差=列差)。

代码

cpp 复制代码
#include <vector>
#include <string>
using namespace std;

vector<vector<string>> result;

// 检查当前位置(row, col)是否合法
bool isValid(int row, int col, vector<int>& queens) {
    for (int i = 0; i < row; i++) {
        // 列冲突 或 对角线冲突(行差=列差)
        if (queens[i] == col || abs(row - i) == abs(col - queens[i])) {
            return false;
        }
    }
    return true;
}

void backtrack(int n, int row, vector<int>& queens) {
    // 终止条件:所有行都放置了皇后
    if (row == n) {
        // 生成棋盘字符串
        vector<string> board;
        for (int col : queens) {
            string s(n, '.');
            s[col] = 'Q';
            board.push_back(s);
        }
        result.push_back(board);
        return;
    }
    // 尝试当前行的每一列
    for (int col = 0; col < n; col++) {
        if (!isValid(row, col, queens)) continue; // 剪枝:冲突则跳过
        
        queens[row] = col;      // 放置皇后(记录列号)
        backtrack(n, row + 1, queens); // 下一行
        queens[row] = -1;       // 撤销(可省略,因后续会覆盖)
    }
}

vector<vector<string>> solveNQueens(int n) {
    vector<int> queens(n, -1); // 记录每行皇后的列号
    backtrack(n, 0, queens);
    return result;
}
六、剪枝策略(优化核心)

回溯算法的效率取决于剪枝的有效性,常见剪枝方式:

  1. 可行性剪枝:若当前选择违反问题约束(如N皇后的冲突),直接终止当前路径;
  2. 最优性剪枝:若问题求最优解(如旅行商问题),当前路径代价已超过已知最优解,无需继续探索;
  3. 重复性剪枝:通过排序+跳过重复元素(如全排列II中处理重复数字),避免生成重复解。
七、时间与空间复杂度
  • 时间复杂度:取决于解空间树的规模,通常为O(N!)(排列问题)或O(2ⁿ)(子集问题),剪枝可降低实际运行时间,但理论复杂度不变;
  • 空间复杂度:主要为递归栈深度(O(N))和存储解的空间(O(K),K为解的总数)。

回溯算法是解决"枚举所有可能解"问题的通用框架,其核心是"选择-递归-撤销"的循环和有效的剪枝策略。掌握回溯算法需理解解空间树的结构,针对不同问题设计合理的选择列表、终止条件和剪枝规则。

相关推荐
star _chen1 小时前
C++实现完美洗牌算法
开发语言·c++·算法
hzxxxxxxx1 小时前
1234567
算法
Sylvia-girl2 小时前
数据结构之复杂度
数据结构·算法
CQ_YM2 小时前
数据结构之队列
c语言·数据结构·算法·
VekiSon2 小时前
数据结构与算法——树和哈希表
数据结构·算法
大江东去浪淘尽千古风流人物3 小时前
【DSP】向量化操作的误差来源分析及其经典解决方案
linux·运维·人工智能·算法·vr·dsp开发·mr
Unstoppable224 小时前
代码随想录算法训练营第 56 天 | 拓扑排序精讲、Dijkstra(朴素版)精讲
java·数据结构·算法·
饕餮怪程序猿4 小时前
A*算法(C++实现)
开发语言·c++·算法
电饭叔4 小时前
不含Luhn算法《python语言程序设计》2018版--第8章14题利用字符串输入作为一个信用卡号之二(识别卡号有效)
java·python·算法