数论模板及分析汇总

前言

进入第四章数论,主要包括了质数约数,欧拉函数,快速幂,欧几里得函数,中国的剩余定理,以及快速常用的数学模板 ,高斯消元用于线代的运算思路,求组合数,以及概率论中的容斥原理,下棋题中场景的博弈论等。这章的模板主要是用于理解,和算法思维的应用,模板过于复杂的,留着运算的思路和想法明白即可。

数论质数

质数

判断质数是常见的签到题,很少遇到被卡的,因当时时间或者存储空间都没有特殊的要求,如果提高了对算法的要求,那么就要更改优化。
第一种通俗且易懂的方法,这时候才知道自己有多low、

ool is_prime(int n){
    if(n < 2) return false; //2是最小的质数,如果n小于2,那n肯定就不是质数
    for(int i = 2;i < n;i ++){ //这个很好理解,从最小的质数2开始枚举到n - 1
        if(n % i == 0){ //如果可以被i整除,说明这个数不是质数
            return false; //返回不是
        }
    }
    return true; //返回是
}

考虑都根号减少次数

bool is_prime(int n){
    if(n < 2) return false; //2是最小的质数,如果n小于2,那n肯定就不是质数
    for(int i = 2;i <= sqrt(n);i ++){ //优化部分
        if(n % i == 0){ //如果可以被i整除,说明这个数不是质数
            return false; //返回不是
        }
    }
    return true; //返回是
}

最后max超进化的算法

bool is_prime(int n){
    if(n < 2) return false;
    for(int i = 2;i <= n / i;i ++){ //优化内容
        if(n % i == 0){
            return false;
        }
    }
    return true;
}

质因数

将每一个数分解为质因数,并按照质因数从小到大的顺序输出每一个质因数的底数和指数
何为质因数,数论里是指能整除给定正整数的质数。除了1以外,两个没有其他共同质因子的正整数称为互质。换句人话来讲就是,能作为约数的质数,并且尽量算出它对应的指数计算。
套用刚才质数的公式

for(int i=2;i<=v/i;i++){
            if(v%i==0){
                 int res=0;
                while(v%i==0){
                    v/=i;
                    res++;
                }//这个while也就是算指数的地方
                cout << i << ' ' << res << endl;
            }
        }

筛质数

还是普通的筛选法,是0(nlogn)埃氏筛法则是O(nloglogn)
先存放素数,从质数加本身的基础上必然是合数。是埃氏,如果是普通的就是不管合数还是死质数,都用来筛后面的倍数。

//埃氏
void get_primes1(){
    for(int i=2;i<=n;i++){
        if(!st[i]){
            primes[cnt++]=i;
            for(int j=i;j<=n;j+=i) st[j]=true;//可以用质数就把所有的合数都筛掉;
        }
    }
}
//普通
void get_primes2(){
    for(int i=2;i<=n;i++){

        if(!st[i]) primes[cnt++]=i;//把素数存起来
        for(int j=i;j<=n;j+=i){//不管是合数还是质数,都用来筛掉后面它的倍数
            st[j]=true;
        }
    }
}

线性筛O(n)

void get_primes(){
    //外层从2~n迭代,因为这毕竟算的是1~n中质数的个数,而不是某个数是不是质数的判定
    for(int i=2;i<=n;i++){
        if(!st[i]) primes[cnt++]=i;
        for(int j=0;primes[j]<=n/i;j++){
            st[primes[j]*i]=true;//用最小质因子去筛合数
 /*1)当i%primes[j]!=0时,说明此时遍历到的primes[j]不是i的质因子,那
 么只可能是此时的primes[j]<i的最小质因子,所以primes[j]*i的最小质因子就是primes[j];
 2)当有i%primes[j]==0时,说明i的最小质因子是primes[j],因此primes[j]*i的最小质因子也就应该是
 prime[j],之后接着用st[primes[j+1]*i]=true去筛合数时,就不是用最小质因子去更新了,因为i有最小
 质因子primes[j]<primes[j+1],此时的primes[j+1]不是primes[j+1]*i的最小质因子,此时就应该
 退出循环,避免之后重复进行筛选。*/
            if(i%primes[j]==0) break;
        }
    }
}

数论约数

约数

约数也就是可以除数,反着素数求就可以了。

