Acwing 第四章模板及详解(数学知识)

  • Post author:
  • Post category:其他



一、质数

二、约数

三、欧拉函数

四、快速幂

五、扩展欧几里得算法

六、中国剩余定理

七、高斯消元

八、组合计数

九、容斥原理

十、简单博弈论


一、质数


质数

质数,在大于1的整数中,有且只有1和他本身两个因数的数,也叫做素数


试除法判定质数


1、sqrt(x),函数计算的时间比较高

bool is_prime(int x) 
{
    // 质数大于1
    if(x < 2) return false;
    for(int i = 2; i <= sqrt(x); i++) 
    {
        if(x % i == 0) return false;
    }
    return true;
}


2、i × i ≤ x

bool is_prime(int x) 
{
    // 质数大于1
    if(x < 2) return false;
    for(int i = 2; i * i <= x; i++)
    {
        if(x % i == 0) return false;
    }
    return true;
}


3. 推荐此方法i ≤ n / i

bool is_prime(int x) 
{

    // 质数大于1
    if(x < 2) return false;
    for(int i = 2; i <= x / i; i++) 
    {
        if(x % i == 0) return false;
    }
    return true;
}


试除法判定质数

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return false;
    return true;
}
试除法分解质因数 
void divide(int x)
{
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
        {
            int s = 0;
            while (x % i == 0) x /= i, s ++ ;
            cout << i << ' ' << s << endl;
        }
    if (x > 1) cout << x << ' ' << 1 << endl;
    cout << endl;
}



(1)朴素筛法求素数


朴素筛法O(nlogn)

算法核心:把每个数的所有倍数筛掉


调和级数:1 + 1/2 + 1/3 +…+ 1/n = lnn + c(c欧拉常数=0.577)


算法时间复杂度:最外层遍历整个数组是n(其实不用管,只用看内部总次数即可),内部循环总次数是n/2,n/3,n/4…1,累加得n(1/2 + 1/3 + 1/4 +…+1/n)=nlnn=nlogn

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (st[i]) continue;
        primes[cnt ++ ] = i;
        for (int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}
//void get_prime(int x)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++] = i;

		for(int j = i + i ; j <=  n ; j += i) st[j] = true;
    }
}

(2)埃式筛法O(nloglogn)

算法核心:把每个质数的所有倍数筛掉

质数定理:1~n中由n/logn个质数

算法时间复杂度:由(1)可得:O(nlonglongn)当数据不是足够大时与O(n)接近


void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (st[i]) continue; //st如果是true 说明被筛过,那么它的倍数肯定被筛过,所以直接跳过
        
        //接下来对该质数的所有倍数进行筛选
        primes[cnt ++ ] = i;
        for (int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}



(3)线性筛法求素数



算法核心:x只会被它的最小质因数筛去,即每个数字只被筛选一次,因此是线性的n。



证明每个x都能被筛掉:

对于一个合数x,x一定存在一个最小质因子,假设pj是x 的最小质因子,当i枚举到x/pj时,x就被筛了,因为x只有一个最小质因数,因此每个数字只被筛选一次。



算法时间复杂杂度:因为每个数只被筛过一次,因此为O(n)

void get_prime(int x)
{
    for(int i = 2 ; i <= x ; i++)
    {
        if(!st[i]) primes[cnt ++] = i;
	/**
	for循环判断语句中不需要j<cnt。分两种情况。
	1.i为合数,当primes[j]取到i的最小质因子时就break 此时 j<cnt
	2.i为质数,当primes[j]的值和i相等时就break 此时j == cnt-1
    **/
        for(int j = 0 ; primes[j] <= x / i ; j++)
        {										
            st[primes[j] * i] = true; //筛去primes[j]的倍数
   /*
   针对st[primes[j] * i] = true;本质上分两种情况
   1.i%pj == 0, 因为primes[j]是顺序遍历,因此当当一次模为零时,primes[j]一定为i的最小质因
   子,primes[j]也一定为primes[j]*i的最小质因子
   2.i%pj != 0, 同样因为primes[j]是顺序遍历,primes[j]一定小于i的所有质因子
   所以primes[j]也一定为primes[j]*i最小质因子
   */
            
            if(i % primes[j] == 0) break;//当primes[j]是i的最小质因数时break(为了  			   
            //遵守算法的核心,避免重复的筛选)。如果继续用primes[j+1]去筛选,此时,
            //primes[j+1]大于i的最小质因子,那么也同样不是primes[j+1]*i的最小质因子
   
        }
        
    }
}

