" /> " /> " />

数论

数学能不能去死啊!!!

本文缺少一些比较困难证明,且对于部分杂乱的定理缺少记录,主要原因是实力不够。

By Pan_g

筛法

筛到你的时候,你应该感到幸运。

埃拉托斯特尼筛法

简称埃氏筛。

思路

思路就是把每一个素数的倍数标记一遍,非常简单。

优化

因为遍历到 ​x 时, ​2 \sim x - 1 都筛过了,所以只用从 ​x^2 开始标记,这时的复杂度是 ​O(N \log\log N) (有的题会用上)。

剩下的就是一些常数优化,比如筛到平方根、只筛奇数啥的。

不过有一种分块优化,不过应该不重要。

欧拉筛

也称线性筛,由其复杂度为线性得名。

思路

埃氏筛的每一个合数会标记很多遍,所以考虑如何只让其标记一遍。

考虑反过来想,把每个数的素数倍标记。

接着再思考,当遍历到一个当前数 ​x 的质因子 ​d 时,那么接着往后标记就没有意义了。

为什么呢?因为再往后的倍数一定已经被 ​d 及其他的倍数(包括后面的)标记掉了,所以一定就没有意义了,这个感性理解一下,反正想想也就明白了,不会细说。

std::vector<int> sieve(int n){
    std::vector<int> np(n, false), prime;
    for(int i = 2;i < n;i++){
        if(!np[i]) prime.emplace_back(i);
        for(auto p : prime){
            if(1ll * i * p >= n) break;
            np[i * p] = true;
            // 切记先标记再判断倍数
            if(i % p == 0) break;
        }
    }
    return prime;
} // Eular Sieve

应用

筛法除了筛素数之外还有求积性函数以及类似性质函数的功能。

这个就是按照性质筛就好了,推式子也就分为三类:

  • ​x 为质数的答案;
  • ​p | x​p 是质数的转移;
  • ​p \bot x​p 是质数的转移;

欧几里得算法

相约之时,一定有着人格上的相等。

最大公约数 & 最小公倍数

最大公约数一般记为 ​\gcd ,最小公倍数一般记为 ​\operatorname{lcm}

辗转相除法

都知道辗转相除法,也就是欧几里得算法,简单证明一下:

​\gcd(x, y) = k, x = ak, y = bk, a = pb + q(p, q\in \mathbb{Z}, q < b)

所以 ​x = pbk + qk, y = bk, x \bmod y = qk

由于 ​q < b ,所以 ​q\bot b ,那么就有 ​\gcd(x,y) = \gcd(y, x \bmod y)

实现比较简单。

都知道 ​\operatorname{lcm}(x, y) = \dfrac{xy}{\gcd(x, y)} 不写了。

更相减损术

​\gcd(a, b) = \gcd(b, a - b) 证明比较简单。

有一个优化就是当 ​2 | a​2 | b 时,有 ​\gcd(a, b) = 2\gcd(\dfrac a2, \dfrac b2) ,然后可以大幅提升效率。

主要适用于大数的相关计算,也就高精度上有用。

扩展欧几里得算法

辗转相除法叫做欧几里得算法,而扩展欧几里得也就叫 exgcd 。

原理

用于求解一个不定方程 ​ax + by = \gcd(a, b) 的可行解,根据裴蜀定理,该方程一定有解。

注意到 ​\gcd(a, b) = \gcd(b, a \bmod b) ,所以将这个式子拓展出去得到:

\begin{cases} ax_1 + by_1 = \gcd(a, b) \\ bx_2 + (a \bmod b)y_2 = \gcd(b, a \bmod b) \\ \gcd(a, b) = \gcd(b, a \bmod b) \\ \end{cases}
\begin{aligned} ax_1 + by_1 &= bx_2 + (a \bmod b)y_2 \\ ax_1 + by_1 &= bx_2 + (a - b \lfloor \dfrac a b \rfloor)y_2 \\ x_1a + y_1b &= y_2 a + (x_2 - \lfloor \dfrac a b \rfloor y_2)b \end{aligned}
\begin{cases} x_1 = y_2 \\ y_1 = x_2 - \lfloor \dfrac a b \rfloor y_2 \end{cases}

所以可以在辗转相除的过程中求出一个解。

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

通解为,下面 ​t\in \mathbb{Z}

\begin{cases} x = x_0 + bt \\ y = y_0 - at \end{cases}

其他分析

复杂度同欧几里得算法为 ​O(\log \max\{a, b\}) ,且运算过程中 ​|x| \leq |b|, |y| \leq |a| ,妈妈再也不用担心我爆 long long 了。

类欧几里德算法

是一类用于求类似欧几里得算法过程的题目,一般可以在题目中发现 ​\lfloor \rfloor 之类的符号求和,或者一条直线截取一个平面之类的,当然也有可能是很多类似的构造,比较灵活,应题而变。

求解过程

接下来我们基于模板题研究欧几里得算法的过程。

求解 ​f(a, b, c, n) = \sum_{i = 0}^n \lfloor \dfrac {ai + b}{c} \rfloor

首先,我们可以把欧几里得的过程拉出来看一眼。

就可以分情况讨论了:

对于 ​a = 0 的情况,我们可以得到 ​f(a, b, c, n) = (n + 1)\lfloor \dfrac b c \rfloor

对于 ​a \geq c 或者 ​b \geq c 的情况,

\begin{aligned} f(a, b, c, n) &= \sum_{i = 0}^n \lfloor \dfrac {(a \bmod c)i + (b \bmod c)}{c} \rfloor + \lfloor \dfrac ac \rfloor i + \lfloor \dfrac bc \rfloor \\ &= f(a \bmod c, b \bmod c, c, n) + \dfrac {n(n + 1)}2 \lfloor \dfrac ac \rfloor + (n + 1) \lfloor \dfrac bc \rfloor \\ \end{aligned}

