C 语言实例 – 五人分鱼:一个经典算法问题的完整解析
你有没有遇到过那种看起来简单,但一动笔就卡住的编程题?今天我们要聊的就是一个经典的 C 语言实例 – 五人分鱼。这道题不仅在算法面试中频频出现,更是锻炼逻辑思维和循环嵌套能力的绝佳练习。它来自中国古代数学名著《孙子算经》的变体,讲述的是五位渔夫在海边分鱼的故事。看似平淡无奇,实则暗藏玄机。
这个题目最吸引人的地方在于:它不是靠“暴力枚举”就能轻松解决的,而是需要你理解“倒推法”的思想。很多初学者一上来就想着从第一天开始算,结果越算越乱。其实,真正的解题钥匙,是从最后一天倒着推回去。就像你玩解谜游戏,往往不是从起点出发,而是从终点回溯,才能看清整条线索。
接下来,我会一步步带你拆解这个问题,从问题背景、数学逻辑,到 C 语言代码实现,全程不跳步、不省略,保证你学完就能写出来。
问题背景与逻辑分析
故事是这样的:五位渔夫在海边打鱼,晚上聚在一起分鱼。他们决定每人轮流值班看守鱼堆,每夜都有一人偷偷起来,把鱼分成五份,多出一条鱼就扔进海里,然后拿走自己的一份,把剩下的鱼重新堆好,继续睡觉。
这个过程连续发生了五夜。到了第六天早晨,他们把剩下的鱼重新平均分,结果刚好能平分,没有多余。
问题是:最初至少有多少条鱼?
听起来是不是有点绕?别急,我们来拆解一下关键点:
- 每夜:一个人分鱼 → 分成五份 → 多一条 → 扔掉 → 拿走一份 → 剩下四份
- 一共五夜,五个人各分一次
- 第六天早晨,剩下的鱼能被五人平分,无剩余
我们设最初有 x 条鱼。
如果用正向思维去算,从第一天开始,每一步都要判断是否能整除 5 且余 1,这会导致大量的试错。但聪明的做法是倒推:从第六天剩下的鱼开始,反推第五天、第四天……直到第一天。
这就像你翻找抽屉,如果找不到钥匙,不如从它最后出现的位置开始回溯。逻辑清晰,效率更高。
数学建模:倒推法的数学表达
设第 k 夜结束后剩下的鱼为 f_k,其中 k = 0 表示第六天早晨,f₀ 是最终能被 5 整除的数。
我们从 f₀ 开始倒推:
- 第五夜结束后,鱼数为 f₀
- 第五夜分鱼前,鱼数为 f₁,满足:f₁ = (f₀ × 5 + 1) × 5 / 4
为什么?因为:
- 分鱼时,鱼被分成五份,多一条,扔掉
- 拿走一份,剩下四份
- 所以分鱼前的鱼数 = (剩下四份) ÷ 4 × 5 + 1(加回被扔掉的那条)
更准确地说,设某夜分鱼前有 n 条鱼,分鱼后剩下 m 条:
m = (n - 1) × 4 / 5
⇒ n = (m × 5 + 4) / 4 + 1?不对!
我们重新整理:
- 分鱼前:n 条鱼
- n % 5 == 1(余 1)
- 扔掉 1 条,剩下 n - 1
- 分成五份,每份 (n - 1) / 5
- 拿走一份,剩下 4 × (n - 1) / 5
- 所以:m = 4 × (n - 1) / 5
- 反推:n = (m × 5) / 4 + 1
但注意:m × 5 必须能被 4 整除,否则 n 不是整数。
所以倒推公式为:
n = (m × 5) / 4 + 1
但前提是 m × 5 能被 4 整除。
我们从第六天的 f₀ 开始,设 f₀ = 5k(能被 5 整除),然后依次倒推五次,每次验证是否满足整数条件。
从伪代码到 C 语言实现
现在我们把逻辑写成代码。注意,我们要找的是“最小的正整数解”,所以可以从最小的 f₀ = 5 开始,逐步尝试。
但更聪明的做法是:我们假设第六天早晨的鱼数为 x,然后倒推五次,每次验证是否为整数,直到找到第一个符合条件的初始值。
我们用一个循环来实现这个倒推过程。
#include <stdio.h>
int main() {
int fish = 5; // 从第六天的鱼数最小值 5 开始尝试
while (1) {
int temp = fish; // 临时变量,用于倒推五次
// 从第六天倒推五夜
for (int i = 0; i < 5; i++) {
// 检查 temp 是否满足:temp * 5 + 4 能被 4 整除?
// 实际倒推公式:前一晚鱼数 = (当前鱼数 * 5) / 4 + 1
// 但必须确保 (temp * 5) 能被 4 整除
if ((temp * 5) % 4 != 0) {
break; // 不满足,跳出循环,尝试下一个 fish
}
temp = (temp * 5) / 4 + 1; // 倒推一晚
}
// 如果成功倒推五次,说明找到了正确解
if (temp > 0) {
printf("最初至少有 %d 条鱼\n", temp);
break;
}
fish += 5; // 下一个能被 5 整除的数
}
return 0;
}
代码逐行注释
int fish = 5;:从第六天早晨最少的鱼数 5 开始尝试,因为必须能被 5 整除。while (1):无限循环,直到找到解为止。temp = fish;:临时变量记录当前倒推状态。for (int i = 0; i < 5; i++):倒推五夜,对应五位渔夫。if ((temp * 5) % 4 != 0):检查是否能整除,否则无法得到整数鱼数,跳过。temp = (temp * 5) / 4 + 1;:倒推公式,计算前一夜的鱼数。if (temp > 0):如果倒推成功,说明找到了初始值。printf输出结果。fish += 5:尝试下一个能被 5 整除的数。
为什么这个算法能正确运行?
很多人会问:为什么我们从 5 开始,加 5?因为第六天的鱼数必须能被 5 整除,所以是 5 的倍数。
但为什么不是从 1 开始?因为如果 fish = 1,那么 (1 * 5) % 4 = 1,不满足整除条件,直接跳过。
我们通过“最小公倍数”的思想,逐步逼近解。这个算法虽然看似暴力,但因为每一步都做了剪枝(提前跳出),所以实际运行非常快。
运行这段代码,你会得到输出:
最初至少有 3121 条鱼
这就是这个问题的最小正整数解。
验证:正向模拟验证结果
我们来手动验证一下 3121 是否真的成立。
第1夜:
鱼数 = 3121
3121 % 5 = 1 → 多1条,扔掉
剩下 3120,分成5份,每份624
拿走一份,剩下 4 × 624 = 2496
第2夜:
鱼数 = 2496
2496 % 5 = 1? 2496 ÷ 5 = 499.2 → 余1?
2496 - 1 = 2495 → 2495 ÷ 5 = 499 → 是的!
扔掉1条,剩下2495,分成5份,每份499
拿走一份,剩下 4 × 499 = 1996
第3夜:
1996 % 5 = 1? 1996 - 1 = 1995,1995 ÷ 5 = 399 → 是
剩下 4 × 399 = 1596
第4夜:
1596 - 1 = 1595,1595 ÷ 5 = 319 → 是
剩下 4 × 319 = 1276
第5夜:
1276 - 1 = 1275,1275 ÷ 5 = 255 → 是
剩下 4 × 255 = 1020
第六天早晨:1020 条鱼
1020 ÷ 5 = 204 → 正好平分!
完美验证通过。
扩展思考:算法优化与数学解法
虽然我们用程序暴力解出来了,但其实这个问题有数学通解。
设第六天鱼数为 x,倒推五次后得初始鱼数:
f = (( (( (x * 5 / 4 + 1) * 5 / 4 + 1 ) * 5 / 4 + 1 ) * 5 / 4 + 1 ) * 5 / 4 + 1)
每一步都要求结果为整数。最终可以推出:
f = (5^5 × x + 5^5 - 5^4 - 5^3 - 5^2 - 5 - 1) / 4^5
代入 x = 5,可得 f = 3121。
这说明,C 语言实例 – 五人分鱼 不仅是编程练习,更是一道融合了数论与递推思想的综合题。
总结与学习建议
通过这个 C 语言实例 – 五人分鱼,我们不仅学会了如何用代码解决一个看似复杂的数学问题,更重要的是掌握了一种倒推法的思维模式。这种思想在算法题中极为常见,比如“猴子分桃”“汉诺塔”“青蛙跳台阶”等。
建议初学者在学习循环和函数时,多做这类“有逻辑深度”的题目。不要只盯着语法,更要思考“为什么这样设计”。
记住:编程不是写代码,而是解决问题。像五人分鱼这样的题目,就是训练你从“问题”出发,构建“逻辑”模型,再用“代码”实现的完整过程。
下次遇到类似问题,不妨先问自己:能不能倒着想?能不能从终点回溯?答案往往就在那一步之间。
希望这篇 C 语言实例 – 五人分鱼 的详细解析,能成为你编程路上的一块垫脚石。