算法-回溯

一、算法介绍

回溯算法(Backtracking)本质上是一种深度优先搜索(DFS)。它尝试在问题的解空间树中搜索答案,当探索到某一步发现原先的选择不能得到正确解时,就退回上一步重新选择。

回溯算法就是通过不断的尝试和"后悔",穷举所有可能的解。

二、回溯算法的核心

  • 路径:已经做出的选择。
  • 选择列表:当前还可以做的选择。
  • 结束条件:到达决策树底层,无法再做选择的条件。

三、模版代码

复制代码
void backtrack(路径, 选择列表) {
    if (满足结束条件) {
        res.add(new ArrayList<>(路径)); // 必须拷贝一份快照 因为集合引用一直在变化
        return;
    }

    for (选择 : 选择列表) {
        // 1. 做选择
        处理节点;
        
        // 2. 递归进入下一层决策树
        backtrack(路径, 选择列表);
        
        // 3. 撤销选择
        回溯,撤销处理结果;
    }
}

四、什么时候使用回溯?

当题目具有以下特征时,通常考虑回溯:

组合问题:N 个数里面按一定规则找出 K 个数的集合。

切割问题:一个字符串按一定规则有几种切割方式。

子集问题:一个集合的所有子集。

排列问题:N 个数按一定规则全排列。

棋盘问题:N 皇后、解数独等。

五、Leetcode77:组合

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

复制代码
输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

复制代码
输入:n = 1, k = 1
输出:[[1]]

n=5,k=3决策树:

当第一层选了 4 时,后面只剩 5 可选,无法凑齐 3 个数,所以 4 和 5 开头的分支在"剪枝"逻辑下会被直接舍弃。

每一层搜索的起始位置 start 都在递增,确保了结果中元素的顺序始终是升序的,从而避免了重复。

第一阶段:向下探索 (Depth First)

Level 1: start = 1。循环 i 从 1 到 5。

选择 1,path = [1]。进入递归 backtrack(start=2)。

Level 2: start = 2。循环 i 从 2 到 5。

选择 2,path = [1, 2]。进入递归 backtrack(start=3)。

Level 3: start = 3。循环 i 从 3 到 5。

选择 3,path = [1, 2, 3]。

触发终止: path.size() == 3,保存 [1, 2, 3] 到结果集。

回溯: 弹出 3,path = [1, 2]。

继续 Level 3 的循环,选择 4,path = [1, 2, 4]。

触发终止: 保存 [1, 2, 4],回溯弹出 4。

继续 Level 3 的循环,选择 5,path = [1, 2, 5]。

触发终止: 保存 [1, 2, 5],回溯弹出 5。

Level 3 循环结束,返回上一层。

第二阶段:回溯并横向移动

回到 Level 2: 刚才 i=2 的分支跑完了。

回溯: 弹出 2,path = [1]。

继续 Level 2 的循环,i 变成 3。

选择 3,path = [1, 3]。进入递归 backtrack(start=4)。

Level 3 (新分支): start = 4。

选择 4,path = [1, 3, 4] -> 保存并回溯。

选择 5,path = [1, 3, 5] -> 保存并回溯。

start:在组合中,为了不重复取,规定只能取当前元素之后的元素。start 就是用来控制"搜索范围"的。

java 复制代码
class Solution {

    List<List<Integer>> resList = new ArrayList();
    List<Integer> pathList = new ArrayList();

    public List<List<Integer>> combine(int n, int k) {
        backtrack(n,k,1);
        return resList;
    }

    public void backtrack(int n, int k, int start){
        if(pathList.size()==k){
            resList.add(new ArrayList<>(pathList));
            return;
        }
        for(int i = start;i<=n - (k - pathList.size()) + 1;i++){
            pathList.add(i);
            backtrack(n,k,i+1);
            pathList.remove(pathList.size() - 1);
        }
    }
}

伪代码:

