数论定理以及C++实现

该笔记是在大二时学习离散数学这门课程时做的,同时也看了其他有关的书,并用C++来验证这些定理。😄

素数-合数

a,ba,b是两个整数,且b0b\ne 0。如果存在整数 cc 使得 a=bca=bc,则称 aabb 整除 或者 bb 整除 aa,记作 bab\mid a ,不整除记作 bab\nmid a

任何正整数都有两个正因子:1和它本身,称作它的平凡因子,除了平凡因子外的因子称作 真因子

  • 如果正整数 aa 大于 1 且只能被1和它本身自己整除,则称 aa素数质数;如果 aa 大于1且不是素数,则 aa合数

  • a>1a>1 是合数当且仅当 a=bca=bc,其中 1<b<a,1<c<a1<b<a,1<c<a

算术基本定理

a>1a>1,则

a=p1r1×p2r2×p3r3×...×pkrka=p_1^{r_1}\times p_2^{r_2}\times p_3^{r_3}\times ... \times p_k^{r_k}

其中 p1,p2,...,pkp_1,p_2,...,p_k 是不同的素数,r1,r2,...rkr_1,r_2,...r_k正整数。定理中的表达式称作整数aa素因子分解。比如

30=2×3×588=23×1199099=32×7×112×131024=21030=2\times 3\times 5\\ 88=2^3\times 11\\ 99099=3^2\times 7\times 11^2\times 13\\ 1024=2^{10}

若所有的ri=0r_i=0,则 a=1a=1。显然,aa的素因子只能含有 aa 中的素因子,即有下面推论:

推论:设 a=p1r1×p2r2×p3r3×...×pkrka=p_1^{r_1}\times p_2^{r_2}\times p_3^{r_3}\times ... \times p_k^{r_k} ,其中 p1,p2,...,pkp_1,p_2,...,p_k 是不同的素数,r1,r2,...rkr_1,r_2,...r_k正整数

则正整数 ddaa因子 的充要条件是 d=p1s1×p2s2×...×pkskd=p_1^{s_1}\times p_2^{s_2}\times ...\times p_k^{s_k}。其中 0siri,i=1,2,...,k0\le s_i\le r_i,i=1,2,...,k

例子:9909999099 有多少个正因子

解:由于 99099=32×7×112×1399099=3^2\times 7\times 11^2\times 13,有推论可知, d=3s1×7s2×11s3×13s4d=3^{s_1}\times 7^{s_2}\times 11^{s_3}\times 13^{s_4}

s1[0,2],s2[0,1],s3[0,2],s4[0,1]s_1\in [0,2],s_2\in [0,1],s_3\in [0,2],s_4\in [0,1],因此 9909999099 的正因子个数为 3×2×3×2=363\times 2\times 3\times 2=36(排列组合)

素数定理

limn+π(n)n/lnn=1\lim_{n\to +\infty} \frac{\pi(n)}{n/\ln n}=1

检查一个正整数是否为素数称作 素数测试

  • 如果 aa 是一个合数,则 aa 必有一个 小于等于 a\sqrt a 的 真因子

    推论:如果 aa 是一个合数,则 aa 必有一个 小于等于 a\sqrt a 的 素因子

  • 埃拉托斯特尼(Eratosthene)素数筛选法

    要求某一个正整数 aa 在一定范围内的所有素数,从某个素数 b1b_1 开始,从头到尾删去 bb 能够整除 aa 的所有正整数,接着在从第二个素数 b2b_2 开始重复上面的步骤,直到在 aa 的范围内只剩下素数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef unsigned long long ull;
const ull XX = 1e9;
bool del[XX + 1];
void eratosthene(ull NUM) {
// 注意 最终要遍历到 NUM
for (ull n = 2; n <= NUM; n++)
// 表示还没有被标记删除
if (!del[n]) // 删去 n 的 [整数倍] 的其他数字
// 注意这里一开始就是 i=n*n,目的是保留最初了 单位质数n
for (ull i = n * n; i <= NUM; i += n)// 打表标记删除
del[i] = true;
}
int main() {
ull n = 1000000000;
eratosthene(n);
for (int i = 2; i <= n; i++) if (!del[i]) cout << i << " ";
}

