文章目录
一、问题说明
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问题多变,还需多见题型,重在思考与思路,体会如何找到一道题的状态表示,并根据数据关系找到状态计算方法。