模板:

int primes[N], cnt;     // primes[]存储所有素数
bool st[N];         // st[x]存储x是否被筛掉

void get_primes(int 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;
            if (i % primes[j] == 0) break;
        }
    }
}


二、约数

根据分摊分析,当数据最够多时,每个数的约数平均为logn个。

(第一个数是n个数的约数 , 第二个数是n/2个数的约数 , 以此类推第n个数是n/n个数的约数。

累加得n(1/1+1/2+1/3+…+1/n)=n(lnn+c)≈nlogn)


(1)试除法O(sqrt(n))

思路:从小到大枚举较小的约数即可

vector<int> get_divisors(int x)
{
    vector<int> res;
    
    for(int i = 1 ; i <= x / i ; i++) 
    {
        if(x % i == 0) 
        {
            res.push_back(i);
            if(i != x / i) res.push_back(x / i); //特判边界
        }
    }
    
    sort(res.begin() , res.end());
    
    return res;
}


(2)约数个数


题目:给定n个正整数ai,请你输出这些数的乘积的约数个数,答案对109+7取模。

算法思想:

基于算数基本定理:N = p1a1 * p2a2 * … * pkak

N的任意一项约数可以写成 d = q1b1+q2b2+…+qkbk

不同的b数组组合而成的约数d是不同(即因式分解不同)

同理不同的a数组组合而成的N也是不同的,

而a1有a1+1种选法,a2有a2+1种选法…ak有ak+1种选法

因此约数个数:(a1+1)(a1+1)(a3+1)…(ak+1)

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const int P = 1e9 + 7;
int main()
{
    unordered_map<int , int> primes; //利用哈希表存储
    int n;
    cin >> n; 
    while(n --) //分解质因数
    {
        int x;
        cin >> x;    
        for(int i = 2 ; i <= x / i; i++)
        {
            while(x % i == 0)
            {
            primes[i]++;
            x /= i;
            }
        }
        if(x > 1) primes[x]++;
    }
    
    ll ans = 1;
    
    for(auto p : primes) //迭代一次,套用公式即可
        ans = ans * (p.second + 1) % P;  
    cout << ans << endl; 
    return 0;
}





(3)约数之和


同样的某个数N可以展开为 N = p1a1 * p2a2 * … * pkak

约数之和为:(p10+p11+p12+…+p1a1) * (p20+p21+p22+…+p2a2) * …* (pk0+“pk1+pk2+…+pkak)

即排列问题,展开后就是每一个约数,且都不相等

//求质因数与上方代码相同
for(auto t : primes)
    {
        int p = t.first , q = t.second;
        ll res = 1;
        while(q--)  res = (res * p + 1) % P; //这里有一个小技巧><
        
        ans = ans * res % P;
    }


(4) 最大公约数


(greast common divisor简称“gcd”)

递归版辗转相除法(欧几里得算法):核心是gcd(a , b) = gcd(b , a % b)

原理:

由基本定理得:当d能分别整除a,b时,d就能整除xa+yb。

显然a % b = a – c * b(其中c=a/b)。

即证:(a , b) = (b , a – c * b)

记d=(a,b),则d|a,d|b,所以根据基本定理得d|a-cb,所以d=gcd(b , a-cb)

记d=(b , a-cb) , 则d|b , d|a-cb ,所以根据基本定理得:d|(cb)+(a-cb) 即a|b ,所以d=(a , b)

所以(a , b) = (b , a-c*b)成立,说明(a,b)的公约数就是(b , a – c * b)的公约数,所以等号两边的最大公约数相同。

不断地辗转相除直到第二个参数为0,因为0模上任何一个非零的整数都是0,所以任何一个数都可以看作是0的约数,而b的最大约数是b,那取交集后,gcd(b,0)当然是b。

所以gcd(a , b) = gcd(b , a % b)成立


三、欧拉函数


欧拉函数 O(N)=1到N中所有与N互质的数的个数,如果可以把N写成它的分解质因数公式:


N=p1的a1次方 * p2的a2次方 * …..pk的ak次方


则它的欧拉函数等于N*(1-1/p1)*(1-1/p2)*…(1-1/pk),这是由容斥原理得到的(数学集合论),


我们将n减去所有它的因子的倍数,这其中有些数被减了两次,如p1,p2共同的倍数,我们再逐一加上每两个数的倍数,再减去每三个数的倍数…就能得到与N互质的数的个数了。