nn 是合数时, 2n12^n-1一定是合数

pp 是素数时,称 2p12^p-1 的数为 梅森素数

线性筛

复杂度为 O(n)O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>
#include<string.h>
using namespace std;

const int M=100001;
int prime[M],isprime[M];

int n,cnt;
void fastprime(){
memset(isprime,1,sizeof(isprime));
isprime[1]=0;
for(int i=2;i<=n;i++){
if(isprime[i]) prime[cnt++]=i;
for(int j=0;j<=cnt && i*prime[j]<=n;j++){
isprime[i*prime[j]]=0;
if(i%prime[j]==0)break;
}
}
}

int main(){
cin>>n;
fastprime();
for(int i=0;i<cnt;i++)
cout<<prime[i]<<" ";
cout<<endl;
return 0;
}
自然数分解为素数的乘积
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
#include<string.h>
using namespace std;

const int M=100001;
int prime[M],isprime[M];

int n,cnt;
void fastprime(){
memset(isprime,1,sizeof(isprime));
isprime[1]=0;
for(int i=2;i<=n;i++){
if(isprime[i]) prime[cnt++]=i;
for(int j=0;j<=cnt && i*prime[j]<=n;j++){
isprime[i*prime[j]]=0;
if(i%prime[j]==0)break;
}
}
}
int main(){
cin>>n;
fastprime();
for(int i=0;i<cnt;i++){
while(n%prime[i]==0){
cout<<prime[i]<<" ";
n/=prime[i];
}
}
cout<<endl;
return 0;
}

同余

a与b模m同余 ab(modm)a\equiv b(\mod m),即: $a \mod m=b \mod m $

同余关系是等价关系

  • 自反性. a(amodm)a\equiv (a\mod m)
  • 传递性. ab(modm),bc(modm)ac(modm)a\equiv b(\mod m),b\equiv c(\mod m)\Rightarrow a\equiv c(\mod m)
  • 对称性. ab(modm)ba(modm)a\equiv b(\mod m)\Rightarrow b\equiv a(\mod m)

模算术运算。ab(modm)a\equiv b(\mod m)cd(modm)c\equiv d(\mod m),则

a±cb±d(modm)a×cb×d(modm)akbk(modm)a\pm c\equiv b\pm d(\mod m)\\ a\times c\equiv b\times d(\mod m)\\ a^k\equiv b^k(\mod m)\\

c,mc,m 互为素数,则 ab(modm)c×ac×b(modm)a\equiv b(\mod m) \Leftrightarrow c\times a\equiv c\times b(\mod m)

欧拉定理和费马小定理

欧拉定理:设 aann 互为素数,则 af(n)1(modn)a^{f(n)}\equiv 1(\mod n)

pp 为素数时,f(n)=p1f(n)=p-1,于是得到费马小定理

pp 是素数,aapp 互素,则 ap11(modp)a^{p-1}\equiv 1(\mod p) 或者另外一种形式 apa(modp)a^p\equiv a(\mod p)

利用费马小定理,可以在不用因子分解的情况下就可以判断一个数是否为 合数。比如 99,取 a=2a=2,计算 2914(mod9)2^{9-1}\equiv 4(\mod 9),对比费马小定理可知 9 是合数。

产生均匀伪随机数–线性同余法LCG

这种产生的随机数属于 统计学伪随机数生成器

产生伪随机数递推公式:

xn=(a×xn1+c)modm,n=1,2,...x_n=(a\times x_{n-1}+c)\mod m ,n=1,2,...

