01背包
问题描述:
给定 N N N 个物品,其中第 i i i 个物品的重量为 w i w_i wi,价值为 v i v_i vi。有一容积为 M M M 的背包,要求选择一些物品存入背包,使得物品的总重量不超过 M M M 的前提下,物品的价值总和最大。
设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示,使用前 i i i 个物品,当背包重量为 j j j 时,装入价值的最大值。
考虑第 i i i 个物品有拿与不拿两种选择,若拿,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j − w i ] + v i dp[i][j]=dp[i-1][j-w_i]+v_i dp[i][j]=dp[i−1][j−wi]+vi;若不拿,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i−1][j]。
所以:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
d
p
[
i
−
1
]
[
j
−
w
i
]
+
v
i
dp[i][j] = \begin{cases} dp[i-1][j] \\ dp[i-1][j-w_i]+v_i \\ \end{cases}
dp[i][j]={dp[i−1][j]dp[i−1][j−wi]+vi
代码
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++)
f[i][j]=f[i-1][j];
for(int j=w[i];j<=m;j++)
f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
}
滚动数组优化空间
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++)
f[i&1][j]=f[(i-1)&1][j];
for(int j=w[i];j<=m;j++)
f[i&1][j]=max(f[i&1][j],f[(i-1)&1][j-w[i]]+v[i]);
}
倒序循环
直接将空间压缩成一维,如果继续使用正序循环,那么一个物品可能重复装入背包多次。
当有一个物品的重量为3,正序循环时,更新情况如下:
使用了一个已更新的状态来更新当前状态,也即是说,重复使用了第
i
i
i 个物品两次。
倒序循环永远使用未更新之前的数据来更新当前结点,也即是说,每个物品只会使用一次。
完全背包
给定 N N N 个物品,其中第 i i i 种物品的重量为 w i w_i wi,价值为 v i v_i vi,并且有无数个。有一容积为 M M M 的背包,要求选择一些物品存入背包,使得物品的总重量不超过 M M M 的前提下,物品的价值总和最大。
多重背包
给定 N N N 种物品,其中第 i i i 种物品的重量为 w i w_i wi,价值为 v i v_i vi,并且有 c i c_i ci 个。有一容积为 M M M 的背包,要求选择一些物品存入背包,使得物品的总重量不超过 M M M 的前提下,物品的价值总和最大。
直接拆分法
01 01 01 背包的复杂度为 O ( M N ) O(MN) O(MN);
多重背包的复杂度为 O ( M × ∑ i = 1 N C i ) O(M \times \sum^N_{i=1}C_i) O(M×∑i=1NCi)
把每种物品拆分为 C i C_i Ci 个,效率较低。
二进制拆分法
设 2 0 + 2 1 + … + 2 p ≤ C i 2^0+2^1+…+2^p \le C_i 20+21+…+2p≤Ci, R i = C i − ( 2 p + 1 − 1 ) R_i=C_i-(2^{p+1}-1) Ri=Ci−(2p+1−1)。
可以把 C i C_i Ci 拆为 2 0 2^0 20、 2 1 2^1 21、…… 、 2 p 2^p 2p 、 R i R_i Ri。
2 0 2^0 20、 2 1 2^1 21、…… 、 2 p 2^p 2p 、 R i R_i Ri 可以组成 0 0 0 ~ C i C_i Ci 之间的任意整数。
把每种物品拆分为 l o g C i logC_i logCi 个,效率较高。
使用单调队列优化的动态规划算法可以进一步降低时间复杂度。
【例题】Coins
简化题意:
有
N
N
N 种硬币,第
i
i
i 种面值为
A
i
A_i
Ai,有
C
i
C_i
Ci 个。求
1
1
1 ~
M
M
M 之间可以被拼成的面值数量。
1
≤
N
≤
100
1 \le N \le 100
1≤N≤100 ,
1
≤
M
≤
1
0
5
1 \le M \le 10^5
1≤M≤105 ,
1
≤
A
i
≤
1
0
5
1 \le A_i \le 10^5
1≤Ai≤105 ,
1
≤
C
i
≤
1000
1 \le C_i \le 1000
1≤Ci≤1000 。
#include<bits/stdc++.h>
using namespace std;
int n,m,v[110],dp[100010],b[110];
vector<int> c[110];
void solve(int n,int m){
for(int i=0;i<110;i++) c[i].clear();
memset(dp,0,sizeof(dp)); dp[0]=1;
for(int i=1;i<=n;i++) cin>>v[i];
for(int i=1;i<=n;i++) cin>>b[i];
for(int i=1;i<=n;i++){
int bit=2;
while(bit-1<=b[i]){
c[i].push_back(bit>>1);
bit<<=1;
}
bit=(bit-1)>>1;
if(b[i]>bit) c[i].push_back(b[i]-bit);
}
for(int i=1;i<=n;i++){
for(int j=0;j<(int)c[i].size();j++){
int y=v[i]*c[i][j];
for(int k=m;k>=y;k--)
dp[k]|=dp[k-y];
}
}
int ans=0;
for(int i=1;i<=m;i++) if(dp[i]) ans++;
cout<<ans<<"\n";
}
int main(){
ios::sync_with_stdio(false);
while(cin>>n>>m,n!=0&&m!=0) solve(n,m);
}
分组背包
给定 N N N 组物品,其中第 i i i 组有 C i C_i Ci 个物品。第 i i i 组的第 j j j 个物品的重量为 w i j w_{ij} wij,价值为 v i j v_{ij} vij 。有一容积为 M M M 的背包,要求选择一些物品存入背包,使得每组最多选择一个并且物品,且总重量不超过 M M M 的前提下,物品的价值总和最大。
【例题】NIH Budget
简化题意:
给出 n n n 个疾病治疗方案,每个方案有四个阶段:投入资金 a i a_i ai,挽救生命 b i b_i bi。求拥有研发资金m时,最优的分配策略下可以挽救的生命数量。
思路:
可以将 n n n 种治疗方案看作是 n n n 种物品,每种方案中的四个阶段看作是同一类背包中的四个物品,题意恰好有 “每组最多选择一个物品” 这个要求,那么原题就转换为分组背包问题。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int T,n,m,ca,dp[N],w[12][5],v[12][5];
void solve(){
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=4;j++)
cin>>w[i][j]>>v[i][j];
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
for(int k=1;k<=4;k++)
if(j-w[i][k]>=0)
dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
cout<<"Budget #"<<++ca<<": Maximum of "<<dp[m]<<" lives saved.\n";
for(int i=0;i<=m;i++) dp[i]=0;
}
int main(){
ios::sync_with_stdio(false);
for(cin>>T;T;T--) solve();
}
【例题】Jury Compromise
简化题意:
给出 n n n 个物品,每个物品有两个属性 a [ i ] 、 b [ i ] a[i]、b[i] a[i]、b[i]( 0 ≤ a [ i ] , b [ i ] ≤ 20 0\le a[i],b[i]\le 20 0≤a[i],b[i]≤20)。从中选出 m m m 个物品,使得 ∑ ∣ a [ i ] − b [ i ] ∣ \sum|a[i]-b[i]| ∑∣a[i]−b[i]∣ 最小,若方案不唯一,再从中选择 ∑ a [ i ] + b [ i ] \sum a[i]+b[i] ∑a[i]+b[i] 最大的方案。( 1 ≤ n ≤ 200 1\le n \le200 1≤n≤200 , 1 ≤ m ≤ 20 1\le m \le20 1≤m≤20 )
思路:
d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k] 表示前 i i i 个物品,已经选择了 j j j 个,当 ∑ ∣ a [ i ] − b [ i ] ∣ \sum|a[i]-b[i]| ∑∣a[i]−b[i]∣ 为 k k k 时, ∑ a [ i ] + b [ i ] \sum a[i]+b[i] ∑a[i]+b[i] 的最大值。
d i d_i di 为 a [ i ] − b [ i ] a[i]-b[i] a[i]−b[i], s i s_i si 为 a [ i ] + b [ i ] a[i]+b[i] a[i]+b[i]。
若没有选择第
i
i
i 个物品,那么
d
p
[
i
]
[
j
]
[
k
]
=
d
p
[
i
−
1
]
[
j
]
[
k
]
dp[i][j][k]=dp[i-1][j][k]
dp[i][j][k]=dp[i−1][j][k]。
若选择第
i
i
i 个物品,那么
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
d
[
i
]
]
+
s
[
i
]
dp[i-1][j-1][k-d[i]]+s[i]
dp[i−1][j−1][k−d[i]]+s[i]
状态转移方程:
d
p
[
i
]
[
j
]
[
k
]
=
m
a
x
{
d
p
[
i
−
1
]
[
j
]
[
k
]
d
p
[
i
−
1
]
[
j
−
1
]
[
k
−
d
i
]
+
s
i
dp[i][j][k]=max \begin{cases} dp[i-1][j][k]\\ dp[i-1][j-1][k-d_i]+s_i \end{cases}
dp[i][j][k]=max{dp[i−1][j][k]dp[i−1][j−1][k−di]+si
初始化 d p [ 0 ] [ 0 ] [ 0 ] = 0 dp[0][0][0]=0 dp[0][0][0]=0 ,其余为负无穷。
目标为 d p [ n ] [ m ] [ k ] dp[n][m][k] dp[n][m][k] , k k k 尽量小。
#include<bits/stdc++.h>
#define H(x) ((x)+N)
using namespace std;
const int N=450;
int n,m,dp[22][2*N],a[210],b[210],ca,sel[22],D[22][2*N];
int ver[500000],Next[500000],tot;
void dfs(int x,int y){
if(y==0||x==0) return;
sel[x]=ver[y];
dfs(x-1,Next[y]);
}
void solve(int n,int m){
memset(dp,0xcf,sizeof(dp)); dp[0][H(0)]=0;
memset(D,0,sizeof(D)); tot=0;
memset(sel,0,sizeof(sel));
for(int i=1;i<=n;i++) cin>>b[i]>>a[i];
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--)
for(int k=-N;k<=N;k++)
if(H(k-(a[i]-b[i]))>=0&&dp[j-1][H(k-(a[i]-b[i]))]>=0)
if(dp[j-1][H(k-(a[i]-b[i]))]+a[i]+b[i]>dp[j][H(k)]){
dp[j][H(k)]=dp[j-1][H(k-(a[i]-b[i]))]+a[i]+b[i];
ver[++tot]=i,Next[tot]=D[j-1][H(k-(a[i]-b[i]))],D[j][H(k)]=tot;
}
for(int i=0;i<=N;i++){
if(dp[m][N+i]>=0&&dp[m][N+i]>=dp[m][N-i]){ dfs(m,D[m][N+i]); break; }
if(dp[m][N-i]>=0&&dp[m][N-i]>=dp[m][N+i]){ dfs(m,D[m][N-i]); break; }
}
int retA=0,retB=0;
for(int i=1;i<=m;i++) retA+=a[sel[i]],retB+=b[sel[i]];
cout<<"Jury #"<<++ca<<"\n";
cout<<"Best jury has value "<<retB<<" for prosecution and value "<<retA<<" for defence:"<<"\n";
sort(sel+1,sel+m+1);
for(int i=1;i<=m;i++) cout<<" "<<sel[i];
cout<<"\n\n";
}
int main(){
ios::sync_with_stdio(false);
while(cin>>n>>m,n!=0&&m!=0) solve(n,m);
}
线性dp
【例题】Armchairs
简化题意:
给出一个长度为
n
n
n 的
01
01
01 序列,代表有
n
n
n 个位置,每个位置上的数字若为
1
1
1 ,则该位置上有人;若为
0
0
0 ,则该位置空闲。
将一个人从座位
i
i
i 移动到座位
j
j
j 的花费为
∣
i
−
j
∣
|i-j|
∣i−j∣ ,求一个最小花费,使得最初有人的位置都变得空闲。保证存在这样的移动方案。
(
2
≤
n
≤
5000
)
(2 \le n \le 5000)
(2≤n≤5000)
一个性质:
若有
x
x
x 个人,移动到
x
x
x 个位置上(起点、终点都已知),那么最优的移动策略为,将人与座位从左到右排序,第
i
i
i 个人移动到第
i
i
i 个位置上。
设前
i
i
i 个人,可以移动到前
j
j
j 个位置(
i
≤
j
i \le j
i≤j),最优解为
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j],那么
d
p
[
i
]
[
j
]
=
m
i
n
{
d
p
[
i
]
[
j
−
1
]
(
不
占
用
第
j
个
0
的
位
置
)
d
p
[
i
−
1
]
[
j
−
1
]
+
a
b
s
(
f
u
l
[
i
]
−
e
m
p
[
j
]
)
(
第
i
个
1
占
用
第
j
个
0
的
位
置
)
dp[i][j]=min \left\{ \begin{array}{lr} dp[i][j-1]&(不占用第j个0的位置)\\ dp[i-1][j-1]+abs(ful[i]-emp[j])&(第i个1占用第j个0的位置)\\ \end{array} \right.
dp[i][j]=min{dp[i][j−1]dp[i−1][j−1]+abs(ful[i]−emp[j])(不占用第j个0的位置)(第i个1占用第j个0的位置)
#include<bits/stdc++.h>
using namespace std;
const int N=5010;
int n,a[N],emp[N],ful[N],t1,t2,dp[N][N];
int main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) if(a[i]) ful[++t1]=i;
for(int i=1;i<=n;i++) if(!a[i]) emp[++t2]=i;
for(int i=1;i<=n;i++) dp[i][i]=dp[i-1][i-1]+abs(emp[i]-ful[i]);
for(int i=1;i<=t1;i++)
for(int j=i+1;j<=t2;j++)
dp[i][j]=min(dp[i][j-1],dp[i-1][j-1]+abs(emp[j]-ful[i]));
cout<<dp[t1][t2]<<"\n";
}
【例题】匹配正则表达式
【例题】Mobile Service
简化题意:
有三个服务员,最初的位置在 1 1 1, 2 2 2 , 3 3 3 处。
有一个长度为 n n n 的请求序列,要求派遣员工去位置 a [ i ] a[i] a[i],从位置 i i i 到 j j j 的花费为 c ( i , j ) c(i,j) c(i,j)。花费函数 c ( i , j ) c(i,j) c(i,j) 不一定对称,不过保证 c ( i , i ) = 0 c(i,i)=0 c(i,i)=0。
要求移动员工,按照顺序依次满足所有请求,并且计算最小花费。
同一时间同一个位置只能有一个员工。
N ≤ 1000 N\le 1000 N≤1000 , 位置为 1 1 1 ~ 200 200 200 之间的整数。
思路:
首先构造一个状态的表示方法:
设 d p [ i ] [ x ] [ y ] [ z ] dp[i][x][y][z] dp[i][x][y][z] 表示,满足前 i i i 个要求,三个员工的位置分别在 x x x , y y y , z z z 的最小花费。
d p [ i ] [ x ] [ y ] [ z ] dp[i][x][y][z] dp[i][x][y][z] 可以更新 d p [ i + 1 ] [ ] [ ] [ ] dp[i+1][~][~][~] dp[i+1][ ][ ][ ] 的三个状态:
-
派 x x x 位置的人去 p i + 1 p_{i+1} pi+1 位置
d p [ i + 1 ] [ p i + 1 ] [ y ] [ z ] = m i n ( d p [ i + 1 ] [ p i + 1 ] [ y ] [ z ] , d p [ i ] [ x ] [ y ] [ z ] + c ( x , p i + 1 ) ) dp[i+1][p_{i+1}][y][z]=min(~dp[i+1][p_{i+1}][y][z] ~,~ dp[i][x][y][z]+c(x,p_{i+1})~) dp[i+1][pi+1][y][z]=min( dp[i+1][pi+1][y][z] , dp[i][x][y][z]+c(x,pi+1) ) -
派 y y y 位置的人去 p i + 1 p_{i+1} pi+1 位置
d p [ i + 1 ] [ x ] [ p i + 1 ] [ z ] = m i n ( d p [ i + 1 ] [ x ] [ p i + 1 ] [ z ] , d p [ i ] [ x ] [ y ] [ z ] + c ( y , p i + 1 ) ) dp[i+1][x][p_{i+1}][z]=min(~dp[i+1][x][p_{i+1}][z] ~,~ dp[i][x][y][z]+c(y,p_{i+1})~) dp[i+1][x][pi+1][z]=min( dp[i+1][x][pi+1][z] , dp[i][x][y][z]+c(y,pi+1) ) -
派 z z z 位置的人去 p i + 1 p_{i+1} pi+1 位置
d p [ i + 1 ] [ x ] [ y ] [ p i + 1 ] = m i n ( d p [ i + 1 ] [ x ] [ y ] [ p i + 1 ] , d p [ i ] [ x ] [ y ] [ z ] + c ( z , p i + 1 ) ) dp[i+1][x][y][p_{i+1}]=min(~dp[i+1][x][y][p_{i+1}] ~,~ dp[i][x][y][z]+c(z,p_{i+1})~) dp[i+1][x][y][pi+1]=min( dp[i+1][x][y][pi+1] , dp[i][x][y][z]+c(z,pi+1) )
这个方法的规模为 1000 × 20 0 3 1000 \times200^3 1000×2003 ,会超时。
其实上面这种表示状态的方法存在着冗余,当满足了前 i i i 个请求的时候,必定有一个人当前位置在 p i p_i pi 。只需要再知道其他两个人的位置,就能描述当前的状态。所以设 d p [ i ] [ x ] [ y ] dp[i][x][y] dp[i][x][y] 表示,满足了前 i i i 个请求,现在一个人的位置在 p i p_i pi ,另外两个人的位置在 x x x 与 y y y 的状态下,花费的最小值。
那么 d p [ i ] [ x ] [ y ] dp[i][x][y] dp[i][x][y] 也能更新三个状态:
- 派
p
i
p_i
pi 位置的人去
p
i
+
1
p_{i+1}
pi+1 位置
d p [ i + 1 ] [ x ] [ y ] = m i n ( d p [ i + 1 ] [ x ] [ y ] , d p [ i ] [ x ] [ y ] + c ( p i , p i + 1 ) ) dp[i+1][x][y]=min(~dp[i+1][x][y]~,~dp[i][x][y]+c(p_i,p_{i+1})~) dp[i+1][x][y]=min( dp[i+1][x][y] , dp[i][x][y]+c(pi,pi+1) )
- 派
x
x
x 位置的人去
p
i
+
1
p_{i+1}
pi+1 位置
d p [ i + 1 ] [ p i ] [ y ] = m i n ( d p [ i + 1 ] [ p i ] [ y ] , d p [ i ] [ x ] [ y ] + c ( x , p i + 1 ) ) dp[i+1][p_i][y]=min(~dp[i+1][p_i][y]~,~dp[i][x][y]+c(x,p{i+1})~) dp[i+1][pi][y]=min( dp[i+1][pi][y] , dp[i][x][y]+c(x,pi+1) )
- 派
y
y
y 位置的人去
p
i
+
1
p_{i+1}
pi+1 位置
d p [ i + 1 ] [ p i ] [ x ] = m i n ( d p [ i + 1 ] [ p i ] [ x ] , d p [ i ] [ x ] [ y ] + c ( y , p i + 1 ) ) dp[i+1][p_i][x]=min(~dp[i+1][p_i][x]~,~dp[i][x][y]+c(y,p{i+1})~) dp[i+1][pi][x]=min( dp[i+1][pi][x] , dp[i][x][y]+c(y,pi+1) )
现在,这个算法的规模变为
1000
×
20
0
2
1000 \times200^2
1000×2002。
#include<bits/stdc++.h>
using namespace std;
const int N=210;
int T,n,m,c[N][N],dp[2][N][N],p[1010],ans=0x3f3f3f3f;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>c[i][j];
for(int i=1;i<=m;i++) cin>>p[i];
p[0]=3;
memset(dp,0x3f,sizeof(dp));
dp[0][1][2]=dp[0][2][1]=0;
for(int i=1;i<=m;i++){
for(int j=0;j<=n;j++)
for(int k=0;k<=n;k++)
dp[i&1][j][k]=(int)1e8;
for(int x=1;x<=n;x++){
for(int y=1;y<=n;y++){
if(x==y||x==p[i-1]||y==p[i-1]) continue;
if(x!=p[i]&&y!=p[i]) dp[i&1][x][y]=min(dp[i&1][x][y],dp[(i-1)&1][x][y]+c[p[i-1]][p[i]]);
if(y!=p[i]&&p[i]!=p[i-1]) dp[i&1][p[i-1]][y]=min(dp[i&1][p[i-1]][y],dp[(i-1)&1][x][y]+c[x][p[i]]);
if(x!=p[i]&&p[i]!=p[i-1]) dp[i&1][x][p[i-1]]=min(dp[i&1][x][p[i-1]],dp[(i-1)&1][x][y]+c[y][p[i]]);
}
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
ans=min(ans,dp[m&1][i][j]);
cout<<ans<<"\n";
}