接下来来到相对复杂的情况,对于 ​a < c​b < c 的情况,可以考虑如下变换,

\begin{aligned} f(a, b, c, n) &= \sum_{i = 0}^n \sum_{j = 1}^m \big[ j \leq \lfloor \dfrac {ai + b}c \rfloor \big] & m = \lfloor \dfrac{an + b}c \rfloor \\ &= \sum_{i = 0}^n \sum_{j = 0}^{m - 1} \big[ jc + c < ai + b + 1 \big] \\ &= \sum_{i = 0}^n \sum_{j = 0}^{m - 1} \big[ jc + c - b - 1 < ai \big] \\ &= \sum_{j = 0}^{m - 1} \sum_{i = 0}^n \big[ i > \lfloor \dfrac {jc + c - b - 1}a \rfloor \big] \\ &= \sum_{j = 0}^{m - 1} n - \lfloor \dfrac {jc + c - b - 1}a \rfloor \\ &= nm - f(c, c - b - 1, a, m) \end{aligned}

至于剩下那两个就随她去吧。

几何意义

其实类欧的过程就类似于一个直线 ​l : ax - cy + b = 0 下方(包含直线上)整点计数 的问题。

​a \geq c 或者 ​b \geq c 的情况,相当于再直线 ​l 下方找一条 最接近 ​l整系数直线 ​q (也就是 ​y = kx + b 中的 ​k \in \mathbb{Z} ) ,然后将 ​q 减掉得到 ​l' = l - q ,再对 ​l' 求解与 ​q 的贡献相加。

​q 由于是一个整系数的直线,所以截得的贡献等价于一个 梯形面积 ,套用梯形的面积公式,可以得到与上文相同的式子。

​a < c​b < c 的情况,也是很好理解的,就是将直线下方的贡献转变为总点数减去直线上方贡献,操作就是 翻转坐标轴 ,依然可以得到上文相同的式子。

所以其实 ​a < c​b < c 的情况,就是对原来的式子 取倒数 的过程(翻转坐标轴会使得斜率 ​k 取倒数)。

可以通过几何意义,比较显然的认为复杂度是 ​O(\log \min\{a, c, n\})

欧拉函数

我们融合吧,发现并不是超融合,只是重新成为了我们本身。

定义

表示小于等于 ​n 的所有与 ​n 互质的数的个数,一般记作 ​\varphi(n) ,所以 ​\varphi(1) = 1​\varphi(p) = p - 1​p 是素数)。

重要结论

积性函数

欧拉函数是积性函数,满足当 ​a \bot b 时, ​\varphi(ab) = \varphi(a)\varphi(b) ,这个我不会证。

决定了可以用筛法求。

std::vector<int> sieve(int n){
    std::vector<int> np(n, false), prime, phi(n);
    phi[1] = 1;
    for(int i = 2;i < n;i++){
        if(!np[i]){
            prime.emplace_back(i);
            phi[i] = i - 1;
        }
        for(auto p : prime){
            if(1ll * i * p >= n) break;
            np[i * p] = true;
            if(i % p == 0){
                phi[i * p] = phi[i] * p;
                break;
            }
            phi[i * p] = phi[i] * phi[p];
        }
    }
    return phi;
} // Eular Sieve

约数性质

对于 ​n = p^k​p 为质数,则 ​\varphi(n) = p^k - p^{k - 1} ,这是极其显然的。

更显然的,由唯一分解定理,设 ​n = \prod_{i = 1}^{c}p_i^{k_i} ,其中 ​p_i 都是质数,则有 ​\varphi(n) = n\prod_{i = 1}^c \dfrac{p_i - 1}{p_i}

决定了可以用 ​O(\sqrt N) 的复杂度求,可以用 Pollard-Rho 优化。

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

欧拉反演

要注意欧拉反演仅限在国内这么叫。

内容:

n = \sum_{d | n} \varphi(d)

证明是简单的,变为狄利克雷卷积的形式就是 ​\operatorname{id} = \varphi \circ 1

应用实例

\sum_{i = 1} ^ n \sum_{j = 1} ^ n \gcd(i, j)

两种求法(这个例子之后还会提到):

\begin{aligned} &\sum_{i = 1} ^ n \sum_{j = 1} ^ n \gcd(i, j) \\ =& \sum_{k = 1} ^ n k \sum_{i = 1} ^ n \sum_{j = 1} ^ n [\gcd(i, j) = k] \\ =& \sum_{k = 1} ^ n k \sum_{i = 1} ^ {\lfloor \frac nk \rfloor} \sum_{j = 1} ^ {\lfloor \frac nk \rfloor} [\gcd(i, j) = 1] \\ =& \sum_{k = 1} ^ n k \Big(\big(2\sum_{i = 1} ^ {\lfloor \frac nk \rfloor} \varphi(i)\big) - 1\Big) \end{aligned}

我们对 ​\varphi(x) 进行前缀和,处理掉后面的一坨,可以 ​O(N) 求解。

\begin{aligned} &\sum_{i = 1} ^ n \sum_{j = 1} ^ n \gcd(i, j) \\ =& \sum_{i = 1} ^ n \sum_{j = 1} ^ n \sum_{d | \gcd(i, j)} \varphi(d) \\ =& \sum_{d = 1} ^ n \varphi(d) \sum_{i = 1} ^ {\lfloor \frac nk \rfloor} \sum_{j = 1} ^ {\lfloor \frac nk \rfloor}1 \\ =& \sum_{d = 1} ^ n \varphi(d) \lfloor \frac n{d} \rfloor ^ 2 \end{aligned}

也可以 ​O(N) 求解。

欧拉定理 & 费马小定理

毫无关联之人,却在最初的起点相遇。

费马小定理

内容:若 ​p 是素数,且 ​a\bot p ,则 ​a^{p - 1} \equiv 1 \pmod p

证明:

构造数列 ​\{a_{p - 1}\} ,其中 ​a_i = i ,先证:

\prod_{i = 1}^{p - 1} A_i \equiv \prod_{i = 1}^{p - 1} aA_i \pmod p

因为 ​a\bot p, A_i\bot p ,所以 ​aA_i \bot p

那么可以推知, ​aA_i \bmod p 两两不同,得证。

​x = (p - 1)! ,有 ​a^{p - 1}x \equiv x \pmod p

消去 ​x 得证。

欧拉定理

内容:若 ​a\bot p ,则 ​a^{\varphi(p)} \equiv 1 \pmod p

证明同上,因为 ​p​\varphi(p) 个与之互质的数,将这些数拎出来成一个序列,同上证明即可(一些简单性质可以反证)。

​p 为素数时, ​\varphi(p) = p - 1 ,得到费马小定理。

扩展欧拉定理

可以用于光速幂 / 幂塔等问题,内容如下:

a^b \equiv \begin{cases} a^{b \bmod \varphi(p)}, &a \bot p \vee b < \varphi(p) \\ a^{b \bmod \varphi(p) + \varphi(p)}, &a \not\bot p \end{cases} \pmod p

线性同余方程

你怎么跑的比我还慢?不,我套了你一圈。

定义

求形如

ax \equiv b \pmod p

​[0, p - 1] 内唯一解或所有解。

逆元解

将原式变为 ​x = a^{-1}b \bmod p

费马小定理

​a \bot p​p 为质数时,可以用费马小定理: ​a^{-1} = a^{p - 2} \bmod p

然后 ​x = a^{p - 2}b \bmod p ,同理在 ​p 不为素数时,也可以用欧拉定理。

线性求逆元

假设已经计算掉了 ​1 \sim x - 1 的所有 ​\mod p 意义下的逆元,来求 ​x 的逆元。

​a = \lfloor \dfrac p x\rfloor, b = p \bmod x ,则有 ​p = ax + b

​\mod p 意义下,写同余式,然后两边同乘 ​x^{-1}b^{-1}

ab^{-1} + x^{-1} \equiv 0 \pmod p

所以,就有 ​x^{-1} = \lfloor \dfrac p x\rfloor (p \bmod x)^{-1} \mod p ,同时显然有 ​1^{-1} \equiv 1 \pmod p

以上计算全部基于 ​p 为素数,因为 ​p 非素时,会存在 ​b^{-1} 不存在的情况。

可以做到 ​O(N)​1 \sim N 的逆元。

对于同余方程,解法同上。

扩展欧几里得解

变为 ​ax + py = b 的形式,那么无解情况与多解情况一目了然。

无解,可以用裴蜀定理;多解,跑完扩欧后直接套公式就好了。

中国剩余定理

有物不知其数,问物几何?什么几何?平面几何怎么比立体几何还难?世界上的巧合真多啊!

定义

最开始由中国人发现这个问题,所以称为中国剩余定理,简称 CRT 。

求线性同余方程组

\begin{cases} x \equiv a_1 \pmod {m_1} \\ x \equiv a_2 \pmod {m_2} \\ \vdots \\ x \equiv a_n \pmod {m_n} \\ \end{cases}

最小的一个答案。

解法

​p = \prod_{i = 1}^n m_i

对于第 ​i 个方程:

  • 计算 ​k_i = \dfrac p{m_i}
  • 计算 ​k_i​\mod m_i 意义下的逆元 ;
  • 计算 ​c_i = k_i(k_i ^{-1} \mod m_i)

答案 ​x = (\sum_{i = 1}^n a_ic_i) \mod p

证明:

显然,对于 ​i \neq j​c_j \equiv k_j \equiv 0 \pmod {m_i}​c_i \equiv k_i(k_i^{-1} \mod m_i) \equiv 1 \pmod {m_i} ,所以

\begin{aligned} x &\equiv \sum_{j = 1}^n a_jc_j &\pmod {m_i} \\ &\equiv \sum_{j = 1}^n a_jk_j(k_j^{-1} \mod m_j) & \pmod {m_i} \\ &\equiv a_ik_i(k_i^{-1} \mod m_i) &\pmod {m_i} \\ &\equiv a_i &\pmod {m_i} \end{aligned}
int exgcd(int a, int b, int &x, int &y){
	if(b == 0){
		x = 1, y = 0;
		return 0;
	}
	int t = exgcd(b, a % b, y, x);
	y -= a / b * x;
	return t;
}
int CRT(int n, std::vector<int> a, std::vector<int> m){
    int prod = 1, ans = 0;
    for(int i = 0;i < n;i++) prod *= m[i];
    for(int i = 0;i < n;i++){
        int k = prod / m[i], inv, y;
        exgcd(k, m[i], inv, y);
        ans = (ans + a[i] * m % prod * inv % prod + prod) % prod;
    }
    return ans;
}

扩展中国剩余定理

简称 exCRT ,我认为比 CRT 的一般解法好理解。

思路

首先,对于 CRT 的思路,仅限模数 ​m_i 全部互质的情况下才能使用。所以,就有了 exCRT 。

思路很简单,就是将两个同余方程合并,最后得到一个同余方程。

已知两同余方程:

\begin{cases} x \equiv a_1 \pmod {m_1} \\ x \equiv a_2 \pmod {m_2} \\ \end{cases}

可以转化成为

\begin{aligned} x = m_1p + a_1 &= m_2q + a_2 \\ m_1p - m_2q &= a_2 - a_1 \end{aligned}

无解情况一目了然,对于可行解 ​p, q ,可以将两个方程合并为

x \equiv m_1p + a_1 \pmod {\operatorname{lcm}(m_1, m_2)}

两两合并即可,两种求解复杂度相同。

卢卡斯定理

“学 ​p 进制有什么用啊?”“可以写 Lucas 定理”“ Lucas 定理又不考,学它干嘛?”“可以 Lu ”

定理内容

对于质数 ​p ,则满足:

