最近在洛谷刷题,接触了不少dp(动态规划)类型的题目。这类题目,我往往都直接暴力递归,一气呵成,结果几乎没有不超时的。没办法,只好用dp。dp的类型有很多,像之前提到过的背包问题。这次,我来讲一讲矩阵类型的dp。
当然,这也要结合我的个人经历。经常会碰到这种类型的问题,输入一个行数m、列数n的矩阵,矩阵的每一个元素都是一个数字,然后从矩阵的左上角到右下角,每次只能向下或向右移动,求路线上经过的数字和的最大值。看到这,估计你已经有感觉了,虽然具体的题目会有所不同,但如果仔细观察,就会发现他们非常相似。
放在以前,我都是用DFS暴力递归来破解,一般代码如下:
# include <iostream>
# include <algorithm>
# define maxn 55
using namespace std;
int m, n; //矩阵行列数
int sum = 0; //保存最大数字和
int p[maxn][maxn] = { {0} }; //保存矩阵数据
void DFS(int x,int y,int s)
{
if (x == m && y == n) { //到达右下角
sum = max(sum, s);
return;
}
if (x + 1 <= m) {
DFS(x + 1, y, s + p[x + 1][y]);
}
if (y + 1 <= n) {
DFS(x, y + 1, s + p[x][y + 1]);
}
}
int main()
{
cin >> m >> n;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
cin >> p[i][j];
}
}
DFS(1, 1, p[1][1]);
cout << sum << endl;
return 0;
}
现在,时代不同了(划掉),所以我们要用dp来写,避免超时。这属于比较基本的dp类型,简单来分析一下,到达任意一点的最大数字和,可由其左边一个点、上边一个点的最大数字和来决定,也就是状态转移方程f[i][j]=max(f[i-1][j],f[i][j-1])+p[i][j],找到了状态转移方程,其他就简单了。
一般代码如下:
//状态转移方程f[i][j]=max(f[i-1][j],f[i][j-1])+p[i][j]
# include <iostream>
# include <algorithm>
# define maxn 55
using namespace std;
int m, n;
int p[maxn][maxn] = { {0} };
int f[maxn][maxn] = { {0} };
int main()
{
cin >> m >> n;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
cin >> p[i][j];
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
f[i][j] = max(f[i - 1][j], f[i][j - 1]) + p[i][j];
}
}
cout << f[m][n] << endl;
return 0;
}
说到这,我们只是讲了一种比较简单基本的情况。要先弄明白上面,才能解决更难的类型。下面就拿出我刷过的题目来升级一下难度。
洛谷P1004 [NOIP2000 提高组] 方格取数
设有 N×N 的方格图(N≤9),我们将其中的某些方格中填入正整数,而其他的方格中则放入数字 0。如下图所示(见样例):
A
0 0 0 0 0 0 0 0
0 0 13 0 0 6 0 0
0 0 0 0 7 0 0 0
0 0 0 14 0 0 0 0
0 21 0 0 0 4 0 0
0 0 15 0 0 0 0 0
0 14 0 0 0 0 0 0
0 0 0 0 0 0 0 0
B
某人从图的左上角的 A 点出发,可以向下行走,也可以向右走,直到到达右下角的 B 点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字 0)。此人从 A 点到 B点共走两次,试找出 2条这样的路径,使得取得的数之和为最大。
输入格式
输入的第一行为一个整数 N(表示N×N 的方格图),接下来的每行有三个整数,前两个表示位置,第三个数为该位置上所放的数。一行单独的 0表示输入结束。
输出格式
只需输出一个整数,表示 2条路径上取得的最大的和。
我第一次写,当然用的是DFS啦(bushi)。结果居然没超时!应该是矩阵比较小的缘故。这里,我DFS的思想是,既然是两条路径,而且都从A点到B点,所以可以把两条路径连接起来视作一个环,也就是先从A到B,标记取走的数,再调头从B到A(区别在于,这次是向上或向左移动)。以下是我DFS的代码:
# include <iostream>
# include <algorithm>
using namespace std;
typedef struct Maps {
int row;
int line;
int num;
}Maps;
int N, M = 0;
int k = 0;
int mx = 0;
Maps a[100]; //保存所有不为0的点
int p[10][10] = { {0} }; //记录每个不为0的点是否已被取走
void DFS(int x, int y, int t, int sum, int d)
{
if (t == 0 && d == -1) { //绕一圈结束
if (sum > mx) {
mx = sum;
}
return;
}
if (d == 1) { //属于前进方向上(第一趟)
if (t == k) { //已前进到尽头,准备回头
if (sum < M) { //剪枝,如果第一趟取走的数还不如方格里面最大的数大,则除去这种情况
return;
}
DFS(N, N, t - 1, sum, -1); //开始回头
}
else if (a[t].row >= x && a[t].line >= y && !p[a[t].row][a[t].line]) {
p[a[t].row][a[t].line] = 1;
DFS(a[t].row, a[t].line, t + 1, sum + a[t].num, 1); //选择这一格
p[a[t].row][a[t].line] = 0;
DFS(x, y, t + 1, sum, 1); //不选这一格
}
else {
DFS(x, y, t + 1, sum, 1); //不选这一格
}
}
if (d == -1) { //回头方向(第二趟)
if (a[t].row <= x && a[t].line <= y && !p[a[t].row][a[t].line]) {
p[a[t].row][a[t].line] = 1;
DFS(a[t].row, a[t].line, t - 1, sum + a[t].num, -1); //选择这一格
p[a[t].row][a[t].line] = 0;
DFS(x, y, t - 1, sum, -1); //不选这一格
}
else {
DFS(x, y, t - 1, sum, -1); //不选这一格
}
}
}
int main()
{
cin >> N;
Maps b;
cin >> b.row >> b.line >> b.num;
while (b.row + b.line + b.num != 0) {
a[k] = b;
M = max(M, b.num);
cin >> b.row >> b.line >> b.num;
k++;
}
DFS(0, 0, 0, 0, 1);
cout << mx << endl;
return 0;
}
当然,我们不能就这样满足,因为一旦矩阵规模继续加大,这个解法肯定要超时了。所以我开始思考dp的解法。这道题和传纸条十分类似,传纸条也是比较经典的问题,这里我就不多说了,详情可以见洛谷P1006 [NOIP2008 提高组] 传纸条。我们来分析一下这题的解法。这题要求的是两条路径,而之前我们已经讲过了一条路径的dp解法,所以,我们要把状态转移方程从一条路径的类型扩展到两条。也就是f[i][j][k][l] = max(f[i - 1][j][k - 1][l], f[i - 1][j][k][l - 1], f[i][j - 1][k - 1][l], f[i][j - 1][k][l - 1]) + a[i][j] + a[k][l]。为什么是这样呢?因为是两个点,所以采用了4维数组。另外,因为两条路线是同时移动的,i,j代表第一条路线上的点,k,l代表第二条路线上的点,所以同一时刻,i,j其中一个发生变化,k,l也是其中一个发生变化,也就产生了4种情况。写到这,还有一点要注意,两条路线可能发生重合,所以要去除重复点。具体的代码如下:
# include <iostream>
# include <algorithm>
using namespace std;
int N;
int a[10][10];
int f[10][10][10][10];
int max4(int x1, int x2, int x3, int x4) //四个数中求最大值
{
int t = 0;
if (x1 > t) {
t = x1;
}
if (x2 > t) {
t = x2;
}
if (x3 > t) {
t = x3;
}
if (x4 > t) {
t = x4;
}
return t;
}
int main()
{
cin >> N;
int x, y, z;
cin >> x >> y >> z;
while (x + y + z != 0) {
a[x][y] = z;
cin >> x >> y >> z;
}
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
for (int k = 1; k <= N; k++) {
for (int l = 1; l <= N; l++) {
f[i][j][k][l] = max4(f[i - 1][j][k - 1][l], f[i - 1][j][k][l - 1], f[i][j - 1][k - 1][l], f[i][j - 1][k][l - 1]) + a[i][j] + a[k][l];
if (i == k && j == l) { //去除重复点
f[i][j][k][l] -= a[i][j];
}
}
}
}
}
cout << f[N][N][N][N] << endl;
return 0;
}
以上主要是我的个人体验,dp的类型还有很多,因为比较抽象,所以这类问题往往都比较难。本人也还在进阶的途中,各位,一起努力吧!