斐波那契博弈(Fibonacci Game)

斐波那契博弈(Fibonacci Game)

一、问题模型

n 枚石子,两人轮流取:

  • Mirko 先取,第一次可以取走任意多枚(至少 1 枚)
  • 之后每次至少取 1 枚,最多取走上一次取走数量的 2 倍
  • 取走的数量不能超过场上剩余石子数
  • 取走最后一枚石子的人获胜

双方都采取最优策略。问:Mirko 第一次至少要取多少枚才能保证获胜?

对应题目:洛谷 P6787 [COCI 2010/2011 #4] HRPA

二、结论

  • 如果 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_aF_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. 预处理斐波那契数列:从 1, 2 开始递推(注意不是 1, 1,否则唯一性会出问题),生成到超过 10^15
  2. 判断 n 是否为斐波那契数:用 binary_search 查找
    • 是 → 输出 n(一次取完)
    • 否 → 继续第 3 步
  3. Zeckendorf 拆分:每次取不超过当前剩余数的最大斐波那契数,从 n 中减去,记录到 part
    • 贪心取最大斐波那契数得到的拆分,必然满足“不相邻”条件,即 Zeckendorf 分解
  4. 输出最小项part 最后压入的就是拆分出的最小斐波那契数

七、总结

情况 先手策略
n 是斐波那契数 必须一次取完 n
n 不是斐波那契数 取走 Zeckendorf 分解中最小的斐波那契数
posted @ 2026-09-22 21:58  leaderling  阅读(4)  评论(0)    收藏  举报