FXJ Wiki

Back

数论(II)Blur image

线性筛#

线性筛(欧拉筛)的核心:每个合数只被它的最小质因子筛掉一次,复杂度 O(n)O(n)

vector<int> primes;
bool vis[N];

void sieve(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!vis[i]) primes.push_back(i);
        for (int p : primes) {
            if (1ll * i * p > n) break;
            vis[i * p] = true;
            if (i % p == 0) break;  // p 是 i 的最小质因子,之后的质数不再需要
        }
    }
}
cpp

if (i % p == 0) break 是关键:此时 p 是 i 的最小质因子,那么对于更大的质数 p’,i * p’ 的最小质因子仍然是 p(而非 p’),应该留给后面的轮次去筛。

线性筛不止能筛素数——只要函数是积性函数gcd(a,b)=1    f(ab)=f(a)f(b)\gcd(a,b)=1 \implies f(ab)=f(a)f(b)),就能在筛的过程中顺便求出。比如欧拉函数、莫比乌斯函数都可以在同一个框架下计算。

埃氏筛#

从小到大遍历,每遇到一个素数,就把它的所有倍数标记为合数。复杂度 O(nloglogn)O(n \log \log n)

只筛到 n\sqrt{n} 即可:

vector<bool> is_prime(n + 1, true);
is_prime[0] = is_prime[1] = false;
for (int i = 2; i * i <= n; ++i) {
    if (is_prime[i]) {
        for (int j = i * i; j <= n; j += i)
            is_prime[j] = false;
    }
}
cpp

内层循环从 i2i^2 开始的原因:比 i2i^2 小的倍数一定含有比 ii 更小的质因子,之前已经被筛过了。如果再只筛奇数,常数可以减半。

分块筛#

nn 很大(如 101210^{12})时,线性筛和埃氏筛内存都扛不住。分块筛将区间分成大小为 SS 的块,只用 n\sqrt{n} 以内的素数去筛每一块,内存降到 O(n+S)O(\sqrt{n} + S)

int count_primes(long long n) {
    const int S = 10000;
    int nsqrt = sqrt(n);
    vector<char> is_prime(nsqrt + 1, true);
    vector<int> primes;
    for (int i = 2; i <= nsqrt; ++i) {
        if (is_prime[i]) {
            primes.push_back(i);
            for (int j = i * i; j <= nsqrt; j += i) is_prime[j] = false;
        }
    }
    int result = 0;
    vector<char> block(S);
    for (long long k = 0; k * S <= n; ++k) {
        fill(block.begin(), block.end(), true);
        long long start = k * S;
        for (int p : primes) {
            long long start_idx = max((start + p - 1) / p, (long long)p);
            for (long long j = start_idx * p - start; j < S; j += p)
                block[j] = false;
        }
        if (k == 0) block[0] = block[1] = false;
        for (int i = 0; i < S && start + i <= n; ++i)
            if (block[i]) result++;
    }
    return result;
}
cpp

块大小 SS 一般取 10410510^4 \sim 10^5

欧拉函数#

φ(n)\varphi(n) 表示 11nn 中与 nn 互质的数的个数:

φ(n)=i=1n[gcd(i,n)=1]\varphi(n) = \sum_{i=1}^n [\gcd(i, n) = 1]

性质#

  • 积性:gcd(a,b)=1    φ(ab)=φ(a)φ(b)\gcd(a, b) = 1 \implies \varphi(ab) = \varphi(a)\varphi(b)
  • n=dnφ(d)n = \sum_{d \mid n} \varphi(d)
  • n=pkn = p^kpp 为质数),则 φ(n)=pkpk1\varphi(n) = p^k - p^{k-1}
  • 由唯一分解 n=pikin = \prod p_i^{k_i}φ(n)=n(11pi)\varphi(n) = n \prod \left(1 - \frac{1}{p_i}\right)

单点求值#

O(n)O(\sqrt{n})

int phi(int n) {
    int ans = n;
    for (int i = 2; i * i <= n; ++i) {
        if (n % i == 0) {
            ans = ans / i * (i - 1);
            while (n % i == 0) n /= i;
        }
    }
    if (n > 1) ans = ans / n * (n - 1);
    return ans;
}
cpp

线性筛求欧拉函数#

在线性筛框架下递推:

vector<int> primes, phi;

void sieve(int n) {
    vector<int> minp(n + 1);
    phi.assign(n + 1, 0);
    phi[1] = 1;
    for (int i = 2; i <= n; ++i) {
        if (!minp[i]) {
            minp[i] = i;
            primes.push_back(i);
            phi[i] = i - 1;
        }
        for (int p : primes) {
            if (i * p > n) break;
            minp[i * p] = p;
            if (p == minp[i]) {
                phi[i * p] = phi[i] * p;      // p 是 i 的最小质因子
                break;
            }
            phi[i * p] = phi[i] * phi[p];     // i 与 p 互质
        }
    }
}
cpp

