最长上升子序列朴素做法(dp)及贪心+二分优化 附详细注释代码

最长上升子序列例题

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000001≤N≤100000,
−10^9≤数列中的数≤10^9

也就是给一个序列,求上升子序列的最长长度。

一、朴素dp做法

用f数组存以i为结尾的最长子序列的长度。

用li数组存第i个数的值

每一个f[i]初始值都为1(因为最小为1)

状态转移:

对于每一个 i ,遍历 i 之前的所有数 j:

li[j]<li[i]  f[i] =max(f[i], f[j]+1)

代码:

#include<bits/stdc++.h>

using namespace std;

typedef pair <int,int> pii;
const int N = 1010;

int li[N],f[N];

int main()
{
    int ans = 0;
    int n;
    cin >> n;
    for(int i = 1;i<=n;i++)
    {
        cin >> li[i];
    }
    for(int i = 1;i<=n;i++)
    {
        f[i] = 1;
        for(int j = 1;j<=i;j++)
        {
            if(li[i]>li[j])f[i] = max(f[i],f[j]+1);
            ans = max(f[i],ans);
        }
    }
    cout << ans;
    
}

二、二分优化做法

存储

用q数组存长度为i的子序列末尾可能的最小值

用li数组存第i个数的数值

用len存当前q的长度(长度就是目前的最长子序列的长度)

具体实现

对于每个下标i 

如果 li[i] > q[len]也就是比目前最大的数还要大

那么这个数就可以用来更新最大子序列的长度,也就是len++,q[len] = li[i]

如果这个li[i]小于q[len]

那么这个数可以用来更新从前往后第一个大于他的数

结论和证明

1.q是严格递增的

反证:如果q不是严格递增,则存在q[x] >= q[y]且x<y

q[x] 所代表子序列为:{x1,x2,...,q[x]}  长度为x

q[y] 所代表子序列为:{y1,y2,...,yn,q[y]}yn为q[y]前一个数   长度为y>x 

因为y>x 所以y-1 >= x

则有yn为末的子序列长度(长度为y-1) >= q[x]为末的子序列长度(长度为x)且 yn<q[y]<=q[x]

所以q[x]一定能替换为yn,也就是长为x的子序列末尾最小值不是q[x] 可能是yn或小于yn的值。

出现矛盾,所以q严格递增。

2.只有数字x是目前q中最大值时才有可能进行子序列长度的增长

因为q严格递增,所以如果x比q[len](q最后一个值)小 那么一定不能填在q[len]后,也就是不能进行子序列长度增长。

3.一个新的数字如果小于q[len] ,那么他可以替掉q中从前往后的第一个大于等于这个数字的数

设q中从前往后第一个大于等于x的数为y = q[a] (代表长度为a的子序列末尾最小值为y)

y >= x  且 q中q[a]之前的数字都 <x 也就是说 长度小于a的所有子序列的末尾最小值都小于x

取长度 = a-1的子序列(末尾为q[a-1]<x),将x添入这个序列的末尾,构成一个长为a的新序列,这个序列的末尾值显然是x,x<=y,而y是目前的长为a的序列末尾最小值,所以x显然是更优解(或相同解) 所以x可以代替 y。

由于1证明了q严格递增,我们可以用二分进行从前往后第一个大于等于这个数字的位置的查找

ps:为什么要代替

因为遍历时如果某个长度的序列的末尾最小值更小,下次代替就可以替到更靠后的位置(找到第一个大于等于的数字位置),如果替到末尾,那么下次更新长度就可以用更小的数字了,本质是一种贪心的思想。

代码:
#include<bits/stdc++.h>

using namespace std;

const int N = 100010;
int li[N],q[N];

int main()
{
    int n;
    cin >> n;
    for(int i = 1;i<=n;i++)
    {
        cin >> li[i];
    }
    
    int len = 0;
    q[0] = -1e9-10;//初始化  保证第一次读入时li[i]>q[len]
    
    for(int i = 1;i<=n;i++)
    {
        int x = li[i];
        int l = 1,r = len;
        if(x>q[len]){q[++len] = x;continue;}//如果q末尾元素小于x,增长序列长度
        
        while(l<r)//如果q末尾元素大于x,二分找第一个大于等于x的值  
        {
            int mid = l+r >> 1;
            if(q[mid]<x)l = mid+1;
            else r = mid;
        }
        
        q[r] = x;//二分后 r存第一个大于等于x的位置,用x替换
    }
    cout << len;//最后长度就是最长子序列长度
    return 0;
}  
一个易混点

q并不是存最长子序列

比如 对于 {1,3,5,2,7}

最长子序列显然是{1,3,5,7}

但是q是这样存的:

len = 0,1 > q[len]  q = {1}

len = 1,3 > q[len] q = {1,3}

len = 2,5 > q[len] q = {1,3,5}

len = 3,2 < q[len] 替换3 q = {1,2,5}

len = 3,7 > q[len] q = {1,2,5,7} 

len = 4

结束后q={1,2,5,7}与答案不同,但是长度一致,因为q[i]存的是

长度为i的子序列的末尾最小值  而非目标子序列。

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值