一、算法介绍
回溯算法(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
结束