\begin{pmatrix} a \\ b \end{pmatrix} = \begin{pmatrix} \lfloor \frac a p \rfloor \\ \lfloor \frac bp \rfloor \end{pmatrix} \begin{pmatrix} a \bmod p \\ b \bmod p \end{pmatrix}

其中若 ​n < m ,则 ​\begin{pmatrix} n \\ m \end{pmatrix} = 0

证明

​a = \sum m_ip^i, b = \sum n_ip^i ,考虑 ​(a + b) ^ p \equiv a ^ p + b ^ p \pmod p (用二项式定理可证),借此进行生成函数。

\begin{aligned} &(1 + x) ^ a \\ =& (1 + x) ^ {\sum m_i p ^ i} \\ =& \prod (1 + x ^ {p ^ i})^{m_i} \pmod p \end{aligned}

对于 ​x ^ b 求系数:

\begin{aligned} \begin{pmatrix} a \\ b \end{pmatrix} =& [x ^ b](1 + x) ^ a \\ =& [x ^ b]\prod (1 + x ^ {p ^ i})^{m_i} \\ =& \prod \begin{pmatrix} m_i \\ n_i \end{pmatrix} \end{aligned}

稍微变换一下,得证。

实用案例

对于较小的质数 ​p ,可以预处理比较小的阶乘,然后算组合数。

接着利用 Lucas 定理递归求解组合数。

i64 C(int n, int m){
	if(n < m) return 0LL;
	return fac[n] * inv[fac[m]] % p * inv[fac[n - m]] % p; 
}
i64 Lucas(int n, int m){
	if(n == 0) return 1LL % p;
	return Lucas(n / p, m / p) * C(n % p, m % p) % p;
}

威尔逊定理

除了我,就是一切。

内容

对于每个 ​p > 1 ,且 ​p 为质数,则有 ​(p - 1)! \equiv -1 \pmod p ,而且这是充要条件。

证明

不想证,不会,考得不多。

原根

原来不是每个人都有属于自己的那个根。

概念

我们考虑对模意义下的乘法域建立一个群,对于最小的 ​k 满足 ​a^k \equiv 1 \pmod p ,则称 ​k​a​\mod p 意义下的阶,记为 ​\delta _p(a)

对于原根 ​g 来讲,满足 ​\delta _p(g) = \varphi(p)

阶的性质

一般来讲,常用的就前两个。

  • ​a^k(k \in [1, \delta _p(a)]\cap\mathbb{Z}) \bmod p 两两不同。
  • ​\forall a^k \equiv 1 \pmod p ,都有 ​\delta _p(a) | k
  • ​a, b\bot p 时,则 ​\delta _p(ab) = \delta _p(a)\delta _p(b) \Leftrightarrow \delta _p(a) \bot \delta _p(b)
  • ​a\bot p 时, ​\delta _p(a^k) = \dfrac{\delta _p(a)}{\gcd(\delta _p(a), k)}

原根性质

原根判定

对于 ​p\geq 3, g \bot p ,则 ​g 为原根有充要条件:

对于 ​\varphi(p) 的所有质因数 ​k,满足 ​g ^ {\frac {\varphi(p)}k} \not\equiv 1 \pmod p

原根个数

对于 ​p 有原根,则个数为 ​\varphi(\varphi(p)) ,且满足 ​g \equiv g_0 ^k \pmod p​g_0, g 是其中的原根, ​k 是正整数。

原根存在定理

​p 存在原根,当且仅当 ​p = 2, 4, a^k, 2a^k​a 时不为 ​2 的质数, ​k 是正整数。

最小原根范围

对于最小的原根,王元证明了质数 ​p 的最小原根为 ​O(p^{0.25 + \varepsilon})

离散对数

没有根,就不会生长。

定义

取有原根的 ​p ,设其中一个原根为 ​g ,若 ​a \bot p ,可以证明有且一个 ​0\leq k < \varphi(p) 使得:

g ^ k \equiv a \pmod p

其中 ​k 记为 ​\operatorname{ind}_g a ,称为离散对数。

性质

其性质与对数相似。

​a\bot p, b\bot p​g, g_1, g_2 都是 ​p 的原根。

  • ​\operatorname{ind}_g (ab) \equiv \operatorname{ind}_g a + \operatorname{ind}_g b \pmod {\varphi(p)}
  • ​\operatorname{ind}_{g_1} a \equiv \operatorname{ind}_{g_2} a \operatorname{ind}_{g_1} g_2 \pmod {\varphi(p)}
  • ​a \equiv b \pmod p \iff \operatorname{ind}_g a = \operatorname{ind}_g b

大步小步算法

简称 BSGS(baby-step giant-step) ,可以用于求解 ​a^x \equiv b \pmod p 的问题,保证 ​a\bot p

思路

考虑将答案分为 ​x = jS - i 两部分,显然可以猜到 ​S = \sqrt p 最优。

那么就变为 ​a^{jS - i} \equiv b \pmod p ,简单变换一下, ​a^{jS} \equiv b a^i \pmod p (此处要保证 ​a^i \bot p )。

于是将 ​ba^i \mod p 存下来(小步),然后用 ​a^{jS} 去找相等,得到答案,时间复杂度为 ​O(\sqrt p \log p) (如果用的是 std::map )。

i64 BSGS(i64 a, i64 b, i64 p){
    i64 S = std::sqrt(p) + 1;
    std::map<i64, i64> mp;
    i64 G = 1;
    for(i64 i = 0;i < S;i++, G = G * a % p){
        mp[b * G % p] = i;
    }
    for(i64 i = 1, Gj = G;i <= S;i++, Gj = Gj * G % p){
        if(mp.count(Gj)){
            return i * S - mp[Gj];
        }
    }
    return -1;
}

小小拓展

对于求解 ​x^a \equiv b \pmod p​p 是质数。

