树分治

点分治

点分治适合处理大规模的树上路径信息问题。

本质思想是选择一点作为分治中心,将原问题划分为几个相同的子树上的问题,进行递归解决。

常见的用于统计树上有关路径的信息。假设当前选定根节点为 \(rt\),则符合条件的路径必然是以下两种之一:

  1. 经过 \(rt\) 或一段在 \(rt\) 上;
  2. \(rt\) 的子树上。

P3806

P3806【模板】点分治 1

给定一棵树,边有权值。

多次询问,查询树上是否有长度为 \(k\) 的路径。

先随意取一个点作为根节点 \(rt\)。对于经过 \(rt\) 的路径,先枚举其所有子节点 \(v\),以 \(v\) 为根计算 \(v\) 子树中所有节点到 \(rt\) 的距离。

\(dis_i\) 为当前节点 \(i\)\(rt\) 的距离,\(pd_{key}\) 为之前处理过的子树中是否存在一个节点 \(v\) 使得 \(dis_v=key\)

若一个询问的 \(k\) 满足 \(pd_{k-dis_i}={true}\) 则存在一个长度为 \(k\) 的路径。

在计算完 \(v\) 子树中所连的边能否成为答案后,将这些新的距离加入到 \(pd\) 中。

注意清空 \(pd\) 时应将之前占用过的 \(pd\) 位置加入到一个队列中清空,依此来保证时间复杂度。

点分治过程中,每一层的所有递归过程合计对每个点处理一次,假设共递归 \(h\) 层,则总时间复杂度为 \(\mathcal O(hn)\)

若每次选择子树的重心作为根节点,可以保证递归层数最少,时间复杂度为 \(\mathcal O(n\log n)\)

const int N=1e4+5;
const int M=1e7+5;
int n,m,q[N],rt,sum,siz[N],dp[N],dis[N],pd[M],ret[N],vis[N],d[N],tot;
vector<tuple<int,int>>G[N];
queue<int>tag;

inline void getroot(int u,int f){
    siz[u]=1;dp[u]=0;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

inline void getdis(int u,int f){
    d[++tot]=dis[u];
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            dis[v]=dis[u]+w;
            getdis(v,u);
        }
}

inline void solve(int u,int f){
    pd[0]=vis[u]=1;
    tag.push(0);
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            dis[v]=w;
            getdis(v,u);
            for(int k=1;k<=tot;k++)
                for(int i=1;i<=m;i++)
                    if(q[i]>=d[k])
                        ret[i]|=pd[q[i]-d[k]];
            for(int k=1;k<=tot;k++)
                if(d[k]<M)
                    tag.push(d[k]),pd[d[k]]=1;
            tot=0;
        }
    while(tag.size())
        pd[tag.front()]=0,tag.pop();
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            sum=siz[v];
            dp[rt=0]=inf;
            getroot(v,u);
            solve(rt,u);
        }
}

signed main(){
    sum=n=read();m=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    for(int i=1;i<=m;i++)
        q[i]=read();
    dp[rt=0]=inf;
    getroot(1,0);
    solve(rt,0);
    for(int i=1;i<=m;i++)
        puts(ret[i]?"AYE":"NAY");
    return 0;
}

P4178

P4178 Tree

给定一棵树,边有权值。

查询树上长度小于等于 \(k\) 的路径数量。

递归时,对于当前根节点 \(rt\),把每一个子节点到 \(rt\) 的距离排序,然后用类似双指针的方法来求小于等于 \(k\) 的边的数量。

在同一子树内的路径是不合法的,但是同样也会算进答案中。

所以对 \(rt\) 的每一条边进行遍历,利用容斥的思想减去不合法的路径,把经过了从重心到新遍历的点的边两次的路径剪掉,统计答案。

const int N=4e4+5;
int n,k,q[N],rt,sum,siz[N],dp[N],dis[N],ret[N],vis[N],d[N],tot,ans;
vector<tuple<int,int>>G[N];
queue<int>tag;

inline void getroot(int u,int f){
    siz[u]=1;dp[u]=0;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

inline void getdis(int u,int f){
    d[++tot]=dis[u];
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            dis[v]=dis[u]+w;
            getdis(v,u);
        }
}

inline int work(int u,int w){
    tot=0;dis[u]=w;
    getdis(u,0);
    sort(d+1,d+1+tot);
    int l=1,r=tot,res=0;
    while(l<=r)
        if(d[l]+d[r]<=k)
            res+=r-l,l++;
        else
            r--;
    return res;
}

