博弈论例题

例 1:洛谷 P2148 [SDOI2009] E&D

暴力推 SG 函数找规律。
显然有 \(sg(x, y) = \text{mex} \{ sg(a, b) \}\),需要满足 \(a, b \geq 1\)\(a+b\) 等于 \(x\)\(y\)

0 1 0 2 0 1 0 3 0 1 0 2 0 1 0 4 
1 1 2 2 1 1 3 3 1 1 2 2 1 1 4 4 
0 2 0 2 0 3 0 3 0 2 0 2 0 4 0 4 
2 2 2 2 3 3 3 3 2 2 2 2 4 4 4 4 
0 1 0 3 0 1 0 3 0 1 0 4 0 1 0 4 
1 1 3 3 1 1 3 3 1 1 4 4 1 1 4 4 
0 3 0 3 0 3 0 3 0 4 0 4 0 4 0 4 
3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4 
0 1 0 2 0 1 0 4 0 1 0 2 0 1 0 4 
1 1 2 2 1 1 4 4 1 1 2 2 1 1 4 4 
0 2 0 2 0 4 0 4 0 2 0 2 0 4 0 4 
2 2 2 2 4 4 4 4 2 2 2 2 4 4 4 4 
0 1 0 4 0 1 0 4 0 1 0 4 0 1 0 4 
1 1 4 4 1 1 4 4 1 1 4 4 1 1 4 4 
0 4 0 4 0 4 0 4 0 4 0 4 0 4 0 4 
4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 

规律还是有点难找的,从分治角度思考会容易一些。
由于矩阵长得很像分治形式,所以可以从小到大枚举分治长度。
附 SG 打表代码。

#include <bits/stdc++.h>
using namespace std;

/*
int sg[N][N];
bool st[N][N];

inline int get(int x, int y) {
    if (st[x][y]) return sg[x][y];
    set<int> s;
    for (int a = 1; a < x && a <= x - a; a++)  s.insert( get(a, x - a) );
    for (int a = 1; a < y && a <= y - a; a++) s.insert( get(a, y - a) );
    sg[x][y] = 0; while (s.find(sg[x][y]) != s.end()) sg[x][y]++;
    st[x][y] = 1; return sg[x][y];
}
*/

inline int get(int x, int len) { return (x % len == 0 ? len : x % len); }
inline int sg(int x, int y) {
    if (x & y & 1) return 0;
    for (int t = 1; t <= 30; t++) {
        int len = 1 << t, mid = len >> 1;
        // cout << t << ' ' << get(x, len) << ' ' << get(y, len) << ' ' << mid << endl;
        if (get(x, len) <= mid && get(y, len) <= mid) return t - 1;
    }
    return 31;
}

int T, n, x, y, res;
int main() {
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &n), res = 0, n >>= 1;
        for (int i = 1; i <= n; i++) scanf("%d%d", &x, &y), res ^= sg(x, y);
        puts(res ? "YES" : "NO");
    }
    return 0;
}

例 2:CF317D Game with Powers

暴力推 SG 函数感觉没什么道理。
不过可以想到把 \(x, x^2, x^3, \dots\) 看作同一个子问题,求子问题的 SG 值即可(这里规定 \(x \not= a^k\),即 \(x\) 是极小的底数)。
\(x^c \leq n, x^{c+1} \gt n\)(即 \(c\) 为极大的指数),由于对于这些子问题 \(x\) 是多少并不重要,所以求出 \(sg_c\) 即可。
不难写出打表 SG 函数代码,对于当前局面枚举对哪一个指数进行操作,它的倍数会全部被覆盖:

然后就可以直接分类子问题做了。
注意对于 \([\lfloor \sqrt{n} \rfloor + 1, n]\) 这一段,参与答案统计的数需要保证它不能被表示为 \(x=a^k\) 的形式

/*
map<int, int> sg[40];
inline bool chk(int S, int i) { return (S >> i) & 1; }
int dfs(int S, int u) {
	if (S == 0) return 0;
	if (sg[u][S]) return sg[u][S];
	set<int> s; s.clear();
	for (int i = 1; i < u; i++) {
		if (!chk(S, i - 1)) continue;
		int T = S;
		for (int j = i; j <= u; j += i)
			if (chk(T, j - 1)) T ^= (1 << j - 1);
		s.insert( dfs(T, u) );
	}
	while (s.find(sg[u][S]) != s.end()) sg[u][S]++;
	return sg[u][S];
}

for (int i = 1; i <= 30; i++) dfs((1ll << i) - 1, i), printf("%d, ", sg[i][(1 << i) - 1]);
*/
-------------------

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
const int sg[] = {0, 1, 2, 1, 4, 3, 2, 1, 5, 6, 2, 1, 8, 7, 5, 9, 8, 7, 3, 4, 7, 4, 2, 1, 10, 9, 3, 6, 11, 12};
int n, m, res, alone;
bool st[N];

