【讲课】基础的数论知识

翻车是必然的




gcd

口胡吧。。。

inline int gcd (int a, int b) {
	return b ? gcd (b, a % b) : a;
}

exgcd

先裴蜀

\(\large x, y\in Z^+\), 使得 $\large ax+by= m $ 成立的充要条件是 \(\large \gcd (a,b) | m\)

之前讲的有 \(\large \gcd (a, b) = \gcd (b, b \ mod \ a)\) 来找。。。

继续设 \(\large s=\gcd(a,b)\)

假如真就有解

明显就有 \(\large s|ax, \ s|by\)

然后证明就完了

放在exgcd里

只考虑 $\large x, y\in Z^+ \ \large ax+by= \gcd(a,b) $

\(\large b=0\) 时,\(\large \gcd (a,b)=a\) , 答案很显然了

\(\large b \neq 0\) 时,

\[\large{ \begin{aligned} &a \ mod \ b = a-\lfloor\frac{a}{b}\rfloor b\\ &ax+by\\ &=\gcd(a,b)\\ &=\gcd(b,a \ mod \ b)\\ &=bx_2+(a \ mod \ b)y_2\\ &=bx_2+ay_2-\lfloor\frac{a}{b}\rfloor by_2\\ &=ay_2+b(x_2-\lfloor\frac{a}{b}\rfloor y_2) \end{aligned} } \]

就很显然的得出

\(\large x=y_2,y=x_2-a/b*y_2\)

inline int exgcd (int &x, int &y, int a, int b) { // 取地址为了方便随时修改
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    int gcd = exgcd (x, y, b, a % b);
    int t = x;
    x = y;
    y = t - a / b * y;
    return gcd;
} // 顺便处理了一下gcd

放个题

luogu P2508

\[\large { \begin{aligned} &x^2+y^2=r^2\\ &x^2=r^2-y^2=(r-y)(r+y) \end{aligned} } \]

\[\large{ \begin{aligned} &设r-y=k*u,r+y=k*v,这里很明显gcd(u,v)=1\\ \\ &y=\frac{v-u}{2}*k,2*r=(v+u)*k\\ \\ &x^2=k^2*u*v\\ \\ &那 \ u*v \ 一定是个平方数\\ \\ &但gcd(u,v)=1\\ \\ &所以 \ u,v \ 分别是个平方数\\ \\ &再设u=s^2,v=t^2\\ \\ &x^2=d^2*s^2*t^2\\ \\ &x=d*s*t\\ \\ &y=\frac{v-u}{2}*k=\frac{t^2-s^2}{2}*k\\ \\ &2*r=(v+u)*k=(t^2+s^2)*k\\ \end{aligned} } \]

由此得出最后三条新关系式,我们暴力枚举 k , s ,从而算出整数 t (有可能不是整数),判断 \(\large gcd(t,s)=1\),带入得到 \(\large x,y\) ,符合题意就 ans+=8 (四个象限,(x,y)(y,x)算两个)。

最后 ans+=4(r为整数,坐标轴还有4个)

#include <bits/stdc++.h>

#define ll long long

using namespace std;

template <typename T>
inline void read (T &a) {
	T x = 0, f = 1;
	char ch = getchar ();
	while (! isdigit (ch)) {
		if (ch == '-') f = 0;
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ '0');
		ch = getchar ();
	}
	a = f ? x : -x;
}

inline ll gcd (ll a, ll b) {
    if (!b) return a;
    return gcd (b, a % b);
}

ll r, ans, x, y;

inline void blanc (ll k) {
    for (ll s = 1, t; s * s <= r / k; s++) {
        t = sqrt (r / k - s * s);
        if (s * s + t * t == r / k) {
            if (gcd (s, t) == 1) {
                x = k * s * t;
                y = (t * t - s * s) / 2 * k;
                if (x > 0 and y > 0 and x * x + y * y == r / 2 * (r / 2)) {
                    // 只考虑一象限,判断整数和是否满足条件,r先除后乘防爆ll
                    ans += 8;
                }
            }
        }
    }
}

signed main () {
    read (r);
    r *= 2;
    for (ll k = 1; k * k <= r; k++) {
        if (! (r % k)) { // 优先级!优先级!!!!!
            blanc (k);
            if (r / k != k) blanc (r / k);
        }
    }
    printf ("%lld", ans + 4);
}

再放一个

讲完逆元再讲,记得提醒我

