把排序后的棋子间隙看成从右往左流动的石子堆,问题化为阶梯博弈,奇数段间隙异或和非零则先手必胜。
OJ: roj
题目 ID: 3105
难度:普及+/提高-
标签:博弈论Nim数学python
创建: 2026-10-01 17:16
更新: 2026-10-07 13:50
形式化题目
在一条从左到右编号
正解
思路
第一步:把棋盘切成"间隙"。 把棋子按位置从左到右排序,记位置为
- 第
个棋子左边能自由移动的空间 (左边界是 1 号格的左侧); - 第
个棋子与左邻棋子之间的空格数 ( )。
向左移动第
第二步:翻个方向,变成阶梯博弈。 想象石子(空格)受重力从右往左流:把最右棋子右侧的空隙与棋子间空隙重新编号,第
第
证明分三步(把操作后棋子全挤到最左、所有间隙为 0 的局面记为终局,终局
时先手能把它变成 。 标准 Nim 论证给出一个奇数段 ,减少若干颗后 变 : - 如果"减少"合法(通常就是这种情况),直接照做;
- 如果那个奇数段必须增加才行,就取最靠右的可增奇数段
( ),把 颗石子从 搬进它的右邻——第 段。第 段是偶数段,不参与异或,所以 同样变成 ;而 颗石子的来源不成问题,因为 右侧更靠右的奇数段里必有可减的段。
时任何一步都会让 。 动奇数段,奇数段异或和改变;动偶数段 ,是把石子送进奇数段 ,同样改变奇数段异或和。 - 于是
的局面总能一步走到 ,且终局 ,故 当且仅当先手必胜。
回到本题:从右往左数,处于第
用样例二
:::tip 方向搞反会怎样
如果错误地从左往右取奇数段(即取
代码
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-09-28 21:30
# update_at: 2026-09-28 21:30
import sys
def nim_xor(gaps: list[int]) -> int:
"""奇数位间隔(从最右棋子数起第 1、3、5……段)的异或和:0 表示先手必败。"""
xor_sum = 0
for i in range(len(gaps) - 1, -1, -2): # 从右往左,隔一个取一个
xor_sum ^= gaps[i]
return xor_sum
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
T = next(data)
for _ in range(T):
n = next(data)
pos = sorted(next(data) for _ in range(n)) # 按网格编号从左到右排列
# gaps[i] = 第 i 个棋子左边可自由移动的空间,最左棋子以 1 号格为左界
gaps = [pos[0] - 1] + [pos[i] - pos[i - 1] - 1 for i in range(1, n)]
# 阶梯 Nim:动第 0、2、4……段(从右数奇数段)等价于取石子堆,
# 动其余段总能被对手镜像回去,所以只看奇数段异或和
winner = "Georgia will win" if nim_xor(gaps) else "Bob will win"
out.append(winner)
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 <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
typedef long long ll;
ll T;
ll n;
ll pos[MAXN]; // pos[i]:第 i 个棋子所在的网格编号,排序后从左到右
void solve() {
cin >> T;
while (T--) {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> pos[i];
}
sort(pos + 1, pos + n + 1);
// 把棋子间的空格看成沿阶梯从右往左流动的石子:
// gap[i] = 第 i 个棋子左边可自由移动的空格数,最左棋子以 1 号格左侧为界。
// 从右往左数第 1、3、5……段参与 Nim 异或,这里直接从最右棋子隔一个取一个。
ll nim_sum = 0;
for (ll i = n; i >= 1; i -= 2) {
if (i == 1) {
nim_sum ^= pos[i] - 1;
} else {
nim_sum ^= pos[i] - pos[i - 1] - 1;
}
}
if (nim_sum != 0) {
cout << "Georgia will win\n";
} else {
cout << "Bob will win\n";
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
solve();
return 0;
}复杂度
每组数据排序
图示解析
阶梯重编号示意图
下图把样例二
flowchart LR
G0["g0=0<br/>第8段"] --> G1["g1=3<br/>第7段"]
G1 --> G2["g2=0<br/>第6段"] --> G3["g3=0<br/>第5段 ⭐"]
G3 --> G4["g4=1<br/>第4段"] --> G5["g5=2<br/>第3段 ⭐"]
G5 --> G6["g6=1<br/>第2段"] --> G7["g7=2<br/>第1段 ⭐"]
G7 --> OUT["流出游戏"]
棋子越靠右,其左侧间隙的"阶梯编号"越小;标 ⭐ 的第 1、3、5 段是参与 Nim 异或的奇数段。观察重点:向左移动任何一个棋子,都恰好是"某段减
样例一推演
| 间隙 | |||
|---|---|---|---|
| 值 |
奇数段异或和
总结
- 关键转化:排序后用相邻间隙描述局面,移动棋子 = 石子沿阶梯向左搬一段。
- 阶梯博弈:从右数奇数段石子异或和为
则先手必败,否则必胜;证明核心是" 时任何操作都会破坏它,而 时总能一步修复"。 - 两个易错点:最左间隙是
;异或的是从右数的奇数段。 - 复杂度
,代码核心只有一行异或循环。

