数论学习笔记

· · 个人记录

一、整除分块

1.整除分块的思想:

一个数字 n 被另一个正整数整除,得到的商只可能有根号 n 种。

简单证明:

把数字分为小于 \sqrt{n} 和大于 \sqrt{n} 两种。

所有小于 \sqrt{n} 的数字不重复时最多除出来 \sqrt{n} 个数字,所有大于 \sqrt{n} 的数字最多除出来 \sqrt{n} 个数字,所以极端情况下最多有 2\sqrt{n} 个数字。

2.相同的商,除数一定是连续的一段。例如数字 100 被 34 ~ 50 整除,得到的商都是 2.

3.假设某一段的左端点为 l,右端点为 r,被除数为 n,请根据 nl 的值算出 r 的值:

易知 r = n / (n / l);

4.写出循环,求出所有区间的左右端点,以及这个区间对应的商。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll l = 1, n, r;
int main()
{
    cin >> n;
    while (l <= n)
    {
        r = n / (n / l);
        cout << l << " " << r << " " << n / l << endl;
        l = r + 1;
    }
    return 0;
}

洛谷习题:P2261 余数求和。

思路:1.求余没有规律,试图找一些有规律的东西,可以得如下等式:

等式中是否包含题目中所求的东西?若想得到题目中求的东西,应该如何构造? ## 二、组合数 $n$ 个数字里选 $m$ 个元素组成一个集合,方案数为 $C_n^m$,请用阶乘运算表示 $C_n^m$ 的值。 假设 $n,m$ 的数据范围为 $10^5$,如何求出对应的 $C_n^m$?(答案对 $10^9+7$ 取模) 若题目中多次询问 $C_n^m$ 的值,应该如何计算? 洛谷习题:T245093 【模板】组合数 ```cpp #include <bits/stdc++.h> using namespace std; #define ll long long const int maxn = 1e5 + 5; const int mod = 1e9 + 7; ll t, n, m, f[maxn], inv[maxn], finv[maxn]; void init() { f[0] = 1; for (int i = 1; i <= 1e5; i++) { f[i] = f[i - 1] * i % mod; } inv[1] = 1; for (int i = 2; i <= 1e5; i++) { inv[i] = (mod - mod / i) * inv[mod % i] % mod; } finv[0] = finv[1] = 1; for (int i = 2; i <= 1e5; i++) { finv[i] = finv[i - 1] * inv[i] % mod; } } ll C(ll n, ll m) { return f[n] * finv[m] % mod * finv[n - m] % mod; } int main() { init(); cin >> t; while (t--) { cin >> n >> m; cout << C(n, m) << endl; } return 0; } ``` ## 帕斯卡恒等式 $C_n^k = C_{n - 1}^{k - 1} + C_{n - 1}^{k}

使用递推的算法来看这个等式。从前 n 个物品里面选取其中的 k 个,有两种情况:

1.从前 n - 1 个物品里面选取其中的 k 个,第 n 个物品被忽略,在这种情况有 C_{n - 1}^k 种方式。

2.我们取用了第 n 个物品,那么从前 n - 1 个物品里面,便要选取其中的 k - 1 个,这种情况有 C_{n - 1}^{k - 1} 种方式。

加起来便是从前 n 个物品里选取其中 k 个总方式数。

杨辉三角

1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
...

杨辉三角的第 n 行第 m 列是几?

C_{n - 1}^{m - 1},因为 C_n^x 中合法的 x 在 0 ~ n 之间,共有 n+1 种取值,和杨辉三角的第 n + 1 行对应上。

C_n^0 + C_n^1 + ... + C_n^n = ?

答: 2^n

C_n^0 + C_n^2 + C_n^4 + ... + C_n^n = ?

答:2^{n-1}

注:可用杨辉三角帮助证明。

三、异或

给定一棵树,每个节点上有一只蚂蚁。每只蚂蚁上有一只蚂蚁。每只蚂蚁都可以向上爬一格或者不爬(根节点的蚂蚁不能爬)。且每个蚂蚁都有一个属性值,若多只蚂蚁在同一个节点,则产生的快乐值是这个结点的所有蚂蚁属性值的异或。

求所有方案的所有快乐值之和。

思路:

按位算贡献。

每个节点都有多个孩子节点,这几个孩子节点与当前节点中有 x 个人二进制下第 m 位为 1,有 y 个人二进制下第 m 位为 0。我们可以进行计算使第 m 位异或后为 1 的方案数有:

$b=2^y$ (选不选 0 对结果无关,所以每一个都有选或不选两种情况) 属性值的方案数为 $2^{x-1} \times 2^y \times 2^m$。 需要再与其他无关的节点相乘,结果为 $2^{x-1} \times 2^y \times 2^m \times 2^{n-(x+y)}$。 最后进行相加即可。 ## 四、因子预处理 洛谷习题:T244840 序列排序 难点:如何判断是否为 ```yes```(什么情况说明是 ```no```) 思路: 1.当某两个数不互质且大的数字在前面是 ```no``` (因为大的数字总要交换到后面去) 2.假设所有数字都是 2 的倍数,那么什么情况下是 ```no```?所有数字都是 3 的倍数,什么情况下是 ```no```? 实现 1:邻接表法。对于每个数字,将它的所有因子拿出来(需要因子预处理)。再汇总地看所有因子考虑这些因子的倍数的位置是否递增。 ```cpp #include <bits/stdc++.h> using namespace std; const int maxn = 1e5 + 5; vector<int>yue[maxn]; vector<int>ans[maxn]; int a, flag = 1, n, t; void cal(){ flag = 1; for(int i = 0; i < maxn; i++) ans[i].clear(); cin >> n; for(int i = 1; i <= n; i++){ cin >> a; for(int j = 0; j < yue[a].size(); j++) ans[yue[a][j]].push_back(a); } for(int i = 2; i <= n; i++) for(int j = 1; j < ans[i].size(); j++) if(ans[i][j] < ans[i][j-1]) flag = 0; if(flag) puts("Yes"); else puts("No"); } int main(){ for(int i = 2; i < maxn; i++) for(int j = i; j < maxn; j += i) yue[j].push_back(i); cin >> t; while(t--){ cal(); } } ``` 实现 2:对于每个因子,将它的所有倍数拿出来。考虑这些倍数的位置是否递增。 ```cpp #include <bits/stdc++.h> using namespace std; int main(){ int t, n; cin >> t; while(t--){ cin >> n; vector<int>a(n+1), pos(n+1); for(int i = 1; i <= n; i++){ cin >> a[i]; pos[a[i]] = i; } bool flag = true; for(int i = 2; i <= n; i++){ for(int j = 2 * i; j <= n; j += i){ if(pos[j] < pos[j - i]) flag = false; } } if(flag) cout << "Yes" << endl; else cout << "No" << endl; } } ``` ## 五、筛法 如果我们想要知道小于等于 $n$ 有多少个素数呢? 一个自然的想法是对于小于等于 $n$ 的每个数进行一次质数检验。这种暴力的做法显然不能达到最优复杂度。 ### 埃氏筛: 考虑这样一件事情:对于任意一个大于 $1$ 的正整数 $n$,那么它的 $x$ 倍就是合数($x>1$)。利用这个结论,我们可以避免很多次不必要的检测。 如果我们从小到大考虑每个数,然后同时把当前这个数的所有(比自己大的)倍数记为合数,那么运行结束的时候没有被标记的数就是素数了。 ```cpp int Eratosthenes(int n) { int p = 0; for (int i = 0; i <= n; ++i) is_prime[i] = 1; is_prime[0] = is_prime[1] = 0; for (int i = 2; i <= n; ++i) { if (is_prime[i]) { prime[p++] = i; // prime[p]是i,后置自增运算代表当前素数数量 if ((long long)i * i <= n) for (int j = i * i; j <= n; j += i) // 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从 i // 的倍数开始,提高了运行速度 is_prime[j] = 0; // 是i的倍数的均不是素数 } } return p; } ``` 可以证明这个方法的时间复杂度是 $O(n\log n) $ 的。 ### 线性筛: 埃氏筛法仍有优化空间,它会将一个合数重复多次标记。有没有什么办法省掉无意义的步骤呢?答案是肯定的。 如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 $O(n)$ 了。 ```cpp void init() { for (int i = 2; i < MAXN; ++i) { if (!vis[i]) { pri[cnt++] = i; } for (int j = 0; j < cnt; ++j) { if (1ll * i * pri[j] >= MAXN) break; vis[i * pri[j]] = 1; if (i % pri[j] == 0) { // i % pri[j] == 0 // 换言之,i 之前被 pri[j] 筛过了 // 由于 pri 里面质数是从小到大的,所以 i 乘上其他的质数的结果一定也是 // pri[j] 的倍数 它们都被筛过了,就不需要再筛了,所以这里直接 break // 掉就好了 break; } } } } ``` ## 六、快速幂 快速幂,二进制取幂(Binary Exponentiation,也称平方法),是一个在 $O(\log n)$ 的时间内计算 $a^n$ 的小技巧,而暴力的计算需要 $O(n)$ 的时间。而这个技巧也常常用在非计算的场景,因为它可以应用在任何具有结合律的运算中。其中显然的是它可以应用于模意义下取幂、矩阵幂等运算,我们接下来会讨论。 ### 算法描述: 计算 $a$ 的 $n$ 次方表示将 $n$ 个 $a$ 乘在一起:$a^n=\begin{matrix}\underbrace{a\times a \times \cdots \times a}\\n\text{个}a\end{matrix}$。然而当 $n$ 太大的时侯,这种方法就不太适用了。不过我们知道:$a^{b+c} =a^b \cdot a^c, a^{2b}=a^b\cdot a^b=(a^b)^2$。二进制取幂的想法是,我们将取幂的任务按照指数的 **二进制表示** 来分割成更小的任务。 首先我们将 $n$ 表示为 2 进制,举一个例子: $$3^{13}=3^{(1101)_2}=3^8\cdot 3^4 \cdot 3^1$$ 因为 $n$ 有 $\left\lfloor{\log_2 n}\right\rfloor + 1$ 个二进制位,因此当我们知道了 $a^1,a^2,a^4,a^8,\cdots,a^{\left\lfloor{\log_2 n}\right\rfloor}$ 后,我们只用计算 $O(\log n)$ 次乘法就可以计算出 $a^n$。 于是我们只需要知道一个快速的方法来计算上述 3 的 $2^k$ 次幂的序列。这个问题很简单,因为序列中(除第一个)任意一个元素就是其前一个元素的平方。举一个例子: $$3^1 =3 $$ $$3^2 = (3^1)^2 = 3^2 = 9$$ $$3^4=(3^2)^2 = 9^2=81$$ $$3^8 = (3^4)^2=81^2=6561$$ 因此为了计算 $3^{13}$,我们只需要将对应二进制位为 1 的整系数幂乘起来就行了: $$3^{13}=6561\cdot81\cdot3=1594323$$ 将上述过程说得形式化一些,如果把 $n$ 写作二进制为 $(n_tn_{t-1}\cdots n_1n_0)_2$,那么有: $$n=n_t2^t+n_{t-1}2^{t-1}+n_{t-2}2^{t-2}+\cdots+n_12^1+n_02^0$$ 其中 $n_i \in \{0,1\}$($n_i$ 是 0 或 1)。那么就有: $$a^n=a^{n_t2^t+\cdots+n_02^0}=a^{n_02^0} \times a^{n_12^1} \times \cdots \times a^{n_t2^t}$$ 根据上式我们发现,原问题被我们转化成了形式相同的子问题的乘积,并且我们可以在常数时间内从 $2^i$ 项推出 $2^{i+1}$ 项。 这个算法的复杂度是 $O(\log n)$ 的,我们计算了 $O(\log n)$ 个 $2^k$ 次幂的数,然后花费 $O(\log n)$ 的时间选择二进制为 1 对应的幂来相乘。 ### 简单来说: 想要得到 $a^n$ 的值,可以先算出 $a^{\left\lfloor\frac{n}{2}\right\rfloor}$ 的值。 1.如果 $n$ 是偶数,则 $a_n=a^{\left\lfloor\frac{n}{2}\right\rfloor}\cdot a^{\left\lfloor\frac{n}{2}\right\rfloor}$。 2.如果 $n$ 是奇数,则 $a_n=a^{\left\lfloor\frac{n}{2}\right\rfloor}\cdot a^{\left\lfloor\frac{n}{2}\right\rfloor}\cdot a$。 于是我们每次都只需要算出 $a^{\left\lfloor\frac{n}{2}\right\rfloor}$ 的值,时间复杂度 $O(\log n)$。 **实现 1:** 可以直接按照上述递归方法实现: ```cpp long long binpow(long long a, long long b) { if (b == 0) return 1; long long res = binpow(a, b / 2); if (b % 2) return res * res * a; else return res * res; } ``` **实现 2:** 第二种实现方法是非递归式的。它在循环的过程中将二进制位为 1 时对应的幂累乘到答案中。尽管两者的理论复杂度是相同的,但第二种在实践过程中的速度是比第一种更快的,因为递归会花费一定的开销。 ```cpp long long binpow(long long a, long long b) { long long res = 1; while (b > 0) { if (b & 1) res = res * a; a = a * a; b >>= 1; } return res; } ``` ## 七、欧拉函数 $\varphi(x)$ 为小于等于 $x$ 的所有正整数中与 $x$ 互质的数字的个数,即欧拉函数研究的问题。 #### 想一想 30000 以内有多少个和 30000 互质的数字? #### 欧拉函数的计算 假设 $x$ 的素因子分解为 $x = p_1^{k_1} \times p_2^{k_2} \times \cdots \times p_n^{k_n}