\[\large {} \begin {aligned} &b * b^{-1}\equiv1 \ (mod \ p)\\ &b*inv(b)\equiv1 \ (mod \ p)\\ &b^{-1}\equiv inv(b) \ (mod \ p)\\ &a*b^{-1}\equiv a*inv(b) \ (mod \ p)\\ &a*b^{-1}\equiv a*inv(b) \ (mod \ p)\\ &a*inv(b)\equiv \ ? \ (mod \ p) \end {aligned} \]

洛谷P2613

#include <bits/stdc++.h>

#define mod 19260817
#define ll long long

using namespace std;

template <typename T>
inline void read (T &a) {
	T x = 0, f = 1;
	char ch = getchar ();
	while (! isdigit (ch)) {
		if (ch == '-') f = 0;
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ '0');
        x %= mod;
		ch = getchar ();
	}
	a = f ? x : -x;
}

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

signed main() {
    read (a);
    read (b);

    if (b) {
        exgcd(x, y, b, mod);
	    x = (x % mod + mod) % mod;
    	printf("%lld\n", a * x % mod);
        return 0;
    }
    cout << "Angry!" << endl;
}

素数

  • 这一栏 copy 自大半年前的课件

素数是啥不用说

这里说一说怎么找素数,也就是素数筛法

最朴素的

int pri[MAXN];
int cnt;
inline void prime (int n) {
	cnt = 0;
	pri[++cnt] = 2;
	int i,j;
	for (i = 3; i <= n; ++i) {
		for (j = 2; j < i; ++j) {
			if (i % j == 0)
			break;
		}
		if (j >= i)
		pri[++cnt] = i;
	}
}
// 如你所见,就是纯暴力

过于垃圾,无法接受,直接pass

好一点的

埃氏筛
埃式筛法的基本思想就是,当我们遍历到一个素数时,把所有该素数的倍数都筛选出来。

int cnt = 0;
int pri[20000];
bool v[20000];
int n;
inline void prime () {
	read (n);
	for (int i = 2; i <= n; ++i) {
		if (! v[i]) {
			pri[++cnt] = i;
			for (int j = i; j <= n; j += i) {
				v[j] = true;
			}
		}
	}
	for (int i = 1; i <= cnt; i++) cout << pri[i] << " ";
}

埃式筛法很容易理解,并且在效率上也比较优秀,时间复杂度为 \(O(N \ logN)\)

埃氏筛在筛选时有重复,或许我们可以通过某种方法避免这种重复

目前最好的

欧拉筛

int prime[MAXN];//它存的是最小素因子
bool vis[MAXN];

inline void phigros (int n) {
	for (int i = 2; i <= n; ++i) {
		if (! vis[i]) {
			prime[++prime[0]] = i;
		} // 判断素数

		for (int j = 1; j < prime[0] and i * prime[j] < n; j++) {
			vis[i * prime[j]] = true;//筛数
			if(i % prime[j] == 0)//时间复杂度为O(n)的关键!
				break;
		}
	}
}

为什么 i % prime[j] == 0 就break?
当 i是prime[j]的倍数时,i = k * prime[j],如果继续运算 prime[j + 1],

i * prime[j + 1] = prime[j] * (k * prime[j + 1]),
这里prime[j]是最小的素因子,
当i = k * prime[j+1]时,同样会在里循环中运算到,会重复,所以才跳出循环

线性筛还有其他用法

线性筛

筛素数

bool vis[N];
int pri[N], cnt;
inline void OSU () {
    for (int i = 2; i <= n; i++) {
        if (! vis[i]) pri[++cnt] = i;
        for (int j = 1; j <= cnt and i * pri[j] <= N; i++) {
            vis[i * pri[j]] = 1;
            if (i % pri[j] == 0) break;
        }
    }
}

筛欧拉函数

容斥证明以下式子:

\[\large \varphi(n)=n\prod_{i = 1}(1-\frac{1}{p_i}) \]

\(\large \varphi\) 积性函数证明:

\[\large{ \begin{aligned} &\varphi(a)=a\prod_{i=1}^m(1-\frac{1}{p_i})\\ &\varphi(b)=b\prod_{i=1}^n(1-\frac{1}{q_i})\\ &(a,b)=1\\ &\varphi(a)\varphi(b)=a\prod_{i=1}^m(1-\frac{1}{p_i})b\prod_{j=1}^n(1-\frac{1}{q_j})\\ &=ab\prod_{i=1}^{m+n}(1-\frac{1}{c_i}) \end{aligned} } \]

