最长上升子序列(LIS) 学习总结

    前言

        鉴于本蒟蒻的dp学的实在是一坨答辩,临近lqb开始重新学习一遍dp,在acwing和LIS搏斗两天之后,写一篇总结,加深一下印象。

模板

最长上升子序列

题意与大致思路

题意:              

        给一个数组,找数组的一个子序列,该序列严格递增且长度最大

状态表示:

         dp[i] 表示 以a[i]为结尾的最长上升子序列的长度

状态转移:

         对于每个i,遍历1~i,如果找到一个a[j] < a[i],那么ai就可以接到以aj为结尾的LIS后面,此时就可以更新 dp[i] = max(dp[j] + 1 , dp[i]) ,时间复杂度 O (n^2)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int a[maxn],dp[maxn];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i],dp[i] = 1; //dp[i]初始化为1,因为显然a[i]本身即为LIS
    int res=0;
    for(int i = 1; i <= n;i ++)
    {
        for(int j = 1;j < i;j ++)
            if(a[j] < a[i])
                dp[i] = max(dp[i] , dp[j] + 1);
        res = max(res , dp[i]);  //维护dp[i]的最大值
    }
    cout<<res<<endl;
    return 0;
}

 接下来上题

Acwing 1017 - 怪盗基德的滑翔翼

怪盗基德的滑翔翼

Think

        该题可以选择向左或者向右选择一条严格递减的序列,往右走就是最长下降子序列,而往左走实际上为最长上升子序列,直接dp并维护最大值即可

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e2+10;
int a[maxn],dp_up[maxn],dp_down[maxn];
int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        int n;
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>a[i];
            dp_up[i]=1;
            dp_down[i]=1;
        }
        for(int i=1;i<=n;i++)
            for(int j=0;j<i;j++)
                if(a[i]>a[j])
                    dp_down[i]=max(dp_down[i],dp_down[j]+1);
                else if(a[i] < a[j])
                     dp_up[i]=max(dp_up[i],dp_up[j]+1);
        int res=0;
        for(int i=1;i<=n;i++) {
            res=max(res,dp_down[i]);
            res=max(res,dp_up[i]);
        }
        cout<<res<<endl;
    }
    return 0;
}

 Acwing 482-合唱队列

合唱队列

Think

        不难想到把问题转化为找一个以i为终点的最长不下降子序列和一个以i为起点的最长不上升子序列,如何得到以i为起点的LIS呢,只需要反向跑一个最长不下降子序列就可以了

//
//  main.cpp
//  合唱团
//
//  Created by 77777 董昊鹏 on 2022/3/4.
//

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e2+10;
int a[maxn],dp_up[maxn],dp_down[maxn];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        dp_up[i]=1;
        dp_down[i]=1;
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<i;j++)
            if(a[i]>a[j]) dp_up[i]=max(dp_up[i],dp_up[j]+1);
    }
    for(int i=n;i>=1;i--)
        for(int j=n;j>i;j--)
            if(a[i]>a[j]) dp_down[i]=max(dp_down[i],dp_down[j]+1);
    int res=0;
    for(int i=1;i<=n;i++)
        res=max(res,dp_up[i]+dp_down[i]-1);
    cout<<n-res<<endl;
}

Acwing 1012-友好城市

友好城市

Think

       该题目要求两边都无交叉,显然对于两边的城市都要保证一个严格递增,按照其中一边排序后,对于另一边跑LIS就可以得到答案。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+10;
pair<int,int> a[maxn];
int dp[maxn];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i].first>>a[i].second,dp[i] = 1;
    sort(a+1,a+1+n);
    int res=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<i;j++)
            if(a[j].second < a[i].second)
                dp[i] = max(dp[j] + 1,dp[i]);
        res= max(res,dp[i]);
    }
    cout<<res<<endl;
}

 Acwing 1010-拦截导弹

导弹拦截

Think

        LIS的究极经典例题,对于第一问无需多谈,重点在于第二问,可以贪心去做,也可以用到一个偏序集相关的定理,dilworth定理:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目。人话就是:如果让你求最少的不上升子序列,这几个序列中包含整个数组的所有元素,即求最大不下降子序列的长度,严格证明可以看tofu佬的:

偏序集,哈斯图与Dilworth定理 - Tofu 的博客 - 洛谷博客 (luogu.org)

所以第二问再跑一个最长不下降子序列即可

#include<bits/stdc++.h>
using namespace std;
int main()
{
    string s,x;
    getline(cin,s);
    stringstream ss(s);
    int n=0,a[1010],dp[1010];
    while(ss>>x)
    {
        a[++n] = stoi(x);
        dp[n] = 1;
    }
    int res=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<i;j++)
            if(a[i] <= a[j])
                dp[i] = max(dp[j] + 1,dp[i]);
        res = max(dp[i],res);
    }
    cout<<res<<endl;
    for(int i=1;i<=n;i++) dp[i] = 1;
    res=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<i;j++)
            if(a[j] < a[i])
                dp[i] = max(dp[j] + 1,dp[i]);
        res = max(res,dp[i]);
    }
    cout<<res<<endl;
}

Acwing  896-最长上升子序列 Ⅱ

