只要你跑的够快,锅就追不上你

0%

「专题选做」博弈论题目选做 2

Sharti

「Codeforces 494E」Sharti

题目描述

$n \times n$ 的网格,给定 $m$ 个矩形,它们的并是白色的,剩下的格子是黑色的。两人博弈,每次可以选择一个边长不超过 $k$ 的正方形满足它的右下角是白色,然后反转这个正方形的颜色。不能操作者输,问先手是否必胜。

数据范围:$m \le 5 \times 10^4, k \le n \le 10^9$。

思路分析

介绍一个翻硬币的模型:给定一排硬币,每次选择一个右端点正面朝上的区间反转,不能操作者输。那么整个局面的 sg 值等于所有正面朝上的硬币单独存在时 sg 值的异或和。证明是考虑另一个问题:本来正面朝上的硬币处有棋子,每次选择一个右端点有棋子的区间然后拿掉右端点的一个棋子,并给其他点加上一个棋子。这两个问题是等价的,因为一个点有两个棋子时它们 sg 值为 $0$,就可以拿掉它们。

这个模型放到这个题里照样成立,所以我们考虑每个白格单独存在时的 sg 值。考虑打表:

1
1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1
2
1 2 1 2 1 2 1 2 | 1 2 1 2 1 2 1 2
3
1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1
4
1 2 1 2 1 2 1 2 | 1 2 1 4 1 2 1 4
5
1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1
6
1 2 1 2 1 2 1 2 | 1 2 1 2 1 2 1 2
7
1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1
8
1 2 1 2 1 2 1 2 | 1 2 1 4 1 2 1 4

发现 $k = 2, 3$ 时 sg 值都是左边的表,$k = 4, 5, 6, 7$ 时 sg 值都是右边的表。所以可以猜测表和不超过 $k$ 的最大 $2$ 的幂次有关。记 $b$ 表示满足 $2^x \le k$ 的最大 $x$,那么有:

考虑对于每个 $2^i$ 分别计算。发现我们最后只要求矩形的面积并,考虑线段树维护两个值:$\text{cov(x)}, \text{ans(x)}$。每次区间 $\pm 1$ 时,我们找出区间对应的 $O(\log n)$ 个结点把它们的 $\text{cov}(x) \pm 1$。然后对于每个点,都有:

直接更新即可。时间复杂度 $O(n \log k \log n)$,具体细节见代码。

代码实现

1
#include <bits/stdc++.h>
2
#define mid ((l + r) >> 1)
3
#define ls (x << 1)
4
#define rs (ls | 1)
5
using namespace std;
6
7
typedef long long ll;
8
const int maxn = 1e5, maxm = 1 << 18;
9
int n, m, k, bit, a[maxn + 3], b[maxn + 3], c[maxn + 3], d[maxn + 3];
10
int cnt, pos[maxn + 3], tot, cov[maxm + 3], len[maxm + 3], ans[50];
11
12
struct event {
13
	int x, l, r, y;
14
	friend bool operator < (const event &a, const event &b) {
15
		return a.x == b.x ? a.y > b.y : a.x < b.x;
16
	}
17
} eve[maxn + 3];
18
19
void maintain(int x, int l, int r) {
20
	if (cov[x]) {
21
		len[x] = pos[r + 1] - pos[l];
22
	} else if (l == r) {
23
		len[x] = 0;
24
	} else {
25
		len[x] = len[ls] + len[rs];
26
	}
27
}
28
29
void build(int x, int l, int r) {
30
	cov[x] = len[x] = 0;
31
	if (l == r) {
32
		return;
33
	}
34
	build(ls, l, mid);
35
	build(rs, mid + 1, r);
36
}
37
38
void modify(int x, int l, int r, int lx, int rx, int y) {
39
	if (l >= lx && r <= rx) {
40
		cov[x] += y;
41
		maintain(x, l, r);
42
		return;
43
	}
44
	if (lx <= mid) {
45
		modify(ls, l, mid, lx, rx, y);
46
	}
47
	if (rx > mid) {
48
		modify(rs, mid + 1, r, lx, rx, y);
49
	}
50
	maintain(x, l, r);
51
}
52
53
ll solve() {
54
	cnt = tot = 0;
55
	for (int i = 1; i <= m; i++) if (b[i] < d[i]) {
56
		pos[++cnt] = b[i], pos[++cnt] = d[i];
57
	}
58
	if (!cnt) {
59
		return 0;
60
	}
61
	sort(pos + 1, pos + cnt + 1);
62
	cnt = unique(pos + 1, pos + cnt + 1) - (pos + 1);
63
	for (int i = 1; i <= m; i++) if (b[i] < d[i]) {
64
		int l = lower_bound(pos + 1, pos + cnt + 1, b[i]) - pos;
65
		int r = lower_bound(pos + 1, pos + cnt + 1, d[i]) - (pos + 1);
66
		eve[++tot].x = a[i], eve[tot].l = l, eve[tot].r = r, eve[tot].y = 1;
67
		eve[++tot].x = c[i], eve[tot].l = l, eve[tot].r = r, eve[tot].y = -1;
68
	}
69
	sort(eve + 1, eve + tot + 1);
70
	cnt--;
71
	build(1, 1, cnt);
72
	ll sum = 0;
73
	for (int i = 1; i <= tot; i++) {
74
		sum += 1ll * (eve[i].x - eve[i - 1].x) * len[1];
75
		modify(1, 1, cnt, eve[i].l, eve[i].r, eve[i].y);
76
	}
77
	return sum;
78
}
79
80
int main() {
81
	scanf("%d %d %d", &n, &m, &k);
82
	for (int i = 1; i <= m; i++) {
83
		scanf("%d %d %d %d", &a[i], &b[i], &c[i], &d[i]);
84
		a[i]--, b[i]--;
85
	}
86
	for (bit = 0; 1 << bit <= k; bit++);
87
	for (int i = 0; i < bit; i++) {
88
		ans[i] = solve() & 1;
89
		for (int j = 1; j <= m; j++) {
90
			a[j] >>= 1, b[j] >>= 1, c[j] >>= 1, d[j] >>= 1;
91
		}
92
	}
93
	int sg = 0;
94
	for (int i = 0; i < bit; i++) {
95
		if (ans[i] ^ ans[i + 1]) {
96
			sg ^= 1 << i;
97
		}
98
	}
99
	if (sg) {
100
		puts("Hamed");
101
	} else {
102
		puts("Malek");
103
	}
104
	return 0;
105
}

