千里之行,始于足下,让我们从今天开始,走上通向神犇的道路!
同志们,加油!
任何一个伟大的思想,都有一个微不足道的开始。
声明:理解为重,代码次之!
前言
真没想到洛谷主题库题目的难度涨得这么快啊
o
r
z
\rm orz
orz。
这篇题解会讲述C/C++
的做法(Python
难一点就不会了😭)
思路分析
什么!!!标签竟有 动态规划,dp \colorbox{3498db}{\color{FFFFFF}动态规划,dp} 动态规划,dp!!!这么难!!!
仔细一看,其实这是一道小学奥数题,就是在许多交叉的线中找到到达终点的最短路线。老师可能教过例如“对角相连”的方法,但是这到底是为什么呢?让我们继续往下看。
思考最简单的方式,如果枚举往右或往下然后回溯搜索,就会超市,得到TLE
。先考虑一个简化版的问题:如果那个马不存在,从左上角到右下角一共有多少种走法?
定义一个二维数组
f
f
f,记录从原点走到坐标
(
i
,
j
)
(i,j)
(i,j) 的方法总数是
f
i
,
j
f_{i,j}
fi,j。当卒从起点开始,笔直往右或者笔直往下,无论走多远都只有唯一的一种走法(因为一旦偏移就再也回不去了),所以当
k
⩾
0
k\geqslant0
k⩾0 时,
f
0
,
k
=
f
k
,
0
=
1
f_{0,k}=f_{k,0}=1
f0,k=fk,0=1,这就是递推的初始条件。如何到点
(
1
,
1
)
(1,1)
(1,1) 呢?要么是从点
(
0
,
1
)
(0,1)
(0,1) 往下走一格,要么是从点
(
1
,
0
)
(1,0)
(1,0) 往右走一格,因此到
(
1
,
1
)
(1,1)
(1,1) 的方案数量就是到
(
0
,
1
)
(0,1)
(0,1) 的数量加上到
(
1
,
0
)
(1,0)
(1,0) 的数量,可得
f
1
,
1
=
f
0
,
1
+
f
1
,
0
f_{1,1} = f_{0,1} + f_{1,0}
f1,1=f0,1+f1,0。可以归纳得到状态转移方程(递推式):
f
i
,
j
=
f
i
−
1
,
j
+
f
i
,
j
−
1
(
i
>
0
,
j
>
0
)
f_{i,j}=f_{i-1,j}+f_{i,j-1}(i>0,j>0)
fi,j=fi−1,j+fi,j−1(i>0,j>0)
有了状态转移方程,有了初始条件,就可以求出完整的没有马的
f
f
f 数组的值了。
如果有些点因为马的把守而不能走呢?其实也没有什么区别,只不过没办法从马的控制点转移到下一个点罢了(换句话说,马的控制点上路径数全部清空)。此外,初始条件和递推范围也都一点变化,只需要 f 0 , 0 = 1 f_{0,0}=1 f0,0=1 即可,同时递推范围就是 i ⩾ 0 , j ⩾ 0 , i ⋅ j ≠ 0 i\geqslant0,j\geqslant0,i\cdot j\not=0 i⩾0,j⩾0,i⋅j=0。
代码演示
因为C
与C++
大体实现相同,所以不再写两份代码偷懒。
#include <iostream>
#define MAXN 22 // C++中推荐使用:const int MAXN = 22;
using namespace std;
long long f[MAXN][MAXN] = {0};
int ctr[MAXN][MAXN], n, m, hx, hy;
int d[9][2] = {{0, 0}, {1, 2}, {1, -2}, {-1, 2}, {-1, -2}, {2, 1}, {2, -1}, {-2, 1}, {-2, -1}};
// 马的控制范围相对于马的位置的偏移量
int main() {
cin >> n >> m >> hx >> hy; // C中用scanf("%d%d%d%d", &n, &m, &hx, &hy);
for (int i = 0; i < 9; i++) {
int tmpx = hx + d[i][0], tmpy = hy + d[i][1];
if (tmpx >= 0 && tmpx <= n && tmpy >= 0 && tmpy <= m) ctr[tmpx][tmpy] = 1;
// 判断是否在棋盘范围内并记录马的控制点
}
f[0][0] = 1 - ctr[0][0]; // 如果原点就是马的控制点,则初始路径就是0,否则是1
for (int i = 0; i <= n; i++)
for (int j = 0; j <= m; j++) {
if (ctr[i][j]) continue; // 若这个点是控制点,则跳过
if (i) f[i][j] += f[i - 1][j]; // 若不在横轴上就加上上面路径数
if (j) f[i][j] += f[i][j - 1]; // 若不在纵轴上就加上左边路径数
}
cout << f[n][m]; // 输出答案
return 0;
}
复杂度
时间复杂度
很明显,程序中有一个 2 2 2 层循环,每个循环都是 0 ∼ n 0 \sim n 0∼n 或 0 ∼ m 0 \sim m 0∼m,所以最终时间复杂度是 O ( n m ) O(nm) O(nm),可以在给定时间内通过。
空间复杂度
定义的最大的一个数组是 f f f,可以看出空间复杂度是 O ( M A X N ⋅ M A X N ) O(MAXN\cdot MAXN) O(MAXN⋅MAXN),也是 O ( n m ) O(nm) O(nm)。
优化
当然,空间复杂度是可以优化成
O
(
n
)
O(n)
O(n) 的,但这可以说是玄学且没有用,感兴趣的读者可以自己膜拜大神。
总结
学习递推开始必刷的题目,NOIP考题,稍有思维难度。