5.2 动态规划 | 线性dp、区间dp
这是我的一个算法网课学习记录,道阻且长,好好努力
线性dp
线性动态规划,是较常见的一类动态规划问题,其是在线性结构上进行状态转移,这类问题不像背包问题、区间DP等有固定的模板。
线性动态规划的目标函数为特定变量的线性函数,约束是这些变量的线性不等式或等式,目的是求目标函数的最大值或最小值。
数字三角形
例题:AcWing 898. 数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
AcWing 898. 数字三角形_寒夜点孤灯的博客-CSDN博客
思考方式和之前的动态规划的题目是一样的,从状态表示(集合、属性)以及状态计算(状态转移)两个方面来思考。
Ans
#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()
{
// 输入
scanf("%d", &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 ++ ) // 从第2行开始
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;
}
最长上升子序列
例题:AcWing 895. 最长上升子序列
给定一个长度为N的数列(w[N]),求数值严格单调递增的子序列的长度最长是多少。
分类标准 枚举最后一位 然后进行dp
时间复杂度
O(n2) = 状态数n * 转移数n
Ans
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int 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; // f[i]只有其一个数
for (int j = 1; j < i; j ++ )
if (a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);
printf("%d\n", res);
return 0;
}
如果要记录序列结果 将状态存下来就好了
Ans_saved
#include <iostream>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N], g[N]; // g用于记录是由哪个数传过来的
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])
if (f[i] < f[j] + 1)
{
g[i] = j;
f[i] = f[j] + 1;
}
}
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];
}
return 0;
}
最长公共子序列
例题:AcWing 897. 最长公共子序列
给定一个长度为N的数列(w[N]),求数值严格单调递增的子序列的长度最长是多少。
f(i - 1, j - 1))
表示A
的前i - 1
个字符与B
的j - 1
个字符的最长公共子序列
f(i - 1, j)
表示A
的前i - 1
个字符与B
前j
个字符的最长公共子序列,这是存在包含关系的。
Ans
#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); // 对于11这类的状态转移方程
}
printf("%d\n", f[n][m]);
return 0;
}
区间dp
石子合并
例题:AcWing 282. 石子合并
设有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。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
Ans
#include <iostream>
#include <cstring>
using namespace std;
const int N = 310;
int a[N], s[N];
int f[N][N];
int main()
{
int n;
cin >> n;
// 求前缀和
for (int i = 1; i <= n; i++)
{
cin >> a[i];
s[i] += s[i - 1] + a[i];
}
// 初始化正无穷
memset(f, 0x3f, sizeof f);
// 进行区间dp
for (int len = 1; len <= n; len++) // len表示区间长度
{
for (int i = 1; i + len - 1 <= n; i++)
{
int j = i + len - 1; // 自动获取右端点
if (len == 1)
{
f[i][j] = 0; // 边界进行初始化
continue;
}
for (int k = i; k <= j - 1; k++) // 必须满足的条件是k + 1 <= j
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
}
}
cout << f[1][n] << endl;
return 0;
}
对于这个问题的更详细拓展可以看看这篇题解: