题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=17
这题其实是跟导弹拦截一样的,因为还有个加强版,所以把这个跟加强版一起贴上来。经典动态规划题,以后的动态规划很多都是从这个衍生出来的,所以就找了段自己认为比较详细的解释来了,保存下来,备用,语言组织能力太差。。。。。。
一, 最长递增子序列问题的描述
设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。
二,算法1:动态规划法:O(n^2)
设f(i)表示L中以ai为末元素的最长递增子序列的长度。则有如下的递推方程:
这个递推方程的意思是,在求以ai为末元素的最长递增子序列时,找到所有序号在L前面且小于ai的元素aj,即j<i且aj<ai。如果这样的元素存在,那么对所有aj,都有一个以aj为末元素的最长递增子序列的长度f(j),把其中最大的f(j)选出来,那么f(i)就等于最大的f(j)加上1,即以ai为末元素的最长递增子序列,等于以使f(j)最大的那个aj为末元素的递增子序列最末再加上ai;如果这样的元素不存在,那么ai自身构成一个长度为1的以ai为末元素的递增子序列。一般在解决问题的时候都是用到动态规划,所以就贴出代码了。主要用这个。。。。。。
代码:
#include<stdio.h>//**O(n^2) #include<string.h> int main() { char str[10001]; int s,len,i,j,dp[10001],max; scanf("%d",&s); while(s--) { max=0; scanf("%s",str); len=strlen(str); for(i=0;i<=len-1;i++) { dp[i]=1;//**dp[i]的最小值为1**// } for(i=len-2;i>=0;i--) { for(j=i+1;j<=len-1;j++) { if(str[i]<str[j]&&dp[i]<dp[j]+1) { dp[i]=dp[j]+1;//**更新dp[i]的值**// } } } for(i=0;i<=len-1;i++) { if(dp[i]>max) { max=dp[i]; } } printf("%d\n",max); } }
三, 算法2:转化为LCS问题求解
设序列X=<b1,b2,…,bn>是对序列L=<a1,a2,…,an>按递增排好序的序列。那么显然X与L的最长公共子序列即为L的最长递增子序列。这样就把求最长递增子序列的问题转化为求最长公共子序列问题LCS了。
最长公共子序列问题用动态规划的算法可解。设Li=< a1,a2,…,ai>,Xj=< b1,b2,…,bj>,它们分别为L和X的子序列。令C[i,j]为Li与Xj的最长公共子序列的长度。则有如下的递推方程:
这可以用时间复杂度为O(n2)的算法求解,由于这个算法上课时讲过,所以具体代码在此略去。求最长递增子序列的算法时间复杂度由排序所用的O(nlogn)的时间加上求LCS的O(n2)的时间,算法的最坏时间复杂度为O(nlogn)+O(n2)=O(n2)。
(额,这种方法没用过,就当作发散下思维吧)LCS算法比较的是任意两个序列的最长公共子序列,在最长递
增子序列中,我们将原序列A首先升序排列得到B,然后将A和B求LCS就可以达到目的。
上面两种方法的复杂度都为O(n^2),第二种方法并没有改进。。。。。。
四, 对算法1的改进(O(nlogn))
题目链接:http://acm.nyist.net/JudgeOnline/problem.php?pid=214
在第一种算法中,在计算每一个f(i)时,都要找出最大的f(j)(j<i)来,由于f(j)没有顺序,只能顺序查找满足aj<ai最大的f(j),如果能将让f(j)有序,就可以使用二分查找,这样算法的时间复杂度就可能降到O(nlogn)。于是想到用一个数组B来存储“子序列的”最大递增子序列的最末元素,即有B[f(j)] = aj;
在计算f(i)时,在数组B中用二分查找法找到满足j<i且B[f(j)]=aj<ai的最大的j,并将B[f[j]+1]置为ai。下面先写出代码,再证明算法的证明性。
#include<stdio.h>//**O(nlogn)**// #include<string.h> #define min -32769//**int型最小数为-32768**// int stack[100001];//**模拟栈,其实不是栈,为了更好形象比较,嘿嘿**// int main() { int n,i,t,top,low,high,mid; memset(stack,0,sizeof(stack)); while(~scanf("%d",&n)) { top=0;stack[0]=min; for(i=0;i<=n-1;i++) { scanf("%d",&t); if(t>stack[top])//**如果输入进来的数比栈顶的数大,直接插入到栈的**// { top++; stack[top]=t; } else { low=1;high=top; while(low<=high)//**二分查找,寻找插入位置**// { mid=(low+high)/2; if(t>stack[mid]) { low=mid+1; } else { high=mid-1; } } stack[low]=t;//**找到插入位置,并替换点原值**// } } printf("%d\n",top); } return 0; }
现在来证明这个算法为什么是正确的。要使算法正确只须证如下命题:
命题1:每一次循环结束数组B中元素总是按递增顺序排列的。
证明:用数学归纳法,对循环次数i进行归纳。
当i=0时,即程序还没进入循环时,命题显然成立。
设i<k时命题成立,当i=k时,假设存在j1<j2,B[j1]>B[j2],因为第i次循环之前数组B是递增的,因此第i次循环时B[j1]或B[j2]必有一个更新,假设B[j1]被更新为元素ai+1,由于ai+1=B[j1]> B[j2],按算法ai+1应更新B[j2]才对,因此产生矛盾;假设B[j2]被更新,设更新前的元素为s,更新后的元素为ai+1,则由算法可知第i次循环前有B[j2]=s< ai+1< B[j1],这与归纳假设矛盾。命题得证。
命题2:B[c]中存储的元素是当前所有最长递增子序列长度为c的序列中,最小的最末元素,即设当前循环次数为i,有B[c]={aj| f(k)=f(j)=c∧k,j≤i+1→aj≤ak}(f(i)为与第二种算法中的f(i)含义相同)。
证明:程序中每次用元素ai更新B[c]时(c=f(i)),设B[c]原来的值为s,则必有ai<s,不然ai就能接在s的后面形成长度为c+1的最长递增子序列,而更新B[c+1]而不是B[c]了。所有B[c]中存放的总是当前长度为c的最长递增子序列中,最小的最末元素。
命题3:设第i次循环后得到的p为p(i+1),那么p(i)为以元素ai为最末元素的最长递增子序列的长度。
证明:只须证p(i)等于第二种算法中的f(i)。显然一定有p(i)<=f(i)。假设p(i)<f(i),那么有两种情况,第一种情况是由二分查找法找到的p(i)不是数组B中能让ai接在后面成为新的最长递增子序列的最大的元素,由命题1和二分查找的方法可知,这是不可能的;第二种情况是能让ai接在后面形成长于p(i)的最长递增子序列的元素不在数组B中,由命题2可知,这是不可能的,因为B[c]中存放的是最末元素最小的长度为c的最长递增子序列的最末元素,若ai能接在长度为L(L> p(i))的最长递增子序列后面,就应该能接在B[L]后面,那么就应该有p(i)=L,与L> p(i)矛盾。因此一定有p(i)=f(i),命题得证。
算法的循环次数为n,每次循环二分查找用时logn,所以算法的时间复杂度为O(nlogn)。这个算法在第二种算法的基础上得到了较好的改进。
如果证明看晕了,暂时先用一组数据进行形象比较,就可以理解代码含义了。。。。(其实我也看晕了,嘿嘿)
假设存在一个序列d[1...9]=2 1 5 3 6 4 8 9 7,可以看出它的LIS长度为5.
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1;
然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1;
接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2
再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2
继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。
第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3
第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了
第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。
于是我们知道了LIS的长度为5。
!!!!!注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。
然后应该发现一件事情了:
在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!
再加上一题来练手,题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1025
仍然是(O(nlogn))的二分插入。思想都是一样,就不再分析了。。。。
代码:
#include<stdio.h> int s[500001],dp[500001]; int main() { int n,i,a,b,len,up,low,mid,count=1; while(~scanf("%d",&n)) { for(i=1;i<=n;i++) { scanf("%d %d",&a,&b); s[a]=b;//**关键思想**// } dp[1]=s[1];len=1; for(i=1;i<=n;i++) { low=1;up=len; while(low<=up) { mid=(low+up)/2; if(dp[mid]>=s[i]) { up=mid-1; } else { low=mid+1; } } dp[low]=s[i]; if(low>len) { len++; } } printf("Case %d:\n",count++); if(len>1) { printf("My king, at most %d roads can be built.\n\n",len); } else { printf("My king, at most %d road can be built.\n\n",len); } } return 0; }