简单数论学习笔记分享

数论

质数

质数的判定——试除法

即最原始的朴素算法

.”d|n”代表的含义是d能整除n,(这里的”|”代表整除)…

一个合数的约数总是成对出现的,如果d|n,那么(n/d)|n,因此我们判断一个数是否为质数的时候,
只需要判断较小的那一个数能否整除n就行了,即只需枚举i<=(n/i),即i*i<=n,i<=sqrt(n)就行了.

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

分解质因数

从2开始往大除,因此不会出现因数为合数

算数基本定理(唯一分解定理):任何一个大于1的自然数N,如果N不为质数,那么N可以唯一分解成有限个质数的乘积
N = P 1 a 1 P 2 a 2 P 3 a 3 . . . . . . P n a n N=P_1a_1P_2a_2P_3a_3......P_na_n N=P1a1P2a2P3a3......Pnan
这里P1<P2<P3…<Pn均为质数,其中指数ai是正整数。

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

质数筛

朴素筛

把2~(n-1)中的所有的数的倍数都标记上,最后没有被标记的数就是质数.
假定有一个数p未被2(p-1)中的数标记过,那么说明,不存在2(p-1)中的任何一个数的倍数是p,

时间复杂度约为O(nlogn)

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_primes(int n)
{
    for (int i = 2; i < n; i++)//埃式筛(优化后接近o(n))
    {
        if (!st[i])
        {
            for (int j = i * i; j < n; j += i)st[j] = 1;
        }
    }
}
线性筛

这个很好用,后面会经常用

若n在10的6次方的话,线性筛和埃氏筛的时间效率差不多,若n在10的7次方的话,线性筛会比埃氏筛快了大概一倍.

然后我们会发现,当找到质数2是会删去6,找到3时也会删去6,6被重复提起了!

因此我们发现,每次只需删去最小质因子就可以,因此时线性的

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;//p[j]是i的最小质因数,跳过,避免后面重复赋值,优化后能到O(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;
}

求约数个数

(int范围内约数个数最多为一千五百个)

首先,我们要知道,一个数字k可以拆成
k = p 1 b 1 + p 2 b 2 + . . . . . . + p n b n k=p_1^{b_1}+p_2^{b_2}+......+p_n^{b_n} k=p1b1+p2b2+......+pnbn
而k的约数u,则可以拆成
u = p 1 a 1 + p 2 a 2 + . . . . . . + p n a n u=p_1^{a_1}+p_2^{a_2}+......+p_n^{a_n} u=p1a1+p2a2+......+pnan
其中
0 < = a < = b 0<=a<=b 0<=a<=b
因此,k的约数个数就是a的组合数

int main()
{
    int n;
    cin >> n;
    unordered_map<int, int> primes;
    while (n -- )
    {
        int x;
        cin >> x;
        for (int i = 2; i <= x / i; i ++ )
            while (x % i == 0)
            {
                x /= i;
                primes[i] ++ ;
            }
        if (x > 1) primes[x] ++ ;
    }
    long long res = 1;
    for (auto p : primes) res = res * (p.second + 1) % mod;
    cout << res << endl;
    return 0;
}

约数之和

对于质因数p1
∑ P 1 = p 1 1 + p 1 2 + . . . + p 1 b 1 \sum{P1}=p_1^1+p_1^2+...+p_1^{b_1} P1=p11+p12+...+p1b1
综合就是
∑ P 1 + ∑ P 2 + . . . + ∑ P n \sum{P_1}+\sum{P_2}+...+\sum{P_n} P1+P2+...+Pn

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

求最大公约数和最小公倍数

采用欧几里得算法计算最大公约数,根据拉梅定理:用欧几里得算法计算两个正整数的最大公约数,需要的除法次数不会超过两个整数中较小的那个十进制数的位数的 5 倍。这个算法时间复杂度会变得很低,而且还短,好记忆,个人非常喜欢。

欧几里得算法的核心为
g c d ( a , b ) = g c d ( a , a m o d    b ) gcd(a,b)=gcd(a,a\mod{b}) gcd(ab)=gcd(a,amodb)
验证很容易,跳过

由此可以得出求解最大公约数的代码gcd