inline void solve(int u,int f){
    vis[u]=1;ans+=work(u,0);
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            ans-=work(v,w);
            sum=siz[v];
            dp[0]=n,rt=0;
            getroot(v,u);
            solve(rt,u);
        }
}

signed main(){
    n=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    k=read();
    dp[rt=0]=inf;
    getroot(1,0);
    solve(rt,0);
    write(ans);
    return c0;
}

P4149

P4149 [IOI2011]

给一棵树,边有权值。

求一条简单路径,权值和等于 \(k\),且边的数量最小。

递归时,对于当前根节点 \(rt\),计算所有经过 \(r\) 的路径,可以开个桶记录。

具体地,令 \(mn_i\) 为从 \(i\) 开始的权值为 \(i\) 的所有路中边数的最小值。

\(dis_i\) 为当前点 \(i\)\(rt\) 的路径长度,\(cnt_i\) 为当前点 \(i\)\(rt\) 的边数。

更新答案时,用当前子树 \(u\)\(dis\) 和前面子树的桶来更新,即 \(ans=\min\limits_{v\in son_u}(ans,cnt_v+mn_{k-dis_v})\)

const int N=2e6+5;
int n,k,siz[N],vis[N],rt,dp[N],sum,dis[N],d[N],cnt[N],c[N],tot,mn[N],ans;
vector<tuple<int,int>>G[N];

