动态规划——线性DP

动态规划的思维图
在这里插入图片描述

1. 线性DP的定义
状态转移方程是线性关系的,即从前向后线性递推

2. 典型例题

(1)数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

          7
       3     8
     8    1    0
   2   7    4    4
 4   5    2   6    5

输入格式
第一行包含整数n,表示数字三角形的层数。

接下来n行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。

输出格式
输出一个整数,表示最大的路径数字和。

数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30

解题思路:
1、确定状态函数:题目求从顶端到底部的最大距离,起点固定,可以用f[i][j]表示从起点到(i,j)的所有路径的集合,f[i][j] = 路径最大值
在这里插入图片描述
2、确定状态转移方程:起点到某个点可以划分为从左上角到达,或从右上角到达,到达该点的最大值就是这两条路径的最大值+该点的值就是起点到该点的最大值,所以状态转移方程为:f[i][j] = max(f[i-1][j-1], f[i-1][j]) + [i, j]

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 510, INF = 1e9;

int n;
int a[N][N];
int f[N][N];

int main()
{
	 cin >> n; 
	 for(int i = 1; i <= n; i ++)
	 	for(int j = 1; j <= i; j++)
	 		scanf("%d", &a[i][j]);
	 
	 for(int i = 0; i <= n; i++)
	 	for(int j = 0; j <= i+1; j++)
	 		f[i][j] = -INF;
	 
	 f[1][1] = a[1][1];
	 for(int i = 2; i <= n; i++)
	 	for(int j = 1; j <= i; j++)
	 		f[i][j] = max(f[i-1][j-1], f[i-1][j]) + a[i][j]; //状态转移方程
	
	int res = -INF;
	for(int i = 1; i <= n; i ++) res = max(res, f[n][i]);

	cout << res << endl;
	return 0;
}

(2)求最长上升子序列I

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

输入格式
第一行包含整数N。

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

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

数据范围
1≤N≤1000,
−10&9≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4

解题思路:
1、确实状态函数为f[i]表示以第i个数结尾的所有子序列集合,f[i]=最大子序列长度
2、确定状态转移方程:枚举第i个数前面的数j,如果a[j] < a[i]说明a[j]可能是以a[i]为结尾的最长子序列的倒数第二个数,如果是就用f[j]+1更新f[i]。所以状态转移方程为f[i] = max(f[i], f[j]+1)
时间复杂度O(n2)

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 10010;

int n;
int a[N];
int f[N];

int main()
{
	cin >> n;
	for(int i = 0; i < n; i++) cin >> a[i];
	
	for(int i = 0; i < n; i++)
	{
		f[i] = 1;  //初始化,假设第i个数比前面的数都小最,即以第i个数结尾子序列为1
		for(int j = 0; j < i; j ++)
			if(a[j] < a[i])
				f[i] = max(f[i], f[j] + 1);
	}

	int res = 0;
	for(int i = 0; i < n; i++) res = max(res, f[i]);
	cout << res << endl;
	return 0;
}
	

如果要保存最长子序列,只需要保存每次转移的路径即可

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;
int n;
int a[N];
int f[N], g[N];   // g[i]用来报存f[i]是从第几个节点转移过来的

int main()
{
	cin >> n;
	for(int i = 0; i < n; i++) cin >> a[i];	

	for(int i = 0; i < n; i++)
	{
		f[i] = 1;
		for(int j = 0; j < i; j++)
			if(a[j] < a[i])
				if(f[i] < f[j] + 1)
				{
					f[i] = f[j] + 1;
					g[i] = j;  // 记录f[i]是从第j个数转移过来的
				}
	}

	//找到最大子序列的下标存在res中
	int res = 0;
	for(int i = 0; i < n; i++)
		if(f[res] < f[i])
			res = i;
	
	cout << f[res] << endl;
	
	//从后往前输出序列;
	for(int i = 0, len = f[res]; i < len; i++)
	{
		printf("%d ", a[res]);
		res = g[res];	//每次将指针指向转移来的那个下标
	}
	return 0;
}
	

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

输入格式
第一行包含整数N。

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

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

数据范围
1≤N≤100000,
−109≤数列中的数≤109
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4

