回溯算法理论知识
算法本质
回溯算法本质上是一种试错的思想。它尝试分步解决一个问题,当发现当前的方案不能得到有效的正确解时,就"回溯"返回,尝试另一种方案。
用一个简单的比喻来理解: 想象你在走迷宫,遇到岔路时就任意选择一条路往前走,当发现走不通时,就退回到上一个岔路口,尝试另一条路。这个过程就是回溯的思想。
代码套路
function backtrack(路径, 选择列表):
if 满足结束条件:
结果.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
核心要素
- 选择: 在每个阶段,我们都面临若干选择
- 约束条件: 对选择施加约束,减少搜索空间
- 目标: 最终要达到的目标状态
与DFS的关系
回溯算法实际上就是DFS(深度优先搜索)的一种应用。区别在于:
- DFS是一个更宽泛的概念,是一种图的遍历方式
- 回溯算法是一种通过穷举所有可能情况来找到所有解的算法,并且在搜索过程中寻找问题的解
应用场景
- 组合问题
- 排列问题
- 子集问题
- 棋盘问题(如N皇后、数独等)
77. 组合
题目
给定两个整数 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]]
提示:
`1 <= n <= 20`
`1 <= k <= n`
解题思路
- 使用回溯算法,从1开始,每次选择一个数字
- 当选择的数字个数等于k时,我们找到了一个有效的组合
- 使用递归来实现回溯,每次递归都从当前数字的下一个开始,以避免重复
代码实现
function combine(n: number, k: number): number[][] {
const result: number[][] = [];
function backtrack(startIndex: number, tempArr: number[]): void {
if (tempArr.length === k) {
result.push([...tempArr]);
return;
}
for (let i = startIndex; i <= n - (k - tempArr.length) + 1; i++) {
tempArr.push(i);
backtrack(i + 1, tempArr);
tempArr.pop()
}
}
backtrack(1, []);
return result
};
216. 组合总和 III
题目
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回所有可能的有效组合的列表。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释: 1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
提示:
`2 <= k <= 9`
`1 <= n <= 60`
解题思路
- 使用回溯算法,从1开始,每次选择一个数字
- 当组合的数量等于k时代码无需继续递归,此时如果sum等于n则放入结果数组中
- 单层遍历时可以对遍历做剪枝操作(当剩余数字不足以凑够k个数时剪枝,无需继续横向分叉遍历下去),并且过程中包含递归、回溯操作
代码实现
function combinationSum3(k: number, n: number): number[][] {
let result: number[][] = [];
function backtrack(startIndex: number, tempArr: number[], sum: number): void {
if (tempArr.length === k) {
if (sum === n) {
result.push([...tempArr])
}
return
}
for (let i = startIndex; i <= 9 - (k - tempArr.length) + 1; i++) {
tempArr.push(i);
backtrack(i + 1, tempArr, sum + i);
tempArr.pop()
}
}
backtrack(1, [], 0);
return result
};
17. 电话号码的字母组合
题目
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入: digits = "23"
输出: ["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入: digits = ""
输出: []
示例 3:
输入: digits = "2"
输出: ["a","b","c"]
提示:
`0 <= digits.length <= 4`
`digits[i]` 是范围 `['2', '9']` 的一个数字。
解题思路
- 首先,我们需要建立数字到字母的映射关系。
- 然后,我们可以使用回溯算法,对每个数字选择一个对应的字母,然后继续处理下一个数字。
- 当我们处理完所有数字时,我们就得到了一个有效的组合。
代码实现
function letterCombinations(digits: string): string[] {
if (digits.length === 0) return [];
const digitsMap: { [key: string]: string } = {
'2': 'abc',
'3': 'def',
'4': 'ghi',
'5': 'jkl',
'6': 'mno',
'7': 'pqrs',
'8': 'tuv',
'9': 'wxyz'
}
const result: string[] = [];
function backtrack(stringSet: string, nextDigits: string) {
if (nextDigits.length === 0) {
result.push(stringSet);
return
}
const letter = digitsMap[nextDigits[0]];
for (let i = 0; i < letter.length; i++) {
backtrack(stringSet + letter[i], nextDigits.slice(1))
}
}
backtrack('', digits);
return result;
};