inline void getroot(int u,int fa){
    siz[u]=1;dp[u]=0;
    for(auto[v,w]:G[u])
        if(v!=fa and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

inline void getdis(int u,int fa){
    if(dis[u]>k)
        return;
    d[++tot]=dis[u];c[tot]=cnt[u];
    for(auto[v,w]:G[u])
        if(v!=fa and !vis[v]){
            dis[v]=dis[u]+w;
            cnt[v]=cnt[u]+1;
            getdis(v,u);
        }
}

inline void work(int u){
    tot=0;mn[0]=0;
    for(auto[v,w]:G[u])
        if(!vis[v]){
            int tmp=tot;
            dis[v]=w;
            cnt[v]=1;
            getdis(v,u);
            for(int i=tmp+1;i<=tot;i++)
                ans=min(ans,mn[k-d[i]]+c[i]);
            for(int i=tmp+1;i<=tot;i++)
                mn[d[i]]=min(mn[d[i]],c[i]);
        }
    for(int i=1;i<=tot;i++)
        mn[d[i]]=inf;
}

inline void solve(int u,int fa){
    vis[u]=1;
    work(u);
    for(auto[v,w]:G[u])
        if(v!=fa and !vis[v]){
            sum=siz[v];rt=0;
            getroot(v,u);
            solve(rt,u);
        }
}

signed main(){
    sum=n=read();k=read();ans=inf;
    for(int i=1;i<n;i++){
        int u=read()+1,v=read()+1,w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    dp[rt=0]=inf;
    memset(mn,0x3f,sizeof(mn));
    getroot(1,0);
    solve(rt,0);
    write((ans>=n)?-1:ans);    
    return 0;
}

P2634

P2634 [国家集训队] 聪聪可可

给一棵树,边有权值。

求任意选定两点,其最短路径的长度为 \(3\) 的倍数的概率。

递归时,对于当前根节点 \(rt\),令 \(cnt_i\) 为其子树内所有点在模 \(3\) 意义下到 \(rt\) 距离为 \(i\) 的数量。

直接统计即可,答案就是 \(cnt_0^2+2\times cnt_1\times cnt_2\)

const int N=2e4+5;
int n,dis[N],vis[N],d[N],tot,siz[N],dp[N],sum,rt,t[4],ans;
vector<tuple<int,int>>G[N];

inline int gcd(int x,int y){return y?gcd(y,x%y):x;}

inline void getroot(int u,int f){
    siz[u]=1;dp[u]=0;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

inline void getdis(int u,int f){
    t[dis[u]]++;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            dis[v]=(dis[u]+w)%3;
            getdis(v,u);
        }
}

inline int work(int u,int w){
    t[0]=t[1]=t[2]=0;
    dis[u]=w%3;
    getdis(u,0);
    return t[0]*t[0]+2*t[1]*t[2];
}

inline void solve(int u,int f){
    vis[u]=1;ans+=work(u,0);
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            ans-=work(v,w);
            dp[rt=0]=inf;
            sum=siz[v];
            getroot(v,u);
            solve(rt,u);
        }
}

signed main(){
    sum=n=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    dp[rt=0]=inf;
    getroot(1,0);
    solve(rt,0);
    int g=gcd(ans,n*n);
    printf("%lld/%lld",ans/g,n*n/g);
    return 0;
}

P2664

P2664 树上游戏

给一棵树,点有颜色。

定义 \(s(i,j)\)\(i\)\(j\) 的颜色的数量。定义 \(sum_i=\sum_{j=1}^ns(i,j)\)

对所有的 \(1\leq i\leq n\),求所有的 \(sum_i\)

发现 \(sum_i\) 在点分治中是不好统计答案的。

考虑转化。考虑每个颜色对 \(sum_i\) 的贡献。

对于每个颜色 \(j\),其中一个端点为 \(i\) 且含有颜色 \(j\) 的路径数量记为 \(cnt_j\),那么 \(sum_i=\sum cnt_j\)

\(cnt_j\) 是较好处理的。只需要每遇到一个新颜色,有 \(cnt_{col_u}\leftarrow cnt_{col_u}+size_u\),表示这个子树里的所有节点都在这个颜色上对 \(u\)​ 的答案有贡献。

考虑到点分治过程中,需要分别考虑统计:

  1. 子树中以当前根节点为端点的路径对根的贡献;
  2. LCA 为当前根节点的路径对子树内每个点的贡献。

对于 1 部分,比较好办,由于点分治中,递归层数不超过 \(\mathcal O(\log n)\),所以每一层都可以遍历所有子树。

此时可以用 \(sum_i\) 的定义来在遍历子树的过程中顺便统计。

对于 2 部分,设当前根节点为 \(rt\),一个子节点为 \(u\)\(u\) 的子树内任取一节点为 \(v\),那么 \(v\) 的答案可分为两部分:

  1. \((u,v)\) 上出现过的颜色,数量设为 \(num\)\(rt\) 除了 \(u\) 以外的其他所有子树的总大小设为 \(size_1\),那么这些出现过的颜色对 \(v\) 的答案贡献为 \(num\times size_1\)
  2. \((u,v)\) 上没出现过的颜色 \(j\),它们的贡献来自于 \(rt\) 除了 \(u\) 以外的所有其他子树的 \(cnt_j\),这一部分的答案为 \(\sum_{j\notin(u,v)}cnt_j\)
const int N=2e5+5;
int n,dp[N],rt,nn,vis[N],siz[N],c[N];
vector<int>G[N];

inline void getroot(int u,int f){
    siz[u]=1;dp[u]=0;
    for(auto v:G[u])
        if(v!=f and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],nn-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

int ans[N],sum,cnt[N],mk[N],nowrt;

inline void getdis(int u,int f,int now){
    siz[u]=1;
    if(!mk[c[u]]){
        sum-=cnt[c[u]];
        now++;
    }
    mk[c[u]]++;
    ans[u]+=sum+now*siz[nowrt];
    for(auto v:G[u])
        if(v!=f and !vis[v]){
            getdis(v,u,now);
            siz[u]+=siz[v];
        }
    mk[c[u]]--;
    if(!mk[c[u]])
        sum+=cnt[c[u]];
}

inline void getcnt(int u,int f){
    if(!mk[c[u]]){
        cnt[c[u]]+=siz[u];
        sum+=siz[u];
    }
    mk[c[u]]++;
    for(auto v:G[u])
        if(v!=f and !vis[v])
            getcnt(v,u);
    mk[c[u]]--;
}

inline void clr(int u,int f,int now){
    if(!mk[c[u]])
        now++;
    mk[c[u]]++;
    ans[u]-=now;
    ans[nowrt]+=now;
    for(auto v:G[u])
        if(v!=f and !vis[v])
            clr(v,u,now);
    mk[c[u]]--;
    cnt[c[u]]=0;
}

inline void clr2(int u,int f){
    cnt[c[u]]=0;
    for(auto v:G[u])
        if(v!=f and !vis[v])
            clr2(v,u);
}

int son[N];

inline void solve(int u){
    vis[u]=1;
    int tot=0;nowrt=u;ans[u]++;
    for(auto v:G[u])
        if(!vis[v])
            son[++tot]=v;
    siz[u]=sum=cnt[c[u]]=1;mk[c[u]]++;
    for(int i=1;i<=tot;i++){
        int v=son[i];
        getdis(v,u,0);getcnt(v,u);
        siz[u]+=siz[v];
        cnt[c[u]]+=siz[v];
        sum+=siz[v];
    }
    clr2(u,0);
    siz[u]=sum=cnt[c[u]]=1;
    for(int i=tot;i;i--){
        int v=son[i];
        getdis(v,u,0);getcnt(v,u);
        siz[u]+=siz[v];
        cnt[c[u]]+=siz[v];
        sum+=siz[v];
    }
    mk[c[u]]--;
    clr(u,0,0);
    for(auto v:G[u])
        if(!vis[v]){
            nn=siz[v];
            dp[rt=0]=inf;
            getroot(v,u);
            solve(rt);
        }
}

signed main(){
    nn=n=read();
    for(int i=1;i<=n;i++)
        c[i]=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        G[u].eb(v);G[v].eb(u);
    }
    dp[rt=0]=inf;
    getroot(1,0);
    solve(rt);
    for(int i=1;i<=n;i++)
        write(ans[i]),puts("");
    return 0;
}

边分治

和点分治类似,边分治每次选一条边,考虑跨过这条边的路径贡献。

为了保证复杂度,会让两边子树大小尽量接近。

发现菊花图无论怎么分治都是无效的,考虑将原树转化为二叉树。

具体地,如果 \(u\) 有两个儿子,就新建一个节点连接第二个儿子,如果还有更多的儿子,就再新建节点与上一次新建的节点当前的儿子相连,这样复杂度是正确的。

具体过程和点分治类似,求当前的子树大小,求当前的重心边,递归处理子问题。

边分治具有优美的性质,每次分治只会考虑两棵子树之间的贡献,也就完全不需要点分治中容斥掉自己贡献的操作。缺点是点数翻倍。

P4565

P4565 [CTSC2018] 暴力写挂

给定两棵树,求 \({depth}(x)+{depth}(y)-({depth}({LCA}(x,y))+{depth}^{\prime}({LCA}^{\prime}(x,y)))\) 的最大值。

考虑把答案改写成 \(\dfrac 12\left({dis}(x,y)+{depth}(x)+{depth}(y)-2\times{depth}^{\prime}({LCA}^{\prime}(x,y))\right)\)

对第一棵树边分治,并建出每个点从根到这个点的边分树,

对于边分树上每个非叶子结点,维护 \(vl,vr\) 分别表示左、右子树所包含的原树中的节点中 \({dis}(x)+{depth}(x)\) 的最大值,其中 \({dis}(x)\)\(x\) 到原树中这个点对应的边某一个端点的距离。

然后对第二棵树 dfs,枚举 \({LCA}^{\prime}(x,y)\),每遇到一条边,就把两个端点的边分树按照线段树的合并方法合并起来,然后在合并过程中统计答案。

假设这两棵边分树同时包括某一个点,这个点在两棵边分树的编号分别是 \(u,v\)

那么可以用 \(\max(vl_u+vr_v,vr_u+vl_v)-2\times{depth}^{\prime}({LCA}^{\prime}(x,y))\) 来更新全局答案并更新合并后的点的 \(vl,vr\)

不难发现这样统计的点对 \((x,y)\)\({LCA}\) 一定都是我们当前枚举到的点。

const int N=366666+5;
template<unsigned M>struct graph{
    int to[M<<1],pre[M<<1],lst[M],tot,w[M<<1];
    inline void add(int u,int y,int z){
        to[++tot]=y;
        pre[tot]=lst[u];
        lst[u]=tot;
        w[tot]=z;
    }
};

graph<N>g1,g3;graph<N<<1>g2;
int n,now,siz[N<<1],nows,ed,li,cnt,mx[N<<5|1][2],ch[N<<5|1][2],lst[N],dis[N],tmp,ans=-inf,rt[N];
bool del[N<<2];

inline void dfs(int u,int fa){
    int las=u;
    for(int i=g1.lst[u];i;i=g1.pre[i]){
        int v=g1.to[i];
        if(v==fa)
            continue;
        g2.add(las,++now,0),g2.add(now,las,0),g2.add(now,v,g1.w[i]),g2.add(v,now,g1.w[i]);
        dis[v]=dis[u]+g1.w[i];las=now;dfs(v,u);
    }
}

inline void dfs2(int u,int fa,int nowd,int op){
    if(!op)
        tmp++;
    if(u<=n){
        cnt++;
        if(!lst[u])
            rt[u]=lst[u]=cnt,cnt++;
        ch[lst[u]][op]=cnt;
        mx[lst[u]][op]=dis[u]+nowd;
        lst[u]=cnt;
    }
    for(int i=g2.lst[u];i;i=g2.pre[i]){
        int v=g2.to[i];
        if(v==fa or del[i])
            continue;
        dfs2(v,u,nowd+g2.w[i],op);
    }
}

inline void get(int u,int fa){
    siz[u]=1;
    for(int i=g2.lst[u];i;i=g2.pre[i]){
        int v=g2.to[i];
        if(v==fa or del[i])
            continue;
        get(v,u);
        siz[u]+=siz[v];
        if(max(siz[v],nows-siz[v])<li)
            li=max(siz[v],nows-siz[v]),ed=(i+1)>>1;
    }
}

inline void solve(int u,int s){
    if(s==1)
        return;
    ed=li=1e9;nows=s;
    get(u,0);
    int r1=g2.to[(ed<<1)-1],r2=g2.to[ed<<1];
    del[(ed<<1)-1]=del[ed<<1]=1;tmp=0;
    dfs2(r1,r2,0,0),dfs2(r2,r1,g2.w[ed<<1],1);
    int tt=siz[u]-tmp;
    solve(r1,tmp);solve(r2,tt);
}

inline int merge(int u,int y,int t){
    if((!u)||(!y))return u+y;
    ans=max(ans,max(mx[u][0]+mx[y][1],mx[y][0]+mx[u][1])+2*t);
    mx[u][0]=max(mx[u][0],mx[y][0]),mx[u][1]=max(mx[u][1],mx[y][1]);
    ch[u][0]=merge(ch[u][0],ch[y][0],t),ch[u][1]=merge(ch[u][1],ch[y][1],t);
    return u;
}

inline void dfs3(int u,int fa,int nowd){
    ans=max(ans,2*(dis[u]-nowd));
    for(int i=g3.lst[u];i;i=g3.pre[i]){
        int v=g3.to[i];
        if(v==fa)
            continue;
        dfs3(v,u,nowd+g3.w[i]);
        rt[u]=merge(rt[u],rt[v],-nowd);
    }
}

signed main(){
    memset(mx,0xc0,sizeof(mx));
    now=n=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        g1.add(u,v,w);g1.add(v,u,w);
    }
    dfs(1,0);
    solve(1,now);
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        g3.add(u,v,w);g3.add(v,u,w);
    }
    dfs3(1,0,0);
    write(ans>>1);
    return 0;
}