int gcd(int a, int b)
{
	return b ? gcd(b, a % b) : a;//欧几里得算法,求最小公约数,即辗转相除法
}

对此,有定理a、b 两个数的最小公倍数(lcm)乘以它们的最大公约数(gcd)等于 a 和 b 本身的乘积。

即求最小公倍数有
l c m ( a , b ) = g c d ( a , b ) ∗ ( a , b ) lcm(a,b)=gcd(a,b)*(a,b) lcm(a,b)=gcd(a,b)(a,b)

int lcm(int a, int b)
{
	return a / gcd(a,b) * b;//即ab积除以a,b的最小公约数,但是怕a*太大,所以先除后乘
}

欧拉函数

定义:

对于一个正整数n,n的欧拉函数ϕ ( n ),表示小于等于n与n互质的正整数的个数

性质(个人总结):

1.其与因数的次数无关,例如N=2100+31000,ϕ(N)=N*(1-1/2)(1-1/3)

公式:

ϕ ( N ) = N ( 1 − 1 p 1 ) ( 1 − 1 p 2 ) . . . ( 1 − 1 p n ) \phi(N)=N (1-\frac1{p_1})(1-\frac1{p_2})...(1-\frac1{p_n}) ϕ(N)=N(1p11)(1p21)...(1pn1)

证明

N = p 1 b 1 + p 2 b 2 + . . . . . . + p n b n N=p_1^{b_1}+p_2^{b_2}+......+p_n^{b_n} N=p1b1+p2b2+......+pnbn

1.去掉这些质因素的倍数,因为这些数i和n有共同的因数pi,

2.加上pi *pj的倍数,因为pipj都是n的质因数被减了两次

3.减去pi*pj *pk,因为这些减了三次,又在第二步加了三次

4.以此类推,即容斥原理,最后得出公式
ϕ ( N ) = N p 1 + . . . N p n − N p 1 p 2 − . . . − N p n − 1 p n + . . . ± N p 1 p 2 . . . p n \phi(N)=\frac{N}{p_1}+...\frac{N}{p_n}-\frac{N}{p_1p_2}-...-\frac{N}{p_{n-1}p_n}+...\pm\frac{N}{p_1p_2...p_n} ϕ(N)=p1N+...pnNp1p2N...pn1pnN+...±p1p2...pnN
化简可得如上欧拉函数公式

代码实现
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        int n;
        cin >> n;
        int res=n;
        for(int i=2;i<= n/i;i++)
        {
           if(n%i==0)
           {
              res=res/i*(i-1);//1-1/i=(1-i)/i,由于从最小质因数开始,所以我if成功的i不会是合数,就实现了上面的公式
              while(n%i==0) n/=i;//去掉这个质因数的所有倍数
           }
        }
        if(n>1)res=res/n*(n-1);
        cout<<res<<endl;
    }
    return 0;
}

筛法求欧拉函数

即求多个欧拉函数,时间复杂度O(N)

其与质因数的次数无关,可证

当i % pi == 0时

pi * i的质因数,pi是i的质因子,所以pi * i和i的质因数一样,所以
ϕ ( p i ∗ i ) = p i ∗ i ∗ ( 1 − 1 p 1 ) ( 1 − 1 p 2 ) . . . ( 1 − 1 p n ) = ϕ ( i ) ∗ p i \phi({p_i*i})=p_i*i* (1-\frac1{p_1})(1-\frac1{p_2})...(1-\frac1{p_n})=\phi({i})*p_i ϕ(pii)=pii(1p11)(1p21)...(1pn1)=ϕ(i)pi
当i % pi ! = 0时

pi * i的质因数,除了i的质因数还多一个pi,所以
ϕ ( p i ∗ i ) = p i ∗ i ∗ ( 1 − 1 p 1 ) ( 1 − 1 p 2 ) . . . ( 1 − 1 p n ) ∗ ( 1 − 1 p i ) = ϕ ( i ) ∗ p i ∗ ( 1 − 1 p i ) \phi({p_i*i})=p_i*i* (1-\frac1{p_1})(1-\frac1{p_2})...(1-\frac1{p_n})*(1-\frac1{p_i})=\phi({i})*p_i*(1-\frac1{p_i}) ϕ(pii)=pii(1p11)(1p21)...(1pn1)(1pi1)=ϕ(i)pi(1pi1)

