最长子序列问题详解

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

先拿最长上升子序列问题来说吧:

很明显这是一个动态规化问题,仔细想想也不难得出其状态转移方程

首先介绍一下dp[]数组的含义:dp[i]表示以第i个数结尾的最长上升子序列长度,接下来就是状态转移的具体方法了

就是每更新到i时,就用dp[1~i-1]更新dp[i]的值,当j

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

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

输入格式

第一行包含整数 N。

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

输出格式

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

数据范围

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

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4
#include
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;ja[j])//只有当待更新位置的元素大小大于之前元素的大小时才能被更新
                    dp[i]=max(dp[i],dp[j]+1);
            ans=max(ans,dp[i]);
        }
    printf("%d",ans);
    return 0;
}

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

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

输入格式

第一行包含整数 N。

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

输出格式

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

数据范围

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

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

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

#include
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; 
}

下面我将对常见的最长子序列问题进行总结:(以下思路复杂度均为nlogn)

最长上升子序列 每次更新第一个大于或者等于当前元素的栈内元素(维护的是单调递增栈)

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

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

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

我们求一个大于或者大于等于当前元素的栈内元素时可以利用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
#include
#include
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;
}

写到这大家有没有一个疑问,我们刚才利用单调栈只能求出最长子序列的长度,那我们要是求最长子序列是什么呢?在朴素版的求解最长子序列问题里面,我们可以设置一个pre[i]来表示第i个数前一个数是什么,就这样求可以利用while循环来求出最长序列是什么,下面附上相应代码,最长子序列存在st数组中,最长子序列的最后一个元素是ans.

tt=0;
while(ans)
{
	st[++tt]=ans;
	ans=pre[ans];
}

那利用单调栈求解最长子序列问题时应该怎么求解最长子序列是什么这个问题呢?

其实还是利用刚才那个贪心的思想,我们这个时候另开一个id数组,id[i]记录第i个数位于最长子序列的哪个位置,虽然第i个数并不一定在最终的最长子序列中,但是他肯定在求解最终最长子序列过程中出现过,我们只要把他在求解过程中出现的位置记录下来就行了,由于贪心的思路是从前往后的,那么也就是说同是位于最长子序列中第k个位置的两个数,谁的位置靠后,谁的字典序就更偏小,这是显然的,要不然前边的也不会被后面的数替换了,所以我们求最长子序列时应该从后往前求,遍历到某个位置,能加就加进去,比如说我们现在遍历到了位置i,已经找到了最长子序列中的最后三个数,最长子序列一共有n个数,那么我们下一个要找的就是位于最长子序列第n-3个位置上的数,也就是id[i]==n-3,如果id[i]>n-3,此时我们应该直接忽略,原因就是我们已经取到了更优的位于最长子序列上第id[i]个位置上的数,而如果id[i]

举个例子(以最长上升子序列为例):

1 3 5 4 6 2 7 8 6

最长子序列问题详解_第1张图片

 最后最长上升子序列的长度为6,但并不是ans里面的数,我们来模拟求一下最长上升子序列的数是多少,先从后往前找第6个,id[9]!=6,继续看下一个,发现id[8]等于6,说明最长上升子序列第6个数是原序列第8个数8,接着找第5个,id[7]=5,说明最长上升子序列第5个数是原序列第7个数7,接着找第4个,id[6]!=4,id[5]=4,说明最长上升子序列第4个是原序列第5个数6,然后找第三个,id[4]=3,说明最长上升子序列第3个数是原序列第4个数4,然后找第二个数,id[3]=3,不符合,继续向前找,id[2]=2,说明最长上升子序列第2个数是原序列第2个数3,然后找第一个,id[1]=1,说明原序列中第一个数就是最长上升子序列中第一个数,就这样我们就找完了最长上升子序列中所有的数,依次是 1 3 4 6 7 8

下面给出对应代码,大家可以结合代码好好理解一下过程:

其中ans中存的是最后遍历结束后的最长子序列(并不是结果),st数组中保留最后的结果,也就是对应的最长子序列在原数组中的位置,id_len是id数组的长度

ans.push_back(s[1]);
int id_len=0;id[++id_len]=1;
for(int i=2;i<=tt;i++)
{
	if(s[i]>ans[ans.size()-1]) 
	{
		ans.push_back(s[i]);
		id[++id_len]=ans.size();//记录第i个数在目前最长子序列中的位置
		continue;
	}
	int t=lower_bound(ans.begin(),ans.end(),s[i])-ans.begin();
	ans[t]=s[i];//替换
	id[++id_len]=t+1;//记录第i个数在目前最长子序列中的位置
}

输出过程:

for(int i=ans.size(),j=id_len;i>=1;j--)
{
	if(id[j]==i) st[++tt]=j,i--;
}

得到st数组后,我们只需要倒着输出即可输出最长上升子序列,再次需要注意的是st数组中存的只是位置,需要用对应的数组去索引

以上就是常见的最长子序列问题啦,如果大家有更好的方法处理这些问题,欢迎大家评论!

你可能感兴趣的:(动态规划,最长子序列问题)