数学基础

质数

定义:对于从2开始的的整数如果只包含1和本身这两个约数,就被称为质数/素数

质数的判定模板:根据定义(试除法)判断质数 O(sqrt(n))

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )//不推荐sqrt,因为每次都要算,不推荐i*i,因为可能溢出成负数。
        if (x % i == 0)
            return false;
    return true;
}

分解质因数模板:试除法 O(sqrt(n))

void divide(int x)
{
    for (int i = 2; i <= x / i; i ++ )//优化:x中最多包含一个大于sqrt(x)的质因子
        if (x % i == 0)
        {
            int s = 0;
            while (x % i == 0) x /= i, s ++ ;//把x的质因子除干净,所以对x的除法是直接永久改变x在这个函数里的值
            cout << i << ' ' << s << endl;
        }
    if (x > 1) cout << x << ' ' << 1 << endl;//如果还剩一个数字,那么这个数字一定大于sqrt(n),而且是它的质因数且只有一个。
    cout << endl;
}
/*推理过程:
x被除i之后,x的所有质数仍然是初始x的所有质数
x可以整除初始x
while循环结束后,当前的i不可能再能整除x,i已不是剩余x的质数
x是质数,即在sqrt(x)的范围内找不到i能整除x
x是初始x的质因数
初始x大于sqrt(x)的质因数最多只有一个
x是初始x质因数,且为1次方
*/

阶乘的质因数分解

n! 中质数p的次数是 [n / p] + [n / p^2] + [n / p^3] + ...

原理:1~a中p的倍数+p^2的倍数+...得到p所有的倍数之和,也就是质因数分解后p是多少次方。详见《算法竞赛进阶指南》P138。

思路:线性筛法得一张素数表,素数表的范围要够得上n,然后从素数表里一个一个取质数p,每个p的次方数就是 [n / p] + [n / p^2] + [n / p^3] + ...

具体实现见”求组合数IV“,其中用到了相关思想。

朴素筛法求质数 O(nloglogn) (埃氏筛:只要现在的i不是之前数字的倍数,那么i就是质数,同时也只要筛去质数的倍数即可,这里面已经包含了非质数的质数)

int primes[N], cnt;     // primes[]存储所有素数,cnt就是count计数
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;
    }
}

线性筛法求质数

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 ++ )//从质数表primes第0位开始,找到第一个大于n/i的质数之前的所有质数
        {
            st[primes[j] * i] = true;//那么这个质数乘i便筛掉了一个数字
            if (i % primes[j] == 0) break;//到了i的最小质因子,就停下来
        }
    }
}
/*
n只会被最小质因子找到并筛去
1. i % primes[j] == 0说明prime[j]一定是i的最小质因子,同时也是primes[j]*i的最小质因子
2. i % primes[j] != 0说明prime[j]一定小于i的所有质因子,同时也是primes[j]*i的最小质因子
对于任意一个合数x,假设primes[j]是x的最小质因子,当i枚举到x/primes[j]的时候一定存在j,在for(j)循环的过程中可以把x筛掉
没有必要让第二层for循环增加一个条件j<cnt
*/

判断两个数是否互质

看他们是否是倍数关系,即假设a大的情况下a%b是否等于0,a或b中有1除外,1一定是任何一个正整数的倍数,同时又与他们互质。

约数

试除法求所有约数

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

约数个数和、约数之和:通过质数个数来反映约数

如果 \(N = p1^c1 * p2^c2 * ... *pk^ck (质因数分解)\)
约数个数:
\( (c1 + 1) * (c2 + 1) * ... * (ck + 1) \)
原理:约数一定是p1^b1 * p2^b2 * ... *pk^bk,而bi一定在0~ci之间,即bi的取值有ci+1种,那总共的取值个数(约数)就是刚刚的公式

约数之和:
\( (p_1^0 + p_1^1 + ... + p_1^c1) * ... * (p_k^0 + p_k^1 + ... + p_k^ck) \)
原理:展开后的每一个单项,都是N的约数 实际计算利用
\( p_1^0 + p_1^1 + ... + p_1^c1=1+p_1✖(1+p_1✖(...)) \)
\(t = (t * a + 1)\)不断循环ci次得到k项里的每一项,然后相乘得到约数之和

应用范例

#include <iostream>
#include <algorithm>
#include <unordered_map>

using namespace std;

typedef long long LL;

const int mod = 1e9 + 7;

