P3848 [TJOI2007] 跳棋
是群友给的题目,但是用的知识我好多都没学到,所以只是复现一遍,群友们真是太强了
题目背景
本题可能为错题,目前数据只是随机生成的 的情况,测试数据和题解仅供参考。
在一个n×n的棋盘上,布满了0和1,如图(a)所示(n=7),为叙述方便,将0用字母表示,如图(b)。

题目描述
跳棋规则:
(1)从某个0格出发,可以向上,下,左,右4个方向连续越过若干个(至少1个)
1格而跳入下一个0格。如图(b)中从A出发,可跳到B,或者到E,但不能直接到K。在跳到B之后还可以继续跳到F;在跳到E之后可继续跳到F或K。直到不能再跳为止。
(2)每个0格只能到达一次,给出的起始点不能再到达,也不能越过。
跳过的距离为跳过1格个数加1,如从A到B,跳过距离为3,从B到F,跳过距离为2。
问 题: 当棋盘和起始点给出之后,问最远能跳的距离是多少?
如上图(b)中,从A出发,可跳过的路线不止一条,其中一条为:
A - B - F - L - K - E (可能不唯一)
3 2 3 3 3
它的距离为14。
输入格式
第一行三个整数 n(1≤n≤100),x,y(起点坐标,上图(b)中A处坐标为1,3)
接下来n行,每行n个数(0或1),数与数之间用一个空格分隔。
输出格式
一个整数,即最大可跳距离(若不能跳,输出0)。
输入输出样例 #1
输入 #1
4 3 21 0 1 01 1 1 10 0 1 01 1 0 1输出 #1
6说明/提示
:新增加一组 Hack 数据。
源码
#include <stdio.h>#include <stdlib.h>
#define MAXN 105int n;int grid[MAXN][MAXN]; // 存储地图,1表示障碍,0表示落脚点int visited[MAXN][MAXN]; // 标记数组,记录0是否被访问过int max_ans = 0; // 记录最大跳跃距离
// 方向数组:上,下,左,右int dx[4] = {-1, 1, 0, 0};int dy[4] = {0, 0, -1, 1};
// 检查坐标是否在地图范围内int isValid(int x, int y) { return x >= 0 && x < n && y >= 0 && y < n;}
/** * 深度优先搜索 * @param x 当前行坐标 * @param y 当前列坐标 * @param current_dist 从起点到当前的累计距离 */void dfs(int x, int y, int current_dist) { // 更新最大值 if (current_dist > max_ans) { max_ans = current_dist; }
// 尝试四个方向 for (int i = 0; i < 4; i++) { int nx = x + dx[i]; int ny = y + dy[i];
// 规则1: 必须紧邻着 '1' 才能起跳 // 如果越界或者紧邻的不是1,说明这个方向不能跳 if (!isValid(nx, ny) || grid[nx][ny] != 1) { continue; }
// 规则2: 连续越过所有的 '1' int count_ones = 0; while (isValid(nx, ny) && grid[nx][ny] == 1) { nx += dx[i]; ny += dy[i]; count_ones++; }
// 规则3: 落地检查 // 此时 nx, ny 指向了跳过一串1之后的位置 // 该位置必须在界内,必须是 '0',且必须未被访问过 if (isValid(nx, ny) && grid[nx][ny] == 0 && !visited[nx][ny]) { // 标记访问 visited[nx][ny] = 1;
// 距离计算:跳过的1的个数 + 1 int step_dist = count_ones + 1;
// 递归搜索 dfs(nx, ny, current_dist + step_dist);
// 回溯:取消标记,以便其他路径可以使用该点 visited[nx][ny] = 0; } }}
int main() { int start_x, start_y;
// 输入处理 if (scanf("%d %d %d", &n, &start_x, &start_y) != 3) { return 0; }
// 题目输入坐标是1-based,转换为C语言的0-based start_x--; start_y--;
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { scanf("%d", &grid[i][j]); visited[i][j] = 0; // 初始化访问数组 } }
// 起点处理:起点算作已访问,不能再次到达 visited[start_x][start_y] = 1;
// 开始搜索,初始距离为0 dfs(start_x, start_y, 0);
// 输出结果 printf("%d\n", max_ans); system("pause"); return 0;}宏定义
#define MAXN 105在具体作用上,功能上可以理解成const int,但两者在计算机底层原理上是不同的
-
const int N = 105;这是**“定义变量”**。计算机真的会在内存里开辟一个小格子,里面存着105,只不过给这个格子加了把锁,不许修改。 注意:在老版本的 C 语言(C89)中,全局数组的大小不能用const int变量,必须用具体的数字或宏定义。这也是为什么你看很多 C 语言代码都爱用#define。 -
#define N 105这是**“文本替换”**。它发生在编译之前。 编译器在看代码时,会像 Word 里的“查找替换”功能一样,把你代码里所有的N全部删掉,换成105。 它不占内存,它就是个单纯的数字。
定义方向数组
// 方向数组:上,下,左,右int dx[4] = {-1, 1, 0, 0};int dy[4] = {0, 0, -1, 1};我觉得这个程序中方向的转变也很巧妙(也可能是我太蠢了,没有想到
for (int i = 0; i < 4; i++) { int nx = x + dx[i]; int ny = y + dy[i];核心原理: 使用 dx 和 dy 两个常量数组定义四个移动方向的坐标增量。在搜索过程中,通过一个循环 for(i=0; i<4; i++) 配合 nx = x + dx[i] 和 ny = y + dy[i],即可优雅地遍历当前点周围的四个相邻位置,避免冗余的分支判断代码。
辅助函数(isValid)
// 检查坐标是否在地图范围内int isValid(int x, int y) { return x >= 0 && x < n && y >= 0 && y < n;}代码详解与知识点:
return x >= 0 && ...:- 知识点:边界检查(Boundary Check)与布尔逻辑。
- 作用:在 C 语言中访问数组
grid[-1][0]或grid[1000][0]会导致程序崩溃(Segmentation Fault)。这个函数是程序的“护栏”,确保我们在棋盘内移动。C 语言中非 0 即为真,返回 1 代表合法,0 代表越界。
核心逻辑 dfs 函数
这是程序的“大脑”,通过制定的规则,寻找路径。
/** * 深度优先搜索 * @param x 当前行坐标 * @param y 当前列坐标 * @param current_dist 从起点到当前的累计距离 */void dfs(int x, int y, int current_dist) { // 1. 更新最大值 if (current_dist > max_ans) { max_ans = current_dist; }
// 2. 尝试四个方向 for (int i = 0; i < 4; i++) { int nx = x + dx[i]; // nx: next x (下一步的x) int ny = y + dy[i]; // ny: next y (下一步的y)
// 3. 起跳检查(规则1) // 如果越界,或者紧邻的那一格不是 '1',就不能跳 if (!isValid(nx, ny) || grid[nx][ny] != 1) { continue; // 跳过当前方向,尝试下一个方向 }
// 4. 飞行过程(规则2) // 只要遇到 '1' 且不越界,就一直往前飞 int count_ones = 0; while (isValid(nx, ny) && grid[nx][ny] == 1) { nx += dx[i]; // 继续沿当前方向移动 ny += dy[i]; count_ones++; // 记录跨过了几个1 }
// 5. 落地检查(规则3) // 此时 nx, ny 指向了跳过一串1之后的位置 // 该位置必须在界内,必须是 '0',且必须没被访问过 if (isValid(nx, ny) && grid[nx][ny] == 0 && !visited[nx][ny]) {
// 6. 标记现场 visited[nx][ny] = 1;
// 7. 计算新距离 // 题目定义:跳过距离 = 跳过1的个数 + 1 int step_dist = count_ones + 1;
// 8. 递归进入下一层 dfs(nx, ny, current_dist + step_dist);
// 9. 回溯(还原现场) visited[nx][ny] = 0; } }}代码详解与知识点:
dfs(x, y, current_dist):- 知识点:递归(Recursion)。
- 做法:函数自己调用自己。参数不仅包含“我在哪”(x, y),还包含“我已经走了多远”(current_dist),这样状态就能一直传递下去。
if (current_dist > max_ans):- 知识点:打擂台法求最大值。
- 做法:每到达一个新的合法点,就看看当前的距离是否比已知的最大值大,如果是,就更新。
for (int i = 0; i < 4; i++):- 做法:配合前面的
dx/dy数组,循环 4 次分别代表向 4 个方向尝试。
- 做法:配合前面的
if (... || grid[nx][ny] != 1) continue;:- 知识点:**剪枝(Pruning)**的一种简单形式。
- 做法:题目规定“必须越过至少 1 个 1”。如果我想往右跳,结果右边紧挨着就是 0 或者墙壁,那这个方向直接废弃,不用往下算了。
while (... grid[nx][ny] == 1):- 知识点:模拟(Simulation)。
- 做法:模拟跳棋“飞”的过程。只要脚下是 1,就停不下来,必须一直按原方向冲,直到脚下不是 1 为止。
visited[nx][ny] = 1;…dfs(...)…visited[nx][ny] = 0;:- 知识点:回溯(Backtracking) —— 全代码最重要的概念。
- 详细解释:
- 标记 (
=1):假设我决定跳到 B 点,我在 B 点插个旗子“我来过了”,防止之后在兜圈子时又回到 B。 - 递归 (
dfs):以 B 点为新起点,继续去探索更远的路。 - 还原 (
=0):这是关键!当以 B 为起点的所有可能性都探索完了,函数会返回。这时由于我们要寻找最长路径,也许另一条路线(比如先去 C,再去 B)能得到更好的结果。如果不把 B 的旗子拔掉(还原为 0),别的路线走到 B 时发现“被占用了”,就会错过这种可能性。 - 比喻:就像你在走迷宫,你走到一个路口,决定先走左边。你在路口放块石头标记“这里走过了”。等你把左边走通了或者走死了,退回到路口时,必须把石头拿走,这样你接下来尝试走右边时,如果右边的路绕一圈又回到了这个路口,才不会受影响(或者说,为了让父节点能尝试别的路径,需要清理子节点造成的环境影响)。
- 标记 (
主函数 main
程序开始执行的地方。
int main() { int start_x, start_y;
// 输入处理:读取 N 和 起点坐标 if (scanf("%d %d %d", &n, &start_x, &start_y) != 3) { return 0; }
// 坐标转换 start_x--; start_y--;
// 读取棋盘 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { scanf("%d", &grid[i][j]); visited[i][j] = 0; // 顺手初始化一下访问数组,虽然全局变量默认是0,但这样更保险 } }
// 起点处理:起点算作已访问 visited[start_x][start_y] = 1;
// 开始搜索,初始距离为0 dfs(start_x, start_y, 0);
// 输出最终结果 printf("%d\n", max_ans);
return 0;}代码详解与知识点:
start_x--; start_y--;:- 知识点:索引偏移(0-based indexing vs 1-based indexing)。
- 做法:题目(数学描述)通常喜欢从第 1 行第 1 列开始数。但 C 语言数组是从
0开始的。- 题目说
(1, 3),对应程序里的grid[0][2]。 - 所以必须减 1,否则坐标会错位,甚至越界。
- 题目说
visited[start_x][start_y] = 1;:- 做法:题目规定“给出的起始点不能再到达”。所以在 DFS 开始前,先手动把起点锁死。
dfs(start_x, start_y, 0);:- 做法:启动引擎。从起点开始,当前的跳跃总距离是 0。
源码2.0
#include <stdio.h>
#define MAXN 105
int n, grid[MAXN][MAXN], visited[MAXN][MAXN];int dx[] = {-1, 1, 0, 0}; // 也可以省略大小,编译器自动推断int dy[] = {0, 0, -1, 1};
// 优化:函数返回 int,表示“从当前点出发能得到的最大后续距离”int dfs(int x, int y) { int local_max = 0; // 记录从这一步出发,四个方向里最好的那个结果
for (int i = 0; i < 4; i++) { int nx = x + dx[i], ny = y + dy[i]; int steps = 0; // 记录越过的1的个数
// 1. 起跳检查:利用 (unsigned) 技巧合并边界判断 // 必须不越界 且 下一格是 1 if ((unsigned)nx < n && (unsigned)ny < n && grid[nx][ny] == 1) {
// 2. 飞行过程:只要是 1 就一直飞 while ((unsigned)nx < n && (unsigned)ny < n && grid[nx][ny] == 1) { nx += dx[i]; ny += dy[i]; steps++; }
// 3. 落地检查:必须不越界 且 是0 且 未访问 if ((unsigned)nx < n && (unsigned)ny < n && !grid[nx][ny] && !visited[nx][ny]) { visited[nx][ny] = 1;
// 核心逻辑:本次跳跃距离(steps+1) + 后续能跳的最远距离 int val = (steps + 1) + dfs(nx, ny);
if (val > local_max) local_max = val; // 更新当前点的最优解
visited[nx][ny] = 0; // 回溯 } } } return local_max;}
int main() { int sx, sy; if (scanf("%d %d %d", &n, &sx, &sy) != 3) return 0;
for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) scanf("%d", &grid[i][j]);
visited[--sx][--sy] = 1; // 坐标 -1 并标记起点
printf("%d\n", dfs(sx, sy)); // 直接打印返回值 return 0;}1. “无符号整型魔法”:(unsigned)
if ((unsigned)nx < n && (unsigned)ny < n ... )以前的写法:
if (nx >= 0 && nx < n && ny >= 0 && ny < n)为什么这一句能顶原来的两句?这里利用了计算机存储数字的原理。
- 原理:
int是有符号的(可以是正数也可以是负数)。unsigned int是无符号的(只能是非负整数)。 - 发生了什么? 如果
nx变成了 -1(比如在边缘向左越界了):- 当它是
int时,它是 -1(小于 0,不合法)。 - 当你把它强制转换为
unsigned时:计算机里的二进制码没变,但解释方式变了。-1 会瞬间变成一个巨大的正整数(通常是 4294967295)。
- 当它是
- 结论: 这个巨大的数远远大于
n(因为 n 最大才 100)。 所以(unsigned)-1 < n这个判断会变成假。
2. “回报式递归”:老板思维
在优化后的代码中,我们把 void dfs(...) 改成了 int dfs(...),并且用上了这样一句核心代码:
C
// 核心方程:当前总收益 = 眼前这一跳的收益 + 后面能拿到的最大收益int val = (steps + 1) + dfs(nx, ny);以前的写法像是**“接力跑”:拿着一个累加器一直跑,跑到终点记录成绩。 现在的写法变成了“层层外包”**:每个人只负责自己这一棒,剩下的路交给下属去探,最后汇总结果。
核心原理:
- 暂停与等待:当你调用
dfs(nx, ny)时,当前的函数并不会结束,而是会**“冻结”**在这一行,死等下一层函数算完给出一个结果,它才会醒过来继续执行。 - 局部与整体:每个函数只关心“从我脚下开始能拿多少分”,不需要关心全局变量,逻辑完全解耦。
微观演示:A -> B -> C -> 墙
为了看懂这个过程,我们假设地图上有一条路:A -> B -> C -> 墙。
- A 跳到 B 能赚 3 分。
- B 跳到 C 能赚 2 分。
过程拆解:
- 第一层(你是 A):
- 你发现跳一步能到 B,赚 3 分。
- 你想知道后面还能赚多少,于是你对 B 说:“dfs(B),你去帮我算算从你那里开始最多能赚多少,我在这里等你结果。”
- A 进入“冻结”状态,等待 B。
- 第二层(分身 B):
- B 发现跳一步能到 C,赚 2 分。
- B 想知道后面还能赚多少,于是他对 C 说:“dfs(C),你去帮我算算,我在这里等你。”
- B 进入“冻结”状态,等待 C。
- 第三层(分身 C):
- C 四处一看,全是墙(或者出界了)。
- C 没有任何路可走,于是得出结论:从我这里出发,收益是 0。
- C 返回 0,任务结束,C 消失。
- 回到第二层(B 醒了):
- B 收到了 C 的汇报:0。
- B 开始计算自己的总账:
val = 2(眼前) + 0(后面) = 2。- B 向上一级汇报 2,任务结束,B 消失。
- 回到第一层(A 醒了):
- A 收到了 B 的汇报:2。
- A 开始计算自己的总账:
val = 3(眼前) + 2(后面) = 5。- A 得到最终结果 5。
结论: 这种写法利用了计算机的**“函数调用栈”自动保存了每一层的状态。代码虽然变短了,但逻辑却变得更加严密和高级,这正是动态规划(Dynamic Programming)**思想的雏形。
虽然暂时写不出这种题来,但是借助这个题还是学到了很多东西,思维也拔高了
部分信息可能已经过时









