数论总结

数论还是有很多没学完 只是小小的总结

一、同余定理

1.反身性:\(a\equiv a (mod\ m)\)
2.对称性:若\(a\equiv b(mod\ m)\),则\(b\equiv a (mod\ m)\)
3.传递性:若\(a\equiv b(mod\ m)\)\(b\equiv c(mod\ m)\),则\(a\equiv c(mod\ m)\)
4.同余式相加:若\(a\equiv b(mod\ m)\)\(c\equiv d(mod\ m)\),则\(ac\equiv bd(mod\ m)\)
5.同余式相乘:若\(a\equiv b(mod\ m)\)\(c\equiv d(mod\ m)\),则\(ac\equiv bd(mod\ m)\)

二、整除

给定\(a\),\(b\)两个数,若b能整除a,记作\(b\mid a\),反之记作\(a\nmid b\)
简单定理:

  • \(b\mid a\),且\(c\mid b\),则\(c\mid a\)
  • \(c\mid a\),且\(c\mid b\),则\(c\mid \left(na+mb\right)\)

三、最小公倍数与最大公约数

最大公约数:GCD

辗转相除法:设\(gcd(a,b)\)\(a\)\(b\)的最大公约数

\[gcd(a,b)=gcd(a-b,b)\Rightarrow gcd(a,b)=gcd(b,a\%b) \]

\(b\)为0时,此时的\(a\)即为二者的最大公约数

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

最小公倍数:LCM

\(d=gcd(a,b)\)\(a=a'd\)\(b=b'd\),可以看出 \(lcm(a,b)=\frac{ab}{gcd(a,b)}\)

拓展内容:拓展欧几里得 \(exgcd\)

试想解决这样一个问题:\(ax+by=c\),求得所有\(x\)\(y\)组合(\(x\)\(y\)均为整数)

  1. 首先可以确定的是,若\(gcd(a,b) \nmid c\),则一定没有整数解
    这点很容易看出,由于\(ax\)一定是\(gcd(a,b)\)的倍数,\(by\)也是,所以他们的和也一定得是\(gcd(a,b)\)的倍数
  2. 其次,倘若原问题有解,则另一个问题\(bx+(a\%b)y=c\)也同样有解。可以发现其实这就是辗转相除法的过程,到了终止条件\(b=0\),也就是\(gcd(a,b)x+0\times y=gcd(a,b)\),可以得到解\(x=1,y=0\)
  3. 得到解以后观察\(ax+by=c\)\(bx+(a\%b)y=c\)解的的递推关系。为了简化问题,可以将题目看作\(ax+by=gcd(a,b)\),得到的解乘以\(\frac{c}{gcd(a,b)}\)即为最终答案

\[bx’+a\%b\times y’=bx’+(a-\left\lfloor\frac{a}{b}\right\rfloor\times b)\times y’=gcd(a,b)=ax+by \]

\[ax+by=bx’+(a-\left\lfloor\frac{a}{b}\right\rfloor\times b)\times y’=ay’+b(x’-\left\lfloor\frac{a}{b}\right\rfloor y’) \]

\[\Rightarrow x=y’,y=x’-\left\lfloor\frac{a}{b}\right\rfloor y’ \]

得到了上述的递推关系以后,可以得到拓展欧几里得的求解代码

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

巧妙之处,在于每次\(exgcd\)传递的是\(x\)\(y\)的引用,且每次交换位置,实现了\(x=y’\),在这样的情况下就得注意\(y=x’-\left\lfloor\frac{a}{b}\right\rfloor y’\Leftrightarrow y=y-\left\lfloor\frac{a}{b}\right\rfloor x\)

这样通过\(exgcd\)就获得了\(ax+by=gcd(a,b)\)的一组特解\(x_0,y_0\)
进而较易获得通解(如果是某个范围内的解,解两个一元一次不定方程即可):

\[\left\{\begin{aligned} x=x_0+t\left(\frac{b}{gcd(a,b)}\right) &\\ y=y_0-t\left(\frac{a}{gcd(a,b)}\right) \end{aligned}\right. \]

四、素数与合数

对于任意一个大于1的自然数,只有1和它本身两个因子,则称为素数
素数定理:小于等于x的素数个数 \(\approx \frac{x}{\ln x}\) ,可以用来估计素数个数,进而估算所开数组的大小
不是素数的大于1的自然数称为合数

素数筛法:

1、暴力枚举

复杂度:\(O(\log{n})\)
由于任意一个数\(x\)的因子可看为两部分,小于\(\sqrt{x}\)与大于\(\sqrt{x}\),因此可以枚举所有\(\{i\mid i\le \sqrt{x}\}\),如若出现\(i\mid x\),则不是素数,反之是素数。
一般用于对某单个数的素性判定

bool check(int x)
{
	int end = sqrt(x);
	for (int i = 2; i <= end; ++i)
	{
		if (x % i == 0)
			return false;
	}
	return true;
}

拓展内容(求单个合数的最大质因数)
对于任何一个数\(x\),可以将他进行质因数分解,且同时保证\(prime[i]^2\le x_{cur}\)进行优化。
首先可以预处理出所有\(\{prime\mid prime \le \sqrt{x}\}\),这样\(x\)的质因数分解一定是在这个集合中,或者只有最大质因数不在这个集合中。如果所剩下的最后一个数为1,即完美的进行了质因数分解,则最大质因数为最后一次除的质数,反之则最后剩下的数即为最大质因数

const int maxn = 10000;
int vis[maxn];
int cnt, prime[maxn/10];
int Maximum_prime_factor(int x)
{
	/*筛出sqrt(x)内的所有素数

	*/
	int ans;
	for (int i = 0; i < cnt && prime[i] * prime[i] <= x; ++i)
	{
		if (x % prime[i] == 0)
		{
			ans = prime[i];
			while (x % prime[i] == 0)
				x /= prime[i];
		}
	}
	return x == 1 ? ans : x;
}

2、埃氏筛法

复杂度:\(O(\log{\log{n}})\)
由于对于任何合数而言,他们能够被任意\(prime\) 整除,所以,可以通过枚举\(k*prime(k*prime\le lim_{up})\),来筛选出一些约数,而没有被筛选过的自然就是素数
值得说明的是:当选中某个\(prime\)时,比\(prime\)小的质数的倍数已经被筛出了,所以为了减小时间复杂度,可以从\(prime^2\)开始筛选

const int maxn = 10000;
bool vis[maxn];
int cnt, prime[maxn / 10];
void Eratosthenes_Sieve()
{
	for (int i = 2; i < maxn; ++i)
	{
		if (vis[i]) continue;
		prime[cnt++] = i;
		for (int j = i * i; j < maxn; j += i)
			vis[j] = true;
	}
}

拓展内容(求出多个合数的最大质因数)
利用埃氏筛法是由小质数到大质数的筛选过程,每次大质数筛选时会覆盖之前小质数的结果,因此可以得到实现代码
注意:开始条件从\(i^2\)变为了\(2i\)

const int maxn = 10000;
int vis[maxn];
int cnt, prime[maxn];
void get_Maximum_prime_factors()
{
	for (int i = 2; i < maxn; ++i)
	{
		if (vis[i]) continue;
		prime[cnt++] = i;
		for (int j = 2*i; j < maxn; j += i)
			vis[j] = i;
	}
}

再拓展,将vis[j] = i改成++vis[j]可以求得n分解出的不同质因数的个数

3、欧拉筛

复杂度:\(O(n)\)
通过对每个合数,只用其最小的质因数进行筛选的思想,每次将\(cur*prime[i]\)对应的数筛出,为了保证最小的质因数筛出,当\(prime[i]\mid cur\)时,需要break
原因在于设\(cur=k*prime[i]\),那么如果继续筛即对于

\[n=cur*prime[i+1]=(prime[i]*k)*prime[i+1]=prime[i]*(k*prime[i+1]) \]

则可以看出来,这个数\(n\)本应该在枚举到数\(k*prime[i+1]\)(比\(cur\)大)被\(prime[i]\)筛出

const int maxn = 10000;
int vis[maxn];
int cnt, prime[maxn];
void Euler_Sieve()
{
	for (int i = 2; i < maxn; ++i)
	{
		if (!vis[i]) prime[cnt++] = i;
		for (int j = 0; j < cnt && prime[j] * i < maxn; ++j)
		{
			vis[prime[j] * i] = true;
			if (i % prime[j] == 0)
				break;
		}
	}
}

拓展内容(求出多个合数的最小质因数)
利用欧拉筛每个数都被其最小质因数所筛去

#include<cstdio>
const int maxn = 10000;
int vis[maxn];
int cnt, prime[maxn / 10];
void get_Maximum_prime_factors()
{
	for (int i = 2; i < maxn; ++i)
	{
		if (!vis[i]) vis[i] = prime[cnt++] = i;
		for (int j = 0; j < cnt && prime[j] * i < maxn; ++j)
		{
			vis[prime[j] * i] = prime[j];
			if (i % prime[j] == 0)
				break;
		}
	}
}

4、min25筛

待学

五、积性函数

定义:满足下列条件的函数为积性函数

  1. \(f(1)=1\)
  2. \(f(nm)=f(n)f(m)\qquad\text{if gcd(n,m)=1}\)

另外若对于任意整数\(n,m\),满足\(f(n,m)=f(n)f(m)\)称为完全积性函数

对于积性函数\(f(n)\),有如下性质

  • 积性函数的乘积仍为积性函数
  • 根据唯一分解定理\(n=p_1^{a_1}p_2^{a_2}\dots p_k^{a_k}\),所以\(f(n)=f(p_1^{a_1})f(p_2^{a_2})\dots f(p_k^{a_k})\)
  • \(F(n)=\sum_{d|n}f(d)\)也是一个积性函数,且\(F(n)=F(p_1^{a_1})F(p_2^{a_2})\dots F(p_k^{a_k})\\=(1+f(p_1)+f(p_1^2)+\dots+f(p_1^{a^1}))(1+f(p_2)+f(p_2^2)+\dots+f(p_2^{a^2}))\ldots\)

迪利克雷卷积

\[(f*g)(n)=\sum_{d|n}f(d)g(\frac{n}{d}) \]

常见的积性函数:

  • \(\epsilon ,\epsilon=\bold{1}*\mu\) 单位元
  • \(I(\bold{1})\) 不变函数
  • \(\mu(n)\) 莫比乌斯函数
  • \(\varphi(n)\) 欧拉函数
  • \(Idk(n)=n^k\) 幂函数
  • \(Id(n)=n\) 单位函数
  • \(d(n),d=\bold{1}*\bold{1}\) 因子个数函数
  • \(\sigma(n),\sigma=\bold{1}*Id\) 因子和函数
  • \(\sigma k(n) ,\sigma k=\bold{1}*Idk\) 因子函数

莫比乌斯函数

\[\mu(n)=\begin{cases} 1&{n=1} \\ (-1)^k&\text{if } {a_1=a_2=...=a_k=1}\\ 0&{others} \end{cases} \]

\(f\)为积性函数,则\(\sum_{d|n}f(d)\mu(d)=(1-f(p_1))(1-f(p_2))\ldots\)

小tips

非常显然,易于理解后面的性质

  • \(f\ne f*\bold{1},f=f(n),f*\bold{1}=\sum_{d|n}f(d)\)

莫比乌斯反演

利用反演可以将某个比较难求的积性函数转化成另一个好求的积性函数

(因子关系)若 \(g(n)=\sum_{d|n}f(d)\),则\(f(n)=\sum_{d|n}\mu(d)g(\frac{n}{d})\)
证明:

\[g=f*\bold{1}\\ \mu*g=f*\bold{1}*\mu\\ \mu*g=f*\epsilon=f \]

(倍数关系)若 \(g(n)=\sum_{n|d}f(d)\),则\(f(n)=\sum_{n|d}\mu(\frac{d}{n})g(d)\)
证明:
\(k=\frac{d}{n}\),则

\[\sum_{n|d}\mu(\frac{d}{n})g(d)=\sum_k\mu(k)g(nk)\\ \Leftrightarrow\sum_k\mu(k)\sum_{nk|t}f(t)=\sum_tf(t)\sum_{nk|t}\mu(k)\\ \Leftrightarrow\sum_tf(t)\epsilon(\frac{t}{n})=f(n) \]

线性求取方法

const int maxn = 10000;
int vis[maxn];
int cnt, prime[maxn], mu[maxn];
void getMu()
{
	mu[1] = 1;
	for (int i = 2; i < maxn; ++i)
	{
		if (!vis[i]) prime[cnt++] = i, mu[i] = -1;
		for (int j = 0; j < cnt && prime[j] * i < maxn; ++j)
		{
			vis[prime[j] * i] = true;
			if (i % prime[j] == 0)
				break;
			mu[prime[j] * i] = -mu[i];
		}
	}
}

欧拉函数

欧拉函数,记作\(\varphi (n)\),表示\(1\sim n-1\)中和n互质的数的个数

1、求取方式

若对\(x\)进行质因数分解,可得\(x=\prod_{i=1}^kp_i^{\alpha_i}\),由容斥原理可得

\[\varphi(x)=\{n_1\mid n_1\le x\wedge n_1\ne kp_i\}-\{n_2\mid n_2\le x\wedge n_2\ne kp_ip_j\}+...\\ \Leftrightarrow \varphi(x)=x\prod_{i=1}^k(1-\frac{1}{p_i})\]

2、基本性质

  1. 对于任意质数\(p\)\(\varphi(p)=p-1\)
  2. \(gcd(n,m)=1\)时,\(\varphi(nm)=\varphi(n)\times\varphi(m)\)
    可以很显然的看出,当\(n\)\(m\)互质时,二者分解出的质因数是不同的,则

\[\varphi(nm)= nm\prod_{i=1}^k(1-\frac{1}{p_i})=n\prod_{i=1}^{k_n}(1-\frac{1}{p_{ni}})\times m\prod_{i=1}^{k_m}(1-\frac{1}{p_{mi}}) \]

  1. \(m\mid n\)时,\(\varphi(nm)=m\times\varphi(n)\)
    \(m\)能够整除\(n\),证明\(\{p_m\}\in\{p_n\}\),则显然对于\(nm\)来说,\(m\)的存在只是让\(nm\)相较于\(n\)的质因数分解的结果多了一些幂次数而已

\[\varphi(nm)=nm\prod_{i=1}^k(1-\frac{1}{p_i}) \]

  1. \(\sum_{d|n}\varphi(d)=n\)
    证明:
    • \(n=1\)时,\(\varphi(n)=1=n\)
    • \(n\)是质数时,\(\sum_{d|n}\varphi(d)=\varphi(n)+\varphi(1)=n\)
    • \(n\)是质数的幂,即\(n=p^k\)时,\(\sum_{d|n}\varphi(d)=(\sum_{i=1}^kp^{i-1}(p-1))+1=p^k\)
    • \(n\)有多质因子时,利用\(\varphi\)积性函数的性质,\(s(n)=\sum_{d|n}\varphi(d)=s(p_1^{a_1})s(p_2^{a_2})\ldots=p_1^{a_1}p_2^{a_2}\ldots p_k^{a_k}=n\)
  2. 利用4中性质不难看出\(\bold{1}*\varphi=Id\),那在两边乘以\(\mu\)可以得到\(\varphi=\mu*Id=\sum_{d|n}\mu(d)\frac{n}{d}=n\times\sum_{d|n}\mu(d)\frac{1}{d}\),利用莫比乌斯函数的卷积展开式也可以得到\(n\prod_{i=1}^k(1-\frac{1}{p_i})\)

3、线性递推求法

已知上述性质后,可以很明显发现与欧拉筛递推过程可以完美的结合到一起,于是可以得到以下代码

const int maxn = 1e4;
int prime[maxn / 10], cnt;
int euler[maxn];
void get_euler()
{
	for (int i = 2; i < maxn; ++i)
	{
		if (!euler[i]) prime[cnt++] = i, euler[i] = i - 1;
		for (int j = 0; j < cnt && i * prime[j] < maxn; ++j)
		{
			if (i % prime[j])
				euler[i * prime[j]] = euler[prime[j]] * euler[i];
			else
                        {
				euler[i * prime[j]] = prime[j] * euler[i];
				break;
			}
		}
	}
}

杜教筛

预备知识:整除分块

快速计算\(\sum_{i=1}^k\lfloor\frac{n}{i}\rfloor\)
可以发现\(\lfloor\frac{n}{i}\rfloor\)在某一段区间内的值是一样的,所以如果已知左区间端点为\(l\),则这一段的取值是\(x=\lfloor\frac{n}{l}\rfloor\),而这个区间的右端点即为\(\lfloor\frac{n}{x}\rfloor\)

	for (int l = 1, r; l <= n; l = r + 1) {
		r = n / (n / l);
		ans += (r - l + 1) * (n / l);
	}

这里参考了大佬的博客

是一种在\(O(n^{\frac{2}{3}})\)求取积性函数前缀和的筛法
\(h=f*g,S(n)=\sum_{i=1}^nf(i)\)

\[\sum_{i=1}^nh(i)=\sum_{i=1}^n\sum_{d|i}g(d)*f(\frac{i}{d})\\ \Leftrightarrow \sum_{d=1}^ng(d)\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}f(i)=\sum_{d=1}^ng(d)S(\lfloor\frac{n}{d}\rfloor)\\ \Leftrightarrow\text{抽取第一项}\sum_{i=1}^nh(i)=g(1)*S(n)+\sum_{d=2}^ng(d)S(\lfloor\frac{n}{d}\rfloor)\\ \Leftrightarrow g(1)*S(n)=\sum_{i=1}^nh(i)-\sum_{d=2}^ng(d)S(\lfloor\frac{n}{d}\rfloor)\\ \]