int main()
{
    int n;
    scanf("%d",&n);
    unordered_map<int,int> primes;
    while(n--)
    {
        int x;
        scanf("%d",&x);
        for(int i=2;i<=x/i;i++)
        {
            while(x%i==0)
            {
                primes[i]++;
                x/=i;
            }
        }
        if(x>1) primes[x]++;
    }
    
    LL res=1;
    for(auto p:primes)
    {
        LL a=p.first,b=p.second;
        LL t=1;
        while(b--)
        {
            t = (t * a + 1) % mod;
        }
        res = res * t % mod;
    }
    
    cout<<res;
    
    return 0;
}

求最大公约数:欧几里得算法/辗转相除法

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

原理:数对(a,b)与数对(b,b%a)的最大公约数数量大小完全一样。证明,可以将假设左边数对一个公约数d,能推出来d一定是右边数对的公约数,反之亦然。

欧拉函数

定义

\[\phi(n)=1到n中与n互质的数的个数 \]

互质:若N个整数的最大公因数是1,则称这N个整数互质。

计算公式

将n分解质因数:

\[n=p_1^{\alpha1}p_2^{\alpha2}...p_k^{\alpha k} \]

则有:

\[\phi(n)=n(1-1/p_1)(1-1/p_k)...(1-1/p_k) \]

或:(注意先除后乘防止数据溢出)

\[\phi(n)=n\frac{p_1-1}{p_1}\frac{p_2-1}{p_2}...\frac{p_k-1}{p_k} \]

计算公式原理:容斥原理:n减去所有n/pk,n/pk意味着比n小的pk的倍数的数量,这些数字都与n有同一个公约数pk,故一定不是互质的数,数字总数n要减去这些数字,同时再把某些既是pk1倍数又是pk2倍数的公共部分加回来,因为前一步有重复计算,然后再把减去重复的补,再去重,最后因式分解。

\[\phi(n)=n+(-n/p_1-n/p_2...-n/p_k)+(n/p_1p_2+n/p_1p_3...)+(-n/p_1p_2p_3-... \]

公式法求欧拉函数:求n的欧拉函数 O(sqrt(n))

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

筛法求欧拉函数:求1~n中每一个数的欧拉函数 O(n)模仿线性筛法

int primes[N], cnt;     // primes[]存储所有素数
int euler[N];           // 存储每个数的欧拉函数
bool st[N];         // st[x]存储x是否被筛掉


void get_eulers(int n)//如果要储存/返回1~n欧拉函数结果之和应用long long
{
    euler[1] = 1;//初始化第一种情况euler[1]
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i])//当i是质数时
        {
            primes[cnt ++ ] = i;
            euler[i] = i - 1;//当i是质数时,1~i-1都与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)//primes[j]是i的质因子时
            /*
            primes[j]*i与i都进行质因数分解后,区别无非就是i的质数primes[j]上次方-1,根据欧拉公式,得出euler[t] = euler[i] * primes[j];
            */
            {
                euler[t] = euler[i] * primes[j];//具体解释见公式1
                break;
            }
            //primes[j]不是i的质因子时
            /*
            primes[j]*i对于i而言,不仅拥有i的所有质数,同时还多一个质数primes[j]
            */
            euler[t] = euler[i] * (primes[j] - 1);//具体解释见公式2
        }
    }
}

公式1:(其中primes[j]缩写为pj)

\[\phi(i)=i(1-1/p_1)(1-1/p_k)...(1-1/p_k) \]

由于primes[j]*i与i的质数pk完全相同

\[\phi(p_ji)=p_ji(1-1/p_1)(1-1/p_k)...(1-1/p_k)=p_j\phi(i) \]

故euler[t] = euler[i] * primes[j];

公式2:(其中primes[j]缩写为pj)

\[\phi(i)=i(1-1/p_1)(1-1/p_k)...(1-1/p_k) \]

由于primes[j]*i对于i而言,不仅拥有i的所有质数,同时还多一个质数primes[j]

\[\phi(pj*i)=pj*i(1-1/p_1)(1-1/p_k)...(1-1/p_k)*(1-1/pj)=(pj-1)\phi(i) \]

故euler[t] = euler[i] * (primes[j] - 1);

欧拉定理

定理内容:

跟若a与n互质,则有:

\[a^{\phi(n)}\equiv1(modn) \]

证明:

1~n中与n互质的数一共有 φ(n)个,设它们为

\[a_1,a_2,...,a_{\phi(n)}① \]

由于a与n也互质,故

\[aa_1,aa_2,...,aa_{\phi(n)}② \]

中每一个数字也与n互质

