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

动态规划 - 背包问题

背包问题

背包问题概述

背包问题(Knapsack problem)是一种组合优化的NP 完全问题。其核心是:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,如何选择物品,才能使得物品的总价格最高。

按物品个数分类

根据物品的可选取个数,背包问题分为以下几类:

  • 01 背包问题:每个物品只有一个,即对于每个物品,只有 “选” 或 “不选” 两种决策。
  • 完全背包问题:每个物品有无限多个,可多次选取同一物品。
  • 多重背包问题:每件物品最多有 s_i个,s_i为第 i 种物品的数量上限),选取次数受限于该物品的数量。
  • 混合背包问题:每个物品可能属于上述 01、完全、多重背包中的任意一种情况,是多种背包类型的混合。
  • 分组背包问题:物品被分为 n 组,每组里有若干个物品,且每组里最多选一个物品。

按背包是否装满分类

在上述 “按物品个数” 的分类基础上,还可根据背包是否必须装满,分为两类:

  • 不一定装满背包:目标是在重量限制内,总价值最大,不要求背包完全装满。
  • 背包一定装满:不仅要总价值最大,还要求背包恰好被装满(若无法装满,则问题无解或需特殊处理)。

优化方案

为提升背包问题的求解效率(时间或空间),有多种优化思路:

  • 空间优化 - 滚动数组:利用 “01 背包” 等问题的状态转移特性,将二维 DP 数组压缩为一维,减少空间复杂度(如 01 背包的 “逆序遍历” 优化)。
  • 单调队列优化:针对 “多重背包”“完全背包” 等问题,用单调队列维护状态转移的最优值,将时间复杂度从 O(NV) 优化到 O(N)(N 为物品数,V 为背包容量)。
  • 贪心优化:在某些特殊场景下(如物品 “单位重量价值” 差异显著),用贪心策略(优先选单位重量价值高的物品)快速得到近似最优解(或在特定条件下的精确解)。

按限定条件个数分类

根据限制条件的数量,分为两类:

  • 限定条件只有一个:如仅限制 “体积”,这是最基础的背包问题(普通背包问题)。
  • 限定条件有两个:如同时限制 “体积 + 重量”,属于二维费用背包问题,需同时满足多个限制条件下的价值最大化。

按不同问法分类

根据问题的最终要求(“问法”),还可细分为多类:

  • 输出方案:不仅要总价值,还要输出具体选了哪些物品。
  • 求方案总数:不关注总价值,而是统计有多少种选法能满足重量限制(或同时满足价值要求等)。
  • 最优方案:最典型的问法,即求重量限制内的最大总价值。
  • 方案可行性:判断是否存在一种选法,能满足重量限制(或其他条件,如总价值不低于某个值)。

总结

背包问题种类繁多、题型丰富且难度灵活,但所有变种都是从 “01 背包问题” 演化而来。因此,掌握 “01 背包问题” 的解法(如动态规划思路),是学习其他背包问题的基础。

题目练习

【模板】01背包_牛客题霸_牛客网

解法(动态规划):

算法思路:

我们先解决第一问。

1. 状态表示:

dp[i][j] 表示:从 i 个物品中挑选,总体积不超过 j,所有的选法中,能挑选出来的最大值。

2. 状态转移方程:

线性 dp 状态转移方程一般都是根据 **「最后一步的状态」** 来分析:

  • 不选第 i 个物品:相当于就是前 i - 1 个物品中挑选,总体积不超过 j,此时 dp[i][j] = dp[i - 1][j]

  • 选第 i 个物品:那么就只能从前面 i - 1 个物品中挑选,并且总体积不超过 j - v[i],此时 dp[i][j] = dp[i - 1][j - v[i]] + w[i]

但这种情况下需要保证 j >= v[i]

因此,状态转移方程为:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])(当 j >= v[i] 时)。

3. 初始化:

我们先多加一行一列来处理边界情况:

  • 第一个格子为 0,因为正好能装体积为 0 的背包;

  • 但是第一行后面的格子都是 -1,因为没有物品,无法满足体积大于 0 的情况。

