<更新提示>
<第一次更新>昨天老刘口胡了一波dp,看到除了高级dp以外,自己竟然还有一块dp基本没碰过,然后赶快自学了一晚上,总结成博客如下。
<正文>
双进程类动态规划
初步
顾名思义,就是在动态规划时需要对两个对象同时进行决策,而两个决策并不是互相独立的,而是互相影响的。
例题
最长公共子序列
题目描述
字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。 令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序 < i0,i1,i2,…,ik-1 >,使得对所有的j=0,1,…,k-1,有xij = yj。
例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
对给定的两个字符序列,求出他们最长的公共子序列。
输入格式
第1行为第1个字符序列,都是大写字母组成,以”.”结束。长度小于5000。 第2行为第2个字符序列,都是大写字母组成,以”.”结束,长度小于5000。
输出格式
输出上述两个最长公共自序列的长度。
样例数据
input
ABCBDAB.
BACBBD.
output
4
数据规模与约定
时间限制:1s
空间限制:256MB
分析
这是一道双进程类dp的入门题,也可以说是序列dp的入门题。其中,关键的字眼“他们”,“公共子序列”我已经在题面里标出,代表了这是一道双进程类的dp题。“他们”代表有两个对象,“公共子序列”代表决策互相影响,符合双进程类dp的定义,我们接下来考虑如何解决。
既然两个对象的决策是互相影响的,我们不妨设一个二维状态
代表序列X的前i位,序列Y的前j位当中最长公共子序列的长度。那么我们就可以利用两重循环枚举i,j来对状态进行转移,其状态转移方程如下:
其意义就是如果当前这两个元素不相等,那么从上两个决策中选最优,如果相等,那么该元素可以组成最长公共子序列的一部分,更新状态。
那么 的时间解决该问题。
代码实现如下:
#include<bits/stdc++.h>
using namespace std;
string s1,s2;
char a[5050],b[5050];
int f[5050][5050]={},maxx=0;
int main()
{
cin>>s1>>s2;
for (int i=0;i<s1.length();i++)
a[i+1]=s1[i];
for (int i=0;i<s2.length();i++)
b[i+1]=s2[i];
for(int i=1;i<=s1.length();i++)
{
for(int j=1;j<=s2.length();j++)
{
if (a[i]==b[j])f[i][j]=f[i-1][j-1]+1;
else f[i][j]=max(f[i-1][j],f[i][j-1]);
maxx=max(f[i][j],maxx);
}
}
cout<<maxx<<endl;
return 0;
}
交错匹配
题目描述
有两行自然数,UP[1..N],DOWN[1..M],如果UP[I]=DOWN[J]=K,那么上行的第I个位置的数就可以跟下行的第J个位置的数连一条线,称为一条K匹配,但是同一个位置的数最多只能连一条线。
另外,每个K匹配都必须且至多跟一个L匹配相交且K≠L 。现在要求一个最大的匹配数。
例如:以下两行数的最大匹配数为8
输入格式
第一行有两个正整数N和M。第二行N个UP的自然数,第三行M个DOWN的自然数。其中0 < N、M<=200,UP、DOWN的数都不超过32767。
输出格式
一个整数:最大匹配数
样例数据
input1
12 11
1 2 3 3 2 4 1 5 1 3 5 10
3 1 2 3 2 4 12 1 5 5 3
output1
8
input2
4 4
1 1 3 3
1 1 3 3
output2
0
数据规模与约定
时间限制:1s
空间限制:256MB
分析
这道题也是显而易见的双进程入门题,和最长公共子序列相似。对于状态的设置显而易见,
代表UP串匹配到i,DOWN串匹配到j的最大匹配数。那么显然一开始
。那么我们依据题面考虑情况:
①UP[i]=DOWN[j],不能进行匹配。
②UP[i]≠DOWN[j],我们在两个序列中找到上一个出现对应元素的位置,由于不能再次交错,匹配数从上一个位置转移而来,匹配数加2。
可得状态转移方程如下:
怎么找上一个对应元素出现的位置呢,对于这道题,暴力查找即可解决。
代码实现如下:
#include<bits/stdc++.h>
using namespace std;
inline void read(int &k)
{
int x=0,w=0;char ch;
while(!isdigit(ch))w|=ch=='-',ch=getchar();
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
k=(w?-x:x);return;
}
int n,m,a[280]={},b[280]={},f[280][280]={};
inline void input()
{
read(n),read(m);
for(int i=1;i<=n;i++)read(a[i]);
for(int i=1;i<=m;i++)read(b[i]);
}
inline int findlast(int list[],int last,int value)
{
for(int i=last-1;i>=1;i--)
{
if(list[i]==value)return i;
}
return 0;
}
inline void solve()
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=max(f[i-1][j],f[i][j-1]);
if(a[i]==b[j])continue;
int lastI=findlast(a,i,b[j]);
int lastJ=findlast(b,j,a[i]);
if(lastI&&lastJ)
{
f[i][j]=max(f[i][j],f[lastI-1][lastJ-1]+2);
}
}
}
}
inline void output()
{
cout<<f[n][m]<<endl;
}
int main()
{
input();
solve();
output();
return 0;
}
配置魔药
题目描述
在《Harry Potter and the Chamber of Secrets》中,Ron的魔杖因为坐他老爸的Flying Car撞到了打人柳,不幸被打断了,从此之后,他的魔杖的魔力就大大减少,甚至没办法执行他施的魔咒,这为Ron带来了不少的烦恼。
这天上魔药课,Snape要他们每人配置一种魔药(不一定是一样的),Ron因为魔杖的问题,不能完成这个任务,他请Harry在魔药课上(自然是躲过了Snape的检查)帮他配置。 现在Harry面前有两个坩埚,有许多种药材要放进坩埚里,但坩埚的能力有限,无法同时配置所有的药材。一个坩埚相同时间内只能加工一种药材,但是不一定每一种药材都要加进坩埚里。加工每种药材都有必须在一个起始时间和结束时间内完成(起始时间所在的那一刻和结束时间所在的那一刻也算在完成时间内),每种药材都有一个加工后的药效。
现在要求的就是Harry可以得到最大的药效。
输入格式
输入文件的第一行有2个整数,一节魔药课的t(1≤t<≤500)和药材数n(1≤n≤100)。
输入文件第2行到n+1行中每行有3个数字,分别为加工第i种药材的起始时间t1、结束时间t2、(1≤t1≤t2≤t)和药效w(1≤w≤100)。
输出格式
只有一行,只输出一个正整数,即为最大药效。
样例数据
input
7 4
1 2 10
4 7 20
1 3 2
3 7 3
output
35
【注释】本题的样例是这样实现的:第一个坩埚放第1、4种药材,第二个坩埚放第2、3种药材。这样最大的药效就为10+20+2+3=35。
数据规模与约定
对于30%的数据 1<=t<=500 1<=n<=15 1<=w<=100 1<=t1<=t2<=t
对于100%的数据 1<=t<=500 1<=n<=100 1<=w<=100 1<=t1<=t2<=t
时间限制:1s
空间限制:256MB
分析
“两个坩埚”代表了这道题的双进程本质。对于草药,每一个草药都有时间先后的问题,我们将他们按照时间排一下序,就能有序的解决问题。那么剩下的问题该如何解决呢?
解法1:效仿背包问题
仔细读题,是不是发现这道题很像背包问题?只不过他有两个背包,物品花费的是限定的时间段罢了。事实上,我们可以效仿背包写法,设置状态
代表前i种草药,第一个坩埚用时j,第二个坩埚用时k的最大价值。那么这样就兼顾了题面的双进程本质。
考虑状态转移,对于每一个草药的决策,有三种情况:
①不用该草药
②放入坩埚1
③放入坩埚2
那么状态转移方程根据这三种情况转移即可:
显然,这和背包问题相同,每一个状态都由i-1转移而来,可以逆序循环j,k来压掉第一位i。
那么再来考虑一个问题,如何排序?
注意到状态转移方程, 我们每一次转移时以end[i]作为判断,也就是说,在转移时,我们 只在乎j,k是否大于end[i]来转移,所以我们以结束时间来排序。
其代码实现如下:
#include<bits/stdc++.h>
using namespace std;
inline void read(int &k)
{
int x=0,w=0;char ch;
while(!isdigit(ch))w|=ch=='-',ch=getchar();
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
k=(w?-x:x);return;
}
inline void Read(int &a,int &b,int &c){read(a),read(b),read(c);}
struct magic{int begin,end,value;}Magic[1080]={};
inline bool cmp(magic x,magic y){return x.end<y.end;}
int n,t,f[580][580],ans=-1;
inline void input()
{
read(t),read(n);
for(int i=1;i<=n;i++)Read(Magic[i].begin,Magic[i].end,Magic[i].value);
sort(Magic+1,Magic+n+1,cmp);
}
inline void solve()
{
for(int i=1;i<=n;i++)
{
for(int j=t;j>=0;j--)
{
for(int k=t;k>=0;k--)
{
if(j>=Magic[i].end)f[j][k]=max(f[j][k],f[Magic[i].begin-1][k]+Magic[i].value);
if(k>=Magic[i].end)f[j][k]=max(f[j][k],f[j][Magic[i].begin-1]+Magic[i].value);
}
}
}
ans=f[t][t];
}
inline void output()
{
cout<<ans<<endl;
}
int main()
{
input();
solve();
output();
return 0;
}
解法2:记录上一个状态
我们可以在状态的设置中记录坩埚中上一个草药的编号,那么设置f[i][j][k]代表前i个草药,第一个坩埚加工的最后一个草药为j,第二个坩埚加工的最后一个草药为k。那么这样我们就可以很容易的实现状态转移,只需判断是否和上一个草药的时间是否冲突即可。而且,这样三维都以标号设置状态,时间复杂度大大小于解法1。
状态转移方程如下:
再次考虑排序问题,这次,我们在决策时 以草药i的开始时间是否比上一个草药的结束时间晚做判断,也就是说,我们 只在乎草药i的开始时间,所以我们以开始时间排序。
代码实现如下:
#include<bits/stdc++.h>
using namespace std;
inline void read(int &k)
{
int x=0,w=0;char ch;
while(!isdigit(ch))w|=ch=='-',ch=getchar();
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
k=(w?-x:x);return;
}
inline void Read(int &a,int &b,int &c){read(a),read(b),read(c);}
struct magic{int begin,end,value;}Magic[1080]={};
inline bool cmp(magic x,magic y){if(x.begin==y.begin)return x.end<y.end;else return x.begin<y.begin;}
int n,t,f[180][580][580],ans=-1;
inline void input()
{
read(t),read(n);
for(int i=1;i<=n;i++)Read(Magic[i].begin,Magic[i].end,Magic[i].value);
sort(Magic+1,Magic+n+1,cmp);
}
inline void solve()
{
for(int i=1;i<=n;i++)
{
for(int j=0;j<i;j++)
{
for(int k=0;k<i;k++)
{
f[i][j][k]=f[i-1][j][k];
if(Magic[i].begin>Magic[j].end)f[i][i][k]=max(f[i][i][k],f[i-1][j][k]+Magic[i].value);
if(Magic[i].begin>Magic[k].end)f[i][j][i]=max(f[i][j][i],f[i-1][j][k]+Magic[i].value);
ans=max(ans,max(f[i][i][k],f[i][j][i]));
}
}
}
}
inline void output()
{
cout<<ans<<endl;
}
int main()
{
input();
solve();
output();
return 0;
}
构建双塔
题目描述
2001年9月11日,一场突发的灾难将纽约世界贸易中心大厦夷为平地,Mr. F曾亲眼目睹了这次灾难。为了纪念“911”事件,Mr. F决定自己用水晶来搭建一座双塔。 Mr. F有N块水晶,每块水晶有一个高度,他想用这N块水晶搭建两座有同样高度的塔,使他们成为一座双塔,Mr. F可以从这N块水晶中任取M(1≤M≤N)块来搭建。但是他不知道能否使两座塔有同样的高度,也不知道如果能搭建成一座双塔,这座双塔的最大高度是多少。所以他来请你帮忙。给定水晶的数量N(1≤N≤100)和每块水晶的高度Hi(N块水晶高度的总和不超过2000),你的任务是判断Mr. F能否用这些水晶搭建成一座双塔(两座塔有同样的高度),如果能,则输出所能搭建的双塔的最大高度,否则输出“Impossible”。
输入格式
输入的第一行为一个数N,表示水晶的数量。
第二行为N个数,第i个数表示第i个水晶的高度。
输出格式
输出仅包含一行,如果能搭成一座双塔,则输出双塔的最大高度,否则输出一个字符串“Impossible”。
样例数据
input
5
1 3 4 5 2
output
7
数据规模与约定
时间限制:1s
空间限制:256MB
分析
注意题面,两个水晶塔的高度要相同,也就是说两个水晶塔的高度互相约束,这是双进程类dp问题的标志。但是我们显然不能在搭建时保证两个塔的高度一直都相同,所以我们还是需要求解每一种情况。显然对于使用的水晶数量我们需要用状态中的一维来表示,那么我们再使用一维来表示两个塔的高度差就足以解决两个塔的约束关系。即
代表前i个水晶,两个塔高度差为j时其中较高塔的高度。
状态已经解决,思考如何转移。显然,决策是对第i个水晶进行决策,那么可以分四种情况讨论。
①不放该水晶:
②将该水晶放在较低的塔上,且放置后未超过较高塔的高度:
③水晶被放在较高塔上,此时,我们需要保证j>h[i]:
④水晶被放在较低塔上,但放置后较低塔的高度大于较高塔,此时,我们需要保证h[i]>j:
那么这四种情况就能构成状态转移方程:
枚举i,j转移即可,初始条件f[0][0]=0,其余均为负无穷。
代码实现如下:
#include<bits/stdc++.h>
using namespace std;
inline void read(int &k)
{
int x=0,w=0;char ch;
while(!isdigit(ch))w|=ch=='-',ch=getchar();
while(isdigit(ch))x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
k=(w?-x:x);return;
}
//前i个水晶搭成的两座塔高度差为j时高塔的高度
int n,height[180]={},sum=0;
int f[220][2200]={};
int main()
{
read(n);
for(int i=1;i<=n;i++)read(height[i]),sum+=height[i];
memset(f,-10,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++)
{
for(int j=0;j<=sum;j++)
{
f[i][j]=max(f[i-1][j],f[i-1][j+height[i]]);
if(j>height[i])f[i][j]=max(f[i][j],f[i-1][j-height[i]]+height[i]);
else f[i][j]=max(f[i][j],f[i-1][height[i]-j]+j);
}
}
if(f[n][0]>0)cout<<f[n][0]<<endl;
else cout<<"Impossible"<<endl;
return 0;
}
总结
对于这一类新的dp,一开始表示很懵。但做几道入门题就能发现,这类题目一般在题面中有章可循,可以看出题面是否是双进程dp。对于解决这类题目,一般先从状态的设置入手,在状态中先解决两个决策的约束问题,再想办法进行转移。这类题目为我们提供了一种思考的思路,我们需要更深刻的理解状态和阶段的本质。
<后记>
<废话>