所以②每个数字对n取余的结果与①的数字完全相同,只是数字顺序可能不同,因为①包含了1~n所有与n互质的数,而②内所有数字取余后互不相等(反证法)且与n互质,那么②中每个数字都能对应上①中的一个数字。

故此时再取①中每一项相乘,②中每一项取余相乘,再两边同时取余可得同余,即

\[a_1a_2...a_{\phi(n)}\equiv aa_1aa_2...aa_{\phi(n)}(modn) \]

化简即

\[a^{\phi(n)}\equiv1(modn) \]

费马定理(可看为欧拉定理特殊情况)

再欧拉定理的前提下,特别地,当n为质数时,1n中1n-1全部都与n互质,即φ(n)=n-1,此时有:

\[a^{n-1}\equiv1(modn) \]

快速幂

模板:求 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;
}

乘法逆元

若整数b,m互质,并且对于任意的整数a,如果满足b|a(b能整除a),则存在一个整数x,使得

\[a/b\equiv a*x(mod m) \]

化简即(两遍同时乘b、除以a):

\[b*x\equiv 1(mod m) \]

则称x为b的模m乘法逆元为:(这是标记而不是运算符号,也就是x不是数学运算来的,而是我们就是这么记/写)

\[x=b^{-1}(modm) \]

注:

通解:

b与m互质时,根据欧拉定理

\[b^{\phi(m)}\equiv1(mod m) \]

\[b*b^{\phi(m)-1}\equiv1(mod m) \]

得b模m乘法逆元

\[x=b^{\phi(m)-1} \]

特殊情况:且当模数m为质数

\[x=b^{m-2} \]

即为b的乘法逆元

证明:由费马定理可知,若b与m互质,且m为质数时,有

\[\phi(m)=m-1 \]

\[x=b^{\phi(m)-1}=b^{m-2} \]

--->注意:若b与m不互质,即b与m是倍数关系,有b%m==0,x一般也会等于0(m=2除外,此时m-2=0,0次方特殊),这个特点可说明乘法逆元x不存在

递归求乘法逆元:详见求组合数2:推导1

扩展欧几里得算法:求二元一次方程整数解

裴蜀定理/贝祖定理

定理内容

对于gcd(a,b)=d,对于任意的两个整数x,y,ax+by都一定是d的倍数,特别地,一定存在整数x,y,使ax+by=d成立。换句话说,只要d不是gcd(a,b)的倍数,就无解

推论

a,b互质的充分必要条件是存在整数x,y使ax+by=1

模板

算法目的:给定a,b,求整数x,y,使得ax + by = gcd(a, b),同时返回最大公约数

// 求一组解x, y,使得ax + by = gcd(a, b),解可能不唯一。
int exgcd(int a, int b, int &x, int &y)
{
    if (b==0)//若b等于0,也就是求ax+0y=gcd(a,0)=a的解,即x=1,y=0;
    {
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);//递归求解,a与b位置互换了,x与y互换
    //此时存在关系by+(a mod b)x=d,即by+{a-[a/b]b}x=d
    //即b(y-[a/b]x)+ax=d,此时构造了一个新的y,即y-[a/b]x
    //注:[]为向下取整符号
    y -= (a/b) * x;
    return d;//返回值是最大公约数
}

通解:由已知解\(x_0,y_0\),易得等式:

\[a(x_0-\frac{b}{d}k)+b(y_0+\frac{a}{d}k)=d,k\in Z \]

通解即:

\[\left\{ \begin{matrix} x=x_0-\frac{b}{d}k\\y=y_0+\frac{a}{d}k \end{matrix} \right.  k\in Z \]

线性同余方程

解关于x的方程(解不唯一):

\[ax\equiv b(mod m) \]

思想:等式变形为ax=b+m的倍数,设m的倍数是-y,即ax=b-my,有ax+my=b,那么先解 ax+my=gcd(a,m) 故有解的条件是b为gcd(a,m)的倍数,如果不是则无解,若是则化归为扩展欧几里得算法。

注:若解出来的值过大可以mod m。

注:题目的取模是一般意义的,但负数取模各个语言不一样

中国剩余定理

内容:给定k个两两互质的数字m1,m2,...,mk,解关于x的方程组

\[\left\{ \begin{matrix} x\equiv a_1(mod m_1)\\x\equiv a_2(mod m_2)\\.\\.\\.\\x\equiv a_k(mod m_k) \end{matrix} \right. \]

\[M=m_1m_2...m_k\\M_i=\frac{M}{m_i} i\in [1,k] \]

记Mi^-1为Mi模mi的逆元,写作x

\[M_i*x\equiv 1(mod m_i) \]

其中x

\[x=M_i^{-1}(mod m_i)\\M_i^{-1}=M_i^{\phi(m_i)-1} \]

则解为

\[x=a_1M_1M_1^{-1}+a_2M_2M_2^{-1}+...+a_kM_kM_k^{-1} \]

解的正确性很好验证,以方程组第一个等式为例,由M1M1^-1的性质可得,取模m1后为1,第一项剩a1,此后的每一项,由于Mi里都含有m1,故每一项都可整除,取模后都等于0,故有 x = a1 (mod m1) 。

不满足两两互质时:数学归纳法总结规律。

//以acwing 204题为例
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

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

int main()
{
    int n;
    cin>>n;
    
    bool flag=true;
    LL a1,m1;
    cin>>a1>>m1;
    
    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 = k1 * (m2-m1)/d;//另一个系数k2不需要就不记了
        LL t = a2/d;//通解=k1+k*t,k属于Z
        k1 = (k1%t+t)%t;//通过通解找到最小特解,根据c++取模方法这样取到最小非负的k1+k*t,k属于Z
        
        m1 = k1*a1+m1;
        a1 = abs(a1*a2/d);
    }
    
    if(flag)
    {
        cout<<(m1%a1+a1)%a1;
    }
    else puts("-1");
    
    return 0;
}

高斯消元:解n元线性方程组 O(n^3)

原理

线性代数,经过初等行列变换,高斯消元,如果能化成完美阶梯型,就有唯一解,如果本来应该是0的位置不是0,那就无解,如果有一行系数全是0,就无穷多个解

过程

枚举每一列,假设现在在处理第c列:

找到绝对值最大的一行

将该行换到最上面(但在已经固定的行下面)

将该行第一个数约成1

利用该行将该行下面所有第c列消成0

模板

// a[N][N]是增广矩阵,一般double型,同时0不一定是0,而是一个足够小的数eps = 1e-6
//最后答案就是这个增广矩阵的最后一列
//函数返回值表示解的情况
int gauss()
{
    int c, r;//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;//如果这一列全都是0,直接看下一列。

        for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]);         // 将绝对值最大的行换到最顶端(最顶端的第r行与第t行所有元素交换)
        for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];             // 将当前行的首位变成1,倒着来,把a[r][c]最后变成1
        for (int i = r + 1; i < n; i ++ )// 用当前行将下面所有的列消成0
            if (fabs(a[i][c]) > eps)//如果不是0
                for (int j = n; j >= c; j -- )//也要倒着来,这行打头的数字代表了r行放大的倍数,所以要最后动它。
                    a[i][j] -= a[r][j] * a[i][c];

        r ++ ;//不走这个r++唯一的可能性就是走了上面的continue,这意味着行没有处理就走了下一个列,然后接着从这行开始,这样最后的阶梯矩阵从第r行(从0开始数,这里的r是这个大循环走完最后的r)开始,系数全都是0,这将给我们判断解的情况做铺垫。
    }

    if (r < n)//如果
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)//如果在系数全为0的方程里发现等号右边(即从0开始数的增广矩阵第n列)不为0,则无解
                return 2; // 无解
        //没有发现则意味着某些未知数无论去什么值都满足方程,因为他们的系数是0,所以有无穷多组解。
        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];
	/*上面这段循环的注释
	从最后一行开始,第i+1列开始(第i行第i列已经是1了,现在要把其他的消成0),给这行最后一个数字a[i][n],逐个减去a[i][j]倍的第j行最后一个数字a[j][n]
	我们是按照化为对角矩阵的思路来的,但系数矩阵最后不是对角矩阵,我们只改动了最后一列,在我们大脑里已经默认了处理过的行是0...010...0,不必真的体现到储存矩阵的数组上
	*/
    return 0; // 有唯一解
}

//主函数用for循环读入增广矩阵的时候别忘了列比行多一个,要+1.

高斯消元解异或线性方程组

形式:相比于普通的线性方程组,异或线性方程组把加减变成了异或符号,同时未知数的系数只为0或1,增广矩阵最右侧一列数字只为0或1.

异或可以理解为不进位加法,所以方程组是线性的,可以模仿常规高斯消元来写,只不过加减变成了异或。

1.消成上三角矩阵

1.1.枚举列

1.2.找非零行

1.3.交换

1.4.下面消0(通过异或计算)

2.判断解的情况

示例

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n,a[N][N];