int main() {
    scanf("%d", &n), m = sqrt(n), res = 1;  // i=1 -> 1
    for (int i = 2; i <= m; i++) {
        if (st[i]) continue;
        int cnt = 1;
        for (long long j = i * i; j <= n; j *= i) {
            cnt++;
            if (j <= m) st[j] = 1; else alone++;
        }
        res ^= sg[cnt];
    }
    res ^= (n - m - alone) & 1;
    puts(res ? "Vasya" : "Petya");
    return 0;
}

例 3:CF138D World of Darkraft

比较神秘的题。

由于 \(n, m \leq 20\) 和抽象的操作,预感应该不需要用到打表找规律。
最暴力的当然是把网格压缩成 \(01\) 状态,然后暴力枚举操作哪个格子,找后继状态求 SG 值,复杂度上天。
如何简化状态?只考虑哪些主副对角线被覆盖了即可,解析式分别为 \(x+y=k, y-x+n=k\),状态数 \(O(2^{2(n+m)})\) 级别依然上天。

事实上没有必要纠结于记录“已经操作过的主副对角线”,可以换个角度,去记录“未被覆盖的区域”。
但每一次操作都是斜着切一条线,不好刻画这个区域的状态表示。
不妨旋转坐标轴(类似于转化为切比雪夫距离),每一次操作相当于横着切一刀、竖着切一刀、十字切中的一种。
由于切完之后各部分是独立的,可以看作子问题 SG 函数,所以可以设 \(sg_{x_1, y_1, x_2, y_2}\) 表示左上角为 \((x_1, y_1)\),右下角为 \((x_2, y_2)\) 区域矩形的 SG 值。

于是又变成子问题,递归求解即可。
注意要先对原网格黑白染色,黑白分开做。因为对角线染色,两个子问题是独立的。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <set>
using namespace std;
const int N = 50;
int n, m;
char ch[N][N], mp[N][N];
int sg[N][N][N][N][2]; bool st[N][N][N][N][2];

int dfs(int x1, int x2, int y1, int y2, int opt) {
    if (x1 == x2 && y1 == y2) return 0;
    if (x1 > x2 || y1 > y2) return 0;
    if (st[x1][x2][y1][y2][opt]) return sg[x1][x2][y1][y2][opt];
    set<int> s; s.clear();
    for (int x = x1; x <= x2; x++)
        for (int y = y1; y <= y2; y++) {
            if (mp[x][y] == '.') continue;
            int xx = (x - y + n) / 2, yy = (x + y - n) / 2;
            if ((xx + yy & 1) ^ opt) continue;
            if (mp[x][y] == 'L') s.insert(dfs(x1, x - 1, y1, y2, opt) ^ dfs(x + 1, x2, y1, y2, opt));
            if (mp[x][y] == 'R') s.insert(dfs(x1, x2, y1, y - 1, opt) ^ dfs(x1, x2, y + 1, y2, opt));
            if (mp[x][y] == 'X') s.insert(dfs(x1, x - 1, y1, y - 1, opt) ^ dfs(x1, x - 1, y + 1, y2, opt) ^ dfs(x + 1, x2, y1, y - 1, opt) ^ dfs(x + 1, x2, y + 1, y2, opt));
        }
    st[x1][x2][y1][y2][opt] = 1;
    int &res = sg[x1][x2][y1][y2][opt]; res = 0;
    while (s.find(res) != s.end()) res++;
    return res;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("\n %s", ch[i] + 1);
    for (int i = 1; i <= n + m; i++)
        for (int j = 1; j <= n + m; j++) mp[i][j] = '.';

    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) mp[i + j][j - i + n] = ch[i][j];

    // for (int i = 1; i <= n + m; i++, puts(""))
    //     for (int j = 1; j <= n + m; j++) cout << mp[i][j];
    puts( dfs(1, n + m, 1, n + m, 0) ^ dfs(1, n + m, 1, n + m, 1) ? "WIN" : "LOSE");
    return 0;
}

例 4:CodeForces Gym - 102341G

PS:从一个错误的切入点搞出了正解?

首先,\(\texttt{111}\) 可以转移到 \(\texttt{110, 011, 101}\),其中前两者又可以转移到 \(\texttt{010}\)
不妨设 \(c_i\) 表示第 \(i\)最多能操作多少次,则问题转化为:

给定 \(c_1, \dots, c_n\) 满足 \(0 \leq c_i \leq 2\),每次可以选择 \(c_i \gt 0\),将其替换为 \(0 \leq j \lt c_i\),操作过程中不能出现 \(i \in [1, n), c_i = c_{i+1} = 0\)

写个程序算一下,不妨状压每一位 \(\texttt{0, 1, 2}\),合法的状态数达到了高贵的 \(578,272,256\) 种。
不过 \(c_i = 0\) 可以看作“隔板”?相当于给上下两部分隔开,边界处不能操作成 \(0\)
可以考虑设 \(sg_{l, r, 0/1, 0/1}\)?表示 \([l, r]\) 层,上下界是否有限制……
然后回过头再看一下正确性,发现不对:这样做无法区分 \(\texttt{101, 010}\) 两种情况,它们都被标记为 \(0\)
……


那就怎么暴力做吧,先注意到 \(\texttt{110, 011}\) 是等价的,所以总共只有四种情况状压 \(O(4^n)\)
不过之前的假做法确实有一些启发效果,\(\texttt{101,010}\) 依然可以看作“隔板”,都不能被操作,只是它们的限制不同而已。
那还剩下 \(\texttt{111, 110/011}\) 两种情况,这时候状压就只剩 \(O(2^n)\) 种情况了!

不妨设 \(sg_{n, S}\) 表示长度为 \(n\) 的区间(不含“隔板”),状态为 \(S\) 的 SG 值。
转移枚举是哪一个位置操作,是否会操作成隔板,分两个子问题异或即可。


然后写了一个晚上的代码调不过去 /ll 再加上常数大有 T 飞的风险。
于是换了一种写法:

\(sg_{n,S}\) 表示塔高为 \(n\),状态为 \(S\) 的 SG 值。(不考虑上下界是否能操作成 \(\texttt{010}\)
转移还是一样枚举每一位考虑操作成什么,分裂为子问题合并。
在查询的时候再额外考虑两端即可,具体地,当一个位置 \(i\) 原本为 \(\texttt{010}\),它左右两边若为 \(\texttt{110}\),则 SG 值为 \(0\);若为 \(\texttt{111}\),则 SG 值为 \(1\),因此直接取这一位的状态就可以作为子问题的 SG 值。

#include <bits/stdc++.h>
using namespace std;
const int N = 22, M = (1 << 20) + 5;
int T, n;
char ch[5]; int str[N];
#define chk(S, x) (((S) >> (x)) & 1)
inline int getval() {
    if (ch[1] == 'I' && ch[2] == 'I' && ch[3] == '.') return 0;
    if (ch[1] == '.' && ch[2] == 'I' && ch[3] == 'I') return 0;
    if (ch[1] == 'I' && ch[2] == 'I' && ch[3] == 'I') return 1;
    if (ch[1] == 'I' && ch[2] == '.' && ch[3] == 'I') return 2;
    if (ch[1] == '.' && ch[2] == 'I' && ch[3] == '.') return 3;
    return 0;
}
inline void insert(long long &x, int i) { x |= (1 << i); }

int SG[N][M];
int sg(int n, int S) {
    if (~SG[n][S]) return SG[n][S];
    if (!n) return 0;
    long long vis = 0;
    for (int i = 1; i <= n; i++) {
        if (chk(S, i - 1)) {
            insert( vis, sg(i - 1, S & ((1 << i - 1) - 1)) ^ sg(n - i, S >> i) );    // III -> I.I,隔板
            insert( vis, sg(n, S ^ (1 << i - 1)) );  // III -> II./.II
        } else {
            int ls = 0, rs = 0;
            if (i > 1) ls = sg(i - 2, S & ((1 << i - 2) - 1)) ^ chk(S, i - 2);
            if (i < n) rs = sg(n - i - 1, S >> i + 1) ^ chk(S, i);
            insert( vis,ls ^ rs);
        }
    }
    int res = 0; while (chk(vis, res)) res++;
    return SG[n][S] = res;
}

int solve(int l, int r) {
    if (l > r) return 0;
    if (l == r) {
        if (l > 1 && str[l - 1] == 3) return str[l];
        if (r < n && str[r + 1] == 3) return str[r];
        return sg(1, str[l]);
    }
    int res = 0, S = 0;
    if (l > 1 && str[l - 1] == 3) res ^= str[l], l++;
    if (r < n && str[r + 1] == 3) res ^= str[r], r--;
    for (int i = l; i <= r; i++) S = S << 1 | str[i];
    res ^= sg(r - l + 1, S);
    return res;
}

int main() {
    memset(SG, -1, sizeof SG);
    for (int i = 1, x; i <= 20; i++) for (int S = 0; S < (1 << i); S++) x = sg(i, S);
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        int res = 0;
        for (int i = 1; i <= n; i++) scanf("\n %s", ch + 1), str[i] = getval();
        for (int i = 1, j = 1; i <= n; i = j + 1) {
            j = i; while (j <= n && str[j] <= 1) j++;
            res ^= solve(i, j - 1);
        }
        puts(res ? "First" : "Second");
    }
    return 0;
}

