一、容斥原理
主要思想:通过韦恩图找规律,总结出求n个集合的并集的公式。
例题:给出整数n和m个质数,求出在1-n中能够被至少一个质数整除的数有多少个。
#include<iostream>
using namespace std;
typedef long long LL;
const int N=1010;
int n,m;
int primes[N];
int main(){
cin>>n>>m;
for(int i=0;i<m;i++) cin>>primes[i];
int res=0;
for(int i=1;i<1<<m;i++){//二进制表示某个数是否被选中,详见注释
int t=1,cnt=0;
for(int j=0;j<m;j++)//遍历给定质数
if(i>>j&1){//当前质数被选中
cnt++;
if((LL)t*primes[j]>n){//超出题目范围,不用管
t=-1;
break;
}
t*=primes[j];//用t算多个数的倍数,对应韦恩图中多个集合的并集
}
if(t!=-1){
if(cnt%2) res+=n/t;//偶数项为加号
else res-=n/t;//奇数项为减号
}
}
cout<<res<<endl;
return 0;
}
注:n个数有种取法,将n用二进制表示,第 i 位为1即表示数 i 被取到,遍历
次即可以涵盖所有数的所有取法。
二、简单博弈论
公平组合游戏:两名玩家交替行动,可执行的行动与先后手无关,不能行动判负。
1.Nim游戏
题目要求:给n堆石子,两名玩家依次拿取任意个数的石子,最后无法操作的为失败。若玩家都采用最优策略,请判断先手是必胜还是必败。
问题分析:将n堆石子数量a1,a2...an相异或,如果结果是0,则当前总数可以均分成两堆,玩家取任意个数石子,对手都可以镜像地取同样多个数石子,先手必败;如果结果是x,则在n堆中必有一堆的数量ai在x的二进制表示最高位(第k位)为1,由于 ai^x < ai 玩家只要取 ai - (ai ^ x) 个石子,第 i 堆数量就由 ai 变为 ai ^ x ,n堆石子数量异或变为 x ^ x ,结果为0,先手必胜。
代码:
#include<iostream>
using namespace std;
int main(){
int n,res=0;
cin>>n;
while(n--){
int x;
cin>>x;
res^=x;//进行异或
}
if(res) cout<<"yes"<<endl;
else cout<<"no"<<endl;
return 0;
}
2.集合-Nim游戏
题目要求:在Nim游戏的基础上给定一个由k个不同正整数组成的集合S,玩家每次只能取S中包含的数的个数个石子,在双方都采用最优策略的情况下,判断先手是否必胜。
主要思想:SG函数为没有出现在可取集合中的最小正整数,终点的SG函数为0。对于每一堆石子,每次取k个不同数量的石子会导致k种不同情况,形成一个有向无环图。由终点的SG为0可以逆推出起点的SG值,假设石子只有当前这一堆,则同Nim游戏证明过程,若起点SG函数为0则先手必败,若不为0则先手必胜。扩展到n堆石子问题,将n堆石子数量的SG函数异或,结果为0则先手必败,否则先手必胜。
代码:
#include<iostream>
#include<cstring>
#include<unordered_set>
using namespace std;
const int N=1010,M=1010;
int s[N],f[M];//s数组存每次可取个数,f数组存每堆石子个数对应的sg函数
int n,m;
int sg(int x){
if(f[x]!=-1) return 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));//将取了s[i]导致的情况添加到图中,注意此处递归
}
for(int i=0;;i++){//求SG函数值,count函数为i出现的次数
if(!S.count(i)) return f[x]=i;
}
}
int main(){
cin>>n;
for(int i=0;i<n;i++) cin>>s[i];
cin>>m;
memset(f,-1,sizeof f);
int res=0;
while(m--){
int x;
cin>>x;
res^=sg(x);
}
if(res) puts("yes");
else puts("no");
return 0;
}