「ZROI 967」银

思路分析

把游戏左右翻转,变成翻硬币模型,$\text{sg}(i) = 2^{\text{lowbit}(i)}$。设 $f(x)$ 表示 $x$ 的高位到低位的前缀异或和(例子:$f((100100)_2) = (111000)_2$),那么我们有 $f(0) = 0, f(a)\oplus f(b) = f(a \oplus b)$,以及 $\bigoplus_{i = 1}^{n}f(\text{sg}(i)) = n$。

设当前整个局面的 sg 值为 $X$,题目中合法的 $(l, r)$ 要满足:$\bigoplus_{i = l}^{r} \text{sg}(i) = X$。稍加转化得到 $\bigoplus_{i = l}^{r} f(\text{sg}(i)) = f(X)$,也就是 $(l - 1) \oplus r = f(X)$。发现对于一个 $r$ 存在对应的 $l - 1 < r$ 当且仅当 $f(X)$ 的最高位在 $r$ 的二进制下为 $1$,所以问题转化成了每次加一个数,删一个数,或查询某一位为 $1$ 的数的数量。维护 $f(X)$,直接模拟即可,时间复杂度 $O(n \log v)$。

代码实现

1
#include <bits/stdc++.h>
2
using namespace std;
3
4
const int maxn = 5e5;
5
int n, m, a[maxn + 3], cur, cnt[maxn + 3];
6
char s[maxn + 3];
7
8
void add(int x, int y) {
9
	for (int i = 20; ~i; i--) {
10
		if (x >> i & 1) {
11
			cnt[i] += y;
12
		}
13
	}
14
}
15
16
int lb(int x) {
17
	return x & -x;
18
}
19
20
int hb(int x) {
21
	for (int i = 20; ~i; i--) {
22
		if (x >> i & 1) {
23
			return i;
24
		}
25
	}
26
	return -1;
27
}
28
29
int f(int x) {
30
	int t = 0, s = 0;
31
	for (int i = 20; ~i; i--) {
32
		t ^= x >> i & 1;
33
		s |= t << i;
34
	}
35
	return s;
36
}
37
38
int main() {
39
	scanf("%d %s %d", &n, s + 1, &m);
40
	for (int i = 1; i <= n; i++) {
41
		a[i] = s[n - i + 1] ^ '0';
42
	}
43
	for (int i = 1; i <= n; i++) if (a[i]) {
44
		cur ^= f(lb(i)), add(i, 1);
45
	}
46
	for (int x; m --> 0; ) {
47
		scanf("%d", &x), x = n - x + 1;
48
		cur ^= f(lb(x)), add(x, -a[x]);
49
		a[x] ^= 1, add(x, a[x]);
50
		if (!cur) {
51
			puts("0");
52
		} else {
53
			printf("%d\n", cnt[hb(cur)]);
54
		}
55
	}
56
	return 0;
57
}

Tree-Tac-Toe

