NOIP 算法合集

· · 算法·理论

Ⅰ​.数据结构

1.树状数组

时间复杂度:O(n\log n)

优点:常数小

缺点:可以维护的内容不如线段树

应用:小常数维护前缀和或单点值

int tr[200005];
void add(int x,int y){while(x<=n)tr[x]+=y,x+=(-x)&x;}
int query(int x){
    int res=0;
    while(x)res+=tr[x],x-=(-x)&x;
    return res;
}

2.线段树

时间复杂度:O(n\log n)

优点:可以高效维护各类信息

缺点:常数较大

应用:维护各类可以跨越区间计算的信息

值得注意的是,如果可以离散化的线段树尽量不要使用动态开点线段树,常数和空间比较大,具体的,动态开点线段树见可持久化线段树与线段树合并的实现

int tr[800005],tag[800005];
void add(int p,int s,int t,int l,int r,int x){
    if(l<=s&&t<=r){
        tr[p]+=(t-s+1)*x;
        tag[p]+=x;
        return;
    }
    int mid=(s+t)>>1;
    if(tag[p]){
        tr[p<<1]+=(mid-s+1)*tag[p];
        tr[p<<1|1]+=(t-mid)*tag[p];
        tag[p<<1]+=tag[p];
        tag[p<<1|1]+=tag[p];
        tag[p]=0;
    }
    if(l<=mid)add(p<<1,s,mid,l,r,x);
    if(r>mid)add(p<<1|1,mid+1,t,l,r,x);
    tr[p]=tr[p<<1]+tr[p<<1|1];
}
int query(int p,int s,int t,int l,int r){
    if(l<=s&&t<=r)return tr[p];
    int mid=(s+t)>>1,res=0;
    if(tag[p]){
        tr[p<<1]+=(mid-s+1)*tag[p];
        tr[p<<1|1]+=(t-mid)*tag[p];
        tag[p<<1]+=tag[p];
        tag[p<<1|1]+=tag[p];
        tag[p]=0;
    }
    if(l<=mid)res=query(p<<1,s,mid,l,r);
    if(r>mid)res+=query(p<<1|1,mid+1,t,l,r);
    return res;
}

3. 可持久化线段树(主席树)

时间复杂度:O(n\log n)

空间复杂度:O(n\log n)

应用:可以维护区间第 K 大,维护区间大于 or 小于 k 的连续线段 虽然不在考纲里,但感觉挺好用的,详见 [NOIP 2024 T4 树上查询]

struct node{
    int l,r,val;
}tr[4000005];
int cnt;//注意使用动态开点线段树
void add(int v,int &p,int l,int r,int x,int y){
    if(!p)p=++cnt;
    if(l==r){
        tr[p]=tr[v]+y;
        return;
    }
    int mid=(l+r)>>1;
    if(x<=mid){
        tr[p].r=tr[v].r;
        add(tr[v].l,tr[p].l,l,r,x,y);
    }
    else{
        tr[p].l=tr[v].l;
        add(tr[v].r,tr[p].r,l,r,x,y);
    }
    tr[p].val=tr[tr[p].l].val+tr[tr[p].r].val;
}
int query(int p,int l,int r,int x){
    if(l==r)return tr[p].val;
    int mid=(l+r)>>1;
    if(x<=mid)return query(p<<1,l,mid,x);
    return query(p<<1|1,mid+1,r,x);
}

4.线段树合并

时间复杂度:O(n\log n)

空间复杂度:O(n\log n)

应用:维护一些子树上的信息,并且可以在子树合并时维护父节点子树信息,或者维护其他的多种类合并问题

struct node{
    int l,r,val;
}tr[4000005];
int cnt;//依旧动态开点线段树
int merge(int u,int v){
    if(!u||!v)return u|v;
    if(!tr[u].l&&!tr[u].r&&!tr[v].l&&!tr[v].r){
        tr[u].val+=tr[v].val;
        return u;
    }
    tr[u].l=merge(tr[u].l,tr[v].l);
    tr[u].r=merge(tr[u].r,tr[v],r);
    tr[u].val=tr[tr[u].l].val+tr[tr[u].r].val;
    return u;
}

5.扫描线

时间复杂度:O(n\log n)

应用:计算矩形面积并,二维数点

#include<bits/stdc++.h>
using namespace std;
const int MAXX=2e5;
struct node{
    int x,l,r,val;
}a[400005];//注意二倍空间
struct node{
    int cnt,val;
}tr[200005];
int n;
void add(int p,int s,int t,int l,int r,int x){
    if(l<=s&&t<=r){
        tr[p].cnt+=x;
        if(tr[p].cnt)tr[p].val==t-s+1;
        else{
            tr[p].val=0;
            if(s!=t)tr[p].val=tr[p<<1].val+tr[p<<1|1].val;
        }
        return;
    }
    int mid=(s+t)>>1;
    if(l<=mid)add(p<<1,s,mid,l,r,x);
    if(r>mid)add(p<<1|1,mid+1,t,l,r,x);
    if(!tr[p].cnt)tr[p].val=tr[p<<1].val+tr[p<<1|1].val;
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        int x,y,l,r;
        cin>>x>>y>>l>>r;
        a[i]={x,l,r,1};
        a[i+n]={y+1,l,r,-1};
    }
    sort(a+1,a+1+2*n,[](node x,node y){return x.x<y.x});
    int ans=0,la=0;
    for(int i=1;i<=2*n;i++){
        ans+=(tr[i].x-la)*tr[1].val;
        la=tr[i].x;
        while(la==tr[i].x)add(1,1,MAXX,tr[i].l,tr[i].r,tr[i++].val);
        i--;
    }
    cout<<ans;
    return 0;
}

6.平衡树

时间复杂度:O(n\log n)

应用:维护排名信息,斜率优化时维护凸包

  1. treap$ 感觉各方面不如 $fhq\ treap

给出 fhq\ treap 代码:大抵是只记得怎么敲 fhq 了

mt19937 rng(time(NULL));
struct node{
    int l,r,pri,val,sz;
}tr[200005];
int cnt;
void split(int p,int &l,int &r,int x){
    if(!p){l=r=0;return;}
    if(tr[p].val<=x){
        l=p;
        split(tr[p].r,tr[p].r,r,x);
    }
    else{
        r=p;
        split(tr[p].l,l,tr[p].l,x);
    }
    tr[p].sz=tr[tr[p].l].sz+tr[tr[p].r].sz+1;
}
int merge(int u,int v){
    if(!u||!v)return u|v;
    if(tr[u].pri>tr[v].pri){
        tr[u].r=merge(tr[u].r,v);
        tr[u].sz=tr[tr[u].l].sz+tr[tr[u].r].sz+1;
        return u;
    }
    tr[v].l=merge(l,tr[v].l);
    tr[v].sz=tr[tr[v].l].sz+tr[tr[v].r].sz+1;
}
void insert(int &rt,int x){
    int l,r,p;
    split(rt,l,r,x);
    p=++cnt;
    tr[p].pri=rng();
    tr[p].val=x;
    tr[p].sz=1;
    rt=merge(merge(l,p),r);
}
void del(int &rt,int x){
    int l,r,p;
    split(rt,l,r,x);
    split(l,l,p,x-1);
    rt=merge(l,r);
}

7.字典树

时间复杂度:O(n\log|S|)

空间复杂度:O(\sum|S|)

应用:可以快速查询字符串前缀信息

struct node{
    int son[26],cnt; 
}tr[200005];
int cnt;
void insert(string s){
    int p=0;
    for(auto i:s){
        if(tr[p].son[i-'a'])tr[p].son[i-'a']=++cnt;
        p=tr[p].son[i-'a'];
    }
    tr[p].cnt++;
}
int query(string s){
    int p=0,res=0;
    for(auto i:s){
        if(!tr[p].son[i-'a'])break;
        p=tr[p].son[i-'a'];
        res+=tr[p].cnt;
    }
    return res;
}

