斐波那契博弈(Fibonacci Game)
斐波那契博弈(Fibonacci Game)
一、问题模型
有 n 枚石子,两人轮流取:
- Mirko 先取,第一次可以取走任意多枚(至少 1 枚)
- 之后每次至少取 1 枚,最多取走上一次取走数量的 2 倍
- 取走的数量不能超过场上剩余石子数
- 取走最后一枚石子的人获胜
双方都采取最优策略。问:Mirko 第一次至少要取多少枚才能保证获胜?
二、结论
- 如果
n是斐波那契数,先手必须一次取完(取n枚)才能保证获胜 - 如果
n不是斐波那契数,把n拆成若干斐波那契数之和,先手第一次取走其中最小的那个斐波那契数即可保证获胜
三、为什么是这个结论
核心工具是 ** 齐肯多夫定理 定理**:
任何正整数都可以唯一地表示为若干个不相邻的斐波那契数之和。
例如:83 = 55 + 21 + 5 + 2(55、21、5、2 两两不相邻)。
情况一:n 是斐波那契数(先手必败,除非一次取完)
设 n = F_k。先手第一次不能取完(否则直接赢,但题目问的是“最少取多少能保证赢”,所以如果只取部分,后手有必胜策略)。
证明(归纳法):
假设 n = F_k,先手第一次取了 x 枚。
-
如果
x >= F_{k-2},那么后手可以一次取完剩下的F_k - x枚。因为F_k - x <= F_k - F_{k-2} = F_{k-1} <= 2x(后手最多可取2x),所以后手直接获胜。 -
如果
x < F_{k-2},那么剩下的石子数n - x落在(F_{k-1}, F_k)之间。把n - x拆成 Zeckendorf 表示:n - x = F_{k-1} + ... + F_{j}(其中F_j是最小项)。后手只需要取走F_j枚,就能保证最终取到最后一枚。
因此,当 n 是斐波那契数时,先手只有一次取完才能赢。
情况二:n 不是斐波那契数(先手必胜)
根据 Zeckendorf 定理,把 n 唯一拆成不相邻斐波那契数之和:
n = F_{a1} + F_{a2} + ... + F_{am} (a1 > a2 > ... > am,且互不相邻)
先手第一次取走最小的那一项 F_{am}。
之后,无论后手取多少(设为 y 枚),由于 F_{am} 和次小项 F_{a(m-1)} 不相邻,可以证明后手无法一次取完 F_{a(m-1)} 这一堆。于是先手可以继续用同样的策略,每次取走当前“最小堆”的最后一块。
最终,先手会依次取到每一堆的最后一块,从而取到总数 n 的最后一块,获得胜利。
四、证明思路(归纳法与引理)
引理 1(Zeckendorf 定理):
任何正整数 N 可以唯一地表示为若干个不连续的斐波那契数之和。
存在性证明(贪心法):
- 取不超过
N的最大斐波那契数F_k。 - 若
F_k = N,完成。 - 否则,
N - F_k < F_{k-1}(否则N >= F_k + F_{k-1} = F_{k+1},与F_k是最大项矛盾)。 - 对
N - F_k递归使用同样方法,得到的分解自然满足“不连续”。
唯一性证明(反证法):
假设存在最小的数有两种合法分解,设最大项分别为 F_a 和 F_b。若 F_a > F_b,由引理(一组不相邻斐波那契数之和小于下一项),第一种分解的和小于 F_{b+1} - 1,但第二种分解至少为 F_b,矛盾。因此 F_a = F_b,递归下去可知分解唯一。
引理 2(关键性质):
若 n 是斐波那契数,先手不一次取完则后手必胜。
证明:设 n = F_k。后手策略:
- 先手取
x >= F_{k-2}→ 后手一次取完。 - 先手取
x < F_{k-2}→ 后手面对F_k - x,将其拆成F_{k-1} + ...,后手取走其中最小的斐波那契数。由归纳假设,后手能取到F_{k-1}这一堆的最后一块,进而取到全局最后一块。
引理 3(必胜策略):
若 n 不是斐波那契数,先手取走 Zeckendorf 分解中最小项后必胜。
证明:设 n = F_{a1} + ... + F_{am}(不相邻)。先手取 F_{am} 后,剩余 F_{a1} + ... + F_{a(m-1)}。后手无法一次取完 F_{a(m-1)}(因为 F_{a(m-1)} > 2 * F_{am},而后手最多取 2 * F_{am})。于是先手可以继续用“取最小堆最后一块”的策略,最终取到最后一块。
五、AC 代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N = 1e15 + 5;
vector<ll> Fib;
void pre()
{
Fib.push_back(1);
Fib.push_back(2);
while (true)
{
ll t = Fib[Fib.size() - 1] + Fib[Fib.size() - 2];
if (t > N) break;
Fib.push_back(t);
}
}
int main()
{
pre();
ll n;
cin >> n;
if (binary_search(Fib.begin(), Fib.end(), n))
{
cout << n << '\n';
return 0;
}
ll cur = n;
vector<ll> part;
while (cur > 0)
{
auto it = upper_bound(Fib.begin(), Fib.end(), cur);
// upper_bound(begin, end, value),返回第一个严格大于 value 的元素的迭代器。
it--;
ll f = *it;
cur -= f;
part.push_back(f);
}
cout << part[part.size() - 1] << '\n';
return 0;
}
六、代码思路
- 预处理斐波那契数列:从
1, 2开始递推(注意不是1, 1,否则唯一性会出问题),生成到超过10^15 - 判断
n是否为斐波那契数:用binary_search查找- 是 → 输出
n(一次取完) - 否 → 继续第 3 步
- 是 → 输出
- Zeckendorf 拆分:每次取不超过当前剩余数的最大斐波那契数,从
n中减去,记录到part- 贪心取最大斐波那契数得到的拆分,必然满足“不相邻”条件,即 Zeckendorf 分解
- 输出最小项:
part最后压入的就是拆分出的最小斐波那契数
七、总结
| 情况 | 先手策略 |
|---|---|
n 是斐波那契数 |
必须一次取完 n 枚 |
n 不是斐波那契数 |
取走 Zeckendorf 分解中最小的斐波那契数 |

浙公网安备 33010602011771号