容斥原理
三类容斥原理的定义:
如果被计数的事物有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 - B∩C - C∩A + A∩B∩C
)
拓展到无穷大元素
||绝对值符号
代表取集合的元素个数
- C n 0 + C n 1 + C n 2 + . . . + C n n = 2 n C_n^0+C_n^1+C_n^2+...+C_n^n=2^n Cn0+Cn1+Cn2+...+Cnn=2n
- 左边:从n个数里面选择任意个元素的全部情况,等于右边:对于每一个数来说,只有被选和不被选两种可能
- 对于n类事物求容斥原理,一共存在2n-1个集合
890.能被整除的数
循环处理,时间复杂度>109
利用容斥原理
- 例如m=2,集合A:能被P1整除的数,集合B:能被P2整除的数
|A∪B|=|A|+|B|-|A∩B|
- 时间复杂度:一共有2m-1个集合,对于单个集合,它的元素个数都等于 ⌊ n / p 1 ∗ p 2 . . . ∗ p n ⌋ \lfloor n/p_1*p_2...*p_n \rfloor ⌊n/p1∗p2...∗pn⌋
- 如计算
|A|
= ⌊ n / p 1 ⌋ \lfloor n/p_1 \rfloor ⌊n/p1⌋ ,计算|A∩B|
= ⌊ n / p 1 ∗ p 2 ⌋ \lfloor n/p_1*p_2 \rfloor ⌊n/p1∗p2⌋- 故容斥原理的复杂度近似等于O(2m*m)~ 216*24=220满足要求
步骤:
- 因为需要枚举除了 C n 0 C_n^0 Cn0以外的全部2n-1种
集合互相组合
的情况,所以采用dfs或者位运算的方式,对于每个集合的元素个数都使用 ⌊ n / 质 数 相 乘 ⌋ \lfloor n/质数相乘\rfloor ⌊n/质数相乘⌋求得- 本题使用位运算的方式,对于2n-1种情况,
将数字看成二进制
,从1(0001)开始枚举,代表选中了第一个集合,2(0010)代表选中第二个集合,3(0011)代表选中1,2两个集合…,从而计算元素个数- 公式为 C n 1 − C n 2 + . . . + / − C n k . . . + / − C n n C_n^1-C_n^2+...+/-C_n^k...+/-C_n^n Cn1−Cn2+...+/−Cnk...+/−Cnn对于
k=奇数,其系数为正
,对于k=偶数,其系数为负
#include<iostream>
using namespace std;
typedef long long LL;
const int N=16;
int p[N];
int main(){
int n,m;
cin>>n>>m;
int res=0;
for(int i=0;i<m;i++)cin>>p[i]; //m个质数
for(int i=1;i<1<<m;i++){ //枚举2^m-1种情况(一个集合的组合..两个集合的组合...)
int t=1,s=0; //t代表质数乘积,s区分奇偶项
for(int j=0;j<m;j++){
if(i>>j&1){
if((LL)t*p[j]>n){ //如果质数乘积大于n,n/p等于0,跳过
s=-1;
break;
}
t=t*p[j];
s++;
}
}
if(s!=-1){ //奇数项相加,偶数项相减
if(s%2)res+=n/t;
else res-=n/t;
}
}
cout<<res;
}
博弈论
先手必胜状态:可以走到某一个必败状态,使得对手处于必败状态
先手必败状态:走不到任何一个状态,使得对手处于必败状态
公平组合游戏ICG
若一个游戏满足:
1. 由两名玩家交替行动;
2. 在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
3. 不能行动的玩家判负;
则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。
891.NIM游戏
- 先手必败状态: a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . . . . ⨁ a n = 0 a1 \bigoplus a2 \bigoplus a3 \bigoplus......\bigoplus an =0 a1⨁a2⨁a3⨁......⨁an=0
- 先手必胜状态 a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . . . . ⨁ a n ≠ 0 a1 \bigoplus a2 \bigoplus a3 \bigoplus......\bigoplus an \not=0 a1⨁a2⨁a3⨁......⨁an=0
先手必胜状态证明:
- 假设 a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . . . . ⨁ a n = x a1 \bigoplus a2 \bigoplus a3 \bigoplus......\bigoplus an =x a1⨁a2⨁a3⨁......⨁an=x
- 那么x的
最高位1
,a1~an中一存在一个ai
,它的同样位置也是1 (反证法:如果a1~an的该位置全部是0,那么异或的值一定为0,和x的最高位为1冲突)- 我们从ai堆中
拿走ai-ai^x个石子
,使得ai堆变成ai^x
个石子- 这样该堆的下一个状态就变成了 a 1 ⨁ a 2 ⨁ a 3 ⨁ a i ⨁ x ⨁ . . . . . . ⨁ a n = x ⨁ x = 0 a1 \bigoplus a2 \bigoplus a3 \bigoplus ai \bigoplus x \bigoplus......\bigoplus an =x\bigoplus x=0 a1⨁a2⨁a3⨁ai⨁x⨁......⨁an=x⨁x=0 即一个必败状态
先手必败状态证明
- 无论如何拿走石子,都不可能让下一个状态仍旧是必败状态
- 反证法:假设从
ai
拿走一些石子,使之成为ai*
,并且下一个状态是必败状态- 即 a 1 ⨁ a 2 ⨁ a 3 ⨁ a i ∗ ⨁ . . . . . . ⨁ a n = 0 a1 \bigoplus a2 \bigoplus a3 \bigoplus ai^*\bigoplus......\bigoplus an =0 a1⨁a2⨁a3⨁ai∗⨁......⨁an=0我们让这个等式
与原等式做异或
操作,得到 a i ⨁ a i ∗ = 0 ai \bigoplus ai^* =0 ai⨁ai∗=0即ai和ai*相同,与假设不符
,矛盾
#include<iostream>
using namespace std;
int main(){
int n;
cin>>n;
int res=0;
while(n--){
int x;
cin>>x;
res^=x;
}
if(res)cout<<"Yes";
else cout<<"No";
}
892.台阶-NIM游戏
必胜状态:
所有奇数台阶异或不等于0
必败状态:所有奇数台阶异或等于0
证明
- 假设此时处于必胜状态,通过上一题的证明,可以拿走某一奇数台阶的一些石子,使得奇数台阶异或等于0
- 此时对手处于必败状态,对手有两种选择
- 从奇数台阶拿走石子,那么无论他怎么拿,奇数台阶的异或值都不等于0(
即下一个状态是必胜状态
)- 从偶数台阶拿走石子,那么只需要从他拿走石子的下一级台阶再拿走相同的石子,即可以使对手的奇数台阶异或值回到0(
即处于必败状态
)
#include<iostream>
using namespace std;
int main(){
int n;
cin>>n;
int res=0;
int x;
for(int i=1;i<=n;i++){
cin>>x;
if(i%2)res^=x;
}
if(res)cout<<"Yes";
else cout<<"No";
}
893.集合-NIM游戏
有向图游戏
- 给定一个
有向无环图
,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负
。该游戏被称为有向图游戏。- 任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。
Mex运算
- 设S表示一个非负整数集合。定义
mex(S)
为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S
如S={1,2,3},mex(S)=0
SG函数
- 在有向图游戏中,对于每个节点x,设从x出发
共有k条有向边
,分别到达节点y1, y2, …, yk,定义SG(x)
为x的后继节点y1, y2, ..., yk 的SG函数值构成的集合
再执行mex(S)运算的结果(集合为空则mex(S)的值是0
),即:
SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})- 特别地,整个有向图游戏G的SG函数值被定义为
有向图游戏起点s的SG函数值
,即SG(G) = SG(s)。
步骤:
- 我们的目标是对于每一个集合S,都求出它对应的SG(S)的值,最后再用这些代表集合的SG值做异或操作,得出来的res若不等于0,则代表先手必胜
- 我们用
f[x]
数组来存储集合中各个节点对应的SG的值,x<=10000
- 注意每一个集合都包含k(<=100)个状态节点,因为从集合总数出发,每一次取走的值都取决于数字集合S,所以做多有k*n(<=10000)个状态节点
- 由于S值都是唯一确定的,所以可以共用一个状态数组
f[x]
,将f[x]初始化为-1,避免重复搜索
#include<iostream>
#include<algorithm>
#include<cstring>
#include<unordered_set>
using namespace std;
const int N=110,M=10010;
int n,m;
int s[N],f[M];
int sg(int x){
if(f[x]!=-1)return f[x]; //如果f[x]被枚举过,则返回他的sg值
unordered_set<int> S; //定义哈希表存储当前集合中出现的节点
for(int i=0;i<m;i++){ //枚举每一个可以拿走的数量
int sum=s[i];
if(x>=sum)S.insert(sg(x-sum)); //如果拿走的数量合法,就插入这个节点对应的sg值
}
for(int i=0;;i++){
if(!S.count(i)){ //从小到大枚举每一个自然数,如果他没有在S中出现过,则他就是mex(S)的值,即得到Sg值
f[x]=i;
return i;
}
}
}
int main(){
cin>>m;
for(int i=0;i<m;i++)cin>>s[i];
memset(f,-1,sizeof f);
int res=0;
cin>>n;
for(int i=0;i<n;i++){
int x;
cin>>x;
res^=sg(x);
}
if(res)cout<<"Yes";
else cout<<"No";
}
894.拆分-NIM游戏
这一题是取走一堆石子均摊到另外两个石子堆里
计算出每一堆石子所能到达的局面sg(x)=sg(i)^sg(j),i,j是分出的两堆石子
最后异或所有的sg(x)的值,得到res的结果
#include<iostream>
#include<algorithm>
#include<cstring>
#include<unordered_set>
using namespace std;
const int N=110;
int f[N];
int sg(int x){
if(f[x]!=-1)return f[x];
unordered_set<int> S;
for(int i=0;i<x;i++) //枚举分出的两堆石子的可能数量,假设i<=j
for(int j=0;j<=i;j++)
S.insert(sg(i)^sg(j));
for(int i=0;;i++){
if(!S.count(i)){
f[x]=i;
return i;
}
}
}
int main(){
int n;
cin>>n;
int res=0;
memset(f,-1,sizeof f);
for(int i=0;i<n;i++){
int x;
cin>>x;
res^=sg(x);
}
if(res)cout<<"Yes";
else cout<<"No";
}