质数,同余
gcd 和 lcm
就是最大公因数和最小公倍数。比较常规,一般求的时候是先用辗转相除法求出最大公因数,然后再通过 \(lcm(a,b) = a \times b \div \gcd(a,b)\) 求出最小公倍数。
int gcd(int x,int y) {
if (!y) return x;
return gcd(y,x % y);
}
质数
最小的质数是 \(2\),没有最大的质数。
证明:
最小的质数显然是 \(2\),用反证法证明没有最大的质数。设最大的质数为 \(p_n\),则我们令一个新的数 \(x = p_1 \times p_2 \times p_3 ... \times p_n + 1\),显然之前的任何一个质数都不是这个数的质因子,所以这个数是比 \(p_n\) 更大的一个质数,得证。
筛质数
线性筛:每一个合数都只筛掉一遍,每个合数都只在他的最小质因子处被筛一次
for(int i=2;i<=n;++i)
{
if(!isn_prime[i])
{
prime[++cnt]=i;
}
int temp=i;
for(int j=1;j<=cnt&&prime[j]*temp<=n;j++)
{
isn_prime[prime[j]*temp]=true;
if(temp%prime[j]==0)break;//欧拉筛的核心,将合数的倍数省去不标
}
}
分解质因数的话,可以先线性筛记录一下最小质因子,再一个个求,这个是最快的。
算数基本定理
设 \(n\) 是一个大于 \(1\) 的正整数,则可写成 \(n = p_1 p_2 ... p_k\) ,其中 \(p_i\) 都是质数,并且在不管顺序的情况下,这种有且仅有一个
证明:
同样用反证法,如果还有一个序列使得 \(n = q_1 q_2 ... q_m\) ( \(q_i\) 都是质数),因为不计次序,不妨设两个序列都是单调不减的序列。
因为序列中的数都是质数,所以 \(p_1 p_2 ... p_k | q_1 q_2 ... q_m\) ,因此可以得到 \(p_1 | q_1 q_2 ... q_m\) ,进而可以得知 \(p_1 | q_i\) 因为这两个数都是质数,所以 \(p_1 = q_i\) ,同理可得:\(q_1 = p_j\) 。因为这两个序列都是单调不减序列,所以:
即 \(q_1 = p_1\),然后两个序列同时除去一个 \(q_1\) 和 \(p_1\) ,所以这个式子就变成了:
再重复上面的操作,就可以得到这两个序列是完全相同的,所以定理得证。
通过算数基本定理可以很容易证明 \([a,b] = a * b / (a,b)\),具体证明就不过多阐述
同余
如果 \(a \bmod m = b \bmod m\) ,那么称 \(a,b\) 对模 \(m\) 同余,写作: \(a \equiv b \pmod{m}\)
直观理解可以将自然数数轴截一段,首尾详解变成一个环, \(0\) 和 \(p - 1\) 相邻,所有原来的数学运算均在这个环上进行。
余数域 \(0,1...,p - 1\) 被称作 \(p\) 的剩余系。
同余有几个结论:
-
\(a \equiv b \pmod{m} \Leftrightarrow m | (a - b)\)
-
若 \(a \equiv b \pmod{m}\) 且 \(c \equiv d \pmod{m}\) ,那么 \(a + c \equiv b + d \pmod{m}\) 且 \(a \times c \equiv b \times d \pmod{m}\) (可以加、减、乘,但是不能除)
-
若 \(a \times c \equiv b \times c \pmod{m}\) ,那么 \(a \equiv b \pmod{\frac{m}{(m,c)}}\) ,特别的,当 \(\gcd(m,c) = 1\) 时,\(a \equiv b \pmod{m}\)
-
若 \(a \equiv b \pmod{m}\) ,那么对于 \(m\) 的任意一个正因数 \(n\) ,都有 \(a \equiv b \pmod{n}\)
-
若 \(a \equiv b \pmod{m}\) 并且 \(a \equiv b \pmod{n}\) ,那么 \(a \equiv b \pmod{[m,n]}\)
费马小定理
设 \(p\) 为质数,那么对于 \(a \in Z\) ,都有 \(a^p \equiv a \pmod{p}\) ,特别的,如果 \(a\) 与 \(p\) 互质的话,那么 \(a^{p - 1} \equiv 1 \pmod{p}\)
证明:
按照《初等数论》一书所说,此处是一个利用二项式定理的直接证明。
我们只需要证明当 \(a \in N^*\) 的情况就行,而且显而易见的是,如果 \(a = 1\) 的话,费马小定理是显然成立的。
现在设 \(a = n (n \in N^*)\) 时命题成立,即 \(p | (n ^ p - n)\) 则:
这里用到了当 \(1 \le k \le p - 1\) 时都有 \(p| \mathrm{C}^{k}_{p}\),可以展开证明。
所以对于任意 \(a \in N^*\) ,有 \(p | (a^p - a)\) ,结合 \(p \nmid a\) 时,\(\gcd(a,b) = 1\) ,可知此时有 \(p | (a ^{n - 1} - 1)\),定理获证。
应用:求逆元、质数判定(要结合下面的然后用一堆奇怪的算法,省去)
二次探测定理
如果 \(p\) 是一个奇素数,则 \(x^2 \equiv 1 \pmod{p}\) 的解为 \(x \equiv 1 \pmod{p}\) 或 \(x \equiv -1 \pmod{p}\)
证明:
由 \(x^2 \equiv 1 \pmod{p}\) 可以得到 \(x^2 - 1 \equiv 0 \pmod{p}\) ,我们可以对其进行因式分解,得到 \((x - 1)(x + 1) \equiv 0 \pmod{p}\)
又因为 \(p\) 是一个质数,所以我们可以得出上面的结论。这个定理和费马小定理的“逆定理”结合可以用来判断一个数是不是质数,但是那个方法我没有看懂,所以暂且搁置(费马小定理的“逆定理”不是一定成立的,这个方法是有很大的可能性能判断正确但不保证正确性(Miller-Rabin算法))。
欧拉定理
其实上面的费马小定理就是欧拉定理的一种特殊形式,欧拉定理这里需要用到欧拉函数,故先说说欧拉函数。
欧拉函数
定义:\(\varphi (m)\) 指的是从 \(1\) 到 \(m\) 之间所有与 \(m\) 互质的数的个数。
欧拉函数的计算公式:
这里的 \(p_i\) 指的是 \(x\) 分解质因数后分解出来的质因数(这里省去了质因数的次数),证明的话要用到容斥原理,具体的就冒过去了。
然后欧拉函数的性质(待补充):
-
\(\varphi (mn) = \varphi (n) \times \varphi (m)\),这个式子成立当且仅当 \(\gcd(n,m) = 1\)
-
除了 \(N = 2\) ,所有 \(\varphi(N)\) 都是偶数
-
如果 \(N\) 是质数,那么 \(\varphi (N) = N - 1\)
-
不大于 \(N\) ,且与 \(N\) 互质的一切数,它们的和是 \(\frac{1}{2} N \times \varphi (N)\)
线性筛求 \(1 - N\) 之间所有数的欧拉函数的方法:
我们可以用埃拉托斯特尼筛的思想,每次找到一个质数,都把他的倍数更新掉。
void f(int x){
euler[1]=1;
for(int i=2;i<=x;i++){
if(!st[i]){
prime[++cnt]=i;
euler[i]=i-1;//质数的欧拉函数为i-1
}
for(int j=1;prime[j]<=x/i;j++){
int t=prime[j]*i;
st[t]=true;
if(i%prime[j]==0){
euler[t]=euler[i]*prime[j];
break;
}
euler[t]=euler[i]*(prime[j]-1);
}
}
}
然后是欧拉定理,欧拉定理的内容是:设 \(\gcd(a,m) = 1\),则 \(a ^{\varphi (m)} \equiv 1 \pmod{m}\),这里的 \(m\) 是正整数, \(a\) 是整数。
证明:
设 \(a_1,a_2,a_3...a_{\varphi (m)}\) 是模 \(m\) 的一个简系(如果从与 \(m\) 互质的剩余类中任意取一个数,这些数(共 \(\varphi (n)\))个称为模 \(m\) 的一个简化剩余系,即简系)
由 \(\gcd(a,m) = 1\) 可得 \(aa_1,aa_2,aa_3,...aa_{\varphi (m)}\) 也是一个模 \(m\) 的简系,因此我们可以得到:
通过这个式子我们可以进一步移项得知:
又因为 \(\gcd(a_i,m) = 1\) ,所以我们可以进一步推知:\(\gcd(a_1 a_2 a_3...a_{\varphi (m)},m) = 1\) ,也就可以证明出 \(a ^ {\varphi (m) - 1} | m\) ,定理得证
裴蜀定理
若 \(a,b\) 是整数,且 \(\gcd(a,b) = d\) ,那么对于任意整数 \(x,y\) 一定是 \(d\) 的倍数。特别地,一定存在 \(ax + by = d\) 成立。
还可以证明欧拉函数式积性函数,此处略去。
而求上面式子的 \(x,y\) 的方法就是 \(exgcd()\)
int exgcd(int a,int b,int &x,int &y) {
if (b == 0) {
x = 1,y = 0;
return a;
}
int g = exgcd(b,a%b,y,x);
y -= a / b * x;
return g;
}
求出一组解之后,自然而然就可以求出通解了,这里很好推,就不写了。
这个还可以用于求逆元,由裴蜀定理可知,若 \(a\) 和 \(p\) 互质,那么存在 \(ax + py = 1\) ,即 \(ax \equiv 1 \pmod{p}\) ,这样子就可以求出 \([0,p)\) 之间 \(a\) 在模 \(p\) 意义下的逆元(即这个式子求出来的 \(x\) )
逆元
如果关于 \(x\) 的方程 \(ax \equiv 1 \pmod{p}\) 的一个解为 \(x_0\) ,那么就称 \(x_0\) 是 \(a\) 在模 \(p\) 意义下的逆元,记作 \(x_0 = a^{-1}\) ,也记作 \(x = inv(a)\)
逆元不一定存在(eg.2和4),\(a\) 在模 \(p\) 意义下的逆元存在的充要条件为 \(\gcd(a,b) = 1\)
有了逆元之后,我们就可以在模意义下做除法,求分数了。
求逆元的方法:
单个逆元:
-
快速幂(欧拉定理,费马小定理)
-
\(exgcd\)
预处理多个逆元:
-
线性筛
-
线性递推
线性递推求逆元
是一个公式:
根据这个式子可以线性复杂度预处理 \(n\) 以内的逆元,也可以写成递归的形式用 \(O(\log n)\) 的复杂度求出单个数的逆元。
证明就省去了,以后如果有时间再写好了。
中国剩余定理
即求解关于 \(x\) 的同余方程组:
这个方程可以很轻易的求出来 \(x\) 的解最小为 \(23\) ,(求不出来建议回去重上小学)
类似于这样的一组一元线性同余方程组:
当整数 \(m_1,m_2...m_n\) 两两互质时,对于任意整数 \(a_1,a_2...a_n\) ,这个方程都有解,其解可以用以下方式构造而得:
设 \(M = m_1 \times m_2 \times ... \times m_n\) ,再令 \(M_i = \frac{M}{m_i}\)
对于每一个线性同余方程而言,我们可以很容易的求出每一个同余方程的解 \(t_i\) 然后我们就可以构造出一个解 \(x_0\):\(\sum\limits_{i = 1}^{n} a_iM_it_i \bmod M\)
通解就是在这个 \(x_0\) 上面加几个 \(M\)。
如果不互质的情况下,我们就先合并成互质的然后再这样处理,提高级不会考,所以证明就冒过去了。
T1
令 \(f(i)\) 为 \(i\) 的因子个数,求 \(\sum\limits_{i = 1}^{n} f(i)\) 的值
\(solution\):
数论分块
这一题第二天写了,所以贴贴代码。思路很好懂,也一遍过了,就不多说
int main(){
lwl n;
n = fr();
if (n <= 1e6) {
lwl ans = 0;
for (int i = 1; i <= n; i ++) {
ans += n / i;
}
fw(ans);
return 0;
}
lwl ans = 0;
for (lwl i = 1; i <= n; i ++) {
if (n / i != n / (i + 1)) {
ans += n / i;
continue;
}
lwl t = n / i;
lwl l = i,r = n;
lwl k = i;
while (l <= r) {
lwl mid = (l + r) >> 1;
if (n / mid >= t) l = mid + 1,k = mid;
else r = mid - 1;
}
ans += t * (k - i + 1);
i = k;
}
fw(ans);
return 0;
}
T2
给你一个有 \(n\) 个点的树,每个点有一个权值,给出 \(q\) 次询问,每次询问一个点 \(x\) ,找出这个点的最近的祖先,使得这两个点的权值的最大公约数不为 \(1\)。
\(n,q \le 10^5\) ,\(1 \le w[i] \le 2 \times 10 ^ 6\)
\(solution\):
我们在递归的时候开一个桶,记录一下每一个质因数网上找最近的地方在哪里。然后每一个节点更新的时候都记录一下这个点更新了哪一个质因数,更新前是多少。
查询的时候我们就按照他的质因数去找。
第二天下午模拟赛的时候也做了这道题,所以贴贴代码:
一开始判断质数和分解质因子的地方写的有点问题,不过我会写线性筛了好诶(虽然一开始记录最小质因子的时候记录错了),然后这个地方分解质因数的时候每个质因子只能算一次,不然后面更新的时候会有问题。
int n,m,maxn;
int w[N];
vector<int> e[N],zs;
int de[M];
vector<pii> gx[N];
int pre[M];
int dfn[N],idx;
void init() {
for (int i = 2; i <= 2e6; i ++) {
if (!pre[i]) {
pre[i] = i;
zs.push_back(i);
}
for (int j = 0; j < zs.size() && zs[j] <= 2e6 / i; j ++) {
pre[zs[j] * i] = zs[j];
if (i % zs[j] == 0) break;
}
}
}
void dfs(int u) {
dfn[u] = ++ idx;
for (auto &it : gx[u]) {
it.se = de[it.fi];
de[it.fi] = u;
}
for (auto v : e[u]) {
dfs(v);
}
for (auto it: gx[u]) {
de[it.fi] = it.se;
}
}
int main(){
n = fr(),m = fr();
init();
for (int i = 1; i <= n; i ++) {
w[i] = fr();
while (pre[w[i]] != 0 && w[i] != 1) {
gx[i].push_back({pre[w[i]],0});
int t = pre[w[i]];
while (w[i] % t == 0)
w[i] /= t;
}
}
for (int i = 1; i < n; i ++) {
int a = fr(),b = fr();
e[min(a,b)].push_back(max(a,b));
}
dfs(1);
while (m --) {
int x = fr();
int ans = 0;
for (auto u : gx[x]) {
if (dfn[u.se] > dfn[ans]) {
ans = u.se;
}
}
if (!ans) wj;
else {
fw(ans);
ch;
}
}
return 0;
}
练习
A.快速求质数
直接暴力就可以了,但是这里要注意的一点就是如果 \(n = 1\) 的话是不能输出 \(1\) 的(因为 \(1\) 不是质数,这是显然的),但是如果用单纯的试除法的话是会输出 \(1\) 的,所以要特判一下,其它就是很单纯的试除法了。
lwl n = fr(),m = fr();
for (lwl i = max(n,(lwl)2); i <= m; i ++) {
bool flag = true;
for (lwl j = 2; j <= sqrt(i); j ++) {
if (i != j && i % j == 0) {
flag = false;
break;
}
}
if (flag) {
fw(i),kg;
}
}
B.一道大水题
这个题先分解质因数,然后把分解出来的质因数的个数记录一下,然后算个数。
算个数的时候我们假设全部质因子都不同(就算相同也先按不同算),那么这个路径的数量显然就是 \(sum!\) ( \(sum\) 是所有质因子的次数加起来的值),那么我们再考虑有什么情况需要去重。
发现如果一个质因子有 \(cnt\) 个的话那么这一个质因子的内部排列顺序就有 \(cnt!\) 个,所以我们就可以得出来这个式子:
这个式子出来了之后就很好解决了,要注意的一点就是这里如果用那种不是线性的筛法的话会 \(TLE\) ,但是用线性筛的话可以每次记录一下最小质因子,也就可以比较快的质因子分解,前面那个逆元和阶乘也要预处理一下。
不然会 \(TLE\) 的。恼。
int n,Q;
vector<int> zs;
int cnt[N];
lwl pre[N];
lwl f[N],nf[N];
lwl ksm(lwl a,lwl k) {
lwl ans = 1;
while (k) {
if (k & 1) ans = ans * a % mod;
a = a * a % mod;
k >>= 1;
}
return ans;
}
lwl ny(int x) {
return ksm(x,mod - 2);
}
void init() {
for (int i = 2; i <= n; i ++) {
if (!pre[i]) {
zs.push_back(i);
}
for (int j = 0; zs[j] <= n / i; j ++) {
if (!pre[zs[j] * i]) pre[zs[j] * i] = zs[j];
if (i % zs[j] == 0) break;
}
}
f[0] = 1,nf[0] = 1;
for (int i = 1; i <= 10000; i ++) {
f[i] = (lwl)f[i - 1] * i % mod;
nf[i] = (lwl)nf[i - 1] * ny(i) % mod;
}
}
int main(){
n = fr(),Q = fr();
init();
while (Q --) {
int x = fr();
lwl ans = 1;
int sum = 0;
while (pre[x]) {
int a = 0;
int p = pre[x];
while (x % p == 0) x /= p, a ++;
ans = ans * nf[a] % mod;
sum += a;
}
if (x != 1) sum ++;
ans = ans * f[sum] % mod;
fw(ans);
ch;
}
return 0;
}
C.千鸽笼
数据范围很小,所以我们先预处理出这里所有的质数,然后对于每一个数都弄一个并查集来维护一下这个鸽子可以跟其他哪几个鸽子合并。
遍历每一个质数,然后再遍历一下每一个数,如果这个质数是这个数的质因子的话,我们就把质数和这个数放在一个并查集里面,然后最后判断的话就那样写就可以了,我不知道不加第一个判断过不过的了,但是我觉得是过不了的。因为这个质数有可能不在 \([a,b]\) 这个区间内,但是他的 \(h[]\) 确实是自己,所以加了个特判。
int a,b,p;
bool flag[N];
vector<int> zs;
int h[N];
int siz[N];
void init() {
flag[1] = true;
for (int i = 2; i <= b; i ++) {
if (!flag[i]) {
zs.push_back(i);
for (int j = 2; j <= b / i; j ++)
flag[i * j] = true;
}
}
}
int find(int x) {
if (x != h[x]) h[x] = find(h[x]);
return h[x];
}
int main(){
a = fr(),b = fr(),p = fr();
init();
int cnt = zs.size();
for (int i = 1; i <= b + cnt; i ++) {
h[i] = i;
siz[i] = 1;
}
for (int j = 0; j < cnt; j ++) {
int u = zs[j];
if (u < p) continue;
for (int i = a; i <= b; i ++) {
if (i % u == 0) {
int ha = find(i),hb = find(j + 1 + b);
if (ha == hb) continue;
h[hb] = ha;
siz[ha] += hb;
}
}
}
int ans = 0;
for (int i = 1; i <= b + cnt; i ++) {
if (h[i] == i) {
if (i < a || i > b) {
ans += (siz[i] > 1);
} else ans ++;
}
}
fw(ans);
return 0;
}
D.质因子分解
这个是 \(ACwing\) 上面的原题,甚至数据范围还没有 \(ACwing\) 上面大。
感觉是道比较典的题,但是代码很好理解,所以直接贴代码()
bool flag[N];
vector<int> zs;
map<int,int> ans;
int main(){
int n = fr();
for (int i = 2; i <= n; i ++) {
if (!flag[i]) {
zs.push_back(i);
}
for (int j = 0; zs[j] * i <= n; j ++) {
flag[zs[j] * i] = true;
if (i % zs[j] == 0) break;
}
}
for (auto i : zs) {
int ans = 0;
for (int j = n; j; j /= i) {
ans += j / i;
}
fw(i),kg;
fw(ans),ch;
}
return 0;
}
E.5的倍数的个数
这个主要就是推一个式子就可以了,我们先考虑单独的一个字符串,显然我们要考虑的就是 \(0,5\) 这两个字符的位置,假设 \(0,5\) 在字符串的第 \(a_1,a_2...a_n\) 位(从 \(0\) 开始算),那么一个字符串可以有的次数就是 :
然后我们考虑多个一样的字符串拼在一起的情况,那么其实就是这个式子( \(len\) 指的是字符串的长度,\(ans\) 就是上面求出来的值):
对于前面这个式子我们有两种处理方式,第一种就是直接用等比数列求和的公式(推式子的话直接将这个式子乘 \(2^{k - 1}\) 然后再和上面的式子相减就可以推了)
还有一种方式是分块计算这个值,这是老师说的,但是我不喜欢用分块,所以就这样吧。直到可以用分块算就可以了(雾)
lwl ksm(lwl a,lwl k) {
lwl ans = 1;
while (k) {
if (k & 1) ans = a * ans % mod;
a = a * a % mod;
k >>= 1;
}
return ans;
}
lwl ny(lwl x) {
return ksm(x,mod - 2);
}
int main(){
lwl n;
string s;
cin >> s;
n = s.length();
lwl k = fr();
lwl ans = 0;
for (int i = 0; i < n; i ++) {
if (s[i] == '5' || s[i] == '0')
ans = (ans + ksm(2,i)) % mod;
}
ans = ans * (ksm(2,k * n) - 1) % mod * ny(ksm(2,n) - 1) % mod;
fw(ans);
return 0;
}

浙公网安备 33010602011771号