点分树

点分树是通过更改原树形态使树的层数变为稳定 \(\mathcal O(\log n)\)​ 的一种重构树。

通过点分治每次找重心的方式来对原树进行重构。

将每次找到的重心与上一层的重心缔结父子关系,这样就可以形成一棵 \(\mathcal O(\log n)\) 层的树。

常用于解决与树原形态无关的带修改问题。比如路径问题,联通块问题,寻找关键点问题等等。

点分树有以下性质:

  1. 高度与点分治的深度一样,只有 \(\mathcal O(\log n)\)
  2. 对于任意两点 \(u,v\),唯一可以确定的是 \(u,v\) 在点分树上的 LCA 一定在路径 \(u\leadsto v\) 上。

P6329

P6329 【模板】点分树 | 震波

给定一棵树,点有点权。

多次询问,单点修改,或查询 \(\sum\limits_{{dis}(x,y)\leq k}a_y\)

考虑枚举 \(x,y\) 在点分树上的 LCA \(z\)。显然有 \({dis}(x,y)={dis}(x,z)+{dis}(z,y)\)

所以有 \(ans=\sum\limits_{{dis}(x,z)+{dis}(z,y)\leq k\&{LCA}(x,y)=z}a_y=\sum\limits_{{dis}(z,y)\leq k-{dis}(x,z)\&{LCA}(x,y)=z}a_y\)

