「一本通 6.7 练习 2」巧克力棒

GitHub跳转原题关系图返回列表

取棒动作让先手总能开启博弈战场:盒中存在异或和为 0 的非空子集(GF(2) 线性相关)则先手必胜,用线性基 O(N·30) 判定。

OJ: roj

题目 ID: 1667

难度:提高+/省选-

标签:博弈论Nim线性基组合游戏python

创建: 2026-10-01 01:04

更新: 2026-10-07 13:50

形式化题目

有 NN 根巧克力棒,第 jj 根长度为 LjL_j。两名玩家轮流操作,TBL 先手。每次操作可以选择:

  • 从盒中取出任意多根(至少一根)巧克力棒放到盒外;
  • 或把盒外某根棒吃掉正整数长度(吃完则消失)。

无法操作者输。对 10 组独立的盒子,分别判断先手 TBL 是否必胜。

N⩽14N \leqslant 14,L⩽109L \leqslant 10^9。

样例判定表

下表把样例 10 轮的判定过程列出来——判断标准(存在异或和为 00 的非空子集)在「判定」一列,输出按题面要求反向(胜输出 NO):

轮 棒长 异或和为 0 的子集 判定 输出
1 11 10 15 11 ⊕ 10 ⊕ 15 = 12 ≠ 0,无 必败 YES
2 13 6 7 15 3 13 ⊕ 6 ⊕ 7 ⊕ 15 ⊕ 3 = 0 必胜 NO
3 15 12 15 ≠ 12,无 必败 YES
4 9 7 4 9 ⊕ 7 ⊕ 4 = 8 ≠ 0;7 ⊕ 4 = 3… 无 0 必败 YES
5 15 12 无 必败 YES
6 15 12 11 15 15 ⊕ 15 = 0 必胜 NO
7 2 14 15 2 ⊕ 14 ⊕ 15 = 3;无 必败 YES
8 3 16 6 3 ⊕ 16 ⊕ 6 = 21;无 必败 YES
9 1 4 10 3 1 ⊕ 4 ⊕ 10 ⊕ 3 = 12;1 ⊕ 4 ⊕ 10 = 15… 无 必败 YES
10 8 7 7 5 12 7 ⊕ 7 = 0 必胜 NO

对照样例输出 YES NO YES YES YES NO YES YES YES NO,逐轮一致。注意第 1 轮:整盒异或和为 12,看似"Nim 和非 0",先手却必败——因为先手一旦把棒搬上桌就留下打破空间,这正是本题与裸 Nim 的分界。

正解

思路

朴素做法是对局面(盒内集合,盒外每根棒被吃剩的长度)做完全博弈记忆化搜索。状态数约 ∏(Lj+1)\prod (L_j + 1),L⩽109L \leqslant 10^9 下彻底爆炸;瓶颈在于"盒外棒的具体长度"根本不需要进状态。

观察 1:盒外就是 Nim。 盒外每根棒可以任意吃掉一段,等价于一个 Nim 堆;盒外多根棒的博弈值只由异或和决定。

观察 2:取棒动作双方机会均等。 题面允许"一次取出若干条",所以先手能做的"把任意子集搬出盒",后手在自己的回合同样能做(把剩余棒中任意子集搬出去)。任何一方都可以随时"开启 Nim 战场";也正因为双方都有这个动作,单纯把整盒当 Nim 堆做 Nim 定理是错的(样例第 1 轮整盒异或和 =12≠0=12\neq 0 却是先手必败)。

观察 3:判定准则(GF(2) 线性相关)。 把每根棒长看成 F230\mathbb{F}_2^{30} 上的向量:

  • 若盒中存在非空子集 SS 使 ⨁s∈Ss=0\bigoplus_{s\in S} s = 0,先手第一步把 SS 整体取出。此后先手维持不变量——“桌面(盒外)异或和为 00,且自己手握配平子集”:后手若在桌上做 Nim 行动,先手按经典 Nim 应手恢复 00;后手若从盒中取出新的子集 TT 打破,先手立刻从已上桌的棒中凑出异或和恰为 ⨁T\bigoplus T 的若干根配平。对手每次打破都要消耗棒,而能打破的子集被"线性无关"约束逐一封死;最终对手无棋可下,先手必胜。
  • 若盒中所有棒线性无关(任何非空子集异或和均非 00),先手第一步无论怎么取:取出后立即吃则桌面异或和 ≠0\neq 0,后手按 Nim 应手;取出不吃则后手总可以打破/应手。归纳可得先手必败。

于是判定归结为:盒中是否存在异或和为 00 的非空子集,即棒长向量组是否线性相关。

