数学知识(数论四)

容斥原理

三类容斥原理的定义:
如果被计数的事物有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/p1p2...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/p1p2
  • 故容斥原理的复杂度近似等于O(2m*m)~ 216*24=220满足要求

步骤:

  1. 因为需要枚举除了 C n 0 C_n^0 Cn0以外的全部2n-1种集合互相组合的情况,所以采用dfs或者位运算的方式,对于每个集合的元素个数都使用 ⌊ n / 质 数 相 乘 ⌋ \lfloor n/质数相乘\rfloor n/求得
  2. 本题使用位运算的方式,对于2n-1种情况,将数字看成二进制,从1(0001)开始枚举,代表选中了第一个集合,2(0010)代表选中第二个集合,3(0011)代表选中1,2两个集合…,从而计算元素个数
  3. 公式为 C n 1 − C n 2 + . . . + / − C n k . . . + / − C n n C_n^1-C_n^2+...+/-C_n^k...+/-C_n^n Cn1Cn2+...+/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 a1a2a3......an=0
  • 先手必胜状态 a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . . . . ⨁ a n ≠ 0 a1 \bigoplus a2 \bigoplus a3 \bigoplus......\bigoplus an \not=0 a1a2a3......an=0

先手必胜状态证明:

  • 假设 a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . . . . ⨁ a n = x a1 \bigoplus a2 \bigoplus a3 \bigoplus......\bigoplus an =x a1a2a3......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 a1a2a3aix......an=xx=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 a1a2a3ai......an=0我们让这个等式与原等式做异或操作,得到 a i ⨁ a i ∗ = 0 ai \bigoplus ai^* =0 aiai=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
  • 此时对手处于必败状态,对手有两种选择
  1. 从奇数台阶拿走石子,那么无论他怎么拿,奇数台阶的异或值都不等于0(即下一个状态是必胜状态
  2. 从偶数台阶拿走石子,那么只需要从他拿走石子的下一级台阶再拿走相同的石子,即可以使对手的奇数台阶异或值回到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)。

在这里插入图片描述

在这里插入图片描述

步骤:

  1. 我们的目标是对于每一个集合S,都求出它对应的SG(S)的值,最后再用这些代表集合的SG值做异或操作,得出来的res若不等于0,则代表先手必胜
  2. 我们用f[x]数组来存储集合中各个节点对应的SG的值,x<=10000
  • 注意每一个集合都包含k(<=100)个状态节点,因为从集合总数出发,每一次取走的值都取决于数字集合S,所以做多有k*n(<=10000)个状态节点
  1. 由于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";
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值