8.01-Trie

时间复杂度:O(n\log|S|)

空间复杂度:O(\sum|S|)

应用:快速查询数字在二进制下的信息,尤其是异或类型的

struct node{
    int son[2]; 
}tr[200005];
int cnt;
void insert(int x){
    int p=0;
    for(int i=29;i>=0;i--){
        int k=(x>>i)&1;
        if(tr[p].son[k])tr[p].son[k]=++cnt;
        p=tr[p].son[k];
    }
}
int query(int x){
    int p=0,res=0;
    for(int i=29;i>=0;i--){
        int k=(x>>i)&1;
        if(tr[p].son[k^1]){
            res+=(1<<i);
            p=tr[p].son[k^1];
        }
        else p=tr[p].son[k];
    }
    return res;
}

9.01-Trie 合并与可持久化 01-Trie

时间复杂度:O(n\log|S|)

空间复杂度:O(\sum|S|\log \sum|S|)

我们不难发现,01-Trie 和线段树都是二叉树,所以 无论是合并还是可持久化,01-Trie 和线段树在本质上都是完全相同的,实现方法与过程几乎一模一样,所以实现可以参考线段树合并与可持久化线段树,这里不给出具体的实现方法。

10.k 叉哈夫曼树

时间复杂度:O(n\log n)

空间复杂度:O(n)

应用:哈夫曼编码,计算叶权值 \times 到根路径长的最小值

注意点:k\geq 3 时不同于二叉哈夫曼树,此时需要补充 ((k-1)-[(n-1)mod(k-1)]\hspace{0.5em} m\in[0,k)\ )个空节点

#include<bits/stdc++.h>
using namespace std;
priority_queue<int> q;
int n,k;
int main(){
    cin>>n>>k;
    if(n%(k-1)!=1){
        int m=k-1-((n-1)%(k-1));
        for(int i=0;i<m;i++)q.push(0);
    }
    for(int i=0;i<n;i++){int x;cin>>x;q.push(x);}
    while(q.size()!=1){
        int res=0;
        for(int i=0;i<k;i++)res+=q.top(),q.pop();
        q.push(res);
    }
    cout<<q.top();
    return 0;
}

11.树链剖分

时间复杂度:O(n),使用其他 O(n\log n) 的数据结构时会使其时间复杂度上升为 O(n\log^2 n)

空间复杂度:O(n)

应用:由于一个树上节点到根最多经过 \log n 个重链,所以可以帮助我们将树剖分成链,然后使用其他数据结构维护树上信息

vector<int> e[200005];
int f[200005],top[200005],idx,id[200005],max_son[200005],sz[200005];
void dfs(int x,int fa){
    sz[x]=1;
    int maxx=0;
    for(auto i:e[x])if(i!=fa){
        f[i]=x;
        dfs(i,x);
        sz[x]+=sz[i];
        if(sz[i]>maxx)maxx=sz[i],max_son[x]=i;
    }
}
void dfs1(int x,int fa,int id){
    ::id[x]=id;
    top[x]=fa;
    if(!max_son[x])return;
    dfs1(max_son[x],fa,id);
    for(auto i:e[x])if(i!=f[x]&&i!=max_son[x])dfs1(i,i,++idx);
}

12.树套树

时间复杂度:O(n\log^2 n)

空间复杂度:O(n^2),但动态开点可以使其空间复杂度达到 O(n\log^2 n)

应用:维护一些树上每个点带 O(n) 级别信息或者二维信息,通常有线段树套线段树,线段树套平衡树,Trie 树套线段树……

class{
    struct node{
        int val,l,r;
    }tr[14000000];
public: 
    int cnt=0;
    void modify(int &p,int l,int r,int x,int y){
        if(!p)p=++cnt;
        if(l==r){
            tr[p]+=y;
            return;
        }
        int mid=(l+r)>>1;
        if(x<=mid)modify(tr[p].l,l,mid,x,y);
        else modify(tr[p].r,mid+1,r,x,y);
        if(tr[p].l)tr[p].val+=tr[tr[p].l].val;
        if(tr[p].r)tr[p].val+=tr[tr[p].r].val;
    }
    int query(int p,int s,int t,int l,int r){
        if(l<=s&&t<=r)return tr[p].val;
        int res=0,mid=(s+t)>>1;
        if(l<=mid&&tr[p].l)res+=query(tr[p].l,s,mid,l,r);
        if(r>mid&&tr[p].r)res+=query(tr[p].r,mid+1,t,l,r);
    }
}T;
int rt[200005];
void build(int p,int l,int r){
    rt[p]=++T.cnt;
    int mid=(l+r)>>1;
    build(p<<1,l,mid);
    build(p<<1|1,mid+1,r);
    return;
}
void add(int p,int l,int r,int x,int y,int z){
    T.modify(rt[p],1,n,y,z);
    if(l==r)return;
    int mid=(l+r)>>1;
    if(x<=mid)add(p<<1,l,mid,x,y,z);
    else add(p<<1|1,mid+1,r,x,y,z);
}
int query(int p,int s,int t,int l,int r,int x,int y){
    if(l<=s&&t<=r)return T.query(rt[p],1,n,x,y);
    int res=0,mid=(s+t)>>1;
    if(l<=mid)res=query(p<<1,s,mid,l,r,x,y);
    if(r>mid)res+=query(p<<1|1,mid+1,t,l,r,x,y);
    return res;
}

13.cdq 分治

时间复杂度:处理 k 维偏序的时间复杂度是 O(n\log^{k-1} n)

空间复杂度:O(n)

应用:通常用来处理 k 维偏序数点的问题,还可以维护凸包

值得注意的是,如果存在一维不等号可以取等,应该将其放在最内层用树状数组解决,如果有多维不等号可以取等应当注意去重

#include<bits/stdc++.h>
using namespace std;
struct node{
    int x,y,z,idx,id;
}a[200005];
int n,ans[200005];
int tr[200005];
void add(int x,int y){while(x<=n)tr[x]+=y,x+=(-x)&x;}
int query(int x){
    int res=0;
    while(x)res+=tr[x],x-=(-x)&x;
    return res;
}
void cdq(int l,int r){
    int mid=(l+r)>>1;
    cdq(l,mid);cdq(mid+1,r);
    vector<node>tmp;
    int ll=l,rr=mid+1;
    while(ll<=mid&&rr<=r){
        if(a[ll].y<a[rr].y)tmp.push_back(a[ll++]);
        else tmp.push_back(a[rr++]);
    }
    while(ll<=mid)tmp.push_back(a[ll++]);
    while(rr<=mid)tmp.push_back(a[rr++]);
    for(int i=0;i<r-l+1;i++)a[i+l]=tmp[i];
    for(int i=l;i<=r;i++){
        if(a[i].id<=mid)add(a[i].z,1);
        else ans[a[i].idx]+=query(a[i].z-1);
    }
    for(int i=l;i<=r;i++)if(a[i].id<=mid)add(a[i].z,-1);
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i].x>>a[i].y>>a[i].z,a[i].idx=i;
    sort(a+1,a+1+n,[](node x,node y){return x.x<y.x;});
    for(int i=1;i<=n;i++)a[i].id=i;
    cdq(1,n);
    for(int i=1;i<=n;i++)cout<<ans[i]<<endl;
    return 0;
}

14.并查集

时间复杂度:O(n\log n)\ or\ O(n\alpha(n))

空间复杂度:O(n)

应用:用于维护连通性,维护连通块大小

