Loading

树上操作(启发式合并、淀粉质、边分治、树套树)

树上性感操作

序言

好的又来写序言了,其实我这一块学的不是很扎实,所以这篇文章就是自己的一个整理,写的丑勿喷。

树上启发式合并(Dsu on Tree)

em学这个首先要学会启发式合并,就是说当两个集合需要合并到一起的时候,把大小较小的集合合并到较大的集合里,这个操作的复杂度仅为 \(O(\log n)\),其实我觉得比较神奇,证明不会证,其实也没有必要学证明(谁出题靠你启发式合并的证明啊?)。

现在考虑树上启发式合并,主要过程就是说,当我们需要处理一些关于子树操作的题目时,一般需要把子树的贡献合并一下,这个时候就是树上启发式合并的作用了。

我们考虑这样一个问题:


  • 有一棵 \(n\) 个结点的以 \(1\) 号结点为根的有根树
  • 每个结点都有一个颜色,颜色是以编号表示的, \(i\) 号结点的颜色编号为 \(c_i\)
  • 如果一种颜色在以 \(x\) 为根的子树内出现次数最多,称其在以 \(x\) 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
  • 你的任务是对于每一个 \(i\in[1,n]\),求出以 \(i\) 为根的子树中,占主导地位的颜色的编号和。
  • \(n\le 10^5,c_i\le n\)

那其实就是要求出子树 \(size\) 最大的编号和。

其实可以用 \(O(n^2)\) 暴力硬搞一下,然后就直接T飞。

然后我们发现其实每个子树的贡献是互相独立的,那么其实就可以单独搞一下。

具体过程如下:

  1. 首先 \(O(n)\) 遍历一下每个节点,求出重儿子(啊没错就是树剖的那个)。
  2. 然后对于每个 \(u\) 的子树先处理一下轻儿子答案,再处理一下重儿子(注意,这里轻儿子只计算答案,重儿子不仅要计算答案,还要计算对应的贡献)。
  3. 再处理一下轻儿子贡献合并一下。

这样看似没什么变化,但是复杂度神奇地变成了 \(O(n \log n)\),别问我,我不会证。

#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define PII pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
template<typename P>
inline void read(P &x){
    P res=0,f=1;
    char ch=getchar();
    while(ch<'0' || ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9'){
        res=(res<<3)+(res<<1)+(ch^48);
        ch=getchar();
    }
    x=res*f;
}
template<typename PP>
inline void write(PP x){
    if(x<0) putchar('-'),x=-x;
    if(x>=10) write(x/10);
    putchar('0'+x%10);
}

const int N=1e5+10;

int n;
int col[N],cnt[N];
int siz[N],son[N];
struct edge{
    int to,nxt;
}e[N<<1];
int head[N],tot=0;
ll ans[N],sum;
int flag,maxc;

void add(int u,int v){
    e[++tot].to=v;
    e[tot].nxt=head[u];
    head[u]=tot;
}
void dfs1(int u,int fa){
    siz[u]=1;
    for(int i=head[u];i;i=e[i].nxt){
        int to=e[i].to;
        if(to==fa) continue;
        dfs1(to,u);
        siz[u]+=siz[to];
        if(siz[to]>siz[son[u]]) son[u]=to;
    }
}

void count(int u,int fa,int val){
    cnt[col[u]]+=val;
    if(cnt[col[u]]>maxc) maxc=cnt[col[u]],sum=col[u];
    else if(cnt[col[u]]==maxc) sum+=col[u];
    for(int i=head[u];i;i=e[i].nxt){
        int to=e[i].to;
        if(to==fa || to==flag) continue;
        count(to,u,val);
    }
}
void dfs2(int u,int fa,bool kep){
    for(int i=head[u];i;i=e[i].nxt){
        int to=e[i].to;
        if(to==fa || to==son[u]) continue;
        dfs2(to,u,0); 
    }
    if(son[u]) dfs2(son[u],u,1),flag=son[u];
    count(u,fa,1);
    flag=0;
    ans[u]=sum;
    if(!kep) count(u,fa,-1),sum=maxc=0;
}

signed main(){
    read(n);
    for(int i=1;i<=n;++i) read(col[i]);
    for(int i=1;i<n;++i){
        int u,v;
        read(u),read(v);
        add(u,v);
        add(v,u);
    }
    dfs1(1,0);
    dfs2(1,0,0);
    for(int i=1;i<=n;++i) cout<<ans[i]<<' ';
    cout<<endl;
    return 0;
}

