ACM数论基础

1,109 阅读8分钟

写在前面

首先,感谢up主的视频

数论一般都要用到ll类型,就算没用到,你用ll类型计算也基本上不会多花时间,毕竟ACM考察的核心还是算法。

快速幂模板

int fpow(int a,int b,const int c)
{
    ll base=a%c,ans=1;
    while(b)
    {
        if(b&1)
            ans = ans*base%c;
        base = base*base%c;
        b >>= 1;
    }
    return ans;
}

1. 整数的取余运算

定义: 带余除法
a,b 是整数,且 b>0 ,则存在非负整数 q,r ,使得 a=bq+r0\le r<b ,称 q 为商, r 为余数

显然带余除法中的商和余数都是唯一的,在下文中将商记为 a/b ,将余数记为 a\%b

整数加减乘

(a+b)\%c=(a\%c+b\%c)\%c
(a-b)\%c=(a\%c-b\%c+c)\%c
(ab)\%c=(a\%c)(b\%c)\%c

注意,在计算减法时,通常需要加 c ,防止变成负数

在计算乘法时,如果 c 较大(但不超过64位整数范围),可以使用快速乘法进行计算,原理与快速幂运算类似,复杂度为 O(\log b)

ll fastMul(ll a, ll b, ll p){
    a %= p;
    ll ans = 0;
    while(b > 0){
        if(b&1)
            ans = (ans+a)%p;
        b >>= 1;
        a = (a+a)%p;
    }
    return ans;
}

如果需要更快的快速乘法,可以使用 long double 数据类型进行计算,复杂度为 O(1)

ll modMul(ll a, ll b, ll p){
    if (p<=1000000000) 
        return a*b%p;
    else if (p<=1000000000000ll) 
        return (((a*(b>>20)%p)<<20)+(a*(b&((1<<20)-1))))%p;
    else 
    {
        ll d = (ll)floor(a*(long double)b/p+0.5);
        ll ret = (a*b-d*p)%p;
        if (ret < 0) 
            ret += p;
        return ret;
    }
}

先抄着,作者说目前没有出错的情况,暂且相信他.

整数除法/求逆元

虽然取余运算对于“+”、“-”、“×”不难,但通常情况下

\frac{a}{b}\%c\neq \frac{a\%c}{b\%c}\%c

如何计算 \frac{a}{b}\%c ?我们有时可以找一个乘法逆元 b^{-1} ,使得 bb^{-1}\equiv 1(mod\ c) ,那么就有

\frac{a}{b}\%c=ab^{-1}\%c

如果 c 是素数,有下面的定理

费马小定理

b 是一个整数,c 是一个素数,二者互质,那么b^{c-1}\equiv 1(mod\ c)
将上式改写一下,得到bb^{c-2}\equiv 1(mod\ c)
因此取 b^{-1}=b^{c-2} 即可,一般需要用快速幂计算,但要注意,与除数不能为0类似,要保证 b\%c\neq 0

ll inv(ll a,ll p)
{
    return fpow(a,p-2);
}

上面的方法给出了模数为素数的解决方案,如果模数不是素数,可以用下面的方法

拓展欧几里得

a\cdot b=1 \%p 相当于 b\cdot a+ k \cdot p = 1

只要存在逆元即可求,适用于个数不多但是mod很大的时候。

LL exgcd(LL a,LL b,LL &x,LL &y)
{
    if(b==0)
    {
        x=1,y=0;
        return a;
    }
    LL ret=exgcd(b,a%b,y,x);
    y-=a/b*x;
    return ret;
}
LL getInv(int a,int mod)
{
    LL x,y;
    LL d = exgcd(a, mod, x, y);
    if(d == 1)
        return (x%mod+mod)%mod;
    return -1;//不存在逆元返回-1 
}

只有 gcd(a,p)=1 时才有解,否则逆元不存在

逆元不存在的情况,待补充,哔哩哔哩视频上有。

递推打逆元表

当p是个质数的时候有

inv(a) = (p - p / a) * inv(p \% a) \% p

证明:
x = p \% a,y = p / a
于是有 x + y * a = p
(x + y * a) \% p = 0
移项得 x \% p = (-y) * a \% p
x * inv(a) \% p = (-y) % p
inv(a) = (p - y) * inv(x) \% p
于是 inv(a) = (p - p / a) * inv(p \% a) \% p
然后一直递归到1为止,因为1的逆元就是1

ll inv[N]={0,1};
void init()
{
    for(int i = 2; i < N; i ++)
        inv[i] = (MOD - MOD / i) * inv[MOD % i] % MOD;
}

2. 最大公因数与最小公倍数

2.1 最大公因数(GreatestCommonDivisor)

顾名思义,最大公因数就是公因数中最大的那个,我们记 a,b 的最大公因数为 gcd(a,b) ,有如下性质

性质1: gcd(a,b)=gcd(b,a)

性质2: gcd(a,b)=gcd(a-b,b)(a\ge b)

