LeetCode 39. 组合总和

本文介绍了如何使用回溯算法解决LeetCode的39题——组合总和。分析了终止条件、回溯函数参数、避免重复的方法以及剪枝操作。在终止条件中,通过不断减去nums[i]直到target小于等于0。在回溯过程中,通过维护一个索引来避免重复选择。最后,通过排序数组进行剪枝操作,提高效率。
摘要由CSDN通过智能技术生成

39. 组合总和

  做了46题之后,大概明白了回溯搜索是怎么回事,紧接着我就回过头来做了这道题(怎么感觉有点回溯那味儿了?哈哈哈可能这就是缘分吧),结果一把过!我分析了一下,这道题的基本框架跟46-全排列差不多,都要搜索所有的情况,但是有一些不一样的地方:
  1. 终止的条件不一样:46题中要把给定数组中的数字都用一遍,所以终止条件是一个答案(path)中的元素个数等于给定数组的元素个数(length);而在这里,我们要找到一组目标元素,使得加起来等于目标值,所以最后的终止条件应该跟目标值target有关;
  2. 递归部分不一样:这里的元素可以重复使用,所以for循环的部分肯定不一样。
  但可以肯定的是,肯定有回溯的过程。


总的思路: 回溯搜索 + 无重复添加(不会反向添加)

1. 终止条件的确定

  由于是求和,且每次选取一个数,所以我就想到了,每次将target的值减去选取的nums[i]的值,直到最后这个target小于等于0了,表明再也找不到数了,因此递归终止。
  在递归终止的时候,也比较容易想到两种不同的情况。不像46题中那样,条件终止时,找到的集合肯定是满足条件的;在本题中,找到的可能不满足条件,因为全部的加起来小于了目标值target,因此需要判断target的最后值是否为0,如果是0,那么表示加起来等于原来的目标值,这种情况才应该加入最后的结果中;否则,不应该加入。但是终止条件里面,最后都应该返回了。这部分的代码比较简单,也很容易想到,如下:

if (target <= 0) {
     if (target == 0) {				// 只有目标值最后为0的时候,才加入结果集合
         res.emplace_back(path);
     }
     return;			// 终止了就都要返回
}

2. 回溯函数参数的确定以及避免重复

  最开始没考虑到重复的问题,我写的backtrack函数如下:

  void backtrack(vector<vector<int>>& res, vector<int>& nums, int target) {
	if (target <= 0) {
		if (target == 0) {
			res.emplace_back(path);
		}
		return;
	}

	for (int i = 0; i < nums.size(); i++) {
		path.push_back(nums[i]);
		backtrack(res, nums, target - nums[i]);	  // 递归的是target减去当前选取的元素的值
		path.pop_back();
	}
}

  这样做,跟46不一样。首先,没有用标记数组判断,因为每个答案中可能包含重复的元素,因此不能用used[i]来进行判断和处理;其次,没有一个显示的回溯过程,其实是把回溯过程写到target-nums[i]里面了,这样做并没有改变target的值,不会影响下一次第一个取nums[i+1];这样做相当于:

for (int i = 0; i < nums.size(); i++) {
		path.push_back(nums[i]);
		target -= nums[i]);
		backtrack(res, nums, target);	  // 递归的是target减去当前选取的元素的值
		path.pop_back();
		target += nums[i]);
	}

  但是我们可以看到,pop_back()依然存在,也是回溯必不可少的一部分。这样做可以得到所有的答案,但是会有重复的(错误的答案)。比如,nums = [2, 3, 5], target = 8;
  这个代码的输出结果有:[2, 2, 2, 2], [2, 3, 3], [3, 2, 3], [3, 3, 2], [3, 5], [5, 3]。其中[2, 3, 3], [3, 2, 3], [3, 3, 2]重复, [3, 5], [5, 3]也重复。经过思考,觉得应该是每次选取一个数nums[i]的时候,不应该再考虑它前面的数(索引小于i的那些数),比如在选取5的时候,就不应该考虑它前面的2和3。所以我想到for那里应该不是每次从0开始,因为这样每次都会从最开始选取,因此我想到了添加一个int索引,用来记录当前位置的方式。我在参数列表中添加了一个参数d,其初始值设为0,同时修改for循环的起始从d开始而不是0。同时,递归的时候,让d的递增,表示选取的位置向后移动了。(d++也可以用i代替:backtrack(res, nums, target - nums[i], i); 但似乎要慢点)。

void backtrack(vector<vector<int>>& res, vector<int>& nums, int target, int d) {
        if (target <= 0) {
            if (target == 0) {
                res.emplace_back(path);
            }
            return;
        }
        for (int i = d; i < nums.size(); i++) {
            path.push_back(nums[i]);
            backtrack(res, nums, target - nums[i], d++);
            path.pop_back();
        }
    }

3. 完整的代码

  最后整合一下得到了完整的代码,如下。

class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;

    void backtrack(vector<vector<int>>& res, vector<int>& nums, int target, int d) {
        if (target <= 0) {
            if (target == 0) {
                res.emplace_back(path);
            }
            return;
        }
        for (int i = d; i < nums.size(); i++) {
            path.push_back(nums[i]);
            backtrack(res, nums, target - nums[i], d++);
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        res.clear();
        path.clear();
        backtrack(res, candidates, target, 0);
        return res;
    }
};

4. 剪枝操作

  为了修剪不必要的操作,可以先对数组进行排序。为什么要从小到大排序?因为如果排了序,可以在循环的时候就判断,如果当前的都不满足条件(使得target为负了),那么后面的肯定更不复合条件了,因为后面的比当前的还大。当前选的这个加起来都超过了target,那么后面的肯定不需要考虑了,因此直接break掉,实现剪枝。如果不排序,那么就不行,因为后面可能有比当前值小的,说不定加起来就满足情况了,因此必须排序!!

class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;

    void backtrack(vector<vector<int>>& res, vector<int>& nums, int target, int d) {
        if (target == 0) {			// 由于进入更深层的时候,小于 0 的部分被剪枝,因此递归终止条件值只判断等于 0 的情况
            res.emplace_back(path);
            return;
        }
        for (int i = d; i < nums.size(); i++) {
            if (target - nums[i] < 0) {	      // 重点理解这里剪枝。直接在这里判断一手,如果当前的使得target为负了,后面的都不需要考虑了。
			    break;			// 直接跳出来!
		    }
            path.push_back(nums[i]);
            backtrack(res, nums, target - nums[i], d++);
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        res.clear();
        path.clear();
        sort(candidates.begin(), candidates.end());				// 首先对数组排序
        backtrack(res, candidates, target, 0);
        return res;
    }
};

? 什么时候使用 used 数组,什么时候使用d(begin)变量

  这里从LeetCode的一个解析中看到,感觉说得很有道理。简单总结一下:

  • 排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组;
  • 组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。

总结

  总的来说,此题比46略微复杂一点,关键是如何避免重复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值