其中 x0x_0 为初始时的种子数,若 c=0c=0,上述递推式成为 乘同余数。此时取 m=2311,a=75,c=0m=2^{31}-1,a=7^5,c=0 的乘同余法是最常用的均匀伪随机数发生器。aa 还可以取其他数值 48271

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<bits/stdc++.h>
using namespace std;
size_t __SEED=1;
void my_srand(size_t S){ __SEED=S; }
size_t my_rand(){
// m = (1<<31)-1 a = 7^5
int m=0x7fffffff,c=0,a=16807;
__SEED=(a*__SEED+c) % m;
return __SEED;
}
int main() {
my_srand(time(NULL));
for(int i=0;i<10;i++) cout<<my_rand()<<endl;
}

快速幂

二分思想–递归法

f(a,b)=abf(a,b)=a^b,递归式如下:

ab=f(a,b)={1,b=0a×f(a,b1),bf(ab2,b2)×f(ab2,b2),ba^b=f(a,b)= \begin{cases} 1&,b=0\\ a\times f(a,b-1)&,b为奇数\\ f(a^{\frac{b}{2}},\frac{b}{2})\times f(a^{\frac{b}{2}},\frac{b}{2})&,b为偶数\\ \end{cases}

1
2
3
4
5
6
7
using ll=long double;
ll quick_pow(int a,int b){
if(b==0)return 1;
if(b&1)return a*quick_pow(a,b-1);
ll m=quick_pow(a,b/2);
return m*m;
}
二进制位分解–迭代法

aba^bbb 的二进制位(取其中有效的位: 1) 。指数的变化位 ...6432168421... 64\quad 32\quad 16\quad 8\quad 4\quad 2\quad 1,每次翻倍

b=13=(1101)2=23+22+20=8+4+1b=13=(1101)_2=2^3+2^2+2^0=8+4+1

a13=a8+4+1=a23+22+20=a8×a4×a1a^{13}=a^{8+4+1}=a^{2^3+2^2+2^0}=a^8\times a^4\times a^1

1
2
3
4
5
6
7
8
9
10
using ll=long double;
ll quick_pow(ll a,int b){
ll ans=1;
while(b){ // 直到b=0
if(b&1) ans=ans*a; // 有效位1
a=a*a; // 指数 b=2*i
b>>=1; // b右移取有效位1
}
return ans;
}
快速幂-取模运算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <cstdio>
#include <cstdlib>
int b, p, k;
typedef unsigned long long ll;
unsigned long long qm() {
if (p == 0) return 1 % k;
ll ans = 1, bb = b, pp = p;
bb %= k;
while (pp) {
if (pp & 1) ans = (ans * bb) % k;
bb = (bb * bb) % k;
pp >>= 1;
}
return ans;
}
int main() {
scanf("%d%d%d", &b, &p, &k);
unsigned long long ans = qm();
printf("%d^%d mod %d=%ld\n", b, p, k, ans);
return 0;
}

欧拉函数

欧拉函数(Euler’s totient function),即 ϕ(x)\phi(x) ,表示的是 小于等于 nnnn 互质(公约数只有1) 的数的个数。

ϕ(n)=n(11p1)(11p2)(11p3)(11pk)\phi(n)=n(1-\frac{1}{p_1})(1-\frac{1}{p_2})(1-\frac{1}{p_3})\cdots (1-\frac{1}{p_k})

也可以写成

ϕ(n)=np11p1p21p2pk1pk\phi(n)=n\frac{p_1-1}{p_1}\frac{p_2-1}{p_2}\cdots\frac{p_k-1}{p_k}

其中 p1,p2,pkp_1,p_2,p_k 为分解出来的不同的 质因数。

  • ϕ(1)=1\phi(1)=1

  • nn质数 的时候,显然有 ϕ(n)=n1\phi(n)=n-1 (也就是除了 nn 本身)

  • 欧拉函数的递推公式: 令 ppnn 的最小质因数,如果 p2p^2 能整除 nn ,那么 ϕ(n)=Φ(np)(p1)\phi(n)=\Phi(\frac{n}{p})(p-1)

求某个数的欧拉函数值如下,复杂度为 O(n)O(\sqrt{n})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
#include<cmath>
using namespace std;
int phi(int n){
int ans=n;
int cnt=sqrt(n+0.5)+1;
for(int i=2;i<cnt;i++){
if(n%i==0){
// // 由 ans=ans* (i-1) / i 化简
ans-=ans/i;
while(n%i==0)n/=i;
}
if(n==1)break;
}
if(n>1)ans-=ans/n;
return ans;

}

int main(){
int n;
cin>>n;
cout<<phi(n)<<endl;
return 0;
}
线性筛求欧拉函数表

复杂度为 O(n)O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
#include<cstring>
using namespace std;

const int N=100001;
bool isprime[N];
int prime[N];
int phi[N];
int cnt;
void fastphi(int n){
memset(isprime,1,sizeof(isprime));
isprime[0]=isprime[1]=0;
phi[1]=1;
for(int i=2;i<=n;i++){
if(isprime[i]) {
prime[cnt++]=i;
phi[i]=i-1;
}
// prime[j] 为最小质因数
for(int j=0;j<=cnt && i*prime[j]<=n;j++){
isprime[i*prime[j]]=0;
if(i%prime[j]==0) {
// 欧拉函数递推式
phi[i*prime[j]]=phi[i] * prime[j];
break;
}
// 欧拉函数递推式
phi[i*prime[j]]=phi[i] * (prime[j]-1);
}
}
}


int main(){
int n;
cin>>n;
fastphi(n);
for(int i=0;i<cnt;i++)
cout<<phi[i]<<" ";
cout<<endl;
return 0;
}

霍纳规则

适用范围多项式求值问题

简介:假设有 n2n+2 个实数 a0,a1,...,ana_0,a_1,...,a_nxx 的序列,要对多项式 Pn(x)=anxn+an1xn1+...+a1x+a0P_n(x)= a_nx_n+a_{n-1}x_{n-1}+...+a_1x+a_0 求值,直接方法是对每一项分别求值,并把每一项求的值累加起来,这种方法十分低效,它需要进行 n+(n1)+...+1=n(n+1)/2n+(n-1)+...+1=n(n+1)/2 次乘法运算和 nn 次加法运算。有没有更高效的算法呢?答案是肯定的。

通过如下变换我们可以得到一种非常快的算法,即 Pn(x)=anxn+an1xn1+...+a1x+a0=((...(((anx+an1)x+an2)x+an3)...)x+a1)x+a0P_n(x)= a_nx_n+a_{n-1}x_{n-1}+...+a_1x+a_0=((...(((a_nx +a_{n-1})x+a_{n-2})x+ a_{n-3})...)x+a1)x+a0,这种求值的安排我们称为 霍纳法则

C++ code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
using namespace std;

double horner_rule(double *A,int n,double x){
double ans=0;
// 从 n 开始
for(int i=n-1;i>=0;i--){
ans=ans*x+A[i];
}
return ans;
}

int main(){
double A[]={1,2,3,4};
double x=2;
cout<<horner_rule(A,sizeof(A)/sizeof(A[0]),x)<<endl;
return 0;
}
// 49

排列组合

(1) P(n,r)=n!(nr)!.nr(2) C(n,r)=n!r!(nr)!.nr(1)\ P(n,r) = \frac{n!}{(n-r)!}.n\ge r\\ (2)\ C(n,r) = \frac{n!}{r!*(n-r)!}.n\ge r\\

推论: 设 n,rn,r 为正整数,则

C(n,r)=nrC(n1,r1)C(n,r)=C(n,nr)C(n,r)=C(n1,nr)+C(n1,r)C(n,r)=\frac{n}{r}C(n-1,r-1)\\ C(n,r)=C(n,n-r)\\ C(n,r)=C(n-1,n-r)+C(n-1,r)

例1:从 S={1,2,...,n}S=\{1,2,...,n\} 中选择kk个不相邻的数,有多少种方法?

C(n(k1),k)=C(nk+1,k)C(n-(k-1),k)=C(n-k+1,k)

图论

握手定理

  • 在任何图中,所有顶点的度数之和等于边数的2倍

vVdeg(v)=2E\sum_{v\in V}deg(v)=2|E|

  • 在任何有向图中,所有顶点的 入度 之和等于所有顶点的 出度 之和,等于边数 E|E|

平面图

  • 一个有限平面图中,面的次数之和等于边数的两倍。

  • GG 是连通的平面图,且每条边的次数至少为 k(k3)k(k\ge 3),则平面图的边数 ee 和顶点数 vv 关系 :ekk2×(v2)e\le \frac{k}{k-2}\times(v-2)

  • GGv(v3)v(v\ge 3)ee 条边的简单平面图,则:e3v6e\le3v-6

无向树-树

  • 树:=1e=n1边数=顶点数-1\Leftrightarrow e=n-1

  • GGnnmm条边的无向连通图,则 mn1m\ge n-1

  • 设有完全 mm 叉树,其树叶数为 tt,分支点数(内点+树根)为 ii ,则 (m1)×i=t1(m-1)\times i=t-1

    • 证明:mi=(t+i)1m*i=(t+i)-1
  • GG 是有 nn 个结点,mm 条边的连通图,必须删去 GGmn+1m-n+1 条边,才能确定 GG 的一棵 生成树

最小的生成树

为了造出一棵最小生成树,我们从最小边权的边开始,按边权从小到大依次加入,如果某次加边产生了环,就扔掉这条边,直到加入了 n1n-1 条边,即形成了一棵树。

最小生成树一定是含有 nn 个顶点和 n1n-1 条边。
所以可以得到一个推论:
n1n-1 条边 <=> 11 棵最小生成树
n2n-2 条边 <=> 22 棵最小生成树(将 n1n-1 分为 n1n-111 条边)
n3n-3 条边 <=> 33 棵最小生成树
\vdots
nkn-k 条边 <=> kk 棵最小生成树。其实可以这么理解:从 n-1 条边得到了一颗最小生成树之后,根据 树 的定义,我们知道,若从一颗最小生成树中删除 ii 条边后,会形成多个树,也就是森林;而且每一个子树也是最小生成子树。

注意: 一个顶点也可与作为一颗最小生成树。比如洛谷有一题 p1195

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <algorithm>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;

#define INF 0x7fffffff
#define N 100001

int n, m, k, cnt, ans;
int f[N];
struct edge {
int u, v, w;
};
vector<edge> E;

struct cmp {
bool operator()(edge e1, edge e2) { return e1.w < e2.w; }
};

int find(int x) {
if (f[x] == x) return x;
return f[x] = find(f[x]);
}

int find2(int x) {
while (f[x] != x) {
f[x] = f[f[x]];
x = f[x];
}
return f[x];
}

// n-k 条边得到 k 个最小生成树
// 因此 kruskal算法只能选取 n-k 条[最小成本]的边来将这些边组成 k 个最小生成树
void kruskal() {
for (int i = 0; i < E.size(); i++) {
edge e = E[i];
int f1 = find2(e.u);
int f2 = find2(e.v);
if (f1 == f2) continue;
f[f1] = f2;
ans += E[i].w; // 计算k棵mst的成本
cnt++; // 统计边数
if (cnt >= n - k) break;
}
if (cnt == n - k)
cout << ans;
else
cout << "No Answer";
}
// 一个顶点也可与作为一颗最小生成树
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> k;
int u, v, w;
for (int i = 1; i <= n; i++) f[i] = i;
for (int i = 0; i < m; i++) {
cin >> u >> v >> w;
E.push_back(edge{u, v, w});
}
sort(E.begin(), E.end(), cmp());
kruskal();
return 0;
}