int gauss()
{
    int c,r;
    for(c=0,r=0;c<n;c++)
    {
        int t;
        for(t=r;t<n;t++)
            if(a[t][c]==1) break;
        
        if(t==n) continue;
        
        for(int i=c;i<=n;i++) swap(a[t][i],a[r][i]);
        for(int i=r+1;i<n;i++)
            if(a[i][c])
                for(int j=c;j<=n;j++)
                    a[i][j] = a[i][j]^a[r][j];
                    
        r++;
    }
    
    if(r<n)
    {
        for(int i=r;i<n;i++)
            if(a[i][n])//等式左边全是0,如果最右侧不是0,则无解
                return 2;
        return 1;
    }
    
    for(int i=n-1;i>=0;i--)
        for(int j=i+1;j<=n;j++)
            if(a[i][j]) a[i][n]^=a[j][n];
    
    return 0;
}

int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n+1;j++)
            cin>>a[i][j];
            
    int t = gauss();
    
    if(t==0)
    {
        for(int i=0;i<n;i++)
            printf("%d\n",a[i][n]);
    }
    else if(t==1) puts("Multiple sets of solutions");
    else puts("No solution");
    return 0;
}

求组合数

关键是根据数据范围选择求组合数的方法

基本公式

\[C_a^b=\frac{A_a^b}{b!}=\frac{a*(a-1)*...*(a-b+1)}{b!}=\frac{a!}{b!(a-b)!} \]

基本模板

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;//xmodp/ymodp并不等于(x/y)modp,但等于xmodp*y乘法逆元modp
}

递归法求组合数

数据范围:10万次询问,1<=b<=a<=2000 O(n^2)

原理:递推式

\[C_a^b=C_{a-1}^b+C_{a-1}^{b-1} \]

推理过程(数学也可证):假设从a个苹果里面选b个 等于 给定一个苹果,

情况一:不选这个苹果,从a-1个里面选b个;

情况二:选这个苹果,从a-1个里面选b-1个。

两种情况之和即结果。

模板

// 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;//当b为0时组合数为1
        else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

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

数据范围:10万次询问,1<=b<=a<=1e5 O(nlogn)

原理:预处理好所有阶乘,fact[i]表示i的阶乘模p,然后利用公式计算

\[fact[i]=i! modp\\C_n^r=\frac{n!}{r!(n-r)!} \]

由于两个数取模相除不等于相除再取模

\[\frac{b modp}{a modp}\neq\frac{b}{a} modp \]

故利用逆元,infact[i]存储的是i的阶乘的逆元

\[infact[i]=(i!)^{-1} modp \]

逆元的性质:一个数字除以一个数字b,同余于乘上逆元

\[a/b\equiv a*x(mod p) \]

综上所述

\[C_n^r=\frac{n!}{r!(n-r)!}=fact[n]*infact[r]*infact[n-r] \]

模板

//首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
//如果取模的数是质数,可以用费马小定理求逆元,也就是b^(m-2),要用快速幂
//事实上如果模数p不是质数,那有的数字可能不存在乘法逆元,就不能用这个方法写,因为乘法逆元存在的条件是这个数字fact[i]与模数p互质。
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;//infact[0] = 1请这样理解:一个数除以fact[0](=1)等于乘1
for (int i = 1; i < N; i ++ )
{
    fact[i] = (LL)fact[i - 1] * i % mod;//n!=(n-1)!*n
    infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
    //这步解释看接下来的 推导1 ,infact[i]=qmi(fact[i],mod-2,mod);也对但计算量大。
}

推导1:p为质数,可以用费马小定理时:

\[infact[i]=(i!)^{-1}\\=(i!)^{p-2} (=qmi(fact[i],p-2,p))\\=[i*(i-1)!]^{p-2}         \\=[(i-1)!]^{p-2}*i^{p-2}       \\=infact[i-1]*qmi(i,p-2,p)  \]

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

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


int fact[N], infact[N];


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()
{
    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;
    }


    int n;
    scanf("%d", &n);
    while (n -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        printf("%d\n", (LL)fact[a] * infact[b] % mod * infact[a - b] % mod);
    }

    return 0;
}

Lucas定理求组合数

**数据范围:20次询问,1<=b<=a<=1e18,1<=p<=1e5 **

Lucas定理内容:(若p是质数,则对于任意整数 1 <= b <= a)

\[C_a^b\equiv C_{a mod p}^{b mod p}*C_{[a/p]}^{[b/p]} (mod p) \]