性质3: gcd(a,b)=gcd(a\%b,b)

视频里有个地方讲的不错,抄一下

性质4: gcd(a,b,c)=gcd(gcd(a,b),c)

性质5: gcd(ka,kb)=k\ gcd(a,b)

ll gcd(ll a, ll b)
{
    if(b==0)
        return a;
    return gcd(b,a%b);
}

2.2 最小公倍数(LowestCommonMultiple)

最小公倍数就是公倍数中最小的那个,我们记 a,b 的最小公倍数为 lcm(a,b) ,有lcm(a,b)=\frac{ab}{gcd(a,b)}

ll lcm(ll a, ll b)
{
    return a/gcd(a,b)*b;
}

注意是先除后乘,避免在中间过程中数据超出64位整数的范围

2.3 扩展欧几里得算法

考虑二元一次不定方程 ax+by=c ,其中 a,b,c 是已知的正整数,如何求出方程的解呢?

2.3.1 定理 上述方程有解的充要条件是 gcd(a,b)|ccgcd(a,b) 的倍数)

可以理解为,gcd(a,b)ax+by 可以表示出的最小正整数

2.3.2 定理 方程 ax+by=d,d=gcd(a,b) 的所有解为

\begin{cases}
x=x_0+\frac{b}{d}t\\
y=y_0-\frac{a}{d}t
\end{cases}

其中 x_0,y_0 是一组特解

2.3.3 定理 方程 ax+by=c,gcd(a,b)|c 的所有解为

\begin{cases}
x=\frac{c}{d}x_0+\frac{b}{d}t\\
y=\frac{c}{d}y_0-\frac{a}{d}t
\end{cases}

其中 x_0,y_0 是方程 ax+by=d,d=gcd(a,b) 的一组特解

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

代码未学习!!!

2.3.4 推论 逆元的存在性
存在 b^{-1}\in \mathbb{Z},s.t.bb^{-1}\equiv 1(mod\ c) 的充要条件是 gcd(b,c)=1

3. 同余方程组

有方程就有方程组

\begin{cases}
x\equiv a_1 (mod\ m_1)\\
x\equiv a_2 (mod\ m_2)\\
\dots\\
x\equiv a_n (mod\ m_n)
\end{cases}

其中 a_1,a_2,\dots,a_n 是整数, m_1,m_2,\dots,m_n 是正整数

3.1 中国剩余定理(孙子定理)

设上述方程组中 m_1,m_2,\dots,m_n 两两互质,则上述方程组有解
M=m_1*m_2*...*m_n 是整数 m_1,m_2,\cdot\cdot,m_n 的乘积
并设 M_i=M/m_i 是除了mi以外的n- 1个整数的乘积
t_i=M_i^{-1}M_im_i 意义下的逆元/数论倒数
方程组的通解形式为 x = a_1t_1M_1+a_2t_2M_2+...+a_nt_nM_n+kM=kM+\sum_{i=1}^na_it_iM_i, k\in Z

上面的通解真的简单暴力, 注:+kM 因为是通解

在模 M 的意义(应该就是结果对 M 取余)下,方程组只有一个解 x = (\sum_{i=1}^na_it_iM_i)mod\ M

m[i]*x+lcm*y=gcd(lcm, m[i])

ll CRT(int *m,int *a,int n)//m数组里面存的是模数,a里面存的是余数
{
    ll x,y,res=0,mod=1,tmp;
    for (int i=1;i<=n;++i) 
        mod*=m[i];
    for (int i=1;i<=n;++i)
    {
        tmp=mod/m[i];
        ext_gcd(m[i], tmp, x, y);
        res=(res + y*tmp*a[i])%mod;
    }
    return (res+mod)%mod;//在这里管理一下正负号就OK了
}

3.2 拓展中国剩余定理

假设有两个方程

\begin{cases}
x\equiv a_1 (mod\ m_1)\\
x\equiv a_2 (mod\ m_2)\\
\end{cases}

x=m_1*k_1+a_1,x=m_2*k_2+a_2
合并这两个方程,正负号随意啦!
m_1*k_1+a_1 = m_2*k_2+a_2
m_1*k_1+m_2*k_2 = a_2-a_1
我们可以用扩展欧几里得求出一个最小正整数解 k_1,令 K=m_1*k_1+a1
就有新方程x=K(mod\ lcm(m_1,m_2))
所以,一路向下合并就OK了

代码解释

x=ans(mod\ lcm)和x=a_i(mod\ m_i)
x=lcm*k_0+ans,x=m_i*k_i+a_i
lcm*k_0+ans = m_i*k_i+a_i
lcm*k_0+m_i*k_i = a_i-ans
两边同除以 d=gcd(lcm,m[i])
lcm/d*k_0+m_i/d*k_i = (a_i-ans)/d

求exgcd时

lcm*k_0+m_i*k_i = gcd(lcm,m_i)
方程 ax+by=c,gcd(a,b)|c 的一组解为