1 ~ N 中与 N 互质的数的个数被称为欧拉函数


欧拉函数的证明

利用容斥原理,求1 ~N-1中与N互斥的数的个数s

拆分出N的质因子p1、p2、p3…

s = N – N/p1 – N/p2 – N/p3-…-N/pk

+N/(p1*p2) + N/(p1*p2) + … + N/(p1*pk) + … + N/(pk-1*pk)

– N/(p1*p2*p3) – N/(p1*p2*p4) – … -N/(pk-2*pk-1*pk)

+N/(p1*p2*p3*p4) + … + N/(pp-3*pp-2*pp-1*pp)

…(以此类推)


p1的倍数由N/p1个,p2的倍数有N/p2个…pk的倍数有N/pk个,因此要减去。

但是有的数同时是p1和p2的倍数,所以会被重复减去,因此在第二行加回。

但是有的数同时是p1、p2、p3的倍数,在第一行会被重复减去三次,而在第二行会重复加回三次,相当于没变,因此要在第三行减去。

以此类推。。。。。。。

最后欧拉函数的公式如下:s = N * (1-1/p1) * (1-1/p2) * (1-1/p3) … * (1-1/pk)


接下来用公式来求欧拉函数 O(n*sqrt(n))


#include <bits/stdc++.h>

using namespace std;

typedef long long ll;//数据范围较大记得用long long
const int N = 110;

int n;

int main()
{
    cin >> n;
    while(n--)
    {
        int cnt = 0;
        ll primes[N];
        int x;
        cin >> x;
        ll ans = x;
        
        for(int i = 2 ; i <= x / i ; i++)
        {
            if(x % i == 0)
            {
                ans = ans / i * (i - 1);
                while(x % i == 0) x /= i;
            }
        }
        
        if(x > 1) ans = ans / x * (x - 1);

         
        cout << ans << endl;
    }
    return 0;
}




筛法求欧拉函数

线性筛法求欧拉函数之和O(n)

核心:在找到质数和筛掉和筛掉合数时,利用欧拉函数的公式直接计得出其欧拉函数

int primes[N], cnt;     // primes[]存储所有素数
int euler[N];           // 存储每个数的欧拉函数
bool st[N];         // st[x]存储x是否被筛掉
void get_eulers(int n)
{
    euler[1] = 1;
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i])
        {
            primes[cnt ++ ] = i;
            euler[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            int t = primes[j] * i;
            st[t] = true;
            if (i % primes[j] == 0)
            {
                euler[t] = euler[i] * primes[j];
                break;
            }
            euler[t] = euler[i] * (primes[j] - 1);
        }
    }
}


欧拉定理


定理:若a与n互质,则aΦ(n) ≡ 1(mod n)


证明:


假如1 ~ n中,所有与n互质的数是A数组:a1、a2、a3…aΦ(n)

设M数组:aa1、aa2、aa3…aaΦ(n)(同样与n互质)

下面先证明两个推理:

一、M数列中不存在两个数模n同余。

用假设法证明:

1.假如M数列存在ma与mb模n同余,即ma≡mb(mod n)

2.移项得mi – mj ≡ 0 (mod n)

3.即aai-aaj ≡ 0 (mod n)

4.提取公因式得:a(ai-aj) ≡ 0 (mod n)

5.因为a与n互质,所以要是等式左边与n互质,那么必然:(ai-aj) ≡ 0 (mod n)

6.则ai ≡ aj(mod n),这显然与A数组中的数的性质相违背(A数组中各个数都小于n,并且不同,因此不可能模n同余)。

7.所以假设不成立!即:M数列中不存在两个数模n同余!

二、M中的数对n的余数全部与n互质(即mi%n 与 n 互质)

证明:

1.已知a与n互质,ai与n互质,则aai也与n互质,也可得M数列中的数与n互质。

2.然后带入欧几里得算法中推导即可:gcd(aai , n) = gcd(n , aai%n ) = 1 , 即mi%n 与 n 的最大公约数是1

(a与n互质,pi与n互质,因此api与n互质)

3.得证:M中的数对n的模全部与n互质!

根据这两个推理,可以开始推导欧拉定理。

根据推理二,我们可以知道M中的数模n全部与n互质,而且M中不

存在两两对n取模相等的数,而且A、M数组长度相同,因此一个M中的数一定

可以在A中找到唯一一个数和它模n同余(即两个数组在模n的意义下是等价的),因此可得两个数列分别的乘积对n同余

根据A数组的含义,A数组模n后的数组就是A数组本身