思路,启动lucas(),(写一个计算组合数的函数C),计算C(a%p,b%p),然后对于C([b/p],[a/p]),若b/p,a/p后仍然大于p,则递归lucas()继续处理C([b/p],[a/p]),直到小于p可以被C()计算。

Lucas定理证明:(在Lucas定理所要求条件成立时)

a、b可化为

\[a=a_0p^0+a_1p^1...+a_kp^k\\b=b_0p^0+b_1p^1...+b_kp^k \]

因为

\[(1+x)^p=C_p^0*x^0+C_p^1*x^1+...+C_p^p*x^p \]

由于C(p,k)的分子是p!,所以在没约掉分子p的情况下,模p一定等于0,故

\[(1+x)^p\equiv1+x^p (mod p) \]

此时

\[(1+x)^a=(1+x)^{a_0p^0+a_1p^1...+a_kp^k}\\ =[(1+x)^{p^0}]^{a_0}[(1+x)^{p^1}]^{a_1}...[(1+x)^{p^k}]^{a_k}\\ \equiv(1+x^{p^0})^{a_0}(1+x^{p^1})^{a_1}...(1+x^{p^k})^{a_k}(mod p) \]

对比两边x^b项系数(用二项式定理及p进制数性质可得)

\[C_a^b\equiv C_{a_k}^{b_k}C_{a_{k-1}}^{b_{k-1}}...C_{a_0}^{b_0}(mod p) \]

故得证

模板

//若p是质数,则对于任意整数 1 <= m <= n,有:
//C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)
//n/p与m/p可能仍然大于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;//xmodp/ymodp并不等于(x/y)modp,但等于xmodp*y乘法逆元modp
}

int lucas(LL a, LL b, int p)//LL别忘了
{
    if (a < p && b < p) return C(a, b, p);//在a,b都小于p的情况下直接算
    return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}

/*
主函数调用lucas(a,b,p),返回值即为C(a,b)%p;
*/

分解质因数法求组合数

数据范围:一次计算 1<=b<=a<=5000 a与b很大同时又不取模,要用到高精度乘法

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

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

  2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数(以p为例,即a!中p的次数减去b!与(a-b)!次数,即这个式子的质因子p次数)。 n! 中质数p的次数是 [n / p] + [n / p^2] + [n / p^3] + ...

    原理:1~a中p的倍数+p^2的倍数+...得到p所有的倍数之和,也就是质因数分解后p是多少次方。(利用“阶乘的质因数分解”)

  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);//res的初始值是1,然后逐个把每个质因子次方乘上去

for (int i = 0; i < cnt; i ++ )     // 用高精度乘法将所有质因子相乘
    for (int j = 0; j < sum[i]; j ++ )//sum[i]记录了primes[i]多少次方,快速幂可能会溢出,还是老老实实n次方就是乘n次计算。某个质数不是它的质因数也没关系,因为这个时候sum[i]=0会直接跳过。
        res = mul(res, primes[i]);//大整数乘上某个质因子

问题的方案数:卡特兰数

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

\[Cat(n)=C_{2n}^n-C_{2n}^{n-1}=\frac{C_{2n}^n}{n+1} \]

思路:做一张xy轴图像,x+1代表序列末尾添一个1,y+1代表序列末尾添一个0,要满足任意前缀中0的个数都不少于1的个数,那么这个xy图像一定处处y<=x,也就是在y=x+1这条线之下(或在y=x这条线之上或之下),那么满足条件的序列的个数=总个数-经过y=x+1不满足条件的路径的个数。

总个数=C(2n, n),这很显然,xy轴图像路径由2n次移动形成,其中选n次延x轴前进,那么C(2n, n)便是2n次中选择n次延x轴前进的组合方案总数,也就是总移动方案数。

经过y=x+1不满足条件的路径的个数=C(2n,n-1),由于x与y总数相等,所以最终一定停在y=x上,如果这条路径不满足条件(y<x+1),那么对从路径中不满足条件的点开始的线段做关于y=x+1轴对称,那终点必定落在(n-1,n+1),同时所有终点在(n-1,n+1)的路径都能做到对称后终点在(n,n)且对称后的路径不满足y<x+1要求,两个总数相同,所以”经过y=x+1不满足条件的路径“的个数为C(2n,n-1)。

容斥原理

\[|S_1\bigcup S_2\bigcup...\bigcup S_n|=\sum_{i}|S_i|-\sum_{i,j}|S_i\bigcap S_j|+\sum_{i,j,k}|S_i\bigcap S_j\bigcap S_k|-... \]

