Files
final-exam/算法设计与分析/复习文档/回溯法.md
T

8.4 KiB
Raw Blame History

tags, create time
tags create time
算法设计与分析
复习
回溯法
2026-06-10 11:11

回溯法

概述

回溯法(Backtracking)是一种在解空间树中进行深度优先搜索的算法策略。它系统地搜索问题的解空间,当发现当前路径不可能得到解时,回退到上一步尝试其他路径。回溯法是求解NP-hard问题(如图着色、N皇后)的常用方法。

正文

一、回溯法基本思想

回溯法的核心思想是:在解空间树中按 DFS(深度优先搜索) 的方式搜索,当发现某个分支不满足约束条件时,剪枝——跳过该分支,回退到父节点尝试其他分支。

[!warning] 回溯法使用DFS实现,不是队列 回溯法是沿着一条路径尽可能深地搜索,走不通就回退。它使用栈(或递归调用栈)实现DFS,而不是使用队列(BFS)。

二、解空间结构

回溯法的解空间是一棵树(不是图),常见的解空间结构有两种:

  1. 子集树(Subset Tree):用于子集选择问题。每个节点表示是否选择某个元素,共 2ⁿ 个叶节点。
  2. 排列树(Permutation Tree):用于排列问题。每个节点表示排列中的某个位置放哪个元素,共 n! 个叶节点。
flowchart TD
    Root["Root (empty)"]
    L1_1["Include item 1"]
    L1_2["Exclude item 1"]
    L2_1["Include item 2"]
    L2_2["Exclude item 2"]
    L2_3["Include item 2"]
    L2_4["Exclude item 2"]
    Leaf1["{1,2}"]
    Leaf2["{1}"]
    Leaf3["{2}"]
    Leaf4["{}"]

    Root --> L1_1
    Root --> L1_2
    L1_1 --> L2_1
    L1_1 --> L2_2
    L1_2 --> L2_3
    L1_2 --> L2_4
    L2_1 --> Leaf1
    L2_2 --> Leaf2
    L2_3 --> Leaf3
    L2_4 --> Leaf4

[!tip] 子集树与排列树 子集树有 2ⁿ 个叶节点,每个元素有"选/不选"两种选择。排列树有 n! 个叶节点,表示所有可能的排列方式。

三、剪枝函数

剪枝是回溯法效率的关键。两类剪枝函数:

  1. 约束函数(Constraint Function):可行性剪枝。判断当前部分解是否满足约束条件,不满足则剪掉。
  2. 限界函数(Bound Function):最优性剪枝。判断当前路径是否可能到达最优解,不可能则剪掉。

[!question] 回溯法的剪枝函数包括哪两类? 约束函数(可行性剪枝)和限界函数(最优性剪枝)。约束函数判断当前解是否合法;限界函数判断当前路径是否有希望得到最优解。两类函数共同作用,大幅减少搜索空间。

四、节点分类

在回溯搜索过程中,节点分为三类:

节点类型 定义 类比
活结点(Live Node) 正在被扩展、还有未探索子节点的节点 栈中正在处理的节点
死结点(Dead Node) 已被扩展完毕,所有子节点都已探索 已经从栈中弹出的节点
扩展结点(E-Node) 当前正在生成子节点的节点 栈顶正在处理的节点

五、回溯法一般框架

void backtrack(int t) {
    if (t > n)
        output(x);             // 到达叶节点,输出解
    else {
        for (int i = f(n,t); i <= g(n,t); i++) {
            x[t] = h(i);       // 第t个位置尝试第i种选择
            if (constraint(t) && bound(t))
                backtrack(t+1); // 满足约束,继续深入
            // 否则剪枝,回退(x[t]会被下次循环覆盖)
        }
    }
}

各部分含义:

  • t:当前搜索深度(第几个决策)
  • f(n,t) 和 g(n,t):第 t 个位置可选值的范围
  • h(i):将第 i 个候选值赋给 x[t]
  • constraint(t):约束函数,判断可行性
  • bound(t):限界函数,判断是否有希望得到最优解
  • output(x):输出一个完整的解

六、经典应用

6.1 图着色/四色问题

问题:给定一个无向图 G 和 m 种颜色,用最少的颜色给图的每个顶点着色,使得相邻顶点颜色不同。当 m=4 时即为著名的四色问题。

回溯策略:

  • 对每个顶点,尝试 m 种颜色
  • 检查当前着色是否与相邻顶点冲突
  • 冲突则剪枝,换下一种颜色
  • 所有顶点着色完成则输出解

时间复杂度:最坏 O(mⁿ),四色问题为 O(4ⁿ)

