This commit is contained in:
HuangHai
2025-08-30 18:35:01 +08:00
commit 1f397eca87
6258 changed files with 1487241 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int res = 0; // 起始值是0,因为任何数与0进行异或都它本身
while (n--) {
int x;
cin >> x;
res ^= x;
}
if (res)
puts("Yes"); // 异或值非零,必胜
else
puts("No"); // 异或值是零,必败
return 0;
}

View File

@@ -0,0 +1,132 @@
##[$AcWing$ $891$. $Nim$游戏](https://www.acwing.com/problem/content/description/893/)
### 一、题目描述
给定$n$堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
**输入格式**
第一行包含整数 $n$。
第二行包含 $n$ 个数字,其中第 $i$ 个数字表示第 $i$ 堆石子的数量。
**输出格式**
如果先手方必胜,则输出 `Yes`。
否则,输出 `No`。
**数据范围**
$1≤n≤10^5,1≤每堆石子数≤10^9$
**输入样例**:
```cpp {.line-numbers}
2
2 3
```
**输出样例**:
```cpp {.line-numbers}
Yes
```
### 二、历史与传说
据说,它源自中国,经由被贩卖到美洲的奴工们外传。辛苦的工人们,在工作闲暇之余,用石头玩游戏以排遣寂寞。后来流传到高级人士,则用便士($Pennies$),在酒吧柜台上玩。
最有名的玩法,是把十二枚便士放成$3、4、5$三列,拿光铜板的人赢。
后来,大家发现,先取的人只要在$3$那列里取走$2$枚,变成了$1、4、5$,就能稳操胜券了,游戏也就变得无趣了。
于是大家就增加列数,增加铜板的数量,这样就让人们有了毫无规律的感觉,不易于把握。
### 三、博弈论结论
在两名选手都足够聪明,每一步都是最优解的情况下:
$a_1$ ^ $a_2$ ^ ... ^$a_n=0$ 先手必败
$a_1$ ^ $a_2$ ^ ... ^$a_n\neq0$ 先手必胜
就是把所有堆的石子个数异或起来,结果是零,先手必败,不是零,先手必胜。
### 四、证明过程
#### 1、结论$1$
操作到最后时,每堆石子数都是$0$,有$0$ ^ $0$ ^ $…0=0$
#### 2、结论$2$
**在操作过程中,如果 $a_1 ∧ a_2 ∧ … ∧ a_n=x≠0$。那么玩家必然可以通过拿走某一堆若干个石子将异或结果变为$0$。**
**证明**:
* 不妨设结果$x$的二进制表示中最高一位$1$在第$k$位
* 那么在$a_1,a_2,…,a_n$中,必然有一个数$a_i$,它的第$k$位是$1$(**要不那个$1$从哪里来的?**),且$a_i ∧ x<a_i$:
* 让第$i$堆石子保留$a_i ∧ x$个:即从第$i$堆石子中拿走$a_i−(a_i ∧ x)$个石子
* 此时新状态的异或和:
$$\large a_1 ∧ a_2 ∧ … ∧ a_{i^{'}} ∧ … a_n \\
=a_1 ∧ a_2 ∧ … ∧(a_i ∧ x) ∧ a_n \\
=a_1 ∧ a_2 ∧ … ∧a_i ∧... ∧ a_n ∧ x \\
=x ∧ x =0
$$
**证毕**
><font color='red' size=3><b>解释:也就是说,不管给我啥样的情况,我都能找出一种办法,使得一次拿取后满足异或和为$0$。</b></font>
其实,我们也并不非得要找到某个有明显特征(指$x$的最高位$1$在第$k$位,而$a_i$的第$k$位是$1$)的$a_i$,更通用的作法是
```c++
for (int i = 0; i < n; i++) {
if((a[i] ^ x) < a[i]){
1. 拿走 a[i]-(a[i] ^ x)个
2. 剩下 a[i] ^ x个
}
}
```
这样的话,我们也就可以求出有多少种拿法,并且知道每种拿法是拿走多少,剩下多少。
#### 3、结论$3$
**在操作过程中,如果 $a_1 ∧ a_2 ∧ …∧ a_n=0$,那么无论玩家怎么拿,必然会导致最终异或结果不为$0$**
**反证法**:假设玩家从第$i$堆石子拿走若干个,结果仍是$0$。不妨设还**剩下**$a_i^{′}$个,因为不能$1$个都不拿,所以$0≤a_i^{′}<a_i$,且$a_1 ∧ a_2∧…∧a_i^{′}∧…∧a_n=0$。
**进行构造**
$$(a_1 ∧ a_2∧…∧a_i∧…a_n)∧(a_1∧a_2∧…∧a_i^{′}∧…∧a_n)$$
整体构造完式子后,利用异或运算的 **结合律** 性质,让
$$a_1 ∧ a_1=0,a_2 ∧ a_2=0,a_3 ∧ a_3=0,...$$
$$\large \Rightarrow a_i∧a^{′}=0$$
则 $a_i=a′$,与假设$0≤a′<a_i$矛盾。
**证毕**
**基于上述三个结论**:
1. 如果先手面对的局面是$x=a_1∧a_2∧…∧a_n≠0$,那么先手总可以通过拿走某一堆若干个石子($a_i ∧ x$个),将局面变成$a_1∧a_2∧…∧a_n=0$。如此重复,最后一定是后手面临最终没有石子可拿的状态。先手必胜。
<br>
2. 如果先手面对的局面是$a_1∧a_2∧…∧a_n=0$,那么无论先手怎么拿,都会将局面变成$a_1∧a_2∧…∧a_n≠0$,那么后手总可以通过拿走某一堆若干个石子,将局面变成$a_1∧a_2∧…∧a_n=0$。如此重复,最后一定是先手面临最终没有石子可拿的状态,先手必败。
### 五、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
int res = 0; // 起始值是0,因为任何数与0进行异或都它本身
while (n--) {
int x;
cin >> x;
res ^= x;
}
if (res)
puts("Yes"); // 异或值非零,必胜
else
puts("No"); // 异或值是零,必败
return 0;
}
```

View File

@@ -0,0 +1,17 @@
#include <bits/stdc++.h>
using namespace std;
int n, res;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
if (i % 2) res ^= x;
}
if (res)
puts("Yes");
else
puts("No");
return 0;
}

View File

@@ -0,0 +1,100 @@
##[$AcWing$ $892$. 台阶-$Nim$游戏](https://www.acwing.com/problem/content/description/894/)
### 一、题目描述
现在,有一个 $n$ 级台阶的楼梯,每级台阶上都有若干个石子,其中第 $i$ 级台阶上有 $a_i$ 个石子($i≥1$)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
**输入格式**
第一行包含整数 $n$。
第二行包含 $n$ 个整数,其中第 $i$ 个整数表示第 $i$ 级台阶上的石子数 $a_i$。
**输出格式**
如果先手方必胜,则输出 `Yes`。
否则,输出 `No`。
**数据范围**
$1≤n≤10^5,1≤a_i≤10^9$
**输入样例**:
```cpp {.line-numbers}
3
2 1 3
```
**输出样例**:
```cpp {.line-numbers}
Yes
```
### 二、结论
如果 **奇数** 阶台阶的石子个数 **异或值不是零**,则 **先手必胜**。
如果 **奇数** 阶台阶的石子个数 **异或值是零**, 则 **先手必败**。
<center><img src='https://img2018.cnblogs.com/blog/1591144/201910/1591144-20191019153116490-1929300569.png'></center>
这是一个**台阶**$Nim$的初始状态 $2$,$1$,$3$,$2$,$4$ , **只能把大号台阶上的石子往小号台阶上放**。
其实**台阶$Nim$经过转换可以变为经典$Nim$**:
> **把石子从奇数堆移动到偶数堆可以理解为拿走石子**,就相当于几个奇数堆的石子在做$Nim$。
如所给样例,$2(台阶1) ∧ 3(台阶3) ∧ 4(台阶5)= 5$ 不为零,所以**先手必胜**
### 三、策略
我们可以把每级奇数级台阶上的石子 **看做** 经典$Nim$游戏中的每一堆。
然后,我们只需要模拟出经典$Nim$游戏中的操作:
* 把石子拿掉,就相当于**把某奇数级台阶上的若干石子挪到下面的偶数级台阶上**
(拿下地面更好,这也是选奇数级不选偶数级的主要原因)
* 如果对手把上面偶数级台阶上的石子挪下来,也没关系,顺水推舟,把它再挪到下一层偶数台阶就行了
### 四、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
int n, res;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
if (i % 2) res ^= x;
}
if (res)
puts("Yes");
else
puts("No");
return 0;
}
```
### 五、问题与解答
#### $Q1:$为什么可以这样来转化?
答:假设我们是先手,所给的台阶石子状态的奇数堆做$Nim$先手能必胜,我就按照能赢的步骤将奇数堆的石子($a_i-(x ∧ a_i)$个)移动到偶数堆(相当于扔掉)。
* 如果对手也是移动奇数堆,我们继续移动奇数堆。
* 如果对手将偶数堆的石子移动到了奇数堆,那么我们紧接着将对手所移动的这么多石子从那个奇数堆移动到下面的偶数堆,两次操作后,相当于偶数堆的石子向下移动了几个,而奇数堆依然是原来的样子,即为必胜的状态。 就算后手一直在移动偶数堆的石子到奇数堆,我们就一直跟着他将石子继续往下移,保持奇数堆不变,如此做下去,我可以跟着后手把偶数堆的石子移动到$0$,然后你就不能移动这些石子了。
所以整个过程,将偶数堆移动到奇数堆不会影响奇数堆做$Nim$博弈的过程,整个过程可以抽象为奇数堆的$Nim$博弈。
其他的情况,先手必输的,类似推理,只要判断奇数堆做$Nim$博弈的情况即可。
#### $Q2:$为什么只对奇数堆做$Nim$就可以,而不是偶数堆呢?
答:因为如果是对偶数堆做$Nim$,对手移动奇数堆的石子到偶数堆,我们跟着移动这些石子到下一个奇数堆,那么最后是对手把这些石子移动到了$0$,我们不能继续跟着移动,就只能去破坏原有的$Nim$而导致胜负关系的不确定,所以只要对奇数堆做$Nim$判断即可知道胜负情况。

View File

@@ -0,0 +1,42 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 10010;
int n, k;
int a[N]; // 一共几种取法,比如一次取2个或5个。
int f[M]; // SG函数的值
int res;
int sg(int x) {
if (~f[x]) return f[x]; // 记忆化搜索
unordered_set<int> S;
for (int i = 0; i < k; i++)
if (x >= a[i]) S.insert(sg(x - a[i])); // x-s[i]:x的可行路径中终点有哪几个; sg(x-s[i]):这个终点它的sg值
for (int i = 0;; i++)
if (!S.count(i)) return f[x] = i;
}
int main() {
memset(f, -1, sizeof f); // 初始化数组值为-1
cin >> k; // 表示数字集合S中数字的个数
for (int i = 0; i < k; i++) cin >> a[i];
cin >> n; // 一共几堆
// n堆石子,每堆石子都取SG值,然后异或在一起
for (int i = 0; i < n; i++) {
int x;
cin >> x; // 每堆里多少个
res ^= sg(x);
}
if (res)
puts("Yes"); // 如果不是零,必胜
else
puts("No"); // 如果是零,必败
return 0;
}

View File

@@ -0,0 +1,117 @@
##[$AcWing$ $893$. 集合-$Nim$游戏](https://www.acwing.com/problem/content/description/895/)
### 一、题目描述
给定 $n$ 堆石子以及一个由 $k$ 个不同正整数构成的数字集合 $S$。
现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 $S$,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
**输入格式**
第一行包含整数 $k$,表示数字集合 $S$ 中数字的个数。
第二行包含 $k$ 个整数,其中第 $i$ 个整数表示数字集合 $S$ 中的第 $i$ 个数 $s_i$。
第三行包含整数 $n$。
第四行包含 $n$ 个整数,其中第 $i$ 个整数表示第 $i$ 堆石子的数量 $h_i$。
**输出格式**
如果先手方必胜,则输出 `Yes`。
否则,输出 `No`。
**数据范围**
$1≤n,k≤100,1≤s_i,h_i≤10000$
**输入样例**:
```cpp {.line-numbers}
2
2 5
3
2 4 7
```
**输出样例**:
```cpp {.line-numbers}
Yes
```
### 二、理论知识
**理解题意**:
集合$S$中有两个数($k=2$),分别是$2$和$5$。也就是拿一次不是任意个,是必须$2$或$5$个。
有$3$堆石子,个数分别是$2,4,7$。问我们是先手必胜还是先手必败。
* $mex$函数
对于集合$S$,$mex(S)=mex(\{x_1,x_2…\})$表示$S$中没有出现的**最小非负整数**。
例如:$S=\{0,1,2,4\}$,那么$mex(S)=3$。
* $sg$函数
$sg(n)=mex(\{sg(i_1),sg(i_2),sg(i_3)...\})$。 $n$为结点;$i_1,i_2,i_3$…是$n$的后继结点。
* 规定
$sg(G)=sg(head)$。 $G$是一个有向图,$head$是$G$的头结点。
* 结论
$sg(G1)$ ^ $sg(G2)$ ^ $sg(G3)$ ^ $…$ ^ $sg(Gn)$为$n$个有向图的异或和,对于$n$个有向图游戏,这个异或和就是它的答案。
### 三、实例解析
$SG$函数是解决博弈论问题的一把利器。
![https://cdn.acwing.com/media/article/image/2020/10/27/42785_3fee2d8518-D58AFD439ED72CF24BB8C6860A0B818D.jpg](https://cdn.acwing.com/media/article/image/2020/10/27/42785_3fee2d8518-D58AFD439ED72CF24BB8C6860A0B818D.jpg)
### 四、$SG$函数复用的原因
![https://cdn.acwing.com/media/article/image/2020/10/27/42785_42cccf9218-F9733CE7743AB71E8ECEF77AAE759922.jpg](https://cdn.acwing.com/media/article/image/2020/10/27/42785_42cccf9218-F9733CE7743AB71E8ECEF77AAE759922.jpg)
### 五、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 10010;
int n, k;
int a[N]; // 一共几种取法,比如一次取2个或5个。
int f[M]; // SG函数的值
int res;
int sg(int x) {
if (~f[x]) return f[x]; // 记忆化搜索
unordered_set<int> S;
for (int i = 0; i < k; i++)
if (x >= a[i]) S.insert(sg(x - a[i])); // x-s[i]:x的可行路径中终点有哪几个; sg(x-s[i]):这个终点它的sg值
for (int i = 0;; i++)
if (!S.count(i)) return f[x] = i;
}
int main() {
memset(f, -1, sizeof f); // 初始化数组值为-1
cin >> k; // 表示数字集合S中数字的个数
for (int i = 0; i < k; i++) cin >> a[i];
cin >> n; // 一共几堆
// n堆石子,每堆石子都取SG值,然后异或在一起
for (int i = 0; i < n; i++) {
int x;
cin >> x; // 每堆里多少个
res ^= sg(x);
}
if (res)
puts("Yes"); // 如果不是零,必胜
else
puts("No"); // 如果是零,必败
return 0;
}
```

View File

@@ -0,0 +1,35 @@
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n;
int f[N];
int res;
int sg(int x) {
if (~f[x]) return f[x];
unordered_set<int> S;
// 所有能到的局面
for (int i = 0; i < x; i++)
for (int j = 0; j <= i; j++)
S.insert(sg(i) ^ sg(j));
for (int i = 0;; i++)
if (!S.count(i)) return f[x] = i;
}
int main() {
// 初始化
memset(f, -1, sizeof f);
cin >> n;
while (n--) {
int x;
cin >> x;
res ^= sg(x);
}
if (res)
puts("Yes");
else
puts("No");
return 0;
}

View File

@@ -0,0 +1,82 @@
##[$AcWing$ $894$. 拆分-$Nim$游戏](https://www.acwing.com/problem/content/description/896/)
### 一、题目描述
给定 $n$ 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆 **规模更小** 的石子(新堆规模可以为 $0$,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
**输入格式**
第一行包含整数 $n$。
第二行包含 $n$ 个整数,其中第 $i$ 个整数表示第 $i$ 堆石子的数量 $a_i$。
**输出格式**
如果先手方必胜,则输出 `Yes`。
否则,输出 `No`。
**数据范围**
$1≤n,a_i≤100$
**输入样例**:
```cpp {.line-numbers}
2
2 3
```
**输出样例**:
```cpp {.line-numbers}
Yes
```
### 二、解题思路
相比于集合-$Nim$,这里的每一堆可以变成**不大于原来那堆的任意大小的两堆**。
即$a[i]$可以拆分成$(b[i],b[j])$,为了避免重复规定$b[i]>=b[j]$,即:$a[i]>b[i]>=b[j]$
相当于一个局面拆分成了两个局面,由$SG$函数理论:多个独立局面的$SG$值,等于这些局面$SG$值的 **异或和**。
因此需要存储的状态就是$sg(b[i])$^$sg(b[j])$(与集合-$Nim$的唯一区别)。
### 三、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n;
int f[N];
int res;
int sg(int x) {
if (~f[x]) return f[x];
unordered_set<int> S;
// 所有能到的局面
for (int i = 0; i < x; i++)
for (int j = 0; j <= i; j++)
S.insert(sg(i) ^ sg(j));
for (int i = 0;; i++)
if (!S.count(i)) return f[x] = i;
}
int main() {
// 初始化
memset(f, -1, sizeof f);
cin >> n;
while (n--) {
int x;
cin >> x;
res ^= sg(x);
}
if (res)
puts("Yes");
else
puts("No");
return 0;
}
```