欧拉定理与扩展#

欧拉定理gcd(a,m)=1    aφ(m)1(modm)\gcd(a, m) = 1 \implies a^{\varphi(m)} \equiv 1 \pmod m

费马小定理是 mm 为质数时的特例:ap11(modp)a^{p-1} \equiv 1 \pmod p

扩展欧拉定理:用于处理幂次很大的情况:

ab{abmodφ(m)gcd(a,m)=1abgcd(a,m)1,  b<φ(m)a(bmodφ(m))+φ(m)gcd(a,m)1,  bφ(m)(modm)a^b \equiv \begin{cases} a^{b \bmod \varphi(m)} & \gcd(a, m) = 1 \\[4pt] a^b & \gcd(a, m) \neq 1,\; b < \varphi(m) \\[4pt] a^{(b \bmod \varphi(m)) + \varphi(m)} & \gcd(a, m) \neq 1,\; b \ge \varphi(m) \end{cases} \pmod m

练习#

P2568 GCD:求 i=1nj=1n[gcd(i,j) 为素数]\sum_{i=1}^n \sum_{j=1}^n [\gcd(i,j) \text{ 为素数}]

枚举素数 ppgcd(i,j)=p    gcd(i/p,j/p)=1\gcd(i,j) = p \iff \gcd(i/p, j/p) = 1,故:

pn(2i=1n/pφ(i)1)\sum_{p \le n} \left(2 \sum_{i=1}^{n/p} \varphi(i) - 1\right)
#define int long long
signed main() {
    int n; cin >> n;
    sieve(n);
    partial_sum(phi.begin(), phi.end(), phi.begin());
    int ans = 0;
    for (int p : primes) ans += 2 * phi[n / p] - 1;
    cout << ans << '\n';
}
cpp

P2398 GCD SUM:求 i=1nj=1ngcd(i,j)\sum_{i=1}^n \sum_{j=1}^n \gcd(i,j)

枚举 gcd=k\gcd = k,个数为 2i=1n/kφ(i)12 \sum_{i=1}^{n/k} \varphi(i) - 1

int ans = 0;
for (int k = 1; k <= n; ++k)
    ans += k * (2 * phi[n / k] - 1);
cpp

P2158 仪仗队:可见点 (x,y)(x,y) 满足 gcd(x1,y1)=1\gcd(x-1, y-1) = 1。答案为 2i=1n1φ(i)+12 \sum_{i=1}^{n-1} \varphi(i) + 1n=1n=1 时特判为 00)。

威尔逊定理#

内容pp 为素数     (p1)!1(modp)\iff (p-1)! \equiv -1 \pmod p

推论p>4p > 4 且为合数时,(p1)!0(modp)(p-1)! \equiv 0 \pmod p

计算 n!modpn! \bmod p#

nn 很大时可用分段:O(p+logpn)O(p + \log_p n) 预处理,单次 O(logpn)O(\log_p n)

int factmod(int n, int p) {
    vector<int> f(p);
    f[0] = 1;
    for (int i = 1; i < p; ++i) f[i] = 1ll * f[i - 1] * i % p;
    int res = 1;
    while (n > 1) {
        if ((n / p) % 2) res = p - res;
        res = 1ll * res * f[n % p] % p;
        n /= p;
    }
    return res;
}
cpp

n!n! 中素数 pp 的幂次(Legendre 公式)#

vp(n!)=i=1npi=nSp(n)p1v_p(n!) = \sum_{i=1}^{\infty} \left\lfloor \frac{n}{p^i} \right\rfloor = \frac{n - S_p(n)}{p - 1}

其中 Sp(n)S_p(n)pp 进制下 nn 各位之和。O(logn)O(\log n) 实现:

int vp(int n, int p) {
    int cnt = 0;
    while (n) cnt += (n /= p);
    return cnt;
}
cpp

Kummer 定理#

pp 在组合数 (mn)\binom{m}{n} 中的幂次等于 pp 进制下 nnmnm-n 的借位次数:

vp ⁣((mn))=Sp(n)+Sp(mn)Sp(m)p1v_p\!\left(\binom{m}{n}\right) = \frac{S_p(n) + S_p(m-n) - S_p(m)}{p - 1}
数论(II)
https://fxj.wiki/blog/algorithm-number-theory-2
Author 玛卡巴卡
Published at 2023年10月27日
Comment seems to stuck. Try to refresh?✨