即:m1m2m3m4…mΦ(n) ≡ a1a2a3…aΦ(n) (mod n)

将每项m展开的:a * a1 * a * a2 * a * a3…a * aΦ(n) ≡ a1a2a3…aΦ(n) (mod n)

化简得:aΦ(n)(a1a2a3…aΦ(n)) ≡ a1a2a3… aΦ(n) (mod n)

因A数列中的每一项都跟n互质,那么整个数列的乘积也和n互质,所以

可以两边同除a1a2a3…aΦ(n)得:aΦ(n) ≡ 1 (mod n)

证毕!

费马小定理

由欧拉定理可以马上推得费马定理。

当n为质数时,Φ(n) = n – 1

因此当p是质数时,ap-1≡ 1 (mod p) 即费马定理!



四、快速幂

算法核心:将b用二进制表示,然后对每一位进行01判断,进而累乘即可。

求 m^k mod p,时间复杂度 O(logk)。

int qmi(int m, int k, int p)
{
    int res = 1 % p, t = m;
    while (k)
    {
        if (k&1) res = res * t % p;
        t = t * t % p;
        k >>= 1;
    }
    return res;
}


快速幂求逆元


题目:给定n组ai , pi,其中pi是质数,求ai模pi的乘法逆元,若逆元不存在则输出impossible。

逆元定义:若整数b,p互质,并且对于任意的整数 a,如果满足b|a=0,则存在一个整数x,使得a/b≡a∗x(mod p),则称x为b的模p乘法逆元,记为b−1(mod p)。

接下来进行推导:

若:a/b≡a∗x(mod p)

两边同除a,得:1/b ≡ x (mod p)

移项得:x*b ≡ 1 (mod p) ,即x满足该式子即可!

由费马小定理得:若p是质数,且b不是p的倍数, 则bp-1 ≡ 1 (mod p)

因此x = b p-2 就是解。

当b%p = 0时 ,x*b ≡ 1 (mod p)显然无解

//代码中的a是上面文字中的b
#include <iostream>

using namespace std;

typedef long long ll;
//求a的b次方
ll qmi(ll a , ll b , ll p)
{
    ll ans = 1;
    while(b)
    {
        if(b & 1) ans = ans * a % p;
        b >>= 1;
        a = a * a % p;
    }
    
    return ans;
}

int main()
{
    int n;
    cin >> n;
    while(n--)
    {
        ll a , p;
        cin >> a >> p;
        ll t = qmi(a , p-2 , p);
        if(a % p) cout << t << endl;
        else puts("impossible");
    }
    return 0;
}



欧几里得算法

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}
求欧拉函数
int phi(int 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);

    return res;
}



五、扩展欧几里得算法

题目:给定n对正整数ai , bi,对于每对数,求出一组xi,yi,使其满足ai∗xi+bi∗yi=gcd(ai,bi)。

裴蜀定理:若a,b是整数,且gcd(a,b)=d,那么对于任意的整数x,y,ax+by都一定是d的倍数,特别地,一定存在整数x,y,使ax+by=d成立。

显然因为d|a , d|b , 则d|ax+by。

有一个直接的应用就是 如果ax+by=1有解,那么gcd(a,b)=1;

根据裴蜀定理,我们知道ai∗xi+bi∗yi=gcd(ai,bi)一定存在解,要是想求出x,y就得用到扩展欧几里得算法。

需要找出递归关系

求解方程 ax+by=gcd(a,b)

当 b=0 时, ax+by=a 故而 x=1,y=0。

当b != 0 时,因为gcd(a,b)=gcd(b,a%b)

而gcd(a,b) = xa+yb , gcd(b,a%b) = yb+x(a%b) . 其中 a%b = a – (a/b)b

所以 gcd(b , a%b) = yb+x(a – (a/b)b) , 化简得:gcd(b , a%b) = xa+(y-(a/b)x)b

推得:xa+yb = xa+(y-(a/b)x)b

根据恒等定理得:y = (y-(a/b) * x) 这就是递归关系。

#include <bits/stdc++.h>

using namespace std;

void exgcd(int a , int b , int &x , int &y)
{
    if(!b)//终止条件
    {
        x = 1 , y = 0;
        return;
    }
    exgcd(b , a % b , y , x);//这里将y写前面,x写后面可以简化代码,否则要进行交换
    y -= a / b * x;
    /*这是不交换x,y的写法
	exgcd(b, a%b, x, y);
    int t = y;
    y = x - (a/b) * y;
    x = t;
   */
    return ;
}


