文章目录
- 容斥原理
- 博弈论
- SG函数
- 容斥原理练习题
- 890. 能被整除的数
- 博弈论练习题
- 891. Nim游戏
- 893. 集合-Nim游戏
- 892. 台阶-Nim游戏
- 894. 拆分-Nim游戏
容斥原理
若干个相交集合,它们的并集中存在多少个元素?
假设n为所有集合的元素个数相加,因为集合间存在交集,所以n中有重复计算
比如三个集合
A
,
B
,
C
A,B,C
A,B,C
∣
A
∪
B
∪
C
∣
=
∣
A
∣
+
∣
B
∣
+
∣
C
∣
−
∣
A
∩
B
∣
−
∣
A
∩
C
∣
−
∣
B
∩
C
∣
+
∣
A
∩
B
∩
C
∣
|A∪B∪C| = |A| + |B| + |C| - |A∩B| - |A∩C| - |B∩C| + |A∩B∩C|
∣A∪B∪C∣=∣A∣+∣B∣+∣C∣−∣A∩B∣−∣A∩C∣−∣B∩C∣+∣A∩B∩C∣
推广到n个集合,并集的元素个数 = 每个集合的元素个数相加 - 任意两个集合的交集元素个数 + 任意三个集合的交集元素个数 - …
为什么要这样的重复?因为式子的每一项都有重复操作,如:每个集合的元素个数相加,因为任意两个集合间可能存在交集,所以有些元素被二次增加
减去这些被二次增加的元素,因为任意三个集合间可能存在交集,所以有些元素被二次删除
加上这些被二次删除的元素,可以发现每个操作都在弥补上次操作的重复
何时停止?加上或减去所有集合的交集时停止
那么推广公式中有多少项?
观察所有项,有些项是一个集合的交集(本身)元素个数,有些项是两个,有些是三个,最多到n个。所以这是个组合问题,从n个集合中选择k个集合,k从1~n,将这些组合数相加就是项数
根据等式:
∑
k
=
0
n
C
n
k
=
2
n
\sum_{k=0}^{n}C_n^k=2^n
k=0∑nCnk=2n
所以推得:项数 =
2
n
2^n
2n - 1
容斥原理证明:
假设在
n
n
n个集合中,数
x
x
x属于
k
k
k个不同的集合,在容斥原理的展开式中
x
x
x的出现次数为(蓝字):
列出该表达式,根据等式:
∑
i
=
1
k
(
−
1
)
i
−
1
C
k
i
=
1
\sum_{i = 1}^{k}(-1)^{i-1}C_k^i=1
i=1∑k(−1)i−1Cki=1
所以在容斥原理的展开式中,
x
x
x的出现次数为1,即在所有集合的并集中,
x
x
x只出现一次
将
x
x
x推广为集合中的任意元素,即证:容斥原理成立
博弈论
先手不是指第一出手方,而是指下一出手方
必胜态:可以走到(下一回动手)先手必败态
必败态:走不到先手必败态,不论怎么走都将走到先手必胜态
问题:
假设n堆石子的数量为
a
1
,
a
2
,
.
.
.
,
a
n
a_1, a_2, ..., a_n
a1,a2,...,an,若a_1 ^ a_2 ^ ... ^ a_n = 0
,则先手必败
否则先手必胜
分析:
- 不能进行任何操作(所有堆的石子为0),异或结果为0
- 异或结果不是0,一定可以通过某种方式使得异或结果为0
当异或结果非0时,一定有方法使得拿走石子后,异或结果为0
证明:
当异或结果为x(非0),x的二进制表示中,最高的一位1在第k位
其他数中必然存在至少一个数
a
i
a_i
ai,其第k位为1
那么就有
a
i
a_i
ai ^ x <
a
i
a_i
ai ,
a
i
a_i
ai ^ x 后,第k为从1变为0,结果减小
从堆中拿走(
a
i
a_i
ai -
a
i
a_i
ai ^ x)的石子,那么堆中剩下的石子数量为
a
i
a_i
ai-(
a
i
a_i
ai -
a
i
a_i
ai ^ x)=(
a
i
a_i
ai ^ x)
此时将所有堆的石子数量进行异或,结果为0,因为只有
a
i
a_i
ai变为了
a
i
a_i
ai ^ x
其中x为
a
i
a_i
ai变化之前的异或结果,
a
i
a_i
ai变化后,异或结果为x ^ x = 0
异或结果为0时,无论怎么拿,之后的异或结果一定不是0
证明:反证
假设从
a
i
a_i
ai中拿走石子,剩下
a
i
′
a_i'
ai′的石子,若此时的异或结果为0,将此时的异或等式与之前的异或等式进行异或
那么除了
a
i
a_i
ai和
a
i
′
a_i'
ai′剩下的项都是相同的,因为异或结果为0,所以
a
i
a_i
ai ^
a
i
′
a_i'
ai′ = 0
说明
a
i
a_i
ai =
a
i
′
a_i'
ai′,也就是没有拿走石子,与前提
a
i
′
a_i'
ai′ <
a
i
a_i
ai矛盾
所以若异或等式为0,那么无论此时怎么拿,异或结果不是0
因此,若双方都尽力在赢,那么双方就能使异或结果非0的局面转换到异或结果为0,也能使异或结果为0的局面转换为非0
当没有石子剩下,此时异或结果为0
所以,先手方的局面中,异或结果为0,那么先手必败。反之,先手必胜
SG函数
若题目不再允许每次拿走任意石子,而限定每次拿走的石子数量时
以上结论依然有效,只不过用来做异或运算的不再是每堆石子的数量,而是SG值:
先手时,SG(x) == 0为必败态,SG(x) != 0为必胜态
将n堆石子能进行的所有局面看成一张图,将每张图中起点SG的值异或,结果为0必败,否则必胜
如何计算SG值?
每进行一个操作都能从一个局面转移到另一个局面,用点表示局面,对于初始状态的所有可能操作就组成了一张有向图
若一个点没有出边,那么该点就被定义为终点,SG(终点) = 0
对于当前点,也就是,当前局面x,若其能转移成
y
1
y_1
y1,
y
2
y_2
y2, …,
y
k
y_k
yk局面,那么:
SG(x) = Mex(SG(
y
1
y_1
y1), SG(
y
2
y_2
y2), … , SG(
y
k
y_k
yk))
定义Mex运算,找到一个集合中不存在的最小的非负整数,也就是说在集合中,小于该数的所有非负整数是存在且连续的
也就是说,若SG(x) = n,那么从当前局面x一定能够转移到SG值小于n的任意局面
给定一个初始集合,即已知起点局面,此时如何知道其他局面?即如何推导有向图中其他点的SG值?
和扩展欧的递归求系数类似,我们需要递归到不能递归为止,以找到了终点,由于已知终点的SG值为0,所以此时可以更新终点
通过终点的SG值倒推其他点的SG值:若一点只能通向终点,那么该点的SG值为1
推广:若已知一点连通的其他点,我们需要从0开始枚举这些点的SG值,找一个不存在且最小的值,作为当前点的SG值
(出边连通的点的SG值用set存储,这样就不用排序了)
用石子的数量表示图中的一个唯一点,用其查询该点的SG值
如下图,若某个石子堆中出现了剩余石子数量相同的局面(两个剩余石子数量为3的局面),因为之后能转移的局面是相同的,所以这两个局面可以合并,作为图中的一个唯一点(对于不同的石子堆也是如此)
由此可知,一张图中可能存在多个相同的点(石子数量相同的局面),为防止重复查询降低效率,这里使用记忆化搜索。用数组f记录剩余石子数量为i时,该局面的SG值
模板:
int cnt[M]; // 每次能拿走的石子数量(能进行的操作)
int f[N];
memset(f, -1, sizeof(f));
int sg(int x) // x为堆中剩余的石子数量,函数返回该点的SG值
{
if (f[x] != -1) return f[x];
unordered_set<int> s; // 当前点的出边连通的点的SG值
for (int i = 0; i < m; ++ i ) // m为堆的数量
if (x >= cnt[i]) s.insert(sg(x - cnt[i])) ;
for (int i = 0; ; i ++ )
if (!s.count(i)) return f[x] = i;
}
为什么能从0->!0,!0->0?证明的思路和原题一样,
!0->0:
异或结果为x时,因为Mex的存在,
a
i
a_i
ai一定能变化成
a
i
a_i
ai ^ x,因为
a
i
a_i
ai ^ x <
a
i
a_i
ai
0->!0:
用反证法,证明过程与原题相同
容斥原理练习题
890. 能被整除的数
890. 能被整除的数 - AcWing题库
暴力做法:判断1~n中的每个数是否能被质数整除,最坏的情况每个数要判断m次,总共nm次
容斥原理:
每个集合定义为:在1~n中,质数
p
i
p_i
pi的倍数的个数(能被
p
i
p_i
pi整除的数的个数),用下取整
[
n
/
p
i
]
[n/p_i]
[n/pi]即可求得
问题转换成了:求这些集合的并集中元素的个数
首先,容斥原理展开式有
2
n
2^n
2n-1项,
n
n
n为集合的数量
因为题目给定的质数个数最为16,也就是
n
n
n最多为16
对于展开式中的某一项,我们可以直接对一个数的第
i
i
i位进行位运算判断第
i
i
i个集合是否在该项中(作为并集的一部分)
由于每一项至少要选择一个集合,所以数从1开始
若该项有奇数个集合,加上该项,否则减去该项
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 20;
int p[N];
int n, m;
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++ i ) scanf("%d", &p[i]);
int res = 0;
for (int i = 1; i < 1 << m; ++ i )
{
int t = 1, s = 0; // t为分母,s为分母中集合的个数
for (int j = 0; j < m; ++ j )
{
if ((i >> j) & 1)
{
if ((LL)t * p[j] > n)
{
s = -1;
break;
}
t = (LL)t * p[j];
s ++ ;
}
}
if (s == -1) continue;
if (s % 2) res += n / t;
else res -= n / t;
}
printf("%d\n", res);
return 0;
}
博弈论练习题
891. Nim游戏
891. Nim游戏 - AcWing题库
#include <iostream>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
int res = 0, x;
while (n -- )
{
scanf("%d", &x);
res ^= x;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
893. 集合-Nim游戏
893. 集合-Nim游戏 - AcWing题库
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int K = 110, M = 10010;
int cnt[K], f[M];
int k, n;
int sg(int x)
{
if (f[x] != -1) return f[x];
unordered_set<int> s;
for (int i = 0; i < k; ++ i )
if (x >= cnt[i]) s.insert(sg(x - cnt[i]));
for (int i = 0; ; ++ i )
if (!s.count(i)) return f[x] = i;
}
int main()
{
scanf("%d", &k);
for (int i = 0; i < k; ++ i ) scanf("%d", &cnt[i]);
scanf("%d", &n);
memset(f, -1, sizeof(f));
int x, res = 0;
while (n -- )
{
scanf("%d", &x);
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
892. 台阶-Nim游戏
892. 台阶-Nim游戏 - AcWing题库
将所有奇数台阶看成经典Nim游戏即可
当所有奇数台阶的石子异或结果为0,先手必败。反之先手必胜
证明:
必败的局面为:所有奇数台阶的石子不为0,偶数台阶的石子为0,此时只能从奇数台阶拿石子到偶数台阶,但是对方能拿走你拿下的石子。也就是对方始终能保证偶数台阶的石子为0,并且自己的局面中,一定有奇数台阶的石子不为0
最后的情况为:1号台阶的石子不为0,其他台阶的石子为0,此时先手方必胜
推广一下,保证所有奇数台阶的石子数量异或结果为0,那么一定能递达一个局面,即所有奇数台阶的石子数量为0。此时先手必败
对于所有奇数台阶的石子异或结果为0的局面,先手方:
- 从偶数台阶拿下石子,后手方拿走先手方拿下的石子即可,将变化的奇数台阶数量恢复
- 从奇数台阶拿下石子,后手方一定能从某个奇数台阶拿走石子,使得奇数台阶的石子数量异或结果为0。这是经典的Nim游戏
#include <iostream>
using namespace std;
int main()
{
int n, x, res = 0;
scanf("%d", &n);
for (int i = 1; i <= n; ++ i )
{
scanf("%d", &x);
if (i % 2) res ^= x;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
894. 拆分-Nim游戏
894. 拆分-Nim游戏 - AcWing题库
#include <iostream>
#include <unordered_set>
#include <cstring>
using namespace std;
const int N = 110;
int n, f[N];
int sg(int x)
{
if (f[x] != -1) 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));
scanf("%d", &n);
int res = 0, x;
while (n -- )
{
scanf("%d", &x);
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}