值得注意的是,没有任何优化的并查集时间复杂度是 O(n\log n) 的,然而路径压缩并查集的时间复杂度经姚期智证明平均时间复杂度是 O(n\alpha(n)) 的,经 Tarjan 证明最劣时间复杂度的下限是 O(n\log n) 的,所以建议使用路径压缩和按秩合并的并查集,其时间复杂度是严格 O(n\alpha(n))

int f[200005],sz[200005];
int getf(int x){return (f[x]==x)?x:f[x]=getf(f[x]);}
void merge(int u,int v){
    int fu=getf(u),fv=getf(v);
    if(fu!=fv){
        if(sz[fu]>sz[fv]){
            f[fv]=fu;
            sz[fu]+=sz[fv];
        }
        else{
            f[fu]=fv;
            sz[fv]+=sz[fu];
        }
    }
}

15.拓展域并查集

时间复杂度:O(n\log n)\ or\ O(n\alpha(n))

空间复杂度:O(n)

应用:维护存在多类对立集合的连通性与矛盾 值得注意的是的是,我们维护拓展域并查集是应保持其对称性,即 ab+n 在同一集合内,则 a+nb 也应当在同一集合内,同理 ab 在同一集合内,则 a+nb+n 也应当在同一集合内

由于除了维护对象不同外,与并查集几乎没有任何区别,故不放代码先咕着

16.边带权并查集

时间复杂度:O(n\log n)\ or\ O(n\alpha(n))

空间复杂度:O(n)

应用:维护集合间各元素有值上关系的情况

由于除了权值维护外,与并查集几乎没有任何区别,故不放代码继续咕着

17.优先队列/堆

时间复杂度:O(n\log n)

空间复杂度:O(n)

应用:建议使用优先队列,可以小常数维护最大最小值,其一般与反悔贪心等其他算法一起使用 值得注意的是,如果你尝试重载优先队列的比较运算符,注意 return true 表示的是前者比后者的有限级更高

priority_queue<int> q;//默认大根堆
priority_queue<int,vector<int>,greater<int>> p;//小根堆
struct node{
    int u,v,w;
};
auto cmp=[](node x,node y){return x.w<y.w;};
priority_queue<node,vector<node>,function<bool(node,node)>> s(cmp);//自定义优先级

18.单调队列

时间复杂度:O(n)

空间复杂度:O(n)

应用:维护移动窗口即前缀定长为 k 的区间最值,维护凸包

#include<bits/stdc++.h>
using namespace std;
int n,k,a[200005],q[200005],en,st;
int main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        while(st!=en&&q[st]<=i-k)st++;
        while(st!=en&&a[q[en-1]]<=a[i])en--;
        q[en++]=a[i];
        cout<<q[st]<<" ";
    }
    return 0;
}

19.单调栈

时间复杂度:O(n)

空间复杂度:O(n)

应用:找左侧第一个小于当前元素的元素,维护前缀单调性,维护凸包,可以更简便地代替部分平衡树功能

#include<bits/stdc++.h>
using namespace std;
int n,k,a[200005],q[200005],top;
int main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        while(top&&a[q[top-1]>=a[i])top--;
        if(!top)cout<<-1<<" ";
        else cout<<q[top-1]<<" ";
        q[top++]=i;
    }
    return 0;
}

20.分块

时间复杂度:O(n\sqrt{n})

空间复杂度:O(n\sqrt{n})

应用:感觉非常广耶,也可以暴力维护多类信息,比如区间众数,区间最小……

