线性dp与背包dp

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[i1][jwi]+vi;若不拿,则 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i1][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[i1][j]dp[i1][jwi]+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++2pCi R i = C i − ( 2 p + 1 − 1 ) R_i=C_i-(2^{p+1}-1) Ri=Ci(2p+11)

可以把 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 1N100 1 ≤ M ≤ 1 0 5 1 \le M \le 10^5 1M105 1 ≤ A i ≤ 1 0 5 1 \le A_i \le 10^5 1Ai105 1 ≤ C i ≤ 1000 1 \le C_i \le 1000 1Ci1000

#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 0a[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 1n200 , 1 ≤ m ≤ 20 1\le m \le20 1m20 )

思路:

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[i1][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[i1][j1][kd[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[i1][j][k]dp[i1][j1][kdi]+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| ij ,求一个最小花费,使得最初有人的位置都变得空闲。保证存在这样的移动方案。 ( 2 ≤ n ≤ 5000 ) (2 \le n \le 5000) (2n5000)

一个性质:
若有 x x x 个人,移动到 x x x 个位置上(起点、终点都已知),那么最优的移动策略为,将人与座位从左到右排序,第 i i i 个人移动到第 i i i 个位置上。

设前 i i i 个人,可以移动到前 j j j 个位置( i ≤ j i \le j ij),最优解为 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][j1]dp[i1][j1]+abs(ful[i]emp[j])(j0)(i1j0)

#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 N1000 , 位置为 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";
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

m0_51864047

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值