function graphColoring(graph, m, colors, vertex):
    if vertex == n:
        printSolution(colors)    // 所有顶点着色完成
        return

    for c = 1 to m:
        colors[vertex] = c       // 尝试颜色c
        if isSafe(graph, colors, vertex):
            graphColoring(graph, m, colors, vertex + 1)
        colors[vertex] = 0       // 回溯

function isSafe(graph, colors, vertex):
    for each neighbor v of vertex:
        if colors[v] == colors[vertex]:
            return false
    return true
flowchart TD
    V1c1["V1: color 1"]
    V1c2["V1: color 2"]
    V2c1["V2: color 1"]
    V2c2["V2: color 2"]
    V2c3["V2: color 3"]
    V2fail1["V2: color 1 = CONFLICT"]
    V3c1["V3: color 1"]
    V3c2["V3: color 2"]
    V3fail1["V3: color 1 = CONFLICT"]
    V3fail2["V3: color 2 = CONFLICT"]
    Solution["Solution: V1=1, V2=2, V3=3"]

    V1c1 --> V2fail1
    V1c1 --> V2c2
    V1c2 --> V2c1
    V1c2 --> V2c3
    V2fail1 --> |"Pruned"| V1c1
    V2c2 --> V3fail2
    V2c2 --> V3c1
    V2c1 --> V3fail1
    V2c1 --> V3c2
    V3c1 --> Solution

解释:以三节点图为例(V1-V2, V2-V3, V1-V3),先给 V1 尝试颜色1,然后给 V2 尝试颜色1(与V1冲突,剪枝),再试颜色2(可行)。接着给 V3 尝试颜色1(与V1冲突),颜色2(与V2冲突),颜色3(可行)。得到解 V1=1, V2=2, V3=3。

6.2 N皇后问题

问题:在 N×N 的棋盘上放置 N 个皇后,使得任意两个皇后不在同一行、同一列或同一对角线上。

回溯策略:

  • 每行放一个皇后(一行一个,所以只需确定列号)
  • 逐行放置,每列尝试每个位置
  • 检查列冲突和对角线冲突
  • 冲突则剪枝
function solveNQueens(board, row, n):
    if row == n:
        printBoard(board)
        return

    for col = 0 to n-1:
        if isSafe(board, row, col, n):
            board[row][col] = "Q"     // 放置皇后
            solveNQueens(board, row + 1, n)
            board[row][col] = "."     // 回溯

function isSafe(board, row, col, n):
    // 检查同一列
    for i = 0 to row-1:
        if board[i][col] == "Q": return false
    // 检查左上对角线
    for i = row-1, j = col-1; i >= 0 && j >= 0; i--, j--:
        if board[i][j] == "Q": return false
    // 检查右上对角线
    for i = row-1, j = col+1; i >= 0 && j < n; i--, j++:
        if board[i][j] == "Q": return false
    return true

解释:N皇后是经典的回溯应用。由于每行只放一个皇后,解空间是排列树。剪枝函数检查列冲突和两条对角线冲突,大幅减少搜索空间。

6.3 迷宫搜索

问题:给定一个迷宫,找到从入口到出口的路径。

回溯策略:

  • 使用DFS探索迷宫
  • 走过的路径标记为已访问
  • 走到死路则回退,撤销标记
  • 到达出口则输出路径

6.4 骑士巡游

问题:在 m×n 的棋盘上,骑士从某个位置出发,按"日"字形移动(8个方向),能否不重复地走遍所有格子?

回溯策略:

  • 从当前位置出发,尝试8个方向
  • 选择合法且未访问的位置
  • 走不通则回退
  • 走完所有格子则找到解

七、回溯法的复杂度分析

回溯法的最坏时间复杂度通常是指数级或阶乘级:

问题 解空间类型 最坏复杂度
子集问题 子集树 O(2ⁿ)
排列问题 排列树 O(n!)
图着色(m种) m叉树 O(mⁿ)
N皇后 排列树 O(n!)

[!tip] 回溯法是NP-hard问题的常用求解方法 图着色、N皇后、哈密顿回路等问题都是NP-hard问题,不存在多项式时间的精确算法。回溯法虽然最坏是指数级,但通过剪枝可以大幅减少实际搜索量,是实践中最常用的精确求解方法。

[!question] 回溯法和暴力枚举有什么区别? 暴力枚举会检查所有可能的解,而回溯法通过剪枝函数提前排除不可能的分支,大幅减少搜索空间。例如N皇后问题,暴力枚举需要检查 nⁿ 种布局,而回溯法通过冲突检测剪枝,实际搜索量远小于此。

关联笔记