最长上升子序列(LIS)的两种算法

要点:
1.假设一个序列为{3, 4, 2,5},它的最长上升子序列是3,4,5,一开始我以为一定要连续的才行,搞了半天,以为网上的代码都是错的,晕死。实际只要比前面大即可。
2.求这个问题有两种算法,一种时间复杂度是O(n^2),另一种用二分为O(nlogn)

算法一:
举例比如:5,3,4,8,6,7
前1个数的LIS长度d(1)=1(序列:5)
前2个数的LIS长度d(2)=1(序列:3;3前面没有比3小的)
前3个数的LIS长度d(3)=2(序列:3,4;4前面有个比它小的3,所以d(3)=d(2)+1)
前4个数的LIS长度d(4)=3(序列:3,4,8;8前面比它小的有3个数,所以 d(4)=max{d(1),d(2),d(3)}+1=3)
也就是说要满足这样两个条件:最新的a[i]要大于前面dp数组中所有的值,然后+1,寻找i前面所有满足的dp的最大值。
比如最后一个7,它要找比前面它小的比如5,3,4,6,再在这些中寻找最大值,比如5那就只有2个,3就只有2个,4的话对应的dp已经是2了,所以是3个,6的话对应的dp是3所以总的是4。而前面的6啊4啊对应的dp前面已经算出来了,这些就是子问题。最后比较求总的最大值即可。
DP状态转移方程为dp[i]=max(1,dp[j]+1),j小于i且a[i]>a[j]

#include<stdio.h>
int main()
{
    int max = 0, i, j;
    int n;
    int a[1000], lis[1000];
    scanf("%d", &n);
    for (i = 0; i < n; i++)
        scanf("%d", &a[i]);
    for (i = 0; i < n; i++)
    {
        lis[i] = 1;
        for (j = 0; j < i; j++)  //与前面所有的数比较
        {
            if (a[i] > a[j] && lis[j] + 1>lis[i])
                lis[i] = lis[j] + 1; //DP原理,即前面的+1后取最大值
        }
        if (max < lis[i])
            max = lis[i];//总的比较是必须的,后面可能小于前面,dp就不是最大
    }
    printf("%d\n", max);
    return 0;
}

算法二:
现在,我们仔细考虑计算dp[t](即上面的lis[])时的情况。假设有两个元素a[x]和a[y],满足

(1)x < y < t

(2)a[x] < a[y] < a[t]

(3)dp[x] = dp[y]

此时,选择dp[x]和选择dp[y]都可以得到同样的dp[t]值,那么,在最长上升子序列的这个位置中,应该选择a[x]还是应该选择a[y]呢?

很明显,选择a[x]比选择a[y]要好。因为由于条件(2),在a[x+1] … a[t-1]这一段中,如果存在a[z],a[x] < a[z] < a[y],则与选择a[y]相比,将会得到更长的上升子序列。

比如d[9]={2,1,5,3,6,4,8,9,7},用D[]来储存
第一步,d[1]=2先赋值给D[1],2的dp=1
第二步,d[2]=1进行比较,因为1<2,所以1的dp还是1,那么为什么不把1作为D[1]呢?这样序列能更长。
后面同理(有点贪心的感觉)

再根据条件(3),我们会得到一个启示:根据dp[]的值进行分类。对于dp[]的每一个取值k,我们只需要保留满足dp[t] = k的所有a[t]中的最小值。设D[k]记录这个值,即b[k] = min{a[t]} (dp[t] = k)。

利用D[],我们可以得到另外一种计算最长上升子序列长度的方法。设当前已经求出的最长上升子序列长度为len。先判断a[t]与D[len]。若a [t] > D[len],则将a[t]接在D[len]后将得到一个更长的上升子序列,len = len + 1, D[len] = a [t];否则,在D[1]..D[len]中,找到最大的j,满足D[j] < a[t]。令k = j + 1,则有a [t] <= D[k],将a[t]接在D[j]后将得到一个更长的上升子序列,更新D[k] = a[t]。最后,len即为所要求的最长上升子序列的长度。

在上述算法中,若使用朴素的顺序查找在D[1]..D[len]查找,由于共有O(n)个元素需要计算,每次计算时的复杂度是O(n),则整个算法的时间复杂度为O(n^2),与原来的算法相比没有任何进步。但是由于D[]的特点(2),我们在D[]中查找时,可以使用二分查找高效地完成,则整个算法的时间复杂度下降为O(nlogn),有了非常显著的提高。需要注意的是,D[]在算法结束后记录的并不是一个符合题意的最长上升子序列!这种算法只能算长度,不能得出哪个子序列最长。

假设存在一个序列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。

代码如下:

#include<stdio.h>
int a[10000],ans[10000],len;

int lower_bound(int i) //直接用二分法求下界,思路一模一样
{
    int left, right, mid;
    left = 0, right = len;
    while (left < right)
    {
        mid = left + (right - left) / 2;
        if (ans[mid] >= a[i]) //寻找a[i]在数列中的位置
            right = mid;
        else
            left = mid + 1;
    }
    return left;
}

int main()
{
    int n,i;
    scanf("%d", &n);
    for (i = 1; i <= n; i++) //从1开始与数列位置对应,便于理解且不容易弄错
        scanf("%d", &a[i]);
    ans[1] = a[1];
    len = 1;  //第一个不用比较,直接放入ans中
    for (i = 2; i <= n; i++)
    {
        if (a[i] > ans[len])  //如果大于末尾,直接加在后面
            ans[++len] = a[i];
        else 
        {    //反之在中间寻找下界
            int pos = lower_bound(i);
            ans[pos] = a[i];
        }
    }
    printf("%d\n", len);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值