#include<bits/stdc++h.>
using namespace std;
int n,q,a[200005],s[460][200005],f[460][460],bl,k,cnt[200005];
int main(){
    cin>>n>>q;
    for(int i=0;i<n;i++)cin>>a[i];
    bl=sqrt(n);
    k=n/bl;
    if(k*bl<n)k++;
    for(int i=1;i<=k;i++){
        for(int j=(i-1)*bl;j<i*bl&&j<n;j++)s[i][a[j]]++;
        for(int j=1;j<=n;j++)s[i][j]+=s[i-1][j];
    }
    for(int i=1,id=0,maxx=0;i<=k;i++,id=0,maxx=0,memset(cnt,0,sizeof cnt))for(int j=i;j<=k;j++){
        for(int l=(j-1)*bl;l<j*bl&&j<n;j++){
            cnt[a[l]]++;
            if(cnt[a[l]]>maxx||(cnt[a[l]]==maxx&&a[l]<id))maxx=cnt[a[l]],id=a[l];
        }
        f[i][j]=a[l];
    }
    while(q--){
        int l,r,ll,rr;
        cin>>l>>r;
        l--,r--;
        ll/=k,rr/=k;
        ll++,rr++;
        if(ll>=rr-1){
            int maxx=0,id=0;
            for(int i=l;i<=r;i++){
                cnt[a[i]]++;
                if(cnt[a[i]]>maxx||(cnt[a[i]]==maxx&&a[i]<id))maxx=cnt[a[i]],id=a[i];
            }
            for(int i=l;i<=r;i++)cnt[a[i]]--;
            cout<<id<<endl;
        }
        else{
            int maxx=0,id=0;
            for(int i=l;i<ll*bl;i++){
                cnt[a[i]]++;
                if(cnt[a[i]]+s[rr-1][a[i]]-s[ll][a[i]]>maxx||(cnt[a[i]]+s[rr-1][a[i]]-s[ll][a[i]]==maxx&&a[i]<id))maxx=cnt[a[i]]+s[rr-1][a[i]]-s[ll][a[i]],id=a[i];
            }
            for(int i=(rr-1)*bl;i<=r;i++){
                cnt[a[i]]++;
                if(cnt[a[i]]+s[rr-1][a[i]]-s[ll][a[i]]>maxx||(cnt[a[i]]+s[rr-1][a[i]]-s[ll][a[i]]==maxx&&a[i]<id))maxx=cnt[a[i]]+s[rr-1][a[i]]-s[ll][a[i]],id=a[i];
            }
            if(s[rr-1][f[ll+1][rr-1]]-s[ll][f[ll+1][rr-1]]>maxx||(s[rr-1][f[ll+1][rr-1]]-s[ll][f[ll+1][rr-1]]==maxx&&f[ll+1][rr-1]<id))maxx=s[rr-1][f[ll+1][rr-1]]-s[ll][f[ll+1][rr-1],id=f[ll+1][rr-1];
            for(int i=l;i<ll*bl;i++)cnt[a[i]]--;
            for(int i=(rr-1)*bl;i<=r;i++)cnt[a[i]]--;
            cout<<id<<endl;
        }
    }
    return 0;
}

21.莫队

时间复杂度:O(n\sqrt{n})

时间复杂度:O(n)

应用:暴力查询区间可转移信息,例如区间数颜 注意使用奇偶优化莫队

#include<bits/stdc++.h>
using namespace std;
int col[200005],cnt[200005],sum,n,q,ans[200005];
struct node{
    int l,r,id;
}b[200005];
int main(){
    cin>>n>>q;
    for(int i=1;i<=n;i++)cin>>col[i];
    for(int i=1;i<=q;i++)cin>>b[i].l>>b[i].r,b[i].id=i;
    int bl=sqrt(n);
    sort(b+1,b+1+q,[](node x,node y){return(int(x.l/bl)==int(y.l/bl))?((int(x.l/bl)%2)?x.r<y.r:x.r>y.r):x/l<y.l;});
    int l=b[1].l,r=b[1].r;
    for(int i=l;i<=r;i++){cnt[col[i]]++;if(cnt[col[i]]==1)sum++;}
    ans[b[1].id]=sum;
    for(int i=2;i<=q;i++){
        while(b[i].l<l){
            l--;
            cnt[col[l]]++;
            if(cnt[col[l]]==1)sum++;
        }
        while(b[i].r>r){
            r++;
            cnt[col[r]]++;
            if(cnt[col[r]]==1)sum++;
        }
        while(b[i].l>l){
            cnt[col[l]]--;
            if(!cnt[col[l]])sum--;
            l++;
        }
        while(b[i].r<r){
            cnt[col[r]]--;
            if(!cnt[col[r]])sum--;
            r--;
        }
        ans[b[i].id]=sum;
    }
    for(int i=1;i<=q;i++)cout<<ans[i]<<endl;
    return 0;
}

22.带修莫队

时间复杂度:O(n^{\frac{5}{3}})

空间复杂度:O(n)

应用:用于实现一些不存在修改时可以用莫队实现但又有修改的题目

值得注意的是,与莫队不同,莫队的块长是 \sqrt{n},带修莫队的块长是 n^{\frac{2}{3}} 由于除了带一个时间戳用于修改外与普通莫队没有任何区别,所以就不放代码了

Q.E.D

Ⅱ.动态规划

1.区间 dp

时间复杂度:O(n^3)

空间复杂度:O(n^2)

应用:可以解决一些能够将问题拆成多个独立不相互影响的子区间问题的题目

通常转移方程式为:

\large dp_{i,j}=\sum\limits_{i=l}^{r-1} dp_{i,k} \cdot dp_{k+1,j}

下以 [NOIP 2006 提高组] 能量项链 为例

#include<bits/stdc++.h>
using namespace std;
int n,a[105],dp[105][105];
int main(){
    cin>>n;
    for(int i=0;i<n;i++)cin>>a[i];
    for(int i=2;i<=n;i++){
        for(int l=0;l+i-1<n;l++){
            int r=l+i-1;
            for(int k=l;k<r;k++)dp[l][r]=max(dp[l][r],dp[l][k]+dp[k+1][r]+a[l]*a[k+1]*a[(r+1)%n]);
        }
    }
    cout<<dp[0][n-1];
    return 0;
}

2.环形 dp

时间复杂度:O(n^3)

空间复杂度:O(n^2)

应用:顾名思义,适用于维护有关环上信息的题目

通常直接将长度翻倍,这样就可以直接进行区间 dp, 最后答案可以表示为:

\large \max(dp_{1,n},dp_{2,n+1},...,dp_{n,2n-1})

由于与区间 dp 写法一致,所以就不放代码了

3.树形 dp

时间复杂度:O(n)\ or\ O(n\log n) 前者为直接转移贡献的时间复杂度,后者为 01 背包转移贡献的时间复杂度

空间复杂度:O(n)\ or\ O(nV)

应用:树上最大独立集,树上选点最大贡献

下以 没有上司的舞会 为例

#include<bits/stdc++.h>
using namespace std;
vector<int> e[6005];
int dp[6005][2],n,h[6005];
bitset<6005> book;
void dfs(int x){
    for(auto i:e[x]){
        dfs(i);
        dp[x][0]+=max(dp[i][0],dp[i][1]);
        dp[x][1]+=dp[i][0];
    }
    dp[x][1]+=h[x];
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>h[i];
    for(int i=1;i<n;i++){
        int x,y;
        cin>>x>>y;
        book.set(x);
        e[y].push_back(x);
    }
    int rt;
    for(int i=1;i<=n;i++)if(!book[i]){rt=i;break;}
    dfs(rt);
    cout<<max(dp[rt][1],dp[rt][0]);
    return 0;
}

4.换根 dp

时间复杂度:O(n)\ or\ O(n\log n) 前者为直接转移贡献的时间复杂度,后者为 01 背包转移贡献的时间复杂度

空间复杂度:O(n)\ or\ O(nV)

应用:用于求单个根好求并且可以由一个根转移至其他根的信息,比如部分树上随机游走,多根求解题

由于洛谷没有模板题,所以放一道随机游走题,简略题意如下:

一个 n 个节点的无根树,若从某个节点 i 出发,与节点 i 连接且未到达过的节点个数为 t 则前往这些节点中某个节点的概率都是 \frac{1}{t},求从每个节点出发进行随机游走直到无路可走的期望步数是多少,注意,每次从一个结点出发进行一次完整随机游走的过程是与从上一个结点出发进行一次完整随机游走的过程相互独立的,答案对 998244353 取模

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,cnt[200005],ff[200005],f[200005],g[200005];
vector<int> e[200005];
const int mod=998244353;
int power(int x,int y){
    int res=1;
    while(y){
        if(y&1)res=res*x%mod;
        x=x*x%mod;
        y>>=1;
    }
    return res;
}
void dfs(int x,int fa){
    int res=0;
    for(auto i:e[x])if(i!=fa){
        dfs(i,x);
        cnt[x]++;
        res+=ff[i];
    }
    f[x]=(res+cnt[x])%mod*power(cnt[x]+1,mod-2)%mod;
    ff[x]=(res+cnt[x])%mod*power(cnt[x],mod-2)%mod;
}
void dfs1(int x,int fa,int pre=0){
    if(x!=1)g[x]=(f[x]+(pre+1)*power(cnt[x]+1,mod-2)%mod)%mod;
    else g[x]=ff[x];
    int res=pre;
    for(auto i:e[x])if(i!=fa)res+=ff[i];
    for(auto i:e[x])if(i!=fa){
        res-=ff[i];
        if(x==1)dfs1(i,x,(res+cnt[x]-1)*power(cnt[x]-1,mod-2)%mod);
        else dfs1(i,x,(res+cnt[x])*power(cnt[x],mod-2)%mod);
        res+=ff[i];
    }
}
signed main(){
    cin>>n;
    for(int i=1;i<n;i++){int x,y;cin>>x>>y;e[x].push_back(y);e[y].push_back(x);}
    dfs(1,1);
    dfs1(1,1);
    for(int i=1;i<=n;i++)cout<<g[i]<<endl;
    return 0;
}

5.状压 dp

时间复杂度:O(n2^n)

空间复杂度:O(n2^n)

应用:用于一种 n 极小,状态与全局选择相关的问题

下以 售货员的难题 为例

#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,dp[(1<<20)+10][25],dis[25][25];
signed main(){
    memset(dp,0x7f7f7f7f,sizeof dp);
    cin>>n;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)cin>>dis[i][j];
    dp[1][1]=0;
    for(int i=1;i<(1<<n);i++){
        for(int j=1;j<=n;j++)if(!((1<<(j-1))&i)){
            for(int k=1;k<=n;k++)if((1<<(k-1))&i)dp[i|(1<<(j-1))][j]=min(dp[i|(1<<(j-1))][j],dp[i][k]+dis[k][j]);
        }
    }
    int minn=INT_MAX;
    for(int i=1;i<=n;i++)minn=min(minn,dp[(1<<n)-1][i]+dis[i][1]);
    cout<<minn;
    return 0;
}

6.FWT 结合状压 dp

时间复杂度:O(n^22^n)

空间复杂度:O(n2^n)

应用:可以用来代替一些不会写的 dp 比如轮廓线 dp或二维状压 dp

考虑对于状压 dp_S 通常表示的是二进制状态 S 下的方案数,对于每层的 dp 每个可以视为一个数列 A=\{dp_0,dp_1,..,dp_{2^n-1}\},于是对于考虑前 k 层后每个状态下的方案数数列记为 B,如果状态 i,j 满足关系 f(i,j) 即可转移,转移式为 c=a\cdot b,其中 \cdot 为一个运算,则上面的一堆东西就可以写成一个二进制卷积形式:

\large B_{k,t}=\sum\limits_{f(i,j)=true} B_{k-1,i}\cdot A_{k,j}

不难发现 f(i,j) 是一个二进制运算时,我们就可以使用 FWTO(2^n\log2^n) 的时间复杂度下进行卷积

可以详见数学章的 FWT

7.矩阵快速幂优化 dp

