最长子序列算法详解[ZJU1986]
1、最长上升子序列朴素动态规划算法介绍:
设序列为A[1]、A[2]、A[3]、…、A[n]。
设F[i]为以A[i]为结尾的最唱上升子序列,则状态转移方程为:
F[i]=Max{ F[j]+1 | 1<=j<=i-1,A[i]>A[j] }
算法实现:
|
|
|
Procedure DP; Var i,j:Longint; Begin For i:=1 to n do F[i]:=1; For i:=2 to n do For j:=1 to i-1 do If a[i]>a[j] Then If F[i]<F[j]+1 Then F[i]:=F[j]+1; MAxLen:=0; For i:=1 to n do If MaxLen<F[i] Then ans:=F[i]; End; |
显然朴素的动态规划算法的时间复杂度是。对于n>10000的数据规模将会超时。
2、的DP算法:
朴素算法在决策F[i]的值的时候,需要枚举F[1]、F[2]、…、F[i-1]的所有状态,从中选出最优值。
我们假设在A[1]、A[2]、…、A[I-1]中有A[x]和A[y],他们满足:
(1)x < y < i
(2)A[y] < A[x] < A[i]
(3)F[x] = F[y]
此时,选择F[x]和选择F[y]都可以得到同样的F[i]值,那么,在最长上升子序列的这个位置中,应该选择A[x]还是应该选择A[y]呢?很明显,选择A[y]比选择A[x]要好。因为由于条件(2),在A[y+1] ... A[i-1]这一段中,如果存在A[z],A[y] < A[z] < a[x],则与选择A[x]相比,将会得到更长的上升子序列。
再根据条件(3),我们会得到一个启示:
我们对F[1]、F[2]、…、F[I]的值进行分类,假设在F[1]、F[2]、…、F[I]中值为k的有A[x]、A[y]、A[z],用数组D[k]存储A[x]、A[y]、A[z]中最小的哪个,这样我们可以得到序列D[1]、D[2]、…D[k];
例如序列:
根据数组D的定义可知:D[1]=4 D[2]=11 D[3]=12 D[4]=16 D[5]=25
我们现在有求F[10],我们可以在D[1]、D[2]、D[3]、D[4]、D[5]中查找小于a[10]=23最大的k。显然D[1]、D[2]、D[3]、D[4]、D[5]中D[1]、D[2]、D[3]、D[4]都小于23,且最大的k是4,所以F[10]=k+1=4+1=5。
程序: |
|
|
Procedure DP; Var i,j,k,p:Longint; Begin For i:=1 to n do F[i]:=1; For i:=1 to n do D[i]:=Maxint; D[1]:=a[1]; k:=1; For i:=2 to n do Begin p:=0; For j:=1 to k do If D[j]<a[i] Then p:=j; F[i]:=p+1; If D[p+1]>a[i] Then D[p+1]:=a[i]; If p=k Then inc(k); End; MaxLen:=k; End; |
上面的算法平均时间复杂度为
我们继续分析,计算D数组有以下两个特性:
(1) D[k]的值是在整个计算过程中是单调不上升的。
(2) D[]的值是有序的,即D[1] < D[2] < D[3] < ... < D[n]。
由此我们可以运用二分查找D数组算法来决策F[I],因此我们可以时间复杂度就变成了的了。
程序:
|
|
|
Function srch(x,s,t:Longint):Longint; //查找最小于x的且 Var i,j,mid,p:Longint; //最接近x的d[k] Begin i:=s; j:=t; p:=0; while i<=j do begin mid:=(i+j) shr 1; If x<=D[mid] Then j:=mid-1; If x>D[mid] Then begin p:=mid;i:=mid+1; End; end; srch:=p; End;
Procedure DP; Var i,j,k,p:Longint; Begin For i:=1 to n do F[i]:=1; For i:=1 to n do D[i]:=Maxint; D[1]:=a[1]; k:=1; For i:=2 to n do Begin p:=srch(a[i],1,k); F[i]:=p+1; If D[p+1]>a[i] Then D[p+1]:=a[i]; If p=k Then inc(k); End; MaxLen:=k; End; |
3、找出最长上升子序列个数的算法:
求出最长上升子序列的的长度后,现在我们要求出最长上升子序列的个数:
比如:对于
1 2 3 4 6 5 8 10 9
显然上升子序列的最长长度为7,长度为7的子序列有:
1 2 3 4 6 8 10 、1 2 3 4 6 8 9、1 2 3 4 5 8 10、1 2 3 4 5 8 9
怎么计算呢?
我们设数组t[I]表示以a[I]为结尾的长度为F[I]的最长上升子序列的个数,显然当:
f[I]=1时,t[I]=1否则当a[j]<a[I]且f[j]+1=f[I]时:t[I]:=t[I]+t[j]。
1 2 3 4 6 5 8 10 9
f[I]= 1 2 3 4 5 5 6 7 7
t[I]= 1 1 1 1 1 1 2 2 2
计算程序段:
|
|
|
Fillchar(t,sizeof(t),0); t[1]:=1; for I:=2 to n do if f[I]=1 then t[I]:=1 else for j:=I-1 downto 1 do if (a[j]<a[I]) and (f[j]+1=f[I]) then t[I]:=t[I]+t[j]; |
但是这种计算方法对于有相同元素的序列:
1 2 2 3 3 5 4
f[I]=1 2 2 3 3 4 4
t[I]=1 1 1 2 2 4 4
有8个序列:
4个1 2 3 5
4个1 2 3 4
如果我们要求本质不同(元素不完全相同)的最长上升子序列的个数,怎么办呢?
需要对上面的算法进行改进:
如果有x和y,他们符合如下条件:
1、a[x]=a[y]
2、x<y;
3、F[x]=F[y];
对于某个I,符合条件:
1、i>y
2、a[I]>a[y]
3、F[I]=F[y]+1
那么我们计算t[I]的时候,如果按前面的算法,t[x]和t[y]都要被类加,事实上,t[y]已经包含了t[x],所以t[x]被类加了多次,这就是出现重复的原因。
找到出现重复的原因,我们针对这个原因来改进:
我们设数组h,h[j]=x表示在a[j]后面还有一个a[x]
j<=x且a[j]=a[x]且F[j]=F[x]
有了这个信息,我们在计算t[I](I>x>=j)的时候,我们只须累加符合下列条件的t[j]: a[j]>a[I]且F[j]+1=F[I]且h[j]=j且
因为h[j]<>j,那么说明a[j]后面还有一个x,使的a[j]=a[x]且F[j]=F[x]。所以不需要累加t[j]的值。比如:
1 2 2 3 3 5 4
f[I]= 1 2 2 3 3 4 4
计算t[I]的时候:
t[1]=1; h[1]=T;
t[2]=t[2]+t[1]=1; h[2]=T
t[3]=t[3]+t[1]=1; h[3]=T h[2]=F
t[4]=t[4]+t[3]=1; h[4]=T;
t[5]=t[5]+t[3]=1; h[5]=T; h[4]=F;
t[6]=t[6]+t[5]=1; h[6]=T;
t[7]=t[7]+t[5]=1; h[7]=T;
t[8]=t[6]+t[7]=2
所以长度为4的上升子序列的个数是:
t[6]+t[7]=2;
程序:
|
|
|
t[1]:=1; h[1]:=true; F[n+1]:=MaxLen+1; a[n+1]:=MaxLongint; for i:=2 to n+1 do begin if f[i]=1 then t[i]:=1 else for j:=1 to i-1 do if (a[i]>a[j]) and (f[i]=f[j]+1) then if h[j] then t[i]:=t[i]+t[j]; h[I]:=true for j:=I-1 downto 1 do //修改h数组 if (a[i]=a[j]) and (f[i]=f[j]) then h[j]:=false; end; //For I:=1 to n do if f[I]=MaxLen then tans:=tans+t[I];错误 Tans:=t[n+1];
|
作业:
特请你们注意:作业的目的不是为了完成任务,要加强自己分析能力和独立完成任务的能力,在做下面题目的时候尽量不要去看上面的程序!!!
1、低价购买(BUYLOW.PAS)
“低价购买”这条建议是在奶牛股票市场取得成功的一半规则。要想被认为是伟大的投资者,你必须遵循以下的问题建议:“低价购买;再低价购买”。每次你购买一支股票,你必须用低于你上次购买它的价格购买它。买的次数越多越好!你的目标是在遵循以上建议的前提下,求你最多能购买股票的次数。你将被给出一段时间内一支股票每天的出售价(216范围内的正整数),你可以选择在哪些天购买这支股票。每次购买都必须遵循“低价购买;再低价购买”的原则。写一个程序计算最大购买次数。
这里是某支股票的价格清单:
日期 1 2 3 4 5 6 7 8 9 10 11 12
价格 68 69 54 64 68 64 70 67 78 62 98 87
最优秀的投资者可以购买最多4次股票,可行方案中的一种是:
日期 2 5 6 10
价格 69 68 64 62
输入
第1行: N (1 <= N <= 5000),股票发行天数
第2行: N个数,是每天的股票价格。
输出
输出文件仅一行包含两个数:最大购买次数和拥有最大购买次数的方案数(<=231)当二种方案“看起来一样”时(就是说它们构成的价格队列一样的时候),这2种方案被认为是相同的。
BUYLOW.IN |
BUYLOW.OUT |
12 68 69 54 64 68 64 70 67 78 62 98 87
|
4 2 |
样例
2、合唱队形(chorus.pas/dpr/c/cpp)
【问题描述】
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 <= 500000),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(100<= Ti <= 1000000)是第i位同学的身高(厘米)。
【输出文件】
输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
【样例】
chorus.in |
chorus.out |
8 186 186 150 200 160 130 197 220 |
4 |
3、锯齿合唱队形(mchorus.pas)
【问题描述】
德国世界杯正在如火如荼地进行,但组织者们已经在为闭幕式做紧张的准备,他们准备在闭幕式上排练一个上千人的大型合唱节目,演唱本届世界杯的主题歌曲。节目设计者不想沿用传统的由底到高或者由高到低的合唱队型,而是别出心裁地设计出一种锯齿合唱队形,
所谓锯齿合唱队形是指这样的一种队形:设K位合唱队员从左到右依次编号为1, 2, …,K,他们的身高分别为T1, T2, …, TK,则他们的身高满足:
T1 < T2 > T3 < T4> T5… 或者 T1 >T2 < T3 > T4< T5…;用图示形象表示如下:
现在有N个合唱队员站成一排,每个队员的身高已经给出,请你计算最少需要其中几位队员出列,就可以使得剩下的N-K位队员排成锯齿合唱队形。
【输入文件】
输入文件mchorus.in的第一行是一个整数n(2 <= N <= 5000),表示队员的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130 <= Ti <= 230)是第i位队员的身高(厘米)。
【输出文件】
输出文件mchorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
【样例】
mchorus.in |
mchorus.out |
8 186 186 150 200 160 130 197 220 |
3 |
4、轮船(Ships)
【问题描述】
有一个国家被一条河划分为南北两部分,在南岸和北岸总共有N对城镇,每一城镇在对岸都有唯一的友好城镇。任何两个城镇都没有相同的友好城镇。每一对友好城镇都希望有一条航线来往。于是他们向政府提出了申请。由于河终年有雾。政府决定不允许有任两条航线交叉(如果两条航线交叉,将有很大机会撞船)。
你的任务是写一个程序来帮政府官员决定他们应拨款兴建哪些航线以使得没有出现交叉的航线最多。
输入文件(ship.in)
第一行一个整数N(1<=N<=500000),表示分布在河两岸的城镇对数。接下来的N行每行有两个由空格分隔的正数C,D(C、D<=109〉,描述每一对友好城镇沿着河岸与西边境线的距离,C表示北岸城镇的距离而D表示南岸城镇的距离。在河的同一边,任何两个城镇的位置都是不同的。
输出文件(ship.ou)
在安全条件下能够开通的最大航线数目。
示例
Ship.in |
Ship.out |
7 22 4 2 6 10 3 15 12 9 8 17 17 4 2 |
4
|
5、防卫导弹
〖题目描述〗
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入文件(catcher.in):
文件的第一行是一个整数N(0<=N<=4000),表示本次测试中,发射的进攻导弹数。以下N行每行各有一个整数hi(0<=hi<=32767),表示第i个进攻导弹的高度。文件中各行的行首、行末无多余空格,输入文件中给出的导弹是按发射顺序排列的。
输出文件(catcher.out):
文件第一行是一个整数max,表示一套拦截系统最多能截击的进攻导弹数。第2行一个整数,表示要拦截所有来袭导弹,最少需要配备多少系统。
样例
catcher.in |
catcher.out |
8 389 207 155 300 299 170 158 65 |
6 2 |