最长上升子序列问题即是求一个给定数组中,严格递增的最长子序列的长度,如序列1,2,2,4,3。其最长上升子序列为1,2,3or1,2,4.最长子序列的长度即是3。最大上升子序列和问题即是求一个给定数组中,严格递增的子序列中和最大的序列的和(显然,最长子序列未必和是最大的)。如序列100,1,2,3,其最长上升子序列为1,2,3,而最大上升子序列和为100。
由于DP在求解问题最优解时有巨大优势,所以看到“最长”、“最大”我们就最先想到如何用DP求解这两个问题。之所以把这两个问题放在一起讲,因为不仅其题目本身容易被混淆,而且解法的思路上是相通的。
求解思路:对最长上升子序列问题,我们创建一个与给定数组A同样大小的数组B,用B[i]保存数组A中以A[i]结尾的上升子序列的长度,很显然B[i+1]=max(1,B[j]+1)(j={0<=j<=i&&A[j]<A[i+1]&&max(B[j])})(即j等于A[i+1]前面比其小的元素中对应子序列长度最长的那个元素的下标)
对最大子序列和问题,我们类比上面同样创建一个与给定数组A同样大小的数组B,用B[i]保存数组A中以A[i]结尾的上升子序列的和,很显然B[i+1]=max(A[i+1],B[j]+A[i+1])(j={0<=j<=i&&A[j]<A[i+1]&&max(B[j])})。对我们ITers来说,代码总会比文字描述更来的直接。
代码:求最大上升子序列和
#include<stdio.h>
#define MAX 1000
typedef
struct
node
{
int
num;
int
sum;
}Node;
int
main()
{
int
n,i,j,msum,max;
Node set[MAX];
while
(~
scanf
(
"%d"
,&n))
{
for
(i=0;i<n;i++)
{
scanf
(
"%d"
,&set[i].num);
set[i].sum=set[i].num;
}
msum=set[0].sum;
for
(i=1;i<n;i++)
{
max=0; //这个初始化勿漏掉!
for
(j=i-1;j>=0;j--)
{
if
(set[j].num<set[i].num)
{
if
(set[j].sum>max)
max=set[j].sum;
}
}
set[i].sum+=max;
if
(set[i].sum>msum)
msum=set[i].sum;
}
printf
(
"%d\n"
,msum);
}
return
0;
}
上面使用DP求解问题的时间复杂度为O(n^2),下面我们介绍一种求最长上升子序列的更高效的方法,时间复杂度为O(nlogn)。这里我们先给出代码,然后再做分析。代码:求最长上升子序列,O(nlogn)
#include<stdio.h>
#define MAX 100000
int
num[MAX],temp[MAX];
int
find(
int
element,
int
k)
{
int
left=0,right=k,mid;
while
(left<right)
{
mid=(left+right)/2;
if
(temp[mid]>element)
right=mid;
//注意不能是mid-1,否则可能导致返回的结果不正确,不同与传统意义上的二分查找
else
if
(temp[mid]==element)
return
mid;
else
left=mid+1;
}
return
left;
}
int
main()
{
int
n,i,j,k;
while
(~
scanf
(
"%d"
,&n))
{
for
(i=0;i<n;i++)
scanf
(
"%d"
,&num[i]);
temp[k=0]=num[0];
for
(i=1;i<n;i++)
{
if
(num[i]>temp[k])
temp[++k]=num[i];
else
if
(num[i]<=temp[0])
temp[0]=num[i];
else
//temp[0]<num[i]<temp[k]
{
temp[find(num[i],k)]=num[i];
}
}
printf
(
"%d\n"
,k+1);
}
return
0;
}
当访问到num[0]=1时,temp[]={1},len(temp)=1;
当访问到num[1]=2时,由于num[1]>temp[k](k是temp中最后元素的下标,其从0开始),temp[++k]=num[1],从而temp[]={1,2},len(temp)=2;
...
当访问到num[3]=8时,同上,temp[]={1,2,3,8},len(temp)=4;
当访问到num[4]=4时,由于num[4]<temp[k],因此len(temp)不可能变长,我们这里用num[4]替换temp中大于等于num[4]的最小元素,这样使得后面num[4]<num[i]<temp[k](i>4)可以加进temp中,从而保证len(temp)为当前状态下的最长序列的长度。此时有temp[]={1,2,3,4},len(temp)=4;
...
当访问到num[6]=9时,temp[]={1,2,3,4,5,9},len(temp)=6;
当访问到num[7]=1时,temp[]={1,2,3,4,5,9},len(temp)=6;
由此,得到最长上升子序列的长度为6.
分析到这里大家可能又会思考如果要求输出最长上升子序列的各个元素该怎么处理?这里又要用到DP的思路了,设已经用上面的DP思路求得最长上升子序列的长度为maxlen,然后对给定数组B倒着进行遍历,找到B[j]=maxlen时,保存A[j]到新创建的数组C[maxlen-1]中,然后依次保存比A[j]小且B[i]=maxlen-k的元素A[i]到C[maxlen-1-k]中直到找到B[i]=1&&A[i]<A[j](其中k为已经保存的最长子序列元素个数,j={i|B[i]=maxlen-k&&A[i]<A[j]}),最后打印输出数组C。这组操作要进行多次循环,因为可能存在多个j,使得B[j]=maxlen.