这里往往可以采用小数据线性求取,大数据unordered_map递归求取

筛莫比乌斯函数

\(f=\mu,g=\bold{1},h=f*h=\epsilon\)

\[S(n)=1-\sum_{d=2}^nS(\lfloor\frac{n}{d}\rfloor) \]

筛欧拉函数

\(f=\varphi,g=\bold{1},h=f*g=Id\)

\[S(n)=\sum_{i=1}^ni-\sum_{d=2}^nS(\lfloor\frac{n}{d}\rfloor) \]

\(d*\varphi(d)\)

\(f=d*\varphi(d),g=Id,h=f*g=\sum_{d|n}d*\varphi(d)*\frac{n}{d}=n\sum_{d|n}\varphi(d)=n^2\)

\[S(n)=\sum_{i=1}^ni^2-\sum_{d=2}^ndS(\lfloor\frac{n}{d}\rfloor) \]

板子

#include<bits/stdc++.h>
#include<unordered_map>
const int maxn = 6000010;
using namespace std;
bool vis[maxn];
int mu[maxn], sum1[maxn], phi[maxn];
long long sum2[maxn];
int cnt, prim[maxn],lim;
unordered_map<long long, long long>w1;
unordered_map<int, int>w;

void get()
{
    //一定要set lim!!!1e7左右都行
    phi[1] = mu[1] = 1;
    for (int i = 2; i <= lim; i++)
    {
        if (!vis[i])
        {
            prim[++cnt] = i;
            mu[i] = -1; phi[i] = i - 1;
        }
        for (int j = 1; j <= cnt && prim[j] * i <= lim; j++)
        {
            vis[i * prim[j]] = 1;
            if (i % prim[j] == 0)
            {
                phi[i * prim[j]] = phi[i] * prim[j];
                break;
            }
            mu[i * prim[j]] = -mu[i], phi[i * prim[j]] = phi[i] * (prim[j] - 1);
        }
    }
    for (int i = 1; i <= lim; i++)sum1[i] = sum1[i - 1] + mu[i], sum2[i] = sum2[i - 1] + phi[i];
}
int djsmu(int x)
{
    if (x <= lim)return sum1[x];
    if (w[x])return w[x];
    int ans = 1;
    for (int l = 2, r; l >= 0 && l <= x; l = r + 1)
    {
        r = x / (x / l);
        ans -= (r - l + 1) * djsmu(x / l);
    }
    return w[x] = ans;
}
long long djsphi(long long x)
{
    if (x <= lim)return sum2[x];
    if (w1[x])return w1[x];
    long long ans = x * (x + 1) / 2;
    for (long long l = 2, r; l <= x; l = r + 1)
    {
        r = x / (x / l);
        ans -= (r - l + 1) * djsphi(x / l);
    }
    return w1[x] = ans;
}

