算是强迫自己复习一次做的题吧
iPlayForSG不会SG函数(大嘘
P2197 【模板】nim游戏
题目名字说明了一切qwq
-
定理:(Nim)博弈先手必胜,当且仅当(A_1 xorA_2xor...xorA_n eq0)
-
证明:
读者自证不难所有石子都被取光显然是一个必败局面(对手已经胜利),显然此时异或和为(0)(啥都没有了)。对于任意一个局面,如果异或和不为(0),设这个和为(x),且其二进制下最高位的(1)在第(k)位,那么至少存在一堆石子(A_i),它的第(k)位是(1),显然(x) (xor) (A_i<A_i)。那么我们就从第(A_i)堆中取走(A_i-A_i) (xor) (x)个石子,就得到了一个异或和为(0)的局面。
根据题意,我们将所有石子的个数异或起来,只要异或和不为(0)就先手必胜。
代码略。
P1247 取火柴游戏
上一个题的加强版所以为什么上一个是蓝题这个是绿题呢
首先还是考虑(Nim)博弈,异或和为(0)则先手必败。
当先手必胜时,本题还要求我们构造一种方案。我们可以按照上述证明过程来构造,但是还有一种更简洁的方法。
考虑到异或的性质:对于任意一个整数(x),满足(x) (xor) (x=0)。这启发我们:序列(A)异或和为(0)的时候一定存在某个数(A_i),使得序列中其它数异或和为(A_i)。那么我们直接按编号从小到大遍历原序列,找到某个数,使得序列中其它的数的异或和小于它,然后我们考虑将这个数与其它数的异或和作差,那么拿走这些石头就相当于构造出了异或和为(0)的必败局面。其它数的异或和可以用异或前缀和快速求解。
代码如下,已省略数组声明和宏定义声明
int main()
{
read(n);
for (rg int i = 1; i <= n; ++i) read(a[i]), s[i] = s[i - 1] ^ a[i];
if (!s[n])
{
puts("lose");
return 0;
}
for (rg int i = 1; i <= n; ++i)
{
rg int tmp = s[i - 1] ^ (s[n] ^ s[i]);
if (tmp >= a[i]) continue;
print(a[i] - tmp), __space, print(i), __endl;
a[i] = tmp;
for (rg int j = 1; j <= n; ++j) print(a[j]), __space;
return 0;
}
return 0;
}
HDU1846 Brave Game
相信你看的出来这个题是裸的SG函数。考虑递推求解即可
代码如下,已省略数组声明和宏定义声明
int main()
{
read(t);
while (t--)
{
read(n), read(m);
for (rg int i = 1; i <= n; ++i)
{
memset(mex, false, sizeof mex);
for (rg int j = 1; j <= i && j <= m; ++j) mex[SG[i - j]] = true;
for (rg int j = 0; ; ++j)
{
if (mex[j]) continue;
SG[i] = j;
break;
}
}
if (SG[n]) puts("first");
else puts("second");
}
return 0;
}
HDU1847 Good Luck in CET-4 Everybody!
提出两种解法。最暴力的做法是SG函数打表。考虑到这堆牌至多(1000)张,那么我们预处理出所有一次取小于等于(1000)张牌的方法,直接打表预处理每种牌数的可能性。
inline void SG_Form(int n) // 打表写法, 通常情况首选
{
for (rg int i = 1; i <= n; ++i)
{
// 新的SG, 重置后继集合
memset(S, false, sizeof S);
for (rg int j = 1; f[j] <= i && j <= n; ++j)
{
S[SG[i - f[j]]] = true; // 标记后继SG函数值
}
for (rg int j = 0; ; ++j) // Mex运算
{
if (!S[j])
{
SG[i] = j;
break;
}
}
}
}
int n;
int main()
{
f[1] = 1;
for (rg int i = 2; i <= 15; ++i) f[i] = f[i - 1] * 2;
SG_Form(1001);
while (~scanf("%d", &n))
{
if (SG[n]) puts("Kiki");
else puts("Cici");
}
return 0;
}
当然,还有一种巴什博奕的求解方法,你也可以通过输出SG函数来找到这个规律。事实上,暴力求解SG函数通常是不可取的,打表SG函数一般的作用都是找规律。。
考虑让对手陷入必败态。每人每次取的牌数都是(2)的幂次方。我们要构造一种局面让对手不能一次取完所有牌。考虑一个最小的、其倍数不可能是(2)的幂次方的数,显然(3)满足条件。事实上,只要牌的总数不是(3)的倍数则先手必胜,因为他总有一种取法让牌数回到(3)的倍数而对手无法一次性取完所有牌。
证明略,代码略。
HDU1848 Fibonacci again and again
与HDU1847 Good Luck in CET-4 Everybody!的SG函数求法一样,仅将可出牌的情况预处理为(Fibonacci)即可。
代码略。
HDU1849 Rabbit and Grass
这个题初看没有什么头绪,但是我们可以考虑转换一波题意。
注意到该游戏具有以下规则
每一步可以选择任意一个棋子向左移动到任意的位置(可以多个棋子位于同一个方格),当然,任何棋子不能超出棋盘边界;
如果所有的棋子都位于最左边(即编号为0的位置),则游戏结束,并且规定最后走棋的一方为胜者。
考虑转换,把每个棋子分别看做一堆石子,那么它的个数相当于其所在位置的编号。将棋子向左移动相当于取石子,棋子位于最左边相当于取完石子。现在此题转换为了裸的(Nim)博弈游戏。异或和判断即可
代码略
HDU1850 Being a Good Boy in Spring Festival
与P1247 取火柴游戏基本一样,只是要求方案总数。考虑到两数异或和为(0)当且仅当两数相等,所以每一堆石子最多有一种取法。直接遍历输出即可。
代码略
GZOI2015 T1 石子游戏
一眼SG函数,但是显然1e6的数据范围并不支持我们暴力递推SG函数。还记得刚刚怎么说的吗
事实上,暴力求解SG函数通常是不可取的,打表SG函数一般的作用都是找规律
考虑打表
inline void SG_Form(int r)
{
for (rg int i = 1; i <= r; ++i)
{
memset(mex, false, sizeof mex);
for (rg int j = 1; j <= i; ++j)
{
if (gcd(i, j) != 1) continue; // 取石子要满足gcd = 1
mex[SG[i - j]] = true;
}
for (rg int j = 1; ; ++j) // 不从0开始是因为可以取走整堆石子
{
if (mex[j]) continue;
SG[i] = j;
break;
}
}
}
然后可以得到下面这样一张表
1: 1
2: 2
3: 3
4: 2
5: 4
6: 2
7: 5
8: 2
9: 3
10: 2
11: 6
12: 2
13: 7
14: 2
15: 3
16: 2
17: 8
18: 2
19: 9
20: 2
...
看起来除了偶数全是(2),质数递增其他都没规律。然而考虑到质数递增相当于它自己在质数中的排名,带入偶数就是其最小质因数(2)的排名。检验一下奇数,发现也满足条件。
整合一下就是:
- 对于(1)而言,SG值为(1)
- 对于其他数而言,SG值为其最小质因数在质数中的排名
那么直接暴力线性筛就好,甚至不需要预处理每个数的SG。
代码如下
const int maxn = 23333;
int SG[maxn];
bool mex[maxn];
inline int gcd(int x, int y)
{
if (!y) return x;
return gcd(y, x % y);
}
inline void SG_Form(int r)
{
for (rg int i = 1; i <= r; ++i)
{
memset(mex, false, sizeof mex);
for (rg int j = 1; j <= i; ++j)
{
if (gcd(i, j) != 1) continue;
mex[SG[i - j]] = true;
}
for (rg int j = 1; ; ++j) // 不从0开始是因为可以取走整堆石子
{
if (mex[j]) continue;
SG[i] = j;
break;
}
}
}
int tot;
int pri[1000005];
bool flag[1000005];
inline void make_pri_list(int r)
{
flag[1] = true;
for (rg int i = 2; i <= r; ++i)
{
if (!flag[i]) pri[++tot] = i;
for (rg int j = 1; j <= tot && i * pri[j] <= r; ++j)
{
flag[i * pri[j]] = true;
if (!(i % pri[j])) break;
}
}
}
int t, n, x;
int main()
{
make_pri_list(1000000);
// freopen("form.out", "w", stdout);
// SG_Form(2333);
// for (rg int i = 1; i <= 2333; ++i) printf("%d: %d
", i, SG[i]);
// return 0;
read(t);
while (t--)
{
read(n);
rg int ans = 0;
for (rg int i = 1; i <= n; ++i)
{
read(x);
if (!flag[x]) x = lower_bound(pri + 1, pri + 1 + tot, x) - pri + 1;
else if (x != 1)
{
for (rg int j = 1; j <= tot; ++j)
{
if (!(x % pri[j]))
{
x = pri[j];
break;
}
}
x = lower_bound(pri + 1, pri + 1 + tot, x) - pri + 1;
}
ans ^= x;
}
if (ans) puts("Alice");
else puts("Bob");
}
return 0;
}
CF1194D 1-2-K Game
遇事不决先SG打表
(1 1): 1, (1 2): 1, (1 3): 1, (1 4): 1, (1 5): 1, (1 6): 1, (1 7): 1, (1 8): 1, (1 9): 1, (1 10): 1,
(2 1): 2, (2 2): 2, (2 3): 2, (2 4): 2, (2 5): 2, (2 6): 2, (2 7): 2, (2 8): 2, (2 9): 2, (2 10): 2,
(3 1): 0, (3 2): 0, (3 3): 3, (3 4): 0, (3 5): 0, (3 6): 0, (3 7): 0, (3 8): 0, (3 9): 0, (3 10): 0,
(4 1): 1, (4 2): 1, (4 3): 0, (4 4): 1, (4 5): 1, (4 6): 1, (4 7): 1, (4 8): 1, (4 9): 1, (4 10): 1,
(5 1): 2, (5 2): 2, (5 3): 1, (5 4): 2, (5 5): 2, (5 6): 2, (5 7): 2, (5 8): 2, (5 9): 2, (5 10): 2,
(6 1): 0, (6 2): 0, (6 3): 2, (6 4): 0, (6 5): 0, (6 6): 3, (6 7): 0, (6 8): 0, (6 9): 0, (6 10): 0,
(7 1): 1, (7 2): 1, (7 3): 3, (7 4): 1, (7 5): 1, (7 6): 0, (7 7): 1, (7 8): 1, (7 9): 1, (7 10): 1,
(8 1): 2, (8 2): 2, (8 3): 0, (8 4): 2, (8 5): 2, (8 6): 1, (8 7): 2, (8 8): 2, (8 9): 2, (8 10): 2,
(9 1): 0, (9 2): 0, (9 3): 1, (9 4): 0, (9 5): 0, (9 6): 2, (9 7): 0, (9 8): 0, (9 9): 3, (9 10): 0,
(10 1): 1, (10 2): 1, (10 3): 2, (10 4): 1, (10 5): 1, (10 6): 0, (10 7): 1, (10 8): 1, (10 9): 0, (10 10): 1,
这是一部分10x10的表,如果你打的够大你会很容易发现规律:
-
(k)不是(3)的倍数,
-
n是(3)的倍数则(Bob)胜
-
否则(Alice)胜
-
-
(k)是(3)的倍数
-
(n)在模(k+1)后是(3)的倍数且(n)不等于(k)则(Bob)胜
-
否则(Alice)胜
-
代码就不上了
(后面就没那么裸了)
LOJ10243 移棋子游戏
初看题目没什么思路。考虑一下SG函数的本质:
- 有向图游戏的某个局面必胜(/)败,当且仅当该局面对应节点的SG函数值大于(/)等于(0)
显然,出度为(0)的点SG函数一定为(0)。考虑从这些点倒推,由于每次只能移动一步,那该点的上一个局面一定是它的入边上的另一节点,由此可以进行SG函数的递归求解。由于原图是(DAG),采用拓扑排序解决。
inline void topsort()
{
while (!q.empty())
{
memset(mex, false, sizeof mex);
rg int x = q.front();
q.pop();
for (rg int i = head[x]; i; i = e[i].next)
{
rg int v = e[i].v;
mex[SG[v]] = true;
}
for (rg int i = 0; ; ++i)
{
if (mex[i]) continue;
SG[x] = i;
break;
}
for (rg int i = rhead[x]; i; i = re[i].next)
{
rg int v = re[i].v;
if (!(--oud[v])) q.push(v);
}
}
}
int x, y;
int main()
{
read(n), read(m), read(k);
for (rg int i = 1; i <= m; ++i) read(x), read(y), add(x, y), ++oud[x];
for (rg int i = 1; i <= n; ++i)
{
if (oud[i]) continue;
q.push(i);
}
topsort();
for (rg int i = 1; i <= k; ++i) read(x), ans ^= SG[x];
if (ans) puts("win");
else puts("lose");
return 0;
}
LOJ10244 取石子游戏
前一半是个裸题,后面对于神仙来说还是裸题输出方案需要进行一定的转换。考虑到先手必胜则下一步先手必败,先手必败态的SG函数异或和为(0),所以我们要构造一种方案使得SG函数异或和为(0)。想一想SG函数的更新是如何得到的
for (rg int j = 1; 第j个取石子的方案取几个石子 <= 当前石子数 && j <= 取石子的方案总数; ++j)
mex[SG[当前石子数 - 第j个取石子的方案取几个石子]] = true;
我们也可以像这样进行更新。当前异或和 异或 当前石子的SG值,得到其它石子的SG值异或和,枚举当前石子应取几个石子,再计算取石子后的异或和,为(0)则达到了先手必败态。
inline void SG_Form(int r)
{
memset(SG, 0, sizeof SG);
for (rg int i = 1; i <= r; ++i)
{
memset(mex, false, sizeof mex);
for (rg int j = 1; b[j] <= i && j <= m; ++j)
{
mex[SG[i - b[j]]] = true;
}
for (rg int j = 0; ; ++j)
{
if (mex[j]) continue;
SG[i] = j;
break;
}
}
}
int main()
{
read(n);
for (rg int i = 1; i <= n; ++i) read(a[i]);
read(m);
for (rg int i = 1; i <= m; ++i) read(b[i]);
SG_Form(2333);
for (rg int i = 1; i <= n; ++i) ans ^= SG[a[i]];
if (!ans) puts("NO");
else
{
puts("YES");
for (rg int i = 1; i <= n; ++i)
{
for (rg int j = 1; b[j] <= a[i] && j <= m; ++j)
{
if (ans ^ SG[a[i]] ^ SG[a[i] - b[j]]) continue;
print(i), __space, print(b[j]);
return 0;
}
}
}
return 0;
}
LOJ10245 巧克力棒
把巧克力棒看做一堆能吃的石头,题意转换为每次可以选择制造若干堆石头,或者取石头。
最开始读题时感觉这题一脸不可做,但是思考后发现:如果总局面(不考虑制造石头,只考虑有(n)堆石头)是先手必败,则先手必胜。
感觉有点绕对不对,这样考虑:如果先手必败,那么我先手直接把所有石头制造出来,相当于把先手必败态扔给对面了,对面先手必败那我就必胜。
这样一来,题目就变成寻找是否存在一个子序列,使得这些石头构成先手必败局面。
为什么?因为先手可以把这些石头制造出来,把必败态扔给对面。如果对面和我玩这些石头,那他必败,他制造石头就相当于破坏了先手必败态,然而这之后是我先手,他还是必败。
inline void calc()
{
rg int cnt = 0, tans = 0;
for (rg int i = 1; i <= n; ++i)
{
if (vis[i]) tans ^= a[i], ++cnt;
}
if (!cnt) return;
if (!tans)
{
flag = true;
puts("NO");
return;
}
}
inline void dfs(int id)
{
if (flag) return;
if (id == n + 1)
{
calc();
return;
}
vis[id] = true;
dfs(id + 1);
vis[id] = false;
dfs(id + 1);
}
int main()
{
while (t--)
{
read(n);
ans = 0, flag = false;
for (rg int i = 1; i <= n; ++i) read(a[i]), ans ^= a[i];
if (!ans) puts("NO");
else
{
dfs(1);
if (!flag) puts("YES");
}
}
return 0;
}
LOJ10246 取石子
!神仙题警告←神仙无视
这道题我开始还想暴力SG给艹过去,然后发现我想多了。。。
首先考虑规则:一次只能拿(1)个或者合并(2)堆。
如果只考虑第一个情况,那奇数个石子就先手必胜,偶数个石子就先手必败。这个题他难就难在可以进行合并操作。。
考虑一下:当你面临一个先手必败态,你是不是可以考虑选择合并两堆石子来把这个必败态转移给对面?
你可能会开心地想:那我再判一下石子堆数的奇偶性不就行了吗?
然后。。
继续考虑。发现如果某堆石子个数为(1),那它被拿了后就没机会参与合并操作了。但是在没拿它的时候它还是可以被合并。
考虑把所有为(1)的堆单独提出来搞一搞。我们把合并操作当做多一个石子,加在其他的堆里面(本质都是消耗一次操作),注意在求和后要减一,因为最后一堆石子不能合并了。
那就好办了。如果现在石子数是奇数或者(1)的个数为奇数,那就是必胜态,因为如果石子数是偶数那可以考虑用(1)来转移必胜态。
当然还要特判其他石子加起来都没有(3)个的情况,这个时候就要看(1)的个数,(1)的个数不是(3)的倍数就必胜,否则必败(因为它可以用来 合并也可以用来取掉)。
int main()
{
read(t);
while (t--)
{
read(n);
ans = tot = 0;
for (rg int i = 1; i <= n; ++i)
{
read(x);
if (x == 1) ++tot;
else ans += x + 1;
}
--ans;
if (ans >= 3)
{
if ((ans & 1) || (tot & 1)) puts("YES");
else puts("NO");
}
else
{
if (tot % 3) puts("YES");
else puts("NO");
}
// if (n & 1) ++ans;
// if (!(ans & 1)) puts("NO");
// else puts("YES");
}
return 0;
}