4. 填表顺序:

根据 **「状态转移」,我们需要「从上往下」填每一行,每一行「从左往右」**。

5. 返回值:

根据 **「状态表示」**,返回 dp[n][V]

接下来解决第二问,可只做第一问过程的五步即可。

因为有可能存在一个 j 不优化的物品,因此我们不合法的状态设置为 -1

#include <iostream>
#include <string.h>
using namespace std;const int N = 1010;
int n, V, v[N], w[N];
int dp[N][N];
int main() {cin >> n >> V;for(int i = 1; i <= n; ++i)cin >> v[i] >> w[i];for(int i = 1; i <= n; ++i){for(int j = 1; j <= V; ++j){dp[i][j] = dp[i - 1][j];if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);}}cout << dp[n][V] <<endl;memset(dp, 0, sizeof dp);for(int j = 1; j <= V; ++j) dp[0][j] = -1;for(int i = 1; i <= n; ++i){for(int j = 1; j <= V; ++j){dp[i][j] = dp[i - 1][j];if(j >= v[i] && dp[i - 1][j - v[i]] != -1) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);}}cout << (dp[n][V] == -1 ? 0 : dp[n][V]) <<endl;
}
// 64 位输出请用 printf("%lld")

空间优化:

背包问题基本上都是利用 「滚动数组」 来做空间上的优化:

利用「滚动数组」优化;

直接在「原始代码」上修改;

在 01 背包问题中,优化的结果为:

  • 删除所有的横坐标;
  • 修改一下 j 的遍历顺序。

我们甚至可以优化到一维数组:但是要注意填表顺序,防止错误的覆盖问题(我们选取从右往左的遍历顺序)

#include <iostream>
#include <string.h>
using namespace std;const int N = 1010;
int n, V, v[N], w[N];
int dp[N];
int main() {cin >> n >> V;for(int i = 1; i <= n; ++i)cin >> v[i] >> w[i];for(int i = 1; i <= n; ++i){for(int j = V; j >= v[i]; j--){dp[j] = max(dp[j], dp[j - v[i]] + w[i]);}}cout << dp[V] <<endl;memset(dp, 0, sizeof dp);for(int j = 1; j <= V; ++j) dp[j] = -1;for(int i = 1; i <= n; ++i){for(int j = V; j >= v[i]; j--){if(dp[j - v[i]] != -1) dp[j] = max(dp[j], dp[j - v[i]] + w[i]);}}cout << (dp[V] == -1 ? 0 : dp[V]) <<endl;
}
// 64 位输出请用 printf("%lld")

416. 分割等和子集 - 力扣(LeetCode)

解法(动态规划):

算法思路:

先将问题转化成我们 「熟悉」 的题型。

如果数组能够被分成两个相同元素之和相同的子集,那么原数组必须有下面几个性质:

  • ⅰ. 所有元素之和应该是一个偶数;

  • ⅱ. 数组中最大的元素应该小于所有元素总和的一半;

  • ⅲ. 挑选一些数,这些数的总和应该等于数组总和的一半。

根据前两个性质,我们可以提前判断数组能够被划分。根据最后一个性质,我们发现问题就转化成了 「01 背包」 的模型:

  • ⅰ. 数组中的元素只能选择一次;

  • ⅱ. 每个元素面临被选择或者不被选择的处境;

  • ⅲ. 选出来的元素总和要等于所有元素总和的一半。

其中,数组内的元素就是物品,总和就是背包。

那么我们就可以用背包模型的分析方式,来处理这道题。

请大家注意,「不要背」 状态转移方程,因为题型变化之后,状态转移方程就会跟着变化。我们要记住的是分析问题的模式。用这种分析问题的模式来解决问题。

1. 状态表示:

dp[i][j] 表示在前 i 个元素中选择,所有的选法中,能否凑成总和为 j 这个数。

2. 状态转移方程:

