当前位置: 首页 > news >正文

动态规划(01背包恰好装满型详解):和为目标值的最长子序列长度

0-1背包:有n个物品,第i个物品的体积为w[i],价值为v[i],每个物品至多选择一个,求体积和不超过capacity的最大价值和。

对于第i个物品,我们只有两种选择:选,或者不选。如果选,那么背包的剩余容量减少w[i].如果不选,那么背包的剩余容量不变。

而问题实际上是:在剩余容量为c时,从前i个物品中得到的最大价值和为多少?

根据分析,我们可以进一步拆分这个问题。如果不选,那么子问题就为:在剩余容量为c时,从前i-1个物品中得到的最大价值和为多少?如果选,那么子问题就为:在剩余容量为c-w[i]时,从前i-1个物品中得到的最大价值和为多少?

显然可以用动态规划了。

第一步:确定dp数组的意思:dp[i][j]表示:从第0-i种物品中挑选,放进容量为j的背包里,得到的最大价值和。

第二步:初始化dp数组(对边界情况进行处理):当背包容量为0时,无法装下物品,因此价值为0,由此可令dp[i][0]==0;当选择物品的范围为0-0时,意味着我们只能选第一个物品,那么最大价值和只能为v[0],由此可令dp[0][i]==v[0].

第三步:填充。由上可知,要么选,要么不选。但是我们不能忘记选择权从何而来:背包的剩余容量大于等于物品体积。如果不是,那么直接不选。如果是,那么就根据前面的分析进一步处理。如果不选则dp[i][j]=dp[i-1][j].如果选,那么dp[i][j]=dp[i][j-w[i]]+v[i].选择二者中的最大值即可。

int backpack01(int kinds, int capacity, vector<int>& w, vector<int>& v)
{
	//kinds表示物品数量,capacity表示背包总容量,w[i]表示第i+1个物品的体积,v[i]表示第i+1个物品的价值
	vector<vector<int>>dp(kinds, vector<int>(capacity + 1, 0));

	//初始化:

	//1.背包容量为0时,无法装下物品,最大价值和为0
	for (int i = 0; i < kinds; i++)
	{
		dp[i][0] = 0;
	}
	
	//2.只有一种物品时,只能选择第一个物品,最大价值和为v[0]
	for (int i = 0; i <= capacity; i++)
	{
		dp[0][i] = v[0];
	}

	//填充

	for (int i = 1; i < kinds; i++)//物品编号
	{
		for (int j = 1; j <= capacity; j++)//背包剩余容积
		{
			if (j < w[i]) dp[i][j] = dp[i - 1][j];//容不下当前物品,只能不选
			else
			{
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
			}
		}
	}
	return dp[kinds - 1][capacity];
}

我们发现,dp[i][?]只与dp[i-1][?]有关,因此我们可以对空间复杂度进行优化,用一个一维数组dp滚动处理即可。

int backpack01(int kinds, int capacity, vector<int>& w, vector<int>& v)
{
	vector<int>dp(capacity + 1, 0);
	//dp[i]表示:背包容量为i时里面物品的最大价值和
	dp[0] = 0;//背包容量为0,装不下物品,最大价值和为0
	for (int i = 0; i < w.size(); i++)//外层循环:控制某一种物品
	{
		for (int j = capacity; j >= w[i]; j--)//内层循环:对于该种物品进行处理
		{
			dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
		}
	}
	return dp[capacity];
}

关键点:j从capacity开始倒着处理。为什么要倒着处理?实际上,我们可以用“双数组”的理念去理解(双数组滚动也可以,只不过单数组空间复杂度更优,用双数组滚动更容易想象)。我们退化成两个数组,数组a用来保存上一轮的dp值,数组b用来填充接下来的dp值。前面我们说过,dp[i][x]实际上只由dp[i-1][x]决定,所以填充b只需要依赖a中的值。但也正因如此,我们在填充b的时候,就不能改变a中的值了。我们现在只有一个数组,如果我们从前往后处理,那么相当于:我们在填充b中dp值时,它所依赖的a中的dp值已经被改变了。所以我们需要从后往前处理,因为填充依赖前面的原始数据。两个关键词:前面的+原始。

同时我们还进行了一点优化:j>=w[i].这意味着我们不需要再判断当前背包的容量是否能够装下物品了,因为这时候物品已经定下来了(外层循环控制)。所以小于w[i]的j必然都装不下,就不用处理了。

看一道具体的题目:

2915. 和为目标值的最长子序列的长度 - 力扣(LeetCode)

本题中:下标相当于物品编号,下标对应的值相当于物品的体积,target相当于背包的容量,长度相当于价值。

int lengthOfLongestSubsequence(vector<int>& nums, int target)
{
	if (accumulate(nums.begin(), nums.end(), 0) < target) return -1;
	int kinds = nums.size(), capacity = target;
	vector<int>dp(capacity + 1, -1);
	dp[0] = 0;
	for (int i = 0; i < kinds; i++)
	{
		for (int j = capacity; j >= nums[i]; j--)
		{
			if (dp[j - nums[i]] != -1)//注意这里
			{
				dp[j] = max(dp[j], dp[j - nums[i]] + 1);
			}
		}
	}
	return dp[capacity];
}

为什么需要判断dp[j-nums[i]]是否等于-1?

我们先来想想等于-1意味着什么。dp[j-nums[i]]==-1意味着:在考虑前i个元素时,无法组成和为j-nums[i]的子序列。而只有当存在j-nums[i]的子序列时,我们才能通过添加当前元素nums[i]来得到和为j的子序列。

因此,如果dp[j-nums[i]]等于-1,那意味着没有基础来构建dp[j],所以不能更新。

相关文章:

  • An Easy Problem(信息学奥赛一本通-1223)
  • 第2.2节:运行AWK脚本方式
  • overleaf中会议参考文献使用什么标签:inproceedings
  • 脚对齐调研学习笔记
  • 【多线程】synchronized底层实现的方式
  • GPU 模型部署实战以 Llama3 70B、Qwen 与 DeepSeek 为例
  • 算法方法快速回顾
  • mysql入门操作
  • 交易在规则框架下运作,反而能强化自身纪律
  • JavaScript 在 Chrome 中运行详解
  • Buildroot 增加系统启动项并解决后台无法获取输入(串口)
  • PV操作指南
  • 【漏洞复现】Next.js中间件权限绕过漏洞 CVE-2025-29927
  • 穿越之程序员周树人的狂人日记Part7__教育战争2.0
  • C语言 —— 此去经年梦浪荡魂音 - 深入理解指针(卷四)
  • C++类与对象的第一个简单的实战练习-3.24笔记
  • 洛谷题单入门4-P5729 【深基5.例7】工艺品制作-python
  • 一文了解Gradle 依赖管理(五)- 依赖管理缓存依赖
  • K8S学习之基础四十五:k8s中部署elasticsearch
  • Spec2MP:项目管理之项目质量管理
  • 库尔德工人党决定自行解散
  • 应急部:正在积极推动各地逐步科学建设改造应急避难场所
  • 民生谣言误导认知,多方联动守护清朗——中国互联网联合辟谣平台2025年4月辟谣榜综述
  • 人民日报整版聚焦:专家学者看2025中国经济增长点
  • 铁肩担道义,历史鉴未来——中共中央政治局委员、外交部长王毅谈习近平主席对俄罗斯进行国事访问并出席纪念苏联伟大卫国战争胜利80周年庆典
  • 邯郸一酒店办婚宴发生火灾,新郎母亲:饭没吃成酒店还要收费