背包问题讲解

背包问题

前言:大年初一,祝大家新的一年心想事成~

一、01背包

题目背景:

       有 n 个物品,第 i 个物品体积为 vi ,价值为 ci ,现在有一个容积为 V 的背包,要求从这 n 件物品中选出一些装入背包中,使得装入背包的物品体积之和不超过背包的容积 V ,并且装入背包的物品价值和最大。

一般来讲这样的过程我们都可以用动态规划的思想去解决。

用动态规划解决问题需要经过如下几个步骤:

  1. 确定动态规划的状态表示,即 DP 数组代表的含义

  2. 根据第(1)步确定的状态表示来推想动态规划转移方程

  3. 分析转移复杂度,如复杂度较高,尝试优化动态规划转移复杂度或重新定义状态表示(即回到第 1 步)

以本题 01 背包问题为例,按照上述步骤进行求解:

(1) 令 DP[i][j] 表示在前 i 件物品中选取若干件物品,且物品体积和不超过 j ,能取到的物品的最大价值。若能求解出 DP 数组,显然 DP[n][V] 就是我们要求的答案了。

(2) 考虑如何求得 DP[i][j] 。根据我们定义的状态, DP[i][j] 表示在前 i 件物品中选取若干件体积和不超过 j 的最大价值, DP[i][j] 与 DP[i−1][] 的差别只在于第 i 件物品在 DP[i−1][] 中不被考虑,因此我们可以通过 DP[i−1][] 推出 DP[i][j] 。分别考虑第 i 件物品选或不选。对于 DP[i][j] 来说,若选择第 i 件物品,那么前 i−1 件物品中选取出的总体积不能超过 j−vi (因为限定了前 i 件物品中选出的总体积不超过 j ),最大价值为 DP[i−1][j−vi]+ci ;若不选择第 i 件物品,前 i−1 件物品中选取出的物品总体积不能超过 j ,最大价值为 DP[i−1][j] 。因此,动态规划转移方程为:

DP[i][j]=max(DP[i−1][j],DP[i−1][j−vi]+ci)

(3) 不难发现,求解单个状态 DP[i][j] 的复杂度仅为 O(1) ,因此总时间复杂度即为状态数 O(nV) 。

附上代码👇

#include<bits/stdc++.h>
#define N 202
#define M 10010
using namespace std;
int n, V;
int v[N], c[N];
int dp[N][M];
int main()
{
    cin >> n >> v;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> c[i];
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= V; j++)
        {
            if (j >= v[i])
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + c[i]);
            else 
                dp[i][j] = dp[i - 1][j];
        }
    cout << dp[n][V] << endl;
}

二、完全背包

背景描述:

       有 n 个物品,第 i 个物品体积为 vi ,价值为 ci ,现在有一个容积为 V 的背包,要求从这 n 件物品中选出一些装入背包中,使得装入背包的物品体积之和不超过背包的容积 V ,并且装入背包的物品价值和最大。

用动态规划解决问题需要经过如下几个步骤:

  1. 确定动态规划的状态表示,即 DP 数组代表的含义

  2. 根据第(1)步确定的状态表示来推想动态规划转移方程

  3. 分析转移复杂度,如复杂度较高,尝试优化动态规划转移复杂度或重新定义状态表示(即回到第 1 步)

以本题 01 背包问题为例,按照上述步骤进行求解:

(1) 令 DP[i][j] 表示在前 ii 件物品中选取若干件物品,且物品体积和不超过 j ,能取到的物品的最大价值。若能求解出 DP 数组,显然 DP[n][V] 就是我们要求的答案了。

(2) 考虑如何求得 DP[i][j] 。根据我们定义的状态, DP[i][j] 表示在前 i 件物品中选取若干件体积和不超过 j 的最大价值, DP[i][j] 与 DP[i−1][] 的差别只在于第 i 件物品在 DP[i−1][] 中不被考虑,因此我们可以通过 DP[i−1][] 推出 DP[i][j] 。分别考虑第 i 件物品选或不选。对于 DP[i][j] 来说,若选择第 ii 件物品,那么前 i−1 件物品中选取出的总体积不能超过 j−vi (因为限定了前 i 件物品中选出的总体积不超过 j ),最大价值为 DP[i−1][j−vi]+ci ;若不选择第 i 件物品,前 i−1 件物品中选取出的物品总体积不能超过 j ,最大价值为 DP[i−1][j] 。因此,动态规划转移方程为:

(3) 不难发现,求解单个状态 DP[i][j] 的复杂度仅为 O(1),因此总时间复杂度即为状态数 O(nV)。

代码👇