时间复杂度:O(k^3\log n),其中 k 是矩阵大小

空间复杂度:O(k^2)

应用:用于转移类似于矩阵乘法或广义矩阵乘法的 dp

比如存在 dp 转移式:

\large dp_i=dp_{i-1}+2dp_{i-2}

可以写成矩阵形式:

\large\begin{bmatrix} dp_{i-2} & dp_{i-1} & dp_i \end{bmatrix} = \begin{bmatrix} dp_{i-3} & dp_{i-2} & dp_{i-1} \end{bmatrix} \begin{bmatrix} 0 & 0 & 0\\ 1 & 0 & 2\\ 0 & 1 & 1 \end{bmatrix}

假设初始矩阵为 \large\begin{bmatrix}dp_{-2} & dp_{-1} & dp_0 \end{bmatrix}=\begin{bmatrix}0 & 0 & 1\end{bmatrix},于是 n 次转移得到 dp_n 可以写成:

\large\begin{bmatrix} dp_{n-2} & dp_{n-1} & dp_n \end{bmatrix} = \begin{bmatrix} 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 0 & 0 & 0\\ 1 & 0 & 2\\ 0 & 1 & 1 \end{bmatrix} ^n

接下来就成了矩阵快速幂的问题了,可以详见数学章的矩阵快速幂

8.动态 dp

时间复杂度:O(k^3\log^2 n),其中 k 是矩阵大小

空间复杂度:O(k^2n)

应用:用于维护树上带修改的 dp

同矩阵快速幂优化 dp,动态 dp 也需将转移式改为矩阵乘法或者广义矩阵乘法,然后将树进行树链剖分, 用线段树维护重链矩阵积

9.前缀和优化 dp

时间复杂度:O(n)

空间复杂度:O(n)

应用:用于维护对于前缀和转移形式的 dp

如果存在 dp 转移式为:

\large dp_i=\sum\limits_{j=i-k}^{i-1}dp_j

于是我们可以计算前缀和数组 S

\large S_i=\sum\limits_{i=1}^i dp_i

然后上面的 dp 转移式就变为了:

\large dp_i=s_{i-1}-s_{i-k-1}

10.单调队列优化 dp

时间复杂度:O(n)

空间复杂度:O(n)

应用:用于维护对于前缀最值转移形式的 dp

如果存在 dp 转移式为:

\large dp_i=\min(dp_{i-k},dp_{i-k+1},...,dp_{i-1})+\delta

我们不难发现,如果 k 为常数,那么 \min(dp_{i-k},dp_{i-k+1},...,dp_{i-1}) 可以使用单调队列在 O(n) 的时间复杂度下维护

详见数据结构章的单调队列

11.线段树优化 dp

时间复杂度:O(n\log n)

空间复杂度:O(n)

应用:用于维护对于各类最值转移形式的 dp

如果存在 dp 转移式为:

\large dp_i=\min(dp_{i-k}+k*d,dp_{i-k+1}+(k-1)*d,...,dp_{i-1}+d)+\delta

我们就可以维护线段树维护 \min(dp_{i-k}+k*d,dp_{i-k+1}+(k-1)*d,...,dp_{i-1}+d),即区间加 d 单点赋值 dp_i

详见数据结构章的线段树

12.斜率优化 dp

时间复杂度:O(n)\ or\ O(n\log n),前者为单调队列维护凸包,后者为 cdq 分治或平衡树或李超线段树维护凸包

空间复杂度:O(n)

应用:用于维护形似一次函数转移形式的 dp

如果存在 dp 转移式为:

\large dp_i=\min\limits_{j<i}(dp_j+calc(i,j))

如果其中 calc(i,j) 呈现一次函数形状即可实现斜率优化

值得注意的是,斜率单调,横坐标单调,可以使用单调队列实现 O(n),如果其中一个不单调,则可以使用李超线段树或 cdq 分治或平衡树或单调队列/单调栈加二分维护,实现 O(n\log n),如果都不单调,只能用李超线段树或 cdq 分治或平衡树维护,实现 O(n\log n)

数据结构内容详见数据结构章

Q.E.D

Ⅲ.字符串

1.字符串哈希

时间复杂度:O(n)

空间复杂度:O(n)

应用:几乎可以实现所有字符串题目,字符串匹配等

下以 [NOIP2020] 字符串匹配 为例:

#include<bits/stdc++.h>
using namespace std;
const int base=20100611;
unsigned long long h[1100005],p[1100005];
int t,tr[30],cnt[30],sum,nxt[1100005];
long long ans;
string s;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>t;
    p[0]=1;
    for(int i=1;i<=(1<<20);i++)p[i]=p[i-1]*base;
    while(t--){
        cin>>s;
        memset(tr,0,sizeof tr);
        memset(cnt,0,sizeof cnt);
        ans=sum=ans=0;
        int n=s.size();
        s=' '+s;
        for(int i=1;i<=n;i++)h[i]=h[i-1]*base+s[i]-'a';
        for(int i=n;i>=1;i--){
            cnt[s[i]-'a']++;
            if(cnt[s[i]-'a']%2)sum++;
            else sum--;
            nxt[i]=sum;
        }
        memset(cnt,0,sizeof cnt);
        cnt[s[1]-'a']++;
        sum=1;
        for(int i=1;i<=26;i++)tr[i]++;
        for(int i=2;i<n;i++){
            ans+=tr[nxt[i+1]];
            int id=1;
            while(i*(id+1)<n&&h[(id+1)*i]-h[id*i]*p[i]==h[i])id++,ans+=tr[nxt[id*i+1]];
            cnt[s[i]-'a']++;
            if(cnt[s[i]-'a']%2)sum++;
            else sum--;
            for(int j=sum;j<=26;j++)tr[j]++;
        }
        cout<<ans<<endl;
    }
    return 0;
}

2.KMP

时间复杂度:O(n)

空间复杂度:O(n)

应用:用于单模式串匹配

#include<bits/stdc++.h>
using namespace std;
string a,b;
int kmp[200005];
int main(){
    cin>>a>>b;
    a=' '+a,b=' '+b;
    int id=0;
    for(int i=2;i<=b.size()-1;i++){
        while(id&&b[i]!=b[id+1])id=kmp[id];
        if(b[i]==b[id+1])id++;
        kmp[i]=id;
    }
    id=0;
    for(int i=1;i<=a.size()-1;i++){
        while(id&&a[i]!=b[id+1])id=kmp[id];
        if(a[i]==b[id+1])id++;
        if(id==b.size()-1){cout<<i-id+1<<endl;id=kmp[id];}
    }
    return 0;
}

3.manacher

时间复杂度:O(n)

空间复杂度:O(n)

应用:用于寻找以每个位置为中心的最长回文串

#include<bits/stdc++.h>
using namespace std;
string a,b;
int p[400005];
int main(){
    cin>>a;
    b="*|";
    for(int i:a)b+=i,b+='|';
    b+='*';
    int l=0,r=0;
    for(int i=1;i<b.size()-1;i++){
        if(i>r){
            char *x=&b[i-1],*y=&b[i+1];
            while(*x==*y&&*x!='*'&&*y!='*')x--,y++;
            p[i]=y-&b[i];
        }
        else{
            int j=l+r-i;
            if(j-p[j]+1>l)p[i]=p[j];
            char *y=&b[r+1],*x=&b[2*i-r-1];
            while(*x==*y&&*x!='*'&&*y!='*')x--,y++;
            p[i]=y-&b[i];   
        }
        if(p[i]+i-1>r)r=p[i]+i-1,l=i-p[i]+1;
    }
    for(int i=1;i<b.size()-1;i+=2)cout<<p[i]<<" ";
    return 0;
}

4.AC 自动机

时间复杂度:O(\sum|S|+\sum|T|)