例 5:CF1091H New Year and the Tricolore Recreation

首先凭直觉,质数数量一定很少。再凭直觉,质数的乘积 \(\leq 2 \times 10^5\) 的一定也很少。
事实证明,只有 \(108358\) 个,与 \(n\) 同阶。

另外同时将左边两个往右移动,和将右边的往左移动是等价的,所以不难想到双方都可以操作左右两边,让它往中间靠 \(d\) 单位长度。
于是左右两边子问题独立了,现在是 \(2n\) 堆石子的 Nim 游戏,每次只能取 \(p\)\(p_1 \times p_2\) 个石子,问谁有必胜策略。
由于还限定了一个变量 \(f\),不难想到暴力推 SG 函数,但是这大致是 \(O(n^2)\) 的复杂度,撑死跑到第六个点。

然后打个表(钦定 \(f=0\))发现 SG 值最多就 26,凭直觉 \(f \not= 0\) 的时候应该也高不到哪去。
于是就愉快地按 SG 值分类,设 \(\text{list}_t\) 表示 SG 值为 \(t\)下标集合\(A\) 表示 \(p, p_1 \times p_2\) 的集合。
现在相当于要求,是否存在 \(j \in \text{list}_t\),使得 \(i - j \in A\),如果不存在,则 \(\text{mex}\) 集合中没有 \(t\)。从小到大枚举 \(t\) 即可得到 SG 值(\(\text{mex}\))。
这个转化很玄幻,化简一下大概是:

给定一个正整数集合 \(A\),维护一个正整数集合 \(B\),支持两种操作:

  1. \(B\) 中插入一个正整数 \(b\)
  2. 给定一个正整数 \(x\),求是否存在一种方案,使得 \(A\) 中存在 \(a\)\(B\) 中存在 \(b\)\(x=a+b\)

两个集合元素相加为 \(x\),看上去很像 FFT/NTT……然后直接多项式肯定没有什么道理,所以不妨分治 FFT/NTT……
然后在这里硬刚了半小时发现完全没有出路。
但是观察到“多项式”的系数只有 \(0,1\) 两种,所以不妨用二进制表示,联想到 bitset,也可以位移

改成“主动转移”(还是考虑化简的问题),当向 \(B\) 中插入元素 \(b\) 的时候,所有 \(a+b\) 标记为存在。
如果把 \(A\) 中的元素投影到值域上,那么相当于 \(A\) 的 bitset 集体左移 \(b\) 单位,得到的 bitset 值都是存在。

复杂度 \(O(\frac{nm}{\omega} + cn)\),其中 \(m=108358\)\(c\)\(\text{mex}\) 的上界。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, f, a[N], Max;

bool st[N]; int pr[N], tot;
int mul[N], idx = 0;
bitset<N> vis[105], bit;
void init() {
    for (int i = 2; i <= 200000; i++) {
        if (!st[i]) pr[++tot] = i, mul[++idx] = i;
        for (int j = i + i; j <= 200000; j += i) st[j] = 1;
    }
    int ans = 0;
    for (int i = 1; i <= tot; i++)
        for (int j = 1; j <= tot; j++)
            if (pr[i] * 1ll * pr[j] <= 200000) mul[++idx] = pr[i] * 1ll * pr[j];
            else break;
    for (int i = 1; i <= idx; i++) bit[mul[i]] = 1;
    bit[f] = 0;
    // cout << idx << endl;
}
int sg[N];

int main() {
    scanf("%d%d", &n, &f);
    init();
    for (int i = 1, x, y, z; i <= n; i++) scanf("%d%d%d", &x, &y, &z), a[i] = y - x - 1, a[i + n] = z - y - 1;
    n <<= 1; for (int i = 1; i <= n; i++) Max = max(Max, a[i]);
    vis[0] = bit;
    for (int i = 1; i <= Max; i++) {
        for (int j = 0; j <= 100; j++)
            if (!vis[j][i]) { sg[i] = j; break; }
        vis[sg[i]] |= bit << i;
    }
    // for (int i = 1; i <= Max; i++) cout << sg[i] << ' '; puts("");
    int res = 0;
    for (int i = 1; i <= n; i++) res ^= sg[a[i]];
    puts(res ? "Alice\nBob" : "Bob\nAlice");
    return 0;
}