vector <int> acwing(int n){
    vector<int> res;
    for(int i=1;i<=n/i;i++){
        if(n%i==0)
        {
            res.push_back(i);
            if(i!=n/i) res.push_back(n/i);
        }
    }
    sort(res.begin(),res.end());
    return res;
}

约数个数

在这里插入图片描述
开始导入数论的思维,先清楚定义之后,然后针对数可以拆分成刚才的整约数和指数的情况,所以用hash哈希表存比较好,使用pair<int,int>的组合。之后可以看一下auto循环的使用,记住格式。

while(n--){
        cin >> x;
        for(int i = 2;i <= x/i; ++i){
            while(x % i == 0){
                x /= i;
                hash[i] ++;
            }
        }
        if(x > 1) hash[x] ++;
    }
    for(auto i : hash) ans = ans*(i.second + 1) % mod;

约数之和

约数之和是在计算约数个数之后按照数论中公式的推理演化的算法而已

while(n--){
        cin >> x;
        for(int i = 2;i <= x/i; ++i){
            while(x % i == 0){
                x /= i;
                hash[i] ++;
            }
        }
        if(x > 1) hash[x] ++;
    }
    LL res=1;
    for(auto i:hash){
        LL t=1;
        int p=i.first,a=i.second;
        while(a--){
            t=(t*p+1)%mod;
        }
        res=(t*res)%mod;
    }

最后也是用了高中数学学到的提前一项乘数的连乘。

最大公约数

最大公因数(Greatest Common Divisor) 利用“辗转相除法”求,借助递回、非递回思想;
最小公倍数(Least Common Multiple) 通过公式:最大公因数* 最小公倍数=两数乘积 求解。
最大公约数和质数约数也是一样的,都是基础背诵模板。
辗转相除法,又称欧几里得算法。还有c++函数模板里的gcd,可以直接用,但是最好知道辗转相除法的模板。
库numeric中的gcd
库algorithm中的__gcd

/*
求两个正整数 a 和 b 的 最大公约数 d
则有 gcd(a,b) = gcd(b,a%b)
证明:
    设a%b = a - k*b 其中k = a/b(向下取整)
    若d是(a,b)的公约数 则知 d|a 且 d|b 则易知 d|a-k*b 故d也是(b,a%b) 的公约数
    若d是(b,a%b)的公约数 则知 d|b 且 d|a-k*b 则 d|a-k*b+k*b = d|a 故而d|b 故而 d也是(a,b)的公约数
    因此(a,b)的公约数集合和(b,a%b)的公约数集合相同 所以他们的最大公约数也相同 证毕#
*/
int gcd(int a, int b){
    return b ? gcd(b,a%b):a; 
}
int gcd(int a,int b){
 if(!b)return a;
 else gcd(b,a%b);
}
int gcd2(int a, int b){//更相减损 扩展
    while(a != b)
        if(a > b)
            a -= b;
        else
            b -= a;
    return a;
}
int gcd4(int a, int b){//超快,重点是背住这个
    if(!a || !b)
        return max(a, b);
    for(int t; t = a % b; a = b, b = t);
    return b;//也有着gcd的思路,之前的判断就节省了很多时间,此方法和系统自带的algorithm是差不多的
}

数论欧拉

欧拉函数

在这里插入图片描述
先明确一下什么是欧拉函数,并且将其定理推导清楚
在这里插入图片描述
因为算法主要是先求到它们的质因数,算法的瓶颈主要在分解质因数上,分解质因数的时间复杂度为O(√a),但由于有n组数据,所以时间复杂度为O(√a∗n)。
可以和概率论中的互斥定理应用相互挂钩理解

#include<bits/stdc++.h>
using namespace std;
int main(){
    int n;
    cin>> n;
    while(n--){
        int x;
        cin>>x;
        int res=x;
        for(int i=2;i<=x/i;i++){
            if(x%i==0){
                res=res/i*(i-1);
                while(x%i==0)x/=i;
            }
        }
        if(x>1)res=res/x*(x-1);
        cout<<res<<endl;
    }
    return 0;
}

筛法欧拉

