什么是最长递增子序列呢? #include <iostream> using namespace std; int main() { int i,j,n,a[100],b[100],max; while(cin>>n) { for(i=0;i<n;i++) cin>>a[i]; b[0]=1;//初始化,以a[0]结尾的最长递增子序列长度为1 for(i=1;i<n;i++) { b[i]=1;//b[i]最小值为1 for(j=0;j<i;j++) if(a[i]>a[j]&&b[j]+1>b[i]) b[i]=b[j]+1; } for(max=i=0;i<n;i++)//求出整个数列的最长递增子序列的长度 if(b[i]>max) max=b[i]; cout<<max<<endl; } return 0; } 显然,这种方法的时间复杂度仍为o(n^2); #include <iostream> using namespace std; int find(int *a,int len,int n)//若返回值为x,则a[x]>=n>a[x-1] { int left=0,right=len,mid=(left+right)/2; while(left<=right) { if(n>a[mid]) left=mid+1; else if(n<a[mid]) right=mid-1; else return mid; mid=(left+right)/2; } return left; } void fill(int *a,int n) { for(int i=0;i<=n;i++) a[i]=1000; } int main() { int max,i,j,n,a[100],b[100],c[100]; while(cin>>n) { fill(c,n+1); for(i=0;i<n;i++) cin>>a[i]; c[0]=-1;// …………………………………………1 c[1]=a[0];// ……………………………………2 b[0]=1;// …………………………………………3 for(i=1;i<n;i++)// ………………………………4 { j=find(c,n+1,a[i]);// ……………………5 c[j]=a[i];// ………………………………6 b[i]=j;//……………………………………7 } for(max=i=0;i<n;i++)//………………………………8 if(b[i]>max) max=b[i]; cout<<max<<endl; } return 0; } 对于这段程序,我们可以用算法导论上的loop invariants来帮助理解. 列有多个,刚保存末尾元素最小的那个.(这一性质决定是第3条性质成立的前提) 的递增了序列只有一个,c[1]也是最小的; c进入循环前单调递增及find函数的性质可知(见find的注释), 此时c[j+1]>c[j]>=a[i]>c[j-1],所以把c[j]的值更新为a[i]后,c[j+1]>c[j]>c[j-1]的性质仍然成 立,即c仍然是单调递增的; 小不会变大,因为进入循环前c[j]的值是最小的,则循环中把c[j]更新为更小的a[i],当 然此时c[j]的值仍是最小的; 为j有:c[j-1]<a[i]<=c[j],这说明c[j-1]是小于a[i]的,且以c[j-1]结尾的递增子序列有最大的 长度,即为j-1,把a[i]接在c[j-1]后可得到以a[i]结尾的最长递增子序列,长度为(j-1)+1=j; 增子序列的长度均已求出,再通过第8行的循环,即求出了整个数组的最长递增子序列。 仔细分析上面的代码可以发现,每次循环结束后,假设已经求出c[1],c[2],c[3],...,c[len]的值,则此时最长递增子序列的长度为len,因此可以把上面的代码更加简化,即可以不需要数组b来辅助存储,第8行的循环也可以省略。 #include <iostream> using namespace std; int find(int *a,int len,int n)//修改后的二分查找,若返回值为x,则a[x]>=n { int left=0,right=len,mid=(left+right)/2; while(left<=right) { if(n>a[mid]) left=mid+1; else if(n<a[mid]) right=mid-1; else return mid; mid=(left+right)/2; } return left; } int main() { int n,a[100],b[100],c[100],i,j,len;//新开一变量len,用来储存每次循环结束后c中已经求出值的元素的最大下标 while(cin>>n) { for(i=0;i<n;i++) cin>>a[i]; b[0]=1; c[0]=-1; c[1]=a[0]; len=1;//此时只有c[1]求出来,最长递增子序列的长度为1. for(i=1;i<n;i++) { j=find(c,len,a[i]); c[j]=a[i]; if(j>len)//要更新len,另外补充一点:由二分查找可知j只可能比len大1 len=j;//更新len } cout<<len<<endl; } return 0; } |
求最长递增部分序列是一个比较常见的动态规划题。在导弹拦截等题中都有用到。一般来说就是用经典的O(n^2)的动态规划算法。
算法如下:
设A[i]表示序列中的第i个数,F[i]表示从1到i这一段中以i结尾的最长上升子序列的长度,初始时设F[i] = 0(i = 1, 2, ..., len(A))。则有动态规划方程:F[i] = max{1, F[j] + 1} (j = 1, 2, ..., i - 1, 且A[j] < A[i])。
然而在hoj10027中n的值达到了50000。显而易见经典算法是会超时滴。所以只有另谋出路了。
用一个变量len记录到目前为止所找出来的最长递增序列的长度。另外准备一个数组b[],用这个数组表示长度为j的递增序列中最后一个元素的值。在这里长度为j的递增序列不止一个,我们所要保存是那个最小的。为什么呢?因为最后一个元素越小,那么这个递增序列在往后被延长的机会越大。初始化b[0] = -1;len = 0;从第一个元素a[1]开始 a[i]( 1 <= i <= n)。如果这个元素比len长的序列的最大值大。则把这个元素直接添加到b数组的后面。如果这个元素比b数组的第一个元素还要小则把这个元素赋给b数组的第一个值。否则进行二分查找。当在b数组里面找到一个数比a[i]小,并且他的后面的数大于或等于a[i]则跳出。将a[i]添加到这个数的后面。输出len就可以了。
代码如下:
#include <stdio.h>#include <string.h>
int main()
{
int a[50001], b[50001];
int i, j, l, r, len, n, mid;
while (scanf("%d", &n) != EOF)
{
for (i = 0; i < n; i++)
scanf("%d", &a[i]);
len = 0;
memset(b, 0, sizeof(int) * 50001);
b[0] = -1;
for (i = 0; i < n; i++)
{
if (a[i] > b[len])
b[++len] = a[i];
else if (a[i] < b[1] )
b[1] = a[i];
else
{
l = 1; r = len;
while (l <= r)
{
mid = (l + r)>>1;
if (a[i] > b[mid] && a[i] <= b[mid + 1])
{
j = mid;
break;
}
if (b[mid] > a[i])
{
r = mid - 1;
}
else
{
j = mid;
l = mid + 1;
}
}
b[j + 1] = a[i];
}
}
printf("%d\n", len);
}
return 0;