NOIP常用算法笔记

数论部分

常用技巧

快速幂

快速幂的基础:
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÷2modrp((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+(aabb)q=bp+aqbqab=aq+b(pabq)

p = q ′ , q = p ′ − a b ⋅ q ′ p=q',q=p'-\frac{a}{b}\cdot q' p=q,q=pbaq

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;
}

素数

相关定理

  1. 唯一分解定理
    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(a2)
  2. 威尔逊定理
    ( p − 1 ) ! ≡ − 1 ( m o d p ) (p-1)!\equiv-1\pmod{p} (p1)!1(modp)
  3. 费马小定理
    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 disxV 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

其中,有加号的地方就代表有一个点在哪里,而上面的减号横线代表的就是它所覆盖的区域。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值