六、快速幂、快速乘

一般比较简单幂运算使用pow函数即可,但使用该函数有一定缺点:一是浮点计算,可能有精度误差;二是对于幂数很大的情况下,朴素乘法计算较慢,所以需要快速幂
对于任意一个幂运算\(x^n\),可以进行如下条件判断拆分

\[\begin{cases} x^0=1 \\ x^n=x^\frac{n}{2}\times x^\frac{n}{2} &\text{if x is even}\\ x^n=x\times x^{\left\lfloor\frac{n}{2}\right\rfloor}\times x^{\left\lfloor\frac{n}{2}\right\rfloor} & \text{if x is odd} \end{cases} \]

通过上述分析可以获得以下代码

long long q_pow(long long x, int n, int mod)
{
	if (n)
		return (n & 1 ? x : 1) * q_pow(x, n / 2, mod) % mod * q_pow(x, n / 2, mod) % mod;
	return 1;
}

但这样的方法虽然让幂运算复杂度\(O(n)\rightarrow O(\log n)\)。但这样需要开启很多的栈帧进行递归,且需要复制传递许多参数,还是稍有浪费时间。对\(x^n\)进行进一步的分析,将指数化成对应二进制,1代表有第\(i\)位对应的\(x^{2^i}\),0代表无,有了下述公式

