数论

1. 筛法

给定 \(n\),求 \(1 \sim n\) 之间所有的素数。

小学数学,每次把 \(2\) 的倍数标记为合数,再把下一个素数的倍数标记为合数,直到 \(n\) 为止,时间复杂度 \(\mathcal{O}(n\log n)\),代码如下:

void init_prime(int n) {
    for(int i = 2; i <= n; i++) {
        if(!vis[i]) {
            p[++cnt] = i;
            for(int j = 2; i * j <= n; j++) {
                vis[i * j] = true;
            }
        }
    }
}

上述算法的时间复杂度没有非常优秀,其主要原因还是因为每个素数会被多次筛去,那有没有一种算法能使每个数被且仅被筛 \(1\) 次呢?

有的,这种算法就叫做欧拉筛,又叫线性筛,其时间复杂度达到了 \(\mathcal{O}(n)\)

void init_prime(int n) {
    for(int i = 2; i <= n; i++) {
        if(!vis[i]) p[++cnt] = i;
        for(int j = 1; i * p[j] <= n; j++) {
            vis[i * p[j]] = true;
            if(i % p[j] == 0) break;
	}
    }
}

其中 if(i % p[j] == 0) break; 是保证每一个质数仅被筛一次的关键,因为若 \(i\bmod p_j=0\) 说明 \(i\) 之前已经被 \(p_j\) 筛过了,那么 \(i\) 乘上其他质数也一定会被 \(p_j\) 筛掉,就没有必要再筛一遍了,那么此时,\(p_j\) 就是 \(i\) 的最小质因子。

那我们为什么要学习线性筛呢?只是为了 \(\mathcal{O}(n)\) 筛选质数?

当然不是,线性筛还可以在 \(\mathcal{O}(n)\) 的时间复杂度内筛一些积性函数的值,其中积性函数的定义为:若 \(i\)\(j\) 互质,那么 \(f(ij)=f(i)\times f(j)\)

2. 欧拉筛求欧拉函数

我们用 \(\varphi(i)\) 表示比 \(i\) 小且与 \(i\) 互质的数的个数,求 \(1\sim n\)\(\varphi\) 值。

首先很显然,当 \(i\) 为质数时,\(\varphi(i)=i-1\)

近一步,我们令 \(n^\prime\)\(\dfrac{n}{p}\),其中 \(p\)\(n\) 的最小质因子,我们分两种情况讨论。

如果 \(n^\prime \bmod p_1=0\) ,此时 \(\varphi(n)=p_1\times \varphi(n^\prime)\)

如果 \(n^\prime \bmod p_1\ne0\),根据积性函数的定义,\(\varphi(n)=\varphi(p_1)\times \varphi(n^\prime)\)

所以只需要在 break 的时候判断一下更新 \(\varphi\) 值就好了。

void init_phi(int n) {
    phi[1] = 1; 
    for(int i = 2; i <= n; i++) {
        if(!vis[i]) p[++cnt] = i, phi[i] = i - 1;
        for(int j = 1; i * p[j] <= n; j++) {
            vis[i * p[j]] = true;
            if(i % p[j] == 0) {
        	phi[i * p[j]] = phi[i] * p[j];
        	break;
	    } else {
		phi[i * p[j]] = phi[i] * phi[p[j]];
	    }
	}
    }
}

3. 拓展欧几里得算法

给定一个同余方程 \(ax\equiv b\ (\bmod\ n)\),求 \(x\) 的一个可行整数解。

首先这个方程就等价于 \(ax+ny=b\),其中 \(x,y\) 均为未知数,这就相当于求解一个二元一次不定方程组。显然,当 \(\gcd(a,n)\nmid b\) 时方程组不一定有解。

我们先考虑方程 \(ax+ny=\gcd(a,n)\),设:

\[%\begin{aligned} ax_1+ny_1=\gcd(a,n)\\ nx_2+(a\bmod n)y_2=\gcd(n,a\bmod n) %\end{aligned} \]

因为

\[\gcd(a,n)=\gcd(n,a\bmod n) \]

所以