\[C_k^1-C_k^2+C_k^3-...+(-1)^{k-1}C_k^k=-[(1-1)^k-C_k^0]=1 \]

可以用二进制状态压缩来储存每个集合是否被选上,二进制数1的个数可以反应(-1)的多少次方。(比如一共有3个集合,S1∩S3表示为101)

由于没有顺序要求且是遍历,所以二进制状态压缩可以由循环完成比如是0001~1111就相当于是从1开始,然后小于1<<5

例.AcWing890 能被整除的数

给定一个整数 $n$ 和 $m$ 个不同的质数 $p_1, p_2, …, p_m$。

请你求出 $1 \sim n$ 中能被 $p_1, p_2, …, p_m$ 中的至少一个数整除的整数有多少个。

输入格式

第一行包含整数 $n$ 和 $m$。

第二行包含 $m$ 个质数。

输出格式

输出一个整数,表示满足条件的整数的个数。

数据范围

$1 \le m \le 16$,
$1 \le n,p_i \le 10^9$

输入样例

10 2
2 3

输出样例

7

题解

//1~n中p的倍数的个数:[n/p],原理:n是由n个1相加而成,n/p代表有多少个完整的p个1,这些都是p的1、2..倍数
#include <iostream>

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

博弈论

Nim游戏:问题的核心是谁最终会沦为面对全0无可动的(先手)必输局面,然后从结果反向出发,结合NIM定理。

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

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

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

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

^为异或运算,相同为0,相异为1。(不进位加法)

先手必胜状态:可以走到某一个必败状态

先手必败状态:走不到任何一个必败状态//必败状态的主语是对方

/*
定理证明:
1.如果所有堆石子数量都是0,那么0^0^...^0=0,谁是先手谁输,因为先手没有办法行动
,故0^0^...^0=0是目标状态。
2.假设说现在是a1^a2...^an=x,假设说x不是0,假设说x最高位1是第k位,那么这堆石子一定存在某个数字ai,它的第k位也是1(如果不存在这么一个数字,不会最后异或出来x第k位是1),又因为ai>ai^x(必然,因为ai第k位变成0,整个数字变小),那一定可以从这堆里拿(ai-(ai^x))个石子,让ai减少为ai^x,那么这个时候,a1^a2^...^ai^...^an=x就变为了a1^a2^...^ai^x^...^an=x^x=0,这个时候后手行动,又会破坏这个异或结果为0,先手继续把它变为0,最后一定能转化为0^0^...^0=0的状态并交给后手,后手输。
*/

台阶-Nim游戏:普通Nim变种

例题:

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

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

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

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

思路:

/*
先手必败思路:所有的奇数台阶都是0^0...^0=0的状态,偶数台阶无论怎么动,后手只要把先手动的石子再移至下一台阶/地面,最终先手一定无石子可动,同时后手无论何时都有可以移动的石子。比如2号台阶有3个石子,先手无论移动多少到1号台阶,后手转手就把他们移到地面,最后先手输;
先手必胜思路:所有奇数台阶石子个数异或结果不为0,这个时候一定可以移动某个奇数台阶一定数量的石子到偶数台阶上,使所有奇数台阶石子个数异或结果为0,此时后手将面临刚刚先手必败思路中先手的情形。
*/

公平组合游戏ICG

若一个游戏满足:

1.由两名玩家交替行动;
2.在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
3.不能行动的玩家判负;

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

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

集合-Nim游戏

有向图游戏

给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。任何一个公平组合游戏都可以转化为有向图游戏。具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的下一个局面连有向边。

Mex运算

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

如mex({0,1,3,4})=2.

SG函数

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

\[SG(x) = mex(SG(y_1), SG(y_2), …, SG(y_k))\\y_1...y_k为状态x下一步能转化成的所有状态\\对于没有出边的终点:SG(终点)=0 \]

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

一个图时,SG(x)==0必败态,SG(x)!=0必胜态,开局状态即决定胜负

解释:

(a)如果SG(x)!=0,意味着x的下一个状态必有0,否则SG(x)=0,所以这步把状态从x移动到0后,对方无法行动,必赢;

(b)如果SG(x)==0,意味着下一步无论如何都到不了0,因为下一个状态如果有0则SG(x)不会有0,所以下一步绝对能到一个状态y,且G(y)!=0,这时轮到对手来操作y状态至下一个状态,根据(a),对手必赢,故此状态是必败态。

有向图游戏的和:多个图,可以选一个图进行操作(改变状态x)时

输的条件:任何一个图都操作不了

设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)