\[x^{(n)_B}\Rightarrow \prod_{i=0}^kB(i)*x^{2^i} \]

long long q_pow(long long x, int n, int mod)
{
	long long ans = 1;
	while (n)
	{
		if (n & 1) ans = ans * x % mod;
		x = x * x % mod;
		n >>= 1;
	}
	return ans;
}

类似的,快速乘的代码也可以类似的得出,目的是为了解决乘积很大,且要取模运算的情况

long long q_mul(long long x, int n, int mod)
{
	long long ans = 0;
	while (n)
	{
		if (n & 1) ans = (ans + x) % mod;
		x = (x + x) % mod;
		n >>= 1;
	}
	return ans;
}

七、逆元

对与任何一个数\(x\),若\((\exists a)(ax\equiv1\ (mod\ p))\),则称\(a\)\(x\)\(p\)意义下的逆元,记作\(x^{-1}\)
逆元相当于非模运算中的倒数,且很明显发现,逆元不唯一

求逆元方法:

1、拓展欧几里得

复杂度:\(O(\log\max(x,p))\)
对于\(ax\equiv1mod(p)\Leftrightarrow ax-1=np\Leftrightarrow ax-np=1\)
显然这么一个方程肯定可以用\(exgcd\)求得\(a\)对应的解(如果逆元存在,则\(x\)\(p\)肯定互素)
对于模数较大时,且只求单一元素的逆元时,这是一个不错的选择

