[刷题] 博弈论(SG函数)
SPOJ9934 Alice-Alice and Bob
Description
Alice和Bob两个好朋友又开始玩取石子了。
游戏开始时,有N堆石子排成一排,然后他们轮流操作(Alice先手),每次操作时从下面的规则中任选一个:
- 从某堆石子中取走一个
- 合并任意两堆石子
不能操作的人输。Alice想知道,她是否能有必胜策略。
数据范围 \(1\le T\le 4000, 1\le n\le 50, 1\le A_i\le 1000\)
Solution
这题烦就烦在有石子数为\(1\)的堆。所以我们考虑记搜:
令 \(f[i][j]\) 表示有 \(i\) 个1,大于 \(1\) 的堆有 \(j\) 次操作的\(SG\)函数值。
那么,显然可以转移,具体参考代码。
复杂度 \(O(n^2 值域)\)
Code
// Author: wlzhouzhuan
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define rint register int
#define rep(i, l, r) for (rint i = l; i <= r; i++)
#define per(i, l, r) for (rint i = l; i >= r; i--)
#define mset(s, _) memset(s, _, sizeof(s))
#define pb push_back
#define pii pair <int, int>
#define mp(a, b) make_pair(a, b)
inline int read() {
int x = 0, neg = 1; char op = getchar();
while (!isdigit(op)) { if (op == '-') neg = -1; op = getchar(); }
while (isdigit(op)) { x = 10 * x + op - '0'; op = getchar(); }
return neg * x;
}
inline void print(int x) {
if (x < 0) { putchar('-'); x = -x; }
if (x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
const int N = 50005;
int f[51][N], n;
int SG(int a, int b) {
if (~f[a][b]) return f[a][b];
if (!a) return f[a][b] = b % 2; // 如果全是>1的堆,那么判断奇偶即可
if (b == 1) return f[a][b] = SG(a + 1, 0); // 这是一个=1的堆,移回去
int t = 2;
if (b) t = min(t, SG(a, b - 1)); // 取走一个>1的堆的元素
if (a) t = min(t, SG(a - 1, b)); // 取走一个1的堆中的元素
if (a > 1) t = min(t, SG(a - 2, b + 2 + (b ? 1 : 0))); // 合并两个1的堆,如果有>1的堆,则新增一个可合并次数
if (a && b) t = min(t, SG(a - 1, b + 1)); // 将1的堆合并到>1的堆中
if (!t) f[a][b] = 1;
else f[a][b] = 0;
return f[a][b];
}
int main() {
int T = read();
mset(f, -1);
for (int _ = 1; _ <= T; _++) {
//mset(f, -1);
n = read();
int a1 = 0, tot = 0;
for (int i = 1; i <= n; i++) {
int x = read();
if (x == 1) a1++;
else tot += x + 1;
}
if (tot) tot--; // 堆数 - 1 = 可合并次数
printf("Case #%d: ", _);
if (SG(a1, tot)) puts("Alice");
else puts("Bob");
}
return 0;
}
BZOJ1299 巧克力棒
Description
TBL和X用巧克力棒玩游戏。每次一人可以从盒子里取出若干条巧克力棒,或是将一根取出的巧克力棒吃掉正整数长度。TBL先手两人轮流,无法操作的人输。 他们以最佳策略一共进行了10轮(每次一盒)。你能预测胜负吗?
数据范围 \(T=10,1\le n\le 14, 1\le L\le 1e9\)
Solution
我们从这 \(n\) 根巧克力棒中选出 \(m(m>0)\) 根,如果这 \(m\) 根巧克力棒的\(xor\)和为\(0\),并且剩下的 \(n-m\) 根巧克力棒无论怎么取,异或和均不为\(0\),那么这种情况必胜。
事实上,我们只需要判断能否挑出一些巧克力棒,它们的异或和为\(0\)即可。
因为,假设有好多组数都可以做到异或和为\(0\),那么我们一定会把这几组都选上。故只要存在异或和为\(0\),那么说明存在至少一组满足条件,那么剩下的我能取的尽可能取即可。
其实这题可以用线性基维护,但是\(n\)这么小,直接状压枚举即可。
复杂度 \(O(2^n)\),用线性基可做到 \(O(n log值域)\)
emmm 值得一提的是,这题如果必胜要输出NO
,必败输出YES
(出题人有毒吧..)
Code
// Author: wlzhouzhuan
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define rint register int
#define rep(i, l, r) for (rint i = l; i <= r; i++)
#define per(i, l, r) for (rint i = l; i >= r; i--)
#define mset(s, _) memset(s, _, sizeof(s))
#define pb push_back
#define pii pair <int, int>
#define mp(a, b) make_pair(a, b)
inline int read() {
int x = 0, neg = 1; char op = getchar();
while (!isdigit(op)) { if (op == '-') neg = -1; op = getchar(); }
while (isdigit(op)) { x = 10 * x + op - '0'; op = getchar(); }
return neg * x;
}
inline void print(int x) {
if (x < 0) { putchar('-'); x = -x; }
if (x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
int a[15], n;
int main() {
int T = 10;
while (T--) {
n = read();
for (int i = 0; i < n; i++) a[i] = read();
int ok = 0;
int all = 1 << n;
for (int st = 1; st < all; st++) {
int Xor = 0;
for (int i = 0; i < n; i++) if (st >> i & 1) {
Xor ^= a[i];
}
if (!Xor) {
ok = 1;
break;
}
}
if (ok) puts("NO");
else puts("YES");
}
return 0;
}
学园祭的游戏
Description
给定\(n\)堆石子,每堆石子有两个值\(num_i\)和\(b_i\),表示石子数和一个常数。两个人在博弈。
如果取第\(i\)堆石子,那么至多取 \(\lfloor \frac{num_i}{b_i} \rfloor\) 个石子,与此同时\(num_i\)也相应减少。
最后不能操作的人输。
多组数据。
数据范围 \(1\le T\le 100, 1\le n\le 20, 1\le a_i, b_i\le 10^9\)
Solution
显然每堆石子独立,所以最后只需要将每堆石子的\(SG\)值异或起来即可。
对于每一堆石子,它内部也是一个状态集合。
\(SG(n)=mex(SG(n-1),SG(n-2),...,SG(n-\lfloor \frac{n}{b} \rfloor ))\)
当\(n\in [0, b)\)时,\(SG\)值肯定为\(0\);
否则我们用数学归纳法,可知:
\(\begin{cases}SG(n)=\frac{n}{b}, \ [b | n] \\ SG(n)=SG(n-\lfloor \frac{n}{b} \rfloor -1), \ otherwise \end{cases}\)
然后根号分治优化即可。
Code
// Author: wlzhouzhuan
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define rint register int
#define rep(i, l, r) for (rint i = l; i <= r; i++)
#define per(i, l, r) for (rint i = l; i >= r; i--)
#define mset(s, _) memset(s, _, sizeof(s))
#define pb push_back
#define pii pair <int, int>
#define mp(a, b) make_pair(a, b)
inline int read() {
int x = 0, neg = 1; char op = getchar();
while (!isdigit(op)) { if (op == '-') neg = -1; op = getchar(); }
while (isdigit(op)) { x = 10 * x + op - '0'; op = getchar(); }
return neg * x;
}
inline void print(int x) {
if (x < 0) { putchar('-'); x = -x; }
if (x >= 10) print(x / 10);
putchar(x % 10 + '0');
}
int SG(int n, int b) {
while (n % b) {
n -= (int)ceil((n % b) / 1.0 / (n / b + 1)) * (n / b + 1);
}
return n / b;
}
int n;
int main() {
int T = read();
while (T--) {
n = read();
int ans = 0;
while (n--) {
int n = read(), b = read();
ans ^= SG(n, b);
}
puts(ans ? "Setsuna" : "Kazusa");
}
return 0;
}