代码随想录训练营day24| 77. 组合

@TOC


前言

代码随想录算法训练营day24


一、Leetcode●  77. 组合

1.题目

给定两个整数 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

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/combinations

2.解题思路

方法一:非递归(字典序法)实现组合型枚举

小贴士:这个方法理解起来比「方法一」复杂,建议读者遇到不理解的地方可以在草稿纸上举例模拟这个过程。

这里的非递归版不是简单的用栈模拟递归转化为非递归:我们希望通过合适的手段,消除递归栈带来的额外空间代价。

假设我们把原序列中被选中的位置记为 11,不被选中的位置记为 00,对于每个方案都可以构造出一个二进制数。我们让原序列从大到小排列(即 {n,n−1,⋯1,0}{n,n−1,⋯1,0})。我们先看一看 n=4n=4,k=2k=2 的例子: 原序列中被选中的数 对应的二进制数 方案 43[2][1]43[2][1] 00110011 2,12,1 4[3]2[1]4[3]2[1] 01010101 3,13,1 4[3][2]14[3][2]1 01100110 3,23,2 [4]32[1][4]32[1] 10011001 4,14,1 [4]3[2]1[4]3[2]1 10101010 4,24,2 [4][3]21[4][3]21 11001100 4,34,3

我们可以看出「对应的二进制数」一列包含了由 kk 个 11 和 n−kn−k 个 00 组成的所有二进制数,并且按照字典序排列。这给了我们一些启发,我们可以通过某种方法枚举,使得生成的序列是根据字典序递增的。我们可以考虑我们一个二进制数数字 xx,它由 kk 个 11 和 n−kn−k 个 00 组成,如何找到它的字典序中的下一个数字 next(x)next(x),这里分两种情况:

规则一:xx 的最低位为 11,这种情况下,如果末尾由 tt 个连续的 11,我们直接将倒数第 tt 位的 11 和倒数第 t+1t+1 位的 00 替换,就可以得到 next(x)next(x)。如 0011→01010011→0101,0101→01100101→0110,1001→10101001→1010,1001111→10101111001111→1010111。
规则二:xx 的最低位为 00,这种情况下,末尾有 tt 个连续的 00,而这 tt 个连续的 00 之前有 mm 个连续的 11,我们可以将倒数第 t+mt+m 位置的 11 和倒数第 t+m+1t+m+1 位的 00 对换,然后把倒数第 t+1t+1 位到倒数第 t+m−1t+m−1 位的 11 移动到最低位。如 0110→10010110→1001,1010→11001010→1100,1011100→11000111011100→1100011。

至此,我们可以写出一个朴素的程序,用一个长度为 nn 的 0/10/1 数组来表示选择方案对应的二进制数,初始状态下最低的 kk 位全部为 11,其余位置全部为 00,然后不断通过上述方案求 nextnext,就可以构造出所有的方案。

我们可以进一步优化实现,我们来看 n=5n=5,k=3k=3 的例子,根据上面的策略我们可以得到这张表: 二进制数 方案 0011100111 3,2,13,2,1 0101101011 4,2,14,2,1 0110101101 4,3,14,3,1 0111001110 4,3,24,3,2 1001110011 5,2,15,2,1 1010110101 5,3,15,3,1 1011010110 5,3,25,3,2 1100111001 5,4,15,4,1 1101011010 5,4,25,4,2 1110011100 5,4,35,4,3

在朴素的方法中我们通过二进制数来构造方案,而二进制数是需要通过迭代的方法来获取 nextnext 的。考虑不通过二进制数,直接在方案上变换来得到下一个方案。假设一个方案从低到高的 kk 个数分别是 {a0,a1,⋯ ,ak−1}{a0​,a1​,⋯,ak−1​},我们可以从低位向高位找到第一个 jj 使得 aj+1≠aj+1aj​+1​=aj+1​,我们知道出现在 aa 序列中的数字在二进制数中对应的位置一定是 11,即表示被选中,那么 aj+1≠aj+1aj​+1​=aj+1​ 意味着 ajaj​ 和 aj+1aj+1​ 对应的二进制位中间有 00,即这两个 11 不连续。我们把 ajaj​ 对应的 11 向高位推送,也就对应着 aj←aj+1aj​←aj​+1,而对于 i∈[0,j−1]i∈[0,j−1] 内所有的 aiai​ 把值恢复成 i+1i+1,即对应这 jj 个 11 被移动到了二进制数的最低 jj 位。这似乎只考虑了上面的「规则二」。但是实际上「规则一」是「规则二」在 t=0t=0 时的特殊情况,因此这么做和按照两条规则模拟是等价的。

在实现的时候,我们可以用一个数组 temptemp 来存放 aa 序列,一开始我们先把 11 到 kk 按顺序存入这个数组,他们对应的下标是 00 到 k−1k−1。为了计算的方便,我们需要在下标 kk 的位置放置一个哨兵 n+1n+1(思考题:为什么是 n+1n+1 呢?)。然后对这个 temptemp 序列按照这个规则进行变换,每次把前 kk 位(即除了最后一位哨兵)的元素形成的子数组加入答案。每次变换的时候,我们把第一个 aj+1≠aj+1aj​+1​=aj+1​ 的 jj 找出,使 ajaj​ 自增 11,同时对 i∈[0,j−1]i∈[0,j−1] 的 aiai​ 重新置数。如此循环,直到 temptemp 中的所有元素为 nn 内最大的 kk 个元素。

回过头看这个思考题,它是为了我们判断退出条件服务的。我们如何判断枚举到了终止条件呢?其实不是直接通过 temptemp 来判断的,我们会看每次找到的 jj 的位置,如果 j=kj=k 了,就说明 [0,k−1][0,k−1] 内的所有的数字是比第 kk 位小的最后 kk 个数字,这个时候我们找不到任何方案的字典序比当前方案大了,结束枚举。

3.代码实现

```java class Solution { List temp = new ArrayList(); List> ans = new ArrayList>();

public List<List<Integer>> combine(int n, int k) {
    List<Integer> temp = new ArrayList<Integer>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    // 初始化
    // 将 temp 中 [0, k - 1] 每个位置 i 设置为 i + 1,即 [0, k - 1] 存 [1, k]
    // 末尾加一位 n + 1 作为哨兵
    for (int i = 1; i <= k; ++i) {
        temp.add(i);
    }
    temp.add(n + 1);

    int j = 0;
    while (j < k) {
        ans.add(new ArrayList<Integer>(temp.subList(0, k)));
        j = 0;
        // 寻找第一个 temp[j] + 1 != temp[j + 1] 的位置 t
        // 我们需要把 [0, t - 1] 区间内的每个位置重置成 [1, t]
        while (j < k && temp.get(j) + 1 == temp.get(j + 1)) {
            temp.set(j, j + 1);
            ++j;
        }
        // j 是第一个 temp[j] + 1 != temp[j + 1] 的位置
        temp.set(j, temp.get(j) + 1);
    }
    return ans;
}

}

```

猜你喜欢

转载自blog.csdn.net/HHX_01/article/details/131285427
今日推荐