结合线性筛法和欧拉计算公式,数论更像是运算的方式的结合。
筛法求欧拉函数(适用于1~n求欧拉的情况)
① 先写出线性筛算法的模板。
② 考虑特殊情况:若该数是质数pp的话,那么该数的欧拉函数就p−1。
③ 每个数的欧拉函数与质因子的次数无关。例如:N=2100×3100但是N的欧拉函数还是N×(1−12)×(1−13)N×(1−12)×(1−13)
④ 若i%primes[j]==0,由于primes[j]是i的一个质因子,并且在计算i的欧拉函数的时候已经计算了primes[j]出现的情况(1−1primes[j]),所以φ(primes[j]×i)=primes[j]×φ(i)。
⑤ 若i%primes[j]!=0,primes[j]就一定是primes[j]×i的最小质因子,而且primes[j]不包含在i的质因子当中。由于φ(i)=i×(1−1p1)×(1−1p2)×…×(1−1pk),所以φ(primes[j]×i)=primes[j]×i×(1−1p1)×(1−1p2)×…×(1−1pk)×(1−1primes[j])。最终就可以得到φ(primes[j]×i)=primes[j]×φ(i)×(1−1primes[j])=φ(i)×(primes[j]−1)。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn=1e6+10;
LL prime[maxn],cnt,ph[maxn];
bool st[maxn];
LL oula(int n){
    ph[1]=1;//互斥定理ph[1]=1;
    for(int i=2;i<=n;i++){//线性筛
        if(!st[i]){
            prime[cnt++]=i;//质数之后的全是,没筛过的就是质数
            ph[i]=i-1;
        }
        for(int j=0;prime[j]<=n/i;j++){
            st[prime[j]*i]=true;
            if(i%prime[j]==0){
                ph[prime[j]*i]=ph[i]*prime[j]; //
                break;
            }//线性质数筛法
            ph[prime[j]*i]=ph[i]*(prime[j]-1);//公式推导
        }
    }
    /*第二阶段for(int j=i;i<=n/i;j++)对比一下,
    *若是使用prime也就是质数数组解决其做为约数的数据集
    */
    LL res=0;
    for(int i=1;i<=n;i++){
        res+=ph[i];
        
    }
    return res;
}
int main(){
    int n;
    cin>>n;
    cout<<oula(n)<<endl;
    return 0;
}

数论快速幂

快速幂

在这里插入图片描述
<<是*2,>>是/2;
有多种解决方法,第一种暴力, while(b--) res = res * a %p;
迭代版本,画一个重点,计算机中的二进制计算是高效的

long long qmi(long long a,int b,int p)
{
    long long res=1;
    while(b)//对b进行二进制化,从低位到高位
    {
        //如果b的二进制表示的第0位为1,则乘上当前的a
        if(b&1) res = res *a %p;
        //b右移一位
        b>>=1;
        //更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
        a=a*a%p;
    }
    return res;
}

递归版,个人感觉如出一辙,同样的他们的时间复杂度也是一样的,表示有这个印象就好

ull quick_pow(ull a,ull b,ull p)
{
    if(b==0) return 1;
    a%=p;
    ull res=quick_pow(a,b>>1,p);
    if(b&1) return res*res%p*a%p;//如果b的二进制表示的第0位为1,则乘上当前的a
    return res*res%p;
}

快速幂求逆元

在这里插入图片描述
推导过程,求其逆元也就是快速幂p-2次,在推导过程中使用了费马小定理
在这里插入图片描述

数论欧几

扩展欧几里得算法

在这里插入图片描述
在这里插入图片描述
其实数论这一块主要是结合高数线代概率论的知识进行数据分析,最好是在纸上先推演一遍,按照推导的过程进行代码语言辨析就OK了,能简化就简化。
将这一部分的知识和上面的快速幂求逆元结合
在这里插入图片描述

线性同余方程

也就是上面两部分的综合实现
欧几里得的代码

int exgcd(int a,int b,int &x,int &y){
    if(b==0){
        x = 1,y = 0;
        return a;
    }
    int d = exgcd(b,a%b,x,y);
    int tmp = x;
    x = y;
    y = tmp -a/b*y;
    return d;
}
线性同余方程在上一节的最后一图解3上就分析啦
if(b%d==0){
            int t = b/d;
            printf("%d\n",((long long)x*t%(m/d)+(m/d))%(m/d));
        }
        else
            printf("impossible\n");

线代

高斯消元

在这里插入图片描述
建议直接翻线代数,这道题更像是模拟一下解题过程

