动态规划-线性Dp和区间Dp详解

一、问题说明

1.题目问题

在题目的要求基础上,求某个值的最大(小)值

  • 线性Dp:每个状态之间有明显的线性关系
  • 区间Dp:每个状态是根据区间划分的,常用到前缀和、后缀和等算法

2.解题思路

依旧从状态表示和状态计算两个模块入手

  • 状态表示:思考每个状态如何表示
  • 状态计算:根据线性或者区间关系,求解状态值

二、线性Dp-数字三角形

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

2.算法

  • 状态表示:用f[i][j]来表示状态,其中i表示第几行(从1开始),j表示第几斜列(从1开始),斜列是指从右上到左下的列,比如该题中7、3、8、2、4在第1斜列。则f[i][j]表示,从起点到a[i][j]的所有情况集合中,使得路径和最大的一种情况,其f[i][j]的值就是该路径和的值
  • 状态计算:在该三角中,我们可以将f[i][j]的计算划分来源,分为从a[i][j]左上f[i-1][j-1]而来和从a[i][j]右上f[i-1][j]而来
  • 所以综上:f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j])
  • 在遍历计算后,遍历最后一行取最大值即可
代码
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 510, INF = 1e9; //INF定义为无穷 

int n;
int a[N][N]; //存三角形每个数 
int f[N][N]; //存每个状态 

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ )
		for(int j = 1; j <= i; j ++ )
			scanf("%d", &a[i][j]);
	
	//状态全部初始化为负无穷,且边界外的一层也要做此处理
	//处理原因: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] + a[i][j], f[i-1][j] + a[i][j]);
			
	//遍历最后一行 
	int res = -	INF;
	for(int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
	
	printf("%d\n",res);
	return 0;
}

三、线性Dp-最长上升子序列

1.问题描述

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

输入格式

第一行包含整数 N。

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

输出格式

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

数据范围

1≤N≤1000,
−109≤数列中的数≤109

输入样例:
7
3 1 2 1 8 5 6
输出样例:
4

2.算法

  • 子序列并非指在连续的单元形成的子序列,可以不连续
  • 状态表示:用f[i]表示状态,f[i]是指所有以第i个数结尾的上升子序列的集合中,取最长上升子序列,f[i]的值就是该最长上升子序列的长度值
  • 状态计算:我们可以从上一个元素是什么入手,即从倒数第二个元素是什么入手,f[i]=max(f[j]+1),j=0,1,2,…,i-1
  • 遍历计算后,在遍历一遍所有f[i]取最大值
代码
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int n;
int a[N],f[N]; //a[N]存所有元素,f[N]存所有状态 

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
	
	for(int i = 1; i <= n; i ++ )
	{
		f[i] = 1;
		for(int j = 1; j < i; j ++ )
			if(a[j] < a[i]) //只有该元素小于a[i]才符合逻辑 
				f[i] = max(f[i], f[j] + 1);
	}
	
	//遍历所有f[i] 
	int res = 0;
	for(int i = 1; i <= n; i ++ ) res = max(res, f[i]);
	
	printf("%d\n", res);
	
	return 0;
}

3.进阶-输出最长上升子序列

  • 多开一个数组g[N],当以任意一个元素(该元素为第i个元素)结尾满足此时的最长上升子序列时,则用g[i]存满足情况时i的上一个元素是第几个元素
  • 最后遍历完,得到整体的最长上升子序列时以第k个数结尾
  • 从k开始可以反向输出最长上升子序列,注意是反向的
代码
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1010;

int n;
int a[N],f[N],g[N]; //a[N]存所有元素,f[N]存所有状态 

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
	
	for(int i = 1; i <= n; i ++ )
	{
		f[i] = 1;
		g[i] = 0;
		for(int j = 1; j < i; j ++ )
			if(a[j] < a[i]) //只有该元素小于a[i]才符合逻辑 
				if(f[i] < f[j] + 1)
				{
					f[i] = f[j] + 1;
					g[i] = j; //满足条件存上个元素 
				}
	}
	
	//遍历所有f[i]得下标 
	int k = 1;
	for(int i = 1; i <= n; i ++ )
		if(f[k] < f[i])
			k = i;
	
	printf("%d\n", f[k]);
	
	//输出路径 
	for(int i = 0, len = f[k]; i < len; i ++ )
	{
		printf("%d ", a[k]);
		k = g[k]; //k取上一个元素的下标 
	}
	
	return 0;
}

四、线性Dp-最长公共子序列

1.题目描述

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

输入格式

第一行包含两个整数 N和 M。

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

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

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

输出格式

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

数据范围

1≤N,M≤1000

输入样例:
4 5
acbd
abedc
输出样例:
3