例 6:洛谷 P3480 [POI 2009] KAM-Pebbles

不难想到先差分,然后就转化为了阶梯 Nim游戏。
题目条件很诈骗啊,\(n, \sum a_i \leq 1000\),第一眼还以为是设 \(sg_{i,j}\) 然后硬推。

阶梯 Nim 有结论:只和奇数位上的石子有关,因为在偶数位操作,后手一定可以模仿先手把石子往后再移动一次,使得先手操作无效化
因此只要把奇数位上的石子做异或和(普通 Nim)即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int T, n, a[N], b[N], res;
int main() {
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &n), res = 0;
        for (int i = 1; i <= n; i++) scanf("%d", &a[i]), b[i] = a[i] - a[i - 1];
        reverse(b + 1, b + 1 + n);
        for (int i = 1; i <= n; i += 2) res ^= b[i];
        puts(res ? "TAK" : "NIE");
    }
    return 0;
}

例 7:[AGC017D] Game on Tree

非常喵的题。
操作等价于删一个子树,然后删了就是删了,后手没法模仿先手,干瞪着也看不出性质,所以只能试 SG 函数。
\(sg_u\) 表示 \(u\) 子树的 SG 函数,操作的是边,所以只考虑边的贡献,和点没有半毛钱关系。
\(v\)\(u\) 的儿子,所有儿子子树之间是互相独立的子问题,所以可以 SG 值异或。

现在考虑 \(u\) 只有一个儿子 \(v\) 的 SG 值怎么求,暴力地,枚举删子树哪一条边。
根据 \(\text{mex}\) 的性质,随意在 \(v\) 子树内删边只能形成 \([0, sg_v)\) 这些局面,删 \((u, v)\) 可形成 \(0\) 的局面。
手模几个小样例可得 \(sg_u = sg_v + 1\),证明可以归纳,不断往下缩小子树直到叶子仍成立。

再合并子问题,即 \(sg_u = \oplus_{v \in son_u} (sg_v + 1)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5, M = N << 1;
int n, h[N], e[M], ne[M], idx = 0;
inline void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; }
int sg[N];
void dfs(int u, int fa) { for (int i = h[u]; ~i; i = ne[i]) if (e[i] ^ fa) dfs(e[i], u), sg[u] ^= sg[e[i]] + 1; }

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) h[i] = -1;
    for (int i = 1, u, v; i < n; i++) scanf("%d%d", &u, &v), add(u, v), add(v, u);
    dfs(1, 0);
    puts(sg[1] ? "Alice" : "Bob");
    return 0;
}

例 8:洛谷 P2594 [ZJOI2009] 染色游戏

看上去好像只能推 SG 函数。
每次选一个右下角,操作它左上角的连通块,这事情很魔怔,先看弱化版。
一维版本的问题:每次选择一个反面,反转它和它左边的一段连续段。
这个很容易打表得 \(sg_i = \text{lowbit}(i)\)

对于二维情况,手模可得都是 \(2^t\) 形式,且 \(t\)\(i,j\) 相关。
事实上 \(sg_{i,j} = 2^{i+j-2} \quad \quad (i, j \gt 1)\)

然后每一个位置的硬币都是一个子游戏,异或 \(\text{SG}\) 值即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int T, n, m, res;
char mp[N][N];

inline int lowbit(int x) { return x & -x; }
inline int sg(int x, int y) {
	if (x == 1 || y == 1) return lowbit(x + y - 1);
	return 1 << (x + y - 2);
}

int main() {
	scanf("%d", &T);
	while (T--) {
		scanf("%d%d", &n, &m), res = 0;
		for (int i = 1; i <= n; i++) scanf("\n %s", mp[i] + 1);
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= m; j++)
				if (mp[i][j] == 'T') res ^= sg(i, j);
		puts(res ? "-_-" : "=_=");
	}
	return 0;
}

例 9:CF494E Sharti

很难不想到推 SG 函数。
显然根据经典结论,答案的 SG 值等于每个白点单独的 SG 值异或和

