动态规划第九题(LeetCode:416. 分割等和子集)


前言

第八题写在了背包问题里,是一个原汁原味的01背包问题
刚接触背包问题,对于变型脑子还转不太过来。
按照代码随想录刷题的弊端—固定了做题思维,只会往题目所在章节介绍的方法去思考问题

这是第一道01背包的应用
乍一看 一点思路都没
还是得看题解才能有思路

这题写完了后续的题解可能就不写这么详细了,各种变型写在背包问题的那一篇里
贴一个老哥的题解,感觉他的思路很好


一、题目

416. 分割等和子集
在这里插入图片描述

二、题解

1.0_思考(胡思乱想)

分为两种情况讨论

1^数组和为奇数,此时因为数组元素均为整数,所以无法分割数组,返回false
2^和为偶数,此时可以将sum/2设为背包容量,遍历数组,寻找能够恰好装满背包的组合,只要找到存在的情况,就可以返回true
按我上面描述,个人感觉可以用深搜回溯求和,当递归参数大于sum/2时回溯
等于时返回true
这是一题之前做过的回溯题,和上面大同小异(其实有好多题都很相似)
40. 组合总和 II
494. 目标和(这一题基本一样)

看题解的时候还有看到一个记忆化搜索,等理解完动态规划的01背包,再把那个弄懂一下
看了一下,记忆化搜索好像是深搜加剪枝,和我这边的优化完的回溯差不多?

1.1_回溯法

回溯的本质是暴力
一般有更好的做法时,用回溯都很容易超时

优化:

for (int i = startIndex; i < nums.size()&& ①sum+nums[i]<=target; i++) {
if (i > startIndex && nums[i] == nums[i - 1])
continue;

sum += nums[i];
backtracking(nums, i + 1);
sum -= nums[i];
}

主要是剪枝操作
①在for循环上判断下一次会不会越界,越界就提前结束而不是进入下一层递归,节省下面的赋值和系统调用栈的时间。

②树层剪枝:当数组出现大量重复数据时,以第一个重复元素开始的遍历包括了以后面全部节点作为起点的情况,因此可以将以非第一个重复元素作为起点的其他树枝全部剪掉
上面代码里是不用visit数组的做法

代码随想录中在这一题优化中给出树层剪枝的详细解释(挂在这方便我复习)

因为技术菜,优化到这就是极限了,还是会超时,只能换方法了。
有时候题目的测试用例数据量小,还是可以用回溯法,比较好想。

回溯的完整代码:

bool ans=false;
int sum = 0;
int target;
int pre;

void backtracking(vector<int>& nums, int startIndex) {
	// 递归结束条件
	if (sum == target||ans)
		ans = true;
	// 进入递归
	// 每个数字只能选一次->添加参数startIndex
		// 重复数字剪枝
	for (int i = startIndex; i < nums.size()&&sum+nums[i]<=target; i++) {
		if (i > startIndex && nums[i] == nums[i - 1])
			continue;
		sum += nums[i];
		backtracking(nums,  i + 1);
		sum -= nums[i];
	}
}

bool canPartition(vector<int>& nums) {
	// 先判断
	总和的奇偶
	int sum = 0;
	for (int num : nums)
		sum += num;
	if (!(sum % 2)){
		target = sum/2;
		sum = 0;
		sort(nums.begin(), nums.end());
		backtracking(nums,0);	// 总和若为偶数,则进入递归进行回溯,递归内部如果出现符合条件的情况,会将ans改为true
	}
	return ans;	
}

1.2_动态规划

原始的01背包递推公式:

dp[v]=max(dp[v],dp[v-volume[i] ]+worth[i] )

本题要求判断能否找到总和为target

dp数组含义(这题里和物品价值无关)
直接思考一维数组比较困难,可以先从二维数组入手,最后代码写成一维数组就行
dp[n] [v]:有n个物品时,能否恰好装满容量为v的背包
如果不能装满就填入前一个能够恰好转满的背包容量
所以数组需要开target+1的大小
遍历到dp[target]得到答案
如果dp[target]等于target说明物品恰好能够装满容量为target的背包
递推公式:
如何控制物品不重复放入(遍历dp数组时从后往前遍历
第一层for循环(i ; 0->nums.size() ; i++):

第i重循环代表当前可以选取前i个物品
第二重for循环(t ; target->nums[t] ; t-- ):// 部分剪枝:当前物品体积(当前数字大小)大于背包容量(target)时dp值等于i-1层时的dp[t]值,因为是一维数组,所以不用做转移

遍历dp数组,此时可以对dp数组赋值
dp[t]=max(dp[t],dp[t-num[i] ]+nums[i] ) // nums[i]为当前循环对应的第i个数组值

代码:

    bool canPartition(vector<int>& nums) {
	// 动态规划_01背包
	int sum = accumulate(nums.begin(), nums.end(), 0);
	if (sum % 2)
		return false;
	int target = sum / 2;	// target为后续要求和得到的目标值
	// 定义dp数组&全部初始化为0
		// dp数组含义:离i最近的一个放满的索引
	vector<int> dp(target+1,0);
	// 递推遍历
		// 等价于:for(int i;i<nums.size();i++) nums[i];
	for (int numi : nums)	// 遍历nums数组
		// 从后往前遍历,仅当背包容量大于numi(遍历到nums数组值)时进入循环 
		for (int t = target; t >= numi; t--)
			// 
			dp[t] = max(dp[t], dp[t - numi] + numi);

	return dp[target] == target;
    }

每天学一招:accumulate-C++的库函数-累加


总结

这一题一直磨磨蹭蹭的,搞了一天才写完
做完稍微觉得自己又懂了一点背包问题的本质。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值