老规矩,根据 「最后一个位置」 的元素,结合题目的要求,分情况讨论:

  • ⅰ. 不选择 nums[i]:那么我们是否能够凑成总和为 j,就要看在前 i - 1 个元素中选,能否凑成总和为 j。根据状态表示,此时 dp[i][j] = dp[i - 1][j]

  • ⅱ. 选择 nums[i]:这种情况下是有前提条件的,此时的 nums[i] 应该是小于等于 j。因为如果这个元素都比要凑成的总和大,选择它就没有意义呀。那么我们是否能够凑成总和为 j,就要看在前 i - 1 个元素中选,能否凑成总和为 j - nums[i]。根据状态表示,此时 dp[i][j] = dp[i - 1][j - nums[i]]

综上所述,两种情况下只要有一种能够凑成总和为 j,那么这个状态就是 true。因此,状态转移方程为:

dp[i][j] = dp[i - 1][j];
if (nums[i - 1] <= j) dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i]];

3. 初始化:

由于需要用到上一行的数据,因此我们可以先把第一行初始化。

第一行表示不选择任何元素,要凑成目标和 j。只有当目标和为 0 的时候才能做到,因此第一行仅需初始化第一个元素 dp[0][0] = true

4. 填表顺序:

根据 **「状态转移方程」,我们需要「从上往下」填写每一行,每一行的顺序是「无所谓的」**。

5. 返回值:

根据 「状态表示」,返回 dp[n][aim] 的值。

其中 n 表示数组的大小,aim 表示要凑的目标和。

class Solution {
public:bool canPartition(vector<int>& nums) {int n = nums.size(), sum = 0, aim = 0;for(auto x : nums) sum += x;if(sum % 2) return false;aim = sum / 2;vector<vector<bool>> dp(n + 1, vector<bool>(aim + 1));for(int i = 0; i <= n; ++i) dp[i][0] = true;for(int i = 1; i <= n; ++i){for(int j = 1; j <= aim; ++j){dp[i][j] = dp[i - 1][j] || (j >= nums[i - 1] && dp[i - 1][j - nums[i - 1]]);}}return dp[n][aim];}
};

6. 空间优化:

所有的 「背包问题」,都可以进行空间上的优化。

对于 01 背包类型的,我们的优化策略是:

  • ⅰ. 删掉第一维;
  • ⅱ. 修改第二层循环的遍历顺序即可。
class Solution {
public:bool canPartition(vector<int>& nums) {int n = nums.size(), sum = 0, aim = 0;for(auto x : nums) sum += x;if(sum % 2) return false;aim = sum / 2;vector<bool> dp(aim + 1);dp[0] = true;for(int i = 1; i <= n; ++i){for(int j = aim; j >= nums[i - 1]; j--){dp[j] = dp[j] || dp[j - nums[i - 1]];}}return dp[aim];}
};

494. 目标和 - 力扣(LeetCode)

解法(动态规划):

算法思路:

本题可以直接用 「暴搜」的方法解决。但是稍微用数学知识分析一下,就能转化成我们常见的「背包模型」 的问题。

设我们最终选取的结果中,前面加 + 号的数字之和为 a,前面加 - 号的数字之和为 b,整个数组的总和为 sum,于是我们有:

  • a + b = sum

  • a - b = target

上面两个式子消去 b 之后,可以得到 a = (sum + target) / 2

也就是说,我们仅需在 nums 数组中选择一些数,将它们凑成和为 (sum + target) / 2 即可。

问题就变成了 「416. 分割等和子集」 这道题。

我们可以用相同的分析模式,来处理这道题。

1. 状态表示:

dp[i][j] 表示:在前 i 个数中选,总和正好等于 j,一共有多少种选法。

2. 状态转移方程:

老规矩,根据 「最后一个位置」的元素,结合题目的要求,我们有「选择」最后一个元素或者「不选择」 最后一个元素两种策略:

  • ⅰ. 不选 nums[i]:那么我们凑成总和 j 的总方案,就要看在前 i - 1 个元素中选,凑成总和为 j 的方案数。根据状态表示,此时 dp[i][j] = dp[i - 1][j]

  • ⅱ. 选择 nums[i]:这种情况下是有前提条件的,此时的 nums[i] 应该是小于等于 j。因为如果这个元素都比要凑成的总和大,选择它就没有意义呀。那么我们能够凑成总和为 j 的方案数,就要看在前 i - 1 个元素中选,能否凑成总和为 j - nums[i]。根据状态表示,此时 dp[i][j] = dp[i - 1][j - nums[i]]

