矩阵型dp(方格取数、传纸条)

最近在洛谷刷题,接触了不少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的类型还有很多,因为比较抽象,所以这类问题往往都比较难。本人也还在进阶的途中,各位,一起努力吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值