2、费马小定理

复杂度:\(O(\log p)\)
对于费马小定理:\(x^{\varphi(p)}\equiv1(mod\ p)\)
这里\(\varphi(p)\)指的是欧拉函数,于是如果模数\(p\)是一个素数的话$$\varphi(p)=p-1\Rightarrow x*x^{(p-2)}\equiv 1(mod \ p)$$
\(a=x^{p-2}\)\(x\)的逆元,可以通过快速幂求得,当然如果模数不是素数,那费劲地在\(O(\log n)\)求得对应欧拉函数后,就不如前一种方法了

3、线性递推求逆元

复杂度:\(O(n)\)
\(p\)是模数,求\(i\)的逆元,将\(p\)写成\(p=k*i+r\)

\[k*i+r\equiv0(mod\ p)\Leftrightarrow k*r^{-1}+i^{-1}\equiv0 (mod\ p) \]

\[i^{-1}\equiv -k*r^{-1}(mod \ p) \]

分析上式,如果要求\(i^{-1}\),就必须先获得\(r^{-1}\),而\(r\)\(p\%i\)的余数一定小于\(i\),且1的逆元一定是1,那么线性递推的条件便全部都满足了,可以得到以下代码

const int maxn = 1e5;
int inv[maxn];
void get_inv(int mod)
{
	inv[1] = 1;
	for (int i = 0; i < maxn; ++i)
		inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}

\(k\)之前添加一个\(mod\),是为了使得逆元非负,根据同余定理,这样不会改变逆元的性质

八、中国剩余定理

中国剩余定理是为了解决如下形式的同余方程组,满足的最小\(n\)

\[\begin{cases} n\equiv a_1(mod\ m_1) \\ n\equiv a_2(mod\ m_2)\\ \qquad\vdots\\ n\equiv a_k(mod\ m_k) \end{cases} (\forall i)(\forall j)(i\neq j\wedge gcd(m_i,m_j)=1) \]

  • \(m_i\)两两互质时
    对于这样一个方程,可以将\(n\)拆解成\(k\)个部分,即\(n=\sum_{i=1}^kn_i\),每个\(n_i\)对应着第\(i\)个方程的满足数——\((\forall t)((t\neq i\wedge m_t\mid n_i)\vee (t=i\wedge n_i\%m_i=a_i))\)。同时存在着公式\(a\equiv b(mod\ m)\Rightarrow ka\equiv kb(mod\ m)\),很明显可以构造出一个\(x\)(且同时\(x\)会被其他模数整除)的逆元使得\(x*x^-1\equiv 1(mod\ m)\),这里\(x=\frac{\prod_{j=1}^km_j}{m_i}\),于是可以得到下式

