算法 - 最长上升子序列

本文详细介绍了如何使用动态规划和贪心+二分搜索算法解决最长上升子序列问题。动态规划解法通过维护以每个元素结尾的最长子序列长度,最终找到全局最长长度。贪心+二分搜索解法通过维护一个单调不降的low数组,利用二分查找提高效率,达到O(nlogn)的时间复杂度。

算法第三次上机A题

问题描述

一个数的序列 bi,当 b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列( a1, a2, …, aN),我们可以得到一些上升的子序列( ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).

对于给定的序列,求出最长上升子序列的长度。

解法一 : 动态规划
思路:

将问题简化为相同求解方式但规模更小的子问题

1)n个数的最长上升子序列 = 前n-1个数的最长上升子序列 + 比较第n个数
2)前1个数的最长上升子序列 = 1

具体解法:

1)从第一个数开始,寻找以A[i]结尾的最长上升子序列d[i]。
2)寻找d[i]时,比较A[j]=A[1]到A[i-1],当A[j]<A[i],取d[i] 为d[i]和d[j]+1的较大值。
3)最后比较所有d,选出最大的为整个数组的d[i]。
注意:
无法求出最长上升子序列的具体元素

例子:

A = [ 1 7 3 5 9 4 8 ]
d[1] = 1, D[1] = [1]
d[2] = 2, D[2] = [ 1 7 ]
d[3] = 2, D[3] = [ 1 3 ]
d[4] = 3, D[4] = [ 1 3 5 ]
d[5] = 4, D[5] = [ 1 3 5 9 ]
d[6] = 3, D[6] = [ 1 3 4 ]
d[7] = 4, D[7] = [ 1 3 5 8 ]

d = max { d[1] … d[7] } = 4

具体实现:

问题定义:D [ i ] 代表以 A [ i ] 结尾的 LIS 的长度

转子问题:D [ i ] = max { D [ j ] + 1 ,D [ i ] } (1 <= j < i,A[ j ] < A[ i ])

问题终点:D [ i ] = 1 (1 <= i <= n)

时间复杂度:O (n^2)

code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn = 103, INF = 0x7f7f7f7f;
int a[maxn], f[maxn];
int n,ans = -INF;
int main()
{
    scanf("%d", &n);
    for(int i=1; i<=n; i++) 
    {
        scanf("%d", &a[i]);
        f[i] = 1;
    }
    for(int i=1; i<=n; i++)
        for(int j=1; j<i; j++)
            if(a[j] < a[i])
                f[i] = max(f[i], f[j]+1);
    for(int i=1; i<=n; i++) 
        ans = max(ans, f[i]);
    printf("%d\n", ans);
    return 0;
}
解法二:贪心+二分
思路:

1)新建一个 low 数组,low [ i ]表示长度为i的LIS结尾元素的最小值。
对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。
2)对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。
3)维护 low 数组
对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;
否则,就用 a [ i ] 取更新 low 数组。

具体解法:

1)在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。
2)如果从头到尾扫一遍 low 数组的话,时间复杂度仍是O(n^2)。我们注意到 low 数组内部一定是单调不降的,所有我们可以二分 low 数组,找出第一个大于等于a[ i ]的元素。
注意:
二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。

例子:

A = [ 1 7 3 5 9 4 8 ]
A[ ] = 3 1 2 6 4 5 10 7,求LIS长度。

定义B[ i ]来储存可能的排序序列,len 为LIS长度,min为最小末尾
依次把A[ i ]有序地放进B[ i ]里。

A[1] = 1,B[1] = A[1] = 1 => B = [ 1 ], len = 1,min=1
A[2] = 7,7>1,B[2] = A[2] = 7 => B = [ 1 7 ],len = 2,min = 7
A[3] = 3,3<7,B[2] = A[3] = 3 => B = [ 1 3 ],len = 2,min = 3
A[4] = 5,5>3,B[3] = A[4] = 5 => B = [ 1 3 5 ],len = 3,min = 5
A[5] = 9,9>5,B[4] = A[5] = 9 => B = [ 1 3 5 9 ],len = 4,min = 9
A[6] = 4,4<5,B[3] = A[6] = 5 => B = [ 1 3 4 9 ],len = 4,min = 9
A[7] = 8,8<9,B[4] = A[7] = 8 => B = [ 1 3 4 8 ],len = 4,min = 8

最终LIS长度=4
but but but but but
这里的1 3 4 8很明显并不是正确的最长上升子序列。因此,B序列并不一定表示最长上升子序列,它只表示相应最长子序列长度的排好序的最小序列。这有什么用呢?我们最后一步用8替换9并没有增加最长子序列的长度,而这一步的意义,在于记录最小序列,代表了一种“最可能性”,只是此种算法为计算LIS而进行的一种替换。

具体实现:

问题定义:B [ i ] 代表包含 A [ i ] 的和LIS 的长度相同的一个递增序列

转子问题

  1. B [ len ] < A [ i ]
    B [ ++len ] = A [ i ]
  2. 二分法查找第一个 j,B [ j ] < A [ i ]
    B [ j ] = A [ i ]

问题终点:B [ i ] = 1 (1 <= i <= n)

时间复杂度:O (nlogn)

code
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
 
const int maxn =1000, INF = 0x7f7f7f7f;
int low[maxn], a[maxn];
int n, ans;
 
int binary_search(int *a, int n, int x)
//二分查找,返回a数组中第一个>=x的位置 
{
    int l = 1, mid;
    while(l <= n)
    {
        mid = (l+n) >> 1;
        if(a[mid] <= x)
            l = mid + 1;
        else 
            n = mid - 1;
    }
    return l;
}
 
int main()
{
    scanf("%d", &n);
    for(int i=1; i<=n; i++) 
    {
        scanf("%d", &a[i]); 
        low[i] = INF;   //由于low中存的是最小值,所以low初始化为INF 
    }
    low[1] = a[1]; 
    ans = 1;   //初始时LIS长度为1 
    for(int i=2; i<=n; i++)
    {
        if(a[i] > low[ans])    //若a[i]>=low[ans],直接把a[i]接到后面 
            low[++ans] = a[i];
        else       //否则,找到low中第一个>=a[i]的位置low[j],用a[i]更新low[j] 
            low[binary_search(low, ans, a[i])] = a[i];
    }
    printf("%d\n", ans);   //输出答案 
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值