LL get_eulers (int n)
{
    euler[1]=1;
    for(int i=2;i<=n;i++)
    {
        if(!st[i])
        {
            prime[cnt++]=i;//质数
            euler[i]=i-1;//质数的欧拉函数
        }
        for(int j=0;prime[j]<=n/i;j++)
        {
            st[prime[j]*i]=true;//标记质数的倍数
            if(i%prime[j]==0)//质因数
            {
                euler[prime[j]*i]=euler[i]*prime[j];//ϕ(pi*i)
                break;
            }
            euler[prime[j]*i]=euler[i]*(prime[j]-1);
        }
    }
    LL sum=0;
    for(int i=0;i<=n;i++)
    {
        sum+=euler[i];
    }
    return sum;
}

欧拉定理

’ 是同余符号,例如a≡d(mod c)意思是a和b除以c的余数相等

若a与n互质,即gcd(a,n)=1,则

公式

a ϕ ( n ) ≡ 1 ( m o d    n ) a^{\phi_{(n)}} ≡ 1 (mod\;n) aϕ(n)1(modn)

意思就是 a 的 φ(n) 次幂除以 n 的余数和1除以n的余数相等
a ϕ ( n ) m o d    n = 1 a^{\phi_{(n)}}\mod{n}=1 aϕ(n)modn=1

证明:

∵ a 1 , a 2 . . . a ϕ ( n ) 与 n 互质, a 与 n 互质 ∴ a 1 ∗ a , a 2 ∗ a . . . a ϕ ( n ) ∗ a 也和 n 互质(可能相对位置发生变化,但是 m o d    n 后值不变) ∴ a 1 ∗ a 2 ∗ . . . ∗ a ϕ ( n ) ≡ a ϕ ( n ) ∗ a 1 . . . ∗ a ϕ ( n ) ( m o d    n ) ∴ a ϕ ( n ) ≡ 1 ( m o d    n ) \because a_1,a_2...a_{\phi({n})}与n互质,a与n互质\\ \therefore a_1*a,a_2*a...a_{\phi({n})}*a也和n互质(可能相对位置发生变化,但是mod\;n后值不变)\\ \therefore a_1*a_2*...*a_{\phi({n})} \equiv a^{\phi({n})}*a_1...*a_{\phi({n})} (\mod n)\\ \therefore a^{\phi({n})}\equiv 1(\mod n) a1,a2...aϕnn互质,an互质a1a,a2a...aϕ(n)a也和n互质(可能相对位置发生变化,但是modn后值不变)a1a2...aϕnaϕna1...aϕn(modn)aϕn1(modn)

ok,现在也是证明完了,那么有什么用呢(我现在也还不知道)

欧拉定理有个推论叫做

费马定理

即当欧拉定理中的n为质数时,ϕ ( n )=n-1,即存在
a n − 1 ≡ 1 ( m o d    n ) a^{n-1} ≡ 1 (mod\;n) an11(modn)

快速幂

快速幂模板

公式原理为
x n = ( x 2 ) n 2 x^n=(x^2)^{\frac{n}{2}} xn=(x2)2n
如此即可节省接近一半的时间,一直递归下去则可使时间复杂度达到O(logn)

int ksm(int x, int y)//递归实现快速幂
{
    if (y == 1)return x;
    else if (y % 2 == 0)
    {
        int num = ksm(x, y / 2);
        return num * num;
    }
    else if (y % 2 == 1)
    {
        int num = ksm(x, y / 2);
        return x * num * num;
    }
}

如下是非递归的的方式

int ksm(int a, int n) {
    int ans = 1;
    while (n) {
        if (n & 1)        //如果n的当前末位为1
            ans *= a;  //ans乘上当前的a
        a *= a;        //a自乘
        n >>= 1;       //n往右移一位
    }
    return ans;
}

快速幂求逆元

逆元的定义:

对于一个运算,如果存在元素 ab ,使得 ab 进行该运算的结果为单位元(identity element),那么就称 ba 的逆元,a 也是 b 的逆元。其可以将除法运算转化成乘法运算。