综上所述,两种情况如果存在的话,应该要累加在一起。因此,状态转移方程为:

dp[i][j] = dp[i - 1][j];
if (nums[i - 1] <= j) dp[i][j] = dp[i][j] += dp[i - 1][j - nums[i - 1]];

3. 初始化:

由于需要用到 「上一行」 的数据,因此我们可以先把第一行初始化。

第一行表示不选择任何元素,要凑成目标和 j。只有当目标和为 0 的时候才能做到,因此第一行仅需初始化第一个元素 dp[0][0] = 1

4. 填表顺序:

根据 「状态转移方程」,我们需要「从上往下」填写每一行,每一行的顺序是「无所谓的」。

5. 返回值:

根据 「状态表示」,返回 dp[n][aim] 的值。

其中 n 表示数组的大小,aim 表示要凑的目标和。

class Solution {
public:int findTargetSumWays(vector<int>& nums, int target) {int n = nums.size(), sum = 0;for(auto x : nums) sum += x;int aim = (target + sum) / 2;if(aim < 0 || (target + sum) % 2) return 0;vector<vector<int>> dp(n + 1, vector<int>(aim + 1));dp[0][0] = 1;for(int i = 1; i <= n; ++i){for(int j = 0; j <= aim ; ++j){dp[i][j] = dp[i - 1][j] + (j >= nums[i - 1] ? dp[i - 1][j - nums[i - 1]] : 0);}}return dp[n][aim];}
};

6. 空间优化:

所有的 「背包问题」,都可以进行空间上的优化。

对于 01 背包类型的,我们的优化策略是:

  • ⅰ. 删掉第一维;
  • ⅱ. 修改第二层循环的遍历顺序即可。
class Solution {
public:int findTargetSumWays(vector<int>& nums, int target) {int n = nums.size(), sum = 0;for(auto x : nums) sum += x;int aim = (target + sum) / 2;if(aim < 0 || (target + sum) % 2) return 0;vector<int> dp(aim + 1);dp[0] = 1;for(int i = 1; i <= n; ++i){for(int j = aim; j >= nums[i - 1] ; j--){dp[j] += dp[j - nums[i - 1]];}}return dp[aim];}
};

1049. 最后一块石头的重量 II - 力扣(LeetCode)

解法(动态规划):

算法思路:

先将问题 「转化」 成我们熟悉的题型。

  • 任意两块石头在一起粉碎,重量相同的部分会被丢掉,重量有差异的部分会被留下来。那就相当于在原始的数据的前面,加上 「加号」或者「减号」,是最终的结果最小即可。也就是说把原始的石头分成两部分,两部分的和越接近越好。

  • 又因为当所有元素的和固定时,分成的两部分越接近数组 「总和的一半」,两者的差越小。

因此问题就变成了:在数组中选择一些数,让这些数的和尽量接近 sum / 2,如果把数看成物品,每个数的值看成体积和价值,问题就变成了 「01 背包问题」。

1. 状态表示:

dp[i][j] 表示在前 i 个元素中选择,总和不超过 j,此时所有元素的 「最大和」。

2. 状态转移方程:

老规矩,根据 「最后一个位置」 的元素,结合题目的要求,分情况讨论:

  • ⅰ. 不选 stones[i]:那么我们是否能够凑成总和为 j,就要看在前 i - 1 个元素中选,能否凑成总和为 j。根据状态表示,此时 dp[i][j] = dp[i - 1][j]

  • ⅱ. 选择 stones[i]:这种情况下是有前提条件的,此时的 stones[i] 应该是小于等于 j。因为如果这个元素都比要凑成的总和大,选择它就没有意义呀。那么我们是否能够凑成总和为 j,就要看在前 i - 1 个元素中选,能否凑成总和为 j - stones[i]。根据状态表示,此时 dp[i][j] = dp[i - 1][j - stones[i]] + stones[i]