\[ax_1+ny_1=nx_2+(a\bmod n)y_2\\ ax_1+ny_1=nx_2+(a-\left\lfloor\dfrac{a}{n}\right\rfloor\times n) y_2\\ ax_1+ny_1=nx_2+a y_2-\left\lfloor\dfrac{a}{n}\right\rfloor\times ny_2\\ ax_1+ny_1=a y_2+n\left(x_2-\left\lfloor\dfrac{a}{n}\right\rfloor\times y_2\right) \]

那么 \(x_1=y_2\)\(y_1=x_2-\left\lfloor\dfrac{a}{n}\right\rfloor\times y_2\),递归求解即可,时间复杂度 \(\mathcal{O}(\log n)\)

int Exgcd(int a, int n, int& x, int& y) {
    if(n == 0) {
        x = 1, y = 0;
        return a;
    }
    int gcd = Exgcd(n, a % n, x, y), t = x;
    x = y, y = t - (a / n) * y;
    return gcd;
}
例题:P1516 青蛙的约会

求关于 \(k\) 的方程 \(x+km\equiv y+kn\ (\bmod L)\) 的最小正整数解。

我们把式子进行变形:

\[km-kn\equiv y-x\ (\bmod L)\\ k(m-n)\equiv y-x\ (\bmod L) \]

我们令 \(A=m-n,B=y-x\),原方程转换为:

\[kA\equiv B\ (\bmod L)\\ kA+nL=B \]

容易发现这就是欧几里得算法最标准的形式,可是我们求出来的解只是其中一个特解,所以我们需要通过取模数 \(L\) 得到最小正整数解。

#include<iostream>
#define int long long
using namespace std;

int Exgcd(int a, int b, int& x, int& y) {
    if(b == 0) {
    	x = 1, y = 0;
    	return a;
    }
    int ans = Exgcd(b, a % b, x, y);
    int t = x;
    x = y, y = t - a / b * y;
    return ans; 
}

signed main() {
    int x, y, m, n, L;
    scanf("%lld%lld%lld%lld%lld", &x, &y, &m, &n, &L);
    int a = n - m, b = L, c = x - y;
    if(a < 0) a = -a, c = -c;
    x = 0, y = 0;
    int ans = Exgcd(a, b, x, y);
    if(c % ans != 0) return puts("Impossible") & 0;
    else printf("%lld", (x * (c / ans) % (b / ans) + (b / ans)) % (b / ans));
    return 0;
}

3. 乘法逆元

如果正整数 \(x\) 满足 \(ax\equiv 1\ (\bmod\ b)\),那么我们称 \(x\)\(a\) 在模 \(b\) 意义下的乘法逆元,所以模意义下的除法相当于乘上其模意义下的乘法逆元,并且只有 \(b\) 是质数时 \(a\)才存在模 \(b\) 意义下的乘法逆元。

其实本质和 exgcd 一样,同样可以使用 exgcd 算法在 \(\log\) 的时间复杂度内求出一个数的乘法逆元:

int Exgcd(int a, int b, int& x, int& y) {
    if(b == 0) {
    	x = 1, y = 0;
    	return a;
    }
    int ans = Exgcd(b, a % b, x, y);
    int t = x;
    x = y, y = t - a / b * y;
    return ans; 
}

调用 Exgcd(a, b, x, y) 即可求出 \(a\) 在模 \(b\) 意义下的乘法逆元。

当然,我们还可以用费马小定理求解:

费马小定理:\(a^{b-1}\equiv 1\ (\bmod\ b)\),那么 \(a\times a^{b-2}\equiv 1\ (\bmod\ b)\),所以 \(a\) 在模 \(b\) 意义下的乘法逆元就是 \(a^{b-2}\)

4. 计算组合数

为方便阅读,下文使用 \(\dbinom{n}{m}\) 代替 \(C_n^m\)

因为组合数有公式 \(\dbinom{n}{m}=\dfrac{n!}{m!(n-m)!}\),所以可以计算出 \(m!\)\((n-m)!\) 的逆元快速计算:

int C(int n, int m) {
    return fac[n] * inv[fac[m]] % mod * inv[fac[n - m]] % mod;
}

因为 \(\dbinom{n}{m}=\dbinom{n}{m - 1} + \dbinom{n - 1}{m - 1}\),所以可以递推求解,时间复杂度 \(\mathcal{O}(nm)\)

