Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
4033 字
20 分钟
洛谷刷题04
2025-12-20
C

P3848 [TJOI2007] 跳棋#

是群友给的题目,但是用的知识我好多都没学到,所以只是复现一遍,群友们真是太强了

题目背景#

本题可能为错题,目前数据只是随机生成的 n20n\leq 20 的情况,测试数据和题解仅供参考。#

在一个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 2
1 0 1 0
1 1 1 1
0 0 1 0
1 1 0 1

输出 #1#

6

说明/提示#

upd 2022.7.27\text{upd 2022.7.27}:新增加一组 Hack 数据。

源码#

#include <stdio.h>
#include <stdlib.h>
#define MAXN 105
int 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];

核心原理: 使用 dxdy 两个常量数组定义四个移动方向的坐标增量。在搜索过程中,通过一个循环 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;
}

代码详解与知识点:

  1. 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;
}
}
}

代码详解与知识点:

  1. dfs(x, y, current_dist)
    • 知识点递归(Recursion)
    • 做法:函数自己调用自己。参数不仅包含“我在哪”(x, y),还包含“我已经走了多远”(current_dist),这样状态就能一直传递下去。
  2. if (current_dist > max_ans)
    • 知识点打擂台法求最大值
    • 做法:每到达一个新的合法点,就看看当前的距离是否比已知的最大值大,如果是,就更新。
  3. for (int i = 0; i < 4; i++)
    • 做法:配合前面的 dx/dy 数组,循环 4 次分别代表向 4 个方向尝试。
  4. if (... || grid[nx][ny] != 1) continue;
    • 知识点:**剪枝(Pruning)**的一种简单形式。
    • 做法:题目规定“必须越过至少 1 个 1”。如果我想往右跳,结果右边紧挨着就是 0 或者墙壁,那这个方向直接废弃,不用往下算了。
  5. while (... grid[nx][ny] == 1)
    • 知识点模拟(Simulation)
    • 做法:模拟跳棋“飞”的过程。只要脚下是 1,就停不下来,必须一直按原方向冲,直到脚下不是 1 为止。
  6. 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;
}

代码详解与知识点:

  1. start_x--; start_y--;
    • 知识点索引偏移(0-based indexing vs 1-based indexing)
    • 做法:题目(数学描述)通常喜欢从第 1 行第 1 列开始数。但 C 语言数组是从 0 开始的。
      • 题目说 (1, 3),对应程序里的 grid[0][2]
      • 所以必须减 1,否则坐标会错位,甚至越界。
  2. visited[start_x][start_y] = 1;
    • 做法:题目规定“给出的起始点不能再到达”。所以在 DFS 开始前,先手动把起点锁死。
  3. 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(比如在边缘向左越界了):
    1. 当它是 int 时,它是 -1(小于 0,不合法)。
    2. 当你把它强制转换为 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 分。

过程拆解:

  1. 第一层(你是 A)
    • 你发现跳一步能到 B,赚 3 分。
    • 你想知道后面还能赚多少,于是你对 B 说:“dfs(B),你去帮我算算从你那里开始最多能赚多少,我在这里等你结果。”
      • A 进入“冻结”状态,等待 B。
  2. 第二层(分身 B)
    • B 发现跳一步能到 C,赚 2 分。
    • B 想知道后面还能赚多少,于是他对 C 说:“dfs(C),你去帮我算算,我在这里等你。”
      • B 进入“冻结”状态,等待 C。
  3. 第三层(分身 C)
    • C 四处一看,全是墙(或者出界了)。
    • C 没有任何路可走,于是得出结论:从我这里出发,收益是 0
      • C 返回 0,任务结束,C 消失。
  4. 回到第二层(B 醒了)
    • B 收到了 C 的汇报:0
    • B 开始计算自己的总账:val = 2(眼前) + 0(后面) = 2
      • B 向上一级汇报 2,任务结束,B 消失。
  5. 回到第一层(A 醒了)
    • A 收到了 B 的汇报:2
    • A 开始计算自己的总账:val = 3(眼前) + 2(后面) = 5
      • A 得到最终结果 5。

结论: 这种写法利用了计算机的**“函数调用栈”自动保存了每一层的状态。代码虽然变短了,但逻辑却变得更加严密和高级,这正是动态规划(Dynamic Programming)**思想的雏形。

虽然暂时写不出这种题来,但是借助这个题还是学到了很多东西,思维也拔高了

洛谷刷题04
https://btop251.vercel.app/posts/c语言学习/洛谷刷题04/
作者
btop251
发布于
2025-12-20
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时