组合问题-回溯算法

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]]

2链接

题目链接:77. 组合 - 力扣(LeetCode)

视频链接:带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!_哔哩哔哩_bilibili

3解题思路

第一反应找两个数的组合,两层for循环暴力求解,此题可行。

但是如果找50个数的组合,嵌套50层for循环根本不可能,时间复杂度极高,所以要用回溯算法

可以把这种问题都看成树状结构问题,如图所示:

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围

图中可以发现n相当于树的宽度,k相当于树的深度

图中每次搜索到了叶子节点,我们就找到了一个结果

引入类似递归三部曲的回溯算法三部曲

1、确定函数返回值及参数

本题可以使用全局变量来记录结果,无需函数返回值。

注意到组合的特性:无顺序。所以选取的时候不能再反向选取,我们可以通过设定一个标志位startIndex来处理

vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件单一结果
void backtracking(int n, int k, int startIndex)

2、回溯的终止条件

如何终止?答:到达叶子结点(找到了目标组合)

其实也就是path的长度等于所需长度k

此时用result二维数组,把path保存起来,并终止本层递归。

if (path.size() == k) {
    result.push_back(path);
    return;
}

3、单层回溯的过程

回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。

如此我们才遍历完图中的这棵树。

for循环每次从startIndex开始遍历,然后用path保存取到的节点i。

for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历
    path.push_back(i); // 处理节点
    backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
    path.pop_back(); // 回溯,撤销处理的节点
}

可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。

注意下重点在于后面的撤销操作,这样才能起到回溯的效果


接上述内容

我们进行回溯求解时会发现,有很多树枝根本不可能产生结果,所以引入了新的优化方法:剪枝

举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在

第二层for循环,从元素3开始的遍历都没有意义了。如图:

说白了就是更改for循环的判别条件

思考一下整个过程:

1、我们使用一维数组path记录已选择的内容,已选择的个数为path.size()

2、我们所需的元素个数还剩:k-path.size()

3、列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size())

4、在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历。为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。

所以优化后的for循环是:

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置

4代码

class Solution {
private:
    vector<vector<int>> result; // 存放符合条件结果的集合
    vector<int> path; // 用来存放符合条件结果
    void backtracking(int n, int k, int startIndex) {
        if (path.size() == k) {
            result.push_back(path);
            return;
        }
        // i为本次搜索的起始位置
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
        //for (int i = startIndex; i <= n; i++) {
            path.push_back(i); // 处理节点
            backtracking(n, k, i + 1); // 递归
            path.pop_back(); // 回溯,撤销处理的节点
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        result.clear(); // 可以不写
        path.clear();   // 可以不写
        backtracking(n, k, 1);
        return result;
    }
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值