#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int n, k;
int sg[N][N];
bool vis[N];
int main() {
	scanf("%d%d", &n, &k);
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			memset(vis, 0, sizeof vis); vis[0] = 1;
			for (int l = 2; l <= k && l <= min(i, j); l++) {
				int res = 0;
				for (int x = i - l + 1; x <= i; x++)
					for (int y = j - l + 1; y <= j; y++) res ^= sg[x][y];
				vis[res] = 1;
			}
			sg[i][j] = 0; while (vis[sg[i][j]]) sg[i][j]++;
		}
	}
	for (int i = 1; i <= n; i++, puts(""))
		for (int j = 1; j <= n; j++) cout << i << ' ' << j << ' ' << sg[i][j] << endl;
	return 0;
}

注意到:

\[sg(x, y) = \min \{ \text{lowbit}(x), \text{lowbit}(y), \text{highbit}(k) \} \]

具体注意的方法:

  • 全都是 \(2^t\) 的形式,推测和 \(\text{bit}\) 有关。
  • 固定 \(n\) 不断切换 \(k\),发现和 \(k\)\(\text{highbit}\) 相关。
  • 再把 \(\text{highbit}(k)\) 开到足够大,关注 \(x, y\) 两维,发现和 \(\text{lowbit}\) 相关。

于是现在要求染白部分的 \(\text{SG}\) 值异或和。
很难不想到扫描线,但是直接维护三个变量的 \(\min\) 不好做啊。
\(n \leq 5 \times 10^4\) 较小,提示我们按位

具体地,每次枚举 \(p\),求线段树维护区域内有多少 \((x, y)\) 的贡献是 \(2^p\)
不妨只计算 \(\min \{ \text{lowbit}(x), \text{lowbit}(y) \}\),再和 \(\text{highbit}(k)\) 比较。
先扫 \(x\) 维,求出这一块子矩形中有 \(\text{cx}\)\(x\) 满足 \(\text{lowbit}(x) \geq p\),若 \(\text{cx}\) 为奇数(基于异或对消原则)再考虑 \(y\) 维的贡献。
由于钦定 \(\text{lowbit}(x) \geq p\),现在需要钦定 \(\text{lowbit}(y) = p\),求区域内的 \(y\) 的数量。

所以现在相当于要求 \(\text{solve}(l, r, p)\) 表示 \(x \in [l, r]\) 满足 \(\text{lowbit}(x) = 2^p\) 的数量。

\[\text{solve}(l, r, p) \to \Big[ \lfloor \frac{l - 1}{2^p} \rfloor \times 2^p + 2^p , \lfloor \frac{r}{2^p} \rfloor \times 2^p \Big] \]

然后动态开点线段树维护扫描线即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, m, k, high;
int arr[N << 1], tot;

struct node { int x, l, r, opt; } seg[N << 1];
inline bool cmp(node a, node b) { return a.x < b.x; }
inline int pre(int x, int p) { return x / p - x / p / 2; }
inline int get(int l, int r, int p) { return pre(r, p) - pre(l - 1, p); }

struct Tree {
    int l, r, flag;  // 覆盖标记
    int c[31];  // 2^p 的出现次数
} tr[N << 3];
inline void pushup(int u) {
    if (tr[u].flag) {
        int l = arr[tr[u].l], r = arr[tr[u].r + 1] - 1;
        for (int i = 0; i <= 30; i++) tr[u].c[i] = get(l, r, 1 << i);
    } else {
        for (int i = 0; i <= 30; i++) tr[u].c[i] = (tr[u].l == tr[u].r) ? 0 : (tr[u << 1].c[i] + tr[u << 1 | 1].c[i]);
    }
}
void build(int u, int l, int r) {
    tr[u].l = l, tr[u].r = r, tr[u].flag = 0;
    if (l == r) return;
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
void change(int u, int l, int r, int opt) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].flag += opt, pushup(u), void();
    int mid = tr[u].l + tr[u].r >> 1;
    if (l <= mid) change(u << 1, l, r, opt);
    if (r > mid) change(u << 1 | 1, l, r, opt);
    pushup(u);
}

int ans = 0;