空间复杂度:O(\sum|S|)

应用:多模式串匹配

struct node{
    int son[27],fail,cnt,vis;
}tr[5000005];
int cnt,sum[5000005];
vector<int> e[5000005],to;
void insert(string s){
    int p=0;
    for(auto i:s){if(!tr[p].son[i-'a'])tr[p].son[i-'a']=++cnt;p=tr[p].son[i-'a'];}
    tr[p].cnt++;
}
void dfs(int x){for(int i:e[x])sum[i]=sum[x]+tr[i].cnt,dfs(i);}
void build(){
    queue<int> q;
    for(int i=0;i<27;i++){if(tr[0].son[i])q.push(tr[0].son[i]);}
    while(!q.empty()){
        int p=q.front();
        for(int i=0;i<27;i++){if(tr[p].son[i])tr[tr[p].son[i]].fail=tr[tr[p].fail].son[i],q.push(tr[p].son[i]);else tr[p].son[i]=tr[tr[p].fail].son[i];}
        q.pop();
    }
    for(int i=1;i<=cnt;i++)e[tr[i].fail].push_back(i);
    dfs(0);
}
int query(string x){
    int p=0;
    int res=0;
    for(auto i:x){p=tr[p].son[i-'a'];res+=sum[p];}
    return res;
}
Q.E.D

Ⅳ.图论

1.最短路

Floyd SPFA 堆优化 Dijkstra
特点 多源 判负环
时间复杂度 O(n^3) O(nm) O(m\log n)
空间复杂度 O(n^2) O(n+m) O(n+m)

值得注意的是,关于 SPFA它死了,但可以判负环和跑差分约束,接下来讲讲差分约束 考虑式子:

\large a-b\le c

于是联想到最短路松弛式子:

\large dis_a\le dia_b+c

于是将上面式子转化为:

\large a\le b+c

那么我们可以将 a 连一条向 b 的边,长度为 c,然后建一个超级源点到所有点的长度为 0 ,接下来跑一个最长路,只要不存在正环即有解,dis 即为一组特殊解

```cpp int dis[200005]; bitset<200005> book; vector<pair<int,int>> e[200005]; priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> q; void dij(){ memset(dis,0x3f3f3f3f,sizeof dis); dis[1]=0; q.push({0,1}); while(!q.empty()){ auto u=q.top(); if(book[u.second])continue; book[u.second]=1; for(auto v:e[u.second])if(dis[v.first]>u.first+v.second){ dis[v.first]=u.first+v.second; q.push(v.first); } } } ``` #### 2.最小生成树 时间复杂度:$O(m\log m)

空间复杂度:O(m+n)

上面的复杂度分析基于 Kruskal 不会 Prim

#include<bits/stdc++.h>
using namespace std;
struct node{
    int u,v,w;
}a[200005];
int f[200005],sz[200005];
int getf(int x){return (f[x]==x)?x:f[x]=getf(f[x]);}
int n,m;
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++)cin>>a[i].u>>a[i].v>>a[i].w;
    sort(a+1,a+1+m,[](node x,node y){return x.w<y.w;});
    for(int i=1;i<=n;i++)f[i]=i,sz[i]=1;
    int cnt=0,ans=0;
    while(cnt<n-1){
        int fu=getf(a[i].u),fv=getf(a[i].v);
        if(fu!=fv){
            cnt++;
            ans+=a[i].w;
            if(sz[fu]>sz[fv]){
                f[fv]=fu;
                sz[fu]+=sz[fv];
            }
            else{
                f[fu]=fv;
                sz[fv]+=sz[fu];
            }
        }
        cout<<ans;
    }
    return 0;
}

3.tarjan 全家桶

时间复杂度均为:O(n)

空间复杂度均为:O(n)

强连通分量:

int n,m,dfn[200005],low[200005],idx,in[200005],id[200005],scc;
stack<int> q;
vector<int> e[200005];
void tarjan(int x){
    dfn[x]=low[x]=++idx;in[x]=1;q.push(x);
    for(auto i:e[x])if(!dfn[i]){tarjan(i);low[x]=min(low[x],low[i]);}else if(in[i])low[x]=min(low[x],dfn[i]);
    if(low[x]==dfn[x]){++scc;int t;do{t=q.top();q.pop();in[t]=0;id[t]=scc;}while(t!=x);}
}

边双连通分量:

int n,m,dfn[200005],low[200005],idx,sz[200005],dcc,id[200005],siz[200005],cnt;
vector<int> e[200005];
stack<int> q;
void tarjan(int x,int fa){
    dfn[x]=low[x]=++idx;
    q.push(x);
    for(auto i:e[x]){
        if(!dfn[i]){tarjan(i,x),low[x]=min(low[x],low[i]);if(low[i]>dfn[x]){dcc++;int ttt;do{ttt=q.top();q.pop();sz[dcc]++;id[ttt]=dcc;}while(ttt!=i);}}
        else if(fa!=i)low[x]=min(low[x],dfn[i]);
    }
}

割点:

 int n,m,rt,cnt,dfn[200005],low[200005],idx;
bitset<200005> fl; 
vector<int> e[20005];
void tarjan(int x){low[x]=dfn[x]=++idx;int cnt=0;for(int i:e[x]){if(!dfn[i]){cnt++;tarjan(i);low[x]=min(low[x],low[i]);if(!fl[x]&&((rt!=x&&dfn[x]<=low[i])||(rt==x&&cnt>1)))::cnt++,fl[x]=1;}else low[x]=min(low[x],dfn[i]);}}

割边:同边双连通分量

4.拓扑排序

时间复杂度:O(n)

空间复杂度:O(n)

应用:排出 DAG 进行 dp 等操作

#include<bits/stdc++.h>
vector<int> e[200005];
queue<int> q;
int ins[200005];
int n,m;
void top(){
    while(!q.empty()){
        int u=q.front();q.pop();
        for(auto i:e[u]){ins[i]--;if(!ins[i])q.push(i);}
    }
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int x,y;
        cin>>x>>y;
        e[x].push_back(y);
        ins[y]++;
    }
    for(int i=1;i<=n;i++)if(!ins[i])q.push(i);
    top();
    return 0;
}

5.树的直径

时间复杂度:O(n)

空间复杂度:O(n)

应用:树上路径贪心

#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int> e[200005];
int maxx,u,v;
void dfs(int x,int fa,int d){
    if(d>maxx)maxx=d,u=x;
    for(auto i:e[x])if(i!=fa)dfs(i,x,d+1);
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int x,y;
        cin>>x>>y;
        e[x].push_back(y);
        e[y].push_back(x);
    }
    dfs(1,1,0);
    v=u;
    dfs(v,v,0);
    cout<<u<<" "<<v<<" "<<maxx;
    return 0;
}

6.树的重心

时间复杂度:O(n)

空间复杂度:O(n)

应用:点分治

int n,m,sz[200005];
vector<int> e[200005];
void get_rt(int x,int fa,int sz,int &rt){
    ::sz[x]=1;int maxx=0;
    for(auto i:e[x])if(i!=fa)get_rt(i,x,sz,rt),::sz[x]+=::sz[i],maxx=max(maxx,::sz[i]);
    maxx=max(maxx,sz-::sz[x]);
    if((maxx<<1)<=sz) rt=x;
}

7.点分治

时间复杂度:O(n\log n)

空间复杂度:O(n\log n)

应用:求树上是否存在某种长度路径