int gauss(){
    int c = 0, r = 0;
    for(; c < n; c++){
        int t = r;
        for (int i = r; i < n; i ++ ) // 第一步,找绝对值最大的行
        {
            if(fabs(a[i][c]) > fabs(a[t][c]))
                t = i; 
        }

        if(fabs(a[t][c]) < eps) continue; // 代表这一列已经被处理过了

        for(int i = c; i <= n; i++) // 第二步 
            swap(a[t][i], a[r][i]); // 将这一行调到最上面

        for(int i = n; i >= c; i--) // 第三步
            a[r][i] /= a[r][c]; // 要倒着算,否则会影响后面的数

        for(int i = r + 1; i < n; i++) // 第四步
        {
            if(fabs(a[i][c]) > eps)  // 如果是0就不用操作了
            {
                for(int j = n; j >= c; j--)
                {
                    a[i][j] -= a[r][j] * a[i][c];
                }
            }
        }
        r++;
    }

    if(r < n) // 方程数 < n
    {
        for(int i = r; i < n; i++)
        {
            if(fabs(a[i][n]) > eps) // 0 != 0
            {
                return 2; // 无解
            }
        }

        return 1; // 无数解, 0=0
    }

    for(int i = n - 1; i >= 0; i --) // 有解从下往上回代
    {
        for(int j = i + 1; j < n; j++)
        {
            a[i][n] -= a[i][j] * a[j][n];
        }
    }
    return 0;

}

高斯消元解异或线性方程

int gauss()
{
    int r=0,c;
    for(int c=0;c<n;c++)   //按列进行枚举
    {
        int t = r;     //找到非0行,用t进行存储
        for(int i=r;i<n;i++)
            if(g[i][c])
                t=i;

        if(!g[t][c]) continue; //没有找到1,继续下一层循环

        for(int i=c;i<=n;i++) swap(g[r][i],g[t][i]);  //把第r行的数与第t行交换。

        for(int i=r+1;i<n;i++)    //用r行把下面所有行的当前列消成0
            if(g[i][c])
                for(int j=n;j>=c;j--)
                    g[i][j] ^= g[r][j];
        r++;
    }
    if(r<n)
    {
        for(int i=r;i<n;i++)
        {
            if(g[i][n]) return 2;
        }
        return 1;
    }

    for(int i=n-1;i>=0;i--)
    {
        for(int j=i+1;j<n;j++)
        {
            g[i][n] ^= g[i][j ]* g[j][n];
        }
    }
    return 0;
}

数论组合数

组合数的目的很单纯,但根据题目的内容和时间还有空间的限制会对算法进行优化。

组合数四连解

使用暴力流,也就是直接套用高中数学的公式,那个既不省力也不讨好,还容易各种越界超时
所以使用化简替代,一层层叠加上去,这个就很简单了,递推公式
在这里插入图片描述

for(int i=0;i<=2000;i++)
    {
        for(int j=0;j<=i;j++)
        {
            if(!j) f[i][j]=1;
            else f[i][j]=(f[i-1][j-1]+f[i-1][j])%mod;
        }
    }

第二种解决方式是预处理阶乘,用逆元解决除法的操作。其中使用的是快速幂,快速幂求逆元,以及递推

#include<iostream>
using namespace std;
typedef long long LL;
const int mod=1e9+7,N=100005;
int fac[N],facni[N];
int qmi(int a,int b,int m){
    int res=1;
    while(b){
        if(b&1)
            res=(LL)res*a%m;
        a=(LL)a*a%m;
        b>>=1;
    }
    return res;
}
int main(){
    fac[0]=facni[0]=1;  //fac代表阶乘,facni代表逆元
    for(int i=1;i<=100000;i++){
        fac[i]=(LL)fac[i-1]*i%mod;
        facni[i]=(LL)facni[i-1]*qmi(i,mod-2,mod)%mod;
    }
    int n;
    scanf("%d",&n);
    while(n--){
        int x,y;
        scanf("%d%d",&x,&y);
        printf("%d\n",(LL)fac[x]*facni[y]%mod*facni[x-y]%mod);  //直接套用公式
    }
    return 0;
}

Lucas公式解法
在这里插入图片描述
代码已经转化,并不需要全文背诵,但是需要知道运算流程