java 复制代码
// 初始调用:backtrack(resultList, pathList=[], n=5, k=3, start=1)
public void backtrack(pathList=[], start=1) {
    // i = 1
    pathList.add(1); // pathList=[1]
    backtrack(pathList=[1], start=2) {
        // i = 2
        pathList.add(2); // pathList=[1,2]
        backtrack(pathList=[1,2], start=3) {
            // i = 3 -> pathList=[1,2,3], size=3, 记录结果, 回溯
            // i = 4 -> pathList=[1,2,4], size=3, 记录结果, 回溯
            // i = 5 -> pathList=[1,2,5], size=3, 记录结果, 回溯
        }
        pathList.remove(last); // 回溯:pathList=[1]

        // i = 3
        pathList.add(3); // pathList=[1,3]
        backtrack(pathList=[1,3], start=4) {
            // i = 4 -> pathList=[1,3,4], size=3, 记录结果, 回溯
            // i = 5 -> pathList=[1,3,5], size=3, 记录结果, 回溯
        }
        pathList.remove(last); // 回溯:pathList=[1]

        // i = 4
        pathList.add(4); // pathList=[1,4]
        backtrack(pathList=[1,4], start=5) {
            // i = 5 -> pathList=[1,4,5], size=3, 记录结果, 回溯
        }
        pathList.remove(last); // 回溯:pathList=[1]

        // i = 5
        pathList.add(5); // pathList=[1,5]
        backtrack(pathList=[1,5], start=6) {
            // start=6 > n=5, 循环不执行
        }
        pathList.remove(last); // 回溯:pathList=[1]
    }
    pathList.remove(last); // 回溯:pathList=[]

    // i = 2
    pathList.add(2); // pathList=[2]
    backtrack(pathList=[2], start=3) {
        // i = 3
        pathList.add(3); // pathList=[2,3]
        backtrack(pathList=[2,3], start=4) {
            // i = 4 -> pathList=[2,3,4], size=3, 记录结果, 回溯
            // i = 5 -> pathList=[2,3,5], size=3, 记录结果, 回溯
        }
        pathList.remove(last); // 回溯:pathList=[2]

        // i = 4
        pathList.add(4); // pathList=[2,4]
        backtrack(pathList=[2,4], start=5) {
            // i = 5 -> pathList=[2,4,5], size=3, 记录结果, 回溯
        }
        pathList.remove(last); // 回溯:pathList=[2]

        // i = 5
        pathList.add(5); // pathList=[2,5]
        backtrack(pathList=[2,5], start=6) { /* 无 */ }
        pathList.remove(last); // 回溯:pathList=[2]
    }
    pathList.remove(last); // 回溯:pathList=[]

    // i = 3
    pathList.add(3); // pathList=[3]
    backtrack(pathList=[3], start=4) {
        // i = 4
        pathList.add(4); // pathList=[3,4]
        backtrack(pathList=[3,4], start=5) {
            // i = 5 -> pathList=[3,4,5], size=3, 记录结果, 回溯
        }
        pathList.remove(last); // 回溯:pathList=[3]

        // i = 5
        pathList.add(5); // pathList=[3,5]
        backtrack(pathList=[3,5], start=6) { /* 无 */ }
        pathList.remove(last); // 回溯:pathList=[3]
    }
    pathList.remove(last); // 回溯:pathList=[]

    // i = 4
    pathList.add(4); // pathList=[4]
    backtrack(pathList=[4], start=5) {
        // i = 5
        pathList.add(5); // pathList=[4,5]
        backtrack(pathList=[4,5], start=6) { /* 无,因为 size=2 != k */ }
        pathList.remove(last); // 回溯:pathList=[4]
    }
    pathList.remove(last); // 回溯:pathList=[]

    // i = 5
    pathList.add(5); // pathList=[5]
    backtrack(pathList=[5], start=6) { /* 无 */ }
    pathList.remove(last); // 回溯:pathList=[]
}

六、leetcode17.电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

复制代码
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

复制代码
输入:digits = "2"
输出:["a","b","c"]
java 复制代码
class Solution {

    List<String> resList = new ArrayList();
    StringBuilder sb = new StringBuilder();

    public List<String> letterCombinations(String digits) {
        Map<Integer, String> map = new HashMap();
        map.put(2, "abc");
        map.put(3, "def");
        map.put(4, "ghi");
        map.put(5, "jkl");
        map.put(6, "mno");
        map.put(7, "pqrs");
        map.put(8, "tuv");
        map.put(9, "wxyz");
        List<String> letterList = new ArrayList();
        for (int i = 0; i < digits.length(); i++) {
            String letterStr = map.get(digits.charAt(i) - '0');
            letterList.add(letterStr);
        }
        backtrack(digits, letterList, 0);
        return resList;
    }