对于任意一个 ​x ,如果 ​p 有原根 ​g 的话,那么可以表示为 ​x = g^k ,于是就会有 ​g ^ {ka} \equiv b \pmod p ,也可用 BSGS 求解。

我们再用 ​b = g^t ,于是 ​g^{ka} \equiv g^t \pmod p

同时取离散对数 ​ka \equiv t \pmod p ,这是线性同余方程,易于求解。

找到特解后,通解也是容易求的。

exBSGS

对于 ​a\not\bot p 的情况,无法用 BSGS 求解,于是考虑构造让 ​a' \bot p'

再次之前,可以向讲讲其它做法,其实同样也可以结合扩展欧拉定理,将答案上界设为 ​2\varphi(p) ,同样的扩展欧拉定理是一个 rho 型(想象一下好了),所以满足 ​a^{jS} \equiv ba^i \pmod p 的就只有两个可能的答案,然后一通乱搞就好了。

在考虑较简单实现的做法(上面的挺烦的):

通过取模的性质,去掉 ​d = \gcd(a, p) ,得到

​\dfrac ad a^{x - 1} \equiv \dfrac bd \pmod {\dfrac pd}

注意如果 ​d \not | b ,那么就会无解,如果对于上述情况,依旧不满足 ​a\bot \dfrac pd 那么重复以上操作,就会最终变成这个形式:

\dfrac {a ^ k} {D} a ^ {x - k} \equiv \dfrac bD \pmod {\dfrac pD}

把形式转化成 BSGS 的,

a ^ {x - k} \equiv \dfrac b {a ^ k} \pmod {\dfrac pD}

注意特判答案小于 ​k 的情况,最后的 BSGS 的答案一定要加 ​k

i64 gcd(i64 a, i64 b){ return !b ? a : gcd(b, a % b); }
i64 exgcd(i64 a, i64 b, i64 &x, i64 &y){
    if(b == 0){
		x = 1, y = 0;
		return 0;
	}
	i64 t = exgcd(b, a % b, y, x);
	y -= a / b * x;
	return t;
}
i64 BSGS(i64 a, i64 b, i64 p){
    i64 S = std::sqrt(p) + 1;
    std::map<i64, i64> mp;
    i64 G = 1;
    for(i64 i = 0;i < S;i++, G = G * a % p){
        mp[b * G % p] = i;
    }
    for(i64 i = 1, Gj = G;i <= S;i++, Gj = Gj * G % p){
        if(mp.count(Gj)){
            return i * S - mp[Gj];
        }
    }
    return -1;
}
i64 exBSGS(i64 a, i64 b, i64 p){
    b %= p;
    if(b == 1 || p == 1) return 0;
    i64 k = 0, val = 1;
    while(true){
        i64 g = gcd(a, p);
        if(g == 1) break;
        if(b % g) return -1;
        b /= g, p /= g;
        val = val * a / g % p;
        k ++;
        if(b == val) return k;
    }

    i64 inv, y;
    exgcd(val, p, inv, y);
    inv = (inv % p + p) % p;
    b = b * inv % p;
    a %= p;
    i64 res = BSGS(a, b, p);
    return res == -1 ? -1 : res + k;
}

数论分块

不平均也有如同平均一样的美,数如此,人亦此。

求解问题

用以求解

F(n) = \sum_{i = 1}^n f(i)g(\lfloor \dfrac ni \rfloor)

的类似式子,但前提是要能求出 ​f(i) 的前缀和。

其实也叫整除分块。

操作方式

因为 ​1\sim n 的区间内,存在很多相同的 ​\lfloor \dfrac ni \rfloor ,若果将这些一起考虑,就会变得很简单了。

对于 ​\lfloor \dfrac nx \rfloor = \lfloor \dfrac nk \rfloor 的最大的 ​x​\lfloor \dfrac n{\lfloor \frac nk \rfloor} \rfloor ,易证故此处不证。

\sum_{i = 1}^n f(i)g(\lfloor \dfrac ni \rfloor)

就可以以这种方式求解:

int F(int n){
    int res = 0;
    for(int l = 1, r;l <= n;l = r + 1){
        r = (n / (n / l));
        res += (pre_f[r] - pre_f[l - 1]) * g(n / l);
    }
    return res;
}

复杂度分析

考虑分类讨论:

  • ​i < \sqrt N 时,对于 ​\lfloor \dfrac ni \rfloor 显然只有 ​O(\sqrt N) 种取值;
  • ​i > \sqrt N 时,对于 ​\lfloor \dfrac ni \rfloor 一定有 ​\lfloor \dfrac ni \rfloor < \sqrt N ,所以也只有 ​O(\sqrt N) 种取值。

所以复杂度是 ​O(\sqrt N) 的。

莫比乌斯函数

听说莫比乌斯队长发明了莫队算法。

定义

一般记为 ​\mu(n) ,定义为

\mu(n) = \begin{cases} 1, &n = 1 \\ 0, &\exist d > 1, d^2 | n \\ (-1)^k, &\operatorname{otherwise.} (k = \sum_{d | n, d \in \operatorname{Prime}} 1) \end{cases}

本质上来讲 ​\mu(n) 是一个容斥。

性质

积性函数

跟欧拉函数一样,这也是个积性函数,也可以筛法求。

std::vector<int> sieve(int n){
    std::vector<int> np(n, false), prime, mu(n);
    mu[1] = 1;
    for(int i = 2;i < n;i++){
        if(!np[i]){
            prime.emplace_back(i);
            mu[i] = -1;
        }
        for(auto p : prime){
            if(1ll * i * p >= n) break;
            np[i * p] = true;
            if(i % p == 0){
                mu[i * p] = 0;
                break;
            }
            mu[i * p] = -mu[i];
        }
    }
    return mu;
} // Eular Sieve

常用性质

\sum_{d | n} \mu(d) = [n = 1]

狄利克雷卷积的形式为 ​\mu \circ 1 = \varepsilon

这也说明了 ​\mu​1 的逆元。