综上所述,我们要的是最大价值。因此,状态转移方程为:

dp[i][j] = dp[i - 1][j];
if (j >= stones[i]) dp[i][j] = dp[i][j] + dp[i - 1][j - stones[i]] + stones[i];

3. 初始化:

由于需要用到上一行的数据,因此我们可以先把第一行初始化。

第一行表示 「没有石子」。因此想凑成目标和 j,最大和都是 0

4. 填表顺序:

根据 「状态转移方程」,我们需要「从上往下」填写每一行,每一行的顺序是「无所谓的」。

5. 返回值:

  • a. 根据 「状态表示」,先找到最接近 sum / 2 的最大和 dp[n][sum / 2]

  • b. 因为我们要的是两堆石子的差,因此返回 sum - 2 * dp[n][sum / 2]

class Solution {
public:int lastStoneWeightII(vector<int>& stones) {int n = stones.size(), sum = 0;for(auto x : stones) sum += x;int aim  = sum / 2;vector<vector<int>> dp(n + 1, vector<int>(aim + 1));for(int i = 1; i <= n; ++i){for(int j = 0; j <= aim; ++j){dp[i][j] = dp[i - 1][j];if(j >= stones[i - 1]) dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i -1]);}}return sum - 2 * dp[n][aim];}
};

6. 空间优化:

所有的背包问题,都可以进行 「空间」 上的优化。

对于 01 背包类型的,我们的优化策略是:

  • ⅰ. 删掉第一维;

  • ⅱ. 修改第二层循环的 「遍历顺序」 即可。

class Solution {
public:int lastStoneWeightII(vector<int>& stones) {int n = stones.size(), sum = 0;for(auto x : stones) sum += x;int aim  = sum / 2;vector<int> dp(aim + 1);for(int i = 1; i <= n; ++i){for(int j = aim; j >= stones[i - 1]; j--){dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i -1]);}}return sum - 2 * dp[aim];}
};

http://www.dtcms.com/a/454612.html

相关文章:

  • 科耐美安维可三文鱼焕颜精华液问世:妆字号无创水光引领护肤新趋势
  • dede减肥网站源码酒店建筑设计网站
  • 网站模板紫色网站做任务包括什么
  • 双等位基因:遗传学中的核心概念、分子机制与跨领域应用解析--随笔13
  • 广丰区建设局网站什么软件可以免费引流
  • 个人网站可以做信息网站吗专业网站建设办公
  • 百度云域名买了之后建设网站免费网站空间可访问
  • 制作动画的网站模板如何用运行打开wordpress
  • 北京模板网站开发北京注册公司规定
  • 江苏省住房和城乡建设局网站首页磁业 东莞网站建设
  • 建设校园网站的好处专业团队介绍文案
  • 儿童早教网站模板建设网站要那些
  • 淘宝客如何做自己的网站网架加工费多少钱一吨
  • 什么做婚车网站最大wordpress 点击弹出层
  • 网站建设丂金手指科杰新兴街做网站公司
  • (四) Dotnet为AI控制台添加日志输出
  • php做企业网站管理系统购物网站制作公司
  • Shell test 命令详解
  • html网站素材网网站建设课程设计文献综述
  • 现在还做自适应网站网站设计要学什么
  • 品牌网站建设的关键事项网站遮罩是什么
  • 营销型网站建设排名网站建设公司需要申请icp吗
  • 江门营销网站建设推广的公司
  • 呼和浩特企业网站排名优化昌邑建设局网站
  • 手机网站建设多少钿个人在线做网站免费
  • 企业网站开发时间好看的网站设计
  • 公司的服务器能建设网站吗中国住建网证书查询
  • 乐清建设路小学网站淘宝网站开发多少金额
  • 大四记录10.7
  • 网站繁简通dedecms做的网站手机上看图片变形