文章目录
- 背包九讲
- [[题库 - AcWing](https://www.acwing.com/problem/search/1/?search_content=背包九讲\&show_algorithm_tags=0)](#题库 - AcWing)
背包九讲
博客里的题目、内容都在这个链接里
题库 - AcWing
背包问题是一类组合优化问题,是动态规划中的
一个重要而特殊的模型。核心是,将一堆有一定体积、价值的物品,选择若干放进容量有限的背包,是总价值最大。
在不了解DP之前,面对这样的选择问题,也许反应就是暴力枚举不同选择,找到最优,也就是搜索。
而动态规划的巧妙就体现出来了,究其如何巧妙,各位先看01领会一下
01背包
看模板题:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i i i 件物品的体积是 v [ i ] v[i] v[i],价值是 w [ i ] w[i] w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
暴力枚举每一种选择,肯定T,
既然叫01背包 ,顾名思义,每一个物品,只有两种可能,选/不选 。而一个数字的选择与否自然会影响最终的结果。问题的求解一直在动,看似很难有一个确定的方式得出答案
这时候如果有一个确定的 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],表示对于前 i − 1 i-1 i−1 个物品,体积为 j j j时,可容纳的最大价值。
当我们考虑下一个物品 i i i
{ d p [ i ] [ j ] = d p [ i − 1 ] [ j ] (不选择该物品) d p [ i ] [ j ] = d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] (选择) \left\{\begin{matrix} dp[i][j]= dp[i-1][j] (不选择该物品) \\ \\ dp[i][j]=dp[i-1][j-v[i] ]+w[i](选择)\end{matrix}\right. ⎩ ⎨ ⎧dp[i][j]=dp[i−1][j](不选择该物品)dp[i][j]=dp[i−1][j−v[i]]+w[i](选择)
- 不选择物品 i i i, 对于体积 j j j, 所容纳的最大价值也就是只考虑前 i − 1 i-1 i−1的结果
- 选择物品 i i i, 已知 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],同样的体积只考虑了前 i − 1 i-1 i−1,此时要在其中加入 i i i, 就要在 j j j中让出 v [ i ] v[i] v[i]个位置, 所以,用 w [ i ] + d p [ i − 1 ] [ j − v [ i ] ] w[i]+dp[i-1][j-v[i] ] w[i]+dp[i−1][j−v[i]](只考虑前 i − 1 i-1 i−1个物品,体积为 j − v [ i ] j-v[i] j−v[i]时的最大价值) 。
最后将最大的赋值给 d p [ i ] [ j ] dp[i][j] dp[i][j]。
这样不就解决了不确定性的问题 ,只要我们得到确定最优的 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],问题就解决了。** d p [ N ] [ V ] dp[N][V] dp[N][V]**自然就是答案了。
那如何得知 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]呢
相同的,向前查找 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],这就类似dfs了,一直搜索到 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0](数组要怎么初始化赋值?后面会说)。搜索完就要回溯赋值了。
而动态规划就像是回溯 ,得到答案的过程。用式子表达出来,就是上面的递推公式。
问题的答案其实就是一张二维表(dp思想就蕴含其中)
针对这个样例
c++
4 5
1 2
2 4
3 4
4 5
物品i(v,w) ||体积 j | 0 | 1 | 2 | 3 | 4 | 5(选,不选) |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
(1,2) | 0 | 2 | 2 | 2 | 2 | max(2,0) |
(2,4) | 0 | 2 | 4 | 6 | 6 | max(6,2) |
(3,4) | 0 | 2 | 4 | 6 | 6 | max(8,6) |
(4,5) | 0 | 2 | 4 | 6 | 6 | max(6,8) |
~表中将j=5时的选择表示出来,以供理解~
朴素代码
c++
const int N=1010;
int f[N][N];//只看前i个物品,总体积为j情况下,总价值最大是多少
signed main()
{
IOS
int n,k;
cin>>n>>k;
vector<PII> a(n+1);
fir(i,1,n)
cin>>a[i].fi>>a[i].se;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=k;j++)
{
f[i][j]=f[i-1][j]; //1. 不选第i个,f[i][j]=f[i-1][j]
if(j>=a[i].fi)
f[i][j]=max(f[i][j],f[i-1][j-a[i].fi]+a[i].se);
//2. 选择第二个,f[i][j]=f[i-1][j-v[i]];
}
}
int res=0;
fir(i,0,k)//可以直接输出f[n][k],原因看在下面
res=max(res,f[n][i]);
cout<<res<<'\n';
}
在上述代码,我们将 f [ i ] [ j ] f[i][j] f[i][j]放全局,初始化为0,此时 f [ n ] [ k ] f[n][k] f[n][k],表示的就是物品总体积<=k时的最大价值 。所以可以直接输出 f [ n ] [ k ] f[n][k] f[n][k],
而如果只 f [ 0 ] [ 0 ] = 0 f[0][0]=0 f[0][0]=0,其他赋值为负无穷,表示的就是物品总体积==k时的最大价值。这时候就要像代码中一样循环比较一下。
这里有点难理解,建议按 上面样例,用两种赋值方式模拟一遍。
全赋值为0
f [ 1 ] [ 3 ] = m a x ( f [ 0 ] [ 3 ] = 0 , f [ 0 ] [ 3 − 1 ] + 2 ) = 2 f[1][3]=max(f[0][3]=0,f[0][3-1]+2)=2 f[1][3]=max(f[0][3]=0,f[0][3−1]+2)=2
除第一个,赋值为负无穷
f [ 1 ] [ 3 ] = m a x ( f [ 0 ] [ 3 ] = − I N F , f [ 0 ] [ 3 − 1 ] + 2 ) = − I N F f[1][3]=max(f[0][3]=-INF,f[0][3-1]+2)=-INF f[1][3]=max(f[0][3]=−INF,f[0][3−1]+2)=−INF
也就是当物品体积不为3时,是无法成功赋值的,也就是说它无法从** f [ 0 ] [ 2 ] 递推出 f [ 1 ] [ 3 ] f[0][2]递推出f[1][3] f[0][2]递推出f[1][3]**
而 f [ 1 ] [ 1 ] = m a x ( f [ 0 ] [ 1 ] , f [ 0 ] [ 1 − 1 ] + 1 ) = 1 , 从 f [ 0 ] [ 0 ] 可以推出来 f [ 1 ] [ 1 ] f[1][1]=max(f[0][1],f[0][1-1]+1)=1,从f[0][0]可以推出来f[1][1] f[1][1]=max(f[0][1],f[0][1−1]+1)=1,从f[0][0]可以推出来f[1][1]
可以这样说,一个数是由前面的数递推出来的,
全赋值为0,任何体积都可以向后递推, 总体积自然不一定等于k。而第二种赋值,就只允许从0开始赋值,这时 d p [ n ] [ k ] dp[n][k] dp[n][k]体积就一定是K,当然它的值也很可能时-INF。
一维优化代码
c++
const int N=1010;
int f[N];//只看前i个物品,总体积为j情况下,总价值最大是多少
signed main()
{
IOS
int n,k;
cin>>n>>k;
vector<PII> a(n+1);
fir(i,1,n)
cin>>a[i].fi>>a[i].se;
for(int i=1;i<=n;i++)
{
for(int j=k;j>=a[i].fi;j--)
{
f[j]=max(f[j],f[j-a[i].fi]+a[i].se);
}
}
cout<<f[k]<<'\n';
}
将j倒序循环就不需要用二维,可以这样理解记忆。我们选择物品i-1后,如果正着遍历j,从小到大,会提前将 f [ j − a [ i ] . f i ] f[j-a[i].fi] f[j−a[i].fi]更新为考虑前i个物品的结果,这样向后遍历就相当在考虑前i个物品结果的基础上,再次考虑第i个物品。
所以这里倒序。
那如果,每一个物品有无限个呢?这时是不是就可以直接改成正序了?
是的。这就是下面的完全背包
完全背包
和01背包不一样的地方在,每一个物品可以无限次选择。
一维优化的代码,也就是上面提到的,直接将 j正序循环。
而能体现它思想的朴素写法是这样来的:
max中依次时当前物品选择0,1,2,3.......的最大价值
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v ] + w , f [ i − 1 ] [ j − 2 v ] + 2 w , f [ i − 1 ] [ j − 3 v ] + 3 w , . . . . . ) f[i][j]=max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,f[i-1][j-3v]+3w,.....) f[i][j]=max(f[i−1][j],f[i−1][j−v]+w,f[i−1][j−2v]+2w,f[i−1][j−3v]+3w,.....)
f [ i ] [ j − v ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i − 1 ] [ j − 2 v ] + w , f [ i − 1 ] [ j − 3 v ] + 2 w , f [ i − 1 ] [ j − 4 v ] + 3 w , . . . . . ) f[i][j-v]=max(f[i-1][j-v],f[i-1][j-2v]+w,f[i-1][j-3v]+2w,f[i-1][j-4v]+3w,.....) f[i][j−v]=max(f[i−1][j−v],f[i−1][j−2v]+w,f[i−1][j−3v]+2w,f[i−1][j−4v]+3w,.....)
观察两个式子,可以将2式代入1式,由此便得
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i ] [ j − v ] ) f[i][j]=max(f[i-1][j-v],f[i][j-v]) f[i][j]=max(f[i−1][j−v],f[i][j−v])
朴素代码
c++
const int N=1010;
int a[N],b[N],f[N][N];
signed main()
{
IOS
int n,k;
cin>>n>>k;
fir(i,1,n)
cin>>a[i]>>b[i];
fir(i,1,n)
fir(j,1,k)
{
f[i][j]=f[i-1][j];
if(j>=a[i])
f[i][j]=max(f[i][j],f[i][j-a[i]]+b[i]);//f[i][j-v],错位相减
}
cout<<f[n][k]<<"\n";
}
一维优化代码
c+
const int N=1010;
int a[N],b[N],f[N];
signed main()
{
IOS
int n,k;
cin>>n>>k;
fir(i,1,n)
cin>>a[i]>>b[i];
fir(i,1,n)
fir(j,a[i],k)
{
f[j]=max(f[j],f[j-a[i]]+b[i]);
}
cout<<f[k]<<"\n";
}
多重背包I 模板题
多重背包是每件物品,有确定的数量。
这时候,可以在,j循环里面再开个循环,分别是选择0,1,2......s个
依次更新j体积下的最大价值。
代码
c++
const int N=1010;
int a[N],b[N],f[N];
signed main()
{
IOS
int n,k,v,w,s;
cin>>n>>k;
fir(i,1,n)
{
cin>>v>>w>>s;
for(int j=k;j>=0;j--)//倒序,对照01
{
for(int l=1;l<=s&&j>=l*v;l++)
f[j]=max(f[j],f[j-v*l]+w*l);
}
}
cout<<f[k]<<'\n';
}
多重背包 II 二进制优化
按照上面的思路,三重循环,很容易超限。
对于给定的数量s,朴素法就是:1,1,1,1,1,分解成s个1。这就变成了01背包问题。分别是该物品选择0,1,2......s;
那我们能不能换一种分解方式,同样也能组合成(0-s任意一种选择),使分解完的数量减少。
二进制很特殊,仅有01组成,解决问题简单,同时还能把问题大大优化。
s = 17 = 2 0 + 2 1 + 2 2 + 2 3 + 2 s=17=2^0+2^1+2^2+2^3+2 s=17=20+21+22+23+2
类似的,任意一个数都可以由2的幂次方+常数c组成。这就是纯废话,肯定能呀,
关键是,从1到s,任意一个数都可以由他们组成
如何理解呢,先看一串二进制数
1 | 0 | 1 | 1 | 1 | 1 |
---|---|---|---|---|---|
1 | |||||
1 | 0 | ||||
1 | 0 | 0 | |||
1 | 0 | 0 | 0 | ||
1 | 0 | 0 | 0 | 0 | |
C |
表格中第一行是二进制数,其后每行是一个可分解出的 2 n 2^n 2n,由于仅有01组成,所以某个数字的加与否,决定了所组成数字某一位是0还是1.所以对于组合出所有需要的数字。
由于原二进制某个数位可能是零,这时候最后一行不一定就写 C = 2 n C=2^n C=2n,当然这样做,也能组合为所有数,得出答案。我们也可以 C = s − 2 0 − 2 1 − 2 2 . . . . . . 2 n − 1 C=s-2^0-2^1-2^2......2^ {n-1} C=s−20−21−22......2n−1,这样也可以组合成所有数。
将这些拆分出来的数,作为新的物品,每个物品只有一个。
现在就又变为了0 1背包。
代码
c++
const int N=2010;
int n,k,f[N];
signed main()
{
IOS
vector<PII> g;
cin>>n>>k;
for(int i=0;i<n;i++)
{
int v,w,s;
cin>>v>>w>>s;
for(int j=1;j<=s;j*=2)
{
s-=j;
g.push_back({v*j,w*j});//转化成01背包
}
if(s>0)
g.push_back({v*s,w*s});
}
for(auto gg:g)
{
for(int j=k;j>=gg.fi;j--)
f[j]=max(f[j],f[j-gg.fi]+gg.se);
}
cout<<f[k]<<endl;
}
多重背包 III 单调队列优化
有s个物品体积为v,价值为w.
到底存放几个更优呢?
按照朴素做法,在01背包中加一重循环(0~s),更新最优价值。
也就是
循环变量 | 初 | 末 | 含义 |
---|---|---|---|
i i i | 1 | n | 遍历每一个物品 |
j j j | m | v | 更新每一个可容纳体积下最优价值 |
k k k | 1 | s&& k ∗ v < = j k*v<=j k∗v<=j | 遍历可选择数量,更新最优 |
i i i,遍历每一个物品,这是必要的,无法优化。 k k k,一个物品放几次更优,可以采用上面的二进制优化,单调队列中不进行优化,也是需要逐个更新。
而 j j j,就一定要从m到v ? 每种容量都更新一遍?
那让我们看一看关于不同 j j j之间有没有什么联系
f [ i ] [ r + 3 v ] = m a x ( f [ i − 1 ] [ r + 3 v ] , f [ i − 1 ] [ r + 2 v ] + w , f [ i − 1 ] [ r + v ] + 2 w , f [ i − 1 ] [ r ] + 3 w ) f[i][r+3v]=max(f[i-1][r+3v],f[i-1][r+2v]+w,f[i-1][r+v]+2w,f[i-1][r]+3w) f[i][r+3v]=max(f[i−1][r+3v],f[i−1][r+2v]+w,f[i−1][r+v]+2w,f[i−1][r]+3w)
f [ i ] [ r + 4 v ] = m a x ( f [ i − 1 ] [ r + 4 v ] , f [ i − 1 ] [ r + 3 v ] + w , f [ i − 1 ] [ r + 2 v ] + 2 w , f [ i − 1 ] [ r + v ] + 3 w , f [ i − 1 ] [ r ] + 4 w ) f[i][r+4v]=max(f[i-1][r+4v],f[i-1][r+3v]+w,f[i-1][r+2v]+2w,f[i-1][r+v]+3w,f[i-1][r]+4w) f[i][r+4v]=max(f[i−1][r+4v],f[i−1][r+3v]+w,f[i−1][r+2v]+2w,f[i−1][r+v]+3w,f[i−1][r]+4w)
对于物品 i i i, 每次需要考虑放 0 , v , 2 v , 3 v , 4 v . . . 0,v,2v,3v,4v... 0,v,2v,3v,4v...我们总是用 j − k v j-kv j−kv去表示,现在反过来我们用 r + k v r+kv r+kv来表示 j j j
可以发现,当我们选择占用几个体积 v v v时,都是在体积 r r r的基础上添加。
- 之前 j j j是用来循环不同背包容量的,
所以 r + v , r + 2 v , r + 3 v , r + 4 v . . . r+v,r+2v,r+3v,r+4v... r+v,r+2v,r+3v,r+4v...,全部都要循环一遍,不仅如此 ( r + 1 ) + v , ( r + 1 ) + 2 v , ( r + 1 ) + 3 v . . . (r+1)+v,(r+1)+2v,(r+1)+3v... (r+1)+v,(r+1)+2v,(r+1)+3v...,也需要遍历一遍。
总的来说, r ∈ [ 0 , v − 1 ] r\in [0,v-1] r∈[0,v−1],对于每一个 r , r + v , r + 2 v , r + 3 v , r + 4 v . . . . . . r,r+v,r+2v,r+3v,r+4v...... r,r+v,r+2v,r+3v,r+4v......都需要遍历。也就是不同j情况下,都考虑一遍放几个更优
看到这,是不是产生一点点想法了?这些 j j j有一个共同特点,对 v 取模皆为 r v取模皆为r v取模皆为r.
- 现在用 j 代替 r j代替r j代替r ,遍历区间就是 [ 0 , v − 1 ] [0,v-1] [0,v−1],
这样上面两个式子,我们就可以放在一块儿去考虑了,因为他们都是在解决同一个问题, 在 r 上,放几个 v 更优 在r上,放几个v更优 在r上,放几个v更优。
每一个 j j j,可以看作一个队列,需要考虑两点
- 大小 s + 1 s+1 s+1 ( j , j + v , j + 2 v . . . j + s v j,j+v,j+2v...j+sv j,j+v,j+2v...j+sv)
- 单调性 (需要解决的问题,本身就是不同体积不同物品个数最优解,现在放到队列里,也自然是为了比较出最优解)
因为有固定大小,所以也可以联想到滑动窗口;用单调队列,直接代替了max函数比较;
这里用一维dp写的,但需要上一个物品选择后的状态来递推,所以用pre数组去存放。
更多细节,在代码注释中
代码
c++
#include <iostream> 。
#include <cstring>
using namespace std;
const int N = 20010;
int dp[N], pre[N], q[N]; // 定义三个数组,dp用于存储DP状态,pre用于保存dp的上一轮状态,q用于单调队列
int n, m; // n表示物品数量,m表示背包容量
int main() {
cin >> n >> m;
for (int i = 0; i < n; ++i)
{ // 遍历所有物品
memcpy(pre, dp, sizeof(dp)); // 复制当前dp数组到pre数组,以便在更新dp数组时不会丢失之前的状态
int v, w, s; // v表示物品体积,w表示物品价值,s表示物品数量
cin >> v >> w >> s;
for (int j = 0; j < v; ++j) { // 遍历余数0-v
int head = 0, tail = -1; // 初始化单调队列的头和尾,不同余数,不同类,开设一个新的队列
for (int k = j; k <= m; k += v) //向背包里不断装v, 遍历所有可能的容纳总体积,
{
// 控制窗口大小,进行移动。物品只有s个,大于s*v,从新的体积开始放
if (head <= tail && k-q[head] >s*v) //队列不为空情况下,进行判断
++head;
// dp[j+3v] = max(dp[j] + 3w, dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
// 保持队列的单调性, 现在体积为k,状态方程中,需要在max中找最大的,可以发现,体积每小一个v,就多加一个w
//dp[q[tail]]+x*w, dp[k]+y*w 两者就是max中的式子.dp[q[tail]]+x*w<dp[k]+y*w 变换为下面式子
while (head <= tail && pre[q[tail]] +(k-q[tail])/v * w <= pre[k])
{
--tail; //现在更优,去尾。单调队列控制,就不用max函数
}
// 如果队列不为空,使用队列头部的状态更新当前状态
if (head <= tail)
dp[k] = max(dp[k], pre[q[head]] + (k - q[head])/v * w);
//dp[j+3v] = max(dp[j] + 3w, dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
// 将当前体积存入队列尾部
q[++tail] = k;
}
}
}
cout << dp[m] << endl; // 输出背包能够装下的最大价值
return 0; // 程序结束
}
混合背包
其实就是将上面三种混到一块儿。若干物品,有的只有一个(01);有的无数个(完全);有的确定数量(多重)
我们知道,解决多重背包,是将其分解为01背包。
这里我们也可以用二进制优化,转为01背包。
而完全背包,他的解决就是01背包中** j j j正序循环**。直接看代码
代码
c++
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
vector< tuple<int,int,int> > f;
int dp[N];
int main()
{
int n,m,v,w,s;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>v>>w>>s;
if(s==-1) f.push_back({v,w,s});
else if(s==0) f.push_back({v,w,s});
else
{
for(int k=1;k<=s;k<<=1)
{
s-=k;
f.push_back({v*k,w*k,-1});
}
if(s>0) f.push_back({v*s,w*s,-1});
}
}
for(auto [a,b,c]:f)
{
if(c==-1)
for(int i=m;i>=a;i--)
{
dp[i]=max(dp[i],dp[i-a]+b);
}
else
for(int i=a;i<=m;i++)
dp[i]=max(dp[i],dp[i-a]+b);
}
cout<<dp[m]<<'\n';
}
二维费用的背包问题
在原有体积的限制下,加了重量限制。
只需在加一重循环,用来限制重量,更新不同重量下的最优解。
为什么可以直接加一重循环,难道他俩不会影响结果?
我是这样理解的,当进行内层循环时,外层是一个固定的 j j j,此时相当于 j j j是一个无关紧要的定值。
这就联想到数学中的偏导,对某一变量求偏导,不就是将另一视作定值常数,两偏导互不影响,但可以共同组成全微分。
这里将两个循环套起来,不会影响正确性,反而可以共同求解二维费用背包问题。
代码
c++
vector<tup> f;
int dp[1010][1010];
signed main()
{
IOS
int N,V,M,v,m,w;
cin>>N>>V>>M;
fir(i,1,N)
{
cin>>v>>m>>w;
f.push_back({v,m,w});
}
for(auto [a,b,c]:f)
{
for(int j=V;j>=a;j--)//两重循环,将一个变量当成定值
for(int k=M;k>=b;k--)
{
dp[j][k]=max(dp[j][k],dp[j-a][k-b]+c);
}
}
cout<<dp[V][M]<<'\n';
}
分组背包
将物品进行分组,每组最多选择一个。求最大价值。
可以想到前面的多重背包的朴素代码,用了三个循环,第三个循环更新选择不同数量的最优价值。
这里也可以三个循环,第三个用来更新选择不同物品的最优价值
代码
c++
int dp[110],v[105],w[105];
signed main()
{
IOS
int n,m,c;
cin>>n>>m;
fir(i,1,n)
{
cin>>c;
fir(j,1,c)
cin>>v[j]>>w[j];
for(int j=m;j>=1;j--)
{
fir(k,1,c)
{
if(j>=v[k])
dp[j]=max(dp[j],dp[j-v[k]]+w[k]);
}
}
}
cout<<dp[m]<<'\n';
}
有依赖的背包问题
在01背包中,我们遍历物品从一到 n n n,进行状态转移,但因为这是数形dp,我们从子节点到根进行状态转移更优。
采用dfs,在回溯时进行状态转移,这是因为
状态转移:一个节点的状态更新依赖于子节点的状态,回溯时进行状态转移是最佳时机
保持状态独立性: 确保只考虑子节点的状态,不受其他兄弟节点影响
自下而上的转移,根节点的状态就是答案。
有多个节点,又有体积控制,和01背包类似。不过当选择到该节点时,我们需要考虑他的子节点如何选择,分配多少体积,左还是右,全选还是全不选。我们只能做出一个选择。
这就可以想到分组背包,也是在一组物品中,最多选择一个。
我们就可以从分组背包的角度考虑这道题,每个节点是一个组,该节点子树所分配的不同体积为组内物品,因为子树的不同体积恰好可以反应不同的选择方案。(其实也可以和多重联系到一块,因为多重的朴素做法也是三重循环,第三层是考虑选几个更优。)
所以内层循环就是更新给子树分配不同体积的最优状态
那我们如何实现依赖性呢
我们在分配体积时,就需要给自身分配,再考虑子节点
因为我们是从子节点向根进行状态转移。也就是对于每一个dp阶段,他的父节点只能选取就好了。
这种题是背包类树形dp,以节点编号,作为dp的阶段,体积作为第二维状态,而在状态转移时,我们要处理的就是分组背包问题
代码
c++
#include<iostream>
#include<vector>
using namespace std;
//这里不是按子树来分组,而是按每棵子树的不同体积来分组。即我们选择的每种方案,每个子树的体积必然是确定的,
int f[110][110];//f[x][v]表达选择以x为子树的物品,在容量不超过v时所获得的最大价值
vector<int> g[110];
int v[110],w[110];
int n,m,root;
//这里对于树中的每个节点来说,就是一个分组背包问题。每个子节点是一组物品,每个子节点的不同体积和每个体积所对应的最大价值,就是这个物品组中的物品。
void dfs(int x)
{
for(int i=v[x];i<=m;i++) f[x][i]=w[x];//点x必须选,所以初始化f[x][v[x] ~ m]= w[x]
for(int i=0;i<g[x].size();i++)
{
int y=g[x][i];
dfs(y);//一路搜到底
//以体积为第二维,必须包含x节点
for(int j=m;j>=v[x];j--)//j可选体积的范围为v[x]~m, 小于v[x]无法选择以x为子树的物品
{ //以子树所占体积分组
for(int k=0;k<=j-v[x];k++)//分给子树y的空间不能大于j-v[x],不然都无法选根物品x
{
f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
}
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int h;
cin>>v[i]>>w[i]>>h;
if(h==-1)
root=i;
else
g[h].push_back(i);
}
dfs(root);
cout<<f[root][m];
return 0;
}
背包问题求方案数
求几种方案可以得到最优解。
我最开始简单的,用map去记录dp[j],最后直接输出mp[dp[j]]。这样会重复
这就要提到前面01背包所提到的赋值问题。
当dp全部初始化为0时,dp[m]的结果是体积小于等于m,如果体积为k,那么dp[k]就已经记录一次。
那我换成第二种赋值不就好了么?
不行!
还是会重复的!因为j一直在循环,在选择不同物品时,dp[j]都出现过。
代码注释中举例了,第二次看这个代码,竟然又忽略了这个问题,提交又错一发,这么浅显,一个坑掉两次。
上述可以看出,影响计算的就是体积,同种方案不同体积,同种体积同种方案都会重复。
那我们可以开个数组,用来存放某一体积下的方案数呀,这时候再判断一下该体积是否可以达到最优解,累加到一块就是答案
首先初始化mp[i]=0,
mp[0]=1 体积为零,都不选,为1种,依次开端,向后递推。为了确保一定是该体积下的结果。当向后不断考虑更多物品时,我们采用更新,而不是累加,因为最终我们是针对全部物品的不同体积的方案数。
代码
c++
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define fir(i,a,b) for(int i=a;i<=b;i++)
#define PII pair<int,int>
#define fi first
#define se second
#define tup tuple<int,int,int>
const int mod=1e9+7;
const int N=2010;
int dp[N], n,m,v[1100],w[1100];
map<int,int> mp;
signed main()
{
cin>>n>>m;
mp[0]=1;//某体积对应的方案数,体积为零,都不选,为1种
fir(i,1,n)
{
cin>>v[i]>>w[i];
for(int j=m;j>=v[i];j--)
{
int t=max(dp[j],dp[j-v[i]]+w[i]),c=0;
if(t==dp[j])
c+=mp[j];
if(t==dp[j-v[i]]+w[i]) c+=mp[j-v[i]];
dp[j]=t,mp[j]=c%mod;//不能直接用g[j]+=....,这里是根据不同体积进行的更新,+=会重复计算
// mp[dp[j]]= (mp[dp[j]]+1) ;会重复,不能看价值,因为可能考虑有前i个dp[5]=9,考虑前i+1个dp[5]=9,而第i+1个不选,这是一种,累加就多算了
}
}//dp[m]表示总体积<=m的最大价值,此时总体积可能不等于m,但是我们mp计算的就是体积为j的方案数,不会重复
int ans=0;
fir(i,0,m)
{
if(dp[i]==dp[m])
ans=(ans+mp[i])%mod;
}
cout<<ans<<'\n';
}
疑点又来了,怎么计算才能使该体积下的方案数,就一定是等于该体积?
我们分析为什么dp[j]会出现这种
因为 d [ j ] 会从 d p [ j − v [ i ] ] + w [ i ] d[j]会从dp[j-v[i]]+w[i] d[j]会从dp[j−v[i]]+w[i]转移过来。dp[k]初始化为零,巧在 + w [ i ] +w[i] +w[i],让他成功转移。
而mp,我们是从 m p [ j ] 和 m p [ j − 1 ] mp[j]和mp[j-1] mp[j]和mp[j−1]直接转移更新,不会无中生有
背包问题求具体方案
输出最优解中所选物品的编号序列,且该编号序列的字典序最小。
既然字典序最小,那么编号较小的优先考虑装进去。
这里我们倒着遍历物品,然后再正序回溯判断,该物品是否选择。
因为我们是倒序递推的,所以判断在考虑 i + 1 i+1 i+1时的最优解 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j],以及在此基础上选择物品 i i i的价值 d p [ i + 1 ] [ j − v [ i ] ] + w [ i ] dp[i+1][j-v[i]]+w[i] dp[i+1][j−v[i]]+w[i]哪个更大 ,由此得出是否选择物品 i i i。
先看代码,至于为什么后面会说。
代码
c++
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define fir(i,a,b) for(int i=a;i<=b;i++)
#define PII pair<int,int>
#define fi first
#define se second
#define tup tuple<int,int,int>
int dp[1010][1010];
int n,m,v[1010],w[1010];
signed main()
{
IOS
cin>>n>>m;
fir(i,1,n)
cin>>v[i]>>w[i];
for(int i=n;i>=1;i--)
{
fir(j,0,m)
{
dp[i][j]=dp[i+1][j];//因为倒着来,这里是i+1!!!
if(j>=v[i])
dp[i][j]=max(dp[i][j],dp[i+1][j-v[i]]+w[i]);
}
}
int i=1,j=m;
while(i<=n)
{
if(j>=v[i]&&dp[i+1][j-v[i]]+w[i]>=dp[i+1][j])
{
cout<<i<<' ';
j-=v[i];
}
i++;
}
}
为什么递推后,求解断具体方案,该物品取不取,需要逆向回溯去判断?
我理解的也不够透彻
以正序递推举例,递推时i是不断增大去更新的,我们需要的也是更新完n个的结果。所以最后更新的才能反应出最优解。
如果我们按照递推的顺序以 i − 1 i-1 i−1去判断是否选取 i i i,这时候只是得到基于考虑$前i个 $物品的最优解答案,这并不是最终的最有价值。
所以这时我们需要逆向考虑是否选取物品 i i i.
那为什么不这样,像原来的01背包去正向递推,然后再逆向判断该物品是否选择,再反转成字典序递增的顺序输出呢?假如有两个物品4,9,选择任意一个效果都是一样的,但由于你是在倒着判断,先判断9是否选取,那你要不要选择呢?
选择的话,字典序不是最小了
不选择的话,你又不知道会遇到一个物品4和它效果一样。
这时就进退两难了。
那么把顺序反过来不就解决了没,倒着递推,正序判断,能选择就选,正好保证字典序最小。