01背包问题
题目:
有
N
N
N件物品和一个容量为
V
V
V的背包。第
i
i
i件物品的费用是
c
[
i
c[i
c[i],价值是
w
[
i
]
w[i]
w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
解答
f [ i ] [ v ] f[i][v] f[i][v]表示前面 i i i个物品放进空间为 v v v的背包获得的最大收益
f [ i ] [ v ] = m a x ( f [ i − 1 ] [ v ] , f [ i − 1 ] [ v − c [ i ] ] + w [ i ] ) f[i][v]=max(f[i-1][v],f[i-1][v-c[i]]+w[i]) f[i][v]=max(f[i−1][v],f[i−1][v−c[i]]+w[i])
答案在 f [ n ] [ 0 − > v ] f[n][0->v] f[n][0−>v]里面找,如果是“恰好”8装满的话才是 f [ n ] [ v ] f[n][v] f[n][v]
优化空间复杂度
for(int i=1;i<=n;i++)
for(int j=v;j;j--)
f[j]=max(f[j],f[j-c[i]]+w[i])
还有一种空间复杂度的优化:滚动数组,懂得都懂,滚起来就完事了
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 50000
using namespace std;
int n,vv,u[maxn],v[maxn],dp[maxn],ans;
int main(){
scanf("%d %d",&n,&vv);
for(int i=1;i<=n;i++) scanf("%d %d",&u[i],&v[i]);
for(int i=1;i<=n;i++){
for(int j=vv;j>=u[i];j--){
dp[j]=max(dp[j],dp[j-u[i]]+v[i]);
}
}
for(int i=0;i<=vv;i++) ans=max(ans,dp[i]);
printf("%d\n",ans);
return 0;
}
完全背包问题
问题
有
N
N
N种物品和一个容量为
V
V
V的背包,每种物品都有无限件可用。第i种物品的费用是
c
[
i
]
c[i]
c[i],价值是
w
[
i
]
w[i]
w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
解答
- 优化一 : c [ i ] < c [ j ] c[i]<c[j] c[i]<c[j] && w [ i ] > w [ j ] w[i]>w[j] w[i]>w[j] 那就直接把 c [ j ] c[j] c[j]去掉,弄成一个结构体,然后按照 c [ i ] c[i] c[i]排序
- 优化二:直接顺着来不就完了嘛
for(int i=1;i<=n;i++)
for(int j=0;j<=vv-u[i];j++)
dp[j+u[i]]=max(dp[j+u[i]],dp[j]+v[i]);
attention : 这道题和上面那个都要从能够转移的地方开始转移
memset(dp,-1,sizeof dp) 这个是初始化,当然,还要让dp[0]=0
if(dp[j]==-1) continue; 这个是放在转移之前的,不能从-1去转移
多重背包问题
问题
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
- 方法一:把有 n [ i ] n[i] n[i]件可以用,转换成01背包问题,把他们全部拆除来,时间复杂度是 O ( V ∗ ∑ n [ i ] ) O(V*∑n[i]) O(V∗∑n[i])
- 方法二:把有 n [ i ] n[i] n[i]件可以用,转换成01背包问题,拆了,但没完全拆,拆成 2 0 − 1 − 2 − . . . − n + 2^{0-1-2-...-n}+ 20−1−2−...−n+剩下的,这个样子是二进制,反正就是什么都可以拼出来,比如 38 38 38就拆成 1 , 2 , 4 , 8 , 16 , 7 1,2,4,8,16,7 1,2,4,8,16,7.然后再来跑01背包,时间复杂度是 O ( V ∗ ∑ l o g ( n [ i ] ) ) O(V*∑log(n[i])) O(V∗∑log(n[i]))
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 500000
using namespace std;
int n,m,ans,k,dp[maxn];
struct node{
int val,v;
}dara[maxn];
void decompose(int v,int w,int cnt){
int now=1;
while(cnt>0){
if(cnt<=now) {dara[++k].v=v*cnt,dara[k].val=w*cnt; return;}
dara[++k].v=v*now; dara[k].val=w*now;
cnt-=now; now=now<<1;
}
}
int main(){
scanf("%d %d",&n,&m);
for(int i=1,v,w,cnt;i<=n;i++) scanf("%d %d %d",&v,&w,&cnt),decompose(v,w,cnt);
memset(dp,-1,sizeof dp); dp[0]=0;
for(int i=1;i<=k;i++){
for(int j=m;j>=dara[i].v;j--){
if(dp[j-dara[i].v]==-1) continue;
dp[j]=max(dp[j-dara[i].v]+dara[i].val,dp[j]);
}
}
for(int i=0;i<=m;i++) ans=max(ans,dp[i]);
printf("%d\n",ans);
return 0;
}
-
方法三:先看看传统的 d p dp dp方程,其中 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示将前 i i i 种物品放入容量为 j j j 的背包中所得到的最大价值 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w , . . . , d p [ i − 1 ] [ j − k ∗ v ] + k ∗ w ) dp[i][j] = max(dp[i-1][j], dp[i-1][j-v] + w, dp[i-1][j-2*v] + 2*w,..., dp[i-1][j-k*v] + k*w) dp[i][j]=max(dp[i−1][j],dp[i−1][j−v]+w,dp[i−1][j−2∗v]+2∗w,...,dp[i−1][j−k∗v]+k∗w)
也可以优化成 d p [ m ] = m a x ( d p [ m ] , d p [ m − v ] + w , d p [ m − 2 ∗ v ] + 2 ∗ w , d p [ m − 3 ∗ v ] + 3 ∗ w , . . . ) dp[m] = max(dp[m], dp[m-v] + w, dp[m-2*v] + 2*w, dp[m-3*v] + 3*w, ...) dp[m]=max(dp[m],dp[m−v]+w,dp[m−2∗v]+2∗w,dp[m−3∗v]+3∗w,...)
所以很显然, d p [ k ∗ v + j ] dp[k*v+j] dp[k∗v+j]只有可能由 d p [ j ] , d p [ j + v ] , d p [ j + 2 ∗ v ] . . . d p [ j + k ∗ v ] dp[j],dp[j+v],dp[j+2*v]...dp[j+k*v] dp[j],dp[j+v],dp[j+2∗v]...dp[j+k∗v]转移过来,而且是由这个里面最爽(不是最大)的那个转移过来,因为装得越多肯定值越大啊,所以怎么用一个单调队列呢,所以我们把 d p [ j + 3 v ] = m a x ( d p [ j ] + 3 w , d p [ j + v ] + 2 w , d p [ j + 2 v ] + w , d p [ j + 3 v ] ) dp[j+3v] = max(dp[j] + 3w, dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v]) dp[j+3v]=max(dp[j]+3w,dp[j+v]+2w,dp[j+2v]+w,dp[j+3v]) 转化成 d p [ j + 3 v ] = m a x ( d p [ j ] , d p [ j + v ] − w , d p [ j + 2 v ] − 2 w , d p [ j + 3 v ] − 3 w ) + 3 w dp[j+3v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w, dp[j+3v] - 3w) + 3w dp[j+3v]=max(dp[j],dp[j+v]−w,dp[j+2v]−2w,dp[j+3v]−3w)+3w 这样子里面就可以拿来单调了
-
attention 1
if(head<=tail && k-s*v>q[head]) head++;
这也就是说,一个物品最多有 s s s个,那 d p [ j ] dp[j] dp[j]最多从 d p [ j − s ∗ v ] dp[j-s*v] dp[j−s∗v]转移过来,前面的就没意义了,所以 h e a d head head++
-
attention 2
可以使用滚动数组或者 m e m c p y memcpy memcpy来实现转移,也就是上面的空间优化,不能只用dp,不然就会整成无限背包
-
attention 3
q [ ] q[] q[]里面存放的是一个单调递减的数列,而且最好手写双向队列,用 d e q u e deque deque要t
-
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 20010
using namespace std;
int n,m,v,w,s,dp[maxn],q[maxn],pre[maxn];
int main(){
scanf("%d %d",&n,&m);
for(int i=0,v,w,s;i<n;i++){
memcpy(pre,dp,sizeof dp);//滚动数组,其实真的可以用i%2来完成
scanf("%d %d %d",&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){//k就是跳跃式的,k也是体积,k枚举的是
if(head<=tail && k-s*v>q[head]) head++;//因为现在的话,转移是用不到这个前面的了
while(head<=tail && pre[q[tail]]-(q[tail]-j)/v*w<=pre[k]-(k-j)/v*w) tail--;//-(q[tail]-j)/v*w是要把放进去的东西整出来
if(head<=tail) dp[k]=max(dp[k],pre[q[head]]+(k-q[head])/v*w);
q[++tail]=k;
}
}
}
printf("%d\n",dp[m]);
return 0;
}
混合背包问题
问题
有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?
解答
把无限背包单独挑出来跑,然后再把01背包归到多重背包里面来解
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<deque>
#define maxn 50000
using namespace std;
int n,m,dp[maxn],ans,pre[maxn];
deque<int> q;
int main(){
scanf("%d %d",&n,&m);
for(int i=1,v,w,cnt;i<=n;i++){
scanf("%d %d %d",&v,&w,&cnt);
if(cnt==-1) cnt=1;
if(cnt==0){
for(int j=v;j<=m;j++)
dp[j]=max(dp[j-v]+w,dp[j]);
}
else{
memcpy(pre,dp,sizeof dp);
for(int j=0;j<v;j++){
q.clear();
for(int k=j;k<=m;k+=v){
if(!q.empty() && q.front()<k-cnt*v) q.pop_front();
while(!q.empty() && pre[q.back()]-(q.back()-j)/v*w<=pre[k]-(k-j)/v*w) q.pop_back();
q.push_back(k);
dp[k]=max(dp[k],pre[q.front()]+(k-q.front())/v*w);
}
}
}
}
for(int i=0;i<=m;i++) ans=max(ans,dp[i]);
printf("%d\n",ans);
return 0;
}
二维费用的背包问题
问题
二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为
a
[
i
]
a[i]
a[i]和
b
[
i
]
b[i]
b[i]。两种代价可付出的最大值(两种背包容量)分别为
V
V
V和
U
U
U。物品的价值为
w
[
i
]
w[i]
w[i]。
解答
d p [ i ] [ j ] = m a x ( d p [ i − a ] [ j − b ] + w , d p [ i ] [ j ] ) dp[i][j]=max(dp[i-a][j-b]+w,dp[i][j]) dp[i][j]=max(dp[i−a][j−b]+w,dp[i][j])
多加一维就行了
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn
using namespace std;
int n,v,m,dp[120][120],ans;
int main(){
scanf("%d %d %d",&n,&v,&m);
for(int i=1,a,b,w;i<=n;i++){
scanf("%d %d %d",&a,&b,&w);
for(int i=v;i>=a;i--)
for(int j=m;j>=b;j--)
dp[i][j]=max(dp[i-a][j-b]+w,dp[i][j]);
}
for(int i=1;i<=v;i++)
for(int j=1;j<=m;j++)
ans=max(ans,dp[i][j]);
printf("%d\n",ans);
return 0;
}
分组的背包问题
问题
有
N
N
N件物品和一个容量为
V
V
V的背包。第
i
i
i件物品的费用是
c
[
i
]
c[i]
c[i],价值是
w
[
i
]
w[i]
w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
解答
使用滚动数组,所以不会接在这一组的头上,所以每组只选了一个
转移一: d p [ i % 2 ] [ s ] = m a x ( d p [ ( i + 1 ) % 2 ] [ s − v ] + w , d p [ i % 2 ] [ s ] ) dp[i\%2][s]=max(dp[(i+1)\%2][s-v]+w,dp[i\%2][s]) dp[i%2][s]=max(dp[(i+1)%2][s−v]+w,dp[i%2][s])
转移二: d p [ i % 2 ] [ j ] = d p [ ( i + 1 ) % 2 ] [ j ] dp[i\%2][j]=dp[(i+1)\%2][j] dp[i%2][j]=dp[(i+1)%2][j]
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 200
using namespace std;
int n,m,dp[2][maxn],ans;
int main(){
scanf("%d %d",&n,&m);
for(int i=1,t,v,w;i<=n;i++){
scanf("%d",&t);
for(int j=0;j<=m;j++) dp[i%2][j]=dp[(i+1)%2][j];
for(int j=1;j<=t;j++){
scanf("%d %d",&v,&w);
for(int s=m;s>=v;s--)
dp[i%2][s]=max(dp[(i+1)%2][s-v]+w,dp[i%2][s]);
}
}
for(int i=0;i<=m;i++) ans=max(ans,dp[0][i]);
for(int i=0;i<=m;i++) ans=max(ans,dp[1][i]);
printf("%d\n",ans);
return 0;
}
有依赖的背包问题
简化的问题:物品 i i i依赖与物品 j j j,也就是要选 i i i的话就必须先选 j j j,没有一个物品依赖于两个物品,且一个物体“附件”只有一个
解答
按照背包问题的一般思路,仅考虑一个主件和它的附件集合。可是,可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略。(事实上,设有 n n n个附件,则策略有 2 n + 1 2^{n+1} 2n+1个,为指数级。)
考虑到所有这些策略都是互斥的(也就是说,你只能选择一种策略),所以一个主件和它的附件集合实际上对应于分组背包中的一个物品组,每个选择了主件又选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是这个策略中的物品的值的和。但仅仅是这一步转化并不能给出一个好的算法,因为物品组中的物品还是像原问题的策略一样多。
对于一个物品组中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,我们可以对主件 i i i的“附件集合”先进行一次01背包,得到费用依次为 0.. V − c [ i ] 0..V-c[i] 0..V−c[i]所有这些值时相应的最大价值 f ′ [ 0.. V − c [ i ] ] f'[0..V-c[i]] f′[0..V−c[i]]。那么这个主件及它的附件集合相当于 V − c [ i ] + 1 V-c[i]+1 V−c[i]+1个物品的物品组,其中费用为 c [ i ] + k c[i]+k c[i]+k的物品的价值为 f ′ [ k ] + w [ i ] f'[k]+w[i] f′[k]+w[i]。也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为 V − c [ i ] + 1 V-c[i]+1 V−c[i]+1个物品的物品组,就可以直接应用分组背包的算法解决问题了。
更一般的问题:依赖关系以图论中“森林”的形式给出,主件的附件仍然可以具有自己的附件集合,限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。
解答
就是树形 d p dp dp,代码里面有了,去看看吧
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 500
using namespace std;
int n,m,dp[maxn][maxn],k,head[maxn],root;
struct node{
int v,w;
}Data[maxn];
struct Node{
int Next,to;
}edge[maxn];
void add(int u,int v){
edge[++k]=(Node){head[u],v}; head[u]=k;
}
void dfs(int u){
for(int i=head[u];i;i=edge[i].Next){
int v=edge[i].to;
dfs(v);
for(int j=m-Data[u].v;j>=0;j--)
for(int k=0;k<=j;k++)
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);
}
for(int i=m;i>=Data[u].v;i--) dp[u][i]=dp[u][i-Data[u].v]+Data[u].w;//加上刚刚默认选择的父节点价值,相当于强制选择了u
for(int i=0;i<Data[u].v;i++) dp[u][i]=0;
//因为我们是从叶子结点开始往上做,所以如果背包容积不如当前物品的体积大,那就不能选择当前结点及其子节点,因此赋值为零
}
signed main(){
scanf("%d %d",&n,&m);
for(int i=1,temp;i<=n;i++){
scanf("%d %d %d",&Data[i].v,&Data[i].w,&temp);
if(temp==-1) root=i;
else add(temp,i);
}
dfs(root);
printf("%d\n",dp[root][m]);
return 0;
}
以上代码均可在 AcWing上评测
泛化物品
定义
考虑这样一种物品,它并没有固定的费用和价值,而是它的价值随着你分配给它的费用而变化。这就是泛化物品的概念。更严格的定义之。在背包容量为V的背包问题中,泛化物品是一个定义域为
0..
V
0..V
0..V中的整数的函数
h
h
h,当分配给它的费用为v时,能得到的价值就是
h
(
v
)
h(v)
h(v)。
解答:求两个泛化物品的和也就是 f ( v ) = m a x { h ( k ) + h ( v − k ) } f(v)=max\{h(k)+h(v-k)\} f(v)=max{h(k)+h(v−k)},前面的很多种背包问题都可以看做泛化物品
例题:battle ships
将防御塔血量 l 视为背包容量,n 种战舰可以无限生产,将战舰的生产时间视为价值,造成的伤害视为重量,那么问题就转为了完全背包问题
#include<cstdio>
#include<algorithm>
#include<string>
#define maxn 500
using namespace std;
int n,m,dp[maxn];
struct node{
int time,kill;
}data[maxn];
signed main(){
while(scanf("%d %d",&n,&m)!=EOF){
memset(dp,0,sizeof dp);
for(int i=1;i<=n;i++) scanf("%d %d",&data[i].time,&data[i].kill);
for(int i=1;i<=m;i++)//枚举血量
for(int j=1;j<=n;j++)//枚举战舰
dp[i+data[j].time]=max(dp[i+data[j].time],dp[i]+i*data[j].kill);
//先枚举血量,因为战舰可以重复生产
for(int i=1;i<=330;i++) if(dp[i]>=m) {printf("%d\n",i);break;}
}
return 0;
}