这里的数据范围较上一题的大如果直接使用上一题的代码会超时,所以要对过程进行优化。
优化过程如下:
1、在长度相同的情况下,结尾数值更小的序列更有适用性(即:当两个长度相等的子序列分别以3,1结尾,那么后面所有的数可以接在以3结尾的后面一定可以接在以1结尾的后面)
2、又易证随着长度的递增租序列结尾的最小数值也是递增的
3、所以每次判断以某个数结尾的最长子序列 = 结尾不大于这个数的最长序列 +1
所以只需要用一个一维数组存储每个长度的结尾最小值,然后根据(3)更新每个长度结尾的最小值就可以找到结果

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 100010;

int n;
int a[N];
int q[N];  // 存储长度为i的子序列的结尾最小值

int main()
{
	cin >> n;
	for(int i = 0; i < n; i++) scanf("%d", &a[i]);

	int len = 0;
	q[0] = -2e9;
	for(int i = 0; i < n; i++)
	{
		// 二分查找小于a[i]的最大值
		int l = 0, r = len;
		while(l<r)
		{
			int mid = l + r + 1 >> 1;
			if(q[mid] < a[i]) l = mid;
			else r = mid - 1;
		}
		len = max(len, r + 1); //更新最长长度
		q[r + 1] = a[i]; //更新最小值
	}

	printf("%d\n", len);
	return 0;
}
	

(4)最长公共子序列
给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。

输入格式
第一行包含两个整数N和M。

第二行包含一个长度为N的字符串,表示字符串A。

第三行包含一个长度为M的字符串,表示字符串B。

字符串均由小写字母构成。

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

数据范围
1≤N,M≤1000
输入样例:
4 5
acbd
abedc
输出样例:
3

解题思路:
1、确定状态函数f[i][j] 表示在第一个字串中前i个字符中出现,且在第二个字符串前j个字符中出现的所有子序列的集合。f[i][j] = max;
2、确定状态转移方程:
在这里插入图片描述
可以根据a[i]和b[j]是否包含在子序列中划分为四种情况(a[i],b[j]都不在,a[i]不在b[j]在…)
00时:都不包含时,是在第一个字符串前i-1个中出现,且在第二个字符串前j-1个出现的最大子串长度
11时:都包含时,是在第一个字符串前i-1个中出现,且在第二个字符串前j-1个出现的最大子串长度加一
01时:不包含a[i],包含b[j],不好直接算。f[i-1][j]表示在第一个字符串前i-1个中出现,且在第二个字符串前j个出现的最大子串长度,第二个子串中可能包含b[j]也可能不包含,所以f[i-1][j]包含01的情况和00的情况
10时:同01

所以状态转移方程为:f[i][j] = max(f[i-1][j], f[i][j-1], f[i-1][j-1] + 1)

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main()
{
	cin >> n >> m;
	scanf("%s", a+1);
	scanf("%s", b+1);
	
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
		{
			f[i][j] = max(f[i-1][j], f[i][j-1]);
			if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
		}
	
	cout << f[n][m] << endl;
	return 0;
}

(5)最短编辑距离
给定两个字符串A和B,现在要将A经过若干操作变为B,可进行的操作有:

删除–将字符串A中的某个字符删除。
插入–在字符串A的某个位置插入某个字符。
替换–将字符串A中的某个字符替换为另一个字符。
现在请你求出,将A变为B至少需要进行多少次操作。

输入格式
第一行包含整数n,表示字符串A的长度。

第二行包含一个长度为n的字符串A。

第三行包含整数m,表示字符串B的长度。

第四行包含一个长度为m的字符串B。

字符串中均只包含大写字母。

输出格式
输出一个整数,表示最少操作次数。

数据范围
1≤n,m≤1000
输入样例:
10
AGTCTGACGC
11
AGTAAGTAGGC
输出样例:
4

解题思路:
1、确定状态函数:f[i][j]表示把序列a[1~i]变成b[1 ~j]的所有步骤的集合,f[i][j] = min
2、确定状态转移方程:根据把a[1~i]变成b[1 ~j]对a[i]的三种操作划分集合
在这里插入图片描述

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main()
{
	scanf("%d%s", &n, a+1);
	scanf("%d%s", &m, b+1);

	// 初始化f[0][N],f[N][0],因为没有转移到这两个边界值的状态
	for(int i = 0; i <= n; i++) f[i][0] = i;
	for(int i = 0; i <= m; i++) f[0][i] = i;

	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
		{
			f[i][j] = min(f[i-1][j] + 1, f[i][j-1] + 1);
			if(a[i] == b[j]) f[i][j] = min(f[i][j], f[i-1][j-1]);
			else f[i][j] = min(f[i][j], f[i-1][j-1] + 1);
		}
	
	printf("%d\n", f[n][m]);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值