\[n_i=x\times x^{-1}(mod\ m_i)\times a_i \]

long long exgcd(long long a, long long b, long long& x, long long& y)
{
	if (b == 0)
	{
		x = 1, y = 0;
		return a;
	}
	int t = exgcd(b, a % b, y, x);
	y -= a / b * x;
	return t;
}
long long CRT(long long* a, long long* m, int n)
{
	//a是余数数组,m是模数数组
	long long M = 1;
	for (int i = 0; i < n; ++i) M *= m[i];
	long long result = 0, x, y, tmp;
	for (int i = 0; i < n; ++i)
	{
		tmp = M / m[i];
		exgcd(tmp, m[i], x, y);
		result = (result + x * m[i] * a[i]) % M;
	}
	return (result + M) % M;
}

\[\begin{cases} n=a_i(mod \ m_i) \\ n=a_j(mod \ m_j) \end{cases}\\ \]

\[n=m_ik_i+a_i\\ n=m_jk_j+a_j \]

\[m_ik_i+a_i=m_jk_j+a_j\\ m_ik_i=m_jk_j+(a_j-a_i)\\ m_ik_i\equiv(a_j-a_i)(mod \ m_j) \]

如果上述方程有解,则\(gcd(m_i,m_j)\mid(a_j-a_i)\),设\(d=gcd(m_i,m_j),c=a_j-a_i\)

\[\frac{m_i}{d}k_i\equiv \frac{c}{d}(mod \ \frac{m_j}{d})\\ k_i\equiv \frac{c}{d}*\frac{m_i}{d}^{-1}(mod \ \frac{m_j}{d}) \]

\(K= \frac{c}{d}*\frac{m_i}{d}^{-1},k_i=k\frac{m_j}{d}+K\),代入之前的等式\(n=m_ik_i+a_i\)

\[n=m_i(k\frac{mj}{d}+K)+a_i=k\frac{m_im_j}{d}+(m_iK+a_i)\\ m_0=\frac{m_im_j}{d},a_0=m_iK+a_i \]

这样就得到了同时满足两个同余式的\(n\),便可以进行递推

long long exgcd(long long a, long long b, long long& x, long long& y)
{
	if (b == 0)
	{
		x = 1, y = 0;
		return a;
	}
	int t = exgcd(b, a % b, y, x);
	y -= a / b * x;
	return t;
}
long long CRT(long long* a, long long* m, int n)
{
	//a是余数数组,m是模数数组
	if (n == 1)
	{
		if (m[0] > a[0]) return a[0];
		return -1;
	}
	long long x, y, d;
	for (int i = 1; i < n; ++i)
	{
		if (m[i] <= a[i]) return -1;
		d = exgcd(m[0], m[i], x, y);
		if ((a[i] - a[0]) % d) return -1;
		long long t = m[i] / d;
		x = ((a[i] - a[0]) / d * x % t + t) % t;
		a[0] += m[0] * x;
		m[0] = m[0] * m[i] / d;
		a[0] = (a[0] % m[0] + m[0]) % m[0];
	}
	return a[0];
}

九、组合数

组合数记作\(C(n,m)\),代表从\(n\)个物体中取出\(m\)个的方案数

公式:

  1. \(C(n,m)=\frac{n!}{m!(n-m)!}\)
  2. \(C(n,m)=C(n,n-m)\)
  3. \(C(n,m)=C(n-1,m-1)+C(n-1,m)\)
  4. \(\sum_{i=0}^nC(n,i)=2^n\)

一般对于组合数的求取除了只需要单个组合数的情况下,都采用预处理的形式进行存取

求取方法

1、直接线性求取

复杂度:预处理\(O(n^2)\)
也就是利用公式3进行线性递推,适用于\(n\)较小时

const int maxn = 1e3;
long long C[maxn][maxn];
int n;
const long long mod;
void get_C()
{
	C[0][0] = 1;
	for (int i = 1; i <= n; ++i)
	{
		C[i][1] = i % mod;
		C[0][i] = C[i][0] = C[i][i] = 1;
	}
	for (int i = 2; i <= n; ++i)
		for (int j = 2; j < i; ++j)
			C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
}

