0
点赞
收藏
分享

微信扫一扫

动态规划:线性dp、背包问题、区间2

江南北 2022-03-20 阅读 110

令第一个字符串为a,第二个字符串为b,f[i][j]表示到a第i个字母为止和到b第j个字母为止的lcs。

当a[i]==b[j], 则f[i][j]=f[i-1][j-1]+1,即之前的子串的最大公共子序列加上目前相等的这个字母;

当a[i]!=b[j], 则f[i][j]=max(f[i][j-1],f[i-1][j]),即拿a在i之前的子串与目前的b比较、拿b在j之前的子串与目前的a比较。

边界点:若a[1]==b[1],则f[1][1]=1;否则f[1][1]=0。

细节问题:若是记忆化搜索,因为当为0时也需要作为值返回,所以清空数组的时候要用-1.不然会超时QAQ

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f, maxn=205;
string a,b;
int f[maxn][maxn],la,lb;
//int calc(int i,int j){
//	if(f[i][j]!=-1) return f[i][j];
//	if(i<1||j<1) return f[i][j]=0;
//	if(i==1&&j==1){
//		if(a[i]!=b[j]) return 0;
//		else return 1; 
//	}
//	if(a[i]==b[j]) f[i][j]=calc(i-1,j-1)+1;
//	else f[i][j]=max(calc(i,j-1),calc(i-1,j));
//	return f[i][j];
//}
int main(){
	while(cin>>a>>b){
//		memset(f,-1,sizeof(f));
		memset(f,0,sizeof(f));
		la=a.length(); lb=b.length();
		a='0'+a; b='0'+b;
		cout<<a<<endl;
		for(int i=1;i<=la;++i){
			for(int j=1;j<=lb;++j){
				if(i==1&&j==1){
					if(a[i]!=b[j]) f[1][1]=0;
					else f[1][1]=1; 
					continue;
				}
				if(a[i]==b[j]) f[i][j]=f[i-1][j-1]+1;
				else f[i][j]=max(f[i][j-1],f[i-1][j]);
			}
		}
		cout<<f[la][lb]<<endl;
//		cout<<calc(la,lb)<<endl;
	}
	return 0;
}

由于我们只在意最后还剩多少容量,所以可以考虑用可行性描述:bool数组f[i][j]表示前i个物体能否放满体积为j的背包,通过枚举最后一次决定——第i个物品放不放,f[i][j] = f[i-1][j] || f[i-1][j-v[i]],同时初值设f[0][0]=1。

细节问题:都说是前i个了,所以一定要记得让数组继承前一个容纳情况的信息!这就要求j必须从0开始循环,保证信息有效继承!爆哭QAQ

01滚动数组——一个节省空间的做法,就是不断更新数组为当前2个,由于更新数组仅需要前一行的信息,所以只要保留两行数组就好了,方法就是直接对行模2。不过注意,这里滚动的时候数组没有清0是因为本来就继承上面的信息!在其他题目中需要多加小心。

就地滚动——更常用的方法,但需要注意的是,如果从小到大循环数组,会使得某个物品加入不止一次(比如,体积为5的箱子在循环到总体积为10时,因为不能够识别之前的5是在本轮循环中刚加入过的,导致10处也被标记,相当于又加入了一次,15/20等等也重复这种错误),这时我们可以通过倒序循环,就可以避免这种情况啦。

01滚动数组:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f, maxn=35;
int x,n,ans,v[35],f[2][20005];
int main(){
	cin>>x>>n;
	for(int i=1;i<=n;++i) 
		cin>>v[i];
	for(int i=0;i<=n;++i)
		f[i][0]=1;
	for(int i=1;i<=n;++i){
		for(int j=0;j<=x;++j){
			if( j >= v[i] )f[i%2][j]=f[(i-1)%2][j]||f[(i-1)%2][j-v[i]];
			else f[i%2][j] = f[(i-1)%2][j];
			if(f[i%2][j]){
				ans=max(j,ans);
			}
		}
	}
	cout<<x-ans;
	return 0;
}

就地滚动数组:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f, maxn=35;
int x,n,ans,v[35],f[20005];
int main(){
	cin>>x>>n;
	for(int i=1;i<=n;++i) 
		cin>>v[i];
	f[0]=1;
	for(int i=1;i<=n;++i){
		for(int j=x;j>=v[i];--j){
			f[j]=f[j]||f[j-v[i]];
			if(f[j]) ans=max(ans,j);
		}
	}
	cout<<x-ans;
	return 0;
}

背包

设f[i][j]表示前i种草药中选,在时间j之前能够采到的最大价值,c[i]为花费时间,w[i]为草药价值。则可以得到状态转移方程:

f[i][j]=max(f[i-1][j], f[i-1][j-c[i]]+w[i]);

即前i种草药中选、时间j之前能够采到的最大价值 等于 不选第i种草药 与选第i种草药的最大价值。注意,当初始值设为-inf、f[i][0]=0的时候不能保证最终答案出现在最后一个数组空间中,而是确定了怎样不会有剩余时间。

但也可以为没有装满的部分填充“时间”,所有初值设为0可以达到此效果,即假设它用了目前所有的时间,而把最大的数存起来,这样就可以在数组最后轻松取出最大值。不过需要注意,题目中若有明确要求不能有剩余时间,就不可以这样做了。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f, maxn=105;
int t,m,ans,c[maxn],w[maxn],f[1005];
int main(){
	cin>>t>>m;
	for(int i=1;i<=m;++i)
		cin>>c[i]>>w[i];
	for(int i=1;i<=m;++i){
		for(int j=t;j>=1;--j)
			if(j>=c[i]) f[j]=max(f[j],f[j-c[i]]+w[i]);
	}
	for(int i=1;i<=t;++i)
		ans=max(ans,f[i]);
	cout<<ans;
	return 0;
}

扩展

M<=20, T<=10^9:暴搜

M<=40, T<=10^9:分半搜索

M<=100, T<=10^9:去掉无效的空间(第一不会存在没有用的时间,第二不会存在比时间少的价值还低的无用的点),使用map进行信息存储。

转移方程:f[i][j]=max(f[i-1][j], f[i][j- k* c[i]]+ k* w[i]),我们发现就是要不断放入同一个草药,于是想起来之前在01背包中就地滚动时的顺序滚动会重复加入的特点,其实就是它。

细节问题:注意数据范围!

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f, maxn=10000005;
ll t,m,ans,c[maxn],w[maxn],f[maxn];
int main(){
	cin>>t>>m;
	for(int i=1;i<=m;++i)
		cin>>c[i]>>w[i];
	for(int i=1;i<=m;++i)
		for(int j=c[i];j<=t;++j)
			f[j]=max(f[j],f[j-c[i]]+w[i]);
	cout<<f[t];
	return 0;
}

首先,此题可以被拆分为01背包做。优化方法就是可以通过将过多的物品打包成2的0~k次幂。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f, maxn=10000005;
ll t,n,V,num,v[maxn],w[maxn],f[105];
int main(){
	cin>>n>>V;
	int vv,ww,ss;
	while(n--){
		cin>>vv>>ww>>ss;
		int i=1;
		while(ss>0){
			num++;
			v[num]=i*vv;
			w[num]=i*ww;
			ss-=i;
			if(ss>i) i*=2;
			else i=ss;
		}
	}
	for(int i=1;i<=num;++i)
		for(int j=V;j>=v[i];--j)
			f[j]=max(f[j],f[j-v[i]]+w[i]);
	cout<<f[V];
	return 0;
}

当发现题目是由熟悉的动态规划题目添加了限制条件变形得到,可以在原题状态加一维做。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f, maxn=1005;
ll N,V,M,v[maxn],m[maxn],w[maxn],f[maxn][maxn];
int main(){
	cin>>N>>V>>M;
	for(int i=1;i<=N;++i)
		cin>>v[i]>>m[i]>>w[i];
	for(int i=1;i<=N;++i)
		for(int j=V;j>=v[i];--j)
			for(int k=M;k>=m[i];--k)
				f[j][k]=max(f[j][k],f[j-v[i]][k-m[i]]+w[i]);
	cout<<f[V][M];
	return 0;
}

分组背包就是在每个组中挑一个,求总价值最大。需要注意的是循环顺序:先枚举组数,再枚举体积,再枚举组内每个物品。如果将枚举组内每个物品和枚举体积反过来,就成为了01背包。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f, maxn=105;
struct ty{
	int v[maxn],w[maxn],s;
}g[maxn];
int N,V,s,f[maxn];
int main(){
	cin>>N>>V;
	for(int i=1;i<=N;++i){
		cin>>s;
		g[i].s=s;
		for(int j=1;j<=s;++j)
			cin>>g[i].v[j]>>g[i].w[j];
	}
	for(int i=1;i<=N;++i)
		for(int j=V;j>=1;--j)
			for(int k=1;k<=g[i].s;++k)
				if(j>=g[i].v[k])
					f[j]=max(f[j],f[j-g[i].v[k]]+g[i].w[k]);
	cout<<f[V];
				
	return 0;
}
举报

相关推荐

0 条评论