算法 LC 回溯-组合

题目描述

给定两个整数 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 个当中选 k 个的所有方案对应的枚举是组合型枚举。我们可以使用递归来实现组合型枚举。

一个长度为n 的序列 a的所有子序列,代码框架是这样的:

vector<int> temp;
void dfs(int cur, int n) {
    if (cur == n + 1) {
        // 记录答案
        // ...
        return;
    }
    // 考虑选择当前位置
    temp.push_back(cur);
    dfs(cur + 1, n, k);
    temp.pop_back();
    // 考虑不选择当前位置
    dfs(cur + 1, n, k);
}
    static public func combine2(_ n: Int, _ k: Int) -> [[Int]] {
        var res = [[Int]]()
        var output = [Int]()
        backtrackCombine2(n, k, &res, &output, 1)
        return res
    }
    
    static func backtrackCombine2(_ n:Int, _ k:Int, _ res:inout [[Int]], _ output:inout [Int], _ cur:Int) {
        
        // 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 temp
        if output.count + (n-cur+1) < k {
            return
        }
        
        // 记录合法的答案
        if output.count == k {
            res.append(output)
            return
        }
        
        // 考虑选择当前位置
        output.append(cur)
        backtrackCombine2(n, k, &res, &output, cur+1)
        // 考虑不选择当前位置
        output.removeLast()
        backtrackCombine2(n, k, &res, &output, cur+1)
        
        
    }

思路2:使用排列

    static public func combine1(_ n: Int, _ k: Int) -> [[Int]] {
        var res = [[Int]]()
        var output = [Int]()
        for i in 0..<n {
            output.append(I+1)
        }
        backtrackCombine1(n, k, &output, &res, 0)
        return res
    }
    
    static func backtrackCombine1(_ n :Int, _ k :Int, _ output:inout [Int], _ res:inout [[Int]], _ first:Int) {
        if first == k {
            let temp = output[0..<first]
            res.append(Array(temp))
            return
        }
        
        for i in first..<n {
            if first == 0 {
                output.swapAt(i, first)
                backtrackCombine1(n, k, &output, &res, first+1)
                output.swapAt(i, first)
            }else {
                if output[first-1] < (i+1) {
                    output.swapAt(i, first)
                    backtrackCombine1(n, k, &output, &res, first+1)
                    output.swapAt(i, first)
                }
                
            }
            
        }
        
    }

思路3:非递归(字典序法)实现组合型枚举

假设我们把原序列中被选中的位置记为1,不被选中的位置记为0,对于每个方案都可以构造出一个二进制数。我们让原序列从大到小排列(即{n,n−1,⋯1,0})。我们先看一看n=4,k=2 的例子:

截屏2022-04-02 下午2.38.02.png

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

规则一:
x 的最低位为1,这种情况下,如果末尾由t个连续的1,我们直接将倒数第t位的1和倒数第t+1 位的0 替换,就可以得到next(x)。如0011→0101,0101→0110,1001→1010,1001111→1010111。
规则二:
x的最低位为0,这种情况下,末尾有t个连续的0,而这t个连续的0之前有m 个连续的1,我们可以将倒数第t+m 位置的1和倒数第t+m+1 位的0 对换,然后把倒数第t+1 位到倒数第t+m−1 位的1移动到最低位。如0110→1001,1010→1100,1011100→1100011。
至此,我们可以写出一个朴素的程序,用一个长度为n的0/1 数组来表示选择方案对应的二进制数,初始状态下最低的k位全部为
1,其余位置全部为0,然后不断通过上述方案求next,就可以构造出所有的方案。

实际上 规则一 是规则二在t=0的情况下的特殊情况

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

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

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

    static public func combine3(_ n: Int, _ k: Int) -> [[Int]] {
        var res = [[Int]]()
        var temp = [Int]()
        // 初始化
        // 将 temp 中 [0, k - 1] 每个位置 i 设置为 i + 1,即 [0, k - 1] 存 [1, k]
        // 末尾加一位 n + 1 作为哨兵
        for i in 1...k {
            temp.append(i)
        }
        
        temp.append(n+1)
        
        var j = 0
        while j<k {
            res.append(temp.suffix(k))
            
            // 寻找第一个 temp[j] + 1 != temp[j + 1] 的位置 t
            // 我们需要把 [0, t - 1] 区间内的每个位置重置成 [1, t]
            j = 0
            while j<k && temp[j] + 1 == temp[j+1] {
                temp[j] = j+1
                j += 1
            }
            
            temp[j] = temp[j]+1
        }
        return res
    }

参考:https://leetcode-cn.com/problems/combinations
https://leetcode-cn.com/problems/combinations/solution/zu-he-by-leetcode-solution/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容