欢迎光临
我们一直在努力

回溯算法(Backtracking)

更加完整详细内容可查看【免费版Java学习笔记】和【免费版Java面试题】

免费版Java学习笔记(28w字)链接:https://www.yuque.com/aoyouaoyou/sgcqr8 免费版Java面试题(20w字)链接:https://www.yuque.com/aoyouaoyou/wh3hto 完整版Java学习笔记200w字,附有代码实现,图解清楚,仅需9.9 完整版Java面试题,150w字,高频面试题,内容详细,仅需9.9 完整版链接: https://www.xiaohongshu.com/user/profile/63c2d512000000002601232c 祝您新的一年事事马到成功,身体健康,阖家幸福,大展宏图!

一、回溯算法介绍

1.1 算法定义

回溯算法是一种带“回退”的深度优先搜索枚举算法,是在问题求解的多阶段尝试过程中,从一条路径探索解,当发现当前路径不满足求解条件时,就回溯(递归返回)到上一个决策点,放弃当前路径并尝试其他可能的路径,直到找到满足要求的解或枚举完所有可能。

简单来说,回溯的解题逻辑是:一路向前试,走不通就回头,换条路再试,像走迷宫时遇到死胡同,退回最近的岔路口选另一个方向。

1.2 思想:多阶段决策+回退剪枝