考虑什么样的 \(y\) 满足 \({LCA}(x,y)=z\)。显然其组成的集合为 \(z\) 的子树去掉在 \(z\)\(x\) 方向上的儿子 \(s\) 的子树。

现在要求的是在这个集合中距离 \(\leq k-{dis}(x,z)\) 的点权和,这个是可以差分的。

那么对每个点 \(x\) 建一棵动态开点线段树,下标为 \(i\) 的位置维护 \(x\) 子树内所有 \({dis}(x,z)=i\)\(a_z\) 的和。

因此求 \(x\) 子树内到 \(x\) 的距离 \(\leq k-{dis}(x,z)\) 的点权和就在对应线段树上查个区间和就行了。

考虑求 \(z\)\(x\) 方向上的儿子 \(s\) 的子树。对于每个点再建立一棵动态开点线段树,线段树上下标为 \(i\) 的位置维护 \(x\) 子树内到 \(fa_x\) 距离为 \(i\) 的点权和。求解时直接查询 \(\left[0,k-{dis}(x,z)\right]\)​ 的和即可。

const int N=1e5+5;
int n,q,a[N],siz[N],son[N],dep[N],fa[N],top[N];
vector<int>G[N];

inline void dfs(int u,int f){
    fa[u]=f;siz[u]=1;dep[u]=dep[f]+1;
    for(auto v:G[u])
        if(v!=f){
            dfs(v,u);
            siz[u]+=siz[v];
            if(siz[v]>siz[son[u]])
                son[u]=v;
        }
}

