石子游戏

把最大必胜子游戏数转化为区间调度,预处理最早结束区间并用倍增回答询问。

OJ: shumeng

题目 ID: CSP202605D

难度:未知

标签:贪心区间调度倍增前缀异或离线

日期: 2026-07-31 16:22

形式化题目

给定长度为 nn 的序列 b1,,bnb_1,\dots,b_n。定义子游戏 (l,r)(l,r) 为:把 bl,,brb_l,\dots,b_r 重新编号成 1..rl+11..r-l+1 后,所有奇数位置石子数的异或和为 00 则小 C 必胜。共 qq 个询问 [L,R][L,R],要求把 [L,R][L,R] 分割成若干连续子区间,最大化其中小 C 必胜的子游戏数量。

思路

必胜子游戏的前缀异或判定

子游戏 (l,r)(l,r) 必胜当且仅当“奇数位置石子数异或和为 00”。注意判定只看子游戏内部的位置奇偶性:如果 ll 是原数组的奇数下标,那么子游戏内的奇数位置就是原数组的奇数下标,异或和为 00 等价于

odd[r]=odd[l1],\text{odd}[r]=\text{odd}[l-1],

其中 odd[i]\text{odd}[i] 是原数组奇数下标的异或前缀和。若 ll 是偶数下标,则用偶数下标的异或前缀和 even\text{even} 判定。

转成区间调度

不贡献必胜的子区间只负责填补空隙,可以任意插入。因此问题等价于:在 [L,R][L,R] 内选择最多互不相交的必胜子区间。这是经典的区间调度问题,贪心策略是每次选择结束最早且与已选区间不相交的必胜区间。

预处理每个起点的最早结束位置

逆序扫描 ll,维护 latest 记录当前后缀中每种前缀异或值最近出现的位置。对起点 ll,查找使 [l,r][l,r] 必胜的最早 rr,记为 first_good_end[l]。再令 best_end[l] 为“起点不小于 ll 的必胜区间中最早的结束位置”,一次贪心选择后的下一个起点就是 best_end[l] + 1

用倍增回答询问

LL 开始不断选择并跳转到下一个起点,跳转关系只与位置有关,与询问边界无关。对跳转做倍增预处理,询问时用二进制拆分统计在 [L,R][L,R] 内能选多少个区间,每次 O(logn)O(\log n)

代码

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-07-31 16:22
 * update_at: 2026-08-17 22:40
 */
#include <bits/stdc++.h>
using namespace std;

const int LOG = 21; // 2^21 > 1e6

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, q;
    cin >> n >> q;
    // 奇数下标与偶数下标各自的异或前缀和
    vector<unsigned int> prefix_odd(n + 1, 0);
    vector<unsigned int> prefix_even(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        unsigned int value;
        cin >> value;
        prefix_odd[i] = prefix_odd[i - 1];
        prefix_even[i] = prefix_even[i - 1];
        if (i & 1) prefix_odd[i] ^= value;
        else prefix_even[i] ^= value;
    }

    // 逆序扫描后,latest 保存当前后缀中每种前缀异或值的最小位置。
    // 键带一个奇数/偶数标记位,避免两类前缀异或互相串扰。
    unordered_map<unsigned long long, int> latest;
    latest.reserve((n + 1) * 4);
    vector<int> first_good_end(n + 1, n + 1);
    for (int left = n; left >= 1; left--) {
        unsigned long long odd_key =
            (static_cast<unsigned long long>(prefix_odd[left]) << 1) | 1ULL;
        unsigned long long even_key =
            (static_cast<unsigned long long>(prefix_even[left]) << 1);
        latest[odd_key] = left;
        latest[even_key] = left;

        unsigned long long target_key;
        if (left & 1) {
            target_key =
                (static_cast<unsigned long long>(prefix_odd[left - 1]) << 1) | 1ULL;
        } else {
            target_key =
                (static_cast<unsigned long long>(prefix_even[left - 1]) << 1);
        }
        unordered_map<unsigned long long, int>::iterator it =
            latest.find(target_key);
        if (it != latest.end()) first_good_end[left] = it->second;
    }

    // 选择起点不小于 left 的所有必胜区间中,结束位置最靠前的一个。
    // jump[left] 是选完一次最优区间后的下一个起点(结束位置 + 1)。
    vector<int> jump(n + 3, n + 2);
    int best_end = n + 1;
    for (int left = n; left >= 1; left--) {
        best_end = min(best_end, first_good_end[left]);
        if (best_end <= n) jump[left] = best_end + 1;
    }
    jump[n + 1] = n + 2;

    // 对跳转关系做倍增,询问时用二进制拆分快速计数
    vector<vector<int>> up(LOG, vector<int>(n + 3, n + 2));
    for (int i = 1; i <= n + 1; i++) up[0][i] = jump[i];
    for (int level = 1; level < LOG; level++) {
        for (int i = 1; i <= n + 1; i++) {
            up[level][i] = up[level - 1][up[level - 1][i]];
        }
    }

    // 对每个询问做贪心区间调度:从 left 开始,只要下一个区间还落在 [L,R] 内就选择
    for (int query = 0; query < q; query++) {
        int left, right;
        cin >> left >> right;
        int current = left;
        int answer = 0;
        for (int level = LOG - 1; level >= 0; level--) {
            int next_position = up[level][current];
            if (next_position <= right + 1) { // 选择 2^level 个区间后仍不越界
                current = next_position;
                answer += 1 << level;
            }
        }
        cout << answer << '\n';
    }
    return 0;
}

复杂度

  • 时间:预处理 O(nlogn)O(n\log n),每次询问 O(logn)O(\log n),总时间复杂度 O((n+q)logn)O((n+q)\log n)
  • 空间:倍增表 O(nlogn)O(n\log n),空间复杂度 O(nlogn)O(n\log n)

总结

允许零贡献区间后,分割 DP 可以转化为选择不相交的有效区间;固定每个起点的最早结束区间后,区间调度可用倍增加速。判定必胜的关键是把“子游戏内部奇偶位置”转成原数组上带奇偶标记的前缀异或。