所有图总SG为 每个图现在状态的SG值异或和 判断现在这个局面操作者必败还是必胜的依据就是这个值是否等于0,与一个图时类似,等于0必败,不等于0(大于0)必胜。

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

/*
定理证明:类比Nim游戏证明
1.所有图目前状态x,都有SG(x)=0,0^0^...^0=0,目前操作者必败
2.SG(G)=z,其中z!=0,那么一定有一个状态Xi,SG(Xi)第k位是1,k就是z的最高位,并且SG(Xi)^z<SG(Xi),根据SG函数定义(Xi下一个状态不能取到的最小自然数,所以Xi下一个状态SG(Xi)^z是能取到的),状态Xi一定可以走到一个新的状态Yi使得SG(Yi)=SG(Xi)^z,这个时候又变回了所有SG值异或为0,往复操作,目前操作者一定可以把状态移动至目标状态0^0^...^0=0,必胜。
*/

应用:如果相较于普通Nim,操作存在限制条件,每次拿只能拿规定数量,那么就把每堆的情况分别列出来,每堆各自有一个有向图表示本堆状态的改变过程。(n堆石子即n个有向图)

注:多个图情况下,SG值也不用重复计算,只要在某个图里计算过一次就行,因为相同的情况,相同的限制条件,状态改变的方法一定一样,结果一定一样

例题:acwing893:m个数字,表示能选取的石子数,n个数字,表示每堆石子数量。

/*
输入样例:
2
2 5
3
2 4 7
输出样例:
Yes
*/

#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_set>

using namespace std;

const int N = 110,M = 10010;

int n,m;
int s[N],f[M];//s能选的石子个数,f是SG值,SG值不用重复计算,只要在某个图里计算过一次就行,因为相同的情况,相同的限制条件,状态改变的方法一定一样,结果一定一样

int sg(int x)
{
    if(f[x]!=-1) return f[x];
    
    unordered_set<int> S;//存目前状态x所有子状态的sg值,而x子状态即目前的石子数x-能被选作减数的s[i]
    for(int i=0;i<m;i++)
    {
        int t=s[i];
        if(x>=t) S.insert(sg(x-t));
    }
    
    for(int i=0;;i++)//sg(x)=mex(sg(y1),sg(y2),...),而sg(y1)之流具体数字都已经记录到S中,只要挨个从最小自然数检索,找到最小自然数即可。
        if(!S.count(i))
            return f[x]=i;//同时记得保存这个f[x],记忆化搜索。
}

int main()
{
    cin>>m;
    for(int i=0;i<m;i++)
        cin>>s[i];
    
    cin>>n;
    
    memset(f,-1,sizeof f);//记忆化搜索
    
    int res=0;
    for(int i=0;i<n;i++)
    {
        int x;
        cin>>x;
        res^=sg(x);
    }
    
    if(res) puts("Yes");
    else puts("No");
    
    return 0;
}

拆分-Nim游戏:集合-Nim游戏变种

例题

给定 n 堆石子,两位玩家轮流操作,每次操作可以取走其中的一整堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。

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

思路

/*
求每一堆石子状态的SG值,每堆石子的状态就是该堆石子的数量,然后最后异或在一起,之后的操作与集合-Nim游戏相同
*/

性质

当一种状态a1转移成另一种状态的时候,如果接下来的状态(b1,b2)由两个有向图构成,那么SG(b1,b2)=SG(b1)^SG(b2)

比如a1的下一个状态有两种(b1,b2)、(c1,c2),那么有:

\[SG(a)=mex(SG((b1,b2)),SG((c1,c2)))\\=mex(SG(b1)xorSG(b2),SG(c1)xorSG(c2)) \]

具体代码(模仿集合-Nim游戏)

#include <algorithm>
#include <iostream>
#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++)//枚举子状态
        for(int j=0;j<=i;j++)//避免重复,如(1,2)与(2,1)
            S.insert(sg(i)^sg(j));
    
    //mex操作
    for(int i=0;;i++)
        if(S.count(i)==0)//从最小的自然数开始枚举,找到不存于S(子状态集合)的最小的自然数
            return f[x]=i;
}

int main()
{
    int n;
    cin>>n;
    
    memset(f,-1,sizeof f);
    
    int res=0;
    for(int i=0;i<n;i++)
    {
        int x;
        cin>>x;
        res^=sg(x);
    }
    
    if(res) puts("Yes");
    else puts("No");
    
    return 0;
}
posted @ 2022-01-02 21:41  泝涉江潭  阅读(215)  评论(0)    收藏  举报