题解:洛谷 P7995 Walking Home
【题目来源】
洛谷:P7995 [USACO21DEC] Walking Home B - 洛谷
【题目描述】
奶牛 Bessie 正准备从她最喜爱的草地回到她的牛棚。
农场位于一个 \(N\times N\) 的方阵上\((2\le N\le 50)\),其中她的草地在左上角,牛棚在右下角。Bessie 想要尽快回家,所以她只会向下或向右走。有些地方有草堆(haybale),Bessie 无法穿过;她必须绕过它们。
Bessie 今天感到有些疲倦,所以她希望改变她的行走方向至多 \(K\) 次\((1\le K\le 3)\)。
Bessie 有多少条不同的从她最爱的草地回到牛棚的路线?如果一条路线中 Bessie 经过了某个方格而另一条路线中没有,则认为这两条路线不同。
【输入】
每个测试用例的输入包含 \(T\) 个子测试用例,每个子测试用例描述了一个不同的农场,并且必须全部回答正确才能通过整个测试用例。输入的第一行包含 \(T(1\le T\le 50)\)。每一个子测试用例如下。
每个子测试用例的第一行包含 \(N\) 和 \(K\)。
以下 \(N\) 行每行包含一个长为 \(N\) 的字符串。每个字符为 .,如果这一格是空的,或 \(H\),如果这一格中有草堆。输入保证农场的左上角和右下角没有草堆。
【输出】
输出 \(T\) 行,第 \(i\) 行包含在第 \(i\) 个子测试用例中 Bessie 可以选择的不同的路线数量。
【输入样例】
7
3 1
...
...
...
3 2
...
...
...
3 3
...
...
...
3 3
...
.H.
...
3 2
.HH
HHH
HH.
3 3
.H.
H..
...
4 3
...H
.H..
....
H...
【输出样例】
2
4
6
2
0
0
6
【核心思想】
-
问题分析:给定 \(N \times N\) 网格,从左上角 \((1,1)\) 到右下角 \((N,N)\),只能向下或向右走,避开草堆
H。要求改变方向至多 \(K\) 次(\(1 \leq K \leq 3\))的不同路径数。本质是一个带方向变化约束的网格路径计数问题。 -
算法选择:
- BFS状态扩展:每个状态包含位置 \((x, y)\)、上一步方向 \(d\)(\(0\) 为右,\(1\) 为下)、已转弯次数 \(\text{step}\)。用队列进行状态扩展,统计到达终点的合法状态数。
-
关键步骤:
- 初始化:将起点 \((1, 1)\) 以方向 \(-1\)(无方向)、转弯次数 \(0\) 入队
- BFS循环:
- 出队状态 \(\text{tp}\),若 \(\text{tp}.\text{step} > K\) 则剪枝跳过
- 若到达 \((N, N)\) 且 \(\text{step} \leq K\),\(\text{ans}++\)
- 方向扩展:枚举两个方向 \(i \in \{0, 1\}\)(右、下):
- 计算新坐标 \((\text{xx}, \text{yy}) = (\text{tp}.x + \text{dx}[i], \text{tp}.y + \text{dy}[i])\)
- 边界与障碍判定:越界或遇到
H则跳过 - 转弯次数更新:
- 若 \(\text{tp}.d = -1\)(起点):新状态转弯次数为 \(0\)
- 若 \(\text{tp}.d \neq i\)(方向改变):\(\text{step} + 1\)
- 若 \(\text{tp}.d = i\)(方向不变):\(\text{step}\) 不变
- 若新状态转弯次数 \(> K\) 则剪枝,否则入队
- 输出答案:每个测试用例输出 \(\text{ans}\)
-
时间/空间复杂度:
- 时间复杂度:\(O(T \cdot N^2 \cdot K)\),每个状态最多被访问 \(O(K)\) 次(不同转弯次数)
- 空间复杂度:\(O(N^2 \cdot K)\),队列存储状态
-
状态BFS带约束路径计数的核心思想:
- 状态维度设计:将"转弯次数"纳入状态,避免简单DP无法记录方向历史的问题
- 方向连续性利用:同方向移动不增加转弯次数,只有方向切换时才计数,保证状态转移正确性
- 早期剪枝:一旦转弯次数超过 \(K\) 立即停止扩展,减少无效状态
- BFS与DFS的选择:由于只需要计数而非求最短路径,BFS和DFS均可,但BFS按层扩展更符合"路径探索"直觉
- 适用于带约束网格路径、方向限制、状态空间搜索类问题
【解题思路】

【算法标签】
普及 #BFS-二维
【代码详解】
#include <bits/stdc++.h>
using namespace std;
int t, n, k;
int path[2500];
char a[55][55];
int dx[2]={0,1}, dy[2]={1,0}; // 定义向右和向下的偏离坐标
struct node { // 定义结构体,包含x、y坐标,上一次的方向,以及调整次数step
int x, y, d, step;
};
int main()
{
cin >> t; // 输入t
while (t--) { // 遍历t次询问
cin >> n >> k; // 输入n和k
for (int i=1; i<=n; i++) { // 记录n*n矩阵
for (int j=1; j<=n; j++) {
cin >> a[i][j];
}
}
queue<node> q; // 定义队列
node tp = {1, 1, -1, 0}; // 定义队头
q.push(tp); // 压入队列中
int ans=0; // 每轮询问初始化路线数量为0
while (!q.empty()) { // 当队列不为空
tp = q.front(); // 取出队头
q.pop();
if (tp.step>k) { // 剪枝:如果队头的调整次数大于k了,则继续下次循环
continue;
}
if (tp.x==n && tp.y==n) { // 如果队头坐标已经到达[n,n]
if (tp.step<=k) { // 如果次数调整次数小于等于k(肯定满足,因为上面特判了不满足)
ans++; // 路线数量加1
}
continue; // 继续下次循环
}
for (int i=0; i<2; i++) { // 分别计算向右和向下的路线
int xx=tp.x+dx[i], yy=tp.y+dy[i]; // 定义偏移后的坐标
if (xx<1 || xx>n || yy<1 || yy>n || a[xx][yy]=='H') continue; // 如果坐标越界或者遇到'H',则继续下次循环
node t; // 定义即将压入队列的t
if (tp.d == -1) { // 如果是队头坐标[1,1]
t = {xx, yy, i, 0}; // 则调整次数为0
} else if (tp.d != i) { // 否则,只要即将要调整的方向与上一次过来的方向不一致
t = {xx, yy, i, tp.step+1}; // 调整次数加1
} else { // 调整方向与上次过来的方向一致
t = {xx, yy, i, tp.step}; // 调整次数还是上次的调整次数
}
if (t.step>k) break; // 剪枝:如果此时t的调整次数大于k,就不用再继续了
q.push(t); // 将t压入队列中
}
}
cout << ans << endl; // 打印路线总数
}
return 0;
}
【运行结果】
7
3 1
...
...
...
2
3 2
...
...
...
4
3 3
...
...
...
6
3 3
...
.H.
...
2
3 2
.HH
HHH
HH.
0
3 3
.H.
H..
...
0
4 3
...H
.H..
....
H...
6
浙公网安备 33010602011771号