int main() {
    // cout << get(1, 1, 1) << endl; return 0;
    scanf("%d%d%d", &n, &m, &k);
    int tt = k; high = 1; while (tt) high <<= 1, tt >>= 1; high >>= 1;

    for (int i = 1, x, y, xx, yy; i <= m; i++) {
        scanf("%d%d%d%d", &x, &y, &xx, &yy), xx++, yy++;
        // x--, xx--;
        arr[++tot] = y, arr[++tot] = yy;
        seg[(i << 1) - 1] = (node){x, y, yy, 1};
        seg[i << 1] = (node){xx, y, yy, -1};
    } m <<= 1;

    sort(arr + 1, arr + 1 + tot), tot = unique(arr + 1, arr + 1 + tot) - arr - 1;
    for (int i = 1; i <= m; i++) seg[i].l = lower_bound(arr + 1, arr + 1 + tot, seg[i].l) - arr, seg[i].r = lower_bound(arr + 1, arr + 1 + tot, seg[i].r) - arr;
    sort(seg + 1, seg + 1 + m, cmp);
    // for (int i = 1; i <= m; i++) cout << seg[i].x << ' ' << seg[i].l << ' ' << seg[i].r << ' ' << seg[i].opt << endl;

    build(1, 1, tot);
    int sum = 0;
    for (int i = 1; i < m; i++) {
        change(1, seg[i].l, seg[i].r - 1, seg[i].opt);
        if (seg[i].x ^ seg[i + 1].x) {  // 统计扫过矩形的贡献
            int cx = 0, cy = 0, sx = 0, sy = 0; // sx: lowbit(x) >= 2^p 的 x 的数量
            for (int p = 30; p >= 0; p--) {
                sx += (cx = get(seg[i].x, seg[i + 1].x - 1, 1 << p) );
                cy = tr[1].c[p]; // cy: lowbit(y) == 2^p 的 y 的数量
                if (sx & cy & 1) ans ^= min( (1 << p), high );
                if (cx & sy & 1) ans ^= min( (1 << p), high );
                sy += cy;
            }
        }
    }
    // cout << ans << endl;
    puts(ans ? "Hamed" : "Malek");
    return 0;
}

例 10:CF1149E Election Promises

很困难的题 /ll

首先看到 DAG 不难联想到有向图游戏,果断设 \(sg_u\) 表示图中 \(u\)\(\text{SG}\) 值。

\[sg_u = \text{mex} _{u \to v} \{ sg_v \} \]

然后忽略了点权这件事,点权长得很像 Nim 游戏,但是设 \(sg_{u,i}\) 又非常没有道理。
再说,这些子游戏(连通块)内部,不同的“起点”(入度为 \(0\))会互相影响,直接把入度为 \(0\) 的位置进行统计也没有任何道理。
不过可以隐约感觉到是一个 有向图游戏套 Nim 的东西。


\(sg_u\) 表示 DAG 上博弈的 \(\text{SG}\) 值是没有问题的。

结论:
\(sg_u\) 分类,当每一类内部的点权异或和都为 \(0\) 时,先手必败。

\(S_i\) 表示 \(sg=i\) 的点集中的点权异或和。
首先若存在 \(S_i \not= 0\),肯定可以通过一次操作使 \(S_i = 0\)

证明:
\(i = sg_u\),首先由于 \(sg_u = \text{mex} _{u \to v} \{ sg_v \}\),一定有 \(sg_u \not= sg_v\)
一次只能操作一个点,由于它的邻居 \(\text{SG}\) 值和它不同,一定不会影响 \(S_i\)
相当于需要把这个点操作成 \(S_i \oplus a_u\),由于这个点点权必须减小,转化为证明 \(S_i \oplus a_u \lt a_u\)
\(S_i\) 二进制下最高位 \(1\) 对应的那个点即可(最高位 \(1\) 被消掉)。

然后证明可以通过一次操作使所有 \(S_i=0\)

证明:
选择 \(\text{SG}\) 值最大的点 \(u\),对 \(u\) 进行操作,同时由于它的 \(\text{SG}\)最大\(u \to v\) 的点 \(v\) 一定满足 \(sg \in [0, sg_u)\)
因此操作点 \(u\) 可以同时修改所有 \(S_i, i \in [0, sg_u]\),均可以改为 \(S_i = 0\)

因此只要最开始 \(\exists S_i \not= 0\),先手就能通过一次操作把 \(S_i\) 全部改为 \(0\)
接下来后手无论如何操作,先手都能通过一次操作把局面改回 \(0\) 的状态,所以此时先手必胜。
(若 \(\text{SG}\) 值最大的点 \(sg_u = i\),且 \(S_i = 0\),则挑选次大的 \(\text{SG}\) 即可,以此类推)
反之若最开始的局面是 \(\forall S_i = 0\),就是先手必败。

这样 \(\text{SG}\) 值最大的一定会不断减小直到 \(0\),然后操作 \(\text{SG}\) 更小的点。
因此最后的局面一定是 \(\forall a_u = 0\),游戏结束无法操作。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5, M = N << 1;
int n, m, a[N];
int in[N], h[N], e[M], w[M], ne[M], idx = 0;
int sg[N], sum[N], Max;
bool vis[N];
inline void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; }

