77. 组合

87 阅读3分钟

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

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

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

2.

class Solution {
    List<List<Integer>> res = new LinkedList<>();

    public List<List<Integer>> combine(int n, int k) {
        if (k <= 0 || n <= 0) {
            return res;
        }
        List<Integer> track = new LinkedList<>();
        backtrack(n, k, 1, track);
        return res;
    }

    void backtrack(int n, int k, int start, List<Integer> track) {
        // 到达树的底部
        if (k == track.size()) {
            res.add(new LinkedList<>(track));
            return;
        }
        // 注意 i 从 start 开始递增
        for (int i = start; i <= n; i++) {
            // 做选择
            track.add(i);
            backtrack(n, k, i + 1, track);
            // 撤销选择
            track.remove(track.size() - 1);
        }
    }
}

3.

List<List<Integer>> res = new LinkedList<>();

 这一行创建了一个列表 res,用于存储最终的组合结果。这个列表是一个二维列表,其中每个子列表都表示一个符合条件的组合。

public List<List<Integer>> combine(int n, int k) {
    if (k <= 0 || n <= 0) {
        return res;
    }
    List<Integer> track = new LinkedList<>();
    backtrack(n, k, 1, track);
    return res;
} 

这是公共函数 combine,它接受输入 n 和 k。首先,它检查输入是否有效,如果 k 或 n 不大于 0,就立即返回空列表 res。然后,它创建一个空列表 track 用于存储当前的组合,接着调用 backtrack 函数来生成组合。

 backtrack(n, k, 1, track);

这行代码是在 combine 方法中调用 backtrack 方法的起始点,它启动了回溯算法的递归过程来生成组合。

具体解释如下:

  • n 表示数字的范围,即从 1 到 n
  • k 表示要生成的组合的长度。
  • 1start 参数的初始值,表示从数字 1 开始尝试生成组合。
  • track 是一个空的列表,用于记录当前已选的数字组合,初始为空。

通过调用 backtrack(n, k, 1, track);,代码开始从数字 1 开始尝试生成长度为 k 的组合。随着回溯递归的进行,不断地选择数字、递归进入下一层,直到满足条件或无法再选择数字时,回溯退回到上一层,继续尝试其他可能的组合。这个过程会在递归中不断重复,直到生成了所有符合条件的组合,并将它们添加到 res 结果列表中。

所以,backtrack(n, k, 1, track); 是开始整个组合生成过程的触发点。

当你输入 n = 4k = 2,并调用 backtrack(n, k, 1, track);,代码将尝试生成从 1 到 4 的数字中长度为 2 的所有可能组合。

初始情况下,track 是一个空列表,start 的值是 1,表示从数字 1 开始尝试生成组合。

下面是生成组合的过程:

  1. backtrack(n, k, 1, track); 开始执行,此时 track 为空,start 是 1。
  2. backtrack 方法中,从 1 到 4 的数字依次尝试选择。
    • 当选择数字 1 时,track 变为 [1]。
    • 接着,递归调用 backtrack(n, k, 2, track);,此时 start 变为 2。
      • 在第二层递归中,尝试选择数字 2,生成组合 [1, 2]。
      • 这里 k 达到了 2,符合条件,所以将 [1, 2] 添加到结果列表 res 中。
      • 然后递归返回,回到第一层递归。
      • 继续尝试数字 3 和数字 4,但都不满足条件,所以不进行任何操作。
    • 回到第一层递归,继续尝试数字 2、3、4,但都不满足条件。
  3. 最终,backtrack(n, k, 1, track); 完成,res 中包含了所有符合条件的组合,即 [[1, 2]]。

所以,对于输入 n = 4k = 2backtrack(n, k, 1, track); 生成的组合是 [[1, 2]]。这是从 1 到 4 的数字中长度为 2 的所有可能组合之一。

void backtrack(int n, int k, int start, List<Integer> track) {
    // 到达树的底部
    if (k == track.size()) {
        res.add(new LinkedList<>(track));
        return;
    }
    // 注意 i 从 start 开始递增
    for (int i = start; i <= n; i++) {
        // 做选择
        track.add(i);
        backtrack(n, k, i + 1, track);
        // 撤销选择
        track.remove(track.size() - 1);
    }
} 

这是回溯函数 backtrack。它的目标是生成所有符合条件的组合。

  • 如果已经选择了 k 个数字,也就是 k == track.size(),那么当前的 track 列表表示一个符合条件的组合,将它复制到 res 中,并返回。

  • 否则,它使用一个循环遍历从 startn 的数字。在每一次循环中,它将数字 i 添加到 track 列表中,然后递归调用自身来继续生成下一个数字的组合。递归调用时,start 更新为 i + 1,表示下一个数字的选择范围。

  • 最后,需要撤销选择,即从 track 中移除最后一个添加的数字,以便尝试下一个数字。

通过这个递归和回溯的过程,代码将逐步生成范围 [1, 4] 中所有可能的 2 个数字的组合,并将它们存储在 res 列表中。最终,res 中将包含所有符合条件的组合,示例中的输出即是 res 的内容。

对于输入 n = 4k = 2,给定的 backtrack 方法将尝试生成从 1 到 4 的数字中长度为 2 的所有可能组合。让我们按步骤来解释它的执行过程:

  1. 初始时,track 是一个空列表,start 的值是 1。
  2. 在第一次调用 backtrack(n, k, 1, track); 时,进入递归。
    • k 还不等于 track.size(),所以不会执行添加到 res 的操作。
    • 进入循环,从 start(1)到 n(4)依次尝试选择数字。
      • 当选择数字 1 时,track 变为 [1]。
        • 然后递归调用 backtrack(n, k, 2, track);,此时 start 变为 2。
          • 在第二层递归中,再次进入循环,从 2 开始尝试选择数字。
            • 当选择数字 2 时,track 变为 [1, 2]。
            • 此时 k(2)等于 track.size(),因此将 [1, 2] 添加到结果列表 res 中。
            • 然后递归返回,回到第一层递归。
            • 接着尝试数字 3 和数字 4,但它们不满足条件,所以不会进行任何操作。
        • 第一层递归继续,尝试数字 3 和数字 4,但它们也不满足条件,所以不会进行任何操作。
    • 第一层递归的循环结束,回到最初的状态,也就是空的 track
  3. backtrack(n, k, 1, track); 完成,res 中包含了所有符合条件的组合,即 [[1, 2]]。

所以,对于输入 n = 4k = 2backtrack(n, k, 1, track); 生成的组合是 [[1, 2]],这是从 1 到 4 的数字中长度为 2 的所有可能组合之一。