动态规划——子序列问题

一、最长上升子序列(LIS)

1、输出长度
问题描述:
一个数的序列bi,当b1<b2<…<bS的时候,我们称这个序列是上升的。比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。这些子序列中最长的长度是4,比如子序列(1,3,5,8)。 你的任务,就是对于给定的序列,求出最长上升子序列的长度。
输入

输入的第一行是序列的长度N(1≤N≤1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。

输出

最长上升子序列的长度。

样例输入

7
1 7 3 5 9 4 8

样例输出

4

策略分析:

复杂度O(n2):O(n2)的策略也是现在最常见的,首先考虑状态的设计,假设dp[i]表示第i个元素为结尾的最长上升子序列,很明显当前的状态只能由之前的状态得到,我们就要考虑寻找i之前的元素,假设i之前的元素由j表示,那么应该找到a[i]>a[j](因为是上升序列)并且dp[j]要是前面序列中最大的才行。
很容易想到状态转移方程dp[i] = Max{dp[j]+1 | j 属于1~i,并且a[j] < a[i]}
(对于每一个当前状态,都满足无后效性,和最优子结构,同类型的题还有最长下降,最长不上升,最长不下降等,同理)

代码

#include <cstdio>
#define MAXN 1005
int a[MAXN], dp[MAXN]; //dp[i]表示以第i个元素结尾的最长子序列 
int main() {
	int n, mx = 1; //注意mx一定要初始化为1,最长子序列至少都是1
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		dp[i] = 1;	//初始化为1,每个元素的最长子序列至少都是1 
	}
	
	for(int i = 1; i <= n; i++) 
		for(int j = 1; j < i; j++) 
			if(a[i] > a[j] && dp[i] <= dp[j]) {
				dp[i] = dp[j]+1;
				if(dp[i] > mx)	//填充数组的时候不断更新最大值即可
					mx = dp[i];
			}
	printf("%d\n", mx);
	return 0;
}

二、输出路径
现在要求输出这个最长子序列的路径
策略分析:

由于是输出序列,我们在更新dp数组的时候是根据之前的状态更新,那我们很容易想到,每次更新的时候要纪录一下从哪一个地方更新过来的,因此可以使用一个pre数组进行纪录,然后使用递归的方式向前找即可
pre[i]表示第i个元素的前一个元素是pre[i]

代码

#include <cstdio>
#define MAXN 10020
int a[MAXN], dp[MAXN], pre[MAXN]; 

void print(int x) {
	if(x == pre[x]) { //如果找到尽头了
		printf("%d ", a[x]);
		return;
	}
	print(pre[x]);
	printf("%d ", a[x]);
}
int main() {
	int n, mx = 1, loc = 1;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		dp[i] = 1;	//初始化为1,每个元素的最长子序列至少都是1 
		pre[i] = i; //初始化,每个元素的前缀都是自己本身 
	}
	
	for(int i = 1; i <= n; i++)  //实际上i可以从2开始
		for(int j = 1; j < i; j++) 
			if(a[i] > a[j] && dp[i] <= dp[j]) {
				dp[i] = dp[j]+1;
				pre[i] = j;	//记录当前位置的前缀 
				if(dp[i] > mx) {	//记录一下最长序列出现的位置 ,方便递归
					mx = dp[i];
					loc = i;
				}
			}
	printf("%d\n", mx);
	print(loc);	//递归输出序列
	return 0;
}

二、最长公共子序列(LCS)

1、输出长度
问题描述:
给出两个字符串,求最长的公共子序列(子序列就是在该序列中删去若干元素后得到的序列,可以不连续)
输入

共有两行。每行为一个由大写字母构成的长度不超过1000的字符串,表示序列X和Y。

输出

第一行为一个非负整数。表示所求得的最长公共子序列的长度。若不存在公共子序列.则输出文件仅有一行输出一个整数0。

样例输入

ABCBDAB
BDCABA

样例输出

4

策略分析:

复杂度O(n2):我们可以设计如下状态 dp[i][j]表示第一个序列中前i个和第二个序列中前j个的最长公共子序列,那么假如出现了,a[i-1] = b[j-1]的情况,很显然,可以更新dp[i][j] = max(dp[i][j],d[i-1][j-1]+1),如果没有相同的公共元素,当前的状态取决于选取之前的最优值,那么dp[i][j] = max(dp[i-1][j], dp[i][j-1])

代码

#include <cstdio>
#include <algorithm>
#include <cstring>
#define MAXN 1005
using namespace std;
char a[MAXN], b[MAXN];
int dp[MAXN][MAXN]; 

int main() {
	int len1, len2;	
	scanf("%s%s", a, b);
	len1 = strlen(a);
	len2 = strlen(b);
	for(int i = 0; i <= len1; i++) {	//由于dp[i][j]表示的是i之前的元素的状态,因此要循环到len1
		for(int j = 0; j <= len2; j++) {
			if(i == 0 || j == 0) continue;
			if(a[i-1] == b[j-1])
				dp[i][j] = dp[i-1][j-1] + 1;
			else
				dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
		}
	}
	printf("%d\n", dp[len1][len2]); //输出结果
	return 0;
}