inline void dfs2(int u,int topf){
    top[u]=topf;
    if(son[u])
        dfs2(son[u],topf);
    for(auto v:G[u])
        if(v!=fa[u] and v!=son[u])
            dfs2(v,v);
}

inline int lca(int x,int y){
    while(top[x]!=top[y]){
        if(dep[top[x]]<dep[top[y]])
            swap(x,y);
        x=fa[top[x]];
    }
    return dep[x]<dep[y]?x:y;
}

inline int getdis(int x,int y){
    return dep[x]+dep[y]-(dep[lca(x,y)]<<1);
}

int dp[N],cent;
bool vis[N];

inline void findcent(int x,int f,int tot){
	siz[x]=1;dp[x]=0;
    for(auto y:G[x])
        if(y!=f and !vis[y]){
            findcent(y,x,tot);
            dp[x]=max(dp[x],siz[y]);
            siz[x]+=siz[y];
        }
    dp[x]=max(dp[x],tot-siz[x]);
	if(dp[x]<dp[cent])
        cent=x;
}

int dfa[N];

inline void divcent(int x,int tot){
	vis[x]=1;
    for(auto y:G[x])
        if(!vis[y]){
            cent=0;
            int sz=(siz[y]<siz[x])?siz[x]:(tot-siz[x]);
		    findcent(y,x,sz);dfa[cent]=x;divcent(cent,sz);
        }
}

struct segtree{
	int rt[N],cnt=0;
    struct node{int l,r,val;}t[N<<5];

    inline void pushup(int p){
        t[p].val=t[t[p].l].val+t[t[p].r].val;
    }

	inline void modify(int &p,int l,int r,int x,int v){
		if(!p)
            p=++cnt;
		if(l==r){
            t[p].val+=v;
            return;
        }
		int mid=(l+r)>>1;
		if(x<=mid)
            modify(t[p].l,l,mid,x,v);
		else
            modify(t[p].r,mid+1,r,x,v);
        pushup(p);
	}

	inline int query(int p,int l,int r,int x,int y){
		if(!p)
            return 0;
		if(x<=l and r<=y)
            return t[p].val;
		int mid=(l+r)>>1;
		if(y<=mid)
            return query(t[p].l,l,mid,x,y);
		else if(x>mid)
            return query(t[p].r,mid+1,r,x,y);
		else
            return query(t[p].l,l,mid,x,mid)+query(t[p].r,mid+1,r,mid+1,y);
	}
}w1,w2;

inline void modify(int x,int v){
	int cur=x;
	while(cur){
		w1.modify(w1.rt[cur],0,n-1,getdis(cur,x),v);
		if(dfa[cur])
            w2.modify(w2.rt[cur],0,n-1,getdis(dfa[cur],x),v);
		cur=dfa[cur];
	}
}

inline int query(int x,int k){
	int cur=x,pre=0,res=0;
	while(cur){
		if(getdis(cur,x)>k){
			pre=cur;cur=dfa[cur];
            continue;
		}
		res+=w1.query(w1.rt[cur],0,n-1,0,k-getdis(cur,x));
		if(pre)
            res-=w2.query(w2.rt[pre],0,n-1,0,k-getdis(cur,x));
		pre=cur;cur=dfa[cur];
	}
    return res;
}

