luogu P1002 [NOIP2002 普及组] 过河卒
题目链接
难度:普及-
一. 思路简述:
本题对于我这种做题太少的菜鸟来说看上去很吓人,但实际分析可以发现并不难。本题状态转移方程实际很简单。难点反而是如何正确、简单地写出马的可达点以及动规存储的更新细节问题。我在做题时采用了最笨的方法并且成功掉入了细节陷阱中,果然还是要多练题目。
1. “状态”的初步确定
本题“状态”的确定很明确,状态就是“x"和“y”的坐标值。“状态”下的值就是到达(n,m)点的路径数。
2. 子问题分解
子问题分解同样不难,由题意,(n,m)点可通过(n-1,m)点向下或(n,m-1)点向右来到达。因而分解的子问题就是“相邻的上面一点和左面一点的路径数”,而原问题的解是两者的和。
3. 状态转移中的约束限制与特殊状态的表示、初始化
本题真正麻烦的地方在于状态转移中的约束限制。
首先,位于边界上的点可能在右侧或上方没有相邻点,这很好处理。
其次,可以发现马的可达点是不可经过的,必须用一种方式表达这种状态。这里不同的表示方法会影响编程的难易程度。最长采用的就是特殊值标记法。并且这种特殊状态不能参与到状态转移,在递推时必须跳过。另外这种特殊状态会影响相邻状态的更新,这也要在状态转移方程中分段表示出来。
使用特殊值标记法必然涉及特殊值的初始化问题,而本题中需要初始化的点的位置相对复杂些,我用的方法很笨拙,可以优化。
4. 初始值的易错点
从(0,0)到(0,0)路径数可设为1。
二. 最初的代码:
采用“我为人人”型
#include <bits/stdc++.h>
#define LEN 30
using namespace std;
long dp[LEN][LEN]={0};//数据大小不要再错了!
void control(int n,int m,int a, int b){
for (int i = 0; i <= n;i++)//多组输入初始化
for (int j = 0; j <= m;j++)
{
dp[i][j] = 0;
}
for (int i = a - 2;i <= a + 2; i += 4)//标注特殊状态点,非常笨拙
for (int j = b - 1;j <= b + 1; j += 2)
if(i >= 0 && i <= n && j >= 0 && j <= m)
dp[i][j] = -1;
for (int i = b - 2;i <= b + 2;i+=4)
for (int j = a - 1;j <= a + 1;j+=2)
if (i >= 0 && i <= m&&j >= 0 && j <= n)
dp[j][i] = -1;
dp[a][b] = -1;
}
int main(){
int n, m, a, b;
while(cin>>n>>m>>a>>b){
control(n, m, a, b);
if(dp[0][0]!=-1)
dp[0][0] = 1;
for (int i = 0; i <= n;i++)
for (int j = 0; j <= m;j++)
{
if (i<n&&dp[i][j]!=-1&&dp[i+1][j]!=-1)//处理边界问题,注意为防止越界充分利用的短路特性
dp[i+1][j] += dp[i][j];
if (j<m&&dp[i][j]!=-1&&dp[i][j+1]!=-1)
dp[i][j+1] += dp[i][j];
}
if(dp[n][m]==-1)//最后还要处理特殊标注问题
dp[n][m] = 0;
cout << dp[n][m] << endl;
}
return 0;
}
三. 优化方法
-
- 为了避免涉及边界值的问题,我们可以将零点设在(1,1)而不是(0,0)。除此之外,涉及到方法的特殊需求,边界也可以相应扩大。
这是一种很容易想到的处理方法,让我情不自禁的想到在数字图像处理和卷积神经网络中图像边界问题的最基础的处理方法——填充padding。总之就是构造一个新边界。
- 为了避免涉及边界值的问题,我们可以将零点设在(1,1)而不是(0,0)。除此之外,涉及到方法的特殊需求,边界也可以相应扩大。
-
- 在标记特殊状态时,我们可以发现马的可达点的绝对坐标是复杂的,但相对坐标整体上确实有明确规律的,是固定不变的。但另一方面八除自身为的八个可达点很难直接用循环处理,但考虑到情况很少,就可以使用枚举法,将八个点的相对坐标存在方便循环的数组中,再在绝对坐标上减去相对坐标就行了。这样即方便多了。
简化问题的方法就在于充分利用不变的规律性。面对多种情况时,这种规律性未必就是单一的数学表达式,很多时候虽然几种情况之间没有直接关联,但情况本身具有不变性规律。可分别利用各自的不变性枚举出来即可。要记住这个方法。
- 在标记特殊状态时,我们可以发现马的可达点的绝对坐标是复杂的,但相对坐标整体上确实有明确规律的,是固定不变的。但另一方面八除自身为的八个可达点很难直接用循环处理,但考虑到情况很少,就可以使用枚举法,将八个点的相对坐标存在方便循环的数组中,再在绝对坐标上减去相对坐标就行了。这样即方便多了。
-
- 特殊状态的标记可以使用单独的数组,可以采用bool数组,非常省空间。而标记采用单独数组后,dp数组就可以采用滚动数组的方法来优化空间。两个技巧结合就能将空间复杂度从 O ( n 2 ) O(n^2) O(n2)降到 O ( n ) O(n) O(n)
const int fx[] = {0, -2, -1, 1, 2, 2, 1, -1, -2};
const int fy[] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
//枚举出马可以走到相对坐标位置
s[mx][my] = 1;//标记马的绝对坐标位置
for(int i = 1; i <= 8; i++)
s[mx + fx[i]][my + fy[i]] = 1;
四. 优化后的代码:
1. 没采用滚动数组
#include <bits/stdc++.h>
#define LEN 30
using namespace std;
long dp[LEN][LEN]={0};
const int fx[] = {0, -2, -1, 1, 2, 2, 1, -1, -2};
const int fy[] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
bool s[40][40]; //判断这个点有没有马拦住
void control(int n,int m,int a, int b){
for (int i = 0; i <= n;i++)
for (int j = 0; j <= m;j++)
dp[i][j] = 0;
s[a][b] = 1;//标记马的绝对坐标位置
for(int i = 1; i <= 8; i++)
s[a + fx[i]][b + fy[i]] = 1;
}
int main(){
int n, m, a, b;
while(cin>>n>>m>>a>>b){
n +=2; m += 2; a += 2; b += 2;
//坐标+2以防越界
control(n, m, a, b);
dp[2][1] = 1;
for (int i = 2; i <= n;i++)
for (int j = 2; j <= m;j++)
{
if(s[i][j]) continue; // 如果被马拦住就直接跳过
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
//状态转移方程
}
cout << dp[n][m] << endl;
}
return 0;
}
2. 大佬的代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
// 快速读入
template <class I>
inline void read(I &num){
num = 0; char c = getchar(), up = c;
while(!isdigit(c)) up = c, c = getchar();
while(isdigit(c)) num = (num << 1) + (num << 3) + (c ^ '0'), c = getchar();
up == '-' ? num = -num : 0; return;
}
template <class I>
inline void read(I &a, I &b) {read(a); read(b);}
template <class I>
inline void read(I &a, I &b, I &c) {read(a); read(b); read(c);}
const int fx[] = {0, -2, -1, 1, 2, 2, 1, -1, -2};
const int fy[] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
int bx, by, mx, my;
ll f[40]; //这次只需要一维数组啦
bool s[40][40];
int main(){
read(bx, by); read(mx, my);
bx += 2; by += 2; mx += 2; my += 2;
f[2] = 1; //初始化
s[mx][my] = 1;
for(int i = 1; i <= 8; i++) s[mx + fx[i]][my + fy[i]] = 1;
for(int i = 2; i <= bx; i++){
for(int j = 2; j <= by; j++){
if(s[i][j]){
f[j] = 0; // 还是别忘了清零
continue;
}
f[j] += f[j - 1];
//全新的 简洁的状态转移方程
}
}
printf("%lld\n", f[by]);
return 0;
}