每次只动一堆,故整局 SG 等于各堆 SG 的异或和;单堆 p 的后继是把真约数全摆上再删一个,故 SG(p)=mex(nx⊕SG(d)),nx 是全部真约数堆的异或和。
OJ: roj
题目 ID: 3104
难度:普及+/提高-
标签:博弈论SG 函数数学
创建: 2026-10-01 17:27
更新: 2026-10-07 13:50
形式化题目
初始有若干堆正整数
- 选中一堆
,记 的全部真约数(小于 的正约数,包含 、不包含 )为 ; - 把这一堆
替换成 堆,数量分别为 ; - 从这
堆中删去任意一堆。
轮到某人操作时,若所有堆都等于
正解
思路
朴素做法为什么会炸
最直接的想法是把「当前所有堆」当成一个状态去搜:枚举所有
问题出在「把整局面当状态」这件事上:它浪费了题目最强的结构——一次操作只触碰一堆。
第一步:把「多堆」压成异或和
先看一次操作动了什么。它只选中一堆
再看终局条件:「所有堆都是
子游戏独立、双方可用操作集合相同、无路可走者败,三个条件齐备,可以直接套 Sprague–Grundy 定理:
- 若单堆
的 SG 值为 ,则整个局面的 SG 值为 ; - 先手必胜
异或和非零。
这一步就把「
第二步:单堆的后继本身已经是「一堆之和」
即「
于是单堆的 SG 递推是
: ,没有后继, ;这也印证了「全是 必败」。 - 依赖方向:
只用到真约数 的值,所以从 往上递推(或记忆化递归)一定无环。
注意每个约数只贡献一次枚举:先把
下面这张表把
| 真约数 |
后继价值 |
|||
|---|---|---|---|---|
| 1 | 0 | 0 | ||
| 2 | 0 | 1 | ||
| 3 | 0 | 1 | ||
| 4 | 1 | 2 | ||
| 5 | 0 | 1 | ||
| 6 | 0 | 2 | ||
| 7 | 0 | 1 | ||
| 8 | 3 | 0 | ||
| 9 | 1 | 2 | ||
| 10 | 0 | 2 | ||
| 11 | 0 | 1 | ||
| 12 | 0 | 3 | ||
| 13 | 0 | 1 | ||
| 14 | 0 | 2 | ||
| 15 | 0 | 2 | ||
| 16 | 3 | 0 | ||
| 17 | 0 | 1 | ||
| 18 | 0 | 3 | ||
| 19 | 0 | 1 | ||
| 20 | 0 | 3 | ||
| 21 | 0 | 2 | ||
| 22 | 0 | 2 | ||
| 23 | 0 | 1 | ||
| 24 | 3 | 4 |
观察重点:
与 的 SG 值是 0,即「一堆 8」对先手是必败局面。这说明 SG 值完全不随 单调,不能凭直觉代替递推。 为质数时 ,后继只有「局面空掉」一种,SG 恒为 1;这也是后面样例里 、 都取 1 的原因。 - 表里没有出现比 4 更大的值(
时最大 SG 值也只有 4),但这只是数值分布的结果,不构成提前终止递推的理由。
单堆
flowchart TD
P["p = 12<br>真约数 D = (1,2,3,4,6)<br>nx = 0⊕1⊕1⊕2⊕2 = 0"]
P -->|"删掉 1"| A["剩 (2,3,4,6)<br>SG = 1⊕1⊕2⊕2 = 0"]
P -->|"删掉 2"| B["剩 (1,3,4,6)<br>SG = 0⊕1⊕2⊕2 = 1"]
P -->|"删掉 3"| C["剩 (1,2,4,6)<br>SG = 0⊕1⊕2⊕2 = 1"]
P -->|"删掉 4"| D["剩 (1,2,3,6)<br>SG = 0⊕1⊕1⊕2 = 2"]
P -->|"删掉 6"| E["剩 (1,2,3,4)<br>SG = 0⊕1⊕1⊕2 = 2"]
五个后继的 SG 值集合是
第三步:用样例验证整套模型
第一个样例 3 堆 2 2 2:freda。
第二个样例 1 3 5:rainbow。两问都与题面样例一致。
其中「一堆
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 17:27
# update_at: 2026-10-01 17:27
import sys
from functools import cache, reduce
from math import isqrt
from operator import xor
@cache
def sg(p: int) -> int:
"""单堆 p 颗魔法珠的 SG 值。
一次操作把 p 换成它的全部真约数,再删掉其中一堆:等于在一个「真约数堆」的
游戏里挑一堆放弃,剩下各堆仍是互不干扰的子游戏,所以每个可选局面的价值是
剩余堆的异或。于是先求全部真约数的异或 nx,删掉 d 后的价值就是 nx ^ sg(d)。
"""
proper = {d for i in range(1, isqrt(p) + 1) if p % i == 0 for d in (i, p // i)} - {p}
nx = reduce(xor, map(sg, proper), 0) # 全部真约数堆的异或和
options = {nx ^ sg(d) for d in proper} # 每一种「删掉 d」后到达的局面的 SG
mex = 0
while mex in options:
mex += 1
return mex # 没有后继(p = 1)时 mex 就是 0
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out = []
while (n := next(data, None)) is not None: # 组数没有给出,一直读到 EOF
piles = [next(data) for _ in range(n)]
# 每一轮可以动任意一堆,多堆是一个「和游戏」:整局 SG 就是各堆异或
out.append("freda" if reduce(xor, map(sg, piles), 0) else "rainbow")
print("\n".join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 18:57
* update_at: 2026-10-06 18:57
*/
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
const int MAXP = 1005; // a_i 最大为 1000
int sg[MAXP]; // sg[p] 表示单堆 p 颗魔法珠的 SG 值
// 计算 sg[1..MAXP-1],sg[1] = 0
void init_sg() {
sg[1] = 0; // 1 没有真约数,无法操作,SG 为 0
for (int p = 2; p < MAXP; ++p) {
// 枚举 p 的全部真约数
vector<int> divisors;
for (int i = 1; i * i <= p; ++i) {
if (p % i == 0) {
if (i < p) divisors.push_back(i);
int j = p / i;
if (j != i && j < p) divisors.push_back(j);
}
}
// nx = 全部真约数对应 SG 值的异或和
int nx = 0;
for (int d : divisors) nx ^= sg[d];
// 收集每个后继局面的 SG 值:删掉真约数 e 后,剩余堆的异或和为 nx ^ sg[e]
bool vis[128] = {false}; // SG 值很小,用数组做 mex 标记
for (int e : divisors) vis[nx ^ sg[e]] = true;
int mex = 0;
while (vis[mex]) ++mex;
sg[p] = mex;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
init_sg();
int n;
while (cin >> n) {
int xorsum = 0;
for (int i = 0; i < n; ++i) {
int a;
cin >> a;
xorsum ^= sg[a];
}
cout << (xorsum ? "freda" : "rainbow") << '\n';
}
return 0;
}复杂度
设
- 求单个
: 枚举约数,加 次异或与 mex 扫描;每个 只算一次( @cache),最坏情况把全部算出共 , 时约 次内层循环。 - 每组数据
次异或, 。 - 总计
,与标准 C++ 解法(先递推 的 SG 表,再逐组异或)同阶。 - 空间
: @cache中最多 1000 个记录,加上单组的个堆。
总结
- 一次操作只改动一堆,所以每堆是独立子游戏,整个局面是它们的和;又因为「全是
就输」正好等价于 normal play,可直接用 SG 定理把整局压成 。 - 单堆
的后继是「摆出全部真约数、再删一个」,它本身就是一组子游戏之和,价值等于异或和;先算出 ,删掉 的价值就是 。 - 由此得到
,只依赖真约数(严格更小),从 递推即可, 时一次预计算、多组复用。 - 易错点:
值随 不单调( ),必须真算; 的 SG 值为 ,不能漏掉对它的定义;枚举约数时只需到 ,配对取 与 ,完全平方数靠集合去重。