力扣 77 组合【回溯经典问题】【三步法】

力扣 77 组合

全部刷题与学习记录

【C++刷题学习笔记目录】

【C++百万并发网络通信-笔记目录】

原题目

题目地址:https://leetcode-cn.com/problems/combinations/

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

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

考查知识点

回溯:可以参考回溯三步法【算法套路】-【回溯篇】【回溯三步法】


好的解法

这道题是参考【代码随想录】大佬的回溯算法:求组合问题!来写的心得体会。

首先我们按照 k 的取值不同来分析一下这道题,k=2时,先从[1,2,3,4]中随机选取一个数,这里假设选了 1 ,那么第二次选取数字的范围就缩小成了[2,3,4], 同理,如果第一次随机选取了 2 ,那么第二次选取数字的范围就是[3,4]。注意这里之所以是[3,4]而不是[1,3,4]是因为这是个组合问题,并不是排列。

第一次选取范围		第一次选取	第二次选取范围		第二次选取	选取结果
  [1,2,3,4]          1			[2,3,4]			  2			 1,2
  [1,2,3,4]          1			[2,3,4]			  3			 1,3    
  [1,2,3,4]          1			[2,3,4]			  4			 1,4
  [1,2,3,4]  		 2          [3,4]			  3			 2,3
  [1,2,3,4]  		 2          [3,4]			  4			 2,4
  [1,2,3,4]			 3			[4]			 	  4			 3,4

很显然,k的取值就是选取数字的次数,如果使用for循环来解决这个问题的话,只有当k为常数的时候,才可以明确for循环会嵌套使用多少层。那么对于k是变量的情况,是不能用嵌套k层for循环来解决的,因为写不出来。

那么针对k层嵌套循环才能解决的问题,用什么来替代for循环呢?答案就是递归。在一个递归函数中写一个for循环,那么需要嵌套k层循环就变成了一个递归函数递归k次。

按照回溯三步法来考虑这个回溯问题:

1、函数参数和返回值:返回值一般为void;因为要把符合条件的结果存入数组,所以需要一个存放结果的集合vector<vector<int>> result,需要一个变量来记录每一个结果vector<int> path。又因为这是一个组合问题,每一次选取数字后,选取范围都要向后移动,所以需要一个开始下标startIndex表示选取范围的开始

2、终止条件:当path中数字的数量==k时,说明找到一个结果,此时将path存入result

3、单层处理逻辑:到了这里就说明上一步终止条件并没有被满足,那么就要从startIndex开始的范围内选取一个数字存入path,并且向下递归,注意此时startIndex应该++;与一般的递归不一样,回溯问题千万不能忘记撤销操作

下面是代码:

class Solution {
    
    
private:
    vector<vector<int>> result; //存放结果的集合
    vector<int> path; //结果
    void backTracking(int n, int k, int startIndex) {
    
     //1、回溯函数参数以及返回值
        if (path.size() == k) {
    
     //2、终止条件
            result.push_back(path);
            return;
        }

        //3、单层处理逻辑
        for (int i = startIndex; i <= n; ++i) {
    
    
            path.push_back(i);
            backTracking(n, k, i + 1);
            path.pop_back(); //4、回溯,撤销操作
        }

    }
public:
    vector<vector<int>> combine(int n, int k) {
    
    
        backTracking(n, k, 1);

        return result;
    }
};

剪枝

其实这道题按照上面的代码来说还是可以进行剪枝的,比如下面这种情况:

n = 4 k = 3
第一次选取范围		第一次选取	第二次选取范围		第二次选取	选取结果
  [1,2,3,4]          1			[2,3,4]			  2			 1,2
	......
需要剪枝的情况
  [1,2,3,4]          3
  [1,2,3,4]          4  

这是由于第一次取到的数字加上之后所有数字也不能满足path.size() == k,所以对于for循环的循环变量i结束时取值要进行约束,比如上面情况中就不必取到i = 3i = 4两种情况

因此,for循环修改为:

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
    
     // 剪枝
    path.push_back(i); // 处理节点 
    backtracking(n, k, i + 1);
    path.pop_back(); // 回溯,撤销处理的节点
}

注意这个循环变量i的终止条件可以理解为:i-1+(k-path.size()) <= n。从i开始还需要k-path.size()个元素,但此时i还没加进path中,所以实际起始元素是i-1,即i-1+(k-path.size()) <= n

猜你喜欢

转载自blog.csdn.net/weixin_44484715/article/details/113527253
今日推荐