queue<int> q;

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]), h[i] = -1;
    for (int i = 1, u, v; i <= m; i++) scanf("%d%d", &u, &v), add(u, v, 0), add(v, u, 1), in[u]++;
    for (int i = 1; i <= n; i++) if (!in[i]) q.push(i);

    while (q.size()) {
        int u = q.front(); q.pop();
        for (int i = h[u]; ~i; i = ne[i]) if (!w[i]) vis[sg[e[i]]] = 1;
        sg[u] = 0; while (vis[sg[u]]) sg[u]++;
        for (int i = h[u]; ~i; i = ne[i]) if (!w[i]) vis[sg[e[i]]] = 0;
        for (int i = h[u]; ~i; i = ne[i]) { if (!w[i]) continue; if (!(--in[e[i]])) q.push(e[i]); }
    }
    for (int i = 1; i <= n; i++) sum[sg[i]] ^= a[i], Max = max(Max, sg[i]);
    for (int i = Max; i >= 0; i--) if (sum[i]) {
        puts("WIN");
        int u = 0, high = 1, x = sum[i];
        while (x) x >>= 1, high <<= 1; high >>= 1;
        for (int j = 1; j <= n; j++) if (sg[j] == i && (a[j] & high)) { u = j; break; }
        for (int j = h[u]; ~j; j = ne[j]) {
            int v = e[j]; if (w[j] || vis[sg[v]]) continue;
            a[v] = sum[sg[v]] ^ a[v], vis[sg[v]] = 1;
        } a[u] = sum[i] ^ a[u];
        for (int j = 1; j <= n; j++) printf("%d ", a[j]);
        return 0;
    }
    puts("LOSE");
    return 0;
}

例 11:QOJ 7606. Digital Nim

Tag:博弈论 + 数位 DP。

不难想到直接设 \(sg_i\) 表示 \(i\)\(\text{SG}\) 值,设 \(S(i)\) 表示 \(i\)数位和
则有 \(sg_i = \text{mex} _{j=1}^{S(i)} \{ sg_{i-j} \}\)
询问的 \(n \leq 10^{18}\),暴力算复杂度上天,打表也找不出什么规律。

但现在关键在于不需要知道确切的 \(\text{SG}\),只需要求出是否必败
不妨设 \(pre_i\) 表示 \(i\) 和上一个必败态 \(j\) 的距离 \((i-j)\)
只要满足 \(pre_i \gt S(i)\),那么 \(i\) 就是必败态

目前仍然没有什么进展,但 \(n \leq 10^{18}\) 可以考虑数位 DP
考虑数位 DP 的本质:快速计算大量重复或相似的状态。
例如对 \(135 \dots\)\(531 \dots\) 这两个数,前面三位已经确定,且数位和相等,对后面位填数的限制也是相同的。
所以目前影响状态的因素有数位和、剩余位数

要用数位 DP 计算什么
\(d\) 表示到区间左端点最近的必败态距离,我们希望快速算出 \(+k \times 10^t\) 之后的 \(d'\)(到右端点最近必败态距离)
所以还有影响因素距离 \(dist\)

综上所述,设 \(dp_{u,sum,dist}\) 表示剩余 \(u\) 位,已经确定的部分数位和为 \(sum\),最近必败态到左端点距离 \(dist\),转移到 \(+10^u\) 位置的距离是多少。
转移只要枚举下一位填 \(i\),把 \(dp_{u-1,sum+i,}\) 得到的结果首尾拼起来即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 19, M = 185;
int T; long long n;
int s[N], c[N];
int dp[N][M][M];

inline int S(long long x) { int res = 0; while (x) res += x % 10, x /= 10; return res; }
int DP(int u, int sum, int dist) {
    if (!u) return (dist >= sum) ? 1 : dist + 1;
    if (~dp[u][sum][dist]) return dp[u][sum][dist];
    int now = dist;
    for (int i = 0; i <= 9; i++) now = DP(u - 1, sum + i, now);
    return dp[u][sum][dist] = now;
}

int main() {
    memset(dp, -1, sizeof dp);
    scanf("%d", &T);
    while (T--) {
        scanf("%lld", &n), n++; // 计算 (n+1) 到 pre 的距离
        int dist = 1; long long x = n;
        for (int i = 0; i <= 18; i++) s[i] = S(x), c[i] = x % 10, x /= 10;
        for (int i = 18; i >= 0; i--) {
            for (int j = 1; j <= c[i]; j++) dist = DP(i, s[i + 1] + j, dist);
        }
        puts((dist > 1) ? "Algosia" : "Bajtek");
    }
    return 0;
}
posted @ 2025-06-25 15:11  Conan15  阅读(22)  评论(0)    收藏  举报