\begin{cases}
x=\frac{c}{d}x_0+\frac{b}{d}t\\
y=\frac{c}{d}y_0-\frac{a}{d}t
\end{cases}
ll ex_CRT(ll *m,ll *a,int len)
{
    ll lcm = 1;
    ll ans = 0;
    for (int i=0;i<len;i++)
    {
        ll k0,ki;
        ll d = ext_gcd(lcm,m[i],k0,ki);
        if ((a[i]-ans)%d!=0)//无解
            return -1;
        else 
        {
            ll t = m[i]/d;
            k0 = ( k0*(a[i]-ans)/d%t + t)%t;//加这个t为了保持正
            ans = k0*lcm + ans;
            lcm = lcm/d*m[i];
        }
    }
    return ans;
}

4. 素数

素数是只有1和本身两个因数的数,1不是素数

可以理解为素数都有两个因子,而1只有一个因子

4.1 素数的判断

普法

bool isPrime(ll n){
    if(n==1)
        return false;
    for(ll i=2;i*i<=n;i++)
        if(n%i==0)return false;
    return true;
}

这是最简单的素数的判断的参考代码模板,复杂度为 O(\sqrt{n})

原理:对于一个大于1的整数,如果 x 是它的一个大于 \sqrt{n} 的因子,那么 \frac{n}{x} 是它的小于 \sqrt{n} 的因子

kn+i 法

这个我觉得只能判比较小的素数,感觉用处不大。

一个大于1的整数如果不是素数,那么一定有素因子,因此在枚举因子时只需要考虑可能为素数的因子即可。 kn+i 法即枚举形如 kn+i 的数,例如取 k=6 ,那么 6n+2,6n+3,6n+4,6n+6 都不可能为素数,只需要枚举形如 6n+1,6n+5 的数即可,这样复杂度降低了 \frac{2}{3}

下面的模板是 kn+ik=30 的版本

bool isPrime(ll n){
    if(n==2||n==3||n==5)
        return 1;
    if(n%2==0||n%3==0||n%5==0||n==1)
        return 0;
    ll c=7,a[8]={4,2,4,2,4,6,2,6};
    while(c*c<=n)
    {
        for(auto i:a)
        {
            if(n%c==0)
                return 0;
            c += i;//c += i后应该是在遍历小于30的质数
        }
    }
    return 1;
}

Miller-Rabin素性检测

如果 n 极大,可以使用Miller-Rabin素性检测法

未学习!!!

ll Rand(){
    static ll x=(srand((int)time(0)),rand());
    x+=1000003;
    if(x>1000000007)x-=1000000007;
    return x;
}
bool Witness(ll a,ll n){
    ll t=0,u=n-1;
    while(!(u&1))u>>=1,t++;
    ll x=fpow(a,u,n),y;
    while(t--){
        y=x*x%n;
        if(y==1 && x!=1 && x!=n-1)return true;
        x=y;
    }
    return x!=1;
}
bool MillerRabin(ll n,ll s){
    if(n==2||n==3||n==5)return 1;
    if(n%2==0||n%3==0||n%5==0||n==1)return 0;
    while(s--){
        if(Witness(Rand()%(n-1)+1,n))return false;
    }
    return true;
}

当然, Rand() 怎么写可以自由发挥,这会影响其性能

4.2 素数筛

如果要求出不超过 n 的所有素数,素数筛是最好的选择,下面是一种朴素的筛法

埃氏筛

void getPrime(bool p[],int n)
{
    for(int i=1;i<=n;i++)
        p[i]=true;
    p[1]=false;
    for(int i=2;i<=n;i++)
    {
        if(p[i])
        {
            for(int j=i+i;j<=n;j+=i)
                p[j]=false;
        }
    }
}

这种方法的原理是从小到大将素数的倍数筛掉,复杂度为 O(n\log n) ,注意到每个合数如果有多个素因子,那么就会被重复筛掉,造成复杂度的浪费,因此,用下面的方法可以保证每个合数只被它最小的素因子筛掉一遍,以 O(n) 的复杂度解决上述问题

线性筛

ll getPrime(ll n,bool vis[],ll prime[])
{
    ll tot=0;
    for(ll i=1;i<=n;i++)
        vis[i]=0;
    for(ll i=2;i<=n;i++)
    {
        if(!vis[i])
            prime[tot++]=i;
        for(ll j=0;j<tot;j++)
        {
            if(prime[j]*i>n)
                break;
            vis[prime[j]*i]=1;
            if(i%prime[j]==0)
                break;
        }
    }
    return tot;
}

分析:
i为质数时,筛出的数都是 N=p_1*p_2 的形式, p_1,p_2 之间不相等(除了 p_1=p_2=i )
i为合数时,只能筛出不大于 i 的最小质因子 p_1 的质数 *i

一句话解释:每个合数必有一个最大因子(不包括它本身) ,用这个因子把合数筛掉

那又怎么保证都筛干净? 好像就已经保证了....