证明:

由唯一分解定理,设 ​n = \prod_{i = 1}^k p_i^{c_i}, m = \prod_{i = 1}^k p_i

​\sum_{d | n} \mu(d) = \sum_{d | m} \mu(d) = \sum_{i = 0}^k \begin{pmatrix} k \\ i \end{pmatrix} (-1) ^i = \big(1 + (-1)\big) ^ k

对于 ​k 仅在 ​n = 1 时为 ​0 ,得证。

莫比乌斯变换

对于两个数论函数 ​f(n), g(n) ,满足 ​f = g \circ 1 ,则有 ​g = f \circ \mu

用先前 ​\mu \circ 1 = \varepsilon 可以优雅证明,下给出一般的证明方式:

\begin{aligned} g(n) &= \sum_{d | n} \mu(d)f(\lfloor \dfrac n d \rfloor) \\ &= \sum_{d | n} \mu(d) \sum_{k | \lfloor \frac n d \rfloor} g(\lfloor \dfrac n {kd} \rfloor) \\ &= \sum_{k | n} g(k) \sum_{d | \lfloor \frac nk \rfloor} \mu(d) \\ &= g(n) \end{aligned}

最后一步是因为后面依托只有在 ​\lfloor \dfrac nk \rfloor = 1 时才为 ​1 ,否则为 ​0 ,此时 ​k = n ,所以就为 ​g(n)

所以就会有很多有意思的结论,比如先前 ​\operatorname{id} = \varphi \circ 1 ,由上变换可得 ​\varphi = \operatorname{id} \circ \mu ,有的题目会让你求一些抽象的 ​\varphi 可能会用到。

应用实例

\sum_{i = 1} ^ n \sum_{j = 1} ^ n \gcd(i, j)

还是这个老朋友,但是换一个形式,我们求这个(下面的推算,保证 ​n < m ):

\begin{aligned} &\sum_{i = 1} ^ n \sum_{j = 1} ^ m \operatorname{lcm}(i, j) \\ =& \sum_{i = 1} ^ n \sum_{j = 1} ^ m \dfrac {ij}{\gcd(i, j)} \\ =& \sum_{d = 1}^n \sum_{i = 1} ^ n \sum_{j = 1} ^ m \dfrac {ij}{d}[\gcd(i, j) = d] \\ =& \sum_{d = 1}^n \sum_{i = 1} ^ {\lfloor \frac nd \rfloor} \sum_{j = 1} ^ {\lfloor \frac md \rfloor} dij[\gcd(i, j) = 1] \\ =& \sum_{d = 1}^n d\sum_{i = 1} ^ {\lfloor \frac nd \rfloor} \sum_{j = 1} ^ {\lfloor \frac md \rfloor} ij \sum_{k | \gcd(i, j) = 1} \mu(k) \\ =& \sum_{d = 1}^n d\sum_{k = 1}^n k^2\mu(k) \sum_{i = 1} ^ {\lfloor \frac n{kd} \rfloor} \sum_{j = 1} ^ {\lfloor \frac m{kd} \rfloor} ij \\ =& \sum_{d = 1}^n d\sum_{k = 1}^n k^2\mu(k)f(\lfloor \frac n{kd} \rfloor, \lfloor \frac m{kd} \rfloor) & f(x, y) &= \dfrac{x(x + 1)y(y + 1)}4\\ =& \sum_{d = 1}^n g(\lfloor \frac nd \rfloor, \lfloor \frac md \rfloor)d & g(x, y) &= \sum_{k = 1}^n k^2\mu(k)f(\lfloor \frac n{kd} \rfloor, \lfloor \frac m{kd} \rfloor) \end{aligned}

其中 ​g(x, y) 可以用数论分块在 ​O(\sqrt N) 的时间内完成,然后再观察最后一个式子发现其实也是一个数论分块,所以也可以在 ​O(\sqrt N) 的时间内解决,所以时间复杂度为 ​O(N)

杜教筛

只要构造出打破偏见的桥梁,就能到达彼岸。

构造思想

对于数论函数 ​f(n) ,求 ​S(n) = \sum_{i = 1}^n f(i)

对于一个数论函数 ​g(n) ,一定有:

\begin{aligned} &\sum_{i = 1}^n (f\circ g)(i)\\ =& \sum_{i = 1}^n\sum_{d | i} g(d)f(\dfrac i d) \\ =& \sum_{d = 1}^n g(d) \sum_{i = 1}^{\lfloor\frac nd\rfloor} f(i) \\ =& \sum_{i = 1}^n g(i)S(\lfloor\dfrac nd\rfloor) \end{aligned}

然后我们知道,随手相减,就可以得到:

\begin{aligned} g(1)S(n) &= \sum_{i = 1}^n g(i)S(\lfloor\dfrac nd\rfloor) - \sum_{i = 2}^n g(i)S(\lfloor\dfrac nd\rfloor) \\ &= \sum_{i = 1}^n (f\circ g)(i) - \sum_{i = 2}^n g(i)S(\lfloor\dfrac nd\rfloor) \end{aligned}

如果能做到快速算 ​\sum_{i = 1}^n (f\circ g)(i) ,那么就可以数论分块算 ​\sum_{i = 2}^n g(i)S(\lfloor\dfrac nd\rfloor) 轻松解决这个问题,最后除以 ​g(1) 就是 ​S(n)

时间复杂度分析

对于数论分块我们知道,可以分为 ​\sqrt N 上下的两部分。

所以有,

\begin{aligned} T(n) &= \sum_{i = 1}^{\lfloor\sqrt N\rfloor} O(\sqrt i) + \sum_{i = 2}^{\lfloor\sqrt N\rfloor} O(\sqrt \dfrac n i) \\ &= O( \int_{0}^{\sqrt n} (\sqrt x + \sqrt \dfrac n x) \operatorname{d}x) \\ &= O(\dfrac 23 (\sqrt n) ^ {\frac 32} + 2\sqrt n \sqrt{\sqrt N}) \\ &= O(n ^ {\frac 3 4}) \end{aligned}