不管你用不用,我一般不用,时间又长,空间要求的还大

2、利用逆元线性求取

复杂度:预处理\(O(n)\)
通过公式1,预处理阶乘以及阶乘的逆元从而得解,适用于\(n\)较大时
通常采用这种方法,这种方法唯一的缺点可能就是查询的时候不是完全O(1),要进行一定乘法运算和取模运算

const int maxn = 1e6;
long long jc[maxn + 1];//阶层数组
long long inv[maxn + 1];//逆元数组
const long long mod;
void exgcd(long long a, long long b, long long& x, long long& y)
{
	if (b == 0)
		x = 1, y = 0;
	else
	{
		exgcd(b, a % b, y, x);
		y -= a / b * x;
	}
}
long long q_pow(long long x, int n)
{
	long long result = 1;
	while (n)
	{
		if (n & 1)
			result = result * x % mod;
		n >>= 1;
		x = x * x % mod;
	}
	return result;
}
void pre()
{
	long long x, y;
	jc[0] = inv[0] = jc[1] = inv[1] = 1;
	for (int i = 2; i <= maxn; ++i)
		jc[i] = jc[i - 1] * i % mod;

	//inv[maxn] = q_pow(jc[maxn],mod-2);
	exgcd(jc[maxn], mod, x, y);
	inv[maxn] = (x + mod) % mod;

	for (int i = maxn - 1; i >= 2; --i)
		inv[i] = inv[i + 1] * (i + 1) % mod;
	return;
}
long long C(int n, int m)
{
	return jc[n] * inv[m] % mod * inv[n - m] % mod;
}

3、Lucas定理

复杂度:预处理\(O(mod)\) 查询\(O(\log_{mod}m)\)
公式:\(Lucas(n,m,mod)=C(n\%mod,m\%mod)*Lucas(n/mod,m/mod,mod)\\ Lucas(n,0,mod)=1\)
要求模数较小且为素数\(mod\le10^5\)),用来解决大组合数求模问题
这里不必担心如果\(n\%mod<m\%mod\)返回值为0的情况,因为在这样的情况下,一定在取组合数的过程中取到了模数的倍数,返回结果自然是0

  • 当模数较小时
const int mod=997;
long long C[mod][mod];
void pre()
{
	C[0][0] = 1;
	for (int i = 1; i < mod; ++i)
	{
		C[i][1] = i % mod;
		C[i][i] = C[i][0] = 1;
	}
	for (int i = 2; i <= mod; ++i)
		for (int j = 2; j < i; ++j)
			C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
}
long long Lucas(int n, int m)
{
	if (!m) return 1;
	return (C[n % mod][m % mod] * Lucas(n / mod, m / mod) % mod);
}

同样不推荐这种方法

  • 模数较大时
const int mod=997;
long long jc[mod];//阶层数组
long long inv[mod];//逆元数组
void exgcd(long long a, long long b, long long& x, long long& y)
{
	if (b == 0)
		x = 1, y = 0;
	else
	{
		exgcd(b, a % b, y, x);
		y -= a / b * x;
	}
}
long long q_pow(long long x, int n)
{
	long long result = 1;
	while (n)
	{
		if (n & 1)
			result = result * x % mod;
		n >>= 1;
		x = x * x % mod;
	}
	return result;
}
void pre()
{
	long long x, y;
	jc[0] = inv[0] = jc[1] = inv[1] = 1;
	for (int i = 2; i < mod; ++i)
		jc[i] = jc[i - 1] * i % mod;
	//inv[mod-1] = pow(jc[mod-1],mod-2);
		
	exgcd(jc[mod-1], mod, x, y);
	inv[mod-1] = (x + mod) % mod;
	
	for (int i = mod-2; i >= 2; ++i)
		inv[i] = inv[i + 1] * (i + 1) % mod;
	return;
}
long long get_C(int n, int m)
{
	return jc[n] * inv[m] % mod * inv[n - m] % mod;
}
long long Lucas(int n, int m)
{
	if (m == 0)
		return 1;
	return (get_C(n % mod, m % mod) * Lucas(n / mod, m / mod) % mod);
}

两种代码不同点也就是求\(mod\)以内的组合数用了1、2两种方法

十、 卡特兰数

数论学习未完待续……

posted @ 2020-11-15 20:44  DreamW1ngs  阅读(403)  评论(1)    收藏  举报