最长xxx子序列(dp及二分优化)

最长xx子序列分为四类:

1. 最长上升子序列

2. 最长下降子序列

3. 最长不上升子序列

4. 最长不下降子序列

通过名字可以看出他们其实是很相似的,那么就以最长上升为例分析。

对于一串数字序列 eg:3 6 2 3 7 5 10 8 9 12

dp思路

想要找到他的最长上升子列, 可以选择一种很贪心的思想来考虑他:对于每一个数字,它本身都是一个长度为1的子序列,所以最短最短的情况,这个最长上升子列长度是1。顺着的思路想要找到他后边比他大的数字且是上升的序列,却无法想到合适的代码实现,所以逆向思维一下,我们可以找到顺着跑到目前位置可得到的最长上升子列即可了。

对于例子的序列:

dp[1] = 1 (3本身是一个长度为1的上升子列)

dp[2] = 2 (遍历比目前位要小的数字找到比他小的数字的dp后长度 + 1的最大值,此时为3,6)

dp[3] = 1

dp[4] = 2

dp[5] = 3 (不用考虑子列的数字内容,只用看它的长度)

一句话概括这个规律的话,就是递推找到比目前小的数字对应序列长度的最大值+1。

#include<bits/stdc++.h>
#define ll long long
#define int ll
using namespace std;
const int maxn = 1e6 + 10;
int a[maxn], dp[maxn];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie();
	cout.tie();
	int n;
	cin>>n;
	for (int i = 1; i <= n; i++) {
		cin>>a[i];
		dp[i] = 1; //本身就是一个长度为1的子列 
	}
	//最长上升子列 
	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);
			}
		} 
	}
	//最长不下降子列
	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);
			}
		} 
	}
	//最长下降子列
	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);
			}
		} 
	}
	//最长不上升子列
	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);
 			}
 		} 
 	}
	return 0;
}

dp的思想解答这个问题,对于数据范围小的时候是完全可行的,但是数据范围一旦变大,近n^2的时间复杂度必然会超时,就如下边那一题,数据范围达到了惊人的1e5,如此则必然超时了..

于是乎,就产生了一种优化方式

二分优化

虽说是二分优化,实则是另一种考虑方式。 此时的dp数组储存的内容便不再是长度了,而是让他在第i位记录长度为i的子序列的最后一个元素是什么。(对于上升和不下降是最小元素,下降和不上升就是最大元素了) ->因为上升序列的话,对于同样长度的字串,末数字越小,后续元素才可能更大

同样对于样例字符串eg:3 6 2 3 7 5 10 8 9 12

有dp[1] = 2 (长度为1的字串最小的末位数便是整个数组最小的数)

dp[2] = 3

dp[3] = 5

dp[4] = 8

dp[5] = 9

dp[6] = 12

(最长不下降子列到6,则dp再往上就没有内容了)

那我们该如何得到这样的数组呢?

我们可以选择从a数组的第一个数开始往dp数组里进行修改插入,最开始的时候是空的,所以要找的是第一个比a[1]小的数字的长度最大值也只能是0,找到了这个下标之后,我们将a[1]放到dp[1]的位置,即dp[1] = a[1],接下来的步骤也同样,插入一个数字之后,此时的最大长度变成了1,我们就变成了在0到1的范围内找到比a[2]小的最大位置k即可,并将a[2]放到dp[k + 1]的位置上,延长了最大的子序列长度。

而这个所谓“找到比a[2]小的最大位置k”,在小范围内是可以暴力的,但是范围一旦大了,就无法避免超时的情况,所以我们使用二分查找。

(为什么我们可以使用二分查找?显然对于这个已经构造好的dp序列,我们是一定能保证他的数字内容是单调递增的<上升子列>,相反下降子列便是递减的,每找到一个比a[i]小的数字都会将这个大数放在它的后一位),所以这个序列必然是有序的。

构造好dp序列后最大的有数下标便是最长不下降子列了。

