LeetCode体操-22 | 回溯算法理论、77.组合、216. 组合综合III、17. 电话号码的字母组合

54 阅读4分钟

回溯算法理论知识

image.png

算法本质

回溯算法本质上是一种试错的思想。它尝试分步解决一个问题,当发现当前的方案不能得到有效的正确解时,就"回溯"返回,尝试另一种方案。

用一个简单的比喻来理解: 想象你在走迷宫,遇到岔路时就任意选择一条路往前走,当发现走不通时,就退回到上一个岔路口,尝试另一条路。这个过程就是回溯的思想。

代码套路

function backtrack(路径, 选择列表):
    if 满足结束条件:
        结果.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

核心要素

  1. 选择: 在每个阶段,我们都面临若干选择
  2. 约束条件: 对选择施加约束,减少搜索空间
  3. 目标: 最终要达到的目标状态

与DFS的关系

回溯算法实际上就是DFS(深度优先搜索)的一种应用。区别在于:

  1. DFS是一个更宽泛的概念,是一种图的遍历方式
  2. 回溯算法是一种通过穷举所有可能情况来找到所有解的算法,并且在搜索过程中寻找问题的解

应用场景

  • 组合问题
  • 排列问题
  • 子集问题
  • 棋盘问题(如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. 使用回溯算法,从1开始,每次选择一个数字
  2. 当选择的数字个数等于k时,我们找到了一个有效的组合
  3. 使用递归来实现回溯,每次递归都从当前数字的下一个开始,以避免重复

代码实现

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. 使用回溯算法,从1开始,每次选择一个数字
  2. 当组合的数量等于k时代码无需继续递归,此时如果sum等于n则放入结果数组中
  3. 单层遍历时可以对遍历做剪枝操作(当剩余数字不足以凑够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 不对应任何字母。

image.png

示例 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']` 的一个数字。

解题思路

  1. 首先,我们需要建立数字到字母的映射关系。
  2. 然后,我们可以使用回溯算法,对每个数字选择一个对应的字母,然后继续处理下一个数字。
  3. 当我们处理完所有数字时,我们就得到了一个有效的组合。

image.png

代码实现

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;
};