动态规划
动态规划经常用于求解具有某种最有性质的问题,但是一些与时间无关的静态规划(如线性规划、非线性规划),只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解
基本思路:
将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。为了避免大量的重复计算,节省时间,我们用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
线性DP指状态之间有线性关系的动态规划问题。
下面我们来看几道经典的线性DP模板题
例题1:数字三角形
题目描述
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数 n,表示数字三角形的层数
接下来 n行,每行包含若干整数,其中第 i行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
样例
样例输入 :
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出 :
30
解题思路:
首先我们需要用个数组g[n][n]将数字三角形存储起来,我们可以看出最终路径是由一层一层线性关系得到的。然后我们还要想办法找到从顶端到底层的最长的那条路径,这时我们想到用一个集合来装从顶端到各点的最长路径,再从底层数字中选出最大值即为所求答案。
f[i][j] 表示所有从(1, 1) 开始走, 走到(i, j) 这个位置的所有走法的集合 ,属性为max。
实现代码如下:
#include <iostream>
#include <stdlib.h>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int g[N][N], f[N][N];
int main()
{
cin >> n;
for (int i = 0; i <= n; i ++ )
for (int j = 0; j <= n; j ++ )
f[i][j] = -INF;//给数组赋初值,无穷小
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= i; j ++ )
cin >> g[i][j];//存储数字三角形
f[1][1] = g[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]) + g[i][j];//找从g[1][1]到g[i][j]
的最长路径值
int res = -INF;
for (int i = 1; i <= n; i ++ )
res = max(res, f[n][i]); // 在最底层找最大值
cout << res << endl;
return 0;
}
例题2:最长上升子序列
题目描述
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N
第二行包含 N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
样例
样例输入
7
3 1 2 1 8 5 6
样例输出
4
看题目我们又想到创建一个集合: 所有以 a[i] 结尾的上升子序列的集合,属性为 max。
实现代码:
#include <iostream>
using namespace std;
const int N = 1010;
int a[N];
int f[N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
for (int i = 1; i <= n; i ++ )
{
f[i] = 1; // 算f[i] 只会用到 f[1 ~ i - 1];
// f[1] 的时候 f[0] 已经算过
// f[2] 的时候 f[1] 已经算过
// f[3] 的时候 f[1 ~ 2] 已经算过
// f[4] 的时候 f[1 ~ 3] 已经算过
for (int k = 1; k < i; k ++ )
if (a[k] < a[i]) f[i] = max(f[i], f[k] + 1); //找以a[i]结尾的上升子序列最大长度
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);
cout << res << endl;
return 0;
}
例题3:最长公共子序列
题目描述
给定两个长度分别为 N和 M的字符串 A 和 B,求既是 A 的子序列又是 B的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N和 M。
第二行包含一个长度为 N的字符串,表示字符串 A。
第三行包含一个长度为 M的字符串,表示字符串 B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
样例
样例输入
4 5
acbd
abedc
样例输出
3
解题思路:
创建一个集合f[i, j] :所有在a 的 前 i 个字母出现,在 b 的前 j 个字母出现 的公共子序列的集合,属性 为Max
分析f[i, j] 的情况:
1. a[i] 不在公共子序列中, b[j] 不在公共子序列中
f[i, j] = f[i - 1, j - 1];
2. a[i] 不在公共子序列, b[j] 在
f[i, j] = f[i - 1, j]
3. a[i] 在, b[j] 不在
f[i, j] = f[i, j - 1];
4. a[i] 在, b[j] 在
if (a[i] == b[j])
f[i, j] = f[i - 1, j - 1] + 1;
初始化(边界): f[0, i] = 0, f[i, 0] = 0;
实现代码如下:
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int f[N][N];
char a[N], b[N];
int main()
{
cin >> n >> m;
cin >> a + 1 >> b + 1;
for (int i = 0; i <= n; i ++ ) f[i][0] = 0;
for (int i = 0; i <= m; i ++ ) f[0][i] = 0;
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 - 1][j - 1] + 1, f[i][j]);
}
cout << f[n][m] << endl;
return 0;
}