最长子序列问题

提到最长子序列问题,想必大家都不陌生,今天我主要想分享一下我对子序列问题的一些理解:

先拿最长上升子序列问题来说吧:
很明显这是一个动态规化问题,仔细想想也不难得出其状态转移方程

就是每更新到i时,就用dp[1~i-1]更新dp[i]的值,当j<i且a[j]<a[i]时,dp[i]=max(dp[i],dp[j]+1),这样我们就用n^2的复杂度处理完了这个问题

下面给出一道例题及其代码:

 

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

输入格式

第一行包含整数 N。

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

输出格式

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

数据范围

1N1000
10^910^9

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4



#include<bits/stdc++.h>
using namespace std;
int a[1002],dp[1002];
int main()
{
    int n;
    cin>>n;
    int ans=0;
    for(int i=1;i<=n;i++)
        {
            cin>>a[i];
            dp[i]=1;
            for(int j=1;j<i;j++)
                if(a[i]>a[j])
                    dp[i]=max(dp[i],dp[j]+1);
            ans=max(ans,dp[i]);
        }
    printf("%d",ans);
    return 0;
}

 

下面再给出一道题,看看这道题又应该怎么做呢?



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

输入格式

第一行包含整数 N

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

输出格式

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

数据范围

1N100000
10^910^9

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

可能就会有同学问了,这两个题除了数据有一点不同外其余没什么差别啊,我还用原来的方法不就行了么?
分析完数据范围就可以发现,原来的方法的复杂度并不能解决当前这个问题,我们需要找到更优的思路,下面我将给出nlg(n)的思路:
我们不妨先拿出一个栈,这个栈用来存放最长子序列,栈里面的元素是严格单调递增的,那我们应该怎样更新栈里面的元素呢?
做法应该是这样的:
首先要知道栈顶存放的是栈中最大的元素,当遇到一个比栈顶元素还大的元素时,我们没有理由不把他放入栈中,可当发现一个比栈顶元素要小的元素呢?
仅仅不更新栈顶就可以了么?当然不是啦,我们要替换第一个严格大于他的栈内元素,由于我们所构造的栈本来就是严格单调递增的,所以也就是找到第一个大于我们当前所思考元素的栈内元素并将其替换
仔细想想这是为什么?我们栈内被替换的元素被替换前后是不是发挥着一样的作用,被替换后答案序列长度并没有减少,反而更有利于接下来栈内元素的增多。
由于我们维护的栈是有序的,所以我们可以用二分查找,所以复杂度变成了
nlg(n),下面给出代码:


#include<bits/stdc++.h>
using namespace std;
#define INF 999999999
int a[100002],dp[100002];
int main()
{
    int n;
    cin>>n;
    memset(dp,0x3f,sizeof(dp)); 
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        *lower_bound(dp+1,dp+n+1,a[i])=a[i];//lower_bound是二分查找函数,当然也可以手写二分
} printf("%d\n",lower_bound(dp+1,dp+n+1,INF)-dp-1); return 0; }

 

 

 

子序列问题通常分为四种:最长上升子序列   最长不下降子序列    最长下降子序列    最长不上升子序列
我总结出四种问题
nlg(n)做法的替换思路:
最长上升子序列 每次更新第一个大于或者等于当前元素的栈内元素(维护的是单调递增栈)

最长不下降子序列 每次更新第一个大于当前元素的栈内元素
(维护的是单调递增栈)

最长下降子序列 每次更新第一个小于或者等于当前元素的栈内元素(维护的是单调递减栈)

最长不上升子序列 每次更新第一个小于当前元素的栈内元素
(维护的是单调递减栈)


我们求一个大于或者大于等于当前元素的栈内元素时可以利用upper_bound函数和lower_bound函数,但要注意的是
初始时一定要把数组中的所有数都置为最大值
那我们要求一个小于或者小于等于当前元素的栈内元素时又应该怎么办呢?这时候我们就要用一个技巧了,
不难发现如果一个序列是a1,a2,……an,而另一个序列为an,an-1,……a1,那么前一个序列的最长下降子序列就变成了
后一个序列的最长上升子序列,而前一个序列的最长不上升子序列就变为了后一个序列的最长不下降子序列。
这样我们就又可以利用现成的函数了,而不需要手写二分啦(我真是个小天才哈哈).



现在给出一个Dilworth定理推论:

能覆盖整个序列的最少的不上升子序列的个数”等价于“该序列的最长上升子序列长度

能覆盖整个序列的最少的不下降子序列的个数”等价于“该序列的最长下降子序列长度






下面给出一个相应例题及其代码:


某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

共一行,输入导弹依次飞来的高度。

输出格式

第一行包含一个整数,表示最多能拦截的导弹数。

第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。

数据范围

雷达给出的高度数据是不大于 30000 的正整数,导弹数不超过 1000

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6
2




#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1002;
int a[N],dp1[N],dp2[N],h[N];
int main()
{
    int n=0,x;
    while(scanf("%d",&x)!=EOF) a[++n]=x;
    memset(dp1,0x3f,sizeof(dp1));
    memset(dp2,0x3f,sizeof(dp2));
    for(int i=n;i>=1;i--)//求最长不上升子序列,可以转化为逆序求最长不下降子序列 
        *upper_bound(dp1,dp1+n,a[i])=a[i];
    printf("%d\n",lower_bound(dp1,dp1+n,0x3f3f3f3f)-dp1);
    for(int i=1;i<=n;i++)
        *lower_bound(dp2,dp2+n,a[i])=a[i];
    printf("%d",lower_bound(dp2,dp2+n,0x3f3f3f3f)-dp2);
    return 0;
}

 

 

以上就是我对常见的最长子序列问题的理解啦,希望大家能够喜欢!




posted @ 2021-05-15 17:58  AC--Dream  阅读(493)  评论(0编辑  收藏  举报