int main()
{
    int n;
    cin >> n;
    
    while(n--)
    {
        int a , b , x , y;
        cin >> a >> b;
        exgcd(a , b , x , y);
        printf("%d %d\n" , x , y);
    }
    return 0;
}

模板:

// 求x, y,使得ax + by = gcd(a, b)
int exgcd(int a, int b, int &x, int &y)
{
    if (!b)
    {
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y -= (a/b) * x;
    return d;
}

通解 = 特解 + 齐次解

而齐次解即为方程 ax+by=0的解

通解为 x=x′+k∗b/d , y=y′−k∗a/d ,k∈z

利用扩展欧几里德算法求解线性同余方程

题目:给定n组数据ai,bi,mi,对于每组数求出一个xi,使其满足ai∗xi≡bi(mod mi),如果无解则输出impossible。

算法核心:将原方程进行等价代换:

1.ax≡b (mod m) 等价于 ax+my≡b (mod m)

2.即 (ax+my)%m=b

3.只要利用扩展欧几里德算法求出gcd(a,m),如果b是gcd(a,m)的倍数,那么显然可以通过乘积的方式,来使ax+my等于b,否则返回impossible。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int exgcd(int a, int b , int &x , int &y )
{
    if(!b)
    {
        x = 1 , y = 0;
        return a;
    }
    int d = exgcd(b , a % b , y , x);
    y -= a / b * x;
    return d;
}

int main()
{
    int n;
    cin >> n;
    
    while(n--)
    {
        int a , b , m , x , y;
        cin >> a >> b >> m;
        int d = exgcd(a , m , x , y);//求gcd(a,m)
        if(b % d) puts("impossible");
        else cout << (ll) x * b / d % m << endl;//如果b是最大公约数d的倍数,则可以通过乘上一个b/d来使求得的结果由d变成b
    }
    return 0;
}


六、中国剩余定理


描述:给定 2n 个整数a1,a2,…,an和m1,m2,…,mn,求一个最小的非负整数 x,满足∀i∈[1,n],x≡mi(mod ai)。

思路:

合并:将第一条和第二条合并,再合并进第三条……一直合并n-1次,就只剩一条了。

具体实现:1.由题意得x % a1 = m1 , x % a2 = m2,

2.即x = k1a1 + m1 , x = k2a2+m2,

3.取出两式等号右边得:k1a1 + m1 = k2a2+m2

4.移项得:k1a1-k2a2 = m2-m1.( * )

5.接下来利用扩展欧几里得算法求得k1的解和最大公约数d,判断d是否能整除m2-m1,若不行说明无解,否则继续

6.再将k1自乘上一个(m2-m1)/d才是( * )的一个解,可以知道k1的通解是k1+ka2/d,k2的通解是k2+ka1/d

7.为了防止数据溢出,我们需要求得k1的最小正整数解,在c++中,取模会得到负值,因此我们需要以下操作

8.令t=a2/d,k1 = (k1 % t + t) % t

9.将k1= k1+ka2/d,代回x = k1a1 + m1中得:x = k1a1+m1+ka1a2/d

10.发现此时x的表达式和原来的很像,一部分常量,一部分变量,所以我们重新赋值 , m1 = k1a1+m1,a1= abs(a1 / d * a2)

11.这样一来原来的两条式子就合并为一条x=k1a1+m1(此时a1和m1已经变了),继续合并即可。

12.最后出m1就是解,在10中可以发现m1=k1a1+m1就是x的值,不过在输出也要对其a1取模以输出最小的非负整数解。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

ll exgcd(ll a , ll b , ll &x , ll &y)
{
    if(!b)
    {
        x = 1 , y = 0;
        return a;
    }
    int d = exgcd(b , a % b , y , x);
    y -= a / b * x;
    return d;
}

int main()
{
    int n;
    cin >> n;
    
    ll a1 , m1;
    cin >> a1 >> m1;
    
    bool flag = true;
    for(int i = 0 ; i < n - 1 ; i++)
    {
        ll a2 , m2;
        cin >> a2 >> m2;
        ll k1 , k2;
        ll d = exgcd(a1 , a2 , k1 , k2);
        
        if((m2 - m1) % d)
        {
            flag = false;
            break;
        }
        
        k1 *= (m2 - m1) / d;//此时求出来的k1只是众多k1的解中的一个而已
        ll t = a2 / d;//防止溢出,要求得k1的最小正整数解,易得k1+k*(a2/d)是k1的通解,因此使其对t 取模
        k1 = (k1 % t + t) % t;//在c++中,取模的值可得负数,在这样的操作之后会变成同等意义的正数
        
        m1 = a1 * k1 + m1;
        a1 = abs(a1 / d * a2);
    }
    
    if(flag) cout << (m1 % a1 + a1) % a1 << endl;
    else puts("-1");
    return 0;
}



七、高斯消元


描述:解一个包含n个方程n个未知数的线性方程组

在这里插入图片描述


算法流程:对每一列的系数进行如下操作

1.找到一列中系数绝对值最大的一条方程(不考虑已经移动过的方程)

2.将其移到最上方(同样不考虑移动过的方程)

3.将该系数变为1

4.将下面的方程同一列的系数消为0

5.得到一个倒三角形方程组,即可求出解


得出三种情况:

①完美在倒三角i选哪个 唯一解

②0 = 非0 无解

③0 = 0 无穷解

// a[N][N]是增广矩阵
int gauss()
{
    int c, r;
    for (c = 0, r = 0; 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];      // 将当前行的首位变成1
        for (int i = r + 1; i < n; i ++ )       // 用当前行将下面所有的列消成0
            if (fabs(a[i][c]) > eps)
                for (int j = n; j >= c; j -- )
                    a[i][j] -= a[r][j] * a[i][c];

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)
                return 2; // 无解
        return 1; // 有无穷多组解
    }

    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; // 有唯一解
}