    public void backtrack(String digits,List<String> letterList,int start){
        if(sb.length()==digits.length()){
            resList.add(sb.toString());
            return;
        }
        String letterStr = letterList.get(start);
        for(int i = 0;i<letterStr.length();i++){
            sb.append(letterStr.charAt(i));
            backtrack(digits,letterList,start+1);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

输入 digits = "23" 递归流程:

java 复制代码
// 初始调用:backtrack(index=0, path="")
backtrack(0):
    // 当前数字 digits[0] = '2',对应字母串 "abc"
    for each ch in "abc":
        // 选择字母 'a'
        path.append('a')   // path = "a"
        backtrack(1):
            // 当前数字 digits[1] = '3',对应字母串 "def"
            for each ch in "def":
                // 选择字母 'd'
                path.append('d')   // path = "ad"
                backtrack(2):
                    // index == digits.length() (2 == 2) → 记录结果 "ad"
                    result.add("ad")
                    return
                path.deleteLast()   // 回溯,path = "a"
                // 选择字母 'e'
                path.append('e')   // path = "ae"
                backtrack(2):
                    result.add("ae")
                    return
                path.deleteLast()   // path = "a"
                // 选择字母 'f'
                path.append('f')   // path = "af"
                backtrack(2):
                    result.add("af")
                    return
                path.deleteLast()   // path = "a"
            // 结束循环,返回上一层
        path.deleteLast()   // 回溯,path = ""

        // 选择字母 'b'
        path.append('b')   // path = "b"
        backtrack(1):
            // 数字 '3' → "def"
            for each ch in "def":
                path.append('d')   // path = "bd"
                backtrack(2):
                    result.add("bd")
                path.deleteLast()   // path = "b"
                path.append('e')   // path = "be"
                backtrack(2):
                    result.add("be")
                path.deleteLast()   // path = "b"
                path.append('f')   // path = "bf"
                backtrack(2):
                    result.add("bf")
                path.deleteLast()   // path = "b"
        path.deleteLast()   // path = ""

        // 选择字母 'c'
        path.append('c')   // path = "c"
        backtrack(1):
            // 数字 '3' → "def"
            for each ch in "def":
                path.append('d')   // path = "cd"
                backtrack(2):
                    result.add("cd")
                path.deleteLast()   // path = "c"
                path.append('e')   // path = "ce"
                backtrack(2):
                    result.add("ce")
                path.deleteLast()   // path = "c"
                path.append('f')   // path = "cf"
                backtrack(2):
                    result.add("cf")
                path.deleteLast()   // path = "c"
        path.deleteLast()   // path = ""
    // 结束循环,返回

七、Leetcode22.括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

示例 1:

复制代码
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

示例 2:

复制代码
输入:n = 1
输出:["()"]

每一步有两种选择:放一个左括号或放一个右括号,但必须满足以下约束:

  • 已使用的左括号数量不能超过 n。
  • 已使用的右括号数量不能超过左括号数量

步骤

1.递归函数定义:backtrack(current, left, right)

  • current:当前已构建的字符串
  • left:已使用的左括号数
  • right:已使用的右括号数

2.终止条件:当 current 长度等于 2n 时,得到一个有效组合,加入结果列表。

3.递归选择:

如果 left < n,可以放一个左括号,递归调用 backtrack(current + '(', left + 1, right)

如果 right < left,可以放一个右括号,递归调用 backtrack(current + ')', left, right + 1)

4.回溯: 撤销 cur.deleteCharAt(cur.length() - 1);

n=2 时决策树:

复制代码
                                    "" (0,0)
                                   /      
                                  /        
                                "(" (1,0)
                               /      \
                              /        \
                        "((" (2,0)    "()" (1,1)
                        /              /     \
                       /              /       \
                   "(()" (2,1)    "()(" (2,1)  (此处右括号不合法,因为right<left? 1<1不成立)
                   /                /
                  /                /
              "(())" (2,2)    "()()" (2,2)
java 复制代码
class Solution {
    List<String> resList = new ArrayList<>();
    StringBuilder sb = new StringBuilder();

    public List<String> generateParenthesis(int n) {
        backtrack(0, 0, n);
        return resList;
    }

    public void backtrack(int left, int right, int n) {
        if (left == n && right == n) {
            resList.add(sb.toString());
            return;
        }
        // 穷举左括号
        if (left < n) {
            sb.append("(");
            backtrack(left + 1, right, n);
            sb.deleteCharAt(sb.length() - 1);
        }
        if (right < left) {
            sb.append(")");
            backtrack(left, right + 1, n);
            sb.deleteCharAt(sb.length() - 1);
        }
    }
}

执行流程:

java 复制代码
开始 backtrack(cur, left=0, right=0):
    cur = "", left=0, right=0

    // 尝试添加 '('
    left=0 < 2 成立:
        cur.append('(')  // cur = "(", left=1, right=0
        调用 backtrack(cur, left=1, right=0):
            cur = "(", left=1, right=0

            // 尝试添加 '('
            left=1 < 2 成立:
                cur.append('(')  // cur = "((", left=2, right=0
                调用 backtrack(cur, left=2, right=0):
                    cur = "((", left=2, right=0

                    // left=2 已满,不能加 '('
                    // 尝试添加 ')'
                    right=0 < left=2 成立:
                        cur.append(')')  // cur = "(()", left=2, right=1
                        调用 backtrack(cur, left=2, right=1):
                            cur = "(()", left=2, right=1

                            // left 已满
                            // 尝试添加 ')'
                            right=1 < left=2 成立:
                                cur.append(')')  // cur = "(())", left=2, right=2
                                调用 backtrack(cur, left=2, right=2):
                                    cur = "(())", 长度=4,记录结果 "(())"
                                    返回
                                // 回溯,移除最后一个字符
                                cur.deleteCharAt(cur.length()-1)  // cur = "(()", left=2, right=1
                            返回
                        // 回溯,移除最后一个字符
                        cur.deleteCharAt(cur.length()-1)  // cur = "((", left=2, right=0
                    返回
                // 回溯,移除最后一个字符
                cur.deleteCharAt(cur.length()-1)  // cur = "(", left=1, right=0

            // 尝试添加 ')'
            right=0 < left=1 成立:
                cur.append(')')  // cur = "()", left=1, right=1
                调用 backtrack(cur, left=1, right=1):
                    cur = "()", left=1, right=1

                    // 尝试添加 '('
                    left=1 < 2 成立:
                        cur.append('(')  // cur = "()(", left=2, right=1
                        调用 backtrack(cur, left=2, right=1):
                            cur = "()(", left=2, right=1

                            // left 已满
                            // 尝试添加 ')'
                            right=1 < left=2 成立:
                                cur.append(')')  // cur = "()()", left=2, right=2
                                调用 backtrack(cur, left=2, right=2):
                                    cur = "()()", 长度=4,记录结果 "()()"
                                    返回
                                // 回溯,移除最后一个字符
                                cur.deleteCharAt(cur.length()-1)  // cur = "()(", left=2, right=1
                            返回
                        // 回溯,移除最后一个字符
                        cur.deleteCharAt(cur.length()-1)  // cur = "()", left=1, right=1

                    // 尝试添加 ')'? right=1 < left=1? 不成立,跳过
                    返回
                // 回溯,移除最后一个字符
                cur.deleteCharAt(cur.length()-1)  // cur = "(", left=1, right=0
            返回
        // 回溯,移除最后一个字符
        cur.deleteCharAt(cur.length()-1)  // cur = "", left=0, right=0
    结束
相关推荐
实心儿儿2 小时前
算法7:两个数组的交集
算法·leetcode·职场和发展
WolfGang0073212 小时前
代码随想录算法训练营 Day14 | 二叉树 part04
数据结构·算法
爱丽_2 小时前
GC 怎么判定“该回收谁”:GC Roots、可达性分析、四种引用与回收算法
java·jvm·算法
dfafadfadfafa2 小时前
嵌入式C++安全编码
开发语言·c++·算法
仍然.2 小时前
算法题目---前缀和
算法
计算机安禾2 小时前
【C语言程序设计】第34篇:文件的概念与文件指针
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
大熊背2 小时前
双目拼接摄像机中简单的亮度差校正原理
人工智能·算法·双目拼接·亮度差消除
CoovallyAIHub2 小时前
AAAI 2026 | 上海AI Lab发布RacketVision,首次为球拍运动标注球拍姿态
深度学习·算法·计算机视觉
大熊背2 小时前
双目拼接摄像机中简单的色差校正原理
人工智能·算法·isppipeline·双目拼接