2.算法

  • 状态表示:f[i][j]表示所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母中出现的子序列集合中,取最长公共子序列的长度值,即为f[i][j]的值
  • 状态计算:两个序列分别是a[i]、b[j],我们则可以把f[i][j]分为四种情况,不包含a[i]不包含b[j]、不包含a[i]包含b[j]、包含a[i]不包含b[j]、包含a[i]包含b[j]
  • 不包含a[i]包含b[j]:f[i-1][j]包含该种情况,该情况是f[i-1][j]的子集,因为f[i-1][j]可能包含b[j]也可能不包含b[j](包含a[i]不包含b[j]同理)
  • 包含a[i]包含b[j]:这种情况必须满足a[i] == b[j],f[i-1][j-1]+1
  • 不包含a[i]不包含b[j]:已经包含在f[i-1][j]里了
代码
#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%d",&n, &m);
	scanf("%s%s",a+1, 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);
		}
		
	printf("%d\n",f[n][m]);
	
	return 0;
}

五、区间Dp-石子合并

1.题目描述

设有 N堆石子排成一排,其编号为 1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2堆,代价为 4,得到 4 5 2, 又合并 1、2堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入格式

第一行一个数 N表示石子的堆数 N。

第二行 N个数,表示每堆石子的质量(均不超过 1000)。

输出格式

输出一个整数,表示最小代价。

数据范围

1≤N≤300

输入样例:
4
1 3 5 2
输出样例:
22

2.算法

  • 状态表示:f[i][j]表示将第i堆石子和第j堆石子合并的所有方法的集合中,取代价最小值的最优解,f[i][j]的值即为该最小值
  • 状态计算:可以从第i堆石子和第j堆石子合并方法的最后一次合并在何处后入手
  • 利用前缀和:定义为数组s[N]
  • 综上:f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1])
代码
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 110,INF = 1e9;

int n;
int s[N];
int f[N][N];

int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) scanf("%d", &s[i]);
	
	for(int i = 1; i <= n; i ++ ) s[i] += s[i-1]; //计算前缀和
	
	for(int len = 2; len <= n; len ++ ) //遍历每种长度 
		for(int i = 1; i + len - 1 <= n; i ++ ) //遍历每种起点 
		{
			int l = i, r = i + len - 1;
			f[l][r] = INF;//初始化,全局变量初始值为0 
			
			for(int k = l; k < r; k ++ )
				f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l-1]);
		}
		
	printf("%d\n",f[1][n]);
	
	return 0;
}

六、总结

我们可以从上例中找出规律,状态计算多与该状态的上一步来源有关,比如石子合并中状态计算是从最后(即上一次)一次合并的地方入手,最长上升子序列状态计算是从上一个元素是第几个入手,都是目前状态的上一步起源。由于Dp问题多变,还需多见题型,重在思考与思路,体会如何找到一道题的状态表示,并根据数据关系找到状态计算方法。

  • 23
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
动态规划线性规划和非线性规划是三种不同的优化方法,它们的思想和应用场景不同,具体区别如下: 1. 动态规划动态规划是一种在有重叠子问题和最优子结构的情况下可以采用的算法思想。动态规划通常用于解决多阶段决策问题,每个阶段的决策依赖于前面各个阶段的决策。常见的动态规划问题有最长公共子序列、背包问题等。 2. 线性规划:线性规划是一种优化问题,它的目标是在一组线性约束条件下最大化或最小化线性目标函数的值。线性规划问题通常可以用线性规划算法求解,这种算法的核心是单纯形法。常见的线性规划问题有生产计划、运输问题等。 3. 非线性规划:非线性规划是一类目标函数或约束条件中包含非线性项的优化问题。非线性规划问题通常比线性规划问题更难求解,因为它们的解空间通常是非凸的。常见的非线性规划问题有最小二乘法、无约束优化问题等。 举例来说: 1. 动态规划:最长公共子序列问题是一个经典的动态规划问题。给定两个字符串,求它们的最长公共子序列的长度。这个问题可以用动态规划算法求解,其中状态转移方程为:dp[i][j] = dp[i-1][j-1] + 1,当 s1[i] == s2[j] 时;否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1])。 2. 线性规划:假设一个工厂有 2 种机器可以用于生产产品 A 和 B,每种机器的使用时间和成本如下表所示。现在需要制定一个生产计划,使得生产的产品 A 和 B 的总成本最小,同时满足以下约束条件:每种机器的使用时间不得超过 40 小时,产品 A 和 B 的总生产量分别不得少于 100 和 200。 | 机器 | 生产 A 的时间 | 生产 B 的时间 | A 的成本 | B 的成本 | |------|-------------|-------------|---------|---------| | 1 | 10 | 20 | 2 | 3 | | 2 | 20 | 10 | 3 | 2 | 这个问题可以用线性规划算法求解,其中目标函数为:2A + 3B + 3A + 2B = 5A + 5B,约束条件为:10A + 20B ≤ 400,20A + 10B ≤ 400,A ≥ 100,B ≥ 200。 3. 非线性规划:假设有一组数据点 {(x1, y1), (x2, y2), ..., (xn, yn)},现在要求在所有二次函数 y = ax^2 + bx + c 中找到一个最优的拟合函数,使得实际数据点与拟合函数之间的误差最小。这个问题可以用非线性规划算法求解,其中目标函数为误差平方和,即 min Σ(yi - axi^2 - bxi - c)^2。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值