最长上升子序列Ⅱ

 Think

        这次n变为1e5,n^2的算法肯定没法通过,需要进行优化。考虑原本的dp算法,转移的过程是找每个ai接在哪里使得答案最优, 由于n较小可以直接枚举位置并dp取最大值。现在我们来直接讨论这个ai接的位置。

        状态表示与状态转移

        我们维护一个dp[i],表示长度为i的子序列尾部的值。然后遍历ai,考虑对于每个ai可以接在哪个序列后面,比如两个序列,他的结尾值分别是1和1000,显然接在1000绝对不会比接在1后面更差,因为1比较小,后面还有很多数能接。也就是说结尾的数越大越好,我们可以去找第一个小于等于ai的dp[i],而这个dp是一定单调的,证明如下:

        考虑反证:如果res[i] >= res[j] 且i < j,也就是说,更短的子序列有更大的末尾值,这个显而易见是不成立的,比如你在res[j]删除几个元素,使得其长度变为i,此时的末尾值res[k]一定是小于res[j]的,与前提矛盾。

        那么我们找第一个小于等于ai的dpi这个过程就可以用二分来解决,总的时间复杂度就可以优化到O(nlogn)。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
int a[maxn] , dp[maxn];
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    int res=0;
    dp[++res] = a[1];
    for(int i=2;i<=n;i++)
    {
        //cout<<i<<" "<<dp[res]<<endl;
        if(a[i] > dp[res]) dp[++res] = a[i];
        else 
        {
            int pos = lower_bound(dp + 1,dp + res + 1,a[i]) - dp;
            dp[pos] = a[i];
        }
    }
    cout<<res<<endl;
}

 Acwing 187-导弹拦截系统

导弹拦截系统

Think

        船新的导弹拦截,在这个题里面,既可以是最长上升也可以是最长下降,我们可以采取上题的思路,用一个up[i]和一个down[i]分别表示每个序列的末尾数。然后我们可以直接按照上一题的贪心思路,即:一直接在第一个小于等于a[i]的序列后面。进行一波搜索剪枝,更多具体细节看代码注释

#include<bits/stdc++.h>
using namespace std;
int a[60],up[60],down[60],res,n;
void dfs(int now,int cnt_u,int cnt_d) //cnt_u和cnt_d分别表示目前以后的上升子序列和下降子序列的数量。
{
    if(cnt_u + cnt_d >= res) return ; //如果答案不会更优,直接剪枝
    if(now == n + 1)
    {
        res = min(res , cnt_u+cnt_d); //更新答案。
        return ;
    }
    int i=1;
    for(;i<=cnt_u;i++)
        if(up[i] < a[now])
            break;
    //如果按这个贪心思路接下去,up数组一定是单调的,直接遍历到第一个满足条件的即可。
    int info = up[i];
    up[i] = a[now]; //把a接在后面
    dfs(now + 1 , max(cnt_u , i) , cnt_d); //这里如果没有找到满足条件的up得到的i会是cnt_u+1,也就是加了一个新的序列。
    up[i] = info;
    for(i=1;i<=cnt_d;i++)
        if(down[i] > a[now])
            break;
    info = down[i];
    down[i] = a[now];
    dfs(now + 1 , cnt_u , max(cnt_d , i));
    down[i] = info;
}
int main()
{
    while(cin>>n&&n)
    {
        for(int i=1;i<=n;i++) cin>>a[i];
        res = 100;
        dfs(1,0,0);
        cout << res << endl;
    }
}

 Acwing 272-最长公共上升子序列

最长公共上升子序列

Think

        直接类比公共子序列列出dp状态:dp[i][j]表示a[1 ~ i] 和b[1~j]中以b[j]结尾的最长公共上升子序列的长度,接下来考虑转移式。

状态转移:

        按照是否包含a[i]划分

        1.如果不包含a[i],dp[i][j] = dp[i-1][j]

        2.如果包含a[i],我们进行进一步划分,考虑序列的倒数第二个元素b[]是哪个值k

               如果只有b[1],则dp[i][j] = 1

               如果k = 1,则dp[i][j] = dp[i-1][1] + 1

               如果k = 2,则dp[i][j] = dp[i-1][2] + 1

               .....

              如果k = j - 1,则dp[i][j] = dp[i-1][j-1] + 1

        要模拟上述结果,需要三层for循环,过不了

优化:

        我们在枚举k时,可以发现我们实际上是在计算当a[i] > b[k] 时dp[i-1][k] 的前缀最大值,这样我们就可以把这层枚举取掉了。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e3 + 10;
int a[maxn] , b[maxn] , dp[maxn][maxn];
int main()
{
    int n;
    cin>>n;
    for(int i = 1; i <= n;i ++)
        cin >> a[i];
    for(int i = 1; i <= n;i ++)
        cin >> b[i];
    for(int i = 1;i <= n;i ++)
    {
        int MAX = 1;
        for(int j = 1;j <= n; j++)
        {
            dp[i][j] = dp[i-1][j];
            if(a[i] == b[j]) dp[i][j] = max(dp[i][j] , MAX);
            if(a[i] > b[j]) MAX = max(MAX , dp[i-1][j] + 1);
        }
    }
    int res=0;
    for(int i=1;i<=n;i++)
        res = max(res , dp[n][i]);
    cout << res << endl;
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值