signed main(){
    n=read();q=read();
	for(int i=1;i<=n;i++)
        a[i]=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        G[u].eb(v);G[v].eb(u);
    }
	dfs(1,0);dfs2(1,1);
	dp[0]=inf;cent=0;
    findcent(1,0,n);divcent(cent,n);
	for(int i=1;i<=n;i++)
        modify(i,a[i]);
	int lst=0;
	while(q--){
        int opt=read(),x=read(),y=read();
		x^=lst;y^=lst;
		if(opt==0){
            lst=query(x,y);
            write(lst);puts("");
        }else{
            modify(x,y-a[x]);
            a[x]=y;
        }
	}
	return 0;
}

P3241

P3241 [HNOI2015] 开店

给定一棵树,点有点权,边有边权,点的度数小于等于 3。

多次询问,给定 \(x,l,r\),令 \(a_y\) 为点 \(y\) 的点权,查询 \(\sum\limits_{l\leq a_y\leq r}dis(x,y)\)

建立完点分树后,对每个点 \(u\),开两个 vector 按照 \(x_i\) 排序并取前缀和,分别存储 \(u\) 子树中的点到 \(u\) 的距离以及 \(u\) 子树中的点到 \(fa_u\)​ 的距离。

对于每一次查询,在点分树上暴力跳父亲节点,直接在 vector 上二分查找满足 \([l,r]\)​ 区间的点和距离,直接计算即可。

const int N=2e5+5;
int n,Q,A,x[N],vis[N],rt,ans;
vector<tuple<int,int>>G[N];

struct Father{int u,num,dis;};
vector<Father> fa[150010];
struct Son{
    int age,dis;
    inline bool operator<(const Son& a)const{
        return age<a.age;
    }
};
vector<Son>son[N][3];

inline int getsiz(int u,int f){
    if(vis[u])
        return 0;
    int res=1;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v])
            res+=getsiz(v,u);
    return res;
}

inline int getrt(int u,int f,int sum,int &rt){
    if(vis[u])
        return 0;
    int res=1,mx=0;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            int V=getrt(v,u,sum,rt);
            mx=max(mx,V);
            res+=V;
        }
    mx=max(mx,sum-res);
    if(mx<=sum/2)
        rt=u;
    return res;
}

inline void getdis(int u,int f,int dis,int rt,int k,vector<Son>&p){
    if(vis[u])
        return;
    fa[u].push_back({rt,k,dis,});
    p.push_back({x[u],dis});
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v])
            getdis(v,u,dis+w,rt,k,p);
}

inline void divid(int u){
    if(vis[u])
        return;
    getrt(u,0,getsiz(u,0),u);
    vis[u]=1;
    int k=0;
    for(auto[v,w]:G[u])
        if(!vis[v]){
            auto&p=son[u][k];
            p.push_back({-1,0});
            p.push_back({A+1,0});
            getdis(v,0,w,u,k,p);
            sort(p.begin(),p.end());
            for(int i=1;i<p.size();i++)
                p[i].dis+=p[i-1].dis;
            k++;
        }
    for(auto[v,w]:G[u])
        if(!vis[v])
            divid(v);
}

inline int query(int u,int l,int r){
    int res=0;
    for(auto&t:fa[u]){
        int Age=x[t.u];
		if(Age>=l and Age<=r)
            res+=t.dis;
		for(int i=0;i<3;i++){
			if(i==t.num)
                continue;
			auto& p=son[t.u][i];
			if(p.empty())
                continue;
			int a=lower_bound(p.begin(),p.end(),Son({l,-1}))-p.begin();
			int b=lower_bound(p.begin(),p.end(),Son({r+1,-1}))-p.begin();
			res+=t.dis*(b-a)+p[b-1].dis-p[a-1].dis;
		}
    }
    for(int i=0;i<3;i++){
		auto&p=son[u][i];
		if(p.empty())
            continue;
		int a=lower_bound(p.begin(),p.end(),Son({l,-1}))-p.begin();
		int b=lower_bound(p.begin(),p.end(),Son({r+1,-1}))-p.begin();
		res+=p[b-1].dis-p[a-1].dis;
	}
    return res;
}