#include <iostream>

using namespace std;

typedef long long LL;

int readInt() {
    int res = 0;
    char ch = getchar();
    while (ch <= '9' && ch >= '0') {
        res =res * 10 + ch - '0';
        ch = getchar();
    }
    return res;
}

LL readLong() {
    LL res = 0;
    char ch = getchar();
    while (ch <= '9' && ch >= '0') {
        res = res * 10 + ch - '0';
        ch = getchar();
    } 
    return res;
}

int quick_power(int a, int k, int p) {
    int res = 1;
    while (k) {
        if (k & 1) res = 1ll * res * a % p;
        k >>= 1;
        a = 1ll * a * a % p;
    }
    return res;
}


int C(int a, int b, int p) {
    int res = 1;
    for (int i = 1, j = a; i <= b; i ++, j --) {
        res = 1ll * res * j % p;
        res = 1ll * res * quick_power(i, p - 2, p) % p;
    }
    return res;
}

int lucas(LL a, LL b, int p) {
    if (a < p && b < p) return C(a, b, p);
    return 1ll * C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
} 

int main() {
    int n;
    n = readInt();

    while (n -- ) {
        LL a, b;
        int p;
        a = readLong(), b = readLong(), p = readInt();
        // cout << a << " " << b << " " << p << endl;
        cout << lucas(a, b, p) << endl;
    }
    return 0;
}

组合数综合

题目极其性感,求Ca -b的值,注意使用精度,因为数值较大
在这里插入图片描述
推导之后,只能说是高数中以后的小tip,知道能解决就好
解题方法使用3步

  1. 筛素数(1-5000)本章节中的for hash’
  2. 求每一个素数的次数 for prime【】
  3. 用高精度乘法把所有质因子都乘上 在数据结构基础模板上的高精度模板
void get_primes(int n)
{
    for(int i=2;i<=n;i++)
    {
        if(!st[i])primes[cnt++]=i;
        for(int j=0;primes[j]*i<=n;j++)
        {
            st[primes[j]*i]=true;
            if(i%primes[j]==0)break;
        }
    }
}
// 对p的各个<=a的次数算整除下取整倍数
int get(int n,int p)
{
    int res =0;
    while(n)
    {
        res+=n/p;
        n/=p;
    }
    return res;
}
vector<int> mul(vector<int> a, int b)
{
    vector<int> c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}

卡特兰数

给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。
在这里插入图片描述
故推出公式
在这里插入图片描述
也就是卡特兰公式

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010, mod = 1e9 + 7;

// 快速幂
int qmi(int a, int k, int p)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}


int main()
{
    int n;
    cin >> n;

    int a = n * 2, b = n;
    int res = 1;
    for (int i = a; i > a - b; i -- ) res = (LL)res * i % mod;

    for (int i = 1; i <= b; i ++ ) res = (LL)res * qmi(i, mod - 2, mod) % mod;

    res = (LL)res * qmi(n + 1, mod - 2, mod) % mod;

    cout << res << endl;

    return 0;
}

容斥原理

概率论中的容斥原理,在之前的模板中也应用了概率论中的定理,这个例题是得出能被整除的数
给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。
请你求出 1∼n 中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。

  1. 每个集合实际上并不需要知道具体元素是什么,只要知道这个集合的大小,大小为|Si|=n/pi, 比如题目中|S1|=10/2=5,|S2|=10/3=3。
  2. 交集的大小如何确定?因为pi均为质数,这些质数的乘积就是他们的最小公倍数,n除这个最小公倍数就是交集的大小,故|S1⋂S2|=n/(p1∗p2)=10/(2∗3)=1
  3. 如何用代码表示每个集合的状态?这里使用的二进制,以m = 4为例,所以需要4个二进制位来表示每一个集合选中与不选的状态,1101 m=4 1这里表示选中集合S1,S2,S4,故这个集合中元素的个数为 n/(p1∗p2∗p4)。
  4. 因为集合个数是3个,根据公式,前面的系数为(−1)^(3−1)=1。所以到当前这个状态时,应该是res+=n/(p1∗p2∗p4)。这样就可以表示的范围从0000到1111的每一个状态.
#include<iostream>
using namespace std;
typedef long long LL;

const int N = 20;
int p[N], n, m;