但是我们可以预处理其中一部分,所以还有,(下设阈值为 ​B(B \geq \sqrt N)

\begin{aligned} T(n) &= T'(B) + \sum_{i = 1}^{\lfloor \frac n B \rfloor} O(\sqrt \dfrac n i) \\ &= T'(B) + O( \int_{0}^{\frac n B} \sqrt \dfrac n x \operatorname{d}x) \\ &= T'(B) + O(2 \sqrt n \sqrt {\dfrac n B}) \\ &= O(B + 2 \dfrac n {\sqrt B}) \\ &\geq O(n ^ {\frac 2 3}) \end{aligned}

此处默认预处理复杂度为 ​O(n) ,即线性筛,此时取等条件是 ​B = n ^ {\frac 23}

实用案例

​S(n) = \sum_{i = 1}^n \mu(i)

已知 ​\mu \circ 1 = \varepsilon ,所以可以:

S(n) = 1 - \sum_{i = 2}^n S(\lfloor\dfrac nd\rfloor)

对于较大的 ​n 所对应的 ​S(n) 要用 std::mapstd::unordered_map 存一下,其它正常数论分块。

对于初学者,可以尝试手推一下用杜教筛求 ​S(n) = \sum_{i = 1}^n \varphi(i)

Min_25 筛

我虽然只能看见你最朴素的一面,却可以合成出你的全貌。

定义

与杜教筛相同,都是一种解决数论函数求和的亚线性筛法,因为由 Min_25 提出,所以叫 Min_25 筛。

一般来讲, Min_25 筛受限制较小,可以打过 Powerful Number 筛,等其他看起来很厉害的筛法。

主要思想

首先, Min_25 筛要求能够对于所有 质数 ​p ,快速求出 ​f(p ^ c) ,并且可以将数论函数 ​f 拆分成多个能够快速求出 ​f(p)完全积性函数 的和( 注意此处的完全积性函数是对于质数处的拆分 ,合数不需要考虑,因为之后需要利用该性质的只是用于计算质数 ​p​f(p) 之和)。

一个很典型的例子就是 多项式函数

主要的思想是根据 质因数 分讨,由于质数个数较多,所以把原来的求和分成 质数和合数 讨论,保证合数 最小质因子 的个数在 ​O(\dfrac {\sqrt N}{\log N}) 级别。

另一种理解方式就是考虑 埃氏筛 的过程。

​\operatorname{lpf}(n) 表示 ​n 的最小质因数, ​p_i 表示第 ​i 个质数,并且钦定 ​p_0 = 1

首先考虑一个式子 ​S(n, j) = \sum_{i = 2}^n f(i)[\operatorname{lpf}(i) > p_j]

接下来,基于质数和合数分类讨论。

对于质数部分的贡献就是 ​\big( \sum_{i = 1}^n f(i)[i \in \operatorname{Prime}] \big) - \sum_{i = 1}^j f(p_i)

对于合数部分的贡献稍微有点复杂但是也很简单,直接枚举最小质因子及其次数,那么贡献就是 ​\sum _ {k > j} \sum _ {c = 1} ^ {p_k^c \leq n} f(p_k^c)(S(\dfrac{n}{p_k^c}, k) + [c > 1])

因为 ​f(p) 的值之前已经算过了,所以这次 ​c = 1 时,是不需要算上 ​1 的贡献的,至于为什么要加 ​1 是因为 ​S 函数的 ​i 是从 ​2 开始算的。

为了方便表示,我们记 ​h(n) = \sum_{i = 1}^n f(i)[i \in \operatorname{Prime}], S_p(j) = \sum_{i = 1}^j f(p_i) ,于是就有。

S(n, j) = h(n) - S_p(j) + \sum _ {k > j} \sum _ {c = 1} ^ {p_k^c \leq n} f(p_k^c)(S(\dfrac{n}{p_k^c}, k) + [c > 1])

那么答案就是 ​S(n, 0) + f(1) ,至于为什么加 ​f(1) ,原因同上。

第一部分

现在可以考虑先算 ​h(n) ,然后 ​S_p(j) 可以线筛求出。

还是考虑构造一下,让 ​f'(n) 表示 ​f(n) 的拆分的一个 完全积性函数 ,令 ​g(n, j) = \sum_{i = 2}^n f'(i) [i \in \operatorname{Prime} \operatorname{or} \operatorname{lpf}(i) > p_j]

很显然的是 ​h(n) = g(n, |p|)

接着考虑递推计算 ​g 函数的值。

首先考虑 ​p_j^2 > n 时,由于 ​p_j 是最小质因子,所以无法造成新的贡献,因此 ​g(n, j) = g(n, j - 1)

接下来考虑 ​p_j^2 \leq n ,我们就需要在 ​g(n, j - 1) 的基础上去除以 ​p_j 为最小质因子的贡献并加上多余的减去的质数的贡献。

具体来讲,就是 ​g(n, j - 1) 中包括了 ​\lfloor \dfrac {n}{p_j} \rfloor 个含有质因子的数,由于 ​\operatorname{lpf}(i) > p_{j - 1} 的限制,所以剩余的质因子一定大于等于 ​p_j ,那么这一部分的贡献应该是 ​g(\lfloor \dfrac {n}{p_j} \rfloor, j - 1) ,但是这也包括了一部分符合条件的质数,这部分的答案是 ​\sum_{i = 1}^{j - 1} f'(p_i)

综上所述,我们可以出来一个关于 ​g 的递推式:

g(n, j) = \begin{cases} g(n, j - 1) &, p_j > n \\ g(n, j - 1) - f'(p_j)\big( g(\lfloor \dfrac {n}{p_j} \rfloor, j - 1) - \sum_{i = 1}^{j - 1} f'(p_i) \big) &, p_j \leq n \end {cases}

