数论部分
常用技巧
快速幂
快速幂的基础:
p
q
m
o
d
r
=
{
(
p
2
)
⌊
q
÷
2
⌋
m
o
d
r
q
m
o
d
2
=
0
p
(
(
p
2
)
⌊
q
÷
2
⌋
)
m
o
d
r
q
m
o
d
2
=
1
1
q
=
1
or
(
p
=
0
and
q
≠
1
)
p^q\bmod r=\begin{cases}(p^2)^{\lfloor q\div2\rfloor}\bmod r&q\bmod2=0\\p((p^2)^{\lfloor q\div2\rfloor})\bmod r&q\bmod2=1\\1&q=1\text{ or }(p=0\text{ and }q\neq1)\end{cases}
pqmodr=⎩
⎨
⎧(p2)⌊q÷2⌋modrp((p2)⌊q÷2⌋)modr1qmod2=0qmod2=1q=1 or (p=0 and q=1)
然后,我们可以一级一级的分下去,分到1为止,时间复杂度为 Θ ( log q ) \Theta(\log q) Θ(logq),可以接受。
int fp(int p,int q,int r){
if(q==0||p==1) return 1;
int g=fp(p*p,q>>1,r)%r;
if(q&1) g=g*p%r;
return g;
}
最大公约数
使用欧里几得的定理:
gcd
{
a
,
b
}
=
{
b
a
m
o
d
b
=
0
gcd
{
b
,
a
m
o
d
b
}
Otherwise
\gcd\{a,b\}=\begin{cases}b&a\bmod b=0\\\gcd\{b,a\bmod b\}&\text{Otherwise}\end{cases}
gcd{a,b}={bgcd{b,amodb}amodb=0Otherwise
int gcd(int a,int b){
return a%b==0?b:gcd(b,a%b);
}
当然,如果你想背代码的话,可以将下面的记下来:
int gcd(int a,int b){
if(b==0) return a;
if(a==0) return b;
int acnt,bcnt;
for(acnt=0;(a&1)==0;acnt++) a>>=1;
for(bcnt=0;(b&1)==0;bcnt++) b>>=1;
if(bcnt<acnt) acnt=bcnt;
while(1){
if(a<b) swap(a,b);
if((a-=b)==0) return y<<acnt;
while((x&1)==0) x>>=1;
}
}
最小公倍数
因为,
a
b
=
gcd
(
a
,
b
)
⋅
lcm
(
a
,
b
)
ab=\gcd(a,b)\cdot\text{lcm}(a,b)
ab=gcd(a,b)⋅lcm(a,b),
所以,
lcm
(
a
,
b
)
=
a
b
gcd
(
a
,
b
)
\text{lcm}(a,b)=\frac{ab}{\gcd{(a,b)}}
lcm(a,b)=gcd(a,b)ab。
扩展欧几里得算法
已知 a , b a,b a,b,求一组 p , q p,q p,q使得 a p + b q = gcd ( a , b ) ap+bq=\gcd(a,b) ap+bq=gcd(a,b)。
∵ gcd ( a , b ) = gcd ( b , a m o d b ) \because \gcd(a,b)=\gcd(b,a\bmod b) ∵gcd(a,b)=gcd(b,amodb)
∴ a p + b q = gcd ( b , a m o d b ) \therefore ap+bq=\gcd(b,a\bmod b) ∴ap+bq=gcd(b,amodb)
∴ a p + b q = b p ′ + ( a m o d b ) q ′ = b p ′ + ( a − b a ⋅ b ) q ′ = b p ′ + a q ′ − b q ′ ⋅ b a = a q ′ + b ( p ′ − b a ⋅ q ′ ) \begin{aligned}\therefore ap+bq&=bp'+(a\bmod b)q'\\&=bp'+(a-\frac{b}{a}\cdot b)q'\\&=bp'+aq'-bq'\cdot\frac{b}{a}\\&=aq'+b(p'-\frac{b}{a}\cdot q')\end{aligned} ∴ap+bq=bp′+(amodb)q′=bp′+(a−ab⋅b)q′=bp′+aq′−bq′⋅ab=aq′+b(p′−ab⋅q′)
即 p = q ′ , q = p ′ − a b ⋅ q ′ p=q',q=p'-\frac{a}{b}\cdot q' p=q′,q=p′−ba⋅q′
而 gcd ( a , b ) \gcd{(a,b)} gcd(a,b)一定会达到 a = 1 , b = 0 a=1,b=0 a=1,b=0时的情况,此时 p = 1 , q = 0 p=1,q=0 p=1,q=0,所以答案是前面的向上递归即可。
放代码:
int exgcd(int a,int b,int &p,int &q){
if(b==0){
p=1,q=0;
return 1;
}
int g=exgcd(b,a%b,p,q);
int t=p;
p=q,q=t-a/b*q;
return g;
}
素数
相关定理
- 唯一分解定理
a = p 1 × p 2 × p 3 × . . . × p d ( a ≥ 2 ) a=p_1\times p_2 \times p_3\times ... \times p_d(a\ge2) a=p1×p2×p3×...×pd(a≥2) - 威尔逊定理
( p − 1 ) ! ≡ − 1 ( m o d p ) (p-1)!\equiv-1\pmod{p} (p−1)!≡−1(modp) - 费马小定理
x n m o d x = n ( x ≠ 2 ) x^n \bmod x = n(x\ne2) xnmodx=n(x=2)
素数筛法
埃拉托色尼筛法
bool isprime[maxn]={};
memset(isprime,true,sizeof(isprime));
isprime[0]=isprime[1]=false;
for(int i=2;i<maxn;i++){
if(isprime[i]){
for(int j=i*2;j<=maxn;j+=i) isprime[j]=false;
}
}
for(int i=2;i<100;i++){
if(isprime[i]) cout<<i<<" ";
}
如果要判定的话,只需要判断isprime[i]
为true即可。
素数判定
素数性质判定
bool isprime(int x){
if(x==0||x==1) return false;
for(int i=2;i<x;i++){
if(x%i==0) return false;
}
return true;
}
图论部分
图的定义
如果使用简单的定义,就是用点将边连起来。若是使用恐怖的定义,那么图的定义 graph = ( V , E ) \text{graph}=(V,E) graph=(V,E),其中 V V V为所有的点, E E E为所有的边。(请记住,下面会用)
单源最短路
单源最短路是图论中最基础的算法,它可以求出一个点到其他的点的所有的最短路径的长度。
Dijkstra算法
Dijkstra算法的保证基础是这样的:对于一个已经被寻找到的点,没有比它更小的点去走。
首先,列一下Dijkstra算法的流程。
如果有一个图 G ( V , E ) G(V,E) G(V,E)且 V V V的大小为 n n n而 E E E的大小为 m m m, s s s到其他点的最短路长用 dis \text{dis} dis记。
初始化 dis \text{dis} dis数组(点 s s s到所有点的最短路径,除了 dis x ∈ V and x ≠ s = ∞ , dis s = ∞ \text{dis}_{x\in V\text{ and }x\neq s}=\infty,\text{dis}_{s}=\infty disx∈V and x=s=∞,diss=∞)并标记 s s s。
循环 n n n遍找到 dis \text{dis} dis数组中未标记过且值最小的点的标号记为 p p p
标记 p p p
更新与 p p p点链接的所有的点的 dis \text{dis} dis值
下面,我们来放一下代码。
#include <iostream>
#include <vector>
#include <map>
#define inf 2147483647
using namespace std;
int main(){
int n,m,s,dis[10001]={};
vector<pair<int,int> > ed[10001]={};
bool book[10001]={};
cin>>n>>m>>s;
for(int i=1;i<=m;i++){
int from,to,dis;
cin>>from>>to>>dis;
ed[from].push_back(make_pair(to,dis));
}
for(int i=1;i<=n;i++) if(i!=s) dis[i]=inf;
for(auto it:ed[s]) dis[it.first]=it.second;
book[s]=true;
for(int i=1;i<=n+1;i++){
int minn=inf,p=-1;
for(int j=1;j<=n;j++) if(!book[j]&&dis[j]<minn) minn=dis[j],p=j;
book[p]=true;
for(auto jt:ed[p]) if(jt.first!=s) dis[jt.first]=min(dis[jt.first],dis[p]+jt.second);
}
for(int i=1;i<=n;i++) cout<<dis[i]<<" ";
return 0;
}
以上就是Dijkstra的代码。
上述算法的时间复杂度为 Θ ( n 2 ) \Theta(n^2) Θ(n2),所以我们要对其进行优化。算法中最耗时间的是找到 p p p的那一步,可以使用优先队列进行保存并每次都可以直接取出最小的点。
下面是代码。
#include <iostream>
#include <vector>
#include <queue>
#include <map>
#define inf 2147483647
using namespace std;
struct heapitem{
int d,u;
bool operator<(const heapitem oth)const{
return d>oth.d;
}
heapitem(int d,int u):d(d),u(u){}
};
int main(){
int n,m,s,dis[10001]={};
vector<pair<int,int> > ed[10001]={};
bool book[10001]={};
cin>>n>>m>>s;
for(int i=1;i<=m;i++){
int from,to,dis;
cin>>from>>to>>dis;
ed[from].push_back(make_pair(to,dis));
}
for(int i=1;i<=n;i++) if(i!=s) dis[i]=inf;
priority_queue<heapitem> que;
que.push(heapitem(0,s));
while(!que.empty()){
heapitem now=que.top();
que.pop();
int p=now.u;
if(book[p]) continue;
book[p]=true;
for(auto it:ed[p]) if(dis[it.first]>dis[p]+it.second) dis[it.first]=dis[p]+it.second,que.push(heapitem(dis[it.first],it.first));
}
for(int i=1;i<=n;i++) cout<<dis[i]<<" ";
return 0;
}
常用数据结构
树状数组
又称fenwick树,是求前缀和等的一种好方法。
首先,这棵树的形态如下(画的不太好:)
---------------------+
---------+ |
---+ | ---+ |
+ | + | + | + |
0 1 2 3 4 5 6 7
其中,有加号的地方就代表有一个点在哪里,而上面的减号横线代表的就是它所覆盖的区域。