线性筛 \(\large\varphi\)

  1. \(\large (i,pri[j])=1\) 过于简单,不予讨论

  2. \(\large i\) 为质数,过于简单,不予讨论

  3. \[\large{ \begin{aligned} &(i,pri[j])=pri[j]\\ &\varphi(i)=i\prod_{k=1}(1-\frac{1}{p_k})\\ &pri[j]\in p\\ &\varphi(i*pri[j])=i*pri[j]\prod_{k=1}(1-\frac{1}{p_k})=\varphi(i)*pri[j] \end{aligned} } \]

bool vis[N];
int pri[N], cnt;
int phi[N];

inline void Arcaea () {
    for (int i = 2; i <= n; i++) {
        if (! vis[i]) pri[++cnt] = i, phi[i] = i - 1;
        for (int j = 1; j <= cnt and i * pri[j] <= N; i++) {
            vis[i * pri[j]] = 1;
            if (i % pri[j] == 0) {
                phi[i * pri[j]] = phi[i] * pri[j];
                break;
            }
            phi[i * pri[j]] = phi[i] * (pri[j] - 1); // 互质,积性函数
        }
    }
}

筛约数个数和,约数和

个数

\(\large d(n)\)\(\large n\) 的约数个数

\(\large num(n)\)\(\large n\) 的最小质因子个数

因为 \(\large pri\) 从小到大枚举,所以 \(\large pri[j]\) 一定是 \(\large i\times pri[j]\) 的最小质因子,毕竟线性筛的原理就在于此。

唯一分解定理:

\[\large n=\prod_{i=1}p_i^{k_i} \]

每个 \(\large p_i\) 都可选择 \([0,k_i]\) 个,彼此相乘,组成新约数。

\(\large n\) 的约数个数为:

\[\large d(n)=\prod_{i=1}(k_i + 1) \]

  1. \(\large i\) 是质数,答案显然

  2. \(\large i\%pri[j]!=0\) 相当于新加了一个质因子

    \[\large{ \begin{aligned} &d(i\times pri[j])=d(i)\times d(pri[j])\\ &=2\times d(i)\\ &num(i\times pri[j])=1 \end{aligned} } \]

  3. \(\large i\%pri[j]==0\) 之前出现过

    \[\large{ \begin{aligned} &d(i\times pri[j])=(1+k_1+1)\prod_{i=2}(k_i+1) \end{aligned} } \]

    之前的 \(\large num\) 起到了作用。

    \[\large d(i\times pri[j])=d(i)/(num(i)+1)\times(num(i)+2) \]

约数和

