O(n^2)算法(DP)
分析:对于一个数列的最长上升子序列,我们用一个数组dp[i]来纪录以a[i]结尾的子序列中的最长上升序列,那么很容易可以想到,
对于第一个元素来说,他的dp[1]=1;
对于后面的每个元素i,他的dp[i]应该是前面dp[]中最大的一个然后再加上1,即dp[i]=max(dp[j]) (j from 1 to i);
综上分析就是,对于一个数列a[],我们用dp[i]来表示以a[i]结尾的序列的最长上升子序列,那么假设现在我们知道了i之前的所
有dp[j],其中0<=j<i,那么状态转移方程为:dp[i] = max(dp[i],dp[j] + 1) ,0<=j<i,a[j]<a[i]。int solve(int a[],int n)
{
int ans=-1;
for(int i=0;i<n;i++)
dp[i]=1;
for(int i=0;i<n;i++)
{
for(int j=0;j<i;j++)
{
if(a[i]>a[j])
dp[i]=max(dp[i],dp[j]+1); //如果求最长上升子序列的最大和,只需让dp[j]+a[i]
}
ans=max(ans,dp[i]);
}
return ans;
}
如果求一个数列可以划分的最长不上升子序列的个数,那么由Dilworth定理可知:
一个数列的最长不上升子序列的数目等于该数列最长上升子序列的长度。
Dilworth定理:
定理1) 令(X,≤)是一个有限偏序集,并令r是其最大链的大小。则X可以被划分成r个但不能再少的反链。
定理2) 令(X,≤)是一个有限偏序集,并令m是反链的最大的大小。则X可以被划分成m个但不能再少的链。
如HDU1257 最少拦截系统
#include <cstdio>
#include <algorithm>
#define MAX 1000
using namespace std;
int main()
{
int n,i,j,ans;
int a[MAX],dp[MAX];
while(scanf("%d",&n)!=-1)
{
ans=0;
for(i=0;i<n;i++)
scanf("%d",&a[i]);
for(i=0;i<n;i++) dp[i]=1;
for(i=0;i<n;i++)
{
for(j=0;j<=i;j++)
{
if(a[i]>a[j])
dp[i]=max(dp[i],dp[j]+1);
}
ans=max(ans,dp[i]);
}
printf("%d\n",ans);
}
return 0;
}
另一种是O(nlogn)的算法
严格来说这种算法应该属于二分,和DP没多大关系,大致是这样的:
我们定义一个数组stack[i],表示长度为 i 的这部分子序列中,ans[]的最小值,即stack[i]=min(a[])(a[]是所有长度为i的子序列的尾元素构成的数组);
有了stack[],我们就可以利用该数组来找出该序列最长的上升子序列了,首先定义len为该序列的最大上升子序列,len=1;
(1)对于数组的第一项,我们可以用ans[0]来初始化,stack[len]=ans[0];
(2)对于后面的每一项ans[i](i from 1 to n-1),我们用一个for循环来遍历:
(a)如果ans[i]>stack[len],那么就把该元素加到len序列上,并更新序列长度:stack[++len]=ans[i];
(b)如果ans[i]<=stack[len],那么找出1到len中最大的j,使ans[i]>stack[j],然后更新这一项:stack[j]=ans[i];
这样就得到了最终的len值,输出即可。
其实这种算法也是O(n^2)的,因为有ans和stack两成遍历,但是有这么一个事实,就是你得到的stack数组始终是单调非递减的,那么这时候遍历就可以用二分了,从而使整个算法降到O(nlogn)。
如 POJ2533 Longest Ordered Subsequence
题目大意:给出一个长度为n的序列,求其LIS的长度。
#include <cstdio>
#include <iostream>
#define MAX 100005
using namespace std;
int ans[MAX],stack[MAX];
int LIS(int temp,int len)
{
int left=0,right=len;
int mid=(left+right)/2;
while(left<=right)
{
if(temp>stack[mid]) left=mid+1;
else right=mid-1;
mid=(left+right)/2;
}
return left;
}
int main()
{
int n,temp,len;
while(scanf("%d",&n)!=-1)
{
for(int i=0;i<n;i++)
scanf("%d",&ans[i]);
len=1;
stack[len]=ans[0];
for(int i=1;i<n;i++)
{
if(ans[i]>stack[len])
stack[++len]=ans[i];
else
{
int j=LIS(ans[i],len);
stack[j]=ans[i];
}
}
printf("%d\n",len);
}
return 0;
}
用lower_bound()来实现的代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
#define maxn 100005
#define inf 0x3f3f3f3f
int dp[maxn];
int a[maxn];
int n;
void solve(){//求最长上升子序列(LIS)(nlogn)
fill(dp,dp+n,inf);//fill函数是用来把0~n的位置赋值为某个值
for(int i=0;i<n;i++)
{
*lower_bound(dp,dp+n,a[i])=a[i];
//如果子序列的长度相同,那么最末位的元素较小的在之后的会更加有优势,
//所以我们反过来用dp针对长度相同的情况下最小的末尾元素进行求解。
}
printf("%d\n",lower_bound(dp,dp+n,inf)-dp);
}
int main()
{
while(scanf("%d",&n)!=-1)
{
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
}
solve();
}
return 0;
}