77. 组合

77. 组合

题目链接:77. 组合(中等)

给定两个整数 nk,返回范围 [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

解题思路

所有回溯问题都可以抽象为一个树,解决这个问题的方法就是对这棵树进行 深度优先遍历,找出所有符合条件的结果。

回溯法模板

来自代码随想录

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
​
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历。

所以针对此题,当 n=4,k=2时,可以先选出1,然后从2、3、4中分别挑出一个元素,得到12、13、14。继续选出2,然后从3、4中分别挑出一个元素,得到23、24。接着选出3,然后挑出4,的得到34。最后选取4,后面没有可以挑选的元素,结束。

代码(C++)

class Solution {
public:
    vector<int> path; // 用来存放符合条件结果
    vector<vector<int>> result; // 存放符合条件结果的集合
    void backTracking(int n, int k, int startIndex) {
        if(path.size() == k){
            result.push_back(path);
            return;
        }
        for (int i = startIndex; i <= n; i++) {
            path.push_back(i); // 处理节点
            backTracking(n, k, i + 1); // 递归
            path.pop_back(); // 回溯,撤销处理的节点
        }
    }
    vector<vector<int>> combine(int n, int k) {
        path.clear();
        result.clear();
        backTracking(n, k, 1);
        return result;
    }
};

代码(JS)

let result = [];
let path = [];
const backTracking = (n, k, startIndex) => {
    if (path.length === k) {
        result.push([...path]);
        return;
    }
    for (let i = startIndex; i <= n; i++) {
        path.push(i);
        backTracking(n, k, i + 1);
        path.pop();
    }
}
​
var combine = function(n, k) {
    result = [];
    path = [];
    backTracking(n, k, 1);
    return result;
};

剪枝

虽然回溯法是暴力搜索,但有时候可以通过剪枝进行优化。

通过分析可以发现上面的代码存在一些多余的操作。对于n=4,k=2,其实我们并不用从4开始遍历,因为要选出的是两个元素,而从4开始的话,仅仅只有一个元素。另如,对于n=4,k=3,我们就不用从3开始遍历,因为要选出的是三个元素,而从3开始的话,仅仅只有两个元素。所以可以通过剪枝对此进行优化。

一般来说,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置

如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。

对于每次遍历,我们相当于在[start, n]这个区间中取出k - path.size()个数,那也就是说[start, n]这个区间中至少要包含k-path.size()个数,也就是n - start + 1 >= k - path.size(),也就是start <= n - (k - path.size()) + 1,说明只要循环变量i至多从n - (k - path.size()) + 1位置开始,就可以确保下面能得到大小为k的组合。

代码(C++)

//剪枝优化
class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backTracking(int n, int k, int startIndex) {
        if (path.size() == k) {
            result.push_back(path);
            return;
        }
        //n - startIndex + 1 >= k - path.size()
        //startIndex <= n - (k - path.size()) + 1
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
            path.push_back(i);
            backTracking(n, k, i + 1);
            path.pop_back();
        }
    }
​
    vector<vector<int>> combine(int n, int k) {
        path.clear();
        result.clear();
        backTracking(n, k, 1);
        return result;
    }
};

代码(JS)

let result = [];
let path = [];
const backTracking = (n, k, startIndex) => {
    if (path.length === k) {
        result.push([...path]);
        return;
    }
    for (let i = startIndex; i <= n - (k - path.length) + 1; i++) {
        path.push(i);
        backTracking(n, k, i + 1);
        path.pop();
    }
}
​
var combine = function(n, k) {
    result = [];
    path = [];
    backTracking(n, k, 1);
    return result;
};

 

 

 

 

 

posted @ 2021-12-21 09:10  wltree  阅读(36)  评论(0编辑  收藏  举报