\varphi(x)= x \times (1-\frac{1}{p_1}) \times (1-\frac{1}{p_2})\times\cdots\times(1-\frac{1}{p_n})

试证明理解

计算一下 \varphi(10007)?(提示:10007 是一个质数)

计算一下 \varphi(30000)

欧拉函数的递推

考虑某一个数字 a=xy,其中 ya 的最小质因子。

那么 \varphi(xy) = \begin{cases}\varphi(x)\times \varphi(y)& \text{if\ gcd}(x,y)=1, \\ \varphi(x)\times y & \text{if\ } x \text{ 是 } y \text{ 的倍数}.\end{cases}

试证明。

想一想

递推的前提是 ya 的最小质因子,这个条件有什么用呢?

代码

bool vis[maxn];
int prime[maxn], f[maxn];
void init() {
    int cnt = 0; // 目前已经发现的素数个数
    for(int i = 2; i < maxn; i++) {
        if(!vis[i]){
            prime[++cnt] = i;
            f[i] = i - 1;
        }
        for(int j = 1; j <= cnt && i * prime[j] < maxn; j++) {
            vis[prime[j] * i] = 1;
            if(i % prime[j] == 0){
                f[prime[j] * i] = f[i] * prime[j];
                break;
            }else{
                f[prime[j] * i] = f[prime[j]] * f[i];
            }
        }
    }
}

洛谷习题:P2158 仪仗队

位于 a,b 的人需要满足什么条件才能被看见?试写出暴力代码。