signed main(){
    n=read();Q=read(),A=read();
    for(int i=1;i<=n;i++)
        x[i]=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    divid(1);
    while(Q--){
        int u=read(),l=(read()+ans)%A,r=(read()+ans)%A;
        if(l>r)
            swap(l,r);
        ans=query(u,l,r);
        write(ans);puts("");
    }
    return 0;
}

P3345

P3345 [ZJOI2015] 幻想乡战略游戏

给定一棵树,点有点权,边有边权,求带权重心,点带修。

点的度数小于等于 20。

\(s_u\)\(u\) 子树内 \(d\) 的和,\(sd_u\)\(sch_u\),记录子树内 \(d\times dis\) 的和,直接容斥即可。

不难注意到一个性质,令当前答案为 \(u\),若 \(u\) 的某个儿子 \(v\) 更优,则有 \(s_v<s_u-s_v\),这个是只有一个的。

所以可以在点分树上暴力跳父亲求答案,时间复杂度为 \(\mathcal O(20n\log^2 n)\)

const int N=1e5+5;
const int lgN=21;
vector<tuple<int,int>>G[N];

namespace Tree{
    int dep[N],siz[N],son[N],fa[N],top[N],dis[N];
    
    inline void dfs(int u,int f){
        siz[u]=1;fa[u]=f;dep[u]=dep[f]+1;
        for(auto[v,w]:G[u])
            if(v!=f){
                dis[v]=dis[u]+w;
                dfs(v,u);
                siz[u]+=siz[v];
                if(siz[son[u]]>siz[v])
                    son[u]=v;
            }
    }
    
    inline void dfs2(int u,int topf){
        top[u]=topf;
        if(son[u])
            dfs2(son[u],topf);
        for(auto[v,w]:G[u])
            if(v!=fa[u] and v!=son[u])
                dfs2(v,v);
    }

    inline int lca(int x,int y){
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]])
                swap(x,y);
            x=fa[top[x]];
        }
        return dep[x]<dep[y]?x:y;
    }
    
    inline int getdis(int x,int y){
        return dis[x]+dis[y]-(dis[lca(x,y)]<<1);
    }
};

int n,Q,rt,sum,dp[N],siz[N],fa[N],dis[N][lgN],vis[N],top[N],dep[N];

inline void getroot(int u,int f){
    siz[u]=1;dp[u]=0;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v]){
            getroot(v,u);
            siz[u]+=siz[v];
            dp[u]=max(dp[u],siz[v]);
        }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[rt])
        rt=u;
}

inline void dfs(int u,int f,int topf){
    top[u]=topf;
    for(auto[v,w]:G[u])
        if(v!=f and !vis[v])
            dfs(v,u,topf);
}

inline void divid(int u){
    vis[u]=1;
    for(auto[v,w]:G[u])
        if(!vis[v]){
            dp[rt=0]=inf;
            sum=siz[v];
            getroot(v,0);
            dfs(v,0,rt);
            fa[rt]=u;dep[rt]=dep[u]+1;
            divid(rt);
        }
}

int s[N],sd[N],sch[N];

inline int calc(int u){
    int res=sch[u];
    for(int p=u;fa[p];p=fa[p]){
        int d=dis[u][dep[u]-dep[fa[p]]];
        res+=d*(s[fa[p]]-s[p])+(sch[fa[p]]-sd[p]);
    }
	return res;
}

inline int query(int u){
    int tmp=calc(u);
    for(auto[v,w]:G[u])
        if(calc(v)<tmp)
            return query(top[v]);
    return tmp;
}

signed main(){
    sum=n=read();Q=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),w=read();
        G[u].eb(mt(v,w));G[v].eb(mt(u,w));
    }
    dp[rt=0]=inf;
    getroot(1,0);int RT=rt;divid(rt);
    Tree::dfs(1,0);Tree::dfs2(1,1);
    for(int i=1;i<=n;i++)
		for(int j=fa[i];j;j=fa[j])
            dis[i][dep[i]-dep[j]]=Tree::getdis(i,j);
    while(Q--){
        int x=read(),y=read();
        s[x]+=y;
        for(int p=x;fa[p];p=fa[p]){
            int d=dis[x][dep[x]-dep[fa[p]]];
            s[fa[p]]+=y;sch[fa[p]]+=y*d;sd[p]+=y*d;
        }
        write(query(RT));puts("");
    }
    return 0;
}
posted @ 2024-09-29 21:24  QcpyWcpyQ  阅读(10)  评论(0编辑  收藏  举报