八、递归法求组合数

1.题目:给定n组询问,每组询问给定两个整数a,b,请你输出Cba mod (109+7)的值。1≤n≤10000,1≤b≤a≤2000. O(N2)

数据较小,共有2000*2000=4e6中形式,直接暴力,先预处理每一种形式的值,再查询即可。

利用这条关系式:Cba = Cba-1 + Cb-1a-1,打个比方:Cba就是从a个苹果中拿b个,所有的拿法可以分成包括某个特殊的苹果,和不包括某个特殊的苹果,包括的情况则Cb-1a-1,不包括则是Cba-1,加起来就是Cba。

// c[a][b] 表示从a个苹果中选b个的方案数
for (int i = 0; i < N; i ++ )
    for (int j = 0; j <= i; j ++ )
        if (!j) c[i][j] = 1;
        else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;



通过预处理逆元的方式求组合数

首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]

如果取模的数是质数,可以用费马小定理求逆元

#include <bits/stdc++.h>

using namespace std;

const int N = 2010;
const int P = 1e9 + 7;
int t[N][N];

int main()
{
  
    
    for(int i = 0 ; i < N ; i++)
        for(int j = 0 ; j <= i ; j++)
        {
            if(!j) t[j][i] = 1;
            else t[j][i] = (t[j][i - 1] + t[j - 1][i - 1]) % P;//递推式
        }
        
    int n;
    cin >> n;
    
    while(n--)
    {
        int a, b;
        cin >> a >> b;
        cout << t[b][a] << endl;
    }
    return 0;
}

2题目:1≤n≤10000, 1≤b≤a≤105 (NlonN)

1e5 * 1e5 = 1e10,此时情况就太多了,就不能用之前的预处理了。

还知道Cba = a!/(b!*(a-b)!),所以我们可以预处理每一个数的阶乘先,然后在查询即可。

因为a/b mod p != (a mod p)/(b mod p),所以我们要转换成乘法,就要用到逆元。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int N = 100010;
const int P = 1e9 + 7;

int fact[N] , infact[N];//infact[i]是fact[i]的逆元,因为P是质数,所以可以用费马小定理,快速幂求逆元。

int qmi(int a, int b , int p)//求
{
    int res = 1;
    while(b)
    {
        if(b & 1) res = (ll)res * a % p;
        b >>= 1;
        a = (ll)a * a % p;
    }
    return res;
}

int main()
{
    fact[0] = infact[0] = 1;
    for(int i = 1 ; i < N ; i++)
    {
        fact[i] = (ll)i * fact[i - 1] % P;
        infact[i] = (ll)qmi(i , P - 2 , P) * infact[i - 1] % P;
    }
    
    int n;
    cin >> n;
    while(n--)
    {
        int a , b;
        cin >> a >> b;
        cout << (ll)fact[a] * infact[b] % P * infact[a - b] % P << endl;
    }
    return 0;
}

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;
}

// 预处理阶乘的余数和阶乘逆元的余数
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ )
{
    fact[i] = (LL)fact[i - 1] * i % mod;
    infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}