#include<bits/stdc++.h>
using namespace std;
namespace fastio{
    #define il inline
    const int isz=1<<25;
    char iin[isz],*is=iin+isz,*it=iin+isz;
    #define gc() (is==it)?(it=(is=iin)+fread(iin,1,isz,stdin),(is==it)?EOF:*is++):*is++
    template<typename T> il void rd(T &x){
        x=0;
        char c=gc();
        bool fla=false;
        while(!isdigit(c)) fla|=(c=='-'),c=gc();
        while(isdigit(c)) x=(x<<1)+(x<<3)+(c&15),c=gc();
        x=(fla)?-x:x;
    }
    template<typename T1,typename...T2> il void rd(T1 &x,T2&...y){rd(x);rd(y...);}
    template<typename T,typename T1> il void rd(T a[],T1 s,T t){for(T i=s;i<=t;i++) rd(a[i]);}
    char iout[isz],*ita=iout;
    #define Flush() fwrite(iout,1,ita-iout,stdout);ita=iout
    template<typename T> il void wr(T x,char la='\n'){
        char c[35];
        int len=0;
        if(x<0) *ita++='-',x=-x;
        do{c[++len]=(x%10+'0');x/=10;}while(x);
        while(len)*ita++=c[len--];
        *ita++=la;
    } 
    il void en(char x='\n'){*ita++=x;}
}
using namespace fastio;
const int N=1e7;
bitset<105> ans;
bitset<10005> del;
bitset<N+5> cnt;
int n,m,q[105],sz[10005],dis[10005];
vector<int> e[10005],w[10005],tmp;
void get_rt(int x,int fa,int sz,int &rt){
    ::sz[x]=1;int maxx=0;
    for(auto i:e[x])if(i!=fa&&!del[i])get_rt(i,x,sz,rt),::sz[x]+=::sz[i],maxx=max(maxx,::sz[i]);
    maxx=max(maxx,sz-::sz[x]);
    if((maxx<<1)<=sz) rt=x;
}
void calc(int x,int fa){
    sz[x]=1;
    for(int i=0;i<e[x].size();i++)if(e[x][i]!=fa&&!del[e[x][i]]){
        dis[e[x][i]]=dis[x]+w[x][i];
        if(dis[e[x][i]]<=N)tmp.push_back(dis[e[x][i]]);
        calc(e[x][i],x);
        sz[x]+=sz[e[x][i]];
    }
}
void solve(int x,int sz){
    if(sz==1)return;
    int rt;
    get_rt(x,x,sz,rt);
    stack<int> q;
    del[rt]=::sz[rt]=1;
    for(int i=0;i<e[rt].size();i++)if(!del[e[rt][i]]){
        tmp.clear();
        dis[e[rt][i]]=w[rt][i];tmp.push_back(w[rt][i]);
        calc(e[rt][i],rt);
        ::sz[rt]+=::sz[e[rt][i]];
        for(auto j:tmp){q.push(j);for(int k=0;k<m;k++)if(::q[k]-j>=0&&::q[k]-j<=N)ans[k]=(ans[k])|(cnt[::q[k]-j]);}
        for(auto j:tmp)cnt[j]=1;
    }
    while(!q.empty())cnt[q.top()]=0,q.pop();
    for(auto i:e[rt])if(!del[i])solve(i,::sz[i]);
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    rd(n,m);
    for(int i=1;i<n;i++){int x,y,z;rd(x,y,z);e[x].push_back(y);e[y].push_back(x);w[x].push_back(z);w[y].push_back(z);}
    for(int i=0;i<m;i++)rd(q[i]);
    cnt[0]=1;
    solve(1,n);
    for(int i=0;i<m;i++)if(ans[i])cout<<"AYE\n";else cout<<"NAY\n";
    return 0;
}

8.树上启发式合并

时间复杂度:O(n\log^2 n)

空间复杂度:O(n)

应用:通过重链性质维护一些颜色数

#include<bits/stdc++.h>
using namespace std;
namespace FastIO{
    #define il inline
    const int isz=1<<25;
    char iin[isz],*is=iin+isz,*it=iin+isz;
    #define gc() (is==it)?(it=(is=iin)+fread(iin,1,isz,stdin),(it==is)?EOF:*is++):*is++
    template<typename T> il void rd(T &x){
        char c=gc();
        bool fla=false;
        x=0;
        while(!isdigit(c)) fla|=(c=='-'),c=gc();
        while(isdigit(c)) x=(x<<1)+(x<<3)+(c&15),c=gc();
        x=(fla)?-x:x;
    } 
    template<typename T1,typename... T2> il void rd(T1 &x,T2 &...y){rd(x);rd(y...);}
    char iout[isz],*ita=iout;
    #define Flush() fwrite(iout,1,ita-iout,stdout);ita=iout
    template<typename T> il void wr(T &x,char la='\n'){
        char c[35];
        int ilen=0;
        if(x<0) x=-x,*ita++='-';
        do{c[++ilen]=(x%10+'0');x/=10;}while(x);
        while(ilen) *ita++=c[ilen--];
        *ita++=la;
    }
    template<typename T1,typename...T2> il void wr(T1 &x,T2 &...y){wr(x,' ');wr(y...);}
}
using namespace FastIO;
#define cr(x,y) e[x].push_back(y),e[y].push_back(x)
const int N=2e5+5;
int n,ans,c[N],f[N],cnt[N],sum[N],sz[N],son[N];
vector<int> e[N];
template<typename T> void mins(T &x,T y){if(sz[x]<sz[y])x=y;}
il void dfs(int x){
    sz[x]=1;
    for(auto i:e[x]) if(i!=f[x]) dfs(i),sz[x]+=sz[i],mins(son[x],i);
}
void add(int x,int d){
    --sum[cnt[c[x]]];
    cnt[c[x]]+=d;
    sum[cnt[c[x]]]++;
    for(auto i:e[x]) if(i!=f[x]) add(i,d);
}
il void check(int x,bool fl=false){
    for(auto i:e[x]) if(i!=son[x]&&i!=f[x]) check(i);
    if(son[x])check(son[x],true);
    --sum[cnt[c[x]]];
    cnt[c[x]]++;
    sum[cnt[c[x]]]++;
    for(auto i:e[x]) if(i!=son[x]&&i!=f[x]) add(i,1);
    if(cnt[c[x]]*sum[cnt[c[x]]]==sz[x]) ++ans;
    if(!fl) add(x,-1);
}
int main(){
    rd(n);
    for(int i=1;i<=n;i++) rd(c[i],f[i]),cr(i,f[i]);
    dfs(1);
    check(1,true);
    wr(ans);
    Flush();
    return 0;
}

9.倍增 LCA

时间复杂度:O(n\log n)

空间复杂度:O(n\log n)

应用:求树上路径

int n,m,f[5005][20],d[5005],w[5005][20];
vector<pair<int,int>>e[5005]; 
void dfs(int x,int fa){
    for(int i=1;i<=13;i++)f[x][i]=f[f[x][i-1]][i-1],w[x][i]=w[x][i-1]+w[f[x][i-1]][i-1];
    for(auto i:e[x])if(i.first!=fa){
        f[i.first][0]=x;
        d[i.first]=d[x]+1;
        w[i.first][0]=i.second;
        dfs(i.first,x);
    }
}
pair<int,int> lca(int x,int y){
    if(d[x]>d[y])swap(x,y);
    int res=0;
    for(int i=13;i>=0;i--)if(d[f[y][i]]>=d[x])res+=w[y][i],y=f[y][i];
    if(x==y)return {x,res};
    for(int i=13;i>=0;i--)if(f[x][i]!=f[y][i])res+=w[x][i]+w[y][i],x=f[x][i],y=f[y][i];
    return {f[x][0],res+w[x][0]+w[y][0]};
}

10.树上差分

时间复杂度:O(n\log n)

空间复杂度:O(n)

值得注意的是,点权差分应该是 d[x]+=a,d[y]+=a,d[lca(x,y)]-=a,d[f[lca(x,y)]]-=a,然而边权差分应该是边权转点权 d[x]+=a,d[y]+=a,d[lca(x,y)]-=2*a