剩下的以后再补上吧,现在还是不太会。

点分治

当我们处理树上问题时,有没有一种算法能够把复杂度为 \(O(n^2)\) 变成 \(O(n\log n)\) 呢。\(\log n\) 让我们联想到分治,由此出现了点分治。

点分治就是说把某个点去掉后,树会被分成两部分,分别处理两部分后,再分别对两部分分别找一个点继续上面的步骤。

但是我们发现如果只找普通的点就不优,怎么办。

这时我们想到了树的重心,每个子树的大小都趋近于相等,现在我们发现如果去掉重心,每次的规模最少减半,所以可以保证复杂度了。

具体的有如下题目:

给定一棵有 $n$ 个点的树,询问树上距离为 $k$ 的点对是否存在。

我们分成两种路径,第一种是经过重心 \(rt\) 的路径,第二种在子树内不经过重心的路径。

然后对于一个新找出的重心,我们先讨论子树之间的贡献,然后对每个子树的贡献进行递归统计。

其实还是很好理解的。这里不再赘述。

#include<bits/stdc++.h>
#define PII pair<int,int>
#define mk(a,b) make_pair(a,b)
using namespace std;
template<typename P>
inline void read(P &x){
    P res=0,f=1;
    char ch=getchar();
    while(ch<'0' || ch>'9'){
        if(ch=='-') f=-1;
        ch=getchar();
    }
    while(ch>='0' && ch<='9'){
        res=res*10+ch-'0';
        ch=getchar();
    }
    x=res*f;
}
int T=1;
const int N=100010,inf=10000010;
struct edge{
    int to,nxt,w;
}e[N<<1];
int n,m,cnt=0,head[N];
int maxp[N],siz[N],dis[N],rem[N];
int vis[N],tet[inf],judge[inf],q[N];
int query[1010];
int sum,rt;
void add(int u,int v,int w){
    e[++cnt].w=w;
    e[cnt].nxt=head[u];
    e[cnt].to=v;
    head[u]=cnt;
}
void getrt(int u,int fa){
    siz[u]=1;maxp[u]=0;
    for(int i=head[u];i;i=e[i].nxt){
        int to=e[i].to;
        if(to==fa || vis[to]) continue;
        getrt(to,u);
        siz[u]+=siz[to];
        maxp[u]=max(maxp[u],siz[to]);
    }
    maxp[u]=max(maxp[u],sum-siz[u]);
    if(maxp[u]<maxp[rt]) rt=u;
}
void getdis(int u,int fa){
    rem[++rem[0]]=dis[u];
    for(int i=head[u];i;i=e[i].nxt){
        int to=e[i].to;
        if(to==fa || vis[to]) continue;
        dis[to]=dis[u]+e[i].w;
        getdis(to,u);
    }
}
void calc(int u){
    int p=0;
    for(int i=head[u];i;i=e[i].nxt){
        int to=e[i].to;
        if(vis[to]) continue;
        rem[0]=0,dis[to]=e[i].w;
        getdis(to,u);
        for(int j=rem[0];j;--j){
            for(int k=1;k<=m;++k){
                if(query[k]>=rem[j]){
                    tet[k]|=judge[query[k]-rem[j]];
                }
            }
        }
        for(int j=rem[0];j;--j) q[++p]=rem[j],judge[rem[j]]=1;
    }
    for(int i=1;i<=p;++i) judge[q[i]]=0;
}
void sol(int u){
    vis[u]=judge[0]=1;calc(u);
    for(int i=head[u];i;i=e[i].nxt){
        int to=e[i].to;
        if(vis[to]) continue;
        sum=siz[to];maxp[rt=0]=inf;
        getrt(to,0),sol(rt);
    }
}
signed main(){
    read(n),read(m);
    for(int i=1;i<n;++i){
        int u,v,w;
        read(u),read(v),read(w);
        add(u,v,w),add(v,u,w);
    }
    for(int i=1;i<=m;++i) read(query[i]);
    maxp[rt]=sum=n;
    getrt(1,0);
    sol(rt);
    for(int i=1;i<=m;++i){
        if(tet[i]) printf("AYE\n");
        else printf("NAY\n");
    }
    return 0;
}

树套树

字面意思,就是某种特殊树的结点还是某颗特殊树。

常见搭配:线段树套平衡树、线段树套主席树、主席树套树状数组...

其实就是说一个树解决不了问题就再套一颗树。

一般来说

posted @ 2024-09-09 16:12  God_Max_Me  阅读(7)  评论(0编辑  收藏  举报