3.题目:给定n组询问,每组询问给定三个整数a,b,p,其中p是质数,请你输出Cba mod p的值。1≤n≤20,1≤b≤a≤1018,1≤p≤105。

这里a和b的数据很大,要用到Lucas(鲁卡斯定理)

在这里插入图片描述

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

int p;

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

int C(int a , int b)
{
    if(a < b) return 0;
    
    int res = 1;
    for(int i = b , j = a ; i ; i-- , j--)//利用定义求出C
    {
        res = (LL) res * j % p;
        res = (LL) res * qmi(i , p - 2) % p;//利用逆元求
    }
    
    return res;
}

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

int main()
{
    int n;
    cin >> n;
    while(n--)
    {
        LL a , b;
        cin >> a >> b >> p;
        cout << lucas(a , b) << endl;
    }
    return 0;
}

4.题目:输入a,b,求Cba的值,1≤b≤a≤5000。

注意结果可能很大,需要使用高精度计算。

算法思路:

1.分解质因数,将Cba=a!/(b!*(a-b)!)分解质因数 p1k1 *p2k2 *p3k3 …pnkn ,求出最终每一个质因数的次数

2.高精度乘法

求a!中p的次数O(logp):

在这里插入图片描述

#include <bits/stdc++.h>

using namespace std;

const int N = 5010;

int primes[N];
int cnt;
int sum[N];
bool st[N];

int get(int n , int p)//返回n的阶乘中 ,p的次数
{
    int s = 0;
    while(n)
    {
        s += n / p;
        n /= p; 
    }
    return s;
}

vector<int> mul(vector<int> a , int b)//高精度乘法
{
    vector<int> ans;
    int t = 0;
    for(int i = 0 ; i < a.size() || t ; i++)
    {
        if(i < a.size())    t += a[i] * b;
        ans.push_back(t % 10);
        t /= 10;
    }
    return ans;
}

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;
       }
    }
}

int main()
{
    int a, b;
    cin >> a >> b;
    
    get_primes(a);
    
    for(int i = 0 ; i < cnt ; i++)//求出最终每一个p的次数
       {
           int p = primes[i]; 
           sum[i] = get(a , p) - get(b , p) - get(a - b , p);
       }
       
    vector<int> res;
    res.push_back(1);
    
    for(int i = 0 ; i < cnt ; i++)//累乘
        for(int j = 0 ; j < sum[i] ; j++)
            res = mul(res , primes[i]);
    
    for(int i = res.size() - 1 ; i >= 0 ; i--)  cout << res[i];
    cout << endl;
    return 0;
}


Lucas定理

若p是质数,则对于任意整数 1 <= m <= n,有:
    C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)

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

int C(int a, int b, int p)  // 通过定理求组合数C(a, b)
{
    if (a < b) return 0;

    LL x = 1, y = 1;  // x是分子,y是分母
    for (int i = a, j = 1; j <= b; i --, j ++ )
    {
        x = (LL)x * i % p;
        y = (LL) y * j % p;
    }

    return x * (LL)qmi(y, p - 2, p) % p;
}

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


九、容斥原理

集合数为奇数——正好

偶数——负号

判断集合个数

在本题中,判断1-n中包含质数p1的个数为 n / p1,向下取整。

同时有可能存在同时是p1 和p2 的倍数,出现重复相加的情况,要减去,于是 n / (p1*p2)。

以此类推,每次要求的就是包含不同集合的情况下的个数。

程序设计思路

由于可能项较多,我们枚举所有可能项——一般用位运算来做

这样可以对应所有的选法,

从1- 2的n次方-1,将他们转换成二进制表示,0表示不选,1表示选,这样可以包含其中的所有情

判断第k位是不是1, i >> k & 1


题目:

给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。

请你求出 1∼n 中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。

模板:

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int N = 20;

int p[N];


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

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

    int res = 0;
    for (int i = 1; i < 1 << m; i ++ )
    {
        int t = 1, s = 0;
        for (int j = 0; j < m; j ++ )
            if (i >> j & 1)
            {
                if ((LL)t * p[j] > n)
                {
                    t = -1;
                    break;
                }
                t *= p[j];
                s ++ ;
            }

        if (t != -1)
        {
            if (s % 2) res += n / t;
            else res -= n / t;
        }
    }

    cout << res << endl;

    return 0;
}



容斥原理一般用来包含这多个集合的求个数

需要

1、集合构造