#include<bits/stdc++.h>
#define N 202
#define M 10010
using namespace std;
int n, V;
int v[N], c[N], f[N][M];
int main()
{
    cin >> n >> v;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> c[i];
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= V; j++)
        {
            if (j >= v[i])
                f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + c[i]);
            else 
                f[i][j] = f[i - 1][j];
        }
    cout << f[n][V] << endl;
}

类似于 01 背包,完全背包也可以用滚动数组进行优化。

完全背包同样可以像 01 背包那样直接用一个一维数组记录状态,按特定的顺序来进行求解。

 

看看 j 的枚举顺序,在 01 背包中,我们要求枚举 j 时必须要从大到小枚举,但在完全背包中我们要求 j 必须从小到大枚举!

代码上的差别仅此而已,但他们的含义可大不相同。

和刚才一样,考虑当外层循环枚举到 i ,内层循环枚举到 j 时,在这里 f[j−1],f[j−2],...,f[0]f[j−1],f[j−2],...,f[0] 的值都在这一轮被更新过了,所以他们实际上代表着

所以我们同样也不过是把 f 数组代入了原本的完全背包递推方程:

三、多重背包

背景描述:

       有 N 种物品,每种物品的数量为 C1,C2......Cn。从中任选若干件放在容量为 W 的背包里,每种物品的体积为 W1,W2......Wn ( Wi 为整数),与之相对应的价值为 P1,P2......Pn( Pi为整数)。求背包能够容纳的最大价值。

他不同于其他背包的特点就是:取物个数是有限的!

思路如下👇

因为对于第 ii 种物品有 Mi+1 种策略:取 0 件,取 1 件……取 Mi 件。令 F[i,v] 表示前 i 种物品恰放入一个容量为 v 的背包的最大价值,则有状态转移方程:

F[i,v]=max(F[i−1,v−k×vi]+k×ci) | (0≤k≤Mi)

状态数为 O(nV) ,对于每种状态 F[i,v] ,求解的复杂度为 O(Mi) 。

因此总复杂度为 

也可以把第 ii 种物品拆分成 Mi个,然后就变成了 01 背包问题, 01 背包的方法去做也是这个复杂度,可以把拆分后的物品数量 ∑Mi 代入计算复杂度,两者是相同的。

算法优化

除了直接将第 i 种物品拆分成 Mi 个之外,还可以用二进制的思想来拆分以降低复杂度。

第 i 种物品有 Mi 个,可以拆分成

即拆分成 k+2 个物品,每个物品的(体积,价值)分别为 :

之后做 01 背包即可。为什么可以这样做呢?

原因很简单,对于 [1,Mi] 的任何一个数 x ,我们都可以从:

这几个数字中选出若干个加起来得到。

所以相当于我们对每种物品进行了分组,每组有  个。假设在最优方案中第 i 种物品要被选 x 个,只要从这些组中挑选合适的一些组,物品的个数之和就能凑出 x 了。

例如,如果 M[i] 为 13 ,就将这种物品分成系数分别为 1,2,4,6的四件物品。

当第 i 种物品要选 10 件时,我们只需选择 6 个的那组和 4 个的那组即可。

同学们可以再试试不同的数字,都是可以取出来的。

这个原理类似于可以用 1,10,100,1000,...中的某几个数相加得到任何一个正整数一样。

这样我们就将第 ii 种物品拆分成了 组,因此在做 0101 背包时,就有 ∑logMi 个物品,总复杂度 

 

四、分组背包

问题背景:

       有 N 件物品,告诉你这 N 件物品的重量以及价值,将这些物品划分为 K 组,每组中的物品互相冲突,最多选一件,求解将哪些物品装入背包可使这些物品的费用综合不超过背包的容量,且价值总和最大。

这个问题变成了每组物品有若干选择策略:是选择本组中的一件还是一件都不选。

设 DP[k][j] 表示考虑前 kk 组物品可获得的最大价值,状态转换方程为:

五、总结总结

虽然我们可以利用动态规划的方法,来解决背包问题,但如果背包容量很大时(比如10^1^2),计算机内存中无法存储规模如此巨大的状态,则 DP 的方法就失效率。所以01背包问题,也被称为 NPC 问题,当背包容量巨大时,唯一有效的方法还是搜索,利用 DFS 求得最优解,复杂度大约为 O(2^n) ,其中 n 为物品数量,虽然可以有些优化及剪枝的方法,但仍然是指数级的复杂度。算法之神 Knuth 曾经出过一道非整数的01背包问题,大意如下:

将 \sqrt{1} ,\sqrt{2},\sqrt{2}.......\sqrt{50} 这 50 个数分为 2 组,让两组的和差值最小。

这样的题目无法用动态规划去做,因为可以凑出的数字数量高达指数级。

对于这样的题目,动态规划的优势就体现不出来了。

 

最后祝大家新年快乐呦~

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值