int main() {
    cin >> n >> m;
    for(int i = 0; i < m; i++) cin >> p[i];

    int res = 0;
    //枚举从1 到 1111...(m个1)的每一个集合状态, (至少选中一个集合)
    for(int i = 1; i < 1 << m; i++) {
        int t = 1;             //选中集合对应质数的乘积
        int s = 0;             //选中的集合数量

        //枚举当前状态的每一位
        for(int j = 0; j < m; j++){
            //选中一个集合
            if(i >> j & 1){
                //乘积大于n, 则n/t = 0, 跳出这轮循环
                if((LL)t * p[j] > n){    
                    t = -1;
                    break;
                }
                s++;                  //有一个1,集合数量+1
                t *= p[j];
            }
        }

        if(t == -1) continue;  

        if(s & 1) res += n / t;              //选中奇数个集合, 则系数应该是1, n/t为当前这种状态的集合数量
        else res -= n / t;                      //反之则为 -1
    }

    cout << res << endl;
    return 0;
}

博弈论

Nim游戏

给定 n 堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
需要推导出一个异或思维,代码会很简单,但是理解异或思维是nim游戏里的关键
假设亦或值为x,x≠0
这时在x的二进制表示中一定是存在1的。
鉴于x为亦或值,所有的1一定可以在A[i(任意正整数)]的二进制表示中被找到。

找到x的二进制表示中最高位的1,
假设它就存在于第i堆石子的二进制表示中。
那么我们只需要从第i堆石子中拿走A[i]-A[i]^x个石子
亦或值就会变成A[1]A[2]……A[i]-(A[i]-A[i]x)……A[n],=A[1]A[2]……A[i]x……A[n]

A[1]A[2]……^A[n]是等于x的。
所以上述的式子就等于x^x=0

所以当x≠0,该谁走了,这个人就一定可以把x变成0,再推给对手
对手不管怎么取,一定会再次把x变成非0(这个很好理解)
最后因为00……^0=0,所以输掉的一定是对手。
加一个小插曲,这也就说明了脑子是个好东西,理解了异或,这道题的代码真就1min就出来了

#include<iostream>
using namespace std;
int main(){
    int n,res=0;
    scanf("%d",&n);
    while(n--){
        int x;
        scanf("%d",&x);
        res^=x;
    }
    puts(res?"Yes":"No");
    return 0;
}

台阶Nim

现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 ai 个石子(i≥1)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。问如果两人都采用最优策略,先手是否必胜。
将奇数台阶看做一个经典的Nim游戏,如果先手时奇数台阶上的值的异或值为0,则先手必败,反之必胜

证明:
先手时,如果奇数台阶异或非0,根据经典Nim游戏,先手总有一种方式使奇数台阶异或为0,于是先手留了奇数台阶异或为0的状态给后手
于是轮到后手:
①当后手移动偶数台阶上的石子时,先手只需将对手移动的石子继续移到下一个台阶,这样奇数台阶的石子相当于没变,于是留给后手的又是奇数台阶异或为0的状态
②当后手移动奇数台阶上的石子时,留给先手的奇数台阶异或非0,根据经典Nim游戏,先手总能找出一种方案使奇数台阶异或为0
因此无论后手如何移动,先手总能通过操作把奇数异或为0的情况留给后手,当奇数台阶全为0时,只留下偶数台阶上有石子。
(核心就是:先手总是把奇数台阶异或为0的状态留给对面,即总是将必败态交给对面)

因为偶数台阶上的石子要想移动到地面,必然需要经过偶数次移动,又因为奇数台阶全0的情况是留给后手的,因此先手总是可以将石子移动到地面,当将最后一个(堆)石子移动到地面时,后手无法操作,即后手失败。

#include <iostream>

using namespace std;

int main()
{
    int res = 0;
    int n;
    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;
}

总结

做一个小总结,Nim游戏通常都是用异或思维来解决的,以后多遇到多总结就好了,IGP公平游戏并没有字面那层意思,要不是先手要不就是后手,反正需要推理,这也是数论这一章较为费脑子的地方。还是建议刷题总结吧。
比较开心的是,下午刚拿到了K580键盘,手感极其丝滑而且还安静的一批,罗技蓝牙鼠标键盘一套,敲代码贼爽,都不用抬手。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

磊哥哥讲算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值