11.网络流

时间复杂度:O(n^2m)

空间复杂度:O(n+m)

应用:构造网络流图进行计算

给出 dinic 的代码:

int n;
int dep[5005],ans=0,flag;
int a[5005][5005];
void dfs(int x){
    for(int i=0;i<=2*n*n+1;i++){
        if(a[x][i]&&!dep[i]){
            dep[i]=dep[x]+1;
            dfs(i);
        }
    }
}
int bfs(int x,int f){
    if(x==2*n*n+1){flag=true;return f;}
    for(int i=0;i<=2*n*n+1;i++)if(dep[i]==dep[x]+1&&a[x][i]){
        int fl;
        fl=bfs(i,min(f,a[x][i]));
        if(flag){
            a[x][i]-=fl;
            a[i][x]+=fl;
            return fl;
        }
    }
    return 0;
}
void dinic(){
    int flow;
      do{
        memset(dep,0,sizeof dep);
        dep[0]=1;
        dfs(0);
        flag=0;
        flow=bfs(0,0x3f3f3f3f);
        ans+=flow;
    }while(flow);
}
Q.E.D

Ⅴ.数学

1.线性筛

时间复杂度:O(n)

空间复杂度:O(n)

应用:筛质数,筛积性函数,筛质数个数

int t,pri[1000005],prime[1000005],idx,mul[1000005],phi[1000005];
const int len=1000000;
void init(){
    mul[1]=1;
    phi[1]=1;
    memset(pri,true,sizeof pri);
    for(int i=2;i<=len;i++){
        if(pri[i]) {prime[++idx]=i;mul[i]=-1;phi[i]=i-1;}
        for(int j=1;j<=idx&&i*prime[j]<=len;j++){
            pri[i*prime[j]]=false;
            if(i%prime[j]){mul[i*prime[j]]=-mul[i];phi[i*prime[j]]=phi[i]*(prime[j]-1);}
            else{mul[i*prime[j]]=0;phi[i*prime[j]]=phi[i]*prime[j];break;}
        }
    }
    for(int i=1;i<=len;i++)mul[i]+=mul[i-1],phi[i]+=phi[i-1];
}

2.费马小定理

时间复杂度:O(\log n)

空间复杂度:O(1)

应用:求模数为质数时的逆元

\large a^{p-1} \equiv 1 \pmod{p}\hspace{0.5em}p\in prime

详见数学章快速幂

3.快速幂

时间复杂度:O(\log n)

空间复杂度:O(1)

数字:

const int mod=1e9;
int power(int x,int y){
    if(x==0) return 0;
    int res=1;
    while(y){
        if(y&1)res=res*x%mod;
        x=x*x%mod;
        y>>=1;
    }
    return res;
}

矩阵:

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int n;
long long k;
struct node{
    long long a[105][105];
    node(){
        memset(a,0,sizeof(a));
    }
    void init(){
        for(int i=0;i<n;i++)
            a[i][i]=1;
    }
}ans,a;
node operator*(node x,node y){
    node z;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            for(int k=0;k<n;k++)
                z.a[j][k]=(z.a[j][k]+x.a[j][i]*y.a[i][k]%mod)%mod;
    return z;
}
void power(){
    ans.init();
    while(k){
        if(k&1)
            ans=ans*a;
        a=a*a;
        k>>=1;          
    }
}
int main(){
    cin>>n>>k;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            cin>>a.a[i][j];
    power();
    for(int i=0;i<n;i++){
        for(int j=0;j<n;j++)
            cout<<ans.a[i][j]<<" ";
        cout<<endl;
    }
    return 0;
} 

4.FWT

时间复杂度:O(n \log n)

空间复杂度:O(n)

应用:二进制卷积

注意 n\neq 2^k 时要补 0

#include<bits/stdc++.h>
using namespace std;
namespace fastio{
    #define il inline
    const int isz=1<<25;
    char iin[isz],*is=iin+isz,*it=iin+isz;
    #define gc() (is==it)?(it=(is=iin)+fread(iin,1,isz,stdin),(is==it)?EOF:*is++):*is++
    template<typename T> il void rd(T &x){
        x=0;
        char c=gc();
        bool fla=false;
        while(!isdigit(c)) fla|=(c=='-'),c=gc();
        while(isdigit(c)) x=(x<<1)+(x<<3)+(c&15),c=gc();
        x=(fla)?-x:x;
    }
    template<typename T1,typename...T2> il void rd(T1 &x,T2&...y){rd(x);rd(y...);}
    template<typename T,typename T1> il void rd(T a[],T1 s,T t){for(T i=s;i<=t;i++) rd(a[i]);}
    char iout[isz],*ita=iout;
    #define Flush() fwrite(iout,1,ita-iout,stdout);ita=iout
    template<typename T> il void wr(T x,char la='\n'){
        char c[35];
        int len=0;
        if(x<0) *ita++='-',x=-x;
        do{c[++len]=(x%10+'0');x/=10;}while(x);
        while(len)*ita++=c[len--];
        *ita++=la;
    } 
    il void en(char x='\n'){*ita++=x;}
}
using namespace fastio;
#define int long long
const int N=(1<<17)+10,mod=998244353;
int n,a[N],b[N],c[N],d[N];
void cop(){for(int i=0;i<n;i++) c[i]=a[i],d[i]=b[i];}
void times(){for(int i=0;i<n;i++)c[i]=c[i]*d[i]%mod;}
void fwt_or(int a[],int opt){for(int i=1;i<n;i<<=1)for(int j=0;j<n;j+=(i<<1))for(int k=0;k<i;k++)a[j+i+k]=(a[j+k+i]+a[j+k]*opt+mod)%mod;}
void fwt_and(int a[],int opt){for(int i=1;i<n;i<<=1)for(int j=0;j<n;j+=(i<<1))for(int k=0;k<i;k++)a[j+k]=(a[j+k+i]*opt+a[j+k]+mod)%mod;}
void fwt_xor(int a[]){for(int i=1;i<n;i<<=1)for(int j=0;j<n;j+=(i<<1))for(int k=0;k<i;k++){int t=a[j+k];a[j+k]=(a[j+k]+a[j+k+i])%mod;a[i+k+j]=(t-a[i+j+k]+mod)%mod;}}
void ifwt_xor(int a[]){for(int i=1;i<n;i<<=1)for(int j=0;j<n;j+=(i<<1))for(int k=0;k<i;k++){int t=a[j+k];a[j+k]=(a[j+k]+a[j+k+i]+(((a[j+k]+a[j+k+i])%2)?mod:0))/2%mod;a[i+k+j]=(t-a[i+j+k]+mod+(((t-a[i+j+k]+mod)%2)?mod:0))/2%mod;}}
signed main(){
    cin>>n;
    n=1<<n;
    rd(a,0,n-1),rd(b,0,n-1);
    cop();fwt_or(c,1);fwt_or(d,1);times();fwt_or(c,-1);for(int i=0;i<n;i++)wr(c[i],' ');en();Flush();
    cop();fwt_and(c,1);fwt_and(d,1);times();fwt_and(c,-1);for(int i=0;i<n;i++)wr(c[i],' ');en();Flush();
    cop();fwt_xor(c);fwt_xor(d);times();ifwt_xor(c);for(int i=0;i<n;i++)wr(c[i],' ');Flush();
    return 0;
}

5.exgcd

时间复杂度:O(\log n)

空间复杂度:O(1)

应用:求逆元,求二元一次不定方程

inline ll exgcd(ll a,ll b){
    if(b==0){
        x=1;
        y=0;
        return a;
    }
    ll d=exgcd(b,a%b);
    ll t=x;
    x=y;
    y=t-a/b*y;
    return d;
}
Q.E.D