然后经过了注意力的考察,我们发现所有的要求求的 ​g(x, j) 一定满足 ​x = \lfloor \dfrac n k \rfloor 的形式。

很自然地,会想到数论分块,对于 ​k​\sqrt n 进行分讨:​k \leq \sqrt n 的情况,用一个 ​\operatorname{id1} 记录 ​k​k > \sqrt n 的情况,用一个 ​\operatorname{id2} 记录 ​\lfloor \dfrac nk \rfloor

由于我们只要求 ​g(n, |p|) 的值,所以 ​j 可以滚掉。

其中初值 ​g(n, 0) = \sum_{i = 2}^n f'(i)

接下来,我们只要对于所有的 ​f' 对应的 ​g 求和就可以得到 ​h 函数需要的值。

关于时间复杂度

复杂度是比较好算的

\begin{aligned} T(n) &\approx \big( \sum_{i^2\leq n} O(\dfrac{\sqrt i}{\log {\sqrt i}}) \big) + \big( \sum_{i^2 \leq n} O(\dfrac{\sqrt \frac ni}{\log \sqrt {\frac ni}}) \big) \\ &\approx O(\int _{1}^{\sqrt n} \dfrac{\sqrt \frac nx}{\log \sqrt {\frac nx}} \operatorname{dx}) \\ &= O(\dfrac {n^{\frac 34}}{\log n}) \\ \end{aligned}

第二部分

对于 ​S 函数的计算,其实已经非常简单了,就是十分暴力的枚举计算就好了。

这个复杂度依然是亚线性的,无法爆炸。

这个部分的复杂度被证明为 ​O(N^{1 - \varepsilon}) ,这个我不会证。

其实还有第二种方法,本质上是洲阁筛的第二部分,理论复杂度也为 ​O(\dfrac {N^{\frac 34}}{\log N}),但是常数巨大,不如暴力。

实用案例

首先还是考虑求 ​\sum_{i = 1}^n \mu(i)

首先 ​f(p) = -1 可以拆分为 ​f'(p) = -1 ,则 ​g(n, 0) = 1 - n ,直接套板子就行。

依然是建议自己手推 ​\sum_{i = 1}^n \varphi(i)

接下来是模板题 ​f(p ^ k) = p^k (p ^ k - 1) ,显然可以拆为 ​p^2​-p 的和,于是就有了两个 ​g ,其中 ​g(n, 0) 分别为 ​\dfrac {n(n + 1)}{2} - 1​\dfrac {n(n + 1)(2n + 1)}{6} - 1

放一下模板题的代码

#include <bits/stdc++.h>
#define endl "\n"
typedef long long i64;

const int _ = 2e5 + 10;
const int P = 1e9 + 7;

bool np[_];
i64 prs[_], sum1[_], sum2[_];
int tot = 0;
void sieve(int n){
    for(int i = 2;i <= n;i++){
        if(!np[i]) prs[++ tot] = i;
        for(int j = 1;j <= tot && prs[j] * i <= n;j++){
            np[prs[j] * i] = true;
            if(i % prs[j] == 0) break;
        }
    }
    for(int i = 1;i <= tot;i++){
        sum1[i] = (sum1[i - 1] + prs[i]) % P;
        sum2[i] = (sum2[i - 1] + prs[i] * prs[i] % P) % P;
    }
    // 线筛求 p 和 p^2 的和
    return ;
}

i64 n, sq;
int id1[_], id2[_], cnt = 0;
i64 g1[_], g2[_], w[_];
i64 S1(i64 x){ return x * (x + 1) / 2 % P; }
i64 S2(i64 x){ return x * (x + 1) % P * (2 * x % P + 1) % P * 166666668 % P; }
//    1/6 = 166666668 (mod 10^9 + 7)
i64 id(i64 x){
    if(x <= sq) return id1[x];
    return id2[n / x];
} // 对于两类 id 进行分讨
i64 S(i64 x, int j){
    if(prs[j] > x) return 0;
    i64 res = ((g2[id(x)] - g1[id(x)] + P) % P - (sum2[j] - sum1[j] + P) % P + P) % P;
    for(int i = j + 1;i <= tot && prs[i] * prs[i] <= x;i++){
        for(i64 c = 1, sp = prs[i];sp <= x;sp *= prs[i], c ++){
            res = (res + sp % P * (sp % P - 1) % P * (S(x / sp, i) + (c > 1) % P)) % P;
        } // 枚举次数
    } // 枚举最小质因数
    return res;
}

void min_25(){
    sq = std::sqrt(n);
    sieve(sq);
    // 最小质因数最大只有 \sqrt n

    for(i64 l = 1, r;l <= n;l = r + 1){
        r = n / (n / l);
        w[++ cnt] = n / l;
        g1[cnt] = S1(n / l % P) - 1;
        g2[cnt] = S2(n / l % P) - 1;
        // 求 g(x, 0)
        if(n / l <= sq) id1[n / l] = cnt;
        else id2[n / (n / l)] = cnt;
        // 对于两类 id 进行分讨
    }

    for(int i = 1;i <= tot;i++){
        for(int j = 1;j <= cnt && prs[i] * prs[i] <= w[j];j ++){
            int k = id(w[j] / prs[i]);
            g1[j] = (g1[j] - prs[i] * (g1[k] - sum1[i - 1] + P) % P + P) % P;
            g2[j] = (g2[j] - prs[i] * prs[i] % P * (g2[k] - sum2[i - 1] + P) % P + P) % P;
        } // 递推 + 滚动 求 g(n, |p|)
    }

    std::cout << (S(n, 0) + 1) % P << endl;
    // 一般来讲 f(1) = 1 ,具体情况具体对待
}

signed main(){
    std::cin.tie(nullptr) -> sync_with_stdio(false);
    std::cin >> n;
    min_25();
    return 0;
}

模拟赛一定要有题!