Files
leetcode-master/problems/0077.组合.md
youngyangyang04 15261056fd Update
2020-09-09 10:26:04 +08:00

5.8 KiB
Raw Blame History

第77题. 组合

给定两个整数 n 和 k返回 1 ... n 中所有可能的 k 个数的组合。 示例:

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

思路

这是回溯法的经典题目。

直觉上当然是使用for循环例如示例中k为2很容易想到 用两个for循环这样就可以输出 和示例中一样的结果。

代码如下:

    int n  = 4;
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            cout << i << " " << j << endl;
        }
    }

输入n = 100, k = 3 那么就三层for循环代码如下

    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            for (int u = j + 1; u <=n; n++) {

            }
        }
    }

如果n为 100k为50呢那就50层for循环是不是开始窒息。

那么回溯法就能解决这个问题了。

回溯是用来做选择的,递归用来节点层叠嵌套,每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。

其实子集和组合问题都可以抽象为一个树形结构,如下:

可以看一下这个棵树,一开始集合是 1234 从左向右去数,取过的数,不在重复取。

第一取1集合变为234 因为k为2我们只需要去一个数就可以了分别取234 得到集合[1,2] [1,3] [1,4],以此类推。

其实这就转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围

如何在这个树上遍历,然后收集到我们要的结果集呢,用的就是回溯搜索法,可以发现,每次搜索到了叶子节点,我们就找到了一个结果。

分析完过程,我们来看一下 回溯算法的模板框架如下:

backtracking() {
    if (终止条件) {
        存放结果;
    }

    for (选择:选择列表(可以想成树中节点孩子的数量)) {
        递归,处理节点;
        backtracking();
        回溯,撤销处理结果
    }
}

分析模板:

什么是达到了终止条件,树中就可以看出,搜到了叶子节点了,就找到了一个符合题目要求的答案,就把这个答案存放起来。

看一下这个for循环这个for循环是做什么的for 就是处理树中节点各个孩子的情况, 一个节点有多少个孩子这个for循环就执行多少次。

最后就要看这个递归的过程了注意这个backtracking就是自己调用自己实现递归。

一些同学对递归操作本来就不熟练递归上面又加上一个for循环可能就更迷糊了 我来给大家捋顺一下。

这个backtracking 其实就是向树的叶子节点方向遍历, for循环可以理解是横向遍历backtracking 就是纵向遍历,这样就把这棵树全遍历完了。

那么backtracking就是一直往深处遍历总会遇到叶子节点遇到了叶子节点就要返回那么backtracking的下面部分就是回溯的操作了撤销本次处理的结果。

分析完模板,本题代码如下:

C++ 代码

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;
        }
        // 这个for循环有讲究组合的时候 要用startIndex排列的时候就要从0开始
        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) {
        backtracking(n, k, 1);
        return result;
    }
};

剪枝优化

在遍历的过程中如下代码

for (int i = startIndex; i <= n; i++) 

这个遍历的范围是可以剪枝优化的,怎么优化呢?

来举一个例子n = 4 k = 4的话那么从2开始的遍历都没有意义了。

已经选择的元素个数path.size();

要选择的元素个数 : k - path.size();

在集合n中开始选择的起始位置 : n - (k - path.size());

因为起始位置是从1开始的而且代码里是n <= 起始位置,所以 集合n中开始选择的起始位置 : n - (k - path.size()) + 1;

所以优化之后是:

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)

整体代码如下:

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;
        }
        // 这个for循环有讲究组合的时候 要用startIndex排列的时候就要从0开始
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
            path.push_back(i); // 处理节点 
            backtracking(n, k, i + 1);
            path.pop_back(); // 回溯,撤销处理的节点
        }
    }
public:

    vector<vector<int>> combine(int n, int k) {
        backtracking(n, k, 1);
        return result;
    }
};

观后感

我来写一下观后感: 很厉害,转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围。 每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。 每一层递归中,尽量节省循环次数,这样在后续的递归调用中,节省下来的循环会被以至少指数等级放大。