2、集合个数求解

3、对于重合部分的集合个数求解

这三步是看题目自己分析



分解质因数法求组合数

当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:

1. 筛法求出范围内的所有质数

2. 通过 C(a, b) = a! / b! / (a – b)! 这个公式求出每个质因子的次数。 n! 中p的次数是 n / p + n / p^2 + n / p^3 + …

3. 用高精度乘法将所有质因子相乘


int primes[N], cnt;     // 存储所有质数

int sum[N];     // 存储每个质数的次数

bool st[N];     // 存储每个数是否已被筛掉

void get_primes(int 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;
            if (i % primes[j] == 0) break;
        }
    }
}


int get(int n, int p)       // 求n!中的次数
{
    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;
}

get_primes(a);  // 预处理范围内的所有质数

for (int i = 0; i < cnt; i ++ )     // 求每个质因数的次数
{
    int p = primes[i];
    sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}

vector<int> res;
res.push_back(1);

for (int i = 0; i < cnt; i ++ )     // 用高精度乘法将所有质因子相乘
    for (int j = 0; j < sum[i]; j ++ )
        res = mul(res, primes[i]);

卡特兰数( Cn2n / (n+1))


给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为: Cat(n) = C(2n, n) / (n + 1)


举个例子:n=6时,就可以画成上图,假设向右是0向上是1,则在红线以下的路径是合法的,可以看出每一条从(0,0)走到(6,6)的非法路径做关于红线的对称,都对应一条(0,0)-(5,7)的路径;反之,每一条从(0,0)-(5,7)的路径都对应一条从(0,0)-(6,6)的非法路径,那么就可以利用(0,0)-(5,7)的路径数间接求出(0,0)-(6,6)的非法路径数。


算法核心:每一条从(0,0)走到(n,n)的非法路径都对应一条从(0,0)走到(n-1,n+1)的非法路径,因此合法路径就是

因此从(0,0)走到(6,6)的不合法路径数就是Cn-12n,即合法的是Cn2n-Cn-12n ,化简得 Cn2n / (n+1),

所以直接从定义出发求出Cn2n ,其中因为模上一个质数,可以用快速幂求逆元。

#include <bits/stdc++.h>

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 ans = 1;
    
    for(int i = 2 * n ; i > n ; i--)    ans =  (LL) ans * i % mod;
    
    for(int i = n ; i >= 1 ; i--) ans = (LL) ans * qmi(i , mod - 2 , mod) % mod;
    
    ans = (LL) ans * qmi(n + 1 , mod - 2 , mod) % mod;
    
    cout << ans << endl;
    
    return 0;
}


十、博弈论


先手必胜和先手必败状态

定义P-position和N-position,其中P代表Previous,N代表Next。直观的说,上一次move的人有必胜策略的局面是P-position,也就是“后手可保证必胜”或者“先手必败”,现在轮到move的人有必胜策略的局面是N-position,也就是“先手可保证必胜”。更严谨的定义是:1.无法进行任何移动的局面(也就是terminal position)是P-position;2.可以移动到P-position的局面是N-position;3.所有移动都导致N-position的局面是P-position。


先手必胜状态(N-position)—— 可以走到某一个必败状态(存在我走的某一步,使得对方是必败状态)

先手必败状态(P-position)—— 走不到任何一个必败状态(我怎么走对方都是赢,对方走不到任何一个必败状态)



NIM游戏 —


给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。


我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。

所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。

NIM博弈不存在平局,只有先手必胜和先手必败两种情况。


定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0


题目


现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 ai 个石子(i≥1)。


两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。


已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。


问如果两人都采用最优策略,先手是否必胜。


模板

#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

int main()
{
    int n;
    scanf("%d", &n);

    int res = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        scanf("%d", &x);
        if (i & 1) res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}



公平组合游戏ICG


若一个游戏满足:


由两名玩家交替行动;

在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;

不能行动的玩家判负;

则称该游戏为一个公平组合游戏。

NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。


有向图游戏

给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。

任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。


Mex运算

设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:

mex(S) = min{x}, x属于自然数,且x不属于S


SG函数

在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:

SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})

特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。


有向图游戏的和 —— 模板题 AcWing 893. 集合-Nim游戏

设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。

有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:

SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)


定理

有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。

有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。


使用Nim模型去求解问题的关键

在于简单判断当前是否为先手必胜状态还是先手必败状态

如果是先手必胜状态则,找到必胜策略。



版权声明:本文为weixin_64393298原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。