洛谷——P1002 过河卒

原题链接

一道比较基础的动态规划的题目,这里细致的讲解一下如何得到状态转移方程以及如何对空间进行压缩。

首先,不难想到,用一个二维数组dp表示状态,那么dp[i][j]就可以表示到达第i行第j列所有的方案总数。因为棋子到达(i,j)位置的方式只有两种,一种是从(i-1,j)向下移动一格,一种是从(i,j-1)向右移动一格。所以,到达(i,j)的方案总数就是(i-1,j)和(i,j-1)的方案数相加。即状态转移方程:
dp[i][j]=dp[i-1][j]]+dp[i][j-1]
但是要注意,如果马无法到达(1,1)位置,那么边界dp[1][1]应该初始化为1。而实际上在循环的过程中dp[1][1]=dp[1][0]+dp[0][1]=0,这就直接导致最终输出的答案为零。因此需要加个特判或是直接写成dp[i][j] = max(dp[i - 1][j] + dp[i][j - 1], dp[i][j])

这里需要注意三点:其一是答案会很大,所以该数组声明为unsigned long long。

其二是原点在(0,0),这样会有越界的危险,所以将输入的四个整数统一加一,这样坐标原点就变成了(1,1)。

其三是马能够到达的点的方案数为0。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 30;
int m, n, x, y;
unsigned long long dp[N][N];
bool board[N][N];
// 储存马移动的八个方向
int dx[] = { -2,-1,1,2,2,1,-1,-2 };
int dy[] = { -1,-2,-2,-1,1,2,2,1 };
int main()
{
	ios_base::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> m >> n;
	cin >> x >> y;
	m += 1, n += 1, x += 1, y += 1;
	// 将马能够到达的地方用board数组记录
	board[x][y] = 1;
	for (int i = 0; i < 8; i++) {
		int tx = x + dx[i], ty = y + dy[i];
		if (tx >= 0 && tx <= m && ty >= 0 && ty <= n)
			board[tx][ty] = 1;
	}
	if (board[1][1]) {
		cout << 0;
		return 0;
	}
	// 边界初始化
	dp[1][1] = 1;
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= n; j++) {
			if (board[i][j])
				continue;
			else {
				dp[i][j] = max(dp[i - 1][j] + dp[i][j - 1], dp[i][j]);
			}
				
		}
	}
	cout << dp[m][n];
	return 0;
}

既然写出来了,我们就尝试一下是否可以压缩空间。
因为状态转移方程dp[i][j] = max(dp[i - 1][j] + dp[i][j - 1], dp[i][j]) 每一次只是用到上一行储存的值和这一行储存的值,那么是否可以数组行数是否可以只开两行呢?

答案是可以的。这里我们需要做的是把i,i-1用0或1代替,因为数组只有两行。因为i每次只增加1,也就是从偶数到奇数再到偶数再到奇数…那么可以尝试用奇偶性对i变换:对i取2的余数。这样,就可以把数组压缩至2行。

由于压缩空间,带来了一些问题。第一是要将马能够到达的位置在dp数组映射后的位置一定要清0,。因为空间压缩会导致dp[i%2][j]在更新前储存别的值,不一定为0。

第二是上文的状态转移方程dp[i][j] = max(dp[i - 1][j] + dp[i][j - 1], dp[i][j]) 这里不能够用max,原因也是空间压缩会导致dp[i][j]在更新前储存别的值,未必会比dp[i - 1][j] + dp[i][j - 1]小。所以对于特殊状态,也就是i=1和j=1的情况需要特判。当然,此时的状态转移方程就变成 dp[i % 2][j] = dp[(i - 1) % 2][j] + dp[i % 2][j - 1]

对于这个地方不太理解的,可以手动模拟一下测试用例,就会明白其中的原理。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 30;
int m, n, x, y;
unsigned long long dp[2][N];
bool board[N][N];
int dx[] = { -2,-1,1,2,2,1,-1,-2 };
int dy[] = { -1,-2,-2,-1,1,2,2,1 };
int main()
{
	ios_base::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> m >> n;
	cin >> x >> y;
	m += 1, n += 1, x += 1, y += 1;
	board[x][y] = 1;
	for (int i = 0; i < 8; i++) {
		int tx = x + dx[i], ty = y + dy[i];
		if (tx >= 0 && tx <= m && ty >= 0 && ty <= n)
			board[tx][ty] = 1;
	}
	if (board[1][1]) {
		cout << 0;
		return 0;
	}
	dp[1][1] = 1;
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= n; j++) {
			if (board[i][j])
				dp[i % 2][j] = 0;
			else if (i == 1 && j == 1)
				continue;
			else 
				dp[i % 2][j] = dp[(i - 1) % 2][j] + dp[i % 2][j - 1];
			

		}
	}
	cout << dp[m % 2][n];
	return 0;
}

那么我们是否能够用1维数组再次优化呢?答案是可以的。
上文写到dp[i%2][j]在更新前储存别的值

那么思考一下,储存的是什么值。很显然,是上一次计算的值,也就是到达(i-1,j)位置的方案总数。所以很容易就压缩到一维数组, 状态转移方程为:
dp[i]+=dp[i-1]

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 30;
int m, n, x, y;
unsigned long long dp[N];
bool board[N][N];
int dx[] = { -2,-1,1,2,2,1,-1,-2 };
int dy[] = { -1,-2,-2,-1,1,2,2,1 };
int main()
{
	ios_base::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> m >> n;
	cin >> x >> y;
	m += 1, n += 1, x += 1, y += 1;
	board[x][y] = 1;
	for (int i = 0; i < 8; i++) {
		int tx = x + dx[i], ty = y + dy[i];
		if (tx >= 0 && tx <= m && ty >= 0 && ty <= n)
			board[tx][ty] = 1;
	}
	if (board[1][1]) {
		cout << 0;
		return 0;
	}
	dp[1] = 1;
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= n; j++) {
			if (board[i][j])
				dp[j] = 0;
			else if (i == 1 && j == 1)
				continue;
			else
				dp[j] += dp[j - 1];
			

		}
	}
	cout << dp[n];
	return 0;
}

如果有帮助还请各位小伙伴们点个赞哦~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值