于数论中,有判别式
a ∗ b ≡ 1 ( m o d    n ) a*b≡1(\mod {n}) ab1(modn)
即可说在mod n的情况下a与b互为逆元

ok,到此为止,对代码的编写都用不到,只是为了打基础

逆元存在的前提是a与n互质,n为质数,a ^ ( p - 1) ≡ 1 ( mod p) (费马定理)

所以a的逆元为a^(p-1)

#include<iostream>
using namespace std;
typedef long long LL;
LL qmi(int a,int k,int b)
{
    LL res=1;
    while(k)
    {
        if(k&1)res=(LL)res*a%b;
        a=(LL)a*a%b;
        k>>=1;
    }
    return res;
}
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        int a,p;
        scanf("%d %d",&a,&p);
        LL ans=qmi(a,p-2,p);
        if(a%p!=0)printf("%lld\n",ans);
        else printf("impossible\n");
    }
}

扩展欧几里得算法

裴蜀定理

对于任意正整数a,b,一定存在一对x,y,使得ax+by=gcd(a,b)

扩展欧几里算法目标就是求解给定 n 对正整数 ai,bi,对于每对数,求出一组 xi,yi,使其满足 ai × xi+bi × yi=gcd(ai,bi)

公式

a ∗ x + b ∗ y = g c d ( a , b ) = b ∗ y + ( a    m o d    b ) ∗ y a    m o d    b = a − ⌊ a / b ⌋ ∗ b a*x+b*y=gcd(a,b)=b*y+(a\;mod{\;b})*y\\ a\;mod{\;b}=a-\lfloor{a/b}\rfloor*b ax+by=gcd(a,b)=by+(amodb)yamodb=aa/bb

代码实现

int gcd_plus(int a, int b,int &x,int &y)
{
	if(b==0)
	{
	    x=1,y=0;
	    return a;//这里y取0,a就是最大公约数
	}
	
    int d=gcd_plus(b,a%b,y,x);//d是最大公约数
	y-=a/b*x;//按照公式更新y的值
	return d;//返回最大公约数
}

线性同余方程

#include<iostream>
using namespace std;
typedef long long LL;
int gcd_plus(int a, int b,int &x,int &y)
{
	if(b==0)
	{
	    x=1,y=0;
	    return a;//这里y取0,a就是最大公约数
	}
	
    int d=gcd_plus(b,a%b,y,x);//d是最大公约数
	y-=a/b*x;//按照公式更新y的值
	return d;//返回最大公约数
}
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        int x,y;
        LL d=gcd_plus(a,c,x,y);
        LL ans=b / d * x % c;
        if(b%d)cout<<"impossible"<<endl;
        else cout<<ans<<endl;
    }
    return 0;
}

中国剩余定理