#include<bits/stdc++.h>
#define ll long long
#define int ll
using namespace std;
const int maxn = 1e6 + 10;
int a[maxn], dp[maxn];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie();
	cout.tie();
	int n, len;
	cin>>n;
	for (int i = 1; i <= n; i++) {
		cin>>a[i];
	}
	//最长上升子列 
	len = 0; // 最开始的最长子列长度为0 
	dp[0] = -1e17; //保证了第1位的数字一定能至少放在长度为0的子列后边 
	for (int i = 1; i <= n; i++) {
		int l = 0, r = len;
		while (l < r) {
			int mid = (l + r + 1) / 2;
			if (dp[mid] <= a[i]) {
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		len = max(len, l + 1);
		dp[l + 1] = a[i]; //不需判断二者大小 因为若原先dp[l+1]不为0 是必然大于a[i]的 
	}
	cout<<len;//最长子列长度 
	//最长不下降子列
	len = 0; // 最开始的最长子列长度为0 
	dp[0] = -1e17; //保证了第1位的数字一定能至少放在长度为0的子列后边 
	for (int i = 1; i <= n; i++) {
		int l = 0, r = len;
		while (l < r) {
			int mid = (l + r + 1) / 2;
			if (dp[mid] < a[i]) {
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		len = max(len, l + 1);
		dp[l + 1] = a[i]; //不需判断二者大小 因为若原先dp[l+1]不为0 是必然大于a[i]的 
	}
	cout<<len;//最长子列长度 
	//最长下降子列
	len = 0; // 最开始的最长子列长度为0 
	dp[0] = 1e17; //保证了第1位的数字一定能至少放在长度为0的子列后边 
	for (int i = 1; i <= n; i++) {
		int l = 0, r = len;
		while (l < r) {
			int mid = (l + r + 1) / 2;
			if (dp[mid] >= a[i]) {
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		len = max(len, l + 1);
		dp[l + 1] = a[i]; //不需判断二者大小 因为若原先dp[l+1]不为0 是必然小于a[i]的 
	}
	cout<<len;//最长子列长度 
	//最长不上升子列
	len = 0; // 最开始的最长子列长度为0 
	dp[0] = 1e17; //保证了第1位的数字一定能至少放在长度为0的子列后边 
	for (int i = 1; i <= n; i++) {
		int l = 0, r = len;
		while (l < r) {
			int mid = (l + r + 1) / 2;
			if (dp[mid] > a[i]) {
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		len = max(len, l + 1);
		dp[l + 1] = a[i]; //不需判断二者大小 因为若原先dp[l+1]不为0 是必然小于a[i]的 
	}
	cout<<len;//最长子列长度 
	return 0;
}

 以下是两个遇到的比较经典的简单题

例题

1.

题目链接:

[NOIP1999]拦截导弹 - 计蒜客 (jisuanke.com)

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

1行,若干个整数(个数≤100000)

输出格式

2行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

样例输入

389 207 155 300 299 170 158 65

样例输出

6
2

这是一道考察比较全面的题目,通过分析,可以判断出

1.一套系统最多拦截导弹数,显然是要找到最长不上升的子列,因为每一个导弹不能达到比上一步更高的位置,但可以平着走。

2.最少需要的导弹套数,对于一串数列,想要打到所有的且最少,首先对于后边的比前边的高的塔,一定是用的不同的导弹才能打到,便转化成了求最长上升子列

3.本题我使用了反转数组,将求最长不上升改成了求最长不下降,其实是没必要的,直接套用上边总结的公式就行了,但这样也算是复习了reverse函数的用法了吧。

4.题目给的数列都是大于0的,所以不用特别初始化dp[0]

已ac代码:

#include<stdio.h>
#include<algorithm>
#include<iostream>
#include<math.h>
#include<string.h>
#include<map>
#include<queue>
#include<vector>
#define ll long long
//#define int ll
using namespace std;
const int maxn = 1e6 + 10;
int mod = 1e9 + 7;
int a[maxn], b[maxn], c[maxn];
signed main()
{
	int x, ans1 = 0, ans2 = 0, count = 0;
	while (cin>>x) {
		a[++count] = x;
	}
	for (int i = 1; i <= count; i++) {
		int l = 0, r = ans2, mid;
		while (l < r) {
			mid = (l + r + 1) / 2;
			if (c[mid] < a[i]) {
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		c[l + 1] = a[i];
		ans2 = max(ans2, l + 1);
	}
	reverse(a + 1, a + count + 1);
	for (int i = 1; i <= count; i++) {
		int l = 0, r = ans1, mid;
		while (l < r) {
			mid = (l + r + 1) / 2;
			if (b[mid] <= a[i]) {
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		b[l + 1] = a[i];
		ans1 = max(ans1, l + 1);
	}
	cout<<ans1<<"\n"<<ans2;
	return 0;
}

2.

题目链接:

Problem - 5256 (hdu.edu.cn)

题意要求我们构造一个严格递增的结果序列,而对原先数组进行修改。但是还要考虑到如果a[i] < a[j]但是 i 到 j 的数字差要小于其中间需要被修改的数字量的情况,是不能选择这个段进行修改中间的。很巧妙的是你可以选择让每个位置的数字变成a[i] - i,此时判断的a[i] < a[j]的前提下,又保证了a[j] - a[i] > j - i,能使中间的数字都能被修改,剩下的就是简单的板子了,找到最长不下降子列后,需要被修改的数字量便是总长度减这个子列长度了。

已ac代码:

#include<iostream>
#include<fstream>
#include<stdio.h>
#include<math.h>
#include<algorithm>
#include<string.h>
#include<queue>
#define buff ios::sync_with_stdio(false);cin.tie();cout.tie()
//#define int long long
#define ll long long
using namespace std;
const int maxn = 1e6 + 10;
int box[50], a[maxn], dp[maxn];

int main() 
{
	
	int t;
	scanf("%d", &t);
	for (int re = 1; re <= t; re++) {
		int n;
		scanf("%d", &n);
		for (int i = 1; i < n; i++) {
			dp[i] = 10000000;
		}
		for (int i = 1; i <= n; i++) {
			scanf("%d", &a[i]);
			a[i] = a[i] - i;
		}
		dp[0] = 0;
		int ans = 0;
		for (int i = 1; i <= n; i++) {
			int l = 0, r = ans;
			while (l < r) {
				int mid = (l + r + 1) / 2;
				if (dp[mid] <= a[i]) {
					l = mid;
				} else {
					r = mid - 1;
				}
			}
			dp[l + 1] = a[i];
			ans = max(ans, l + 1);
		}
		printf("Case #%d:\n%d\n", re, n - ans);
	}
	return 0; 
}

by yq

后记:

早在上学期被罚抄lis的dp板子时就想总结这个子列的问题了,直到最近才完完全全整理出来,对于现在的我们来说可能会很简单,但确实记录了我研究上下升降子列问题的艰苦过程。又及感谢一直以来提供帮助的大佬们!

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超高校级のDreamer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值