「Codeforces 1110G」Tree-Tac-Toe

题目描述

一棵 $n$ 个点的树,一开始一些点是白色。先手是白色,后手是黑色,每次一个人可以把一个没有颜色的点涂成自己的颜色。出现三个连续的同种颜色时持有该颜色的人就赢了,问最后是谁赢,或平局。

数据范围:$T \le 5 \times 10^4, \sum n \le 10^5$。

思路分析

首先我们可以去掉所有白点。考虑下图中的白点:

我们把它变成:

考虑这样的正确性。注意到 BDEF 这棵子树中谁都不可能赢,所以我们的最优策略是把 B 点变成自己的颜色。假设先手把 B 变成白色,那么如果后手不把 E 变成黑色,那么先手在下一步就能把 B 变成白色,然后他就赢了。也就是说第二步后手一定会把 E 变成黑色。那么剩下的两个点就和原图没有关系了,如果后手选了其中一个点先手把另一个点选掉即可。

接下来考虑没有白点的图。考虑下面两种情况:

如果一个点度数为 $4$,或者它度数为 $3$ 且有至少两个非叶儿子,那么一定先手必胜。

考虑排出这些情况的图,图中每个点的度数都不超过 $3$,并且度数为 $3$ 的点最多只有 $2$ 个。考虑下面这种情况:

即两个度数为 $3$ 的点中间夹着奇数个点,其实它等价于下面的图:

即两个白点之间夹着奇数个点。考虑这样的策略:每次选择左边白点向右的第二个点,这样黑点必须填在左边两个白点之间,然后问题就转化成了 $n’ = n - 2$ 的子问题。可以证明除了这种情况一定是平局,直接模拟,时间复杂度 $O(n)$。

代码实现

1
#include <bits/stdc++.h>
2
using namespace std;
3
4
const int maxn = 2e6;
5
int T, n, deg[maxn + 3];
6
vector<int> G[maxn + 3];
7
char s[maxn + 3];
8
9
void clear(int i) {
10
	vector<int>().swap(G[i]);
11
	deg[i] = 0;
12
}
13
14
void add(int u, int v) {
15
	G[u].push_back(v), G[v].push_back(u);
16
	deg[u]++, deg[v]++;
17
}
18
19
int main() {
20
	scanf("%d", &T);
21
	while (T --> 0) {
22
		scanf("%d", &n);
23
		for (int i = 1; i <= n; i++) {
24
			clear(i);
25
		}
26
		for (int i = 1, u, v; i < n; i++) {
27
			scanf("%d %d", &u, &v), add(u, v);
28
		}
29
		scanf("%s", s + 1);
30
		for (int i = 1; i <= n; i++) {
31
			if (s[i] == 'W') {
32
				clear(++n), add(i, n);
33
				clear(++n), add(n - 1, n);
34
				clear(++n), add(n - 2, n);
35
			}
36
		}
37
		bool flag = false;
38
		for (int i = 1; i <= n; i++) {
39
			if (deg[i] >= 4) {
40
				puts("White");
41
				flag = true;
42
				break;
43
			}
44
			if (deg[i] == 3) {
45
				int cnt = 0;
46
				for (int j = 0; j < 3; j++) {
47
					cnt += deg[G[i][j]] != 1;
48
				}
49
				if (cnt >= 2) {
50
					puts("White");
51
					flag = true;
52
					break;
53
				}
54
			}
55
		}
56
		if (!flag) {
57
			int x = 0, y = 0;
58
			for (int i = 1; i <= n; i++) {
59
				if (deg[i] == 3) {
60
					if (!x) {
61
						x = i;
62
					} else if (!y) {
63
						y = i;
64
					} else {
65
						exit(1);
66
					}
67
				}
68
			}
69
			if (x && y && ((n - 4) & 1)) {
70
				puts("White");
71
			} else {
72
				puts("Draw");
73
			}
74
		}
75
	}
76
	return 0;
77
}

Chip Game

「Codeforces 1033G」Chip Game

题目描述

有 $n$ 堆石子,第 $i$ 堆有 $v_i$ 个。两人玩游戏,A 每轮从一堆中拿 $a$ 个石子,B 每轮从一堆中拿 $b$ 个石子,不能拿的人输。对于一种 $(a, b)$,共有四种情况:

  • 不管谁先手都是 A 赢。(A 赢)
  • 不管谁先手都是 B 赢。(B 赢)
  • A 先手 A 赢,B 先手 B 赢。(先手赢)
  • A 先手 B 赢,B 先手 A 赢。(后手赢)

问对于所有 $1 \le a, b \le m$,四种情况各有多少种。

数据范围:$n \le 100, m \le 10^5$。

思路分析