有 n 个数字 m 1 , m 2 . . . m n , 他们两两互质,求解线性同余方程组 { x = a 1 m o d      m 1 x = a 2 m o d      m 2 . . . x = a n m o d      m n 例如用白话描述就是,现在有一个数不知道是多少,只知道这个数除以 3 余 2 ,除以 5 余 3 ,除以 7 余 2 ,问这个数是多少?设 M = m 1 ∗ m 2 ∗ . . . ∗ m n M 1 = M m 1 , 即除了 m i 外所有 m 的乘积 ( 由于所有数都互质,所以 M i 与 m i 互质 ) 令 M − 1 为 M 的逆 ∴ x = a 1 ∗ M 1 ∗ M 1 − 1 + a 2 ∗ M 2 ∗ M 2 − 1 + . . . + a n ∗ M n ∗ M n − 1 有n个数字m_1,m_2...m_n,他们两两互质,求解线性同余方程组\begin{cases} x=a_1\mod\;m_1\\x=a_2\mod\;m_2\\...\\x=a_n\mod\;m_n \end{cases}\\ 例如用白话描述就是,现在有一个数不知道是多少,只知道这个数除以3余2,除以5余3,除以7余2, 问这个数是多少? 设M=m_1*m_2*...*m_n\\M_1=\frac{M}{m_1},即除了m_i外所有m的乘积\\(由于所有数都互质,所以M_i与m_i互质) \\令M^{-1}为M的逆\\ \therefore x=a_1*M_1*M_1^{-1}+a_2*M_2*M_2^{-1}+...+a_n*M_n*M_n^{-1} n个数字m1,m2...mn,他们两两互质,求解线性同余方程组 x=a1modm1x=a2modm2...x=anmodmn例如用白话描述就是,现在有一个数不知道是多少,只知道这个数除以32,除以53,除以72,问这个数是多少?设M=m1m2...mnM1=m1M,即除了mi外所有m的乘积(由于所有数都互质,所以Mimi互质)M1M的逆x=a1M1M11+a2M2M21+...+anMnMn1

cpp实现

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 10;
int n;
int A[N], B[N];
LL exgcd(LL a, LL b, LL &x, LL &y)  // 扩展欧几里得算法, 求x, y,使得ax + by = gcd(a, b)
{
    if (!b)
    {
        x = 1; y = 0;
        return a;
    }
    LL d = exgcd(b, a % b, y, x);
    y -= (a / b) * x;
    return d;
}
int main()
{
    scanf("%d", &n);
    LL M = 1;
    for (int i = 0; i < n; i ++ )
    {
        scanf("%d%d", &A[i], &B[i]);
        M *= A[i];
    }
    LL res = 0;
    for (int i = 0; i < n; i ++ )
    {
        LL Mi = M / A[i];
        LL ti, x;
        exgcd(Mi, A[i], ti, x);
        res = (res + (__int128)B[i] * Mi * ti) % M;
        // B[i] * Mi * ti可能会超出long long范围,所以需要转化成__int128
    }
    cout << (res % M + M) % M << endl;
    return 0;
}

高斯消元

在O(N^3)时间复杂度解决n个多元线性方程组
{ b 1 = a 11 ∗ x 1 + a 12 ∗ x 2 + . . . + a 1 n ∗ x n b 2 = a 21 ∗ x 1 + a 22 ∗ x 1 + . . . + a 2 n ∗ x n . . . b n = a n 1 ∗ x 1 + a n 2 ∗ x 2 + . . . + a n n ∗ x n \begin{cases} b_1=a_{11}*x_1+a_{12}*x_2+...+a_{1n}*x_n\\b_2=a_{21}*x_1+a_{22}*x_1+...+a_{2n}*x_n\\... \\b_n=a_{n1}*x_1+a_{n2}*x_2+...+a_{nn}*x_n \end{cases}\\ b1=a11x1+a12x2+...+a1nxnb2=a21x1+a22x1+...+a2nxn...bn=an1x1+an2x2+...+annxn

操作

高斯消元需要进行的操作有三个

1.把某一行乘一个非零的数

2.交换两行

3.把某一行加到另一行上

结果

最终的结果也有三种情况

1.行列式最终为完美阶梯型——有唯一解

2.行列式出现0=0——无穷解

3.行列式出现0=非零——无解

步骤

1.遍历每一列,找到绝对值最大那一行

2.把这一行移到最上面

3.同除一个数使此行最前面的那个数字变成1

4.通过加减此行使此行下方的行的最前面这个数的系数变成0

实现

int gauss()  // 高斯消元,答案存于a[i][n]中,0 <= i < n
{
    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.当a,b较小时可以预处理,将所有a,b所有情况的值都存下来,通过递推做

c ( a b ) m o d    p = ( c ( a − 1 b − 1 ) + c ( a b − 1 ) ) m o d    p c\binom{a}{b}\mod{p}=(c\binom{a-1}{b-1}+c\binom{a}{b-1})\mod{p} c(ba)modp=(c(b1a1)+c(b1a))modp

#include<iostream>
using namespace std;
typedef long long LL;
const int mod=1e9+7;
const int N=2020;
int c[N][N];
int main()
{
    int n;
    cin>>n;
    for(int i=0;i<N;i++)
    {
        for(int j=0;j<=i;j++)
        {
            if(j==0)c[i][j]=1;
            else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
        }
    }
    for(int i=0;i<n;i++)
    {
        int a,b;
        cin>>a>>b;
        cout<<c[a][b]<<endl;
    }
    return 0;
}

2.当其较大时,就需要换一种方式

c ( a b ) ( m o d    N ) = a ! ( b − a ) ! ( ! b ) ! ( m o d    N ) = ( a ! m o d    N ) ∗ ( ( ( b − a ) ! ) − m o d    N ) ∗ ( ( b ! ) − m o d    N ) c\binom{a}{b}(\mod N)=\frac{a!}{(b-a)!(!b)!}(\mod N)=(a!\mod N)*(((b-a)!)^-\mod N)*((b!)^-\mod N) c(ba)modN=(ba)!(!b)!a!(modN)=(a!modN)(((ba)!)modN)((b!)modN)

就是说对分数取模运算不能简单的上模除下模,要转换为逆元进行运算

逆元的计算,即a * b ≡ 1 ( mod N),可以通过快速幂计算逆元,即费马小定理

当然,此处要求N为质数


3.当a,b非常大时

lucas定理
c ( a b ) ≡ c ( a m o d    p b m o d    p ) ∗ c ( a / p b / p ) m o d    p c\binom{a}{b}≡c\binom{a\mod{p}}{b\mod{p}}*c\binom{a/p}{b/p}\mod{p} c(ba)c(bmodpamodp)c(b/pa/p)modp

LL qmi(int a,int k,int p)//求逆元
{
    LL res=1;
    while(k)
    {
        if(k&1)res=(LL)res*a%p;
        a=(LL)a*a%p;
        k>>=1;
    }
    return res;
}
LL C(int a,int b,int p)
{
    if(b>a)return 0;
    int res=1;
    for(int i=a,j=1;j<=b;i--,j++)
    {
        res=(LL)res*i%p;
        res=(LL)res*qmi(j,p-2,p)%p;
    }
    return res;
}

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

4.a,b非常大,结合高精度的

这里是直接用的y总的

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 5010;
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)
{
    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;
}
int main()
{
    int a, b;
    cin >> a >> b;
    get_primes(a);
    for (int i = 0; i < cnt; i ++ )
    {
        int p = primes[i];
        sum[i] = get(a, p) - get(a - b, p) - get(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 -- ) printf("%d", res[i]);
    puts("");

    return 0;
}

Catalan数

卡特兰数,广泛运用于好多地方,例如合法路径,括号匹配等等

先来介绍题目的具体要求

给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 00 的个数都不少于 1 的个数的序列有多少个。

这就是一道合法路径问题

将每一种组合抽象成一条路径,0是向右,1是向左,而题目的要求抽象后就是不接触红线

而每一条接触红线的路线在接触红线那个点开始作轴对称,能发现它一定经过点(n-1,n+1)

因此答案很轻易可以推导出为
C a r a l a n = C ( n 2 n ) − C ( n − 1 2 n ) Caralan=C\binom{n}{2n}-C\binom{n-1}{2n} Caralan=C(2nn)C(2nn1)
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码如下

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

容斥原理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

其理论非常简单,平时的数学学习中经常使用

代码实现

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 20;
int p[N];

int main()
{
    int n,m;
    cin>>n>>m;
    int res=0;
    for(int i=0;i<m;i++)cin>>p[i];
    for(int i=1;i<1<<m;i++)//执行2^m次,即所有可能
    {
        int t=1,cnt=0;//记录和以及集合个数
        for(int j=0;j<m;j++)
        {
            if(i>>j&1)//二的第几位了
            {
                if((LL)t*p[j]>n)
                {
                    t=-1;
                    break;
                }
                t*=p[j];
                cnt++;
            }
        }
        if(t!=-1)
        {
            if(cnt%2)res+=n/t;//奇数加偶数减
            else res-=n/t;
        }
    }
    cout<<res;
}

博弈论

SG函数

意义

这个函数的参数是游戏的状态,并且返回值是一个非负整数,当函数值为 0 时,先手必败,否则先手必胜。

mex运算

这是一个施加于集合的运算,表示求出这个集合中最小的没有出现过的非负整数,比如 mex{0,2,3}=1,mex{1,2}=0,mex{}=0,mex{1,2,3}=0。

SG函数的求法

设当前游戏的状态为 x ,后继状态(也就是操作一次后能得到的状态)有 a1,a2,...,ap,那么SG[x]=mex{SG[a1],SG[a2],...,SG[ap]}。

我们将终点状态定为0,即当状态为0时获胜

因此当先手面对SG[a]=0时,先手无法到0,则此子游戏先手必败。

若是SG[a]!=0,则先手一定可以到0,则此子游戏先手必胜。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

SG定理

一个游戏的SG 函数值等于各个游戏的 SG 函数值的Nim 和(各个游戏即指子游戏,Nim和即异或和)

Nim游戏

简单来说,将a1到an全部异或,若是0则先手必败,否则先手必胜,具体思路如下

首先,所有情况可以分成两种状态,即必胜状态和必败状态,经验证,改变某个x就可改变两个状态,验证如下

[!NOTE]

一个圆一个加是异或,代码中是^

a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . ⨁ a n = x ∴ 设 x 的第一个 1 在第 k 位 ∴ 一定存在 a i 的第 k 位是 1 ∴ a i ⨁ x < a i ∴ 从 a i 中拿走 a i − a i ⨁ x 个,即可使 a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . ⨁ a n = x = 0 a i = a i − ( a i − a i ⨁ x ) = a i ⨁ x ∵ a 1 ⨁ a 2 ⨁ a 3 ⨁ . . . ⨁ a n ⨁ x = 0 a_1\bigoplus{a_2}\bigoplus{a_3}\bigoplus...\bigoplus{a_n}=x\\ \therefore设x的第一个1在第k位\\ \therefore一定存在a_i的第k位是1\\ \therefore a_i\bigoplus{x}<a_i\\ \therefore 从a_i中拿走a_i-a_i\bigoplus{x}个,即可使a_1\bigoplus{a_2}\bigoplus{a_3}\bigoplus...\bigoplus{a_n}=x=0\\a_i=ai-(a_i-a_i\bigoplus{x})=a_i\bigoplus{x}\\ \because a_1\bigoplus{a_2}\bigoplus{a_3}\bigoplus...\bigoplus{a_n}\bigoplus{x}=0 a1a2a3...an=xx的第一个1在第k一定存在ai的第k位是1aix<aiai中拿走aiaix个,即可使a1a2a3...an=x=0ai=ai(aiaix)=aixa1a2a3...anx=0

代码实现
#include<iostream>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int res=0;
    for(int i=0;i<n;i++)
    {
        int num;
        cin>>num;
        res=res^num;
    }
    if(res==0)cout<<"No"<<endl;
    else cout<<"Yes"<<endl;
}

台阶-Nim游戏

与经典的Nim游戏不同的是,只能往下一节台阶放,而不是直接拿走,所以只需要考虑奇数阶,因为n阶要拿n次才能到0阶,所以可以看做自己和自己异或n次,所以不需要考虑偶数阶台阶,因为偶数阶台阶自身异或必为0

代码实现
#include <iostream>
#include <algorithm>

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

集合-Nim游戏

与经典Nim游戏不同的是,不能拿随机个数的石子,只能拿给定数组的个数

因此用于计算sg函数的值不是简单的0和1,而是每堆石子的个数和集合给的数字,因此需要改一下sg函数即可

代码实现
#include <cstring>
#include <iostream>
#include <algorithm>
#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];

    unordered_set<int> S;
    for (int i = 0; i < m; i ++ )
    {
        int sum = s[i];
        if (x >= sum) S.insert(sg(x - sum));
    }

    for (int i = 0; ; i ++ )
        if (!S.count(i))
            return f[x] = i;
}
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游戏

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由于最大值一直在减小,因此一定能结束

[!NOTE]

题目最狗的是用了“放入”两字,误导性很强,需要注意

而由SG定理可知,若x分为两堆,即x状态变成i和j状态,则SG[x]=SG[i]^SG[j]。进行记忆化搜索即可

代码实现
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
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 ++ )
            S.insert(sg(i) ^ sg(j));
    for (int i = 0;; i ++ )
        if (!S.count(i))
            return f[x] = i;
}

int main()
{
    cin >> n;
    memset(f, -1, sizeof f);
    int res = 0;
    while (n -- )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }
    if (res) puts("Yes");
    else puts("No");
    return 0;
}

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值