三、最长上升公共子序列(LCIS)

输出长度
问题描述:
给出两个序列,求最长上升公共子序列(子序列就是在该序列中删去若干元素后得到的序列,可以不连续)
输入

每个序列用两行表示,第一行是长度M(1 <= M <= 500),第二行是该序列的M个整数Ai (-231 <= Ai < 231 )

输出

在第一行,输出两个序列的最长上升公共子序列的长度L。在第二行,输出该子序列。如果有不止一个符合条件的子序列,则输出任何一个即可。

样例输入

5
1 4 2 5 -12
4
-12 1 2 4

样例输出

2
1 4

策略分析:

首先定义状态d[i][j]表示第一个序列中前i个元素和第二个序列的前j个元素并且以b[j]为结尾的LCIS
可以思考,当前的状态可能是由哪些情况获得的
1、假如a[i]!=b[j],那么一定由d[i][j] = d[i-1][j](因为a[i]!=b[j],说明a[i]对于当前LCIS没有贡献,不会有可能让公共子序列变长,同时,d[i][j]是以b[j]结尾的LCIS,对于LCIS的最后一个元素不会有影响)
2、假如a[i]==b[j],首先至少能获得长度为1的LCIS,其次,我们应该从之前的元素找到最长的LCIS并且结束的地方的值要小于b[j],因此我们需要遍历数组d,应该确定的是,第一维度应该锁定在i-1,因为前i-1个元素中能够出现的LCIS一定比前i-2个多,那么第二维度就应该枚举b[1]~b[j-1]
状态转移方程:
a[i] != b[j] d[i][j] = d[i-1][j]
a[i] == b[j] d[i][j] = max(d[i-1][k]+1) 其中 1<=k<=j-1 && b[j] > b[k]

代码(O(n*m2))

#include <cstdio>
#define MAXN 505
int d[MAXN][MAXN], a[MAXN], b[MAXN];
int main() {
	int n, m;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	scanf("%d", &m);
	for(int i = 1; i <= m; i++)
		scanf("%d", &b[i]);
	
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= m; j++) {
			d[i][j] = d[i-1][j];
			if(a[i] == b[j]) {
				int mx = -1;
				for(int k = 1; k < j; k++) 
					if(b[k] < b[j] && mx < d[i-1][k]) 
						mx = d[i-1][k];
				d[i][j] = mx+1;
			}
		}
	}
	int ans = 0;
	for(int i = 1; i <= m; i++)
		if(ans < d[n][i])
			ans = d[n][i];
	printf("%d\n", ans);
}
不难发现,对于之前的代码,复杂度出现(O(n*m2))的情况,必然是因为当a[i]==b[j]的时候,我们去枚举了从1~j-1的情况,其实我们可以预先将d[x-1][k]的值保存下来,我们思考如何保存,假如出现了情况a[i]==b[j],我们之前找j之前的所有元素,从这里发现a[i] > b[k]的,那么对于之前的每一次遍历,只要满足a[i] > b[j]这个条件,我们都可以更新一下,最大值mx = max(mx,d[i-1][j]),这样就提前找到了d[i-1][j]之前的最大的情况,变成了(O(n*m))

代码(O(n*m))

#include <cstdio>
#define MAXN 505
int d[MAXN][MAXN], a[MAXN], b[MAXN];
int main() {
	int n, m, mx;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	scanf("%d", &m);
	for(int i = 1; i <= m; i++)
		scanf("%d", &b[i]);
	
	for(int i = 1; i <= n; i++) {
		mx = 0;
		for(int j = 1; j <= m; j++) {
			d[i][j] = d[i-1][j];
			if(a[i] > b[j] && mx < d[i-1][j]) mx = d[i-1][j];
			if(a[i] == b[j]) d[i][j] = mx + 1;
		}
	}
	int ans = 0;
	for(int i = 1; i <= m; i++)
		if(ans < d[n][i])
			ans = d[n][i];
	printf("%d\n", ans);
}
同时还可以将dp数组优化成一维的,因为如果a[i]!=b[j],那么当前不会发生任何变化继承之前的即可,只需要改变a[i]==b[j]的时候,d[j]的值即可,最后遍历d数组,找最长即可
#include <cstdio>
#define MAXN 505
int d[MAXN], a[MAXN], b[MAXN];
int main() {
	int n, m, mx;
	scanf("%d", &n);
	for(int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	scanf("%d", &m);
	for(int i = 1; i <= m; i++)
		scanf("%d", &b[i]);
	
	for(int i = 1; i <= n; i++) {
		mx = 0;
		for(int j = 1; j <= m; j++) {
			if(a[i] > b[j] && mx < d[j]) mx = d[j];
			if(a[i] == b[j]) d[j] = mx + 1;
		}
	}
	int ans = 0;
	for(int i = 1; i <= m; i++)
		if(ans < d[i])
			ans = d[i];
	printf("%d\n", ans);
}

参考:https://wenku.baidu.com/view/3e78f223aaea998fcc220ea0.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值