回溯把问题求解拆分为多个有序的决策阶段,每个阶段面对多个选择,操作可概括为:

  • 尝试:在当前阶段选择一个路径,继续向下一阶段探索;
  • 判断:探索过程中检查是否满足解的条件,若满足则记录解/继续探索,若不满足则触发回溯;
  • 回溯:放弃当前阶段的选择,回到上一阶段,尝试该阶段的下一个选择;
  • 剪枝(优化技巧):在尝试前预判当前路径必然无法得到解,直接跳过该路径的所有后续探索,减少无效枚举,提升效率。
  • 1.3 与深度优先搜索(DFS)的关系

    回溯是特殊的DFS,二者都基于深度优先的探索逻辑,但回溯多了“回退”和“剪枝”的特性:

    • DFS:仅做深度优先的遍历,不关注路径是否符合解的条件,遍历完成即结束;
    • 回溯:以DFS为基础,遍历中加入条件判断,不符合则回退,还可通过剪枝减少遍历次数,目标是找到符合要求的解而非单纯遍历。

    此处为语雀内容卡片,点击链接查看:https://www.yuque.com/aoyouaoyou/pbz18g/riiovgtq5us3og5c

    二、回溯算法的经典问题:N皇后问题

    N皇后问题是回溯算法的经典标杆问题,能完美体现“多阶段决策+尝试+回溯+剪枝”的逻辑。

    2.1 问题描述

    在n×n的棋盘上放置n个皇后,要求皇后彼此之间不能相互攻击,即任意两个皇后不能出现在同一行、同一列、同一对角线上,求所有合法的放置方案。

    • 规则:皇后的攻击范围是所在行、列、左上/右下对角线;
    • 阶段划分:将问题拆分为n个阶段,依次在第1行、第2行……第n行放置皇后(每行仅放1个,天然避免同一行攻击,简化问题);
    • 决策选择:每个阶段(第row行)的选择是在第0列到第n-1列中选一个列col放置皇后;
    • 条件判断:放置前检查第row行col列是否与已放置的皇后冲突(不同列、不同对角线)。

    2.2 解题思路

  • 阶段定义:用行做阶段,row从0到n-1,依次处理每一行的皇后放置;
  • 存储方案:用数组result[]记录放置位置,result[row] = col表示“第row行的皇后放在第col列”,下标天然对应行,避免同一行冲突;
  • 尝试选择:对当前行row,遍历列col(0到n-1),依次尝试在col列放置皇后;
  • 冲突判断:检查当前col列是否与已放置的皇后(0到row-1行)冲突(同列、左上对角线、右下对角线);
  • 递归探索:若当前位置合法,记录result[row] = col,递归处理下一行(row+1);
  • 回溯触发:若当前行的所有列都尝试完毕仍无合法位置,递归返回(回溯)到上一行,尝试上一行的下一个列;
  • 解的记录:当row == n时,说明n个皇后已全部合法放置,得到一个完整方案,打印输出。
  • 2.3 冲突判断逻辑(关键)

    对第row行col列,只需向上检查已放置的0~row-1行(后续行未放置,无需检查),判断三个维度:

  • 同列冲突:存在某一行i(i < row),使得result[i] == col;
  • 左上对角线冲突:存在某一行i(i < row),使得result[i] == col – (row – i)(列随行走左,差值为行差);
  • 右下对角线冲突:存在某一行i(i < row),使得result[i] == col + (row – i)(列随行走右,和值为行差+当前列)。
  • 2.4 完整代码实现(支持自定义皇后数)

    代码优点:① 皇后数可自定义(通过常量配置);② 简化冲突判断逻辑;③ 增加方案计数:

    /**
    * 回溯算法:N皇后问题(优化版,支持自定义皇后数)
    * 行作为阶段,列作为选择,冲突判断后递归,无效则回溯
    */
    public class AoyouBacktrackingNQueens {
    // 自定义皇后数(示例:8皇后/4皇后,可修改)
    public static final int QUEENS = 8;
    // 存储皇后位置:result[row] = col → 第row行皇后放在第col列
    private int[] result = new int[QUEENS];
    // 统计合法方案数
    private static int solutionCount = 0;

    /**
    * 回溯方法:在第row行放置皇后(多阶段决策的)
    * @param row 当前处理的行(阶段)
    */
    public void setQueens(int row) {
    // 递归终止条件:row == QUEENS,说明所有皇后已合法放置,找到一个解
    if (row == QUEENS) {
    solutionCount++;
    printQueens(); // 打印当前合法方案
    return;
    }

    // 遍历当前行的所有列(当前阶段的所有选择)
    for (int col = 0; col < QUEENS; col++) {
    // 剪枝:判断当前row行col列是否可放置(无冲突则尝试,有冲突则跳过该列)
    if (isOk(row, col)) {
    result[row] = col; // 记录当前选择:第row行皇后放在col列
    setQueens(row + 1); // 进入下一个阶段:处理下一行
    // 回溯操作(隐式):递归返回后,自动尝试当前行的下一个col,无需额外代码
    }
    }
    // 若当前行所有列都尝试完毕仍无合法位置,递归返回(回溯到上一行)
    }

    /**
    * 辅助方法:判断第row行col列是否可放置皇后(冲突判断+剪枝)
    * @param row 当前行
    * @param col 当前列
    * @return true=可放置,false=不可放置
    */
    private boolean isOk(int row, int col) {
    // 向上遍历已放置的所有行(0 ~ row-1),检查冲突
    for (int i = 0; i < row; i++) {
    // 1. 同列冲突:已放置的皇后在当前列
    if (result[i] == col) {
    return false;
    }
    // 2. 对角线冲突:行差的绝对值 == 列差的绝对值(统一判断左上/右下对角线)
    if (Math.abs(row – i) == Math.abs(col – result[i])) {
    return false;
    }
    }
    // 无冲突,可放置
    return true;
    }

    /**
    * 辅助方法:打印当前合法的皇后放置方案
    */
    private void printQueens() {
    System.out.println("========== 合法方案" + solutionCount + " ==========");
    for (int row = 0; row < QUEENS; row++) {
    for (int col = 0; col < QUEENS; col++) {
    if (result[row] == col) {
    System.out.print("Q | "); // 皇后位置
    } else {
    System.out.print("* | "); // 空位置
    }
    }
    System.out.println(); // 换行到下一行
    }
    }

    // 测试主方法(示例:8皇后/4皇后,修改QUEENS常量即可切换)
    public static void main(String[] args) {
    AoyouBacktrackingNQueens nQueens = new AoyouBacktrackingNQueens();
    nQueens.setQueens(0); // 从第0行开始放置皇后
    System.out.println("=====================================");
    System.out.println(QUEENS + "皇后问题的总合法方案数:" + solutionCount);
    }
    }

    2.5 运行结果(以4皇后为例,QUEENS=4)

    4皇后问题共有2个合法方案,运行结果如下:

    ========== 合法方案1 ==========
    * | Q | * | * |
    * | * | * | Q |
    Q | * | * | * |
    * | * | Q | * |
    ========== 合法方案2 ==========
    * | * | Q | * |
    Q | * | * | * |
    * | * | * | Q |
    * | Q | * | * |
    =====================================
    4皇后问题的总合法方案数:2

    (8皇后问题共有92个合法方案,运行代码可完整打印所有方案)

    2.6 时间复杂度分析

    N皇后问题的时间复杂度为指数级别,这是回溯算法的典型特征:

    • 最坏情况:每个行有n个列选择,共n行,时间复杂度为;
    • 实际情况:通过冲突判断剪枝,大部分路径会被提前终止,实际时间复杂度远低于,但仍为指数级(如8皇后仅需枚举约1.5万次,而非次)。

    结论:回溯算法的时间复杂度通常为

    (k为每个阶段的选择数),属于暴力枚举的范畴,仅适合解决小规模数据问题。

    三、优缺点

    3.1 优点

  • 思想简单,易实现:逻辑是“尝试-判断-回溯”,无需复杂的数学推导,适合用递归实现,代码结构规整,易读易维护;
  • 通用性极强(万金油):几乎所有能用动态规划、贪心解决的问题,都能用水回溯算法解决,尤其适合求解组合、排列、子集等搜索类问题;
  • 能找到所有解:不仅能找到一个合法解,还能枚举所有满足条件的解,并从中筛选最优解(贪心/动态规划通常仅能找到一个最优解);
  • 可通过剪枝优化:通过预判无效路径,剪枝能大幅减少无效枚举,提升算法效率(剪枝是回溯算法的优化手段)。
  • 3.2 缺点

  • 时间复杂度极高:本质是暴力枚举,时间复杂度为指数级别(),处理大规模数据时执行效率极低,甚至无法运行;
  • 空间复杂度较高:递归实现的回溯会占用方法调用栈的空间(栈深度为阶段数),若阶段数过多(如n=100),可能导致栈溢出;
  • 存在重复计算:未对已求解的子问题做记录,若不同路径经过同一子问题,会重复计算(动态规划通过“备忘录”解决此问题)。
  • 四、适用场景

    回溯算法是“万金油”,但因时间复杂度限制,主要适用于小规模数据的搜索/枚举类问题,尤其适合解决需要找到所有解/最优解的场景,经典适用场景可分为4类:

    4.1 排列组合类问题

    从一组数据中选择若干元素,求所有满足条件的排列/组合/子集,如:

    • 全排列问题:求1~n的所有排列方式;
    • 组合求和:从数组中选若干元素,和为目标值的所有组合;
    • 子集问题:求一个数组的所有子集。

    4.2 棋盘类问题

    基于棋盘的多阶段决策问题,如:

    • N皇后问题(经典);
    • 数独问题:填充数独棋盘,满足行、列、3×3宫格无重复数字;
    • 马踏棋盘:马从棋盘某点出发,走遍所有格子且仅走一次。

    4.3 路径探索类问题

    寻找符合条件的路径,如:

    • 迷宫问题:从迷宫起点到终点的所有合法路径;
    • 单词搜索:在二维字符网格中寻找指定单词的所有路径。

    4.4 优化选择类问题

    从所有可能的解中筛选最优解(小规模数据),如:

    • 0-1背包问题(小规模n):求背包能装的最大价值(大规模需用动态规划);
    • 旅行商问题(小规模城市数):求访问所有城市的最短路径。

    适用原则:当问题的数据规模小(n≤20),且需要找到所有解/最优解时,优先选择回溯算法;若数据规模大,则需考虑动态规划/贪心。

    五、回溯算法与动态规划/贪心的对比

    回溯、动态规划、贪心都是解决多阶段决策问题的常用算法,但三者的解题思路、效率、适用场景差异极大,从5个维度对比,快速区分选型:

    对比维度

    回溯算法

    动态规划

    贪心算法

    思想

    暴力枚举+深度优先+回退剪枝,尝试所有路径

    拆分子问题+记录子问题解(备忘录),避免重复计算

    每一步选局部最优,无回退,试图推全局最优

    时间复杂度

    指数级

    ,仅适合小规模数据

    多项式级

    ,适合中大规模数据

    线性/对数级

    ,效率最高

    解的特性

    能找到所有满足条件的解,可筛选最优解

    仅能找到一个最优解(部分场景可找所有解)

    仅能找到一个解(局部最优,未必全局最优)

    子问题处理

    子问题不记录结果,存在重复计算

    记录子问题最优解(备忘录/DP表),避免重复计算

    无显式子问题,仅做局部最优选择

    优势

    通用性强、能找所有解、实现简单

    效率高、适合大规模数据、无重复计算

    效率极高、实现最简单

    劣势

    效率极低、仅适合小规模数据

    需推导状态转移方程、实现较复杂

    适用场景极少、仅特殊问题能推全局最优

    六、实现技巧

    6.1 明确阶段与选择

    将问题拆分为有序的多阶段,明确每个阶段的所有可选路径,这是回溯的基础(如N皇后以“行”为阶段,以“列”为选择)。

    6.2 巧用递归实现回退

    回溯的回退操作无需额外代码,递归的“栈帧”会天然保存上一阶段的状态:递归进入下一个阶段时,当前阶段的选择会被保存;递归返回时,自动回到上一阶段,尝试下一个选择(如N皇后中,setQueens(row+1)返回后,自动遍历当前行的下一个col)。

    6.3 剪枝优化

    剪枝是提升回溯效率的关键,是在尝试路径前预判无效,直接跳过该路径的所有后续探索,常见剪枝方式:

    • 条件剪枝:如N皇后的冲突判断,提前跳过无法放置的列;
    • 边界剪枝:如组合求和中,当前和已超过目标值,直接跳过后续选择;
    • 有序剪枝:将数据排序后,跳过重复的选择,避免生成重复解。

    七、总结

  • 回溯算法的是带回退的深度优先枚举,逻辑为“尝试-判断-回溯-剪枝”,走不通就退回上一阶段换选择,是特殊的DFS;
  • 回溯把问题拆分为多阶段决策,N皇后问题是经典实现:以“行”为阶段,以“列”为选择,冲突判断做剪枝,递归实现天然回退;
  • 回溯的时间复杂度为指数级,仅适合小规模数据(n≤20),但可通过剪枝大幅减少无效枚举;
  • 回溯是“万金油”算法,通用性极强,能找到所有满足条件的解,这是动态规划/贪心不具备的特性;
  • 回溯、动态规划、贪心的区别:回溯暴力枚举所有路径,动态规划记录子问题解避免重复计算,贪心选局部最优无回退;
  • 回溯的实现技巧:明确阶段与选择、用递归实现天然回退、重点做剪枝优化,这三点能让代码更简洁、效率更高。
  • 赞(0)
    未经允许不得转载:171主机测评 » 回溯算法(Backtracking)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址