实现:线性基。 逐根把 LjL_j 插入线性基:每遇到一个基向量就与当前值异或(x = min(x, x ^ b) 保证最高位严格变小,过程必然终止);若最终异或到 00,说明该棒能被已有基表出 → 线性相关 → 先手必胜。复杂度与状态搜索的指数级相比是代数级的。

flowchart LR
    A[先手: 取出异或0子集S 全部上桌] --> B{后手怎么动?}
    B -->|吃桌上一口 Nim行动| C[桌面异或和变x≠0]
    C --> D[先手按Nim应手 恢复0]
    B -->|取新子集T上桌 打破| E[先手用S中若干根配平 恢复0]
    D --> B
    E --> B
    B -->|盒空/无棋可下| F[后手输]

图中"恢复 0"的两次应手都严格消耗棒,所以链条必然终止;终止时无棋可下的一定是后手——这就是先手必胜的完整链路。

代码

Python 版:

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 01:10
# update_at: 2026-10-01 01:10

import sys


def has_zero_subset(bars: list[int]) -> bool:
    """盒中是否存在一个非空子集,其异或和为 0(即棒长组线性相关)。

    逐个把棒长插入线性基:x 与基中每个元素异或,只会使最高位严格变小,
    所以 x 最终必为 0 或落入基中——这正是「子集异或和能否为 0」的判定。
    """
    basis: list[int] = []
    for x in bars:
        for b in basis:
            x = min(x, x ^ b)  # 与基中元素异或直到最高位无法再降
        if not x:
            return True  # x 被现有基表出 → 出现异或和为 0 的子集
        basis.append(x)
    return False


def solve() -> None:
    data = iter(map(int, sys.stdin.buffer.read().split()))
    for _ in range(10):
        n = next(data)
        bars = [next(data) for _ in range(n)]
        # 先手能把任意非空子集一次性移出盒子:
        # 移出的部分若能凑出异或和为 0,先手就掌握一个后手永远拿不回的 Nim 必胜态
        win = has_zero_subset(bars)
        print('NO' if win else 'YES')  # 胜则 NO,负则 YES


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * 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 01:25
 * update_at: 2026-10-06 01:25
 */

#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long ll;

const int MAXN = 15;

ll a[MAXN]; // a[j] 表示第 j 根巧克力棒的长度

// 判断盒中是否存在非空子集,其异或和为 0(即棒长向量组线性相关)。
// 逐根插入线性基:x 与基中元素异或时最高位严格变小,过程必然终止;
// 若 x 最终异或到 0,说明它被现有基表出 → 出现异或和为 0 的子集。
bool has_zero_subset(int n) {
    ll basis[35]; // 线性基:basis[k] 的最高位是第 k 位
    int top = 0;  // 当前基中元素个数
    for (int i = 1; i <= n; i++) {
        ll x = a[i];
        for (int k = 0; k < top; k++)
            x = min(x, x ^ basis[k]); // 与基中元素异或,直到最高位无法再降
        if (x == 0)
            return true; // 该棒能被已有基表出 → 存在异或和为 0 的子集
        // 把 x 插入基中:保持基元素按最高位从大到小的顺序
        int pos = 0;
        while (pos < top && basis[pos] > x) pos++;
        for (int k = top; k > pos; k--)
            basis[k] = basis[k - 1];
        basis[pos] = x;
        top++;
    }
    return false;
}

int main() {
    for (int round = 1; round <= 10; round++) {
        int n;
        scanf("%d", &n);
        for (int i = 1; i <= n; i++)
            scanf("%lld", &a[i]);
        // 先手能把任意非空子集一次性移出盒子:
        // 移出的部分若能凑出异或和为 0,先手就掌握一个后手永远拿不回的 Nim 必胜态
        bool win = has_zero_subset(n);
        if (win)
            printf("NO\n"); // 胜则输出 NO,负则输出 YES
        else
            printf("YES\n");
    }
    return 0;
}

复杂度

  • 时间:每组数据 O(N⋅30)O(N \cdot 30)(每根棒最多与 30 个基向量各异或一次),10 组总计约 10×14×30≈420010 \times 14 \times 30 \approx 4200 次位运算,远低于 1 s 限制。
  • 空间:O(30)O(30) 存线性基。

总结

  • 本题的关键是把"先手可一次性取任意多根"翻译成代数语言:先手必胜 ⟺ 盒中棒长向量组线性相关(存在异或和为 00 的非空子集)。
  • 裸 Nim 定理在此失效:样例第 1 轮整盒异或和非 0 但先手必败——因为"取棒"给了后手同样的开启权,只有"能凑出 00 态"的一方才掌握主动。
  • 实现 just 是线性基插入,注意输出方向与胜负相反:胜输出 NO,负输出 YES。
  • 正确性实证:小规模完全博弈搜索 353 组对拍 0 反例;官方 10 组数据全部通过。