概述
上节课我们学习了递推(新手建议看),这节课我们来学习和递推很类似的动态规划,即DP。
基本思想
- 问题的答案可以通过子问题的答案推出。
- 让我们先回顾一下递归的基本思想:例如问题的参数为(10,11),我们可以将他化解为几个子问题,这几个子问题再继续化解,直到所有子问题都是有解的,在自底向上累加。
- 动态规划其实就是记忆化搜索(不会的小伙伴可以忽略),每次将子问题的解存到一个表里,以后需要直接调用就可以,不用每次都继续向下分。
- 目前,我们可以暂时认为动态规划就是递推。
术语
- 状态
子问题。 - 决策
在目前状态下的所有选择,比如在迷宫里往那个方向走。 - 状态转移方程
关于两个状态的等式,即如何由一个状态的解求出另一个状态的解。 - 策略
即可行方案。 - 最优子结构
满足全局最优化的话必须所有局部最优化。 - 无后效性
后面状态的解不受目前状态的影响。
使用条件
- 最优子结构
- 无后效性
实现步骤
- 状态定义
- 求出状态转移方程
- 找到边界条件
题目1:蒟蒻君逛超市
题目
超市举行活动,从(1, 1)走到(n, m),只能往下或者往右走。蒟蒻君每到达一个格子,就可以拿到一定的money。请问,蒟蒻君最多能拿到多少块钱?例如:
可以这么走:
分析
设aij为(i, j)的拿钱数。
- 状态定义
设dpij为从(1, 1)走到(i, j)的最多拿钱数。 - 状态转移方程
每个格子(除了第一排和第一列的)可以从左边或者上边过来,因此:
dpij = max(dpi-1j, dpij-1) + aij - 边界条件
(1, 1)是初始位置,不需要走。
第一排的只能从左边过来,此时:
dpij = dpij-1 + aij
第一列的只能从上边过来,此时:
dpij = dpi-1j + aij
实现
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int a[N][N], dp[N][N];
int main() {
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> a[i][j];
if (i == 1 && j == 1) {
dp[i][j] = a[i][j];
} else if (i == 1) {
dp[i][j] = dp[i][j - 1] + a[i][j];
} else if (j == 1) {
dp[i][j] = dp[i - 1][j] + a[i][j];
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + a[i][j];
}
}
}
cout << dp[n][m] << '\n';
return 0;
}
题目2:蒟蒻君干饭
题目
蒟蒻君去爬山啦~爬到山顶后,很饿。。
这座山共有n层,第i层有i个饭店。蒟蒻君很自律,每层只去一个饭店。
在第i层第j个饭店可以吃aij斤食物(蒟蒻君都吃了),到达第(i + 1)层(第n层就到底了,不能再去饭店)的第i个和第(i + 1)个饭店。
例如:
在这座山中,可以这样走:
分析
- 状态定义
设dpij为走到第i层第j个饭店能吃的最大斤数。 - 状态转移方程
- 所有饭店都可以从正上方或者左上方的饭店走过来(下标从1开始,不用考虑越界),即:
dpij = max(dpi-1j, dpi-1j-1) + aij - 边界条件*
不需要考虑。
实现
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int a[N][N], dp[N][N];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
cin >> a[i][j];
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j];
}
}
// 答案 = 最下层所有位置的最大值中的最大值
int res = ~0x3f3f3f3f;
for (int i = 1; i <= n; ++i) {
res = max(res, dp[n][i]);
}
cout << res << '\n';
return 0;
}
题目3:[NOIP2002普及组]过河卒
这节课我们学习了最简单的线性DP,下节课我们来学习线性DP的经典问题。