DP动态规划专题(二)动态规划应用例题和代码实现
本篇着重进行动态规划例题展开,关于动态规划的基本模型和基础知识,请移步
DP动态规划专题(一)动态规划基本模型
【例1】最长不下降序列
㈠问题描述:
设有由n个不相同的整数组成的数列,记为:b(1)、b(2)、……、b(n)且b(i)<>b(j) (i<>j),若存在i1<i2<i3< … < ie 且有b(i1)<b(i2)< … <b(ie)则称为长度为e的不下降序列。程序要求,当原数列出之后,求出最长的不下降序列。
- 例如13,7,9,16,38,24,37,18,44,19,21,22,63,15。
- 例中13,16,18,19,21,22,63就是一个长度为7的不下降序列,
- 同时也有7 ,9,16,18,19,21,22,63长度为8的不下降序列。
㈡算法分析:
根据动态规划的原理,由后往前进行搜索(当然从前往后也一样)。
- 对b(n)来说,由于它是最后一个数,所以当从b(n)开始查找时,只存在长度为1的不下降序列;
- 若从b(n-1)开始查找,则存在下面的两种可能性:
①若b(n-1)<b(n)则存在长度为2的不下降序列b(n-1),b(n)。
②若b(n-1)>b(n)则存在长度为1的不下降序列b(n-1)或b(n)。 - 一般若从b(i)开始,此时最长不下降序列应该按下列方法求出:
在b(i+1),b(i+2),…,b(n)中,找出一个比b(i)大的且最长的不下降序列,作为它的后继。
㈢数据结构:
为算法上的需要,定义一个整数类型二维数组b(N,3)
- b(I,1)表示第I个数的数值本身;
- b(I,2)表示从I位置到达N的最长不下降序列长度
- b(I,3)表示从I位置开始最长不下降序列的下一个位置,若b[I,3]=0则表示后面没有连接项。
㈣求解过程:
①从倒数第二项开始计算,后面仅有1项,比较一次,因63>15,不符合要求,长度仍为1。
②从倒数第三项开始其后有2项,需做两次比较,得到目前最长的不下降序列为2,如下表:
㈤一般处理过程是:
①在i+1,i+2,…,n项中,找出比b[I,1]大的最长长度L以及位置K;
②若L>0,则b[I,2]:=L+1;b[I,3]:=k;
最后本题经过计算,其数据存储表如下:
code
#include<iostream>
using namespace std;
int main() {
int n,i,j,l,k,b[200][10];
cout<<"input n:"<<endl;
cin>>n;
for (i=1; i<=n; i++) { //输入序列的初始值
cin>>b[i][1];//value
b[i][2]=1;//long
b[i][3]=0;//position
}
for (i=n-1; i>=1; i--) { //求最长不下降序列
l=0;
k=0;
for (j=i+1; j<=n; j++)
if ((b[j][1]>b[i][1])&&(b[j][2]>l)) {
l=b[j][2];
k=j;
}
if (l>0) {
b[i][2]=l+1;
b[i][3]=k;
}
}
k=1;
for (j=1; j<=n; j++) //求最长不下降序列的起始位置
if (b[j][2]>b[k][2]) k=j;
cout<<"max="<<b[k][2]<<endl; //输出结果
while (k!=0) { //输出最长不下降序列
cout<<' '<<b[k][1];
k=b[k][3];
}
}
【例2】拦截导弹
【题目】
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
【样例】
INPUT
389 207 155 300 299 170 158 65
OUTPUT
6(最多能拦截的导弹数)
2(拦截所有导弹最少要配的系统数)
【算法分析】
第一问即经典的最长不下降子序列问题,可以用一般的DP算法,也可以用高效算法,但这个题的数据规模不需要。
用a[x]表示原序列中第x个元素,b[x]表示长度为x的不下降子序列的长度。当处理第a[x]时,可查找它可以连接到长度最大为多少的不下降子序列后(即与部分b[x]比较)。假设可以连到长度最大为maxx的不下降子序列后,则b[x]:=maxx+1。b数组被赋值的最大值就是第一问的答案。
第二问用贪心法即可。每颗导弹来袭时,使用能拦截这颗导弹的防御系统中上一次拦截导弹高度最低的那一套来拦截。若不存在符合这一条件的系统,则使用一套新系统。
【参考程序】(顺推法)
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int main()
{
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
int i,j,k,x,n,maxx,m,a[10000],b[10000],h[10000];
i=1;n=0;m=0;
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
memset(h,0,sizeof(h));
while (cin>>a[i])
{
maxx=0;
for (j=1;j<=i-1;j++) //计算前i-1个导弹最佳拦截的方案
if (a[j]>=a[i])
if (b[j]>maxx)
maxx=b[j];
b[i]=maxx+1;
if (b[i]>m) m=b[i];
x=0;
for (k=1;k<=n;k++) //计算由哪一套系统拦截导弹
if (h[k]>=a[i])
if (x==0) x=k;
else if (h[k]<h[x]) x=k; //如果有多套系统可拦截,则选择上一次拦截高度最低的
if (x==0) {n++;x=n;} //新增一套导弹拦截系统
h[x]=a[i];
i++;
}
cout<<m<<endl<<n<<endl;
}
【例3】城市交通网
【题目】
下图表示城市之间的交通路网,线段上的数字表示费用,单向通行由A->E。试用动态规划的最优化原理求出A->E的最省费用。
【样例输入】short.in
10
0 2 5 1 0 0 0 0 0 0
0 0 0 0 12 14 0 0 0 0
0 0 0 0 6 10 4 0 0 0
0 0 0 0 13 12 11 0 0 0
0 0 0 0 0 0 0 3 9 0
0 0 0 0 0 0 0 6 5 0
0 0 0 0 0 0 0 0 10 0
0 0 0 0 0 0 0 0 0 5
0 0 0 0 0 0 0 0 0 2
0 0 0 0 0 0 0 0 0 0
【样例输出】short.out
minlong=19
1 3 5 8 10
【算法分析】逆推法
设f[i]表示点i到v10的最短路径长度,则 f[10]=0
f[i]=min{ a[i][x]+f[x] 当a[i][x]>0 ,i<x<=n时}
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int main()
{
long n,i,j,x,f[100],c[100],a[100][100];
memset(a,0,sizeof(a));
memset(c,0,sizeof(c));
cin>>n;
for (i=1;i<=n;i++) //输入各个城市之间距离
for (j=1;j<=n;j++)
cin>>a[i][j];
for (i=1;i<=n;i++)
f[i]=1000000; //初始化,默认每一个城市到达终点都是1000000
f[n]=0;
for (i=n-1;i>=1;i--) //从终点往前逆推,计算最短路径
for (x=i+1;x<=n;x++) //若f[x]=1000000表示城市x到终点城市不通
if ((a[i][x]>0)&&(f[x]!=1000000)&&(f[i]>a[i][x]+f[x]))
{ //a[i][x]>0表示城市i和城市x通路
f[i]=a[i][x]+f[x]; //城市i到终点最短路径的值
c[i]=x;
}
cout<<"minlong="<<f[1]<<endl; //输出最短路径的值
x=1;
while (x!=0) //输出路过的各个城市
{
cout<<x<<' ';
x=c[x];
}
cout<<endl;
}
【例4】挖地雷
【问题描述】
在一个地图上有N个地窖(N<=200),每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径,并规定路径都是单向的,也不存在可以从一个地窖出发经过若干地窖后又回到原来地窖的路径。某人可以从任一处开始挖地雷,然后沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使他能挖到最多的地雷。
【输入格式】
N //地窖的个数
W1,W2,……WN //每个地窖中的地雷数
X1,Y1 //表示从X1可到Y1
X2,Y2
……
0,0 //表示输入结束
【输出格式】
K1-K2-…-Kv //挖地雷的顺序
MAX //最多挖出的地雷数
【输入样例】Mine.in
6
5 10 20 5 4 5
1 2
1 4
2 4
3 4
4 5
4 6
5 6
0 0
【输出样例】Mine.out
3-4-5-6
34
【算法分析】
本题是一个经典的动态规划问题。很明显,题目规定所有路径都是单向的,所以满足无后效性原则和最优化原理。设W[i]为第i个地窖所藏有的地雷数,A[i][j]表示第i个地窖与第j个地窖之间是否有通路,F[i]为从第i个地窖开始最多可以挖出的地雷数,则有如下递归式:
F[i]=max{ W[i]+ F[j]} (i<j<=n , A[i][j]=true)
边界:F[n]=W[n]
于是就可以通过递推的方法,从后F(n)往前逐个找出所有的F[i],再从中找一个最大的即为问题2的解。对于具体所走的路径(问题1),可以通过一个向后的链接来实现。
【参考程序】
#include<iostream>
#include<cstring>
using namespace std;
int main()
{
long f[201]={0},w[201],c[201]={0};
bool a[201][201]={0};
long i,j,n,x,y,l,k,maxx;
memset(f,0,sizeof(f));
memset(c,0,sizeof(c));
memset(a,false,sizeof(a));
cin>>n;
for (i=1;i<=n;i++)
cin>>w[i]; //输入每个地窖中的地雷数
do
{
cin>>x>>y; //表示从X可到Y
if ((x!=0)&&(y!=0)) a[x][y]=true;
}while ((x!=0)||(y!=0));
f[n]=w[n]; //从后F[n]往前逐个找出所有的F[i]
for (i=n-1;i>=1;i--)
{
l=0;k=0;
for (j=i+1;j<=n;j++)
if ((a[i][j])&&(f[j]>l))
{ l=f[j]; k=j; }
f[i]=l+w[i]; //保存从第i个地窖起能挖到最大地雷数
c[i]=k; //k地窖是i地窖最优路径
}
k=1;
for (j=2;j<=n;j++) //计算最多挖出的地雷数
if (f[j]>f[k]) k=j;
maxx=f[k];
cout<<k;
k=c[k];
while (k!=0) //输出挖地雷的顺序
{
cout<<"-"<<k;
k=c[k];
}
cout<<endl;
cout<<maxx<<endl; //输出最多挖出的地雷数
}
【例5】友好城市
【问题描述】
Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。
【输入格式】
第1行,一个整数N(1<=N<=5000),表示城市数。
第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。(0<=xi<=10000)
【输出格式】
仅一行,输出一个整数,表示政府所能批准的最多申请数。
【输入样例】
7
22 4
2 6
10 3
15 12
9 8
17 17
4 2
【输出样例】
4
【算法分析】
我们将每对友好城市看成一条线段,则这道题的描述化为:有N条线段,问最少去掉多少条线,可以使剩下的线段互不交叉?
第一,以北岸为线的起点而南岸为线的终点;先将所有的线按照起点坐标值从小到大排序,按照每条线的终点坐标计算交叉数:求线I的交叉数A[I],则检查所有1…I-1条线,若线J( 1<= J< I)的终点值大于线I的终点值,则线I与线J相交。A[I]与A[J]同时加1。整个搜索量最大为5000*5000。
第二,将A数组从大到小排序,每删除一条线,则将与之相交的线的A值减1,重复这个过程,直到所有A值都为0。此时剩下的线则全不交叉。
于是,我们从上面的分析中再深入,将航线按起点坐标排好序后,如上所述,在本题中,只要线J的起点小于线I的起点,同时它的终点也小于线I的终点,则线J和线I不相交。因此,求所有线中最多能有多少条线不相交,实际上是从终点坐标值数列中求一个最长不下降序列。这就把题目转化为一个非常典型的动态规划题目了。
求最长不下降序列的规划方程如下:
L(Si)=max{L(Sj)}+1;1< = j < i且 Sj < Si。Si为航线的终点坐标值。
从以上分析过程可以得出:当我们拿到一道题时,不要急于求解,而应先将题目的表面现象一层层象剥竹笋一样去掉,只留下最实质的内容。这时再来设计算法,往往能事半功倍。
/**
友好城市
起点排序,得到终点的顺序
求终点的不下降序列
逆推公式 f[i]=max{f[x]+1,x>i}
边界:f[n]=1
*/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1000;
struct node{
int x;
int y;
};
int a[N][N],f[N],w[N],c[N];
int i,j,n,l,k,maxx;
node river[N];
bool cmp(node a, node b){//自定义排序函数
return a.x<b.x;
}
int main(){
cin>>n;
for(i=1; i<=n; i++){
cin>>river[i].x;
cin>>river[i].y;
}
sort(river,river+n+1,cmp);
for(i=1; i<=n; i++){
w[i] = river[i].y;
}
f[n] = 1;//border
for(i=n-1; i>=1; i--){
l = 0; k = 0;
for(j=i+1; j<=n; j++){
if(w[i]<w[j]&&l<f[j]){
l = f[j];//l代表i之后的最大长度
k = j;// k代表i之后的紧接着位置
}
}
f[i] = l+1;
}
maxx = 0;
for(i=1; i<=n; i++){
if(maxx<f[i])
maxx = f[i];
}
cout<<"max = "<<maxx;
return 0;
}
【例6】合唱队形
【问题描述】
N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1, 2, …, K,他们的身高分别为T1, T2, …, TK,则他们的身高满足T1 < T2 < … < Ti , Ti > Ti+1 > … > TK (1≤i≤K)。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
【输入文件】
输入文件chorus.in的第一行是一个整数N(2 ≤ N ≤ 100),表示同学的总数。第二行有n个整数,用空格分隔,第i个整数Ti(130 ≤ Ti ≤ 230)是第i位同学的身高(厘米)。
【输出文件】
输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
【样例输入】
8
186 186 150 200 160 130 197 220
【样例输出】
4
【数据规模】
对于50%的数据,保证有n ≤ 20;对于全部的数据,保证有n≤100。
【算法分析】
我们按照由左而右和由右而左的顺序,将n个同学的身高排成数列。如何分别在这两个数列中寻求递增的、未必连续的最长子序列,就成为问题的关键。设
a 为身高序列,其中a[i]为同学i的身高;
b 为由左而右身高递增的人数序列,其中 b[i]为同学1‥同学i间(包括同学i)身高满足递增顺序的最多人数。显然b[i]=max{b[j]|同学j的身高<同学i的身高}+1;
c为由右而左身高递增的人数序列,其中c[i]为同学n‥同学i间(包括同学i)身高满足递增顺序的最多人数。显然c[i]=max{c[j]|同学j的身高<同学i的身高}+1;
由上述状态转移方程可知,计算合唱队形的问题具备了最优子结构性质(要使b[i]和c[i]最大,子问题的解b[j]和c[k]必须最大(1≤j≤i-1 ,i+1≤k≤n))和重迭子问题的性质(为求得b[i]和c[i],必须一一查阅子问题的解b[1]‥b[i-1]和c[i+1]‥c[n]),因此可采用动态程序设计的方法求解。
显然,合唱队的人数为max{b[i]+c[i]}-1(公式中同学i被重复计算,因此减1),n减去合唱队人数即为解。
【具体算法】
#include<cstring>
#include<iostream>
using namespace std;
int a[200],b[200],c[200];
main()
{
int n,i,j,maxx;
cin>>n; //读学生数
memset(b,0,sizeof(b)); //身高满足递增顺序的两个队列初始化
memset(c,0,sizeof(c));
for (i=1;i<=n;i++) //读每个学生的身高
cin>>a[i];
for (i=1;i<=n;i++) //按照由左而右的顺序计算b序列
{
b[i]=1;
for (j=1;j<=i-1;j++)
if ((a[i]>a[j])&&(b[j]+1>b[i]))
b[i]=b[j]+1;
}
for (i=n;i>=1;i--) //按照由右而左的顺序计算c序列
{
c[i]=1;
for (j=i+1;j<=n;j++)
if ((a[j]<a[i])&&(c[j]+1>c[i]))
c[i]=c[j]+1;
}
maxx=0; //计算合唱队的人数max(其中1人被重复计算
for (i=1;i<=n;i++)
if (b[i]+c[i]>maxx)
maxx=b[i]+c[i];
cout<<n-maxx+1<<endl; //输出出列人数
}
【例7】最长公共子序列
【问题描述】
一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列X=<x1,x2,…,xm>,则另一序列Z=<z1,z2,…,zk>是X的子序列是指存在一个严格递增的下标序列<i1,i2,…,ik>,使得对于所有j=1,2,…,k有:
Xij=Zj
例如,序列Z=<B,C,D,B>是序列X=<A,B,C,B,D,A,B>的子序列,相应的递增下标序列为<2,3,5,7>。给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。例如,若X=<A,B,C,B,D,A,B>和Y=<B,D,C,A,B,A>,则序列<B,C,A>是X和Y的一个公共子序列,序列 <B,C,B,A>也是X和Y的一个公共子序列。而且,后者是X和Y的一个最长公共子序列.因为X和Y没有长度大于4的公共子序列。
给定两个序列X=<x1,x2,…,xm>和Y=<y1,y2….yn>.要求找出X和Y的一个最长公共子序列。
【输入格式】
输入文件共有两行。每行为一个由大写字母构成的长度不超过200的字符串,表示序列X和Y。
【输出格式】
输出文件第一行为一个非负整数。表示所求得的最长公共子序列的长度。若不存在公共子序列.则输出文件仅有一行输出一个整数0。否则在输出文件的第二行输出所求得的最长公共子序列(也用一个大写字母组成的字符串表示)。若符合条件的最长公共子序列不止一个,只需输出其中任意一个。
【样例输入】
ABCBDAB
BDCABA
【样例输出】
4
【算法分析】
最长公共子串(Longest Common Substirng)和最长公共子序列(Longest Common Subsequence,LCS)的区别为:子串是串的一个连续的部分,子序列则是从不改变序列的顺序,而从序列中去掉任意的元素而获得新的序列;也就是说,子串中字符的位置必须是连续的,子序列则可以不必连续。字符串长度小于等于1000。
与最长不下降子序列(LIS)类似的,我们可以以子序列的结尾作为状态,但现在有两个子序列,那么直接以两个子序列当前的结尾作为状态即可。
①确定状态:
设F[x][y]表示S[1…x]与T[1…y]的最长公共子序列的长度。答案为F[|S|][|T|]。
②确定状态转移方程和边界条件:
分三种情况来考虑:
·S[x]不在公共子序列中:该情况下F[x][y]=F[x-1][y];
·T[y]不在公共子序列中:该情况下F[x][y]=F[x][y-1];
·S[x]=T[y],S[x]与T[y]在公共子序列中:该情况下,F[x][y]=F[x-1][y-1]+1。
F[x][y]取上述三种情况的最大值。综上:
状态转移方程:F[x][y]=max{F[x-1][y],F[x][y-1],F[x-1][y-1]+1},其中第三种情况要满足S[x]=T[y];
边界条件:F[0][y]=0,F[x][0]=0。
③程序实现:
计算F[x][y]时用到 F[x-1][y-1],F[x-1][y],F[x][y-1]这些状态,它们要么在F[x][y]的上一行,要么在F[x][y]的左边。因此预处理出第0行,然后按照行从小到大、同一行按照列从小到大的顺序来计算就可以用迭代法计算出来。时间复杂度为O(|S|*|T|)。
【参考程序】
/**
最长公共子序列
字符串S,T
二维数组F[x][y]
代表1-x的字符串s与1-y的字符串T的公共子序列的长度
四种情况:
x不在公共子序列中 ,y在:f[x][y]=f[x-1][y]
x在,y不在:f[x][y]=f[x][y-1]
x,y都在:f[x][y]=f[x-1][y-1]+1
x,y都不在:f[x][y]=f[x-1][y-1]
F[x][y]=max{F[x-1][y],F[x][y-1],F[x-1][y-1]+1}
border:
初试设置为0,遇到第一个相同的子序列设置为1
*/
#include<iostream>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int N = 1000;
int f[N][N];
int i,j,n;
string s,t;
int main()
{
cin>>s;
cin>>t;
int sl = s.length();//字符串长度
int tl = t.length();
//f代表的字符串是从1-n,而字符串本身遍历是从0-n-1
for(i=1; i<=sl; i++)
{
for(j=1; j<=tl; j++)
{
f[i][j] = max(f[i-1][j],f[i][j-1]);
if(s[i-1]==t[j-1])
{//s的第i元素与t的第j元素相同的时候
f[i][j] = max(f[i][j],f[i-1][j-1]+1);
}
}
}
cout<<f[sl][tl];
return 0;
}
(未完待续…)