考虑特定 $(a, b)$ 的情况。首先把所有 $v_i$ 都对 $a + b$ 取模,容易证明这样是对的。不妨设 $a < b$,对于每一个 $v_i$ 讨论:

  1. $0 \le v_i < a$,那么 $v_i$ 就谁都不能选。
  2. $a \le v_i < b$,那么只有 A 能选一次,B 不能选。
  3. $b \le v_i < 2a$,那么 A, B 都只能选一次。
  4. $\max(2a, b) \le v_i < a + b$,那么 A 选完后 B 不能选,B 选完后谁都不能选。

那么我们扔掉 1,然后分类讨论:

  • 若存在 2,那么 A 赢。
  • 若不存在 2 且存在至少两个 4,那么 A 肯定可以抢到一个 4 把它变成 2,也就是 A 赢。
  • 若不存在 2, 4,那么相当于先手后手抢 3,奇数个 3 先手赢,偶数个 3 后手赢。
  • 若不存在 2 且只有一个 4,那么如果 A 先手他可以抢到 4,如果 B 先手他必须抢 4,然后变成全是 3 的情况。所以如果奇数个 3,那么 A 赢,否则先手赢。

这样就得到了一个 $O(nm^2)$ 的算法,不够快。考虑只计算先手、后手赢的情况数,其他两种情况可以用总数减去这些数来推出。枚举 $s = a + b$,首先不能有 2,也就是要满足 $a \ge \min(v_i + 1, s - v_i)$。其次 4 只有 $0, 1$ 个,也就是 $s - v_i \le a \le \lfloor \frac{v_i}{2} \rfloor$ 的 $v_i$ 只有不超过一个。同时我们也要记录 3 的个数,也就是 $a \ge \max(\lfloor \frac{v_i}{2} \rfloor + 1, s - v_i)$ 的个数。我们记录关键的端点,排完序扫一遍即可,时间复杂度 $O(m n \log n)$。

代码实现

1
#include <bits/stdc++.h>
2
using namespace std;
3
4
typedef long long ll;
5
const int maxn = 100, maxm = 1e5;
6
int n, m, t[maxn + 3], tot, cur[5];
7
ll v[maxn + 3];
8
9
struct event {
10
	int x, t, v;
11
	friend bool operator < (const event &a, const event &b) {
12
		return a.x < b.x;
13
	}
14
} ev[maxn * 3 + 3];
15
16
void add(int a, int b, int c) {
17
	ev[++tot].x = a, ev[tot].t = b, ev[tot].v = c;
18
}
19
20
int main() {
21
	scanf("%d %d", &n, &m);
22
	for (int i = 1; i <= n; i++) {
23
		scanf("%lld", &v[i]);
24
	}
25
	ll A = 0, B = 0;
26
	for (int s = 2; s <= m * 2; s++) {
27
		for (int i = 1; i <= n; i++) {
28
			t[i] = v[i] % s;
29
		}
30
		int l = max(1, s - m), r = (s - 1) / 2;
31
		for (int i = 1; i <= n; i++) {
32
			l = max(l, min(t[i] + 1, s - t[i]));
33
		}
34
		if (l > r) continue;
35
		tot = 0;
36
		add(l, 0, 0), add(r + 1, 0, 0);
37
		for (int i = 1; i <= n; i++) {
38
			int L = max(l, s - t[i]), R = min(r, t[i] / 2);
39
			if (L <= R) add(L, 1, 1), add(R + 1, 1, -1);
40
		}
41
		for (int i = 1; i <= n; i++) {
42
			int x = max(l, max(s - t[i], t[i] / 2 + 1));
43
			if (x <= r) add(x, 2, 1);
44
		}
45
		sort(ev + 1, ev + tot + 1);
46
		memset(cur, 0, sizeof(cur));
47
		ev[0].x = l;
48
		for (int i = 1; i <= tot; i++) {
49
			int cnt = cur[1], sum = cur[2];
50
			int len = ev[i].x - ev[i - 1].x;	
51
			if (cnt == 0 && (sum & 1)) A += len;
52
			if (cnt == 0 && (~sum & 1)) B += len;
53
			if (cnt == 1 && (~sum & 1)) A += len;
54
			cur[ev[i].t] += ev[i].v;
55
		}
56
	}
57
	A <<= 1, B <<= 1;
58
	for (int i = 1; i <= m; i++) {
59
		int x = 0;
60
		for (int j = 1; j <= n; j++) {
61
			x ^= (v[j] / i) & 1;
62
		}
63
		if (x) A++;
64
		else B++;
65
	}
66
	ll C = (1ll * m * m - (A + B)) / 2;
67
	printf("%lld %lld %lld %lld\n", C, C, A, B);
68
	return 0;
69
}