【全程NOIP计划】简单数论
【全程NOIP计划】简单数论
基础知识与记号
\(\forall\),\(\exist\)
整除:如果$a=bk \(,其中\)a,b,k\(都是整数,则\)b\(整除\)a\(,记作\)b|a$
也称作b是a的约数(因数),a是b的倍数
如何求出n的所有约数?
如果a是n的约数,则n/a也是n的约数,且a和n/a中必有一个小于等于根号n
所以我们直接枚举1到根号n之间的所有数,判断是不是n的因数就可以了
a和b的最大公因数记为\(\gcd(a,b)\),或者\((a,b)\)
特殊地
1.\((a,a)=(0,a)=a\)
2.如果\(a|b\),则\((a,b)=a\)
3.\((a,b)=(a,a+b)=(a,ka+b),(a,b)=(b,a \space mod \space b)\)
4.\((ka,kb)=k*(a,b)\)
5.\((a,b,c)=((a,b),c)\)
6.如果\((a,b)=1\)则称a和b互质,记为\(a \bot b\)
对于整数a,b,\(b>0\),则存在唯一的整数q,r,满足\(a=bq+r\),其中\(0 \leq r <b\)
\(a \div b = q \dots \dots r\)
其中称q为商,r为余数,余数r用\(a \space mod \space b\)表示
如果有两个整数a满足除以b的余数为r,则我们称\(a \equiv a'(mod \space b)\)
唯一分解定理
对于一个整数n,它可以唯一分解成\(n= \prod p_i^{k_i}\)
例如36可以唯一分解为\(36=2^2 \times 3^2\)
定义域为整数的函数被称为数论函数
对于数论函数\(f\)
1.如果\(\forall a\bot b ,f(ab)=f(a)f(b)\),则称\(f\)为积性函数
2.如果\(\forall a,b,f(ab)=f(a)f(b)\),则称\(f\)为完全积性函数
例如\(f(x)=x\)就是一个完全积性函数
欧几里得算法
给定\(a,b\),快速算出\((a,b)\)
引理:若\(a<b\),则\(b \space mod \space a<b/2\)
不断运用\((a,b)=(a,b\space mod \space a)\),直到某个数为0
至少运行\(\log_2^a\)次或者\(log_2^b\)次,就会有某个数变成0
int gcd(int a,int b)
{
return b? gcd(b,a%b):a;
}
拓展欧几里得算法
裴蜀定理
\(\forall a,b, \exist x,y,s.t. ax+by=(a,b)\)
使用构造法来证明这个东西
当b=0时,x=1,y=0
否则:
如果已经求出来\(bx'+(a \mod b)y'=gcd(a,b)\)的解,则\(x=y',y=x'-\lfloor \frac a b \rfloor y'\)
void exgcd(int a,int b,int &x,int &y)
{
if(b==0)
{
x=1,y=0;
return ;
}
exgcd(b,a%b,y,x);
y-=a/b*x;
}
如何求出\(ax+by=(a,b)\)所有的解?
先用exgcd求出一组x0,y0
其他的解为:\(x=x_0+k \frac b {(a,b)},y=y_0-k \frac a {(a,b)}\)
其中最小的正数x为\((\frac b {(a,b)}+x_0 \mod \frac b {(a,b)}) \mod \frac b {(a,b)}\)
欧拉函数
\(\phi(n)\)表示1~n中与n互质的数的个数,称为欧拉函数
例如1~12中,1,5,7,11和12互质,则\(\phi(12)=4\)
怎么求?
设\(n =\prod p_i^{k_i}\)则\(\phi(n)=n \prod(1-\frac 1 {p_i})=\prod p_i^{k_i-1}*(p_i-1)\)
枚举质因子计算就可以了,时间复杂度\(O( \sqrt{n})\)
(拓展)欧拉定理(欧拉函数)
欧拉定理:\(\forall a \bot b ,a^{ \phi (n)} \equiv 1 (\mod n)\)
拓展欧拉定理:\(\forall a,n,a^{2 \phi(n)} \equiv a^{\phi (n)}(mod \space n)\)
也就是说,\(b> \phi(n) \to a^b \equiv a^{b \space mod \space \phi(n)+\phi(n)}(mod \space n)\)
P5110 块速递推
思路
先运用拓展欧拉定理 ,将b缩小到\(2\phi(c)<2c\)的范围之内
\(a^b=(a^{ \sqrt{c}})^{\lfloor \frac b {\sqrt{c}}\rfloor}*a^{b \space mod \space c}\),其中\(\lfloor \frac b {\sqrt{c}} \rfloor <2 \sqrt{c},b \space mod \space \sqrt{c}< \sqrt{c}\)
预处理\((a^{\sqrt{c}})^i,a^j(1\le i \le 2\sqrt{c},1 \le j <\sqrt{c})\)即可
实际上是一种分块的做法
可以预处理,然后再直接查询
实际上就是光速幂,具体模板题为LOJ.162.快速幂 2
通过题目给定的递推公式,我们首先可以推出来一个用来递推的矩阵
然后预处理一下
最后对于每个n,直接通过预处理的数据直接\(O(1)\)输出
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
#include <cmath>
using namespace std;
const int maxn=50005;
const int p=1e9+7;
unsigned long long T,res;
namespace Mker
{
unsigned long long SA, SB, SC;
void init()
{
scanf("%llu%llu%llu", &SA, &SB, &SC);
}
unsigned long long rand()
{
SA ^= SA << 32, SA ^= SA >> 13, SA ^= SA << 1;
unsigned long long t = SA;
SA = SB, SB = SC, SC ^= t ^ SA; return SC;
}
}
struct matrix
{
long long a,b,c,d;
matrix()
{
a=b=c=d=0;
}
matrix operator *(register matrix &s)
{
matrix temp;
temp.a=(a*s.a+b*s.c)%p;
temp.b=(a*s.b+b*s.d)%p;
temp.c=(c*s.a+d*s.c)%p;
temp.d=(c*s.b+d*s.d)%p;
return temp;
}
}base,m1[maxn],m2[maxn];
int main()
{
cin>>T;
Mker::init();
//初始化矩阵
base.a=233;
base.b=666;
base.c=1;
/*进行矩阵递推*/
m1[1]=base;
int len=sqrt(1e9+6);
for(int i=2;i!=len;i++)
m1[i]=m1[i-1]*base;
m2[1]=m1[len-1]*base;
m1[0].a=m1[0].d=1;
m2[0].a=m2[0].d=1;
for(int i=2;i<=len+1;i++)
m2[i]=m2[i-1]*m2[1];
while(T--)
{
matrix ans;
ans.a=1;
int n=Mker::rand()%1000000006;
if(n<=1)
res^=n;
else
{
ans=ans*m2[(n-1)/len]*m1[(n-1)%len];
res^=ans.a;
}
}
cout<<res<<'\n';
return 0;
}
逆元
如果\(ax \equiv 1(mod \space b)\),则称x为a关于模b的逆元,通常记作\(a^{-1}\)
分数取模:\(\frac ab \equiv a*b^{-1}(mod \space p)\),
\(a^{-1}\)存在的充要条件是\((a,b)=1\)
在\([0,b)\)的范围诶,a关于模b的逆元(如果存在的话),是唯一的
等价于求\(ax+by=1\),课可以直接使用拓展欧几里得解决
如果b是质数,则\(\phi(b)=b-1,a*a^{b-2}=a^{b-1}\equiv 1(mod \space b)\),所以逆元为\(a^{b-2}\),使用快速幂计算
如何在\(O(n)\)的时间内求1~n内的模质数p的所有逆元呢?
$i^{-1} \equiv - \lfloor \frac pi \rfloor (p \space mod \space i)^{-1} \iff p \space mod \space i \equiv -\lfloor \frac pi \rfloor *i \iff \lfloor \frac pi \rfloor *i+p \space mod \space i =p \equiv (mod \space p) $
威尔逊定理
\((p-1)! \equiv -1 (mod \space p)\)
\(x^2 \equiv 1 (mod \space p)\)的解仅有\(x \equiv 1,x \equiv -1(mod \space p)\)
所以2~p-2和逆元两两对应
剩下\(1*(p-1) \equiv -1 (mod \space p)\)
中国剩余定理
求解x,满足方程组
令\(M=m_1m_2\dots m_n,M_i=M/m_i\)
显然有\((M_i,m_i)=1\),所以\(M_i\)关于模\(m_i\)的逆元存在,把这个逆元设为\(t_i\)
于是有\(M_it_i \equiv 1 \text{(mod m)},M_it_i \equiv 0 (mod \space m_j)(j \not= i)\)
进一步有\(a_iM_it_i \equiv a_i(mod \space m_i),a_iM_it_i \equiv 0 (mod \space m_j)(j \not= i)\)
因此解为\(x \equiv a_1M_1t_1+a_2M_2t_2+\dots \dots a_nM_nt_n(mod \space M)\),且在模m意义下唯一解
拓展中国剩余定理
求解x,满足方程组\(x \equiv a_i(mod \space m_i)\),其中\(m_i\)不一定两两互质
同样的,在mod \(lcm(m_i)\)的意义下有唯一解
考虑如何每次将两个方程$\begin{cases} x \equiv a_1 \space mod \space m_1 \x \equiv a_2 \space mod \space m_2\end{cases} \(合并成一个方程\)x \equiv a(mod \space m =lcm(m_1,,m_2))$
先用拓展欧几里得算法求出\(k_1m_1-k_2m_2=gcd(m_1,m_2)\)的一组特解\(K_1,K_2\)
$ \begin{cases} x=a_1+K_1 \frac {a_2-a_1} {gcd(m_1,m_2)}m_1 \ x=a_2+K_2 \frac {a_2-a_1}{gcd(m_1,m_2)}m_2 \end{cases}\(可以发现两个不同的解x的差一定是\)lcm(m_1,m_2)$的倍数
所以\(x \equiv a_1+K_1 \frac{a_2-a_1}{gcd(m_1,m_2)}=a_2+K_2 \frac {a_2-a_1}{gcd(m_1,m_2)}m_2(mod \space lcm(m_1,m_2))\)
其他常见的积性函数
\(\sigma_k(n)=\sum_{i|n}i^k\),\(\sigma_0(n)\)又写作\(d(n)\)表示n的约数个数(除数函数)
\(gcd(n,k)\)
完全积性函数:
\(1(n)=1,id_k(n)=n^k,\epsilon(n)=\begin{cases} 1,n=1 \\0,n \not-1\end{cases}\)
\(N_{\delta}= \prod_pp^{\lfloor \frac 1 {p^{\delta}-1} \rfloor},N_{\delta}\)显然收敛
当\(n>N_{\delta}\)时,有:\(d(n)<n^{(1+\delta)\frac {log2}{log\space logn}}\)
普通筛法
以下整数,代指>1的整数
基本思想:整数的整数倍是合数,时间复杂度\(O(nlogn)\)
for(int i=2;i<=n;i++)
{
if(!vis[i])
p[++t]=i;
for(int j=2*i;j<=n;j+=i)
vis[j]=1;
}
埃氏筛
基本思想:素数的整数倍数为合数,时间复杂度\(O(n\space log \space logn)\)
for(int i=2;i<=n;i++)
{
if(!vis[i])
{
p[++t]=i;
for(int j=2*i;j<=n;j+=i)
vis[j]=true;
}
}
线性筛
基本思想:整数的素数倍数是合数
枚举整数的素数倍数,当枚举到的素数是当前整数的因数时候停止
设n的最小值因子为\(p_n\),n会且仅在\(i=n/p_n\)倍筛到
for(int i=2;i<=n;i++)
{
if(!vis[i])
p[++t]=i;
for(int j=1;j<=t&&i*p[j]<=n;j++)
{
vis[i*p[j]]=1;
if(i%p[j]==0)
break;
}
}
区间筛
求\([a,b]\)中的所有质数,\(a,b \leq 10^{12},b-a \leq 10^6\)
基本思想:埃氏筛
1.先筛出\(1到 \sqrt{b}\)的所有质数
2.用这些质数筛去\([a,b]\)中的合数
线性筛求积性函数
对于每个合数n,在线性筛的过程中都会拆成\(\frac n {p_n}\)和\(p_n\)
设\((p_n^{\inf},n)=p_n^k\),则\(\frac n {p_n^k}\)和\(p_n^k\)是互质的
1.算出\(f(p_n^k)\)
2.对于其他合数,\(f(n)=f(\frac n {p_n^k})*f(p_n^k)\)
//phi
for(int i=2;i<=n;i++)
{
if(!vis[i])
{
p[++t]=i;
phi[i]=i-1;
}
for(int j=1;j<=t&&i*p[j]<=n;j++)
{
vis[i*p[j]]=true;
if(i%p[j])
phi[i*p[j]]=phi[i]*(p[j]-1);
else
{
phi[i*p[j]]=phi[i]*p[j];
break;
}
}
}
P2303 Longge的问题
思路
1.直接计算可以得65分,非常良心
我们设 \(t(x)\) 为 \(1 - n\) 中与 \(n\) 的最大公因数为 \(x\) 的数的个数,即
考虑 \(\phi(x) = \sum\limits_{i = 1}^n[gcd(i, n) = 1]\)
即 \(t(x) = \phi(\lfloor\frac nx\rfloor)\)
由于\(\gcd(i, n) | n\),考虑化简原式为:
对于所有 \(d | n\) 可以在 \(O(\sqrt n)\) 的时间复杂度内枚举,\(\phi\) 函数可以在 \(O(\sqrt n)\) 的时间复杂度内求出,故时间复杂度为 \(O(因数个数 * \sqrt n)\)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <string>
using namespace std;
long long n;
int main()
{
scanf("%lld",&n);
long long ans=n;
for(register long long i=2;i*i<=n;++i)
{
if(n%i==0)
{
int b=0;
while(n%i==0)
{
b++;
n/=i;
}
ans/=i;
ans*=b*i-b+i;
}
}
if(n>1)
{
ans/=n;
ans*=n+n-1;
}
printf("%lld",ans);
return 0;
}
CF757B Bash's Big Day
思路
\(gcd>1\),说明存在一个正整数\(d>1\),满足d整除s内的所有元素
枚举\(d=2 \to max_{a_i}\)并统计答案
设\(V=max_{a_i}\),则复杂度为\(O(VlnV)\)
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>using namespace std;const int maxn=100005;int n,tot,maxx=1;int a[maxn],fac[maxn];inline int read(){ int x=0,f=1;char ch=getchar(); while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();} return x*f;}void record(int x){ for(int i=2;i*i<=x;i++) { if(x%i==0) { fac[i]++; fac[x/i]++; } if(i*i==x) fac[x/i]--; } fac[x]++; return ;}int main(){ n=read(); for(int i=1;i<=n;i++) { a[i]=read(); record(a[i]); } sort(a+1,a+1+n); for(int i=2;i<=a[n];i++) maxx=max(maxx,fac[i]); cout<<maxx<<endl; return 0;}
CF776B Sherlock and his girlfriend
思路
注意到这是二分图,一边是质数,一边是合数
运用埃氏筛把合数都筛掉
然后质数和合数的颜色不同就可以了
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>using namespace std;int n;bool p[100005];int main(){ cin>>n; p[0]=true,p[1]=true; for(int i=2;i*i<=n+1;i++) { if(!p[i]) { for(int j=i*2;j<=n+1;j+=i) p[j]=true; } } if(n<3) cout<<1<<'\n'; else cout<<2<<'\n'; for(int i=2;i<=n+1;i++) cout<<(p[i]? 1:0)+1<<" "; cout<<'\n'; return 0;}
P4139 上帝与集合的正确用法
思路
题目要求出的是\(2^{2^{2^{2…}}} \space mod \space p\)的值
根据扩展欧拉定理
可以通过一个函数来递归求
可是题目中让求的是无限层,并不是有限层,怎么办啊?
不要紧,在一层层的递归中,\(p\)一遍遍的被求其\(φ\)值,最终必定会降到\(1\)。原因很简单,因为:\(φ(p)<p\space(p>1)\)
而且降的很快,所以递归很快就会结束
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>using namespace std;const int maxn=1e7+5;int T,tot;int pri[1000005],phi[maxn];bool vis[maxn];inline int read(){ int x=0,f=1;char ch=getchar(); while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();} return x*f;}int ksm(int a,int b,int p){ int ans=1%p; for( ;b;b>>=1) { if(b&1) ans=(long long)ans*a%p; a=(long long)a*a%p; } return ans;}int dfs(int x){ if(x==1) return 0; int n=phi[x]; return ksm(2,dfs(n)+n,x);}void init(){ for(int i=2;i<=maxn;i++) { if(!vis[i]) pri[++tot]=i,phi[i]=i-1; for(int j=1;j<=tot&&pri[j]*i<=maxn;j++) { vis[pri[j]*i]=1; if(i%pri[j]) phi[i*pri[j]]=phi[i]*phi[pri[j]]; else { phi[i*pri[j]]=phi[i]*pri[j]; } } }}int main(){ init(); T=read(); while(T--) cout<<dfs(read())<<'\n'; return 0;}
一道例题
题目
求\([1,10^7]\)内的所有素数,内存限制1MB
思路
吧\([1,10^7]\)分成k段分别求解
对于区间\([l,r]\),枚举不大于\(\sqrt{r}\)的所有素数p,在\([l,r]\)中筛去p的倍数
需要预处理\([1,\sqrt{n}]\)的所有素数
时间复杂度\(k\sqrt{n}+nlog^{log^n}\)
空间复杂度\(\sqrt{n}+n/k\)
P2568 gcd
思路
线性筛出不大于\(N\)的所有素数\(p\)。问题就转化为求\((x,y)=p\)的个数
设\(x=x'p,y=y'p\)那么有\((x',y')=1\)且\(1 \le x,y \le N/p\)
转化为求\((x,y)=1\)且\(1 \le x,y \le n\)的个数
答案为\(2\sum_{i=1}^n\phi(i)-1\)
线性筛筛出欧拉函数,预处理前缀和就可以了
然后对于每个小于等于\(N\)的素数处理累加到答案里就可以了
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>#define int long longusing namespace std;const int maxn=1e7+5;int pri[maxn],phi[maxn],s[maxn];bool vis[maxn];int n,tot;long long ans=0;void init(int x){ phi[1]=1; for(int i=2;i<=x;i++) { if(!vis[i]) pri[++tot]=i,phi[i]=i-1; for(int j=1;j<=tot&&pri[j]*i<=x;j++) { vis[pri[j]*i]=1; if(i%pri[j]) phi[i*pri[j]]=phi[i]*phi[pri[j]]; else { phi[i*pri[j]]=phi[i]*pri[j]; } } } for(int i=1;i<=x;i++) s[i]=s[i-1]+phi[i];} signed main(){ cin>>n; init(n); for(int i=1;i<=tot;i++) ans+=(2*s[n/pri[i]]-1); cout<<ans<<'\n'; return 0;}
P2155 沙拉公主的困惑
思路
感谢@Siyuan
PS:越来越意识到开long long的重要性
答案是:
我们类比gcd
我们知道\(n \ge m\),所以我们可以把\([1,n!]\)分成若干段
准确地来说是 \(\dfrac {n!} {m!}\)段,每段的答案是一样的,这是由gcd的更相减损术得到的
然后我们就可以更换求和的上界
右边的就是1到m!中与m互质的数,也就是
我们知道,设\(n =\prod p_i^{k_i}\),则\(\varphi(n)=n \prod(1-\frac 1 {p_i})=\prod p_i^{k_i-1}*(p_i-1)\)
所以我们设\(m!=\prod_{i=1}^kp_i^{c_i}\),可以得到:
所以讲这个东西带入上面的式子,答案就是
那个玩意儿我们可以用线性筛求
时间复杂度\(O(N+T)\)
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>#define int long longusing namespace std;const int maxn=1e7+5;int T,R;int tot,pri[maxn],fac[maxn],inv[maxn];int ans[maxn];bool vis[maxn];void init(){ fac[0]=fac[1]=1; inv[0]=inv[1]=1; ans[0]=ans[1]=1; for(int i=2;i<=maxn-5;i++) { fac[i]=(long long)(fac[i-1]*i)%R;//算阶乘 inv[i]=(long long)((R-R/i)*inv[R%i])%R;//逆元 } //欧拉筛 for(int i=2;i<=maxn-5;i++) { if(!vis[i]) pri[++tot]=i; for(int j=1;j<=tot&&i*pri[j]<=maxn-5;j++) { vis[i*pri[j]]=1; if(i%pri[j]==0) break; } } for(int i=2;i<=maxn-5;i++) { ans[i]=ans[i-1]; if(!vis[i]) ans[i]=(long long)(ans[i]*(i-1)%R*inv[i]%R)%R; }}signed main(){ cin>>T>>R; init(); while(T--) { int n,m; cin>>n>>m; cout<<(long long)fac[n]*ans[m]%R<<'\n'; } return 0;}
CF632D Longest Subsequence
思路
我们要选出尽可能多的数字,满足他们的最小公倍数不大于m
分析题意可以得到,如果一个数大于m的话那么它和其他数的最小公倍数一定大于m
所以先把大于m的数字筛掉
然后
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>using namespace std;const int maxn=1000005;int n,m;int a[maxn],ans[maxn];int cnt[maxn];bool vis[maxn];int main(){ std::ios::sync_with_stdio(false); cin>>n>>m; for(int i=1;i<=n;i++) { cin>>a[i]; if(a[i]<=m) cnt[a[i]]++; } for(int i=1;i<=n;i++) { if(a[i]<=m&&!vis[a[i]]) { vis[a[i]]=true; for(int j=1;j*a[i]<=m;j++) ans[a[i]*j]+=cnt[a[i]]; } } int maxx=-9999999; int lcm=1; for(int i=1;i<=m;i++) { if(maxx<ans[i]) { maxx=ans[i]; lcm=i; } } cout<<lcm<<" "<<maxx<<'\n'; for(int i=1;i<=n;i++) { if(lcm%a[i]==0) cout<<i<<" "; } cout<<'\n'; return 0;}
CF582A GCD Table
思路
为啥CF的题目就是这么妙呢?
是我不配了
观察得到三个结论
1.G中最大的数也一定是a中最大的数,因为\(gcd(a,a)=a\),如果它是最大的话,也一定是最大的
2.G中次大的数也一定是a中次大的数
3.第三第四大的数可能是由最大和次大的GCD产生的;
所以算法就有这样的步骤
1.令p为G中最大的数,在G中删掉p,在a中加入p
2.对于a中的所有其他数,设为q,在G中删除两个\(gcd(p,q)\)
3.若G为空则结束,否则回到1
则时间复杂度为\(O(n^2logG_{i,j})\)
因为值域到\(10^9\),所以要用map处理
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <string>#include <map>using namespace std;const int maxn=505;int n,cnt,tot;int arr[maxn],a[maxn*maxn];map <int,int> m;int gcd(int a,int b){ return b? gcd(b,a%b):a;}int main(){ cin>>n; for(int i=1;i<=n*n;i++) { int x; cin>>x; if(!m[x]) a[++cnt]=x; m[x]++; } //从小到大排序,方便从大到小遍历 sort(a+1,a+1+cnt); for(int i=cnt;i>=1&&tot<n; ) { //如果前面没有,就一直减 while(!m[a[i]]) i--; arr[++tot]=a[i]; m[a[i]]--; for(int j=1;j<tot;j++) m[gcd(a[i],arr[j])]-=2; //它和其他数的gcd都删掉了,所以要减两遍 } for(int i=1;i<=tot;i++) cout<<arr[i]<<" "; return 0; }