一、摘花生
传统的 DP 思考方式:阶段、决策、最优子结构、无后效性等,较抽象
全新的思考方式——闫氏 DP 分析法 (从集合角度分析 DP 问题):
每次考虑两个部分:
- 状态表示:
(1) 集合
(2) 属性:最大值/最小值/数量 - 状态计算——集合的划分:依据“最后一步”划分
集合划分原则:不重复(仅在计算数量时需满足),不漏(必须满足)
在考虑状态计算时有两种方法:一个状态由哪些状态更新,或者一个状态可以更新哪些状态。
DP 的计算顺序问题:状态之间可以画成图的形式,可能有交叉,需要按照拓扑序计算,对于线性 DP 来说,一般只要按照下标某一顺序循环即可。
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int w[N][N], f[N][N];
int main(){
int T;
scanf("%d", &T);
while (T --){
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
scanf("%d", &w[i][j]);
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]) + w[i][j];
printf("%d\n", f[n][m]);
}
return 0;
}
二、最低通行费
由于从左上走到右下的最少步数就是 2 N − 1 2N-1 2N−1,因此不难发现题目中 “必须在 ( 2 N − 1 ) (2N−1) (2N−1) 个单位时间穿越出去” 可推出 “只能向左走或向下走,不能走回头路”,即本题与上题在模型上相同。
由于本题求的是最小值,且起点是 ( 1 , 1 ) (1,1) (1,1),因此在第一行与第一列会有边界问题,需要特判一下。
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 110, INF = 1e9;
int n;
int f[N][N], w[N][N];
int main(){
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
scanf("%d", &w[i][j]);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
if (i == 1 && j == 1) f[i][j] = w[i][j]; //左上角
else{
f[i][j] = INF;
if (i > 1) f[i][j] = min(f[i][j], f[i - 1][j] + w[i][j]); //只有不在第一行时才能从上面过来
if (j > 1) f[i][j] = min(f[i][j], f[i][j - 1] + w[i][j]); //只有不在第一列时才能从左边过来
}
printf("%d", f[n][n]);
return 0;
}
三、方格取数
本题需要走两次,可将走一次的做法进行推广:
只走一次:
f
[
i
,
j
]
f[i,j]
f[i,j] 表示所有从
(
1
,
1
)
(1,1)
(1,1) 走到
(
i
,
j
)
(i,j)
(i,j) 的路径的最大值;
走两次:
f
[
i
1
,
j
1
,
i
2
,
j
2
]
f[i_1,j_1,i_2,j_2]
f[i1,j1,i2,j2] 表示所有从
(
1
,
1
)
(1,1)
(1,1) 分别走到
(
i
1
,
j
1
)
(i_1,j_1)
(i1,j1) 和
(
i
2
,
j
2
)
(i_2,j_2)
(i2,j2) 的路径的最大值。
此外还需处理 “同一个格子不能被重复选择” 的限制:
因此四维状态表示
f
[
i
1
,
j
1
,
i
2
,
j
2
]
f[i_1,j_1,i_2,j_2]
f[i1,j1,i2,j2] 还不够,状态还需额外开一维来记录路径上经过的所有点,判断是否有重合。
发现只有在
i
1
+
j
1
=
i
2
+
j
2
i_1+j_1=i_2+j_2
i1+j1=i2+j2 时,两条路径的格子才可能重合。
由于两次路径之间互不影响 (仅需特判是否经过同一格子),所以两条路径可以一起走:
f
[
k
,
i
1
,
i
2
]
f[k,i_1,i_2]
f[k,i1,i2] 表示当前格子的横纵坐标之和,所有从
(
1
,
1
)
(1,1)
(1,1) 分别走到
(
i
1
,
k
−
i
1
)
(i_1,k-i_1)
(i1,k−i1) 和
(
i
2
,
k
−
i
2
)
(i_2,k-i_2)
(i2,k−i2) 的路径的最大值,在
i
1
=
i
2
i_1=i_2
i1=i2 两条路径通过同一格子,需要特殊处理,只加一次。
此外,还可证明最优解一定不会走到相同的格子。证明详见:AcWing 275. 证明传纸条为何可以使用方格取数的代码 by vlehr
状态计算——按最后一步:“下下”,“下右”,“右下”,“右右”,共四种情况。
代码实现:
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 15;
int n;
int w[N][N], f[N * 2][N][N];
int main(){
scanf("%d", &n);
int a, b, c;
while (cin >> a >> b >> c, a || b || c) w[a][b] = c;
for (int k = 2; k <= n + n; k ++)
for (int i1 = 1; i1 <= n; i1 ++)
for (int i2 = 1; i2 <= n; i2 ++){
int j1 = k - i1, j2 = k - i2;
if (j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n){
int t = w[i1][j1];
if (i1 != i2) t += w[i2][j2];
int &x = f[k][i1][i2];
x = max(x, f[k - 1][i1 - 1][i2 - 1] + t);
x = max(x, f[k - 1][i1 - 1][i2] + t);
x = max(x, f[k - 1][i1][i2 - 1] + t);
x = max(x, f[k - 1][i1][i2] + t);
}
}
printf("%d", f[n + n][n][n]);
}
方格取数的终极版本是 k k k 取方格数,其标准做法是最小费用流。
- 那么为什么一个动态规划问题终极版用费用流来做呢?
- 本质上,DP是图论的子集。大部分DP问题可以转化为图论问题,而当图是拓扑图时,图论也可以转化为DP问题。
四、传纸条
本题要求先从左上走到右下,再从右下走回左上,而走的方向不影响最终结果,因此可将两次路径都看作从左上走到右下,便与上题模型相同了。
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 55;
int n, m;
int w[N][N], f[N * 2][N][N];
int main(){
scanf("%d %d", &m, &n);
for (int i = 1; i <= m; i ++)
for (int j = 1; j <= n; j ++)
scanf("%d", &w[i][j]);
for (int k = 2; k <= m + n; k ++)
for (int i1 = 1; i1 <= m; i1 ++)
for (int i2 = 1; i2 <= m; i2 ++){
int j1 = k - i1, j2 = k - i2;
if (j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n){
int t = w[i1][j1];
if (i1 != i2) t += w[i2][j2];
int &x = f[k][i1][i2];
x = max(x, f[k - 1][i1 - 1][i2 - 1] + t);
x = max(x, f[k - 1][i1][i2 - 1] + t);
x = max(x, f[k - 1][i1 - 1][i2] + t);
x = max(x, f[k - 1][i1][i2] + t);
}
}
printf("%d", f[n + m][m][m]);
return 0;
}