动态规划背包问题
这一篇是针对动态规划中一系列背包问题的总结以及接解题模板,在考试里一般不会出现很浅显的背包问题来求解,而是运用了背包的思想进行求解,因此深刻理解背包问题十分重要。
普通动态规划回忆
如走迷宫问题:只能走上和右,状态转移方程如下:
0-1背包
描述:有 N 件物品和一个容量为 V 的背包。 第 i 件物品体积是 W i W_i Wi,价值是 V i V_i Vi 。 求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且总价值最大。
特点:每种物品仅有一件,可以选择放或不放 (对应 1 或 0)。
为什么要遍历容量,因为背包的容量很大,可以装的物品有很多,因此要一步步扩大背包容量再减去当前要放的物品的重量来知道前面最优能放什么物品进去,然后就可以一步步地扩展
状态转移方程
普通解法
f[N][V]为其答案。
滚动数组优化空间复杂度
观察优化公式,可以看出来:每次更新都只需要用到上面那一行,和当前列的前面的 w i w_i wi 列,即 f[i][j] 是由 f[i-1][j] 和 f[i-1][j-w[i]] 两个子问题递推过来的,如下图展示了迭代过程:
那么就可以将更新数组压缩成只开一个容量为 V 的数组,并采用逆序更新,因为这样可以保证在求解的时候所用的 f[i-1][j] 和 f[i-1][j-w[i]] 就是上一次更新留下来的且不会被覆盖。
完全背包
描述:有 N 种物品和一个容量为 V 的背包。 第 i 件物品体积是 w i w_i wi,价值是 v i v_i vi。 求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且总价值最大 。
特点:每种物品有无数件,可以选择 0 或多件。
注意:每种物品最多 V ÷ w[i] 个
状态转移方程
但是这样时间复杂度太高,是 O ( N V ∑ i = 1 N ( V w i ) ) O(NV\sum_{i = 1}^{N}({\frac{V}{w_i}})) O(NV∑i=1N(wiV)) ,需要进行优化。
即在选择了第 i 个物品后,更新的方程仍从不选这个物品和前 i 个物品选一个来判断,注意这里:即使第 i 个物品选了,也可以再选一次,因为更新项目是从前 i 个物品选一个,这就实现了一个物品可以装多次的情况,并且不会超过限制。不选这个物品的情况是价值没有提升或者以下这种:假如有两件物品 i, j,满足: w i < = w j w_i<=w_j wi<=wj 且 v i > = v j v_i >= v_j vi>=vj 那么就不选物品 j 了。
普通写法
用滚动数组优化
这里使用的是 f i − 1 , j f_{i-1,j} fi−1,j 和 f i , j − w i + v i f_{i,j - w_i} + v_i fi,j−wi+vi ,因此为了保证在 j − w i j - w_i j−wi 列上是前 i 个物品的价值最大,需要使用正序更新,才能保证已经是更新到前 i 个而不是前 i - 1 个,这是和 0-1 背包不同的地方。
多重背包
描述:有 N 件物品和一个容量为 V 的背包。 第 i 件物品体积是 w i w_i wi,价值是 v i v_i vi,有 c i c_i ci 件可用,求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且总价值最大。
特点:每种物品有限件
状态转移方程
时间复杂度是 O ( N V ∑ i = 1 N ( c i ) ) O(NV\sum_{i = 1}^{N}(c_i)) O(NV∑i=1N(ci)) ,有点大,需要优化。
普通写法
二进制拆分优化
优化思想:对每一种物品拆分成若干个物品的组合(分组后要求和原来等价),再使用 0-1 背包求解。
分组方法是先使用 2 的幂次来分组,剩下的不够递增的 2 的幂次的就自己分为一组。
如一个物品可以选 13 件,那么可以:
可以看出:1,2,4可以任意组成 [0, 7] 之间的数,而再往上应该拆出 2 4 2^4 24 这组,但是剩下只有 6,就自成一组。
由于由第i个物品拆分出来的新的物品通过组合可以等同于原来第i个物品选择任意个(小于 c i c_i ci 个),因此可以通过 0-1 背包的方法来求解这个问题。由于有个数 c i c_i ci 个的限制,因此不能使用之前完全背包滚动数组的优化方法,因为非常难统计已经选了多少个。
分组背包
描述:有 N 件物品和一个容量为 V 的背包,第 i 种物品的体积是 w i w_i wi ,价值 v i v_i vi, 所有的物品划分成若干组,每个组里面的物品最多选一件。求解将哪些 物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大 。
特点是:每种物品有 1 件,每组只能选 1 件。
如:
状态转移方程
因此可以设计为与完全背包类似:
普通写法
滚动数组优化
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[100000] = {0};
int main()
{
ll N, V;
cin >> N >> V;
vector<vector<pair<ll, ll>>> wupin(1000);//记录每一组有哪些物品
ll K = 0;
for (ll i = 0; i < N; i++)
{
ll w, v, k;
cin >> w >> v >> k;
K = max(k, K);
wupin[k].push_back(make_pair(w, v));
}
for (ll i = 1; i <= K; i++)//遍历每一个组
{
for (ll j = V; j >= 0; j--)
{
for (ll kk = 0; kk < wupin[i].size(); kk++) //遍历组里的每一个物品
if (j - wupin[i][kk].first >= 0)
dp[j] = max(dp[j], dp[j - wupin[i][kk].first] + wupin[i][kk].second);
}
}
cout << dp[V];
return 0;
}
超大背包
描述:有 N 件物品和一个容量为 V 的背包。第 i 种物品的体积是 w i w_i wi ,价值 v i v_i vi。 求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
约束:N <= 40, w i < = 1 0 15 w_i <= 10^{15} wi<=1015 , v i < = 1 0 15 v_i <= 10^{15} vi<=1015
由于背包容量过大,使用动态规划在设置数组就会超过内存,因此不使用DP。
思路
一般这种问题的 N 都不会很大,回归朴素解法,枚举 N 的所有子集,但是需要进行一点优化。
1、首先将物品分成两组,每组 N/2(或 N/2 - 1) 个物品,为了方便,将第一组物品设为 n1 个,第二组物品设为 n2 个,则n1 + n2 = N。
2、对于这两组中的第 i 组 (i = 1,2) ,找出这些物品的 $2^{n_i} $ (i = 1,2) 种组合情况,并将每种组合情况的重量和价值改写成 < w , v > <w, v> <w,v> 的形式添加到记录组合情况的数组 f e n z u i fenzu_i fenzui (i = 1,2) 中,按 w 进行排序,之后进行筛选,那些如 a [ i ] . w > a [ j ] . w a[i].w > a[j].w a[i].w>a[j].w ,且 a [ i ] . v < a [ j ] . v a[i].v < a[j].v a[i].v<a[j].v,那么 a[i] 就可以舍去, 因为选择 a[j] 一定更值。
对于枚举物品的 $2^{n_i} $ (i = 1,2) 种组合情况的方法:基本思想是使用二进制在每一位的 0/1 来判断,因为有 $2^{n_i} $ 种,因此可以让 j in [0, 2 n i 2^{n_i} 2ni] ,对于每一个 j ,令 t = j,每一次 t 和 1 按位与之后就让 t = t / 2,直到 t = 0,这样就可以判断 t 的最低位是 0 还是 1 ,就可以判断当前这个物品要不要选进去当前这个枚举方案。
3、在 fenzu1 和 fenzu2 都已经去除掉那些性价比低的数组后,遍历 fenzu1 数组,也就是假设 fenzu1 数组中的第 k 种方案被采用,也就是 < w k , v k > <w_k, v_k> <wk,vk> 被放进背包了,则现在需要在 fenzu2 中寻找方案中价值最大并且这个方案的重量小于等于 V − w k V - w_k V−wk ,可以使用 upper_bound 函数实现。
代码实现
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
vector<pair<ll, ll>> wupin; //记录最开始题目所给的物品
vector<pair<ll, ll>> fenzu1; //记录分组后第一组枚举的方案
vector<pair<ll, ll>> fenzu2; //记录分组后第二组枚举的方案
int main()
{
ll N, V; //记录物品的个数和背包的容量
cin >> N >> V;
for (ll i = 0; i < N; i++)
{
ll w, v;
cin >> w >> v;
wupin.push_back(make_pair(w, v));
}
ll n1 = N / 2; //分组后第一组的个数
for (ll i = 0; i < pow(2, n1); i++)//!注意这里要从0开始枚举,也就是一个物品都不选入方案。当每一个物品的重量已经大于背包容量的时候就有可能出现这种情况
{
ll w = 0, v = 0;
for (ll j = 0, t = i; t > 0 && j < n1; t /= 2, j++)//查看在这种方案下哪些物品应该被选进去
{
if (t & 1 == 1)//当这个物品对应的判断选不选的那个二进制位是 1 的时候,这个方案就可以选择这个物品
{
w += wupin[j].first;
v += wupin[j].second;
}
}
if (w <= V) //优化时间复杂度的关键,选择能放进背包的方案就可以了
fenzu1.push_back(make_pair(w, v));
}
ll n2 = N - n1; //分组后第二组的个数
for (ll i = 0; i < pow(2, n2); i++)//!注意这里要从0开始枚举,也就是一个物品都不选入方案。当每一个物品的重量已经大于背包容量的时候就有可能出现这种情况
{
ll w = 0, v = 0;
for (ll j = n1, t = i; t > 0 && j < N; t /= 2, j++)
{
if (t & 1 == 1)
{
w += wupin[j].first;
v += wupin[j].second;
}
}
if (w <= V)
fenzu2.push_back(make_pair(w, v));
}
//去除掉第一组枚举所有可能的方案后的那些重量大价值还小的方案
sort(fenzu1.begin(), fenzu1.end());
for (auto i = fenzu1.begin(); i != fenzu1.end() - 1;)//每次对比当前这个方案和后一个方案。为了防止越界,并且方案是和后一种方案做对比的,因此遍历到倒数第二个方案就行
{
if (i->first == (i + 1)->first)//当重量相同就去掉那个价值小的
{
if (i->second > (i + 1)->second)
fenzu1.erase(i + 1);
else
fenzu1.erase(i);
}
else//当重量不同就去掉那个重量大价值还小的
{
if (i->second >= (i + 1)->second)
fenzu1.erase(i + 1);
else
i++;
}
}
//去除掉第二组枚举所有可能的方案后的那些重量大价值还小的方案
sort(fenzu2.begin(), fenzu2.end());
for (auto i = fenzu2.begin(); i != fenzu2.end() - 1;)
{
if (i->first == (i + 1)->first)
{
if (i->second >= (i + 1)->second)
fenzu2.erase(i + 1);
else
fenzu2.erase(i);
}
else
{
if (i->second > (i + 1)->second)
fenzu2.erase(i + 1);
else
i++;
}
}
ll value = 0; //最终的最大价值
for (ll i = 0; i < fenzu1.size(); i++)
{
ll shengyukongjian = V - fenzu1[i].first; //选了fenzu1[i]方案后背包的剩余容量
ll hefaxiabiao = upper_bound(fenzu2.begin(), fenzu2.end(), make_pair(shengyukongjian, 0), [](pair<ll, ll> lhs, pair<ll, ll> rhs)//使用的lambda函数对于第一个关键字进行查找
{ return lhs.first < rhs.first; }) -
fenzu2.begin();//找到第一个大于此时背包剩余容量的方案
if (hefaxiabiao == 0)//当fenzu2的第一种方案的重量就已经超过背包剩余容量的时候,由于方案是按照重量升序的,并且重量是唯一的(因为重量相同的时候就选择了那个价值大的方案),因此出现这种情况就说明整个 fenzu2 已经没有合适的方案了,此时只选 fenzu1 的方案就行
value = max(value, fenzu1[i].second);
else
{
hefaxiabiao--;//由于找到的是第一个大于此时背包剩余容量的方案,并且方案是按照重量升序的,是唯一的,所以下标减去1就是那个合法的方案,且在上面那个if语句就判断了找到的下标是0的情况,所以这里减1不会越界
value = max(value, fenzu1[i].second + fenzu2[hefaxiabiao].second);
}
}
cout << value;
return 0;
}