int cal(int n, int m) {
    C[0][0] = 1;
    for(int i = 1; i <= n; i++) {
        C[i][0] = 1, C[i][i] = 1;
        for(int j = 1; j < i; j++) {
            C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
        }
    }
    return C[n][m];
}

Lucas 定理:\(\dbinom{n}{m}\bmod p=\dbinom{\left\lfloor n / p \right\rfloor}{\left\lfloor m / p \right\rfloor}\times\dbinom{n\bmod p}{m\bmod p}\bmod p\),可以递归求解,通常是在 \(p\) 较小,\(n,m\) 较大的情况下使用:

int C(int n, int m, int p) {
    if(m > n) return 0;
    return fac[n] * fpow(fac[m], p - 2, p) % p * fpow(fac[n - m], p - 2, p) % p;
}

int Lucas(int n, int m, int p) {
    if(m == 0) return 1;
    return (Lucas(n / p, m / p, p) * C(n % p, m % p, p)) % p;
}

5. 中国剩余定理

给定线性同余方程组:

\[\begin{cases} x&\equiv a_1\ (\bmod\ p_1)\\ x&\equiv a_2\ (\bmod\ p_2)\\ x&\equiv a_3\ (\bmod\ p_3)\\ &\cdots\\ x&\equiv a_n\ (\bmod\ p_n) \end{cases} \]

\(x\) 的最小正整数解,其中 \(p_1,p_2,p_3,\cdots,p_n\) 两两互质。

可以使用中国剩余定理(CRT)求解,其核心思想是让其满足其中一个方程组,并且在其他模数意义下答案为 \(0\),把答案加起来就是最后的答案。

我们用方程

\[\begin{cases} x\equiv 2\ (\bmod\ 3)\\ x\equiv 3\ (\bmod\ 5)\\ x\equiv 2\ (\bmod\ 7) \end{cases} \]

来演式中国剩余定理的计算过程:

首先考虑满足第一个方程,并且在模其他模数的意义下均为 \(0\),得到方程:

\[\begin{cases} x\equiv 2\ (\bmod\ 3)\\ x\equiv 0\ (\bmod\ 5\times 7) \end{cases} \]

解得 \(x=140\)

接下来考虑满足第二个方程,并且在模其他模数的意义下均为 \(0\),得到方程:

\[\begin{cases} x\equiv 3\ (\bmod\ 5)\\ x\equiv 0\ (\bmod\ 3\times 7) \end{cases} \]

解得 \(x=63\)

然后考虑满足第三个方程,并且在模其他模数的意义下均为 \(0\),得到方程:

\[\begin{cases} x\equiv 2\ (\bmod\ 7)\\ x\equiv 0\ (\bmod\ 3\times 5) \end{cases} \]

解得 \(x=30\)

最后把解得的三个 \(x\) 值加在一起,得到 \(233\),对模数乘积取模得到 \(233\bmod 3\times 5\times 7 = 23\),所以最小正整数解就是 \(23\)

那么我们现在的问题就变成了,如何解出每个方程组的 \(x\)

对于第 \(i\) 个方程,我们设 \(p_i^\prime=\dfrac{\prod\limits^{n}_{k=1}p_k}{p_i}\),那么可以按照如下方法构造出每个方程组的解 \(x\)

  • 计算 \(p_i^\prime\) 在模 \(p_i\) 意义下的乘法逆元 \(y\)。显然,此时 \(y\times p_i^\prime\times a_i\equiv a_i\ (\bmod \ p_i)\),并且 \(y\times p_i^\prime\times a_i\equiv 0\ (\bmod \ p_i^\prime)\)

所以每个方程组中的 \(x\) 就是 \(y\times p_i^\prime\times a_i\)

int CRT(int n) {
    long long ans = 1, res = 0;
    for(int i = 1; i <= n; i++) ans *= b[i];
    for(int i = 1; i <= n; i++) {
        long long p = ans / b[i];
        int x = 0, y = 0;
        Exgcd(p, b[i], x, y);
         res = (res + (a[i] * p * x) % ans) % ans;
    }
    return (res + ans) % ans;
}
posted @ 2023-01-19 19:48  lnlmz  阅读(56)  评论(0编辑  收藏  举报