\(\large sd(n)\) 表示 \(\large n\) 的约数和(不是质因子和

\(\large num(i)\) 表示

根据算数基本定理有:

\[\large sd(n)=\prod_{i=1}(\sum_{j=0}^{r_i}p_i^j) \]

记录最小质因子那一项,即 \(\large (1+p_1+p_1^2+\cdots+p_1^{r_1})\)

\(\large num(n)\) 表示。

  1. \(\large i\) 是质数

    \[\large{ \begin{aligned} &sd(i)=i+1\\ &num(i)=i+1 \end{aligned} } \]

  2. \(\large i\%pri[j]!=0\) 显然

    \[\large{ \begin{aligned} &sd(i*pri[j])=sd(i)* sd(pri[j])\\ &num(i*pri[j])=pri[j]+1 \end{aligned} } \]

  3. \(\large i\%pri[j]==0\) 显然 \(\large num(i*pri[j]) = num(i)*pri[j]+1\)

    \[\large sd(i*pri[j])=sd(i)/num(i)*num(i*pri[j]) \]

代码略。。。(懒得打了

筛莫比乌斯函数

由定义式:

\[\large \mu(m) = \begin{cases} (-1)^r & \text{$m = p_1p_2...p_r$}\\ 0 & \text{$p_k^2|m$} \end{cases} \]

知,

  1. \(\large i\) 为质数,\(\large mu(i)=-1\).
  2. \(\large i\%pri[j]==0\)\(\large mu(i*pri[j])=0\).
  3. 否则 \(\large mu(i*pri[j])=-mu(i)\).

莫得代码

米勒拉宾检验,这里不讲

CRT

求解同余方程组

\[\large { \begin{cases} x \equiv a_1(mod \ m_1)\\ x \equiv a_2(mod \ m_2)\\ x \equiv a_3(mod \ m_3) &\text{$\gcd(m_t,m_s)=1$},1 \leq t,s\leq k \\ ...\\ x \equiv a_k(mod \ m_k)\\ \end{cases} } \]

举例

一个数 \(n\) ,除以3余2,除以5余3,除以7余2,求 \(n\)

\[\large \begin{cases} n_1 \equiv a_1 (mod \ m_1)\\ n_2 \equiv a_2 (mod \ m_2)\\ n_3 \equiv a_3 (mod \ m_3)\\ \end{cases} \]

\(\large m_1,m_2,m_3\)两两互质 求共解

先不找最小解

考虑

\[\large{ \begin{aligned} &m_2*m_3|n_1\\ \\ &m_1*m_3|n_2\\ \\ &m_1*m_2|n_3\\ \end{aligned} } \]

\(n=n_1+n_2+n_3\) 即是一个解

\(n \ mod \ lcm(m_1, m_2, m_3)\) 就是最小解

问题是咋求出来满足条件的 \(n_1,n_2,n_3\)

用逆元(inv)

举例

\[\large{ \begin{aligned} &n_1 \equiv a_1 (mod \ m_1)\\ &n_1^`=m_2*m_3\\ &n_1^` * inv(n_1^`) \equiv 1 (mod \ m_1)\\ &n_1^`=inv(n_1^`)*m_2*m_3\\ &n_1=a_1*n_1^`=a_1*inv(n_1^`)*m_2*m_3 \end{aligned} } \]

EXCRT

CRT 是 EXCRT 的特殊情况。

\[\large \begin{cases} x\equiv a_1\ (mod\ m_1)\\ x\equiv a_2\ (mod\ m_2)\\ \cdots\\ x\equiv a_n\ (mod\ m_n) \end{cases} \]

模数之间再无任何瓜葛

\[\large{ \begin{aligned} &x=a_1+k_1*m_1=a_2+k_2*m_2\\ &a_1+k_1*m_1=a_2+k_2*m_2\\ &k_2*m_2-k_1*m_1=a_1-a_2\\ \end{aligned} } \]

嘶~最后的式子有些眼熟啊,像不像内个,对,就是内个!\(ax+by=c\)

\[\large{ \begin{aligned} &g=gcd(m_1,m_2)\\ &c=a_1-a_2\\ \end{aligned} } \]

如果 \(\large g\nmid c\) 的话。。。那就无解。-_-

有解,求 \(\large k_2\times m_2+k_1\times m_1=g\)\(\large k_1\)

因为 \(\large g\mid c\),所以 \(\large k_1*=(c/g)\)

防止爆炸,\(\large k_1\%=m_2\)

于是可以反推 \(\large x\)

但我想各位已经发觉到了,原方程式是 \(\large k_2\times m_2-k_1\times m_1=c\)

于是 \(\large x = -k_1\times m_1+a_1\),我们暂且设这个 \(\large x\)\(\large x_0\)

通解就是 \(\large x=x_0+k\times lcm(m_1,m_2)\)

于是这两个同余方程合并成了一个:

\(\large x=x_0\ (mod\ lcm(m_1,m_2))\)

其他方程间以此类推。

最后剩下一个方程,他的 \(\large x_0\) 的最小非负整数解,就是最终答案。

例题:P4777 ~~这题怎么是个紫的?快降蓝啊(doge

#include <bits/stdc++.h>

#define int long long
#define N 1000005

using namespace std;

template <typename T>
inline void read (T &a) {
	T x = 0, f = 1;
	char ch = getchar ();
	while (! isdigit (ch)) {
		(ch == '-') and (f = 0);
		ch = getchar ();
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ '0');
		ch = getchar ();
	}
	a = f ? x : -x;
}

int n, x, y, A, B, C;
int m[N], a[N], ans;

inline int qmul(int a, int b, int mo) {
    int ans = 0, base = a;
    while (b) {
        if (b & 1) {
        	ans = (ans + base) % mo;
        }
        base = (base + base) % mo;
        b >>= 1;
    }
    return ans;
} // 龟速乘(不是快速幂)目的是防止两数相乘的时候溢出爆炸 

inline int exgcd(int a, int b, int &x, int &y) {
    if (!b) {
        x = 1;
		y = 0;
        return a;
    }
    int g = exgcd(b, a % b, x, y);
    int tx = x;
    x = y;
    y = tx - (a / b) * y;
    return g;
} // 不多言,都知道 

inline void excrt() {
    for (int i = 2; i <= n; i++) {
        A = m[1], B = m[i], C = a[i] - a[1];
        C = (C % B + B) % B;
		// 防止 C 是个负的,用 B 是因为在同余式中 B 是模数 
        int g = exgcd(A, B, x, y);
        x = qmul(x, (C / g), B); // 这里的 x 就是博客里的 k1 
        x = (x % B + B) % B;
		// k1 不能是个负的,负的难算还可能会错 
        a[1] = a[1] + qmul(m[1], x, m[1] * (m[i] / g));
        // 原博客 x = -k1 * m1 + a1 
        m[1] = m[1] * (m[i] / g); // 模数变为 lcm (m1, m2) 
        a[1] = (a[1] % m[1] + m[1]) % m[1]; // 防爆 
    }
}

signed main() {
    read (n); 
    for (int i = 1; i <= n; i++) {
    	read (m[i]), read (a[i]);
    }
    excrt();
    printf("%lld", a[1]);
}

逆元

扩欧

最常用的,经常在各大题解里见到。。。(比如上面那篇代码

\(\large a * inv(a) = 1 (mod \ p)\)
根据 exgcd 可以变形成 \(\large a * inv(a) +k * p = 1\)

这说明只有 a 和 p 互质才存在逆元

inline int exgcd (int &x, int &y, int a, int b) { // 取地址为了方便随时修改
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    int gcd = exgcd (x, y, b, a % b);
    int t = x;
    x = y;
    y = t - a / b * y;
    return gcd;
} // 顺便处理了一下gcd

inline long long inv (int a, int p) {
    long long x, y;
    long long d = exgcd (x, y, a, p);
    return d == 1 ? (x % p + p) % p : -1;
}

欧拉

这个比扩欧快一点,但不指望你们掌握,(下边有更好的)

\[\large a^{\varphi(p)} \equiv 1 (mod \ p)\\ \large a*a^{\varphi(p)-1} \equiv 1 (mod \ p) \]

\(\large a^{\varphi(p)-1}\) 即是。

inline ll phi (int p) {
	int a, b;
	a = b = p;
	for (int i = 2; i * i <= a; ++i) { // 根号就够了
		if (a % i == 0) {
			b -= b / i; // 等价于 b = b * (1 - 1 / i), 精度问题 
			while (a % i == 0) {
				a /= i; // 把这几个质因子全去掉。。。 
			}
		}
	}
	if (a > 1) { // 应对素数情况 , 同时将最后一个没有因数用掉 
		b = b - b / a;
	}
	return b;
}

inline ll fpow (ll a, ll p, ll mod) {
    ll ans = 1, cnt = a % mod;
    while (p) {
        if (p & 1) ans = ans * cnt % mod;
        cnt = cnt * cnt % mod;
        p >>= 1;
    }
    return ans;
}

inline ll inv (ll a, ll p) {
    return fpow (a, phi (p), p);
}

递推

线性

套用小绿书上的

p 是模数, i 为待求逆元的数, 我们现在求 \(i^{-1}\) 在 mod p 意义下的值

\[\large { \begin{aligned} &p=k*i+r,k=p/i,r=p\%i\\ &k*i+r\equiv0(mod \ p)\\ &\text{all*$(ri)^{-1}$} \\ &k*r^{-1}+i^{-1}\equiv0(mod \ p)\\ &i^{-1}\equiv-k*r^{-1}(mod \ p)\\ &i^{-1}\equiv-p/i*inv(p\%i)(mod \ p)\\ \end{aligned} } \]

\(i^{-1}\) 就是 inv[i] ,

ll inv[p+5];
inline void Inv (ll p) {
    inv[1] = 1;
    for (int i = 2; i < p; i++) {
        inv[i] = (p - p / i) * inv[p % i] % p;
        // (p - p / i) 在 mod p 意义下等价于 -(p/i);
    }
}

适用于 p 是不大的素数且逆元被多次调用

预处理 \(\large O(p)\),单次查询 \(\large O(1)\)

递归

把上面的 for 变一下

inline ll inv (int i) {
    if (i == 1) return 1;
    return (p - p / i) * inv (p % i) % p;
}

单次查询 \(\large O(log \ p)\).

posted @ 2021-09-23 22:08  aleph_blanc  阅读(66)  评论(0编辑  收藏  举报