潜龙未见静水流,沉默深藏待时秋。一朝破空声势振,惊世骇俗展雄猷。

点分树学习笔记

一、点分树概述

参考资料:https://www.cnblogs.com/Xing-Ling/p/12976848.html

上文提到,点分治可以处理大规模树上路径问题。但是如果权值带修,点分治就无能为力了。

点分树,又称动态点分治,一般用于解决带修的树上路径相关问题

温馨提示:

  • 权值可以带修,但树的结构不可以,否则就要用到 \(\texttt{LCT}\) 了。
  • 一定要分清原树和点分树的区别。

二、点分树建树

点分树的思想很简单,把相邻两层的重心连边,连出来的这棵树就叫做点分树。

比如下面这张图,展示了通过原树是如何构建点分树的:

image

注:第一层边为红边,第二层边为橙边,第三层边为绿边。

代码实现只需要在点分治的 solve 函数中补上连边即可。

void solve(int u)
{
    vis[u]=true;
    for(auto v:g[u])
    {
        if(vis[v]) continue;
        all=sz[v],getroot(v,rt=0),p[rt]=u,solve(rt);
        ///p[x]为点分树上x的父节点
    }
}

注意点分树的根不一定为 \(1\) 号点。


点分树的优美性质:

  • 点分树树高为 \(\mathcal O(\log n)\)

    因此如果需要带修,在点分树上暴力跳 \(fa\) 即可,单次修改的时间复杂度为 \(\mathcal O(\log n)\)

  • 点分树上每个点的 deg (不算连向父节点的边) \(\le\) 其在原树上的 deg

    原因很简单, \(u\) 的每个邻点 \(v\) 至多给 \(u\) 在点分树上贡献 \(1\) 个出度。

  • 点分树上 \(\sum sz=\mathcal O(n\log n)\)

  • 点分树上 \(u\) 的子树即为点分治时以 \(u\) 为分治中心的连通块。

  • 记点分树上的 \(\texttt{lca}(u,v)=p\),则 \(p\) 一定在原树 \(u\to v\) 的路径上。

    常用推论: \(dis_{u,v}=dis_{u,p}+dis_{p,v}\)

    注意 \(p\) 不一定是原树上 \(u,v\) 两点的 \(\texttt{lca}\) ,但 \(dis_{u,v}\) 表示的是原树上 \(u,v\) 两点的距离。

    如果要求 \(dis_{u,v}\) ,仍然需要在原树上倍增或者树剖,这个不是点分树能解决的事情。

    博主强烈推荐用树剖,由于树剖很难卡满 \(\log n\) 条重链并且常数很小,实际表现比倍增快大约 \(\frac 13\) ,并且和 \(\mathcal O(1)\) 查询的欧拉序 \(+\ \texttt{ST}\) 表做法差不多。

    后面的例题在分析时间复杂度时,默认将求 \(\texttt{lca}\) 的代价视为 \(\mathcal O(1)\)

三、点分树相关例题

例1、\(\texttt{P6329 【模板】点分树 | 震波}\)

题目描述

给定一棵 \(n\) 个点的树,点有点权 \(w_i\)

接下来 \(m\) 次操作:

  • 0 x k :求到 \(x\) 距离 \(\le k\) 的所有点的点权之和。
  • 1 x y :将 \(w_x\) 改为 \(y\)

数据范围

  • \(1\le n,m\le 10^5\)
  • \(1\le w_i,y\le 10^4\) ,强制在线。

时间限制 \(\texttt{2s}\) ,空间限制 \(\texttt{250MB}\)

分析

先把点分树建出来。

对每个点 \(u\) 开一棵动态开点线段树,保存在点分树上子树中所有点的信息。具体的,对于子树中的每个点 \(x\) ,将 \(w_x\) 的贡献存放在下标为 \(dis_{x,u}\) 的叶子上。

对于修改操作,枚举 \(x\) 在点分树上的祖先 \(u\) ,在线段树上单点修改即可。

对于询问操作,枚举 \(x\) 在点分树上的祖先 \(u\) ,由 \(dis_{x,y}=dis_{x,u}+dis_{u,y}\le k\) ,知 \(dis_{u,y}\le k-dis_{x,u}\) ,线段树求前缀和即可。

本题数据结构需要支持单点修改,求前缀和。

选择写线段树的原因是从未学过动态开点树状数组。

\(\texttt{upd}\) :后来发现树状数组是可以做的,用 vector 开数组,以 \(u\) 为根时开到 \(sz_u\) 即可。

但还有一个问题, \(y\)\(u\) 子树中并不代表 \(\texttt{lca}(x,y)=u\)

从容斥的角度看,还应该减掉 \(y\)\(u\) 的和 \(x\) 同一方向上的子树中的贡献。

或者换个角度理解,枚举 \(u\)减掉 \(u\) 子树中 \(y\)\(p_u\) 的贡献

对每个点 \(u\) 再开一棵动态开点线段树,将\(w_x\)保存在线段树的第 \(dis_{x,p_u}\) 个叶子上即可。

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

来说一下动态开点线段树空间的问题。

乍一看 \(\mathcal O(n\log n)\) 次插入操作,需要 \(\mathcal O(n\log^2n)\) 的空间。

但是取 \(\log n\approx 20\) ,实测开 \(2n\log n\) 个节点就足够了。

下面分析一下原因,对于第 \(x\) 棵线段树,插入的下标均 \(\le sz_x\)

因此整棵线段树的结构可以看成上面一条链,下面一个完整的,点数为 \(sz_x\) 的线段树,总节点数 \(\le 4\cdot sz_x+\log n\)

由于 \(\sum sz_x\le n\log n\) ,因此总节点数 \(\le 5n\log n\)

不过实际情况下很难卡满(而且上面的估计略显粗略),本题开到 \(2n\log n\) 的空间就足够了。

如果你不想动脑子来算空间复杂度,也可以直接把数组开大一点,比较省事

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int m,n,u,v,op,rt,all,res;
int d[maxn],fa[maxn],son[maxn],top[maxn];
int p[maxn],mx[maxn],sz[maxn];///注意sz在树剖和点分治找重心时都用到了,但含义不同
bool vis[maxn];
vector<int> g[maxn];
int w[maxn];
struct sgmt
{
    int tot,rt[maxn];
    struct node
    {
        int ls,rs,sum;
    }f[40*maxn];
    void pushup(int p)
    {
        f[p].sum=f[f[p].ls].sum+f[f[p].rs].sum;
    }
    void modify(int &p,int l,int r,int pos,int val)
    {
        if(!p) p=++tot;
        if(l==r) return f[p].sum+=val,void();
        int mid=(l+r)/2;
        if(pos<=mid) modify(f[p].ls,l,mid,pos,val);
        else modify(f[p].rs,mid+1,r,pos,val);
        pushup(p);
    }
    int query(int p,int l,int r,int L,int R)
    {
        if(L<=l&&r<=R) return f[p].sum;
        if(!p||l>R||r<L) return 0;
        int mid=(l+r)/2;
        return query(f[p].ls,l,mid,L,R)+query(f[p].rs,mid+1,r,L,R);
    }
}t1,t2;
void dfs1(int u,int father)
{
    sz[u]=1;
    for(auto v:g[u])
    {
        if(v==father) continue;
        d[v]=d[u]+1,fa[v]=u;
        dfs1(v,u),sz[u]+=sz[v];
        if(sz[v]>=sz[son[u]]) son[u]=v;
    }
}
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]||v==son[u]) continue;
        dfs2(v,v);
    }
}
int lca(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(d[top[u]]<d[top[v]]) swap(u,v);
        u=fa[top[u]];
    }
    return d[u]<d[v]?u:v;
}
int getdis(int u,int v)
{
    return d[u]+d[v]-2*d[lca(u,v)];
}
void getroot(int u,int fa)
{
    sz[u]=1,mx[u]=0;
    for(auto v:g[u])
    {
        if(vis[v]||v==fa) continue;
        getroot(v,u);
        sz[u]+=sz[v],mx[u]=max(mx[u],sz[v]);
    }
    mx[u]=max(mx[u],all-sz[u]);
    if(mx[u]<mx[rt]) rt=u;
}
void solve(int u)
{
    vis[u]=true;
    for(auto v:g[u])
    {
        if(vis[v]) continue;
        all=sz[v],getroot(v,rt=0),p[rt]=u,solve(rt);
    }
}
void modify(int x,int y)
{
    for(int cur=x;cur;cur=p[cur])
    {
        t1.modify(t1.rt[cur],0,n,getdis(x,cur),y);
        if(p[cur]) t2.modify(t2.rt[cur],0,n,getdis(x,p[cur]),y);
    }
}
int query(int x,int k)
{
    int res=0;
    for(int cur=x;cur;cur=p[cur])
    {
        res+=t1.query(t1.rt[cur],0,n,0,k-getdis(x,cur));
        if(p[cur]) res-=t2.query(t2.rt[cur],0,n,0,k-getdis(x,p[cur]));
    }
    return res;
}
int main()
{
    scanf("%d%d",&n,&m),mx[0]=1e9;
    for(int i=1;i<=n;i++) scanf("%d",&w[i]);
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d%d",&u,&v);
        g[u].push_back(v),g[v].push_back(u);
    }
    d[1]=1,dfs1(1,0),dfs2(1,1);
    all=n,getroot(1,0),solve(rt);
    for(int i=1;i<=n;i++) modify(i,w[i]);
    while(m--)
    {
        scanf("%d%d%d",&op,&u,&v),u^=res,v^=res;
        if(!op) printf("%d\n",res=query(u,v));
        else modify(u,v-w[u]),w[u]=v;
    }
    return 0;
}

例2、\(\texttt{P2056 [ZJOI2007] 捉迷藏}\)

题目描述

给定一棵 \(n\) 个点的树,每个点有黑白两种颜色,初始全为黑色。

接下来 \(q\) 次操作:

  • C i :改变第 \(i\) 个点的颜色。
  • G :查询最远的两个黑点的距离。如果没有黑点,输出 -1 ;如果只有一个黑点,输出 0

数据范围

  • \(1\le n\le 10^5,1\le q\le 5\cdot 10^5\)

时间限制 \(\texttt{5s}\) ,空间限制 \(\texttt{250MB}\)

分析

还是先建出点分树。我们希望对每个点 \(u\) ,维护\(u\) 为(点分树上) \(\texttt{lca}\) 的最远黑色点对的距离

用一个数据结构(记为 A ),维护 \(u\) 子树内的所有黑点到 \(p_u\) 的距离。

再用一个数据结构(记为 B ),维护对 \(u\) 的每个子节点 \(v\)A[v] 中的最大值。注意如果 \(u\) 是黑点,还要在 B[u]push 一个 0 ,表示 \(u\) 对自己的贡献。

至此,我们已经可以维护以 \(u\)\(\texttt{lca}\) 的最远黑色点对距离了,即 B[u] 中最大值和次大值之和。

最后还需要一个数据结构(记为 C ),维护对每个 \(u\)B[u] 中最大值和次大值之和。

修改比较简单,顺着链往上跳即可。

那么 A,B,C 应该选用什么数据结构呢?

我们需要支持插入、删除、求最大值和次大值的操作,可以用平衡树解决。

但是为了减小常数,最好的选择是可删堆

可删堆的原理: q1 维护已经插入堆中的元素, q2 维护懒惰删除的元素,如果 q1q2 的堆顶相同,将这个元素同时从 q1q2 中删除。

时间复杂度 \(\mathcal O((n+q)\log^2n)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5,inf=1e9;
int n,q,u,v,rt,all,sum;
int d[maxn],fa[maxn],son[maxn],top[maxn];
int p[maxn],mx[maxn],sz[maxn];
bool vis[maxn];
vector<int> g[maxn];
int w[maxn];
char ch[2];
struct pq
{
    priority_queue<int> q1,q2;
    void clean()
    {
        while(q1.size()&&q2.size()&&q1.top()==q2.top()) q1.pop(),q2.pop();
    }
    int size()
    {
        return q1.size()-q2.size();
    }
    void push(int x)
    {
        q1.push(x),clean();
    }
    void del(int x)
    {
        q2.push(x),clean();
    }
    int top()
    {
        return q1.size()?q1.top():-inf;
    }
    int query()
    {
        static int x,y;
        x=top(),del(x),y=top(),push(x);
        return x+y;
    }
}a[maxn],b[maxn],c;
void dfs1(int u,int father)
{
    sz[u]=1;
    for(auto v:g[u])
    {
        if(v==father) continue;
        d[v]=d[u]+1,fa[v]=u;
        dfs1(v,u),sz[u]+=sz[v];
        if(sz[v]>=sz[son[u]]) son[u]=v;
    }
}
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]||v==son[u]) continue;
        dfs2(v,v);
    }
}
int lca(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(d[top[u]]<d[top[v]]) swap(u,v);
        u=fa[top[u]];
    }
    return d[u]<d[v]?u:v;
}
int getdis(int u,int v)
{
    return d[u]+d[v]-2*d[lca(u,v)];
}
void getroot(int u,int fa)
{
    sz[u]=1,mx[u]=0;
    for(auto v:g[u])
    {
        if(vis[v]||v==fa) continue;
        getroot(v,u);
        sz[u]+=sz[v],mx[u]=max(mx[u],sz[v]);
    }
    mx[u]=max(mx[u],all-sz[u]);
    if(mx[u]<mx[rt]) rt=u;
}
void solve(int u)
{
    vis[u]=true;
    for(auto v:g[u])
    {
        if(vis[v]) continue;
        all=sz[v],getroot(v,rt=0),p[rt]=u,solve(rt);
    }
}
void add(int x)
{
    c.del(b[x].query());
    b[x].push(0);
    c.push(b[x].query());
    for(int i=x;p[i];i=p[i])
    {
        c.del(b[p[i]].query());
        b[p[i]].del(a[i].top());
        a[i].push(getdis(x,p[i]));
        b[p[i]].push(a[i].top());
        c.push(b[p[i]].query());
    }
}
void del(int x)
{
    c.del(b[x].query());
    b[x].del(0);
    c.push(b[x].query());
    for(int i=x;p[i];i=p[i])
    {
        c.del(b[p[i]].query());
        b[p[i]].del(a[i].top());
        a[i].del(getdis(x,p[i]));
        b[p[i]].push(a[i].top());
        c.push(b[p[i]].query());
    }
}
int main()
{
    scanf("%d",&n),mx[0]=inf;
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d%d",&u,&v);
        g[u].push_back(v),g[v].push_back(u);
    }
    d[1]=1,dfs1(1,0),dfs2(1,1);
    all=n,getroot(1,0),solve(rt);
    for(int x=1;x<=n;x++)
    {
        w[x]=1,b[x].push(0);
        for(int i=x;i;i=p[i]) a[i].push(getdis(x,p[i]));
    }
    for(int i=1;i<=n;i++) b[p[i]].push(a[i].top());
    for(int i=1;i<=n;i++) c.push(b[i].query());
    scanf("%d",&q),sum=n;
    while(q--)
    {
        scanf("%s",ch);
        if(ch[0]=='C')
        {
            scanf("%d",&u),sum+=w[u]?-1:1,w[u]^=1;
            w[u]?add(u):del(u);
        }
        else printf("%d\n",sum>=2?c.top():sum-1);
    }
    return 0;
}

例3、\(\texttt{P3345 [ZJOI2015]幻想乡战略游戏}\)

题目描述

给定一棵 \(n\) 个节点的树,点有点权 \(d_u\) (初始全为零),边有边权 \(w_i\)

接下来 \(q\) 次操作,每次操作 u e 表示给 \(d_u\) 加上 \(e\) ,保证任意时刻 \(d_u\) 非负。

在每次操作结束后,求 \(\min\limits_{1\le u\le n}\sum_{v=1}^nd_v\cdot dis(u,v)\)

保证每个点的度数 \(\le 20\)

数据范围

  • \(1\le n,q\le 10^5\)
  • \(0\le w_i,|e|\le10^3\)

时间限制 \(\texttt{6s}\) ,空间限制 \(\texttt{250MB}\)

分析

点分治重心移动的套路又出现了!推荐一篇比较清晰的题解

本题相当于动态维护带权重心。

先考虑根节点从 \(u\) 移动到邻点 \(v\) 时,带权距离和的变化。

\(u\) 为根,记 \(s_v\)\(v\) 子树的点权和,则带权距离和的变化量为:

\[\Delta=dis(u,v)\cdot\big((s_u-s_v)-s_v\big)\\ =dis(u,v)\cdot(s_u-2\cdot s_v) \]

\(2\cdot s_v>s_u\) 时,带权距离和会变小,并且这样的 \(v\) 至多只有一个

如果这样的 \(v\) 不存在,则说明 \(u\) 就是重心。

如果树退化成一条链,则移动次数可以卡满 \(\mathcal O(n)\)

但如果每次跳到点分治时 \(v\) 所在连通块的重心,则移动次数为 \(\mathcal O(\log n)\)

划重点:对于点分树上的一条边 \(u\to v\) ,如果 \((u,w)\)原树上的边,满足 \(w\) 在点分树上 \(v\) 子树中。那么判断是否移动用的是 \(u\)\(w\) 比较,如果需要移动,则移动到 \(v\)

如果每次只移动一步,则带权路径和逐渐变小,满足单调性。

但对于 \(u\to v\) 这种移动,相当于在原树上移动了很多条边,不满足单调性。换句话说,以 \(v\) 为根的带权路径和不一定\(u\) 更优。

通过 \(2\cdot s_v\gt s_u\) 进行判断并不方便(需要树状数组维护子树点权和),直接求出以 \(u\)\(v\) 为根时的答案然后进行比较即可。

最后一个问题,固定根节点 \(u\) ,求带权距离和。

其实这一部分仅仅是个点分树板子,是复杂度瓶颈但不是难点。

枚举点分树上 \(u\) 的祖先 \(x\) ,希望统计满足 \(\texttt{lca}(u,v)=x\) 的所有 \(v\) 的贡献。

注意到 \(d_v\cdot dis_{u,v}=d_v\cdot(dis_{u,cur}+dis_{cur,v})\) ,因此我们需要对每个点 \(x\) ,维护点分树上子树的 \(\sum d_v\)\(\sum d_v\cdot dis_{x,v}\)

由于需要容斥和 \(u\)\(x\) 的同一子树的贡献,额外维护 \(\sum d_v\cdot dis_{p_{x},v}\) 即可。

时间复杂度 \(\mathcal O(qd\log n+q\log^2n)\) ,其中 \(d=20\) 为最大度数。

#include<bits/stdc++.h>
#define ll long long
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=1e5+5;
int n,q,u,v,w,x,rt,all;
int c[maxn],d[maxn],fa[maxn],sz[maxn],son[maxn],top[maxn];
int p[maxn],mx[maxn];
bool vis[maxn];
vector<pii> g[maxn],h[maxn];
ll s1[maxn],s2[maxn],s3[maxn];
void dfs1(int u,int f)
{
    sz[u]=1;
    for(auto [v,w]:g[u])
    {
        if(v==f) continue;
        c[v]=c[u]+w,d[v]=d[u]+1,fa[v]=u,dfs1(v,u),sz[u]+=sz[v];
        if(sz[v]>=sz[son[u]]) son[u]=v;
    }
}
void dfs2(int u,int f)
{
    top[u]=f;
    if(son[u]) dfs2(son[u],f);
    for(auto [v,w]:g[u])
    {
        if(v==fa[u]||v==son[u]) continue;
        dfs2(v,v);
    }
}
int lca(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(d[top[u]]<d[top[v]]) swap(u,v);
        u=fa[top[u]];
    }
    return d[u]<d[v]?u:v;
}
int getdis(int u,int v)
{
    return c[u]+c[v]-2*c[lca(u,v)];
}
void getroot(int u,int fa)
{
    sz[u]=1,mx[u]=0;
    for(auto [v,w]:g[u])
    {
        if(vis[v]||v==fa) continue;
        getroot(v,u),sz[u]+=sz[v],mx[u]=max(mx[u],sz[v]);
    }
    mx[u]=max(mx[u],all-sz[u]);
    if(!rt||mx[u]<mx[rt]) rt=u;
}
void solve(int u)
{
    vis[u]=1;
    for(auto [v,w]:g[u])
    {
        if(vis[v]) continue;
        all=sz[v],getroot(v,rt=0),p[rt]=u,h[u].push_back(mp(v,rt)),solve(rt);
    }
}
void modify(int u,int e)
{
    for(int i=u;i;i=p[i])
    {
        s1[i]+=e,s2[i]+=1ll*getdis(u,i)*e;
        if(p[i]) s3[i]+=1ll*getdis(u,p[i])*e;
    }
}
ll calc(int u)
{
    ll res=0;
    for(int i=u;i;i=p[i])
    {
        res+=s1[i]*getdis(u,i)+s2[i];
        if(p[i]) res-=s1[i]*getdis(u,p[i])+s3[i];
    }
    return res;
}
ll query(int u)
{
    ll res=calc(u);
    for(auto [v,w]:h[u]) if(calc(v)<res) return query(w);
    return res;
}
int main()
{
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        g[u].push_back(mp(v,w)),g[v].push_back(mp(u,w));
    }
    d[1]=1,dfs1(1,0),dfs2(1,1);
    all=n,getroot(1,0),solve(x=rt);
    while(q--) scanf("%d%d",&u,&w),modify(u,w),printf("%lld\n",query(x));
    return 0;
}

例4、\(\texttt{P3676 小清新数据结构题}\)

题目描述

给定一棵 \(n\) 个点的树,点有点权 \(w_i\)

接下来 \(q\) 次操作:

  • 1 x y :将第 \(x\) 个点的点权改为 \(y\)
  • 2 x :查询以 \(x\) 为根时,每棵子树点权和的平方之和。

数据范围

  • \(1\le n,q\le 2\cdot 10^5\)
  • \(1\le x\le n,0\le |w_i|,|y|\le 10\)

时间限制 \(\texttt{2s}\) ,空间限制 \(\texttt{256MB}\)

分析

本题最简洁的做法不是点分树,而是树剖树状数组。

推式子,先任意指定根节点 \(p\)

\(sum=\sum_{i=1}^nw_i,s_i=\sum\limits_{j\in subtree(i)}w_j\) ,目标计算 \(\sum_{i=1}^ns_i^2\)

平方和不好维护,先计算 \(\sum_{i=1}^ns_i\)

这个直接拆成每个点的贡献来算:

\[\sum_{i=1}^ns_i=\sum_{i=1}^nw_i\cdot (dis_{i,p}+1)=\sum_{i=1}^nw_i\cdot dis_{i,p}+sum \]

其中 \(\sum_{i=1}^nw_i\cdot dis_{i,p}\) 用上一道题的方法可以快速维护。

接下来需要一个常用二级结论:\(S=\sum_{i=1}^ns_i\cdot(sum-s_i)\)与 \(p\) 无关!

原因也很简单,每条边的贡献是两端连通块点权乘积之和的两倍。

但是这个结论可以用来降次:

\[\sum_{i=1}^ns_i^2=sum\cdot\sum_{i=1}^n s_i-\sum_{i=1}^n s_i\cdot(sum-s_i) \]

最后一个问题是如何快速维护 \(S\)

拆成边的贡献依然不好算,我们把贡献拆到所有点对上:

\[S=\sum_{i=1}^n\sum_{j=1}^nw_i\cdot w_j\cdot dis_{i,j} \]

所以对于修改操作,\(S\)的增量为:

\[\Delta=y\cdot\sum_{j=1}^n w_j\cdot dis_{x,j} \]

直接交给点分树来计算即可。

时间复杂度 \(\mathcal O((n+q)\log n)\)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e5+5;
int n,q,u,v,op,rt,all;
ll cur,sum;
int d[maxn],fa[maxn],son[maxn],top[maxn];
int p[maxn],mx[maxn],sz[maxn];
bool vis[maxn];
vector<int> g[maxn];
int w[maxn];
ll s[maxn],s1[maxn],s2[maxn],s3[maxn];
void dfs1(int u,int father)
{
    sz[u]=1,s[u]=w[u];
    for(auto v:g[u])
    {
        if(v==father) continue;
        d[v]=d[u]+1,fa[v]=u;
        dfs1(v,u),sz[u]+=sz[v],s[u]+=s[v];
        if(sz[v]>=sz[son[u]]) son[u]=v;
    }
}
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]||v==son[u]) continue;
        dfs2(v,v);
    }
}
int lca(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(d[top[u]]<d[top[v]]) swap(u,v);
        u=fa[top[u]];
    }
    return d[u]<d[v]?u:v;
}
int getdis(int u,int v)
{
    return d[u]+d[v]-2*d[lca(u,v)];
}
void getroot(int u,int fa)
{
    sz[u]=1,mx[u]=0;
    for(auto v:g[u])
    {
        if(vis[v]||v==fa) continue;
        getroot(v,u);
        sz[u]+=sz[v],mx[u]=max(mx[u],sz[v]);
    }
    mx[u]=max(mx[u],all-sz[u]);
    if(mx[u]<mx[rt]) rt=u;
}
void solve(int u)
{
    vis[u]=true;
    for(auto v:g[u])
    {
        if(vis[v]) continue;
        all=sz[v],getroot(v,rt=0),p[rt]=u,solve(rt);
    }
}
ll calc(int x)
{
    ll res=0;
    for(int i=x;i;i=p[i])
    {
        res+=s1[i]*getdis(x,i)+s2[i];
        if(p[i]) res-=s1[i]*getdis(x,p[i])+s3[i];
    }
    return res;
}
void modify(int x,int y)
{
    sum+=y,cur+=y*calc(x);
    for(int i=x;i;i=p[i])
    {
        s1[i]+=y,s2[i]+=y*getdis(x,i);
        if(p[i]) s3[i]+=y*getdis(x,p[i]);
    }
}
int main()
{
    scanf("%d%d",&n,&q),mx[0]=1e9;
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d%d",&u,&v);
        g[u].push_back(v),g[v].push_back(u);
    }
    for(int i=1;i<=n;i++) scanf("%d",&w[i]);
    d[1]=1,dfs1(1,0),dfs2(1,1);
    all=n,getroot(1,0),solve(rt);
    sum=s[1];
    for(int x=1;x<=n;x++)
    {
        cur+=s[x]*(sum-s[x]);
        for(int i=x;i;i=p[i])
        {
            s1[i]+=w[x],s2[i]+=w[x]*getdis(x,i);
            if(p[i]) s3[i]+=w[x]*getdis(x,p[i]);
        }
    }
    while(q--)
    {
        scanf("%d",&op);
        if(op==1) scanf("%d%d",&u,&v),modify(u,v-w[u]),w[u]=v;
        else scanf("%d",&u),printf("%lld\n",sum*(calc(u)+sum)-cur);
    }
    return 0;
}

例5、\(\texttt{P3241 [HNOI2015]开店}\)

题目描述

给定一棵 \(n\) 个点的树,点有点权 \(x_i\) ,边有边权 \(w_i\)

\(q\) 次询问,每次给定 \(u,l,r\) ,求 \(\sum\limits_{l\le x_i\le r}dis_{u,i}\) ,强制在线。

数据范围

  • \(1\le n\le 1.5\cdot 10^5,q\le 2\cdot 10^5\)
  • \(0\le x_i\lt 10^9,1\le w_i\le10^3\)

时间限制 \(\texttt{6s}\) ,空间限制 \(\texttt{500MB}\)

分析

根据模板题的套路,枚举 \(u\) 在点分树上的祖先 \(x\) ,统计 \(\sum\limits_{l\le x_v\le r,lca(u,v)=x}dis_{u,v}\)

\(dis_{u,v}\) 拆成 \(dis_{u,x}+dis_{x,v}\),对每个 \(x\)vector 存储二元组 \((y,dis_{x,y})\)\((x,dis_{p_x,y})\) ,查询时直接用 \(l\)\(r\) 二分即可。

时间复杂度 \(\mathcal O((n+q)\log^2n)\)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1.5e5+5,maxm=3e5+5;
int l,n,q,r,u,v,w,rt,all,lim,tot=1;
ll res;
int head[maxn],to[maxm],val[maxm],nxt[maxm];
int x[maxn];
int d[maxn],fa[maxn],dis[maxn],son[maxn],top[maxn];
int p[maxn],mx[maxn],sz[maxn];
bool vis[maxn];
struct node
{
    int x;
    ll dis[2];
};
bool operator<(const node &a,const node &b)
{
    return a.x<b.x;
}
struct vec
{
    vector<node> h;
    void init()
    {
        sort(h.begin()+1,h.end());
        for(int i=1;i<h.size();i++)
            for(int j=0;j<=1;j++)
                h[i].dis[j]+=h[i-1].dis[j];
    }
    ll query(int op,int l,int r)
    {
        l=lower_bound(h.begin()+1,h.end(),(node){l,0,0})-h.begin();
        r=upper_bound(h.begin()+1,h.end(),(node){r,0,0})-h.begin()-1;
        if(op==-1) return r-l+1;
        else return h[r].dis[op]-h[l-1].dis[op];
    }
}t[maxn];
void addedge(int u,int v,int w)
{
    nxt[++tot]=head[u],to[tot]=v,val[tot]=w,head[u]=tot;
}
void dfs1(int u,int father)
{
    sz[u]=1;
    for(int i=head[u];i;i=nxt[i])
    {
        int v=to[i],w=val[i];
        if(v==father) continue;
        d[v]=d[u]+1,dis[v]=dis[u]+w,fa[v]=u;
        dfs1(v,u),sz[u]+=sz[v];
        if(sz[v]>=sz[son[u]]) son[u]=v;
    }
}
void dfs2(int u,int topf)
{
    top[u]=topf;
    if(son[u]) dfs2(son[u],topf);
    for(int i=head[u];i;i=nxt[i])
    {
        int v=to[i];
        if(v==fa[u]||v==son[u]) continue;
        dfs2(v,v);
    }
}
int lca(int u,int v)
{
    while(top[u]!=top[v])
    {
        if(d[top[u]]<d[top[v]]) swap(u,v);
        u=fa[top[u]];
    }
    return d[u]<d[v]?u:v;
}
int getdis(int u,int v)
{
    return dis[u]+dis[v]-2*dis[lca(u,v)];
}
void getroot(int u,int fa)
{
    sz[u]=1,mx[u]=0;
    for(int i=head[u];i;i=nxt[i])
    {
        int v=to[i];
        if(vis[v]||v==fa) continue;
        getroot(v,u);
        sz[u]+=sz[v],mx[u]=max(mx[u],sz[v]);
    }
    mx[u]=max(mx[u],all-sz[u]);
    if(mx[u]<mx[rt]) rt=u;
}
void solve(int u)
{
    vis[u]=true;
    for(int i=head[u];i;i=nxt[i])
    {
        int v=to[i];
        if(vis[v]) continue;
        all=sz[v],getroot(v,rt=0),p[rt]=u,solve(rt);
    }
}
int main()
{
    scanf("%d%d%d",&n,&q,&lim),mx[0]=1e9;
    for(int i=1;i<=n;i++) scanf("%d",&x[i]);
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        addedge(u,v,w),addedge(v,u,w);
    }
    d[1]=1,dfs1(1,0),dfs2(1,1);
    all=n,getroot(1,0),solve(rt);
    for(int i=1;i<=n;i++) t[i].h.push_back({0,0,0});
    for(int u=1;u<=n;u++)
        for(int i=u;i;i=p[i])
            t[i].h.push_back({x[u],getdis(u,i),getdis(u,p[i])});
    for(int i=1;i<=n;i++) t[i].init();
    while(q--)
    {
        scanf("%d%d%d",&u,&l,&r),l=(l+res)%lim,r=(r+res)%lim,res=0;
        if(l>r) swap(l,r);
        for(int i=u;i;i=p[i])
        {
            ll cnt=t[i].query(-1,l,r);
            res+=cnt*getdis(u,i)+t[i].query(0,l,r);
            if(p[i]) res-=cnt*getdis(u,p[i])+t[i].query(1,l,r);
        }
        printf("%lld\n",res);
    }
    return 0;
}

例6、\(\texttt{P5311 [Ynoi2011] 成都七中}\)

题目描述

给定一棵 \(n\) 个节点的树,点有颜色 \(c_i\)

\(m\) 次查询保留编号在 \([l,r]\) 内的所有节点, \(x\) 所在连通块的颜色种类数。

查询操作相互独立。

数据范围

  • \(1\le n,m,c_i\le 10^5\)
  • \(1\le l\le x\le r\le n\)

时间限制 \(\texttt{1s}\) ,空间限制 \(\texttt{250MB}\)

分析

考虑 \(x\) 所在的连通块在点分治过程中的变化。

原本这个连通块是完整的,直到某个分治中心刚好落在连通块中,然后这个连通块就 "散架" 了。

我们要做的第一件事情就是找到这个分治中心 \(u\) ,枚举 \(x\) 在点分树上的祖先 \(u\) ,最浅的满足 \(u\to x\) 路径上所有点都在 \([l,r]\) 内的 \(u\) 即为所求。

再来考虑怎么计算贡献。

由于连通块中任意两点连通,所以 \(x\) 不再重要,可以将询问挂在 \(u\) 上。

\(u\) 子树中的每个点 \(y\) ,用二元组 \((L,R)\) 表示,其中 \(L,R\) 分别为 \(u\to y\) 路径上的最小、最大节点编号。

我们要统计 \(l\le L,R\le r\) (即 \((l,r)\) 右下角)不同颜色数量。

对横坐标从右往左扫描线,记录每种颜色出现的最小纵坐标,并将相应位置设成 \(1\) ,询问用树状数组求前缀和即可。

时间复杂度 \(\mathcal O((n+m)\log^2n)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5,lim=1e5,inf=1e9;
int l,m,n,r,u,v,rt,all;
int mx[maxn],sz[maxn];
int c[maxn],col[maxn],pos[maxn],res[maxn];
bool vis[maxn];
vector<int> g[maxn];
struct node
{
    int l,r,id;
};
vector<node> h[maxn];
struct oper
{
    int l,r,x,op;
    ///op=0,在(l,r)位置插入颜色为x的点
    ///op=1,询问(l,r)右下角颜色数,编号为x
};
vector<oper> vec[maxn];
bool cmp(oper a,oper b)
{
    if(a.l!=b.l) return a.l>b.l;
    return a.op<b.op;
}
void getroot(int u,int fa)
{
    sz[u]=1,mx[u]=0;
    for(auto v:g[u])
    {
        if(vis[v]||v==fa) continue;
        getroot(v,u);
        sz[u]+=sz[v],mx[u]=max(mx[u],sz[v]);
    }
    mx[u]=max(mx[u],all-sz[u]);
    if(mx[u]<mx[rt]) rt=u;
}
void dfs(int u,int fa,int l,int r,int rt)
{
    h[u].push_back({l,r,rt});
    vec[rt].push_back({l,r,col[u],0});
    for(auto v:g[u])
    {
        if(vis[v]||v==fa) continue;
        dfs(v,u,min(l,v),max(r,v),rt);
    }
}
void solve(int u)
{
    vis[u]=true,dfs(u,0,u,u,u);
    for(auto v:g[u])
    {
        if(vis[v]) continue;
        all=sz[v],getroot(v,rt=0),solve(rt);
    }
}
void add(int x,int v)
{
    while(x<=lim) c[x]+=v,x+=x&(-x);
}
int sum(int x)
{
    int res=0;
    while(x) res+=c[x],x-=x&(-x);
    return res;
}
int main()
{
    scanf("%d%d",&n,&m),mx[0]=inf;
    for(int i=1;i<=n;i++) scanf("%d",&col[i]);
    for(int i=1;i<=n-1;i++)
    {
        scanf("%d%d",&u,&v);
        g[u].push_back(v),g[v].push_back(u);
    }
    all=n,getroot(1,0),solve(rt);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&l,&r,&u);
        for(auto p:h[u])
            if(l<=p.l&&p.r<=r)
            {
                rt=p.id;
                break;
            }
        vec[rt].push_back({l,r,i,1});
    }
    for(int i=1;i<=lim;i++) pos[i]=inf;
    for(int i=1;i<=n;i++)
    {
        sort(vec[i].begin(),vec[i].end(),cmp);
        for(auto p:vec[i])
        {
            if(!p.op)
            {
                if(p.r>=pos[p.x]) continue;
                add(pos[p.x],-1),add(p.r,1),pos[p.x]=p.r;
            }
            else res[p.x]=sum(p.r);
        }
        for(auto p:vec[i]) if(!p.op) add(pos[p.x],-1),pos[p.x]=inf;
    }
    for(int i=1;i<=m;i++) printf("%d\n",res[i]);
    return 0;
}

posted on 2025-02-12 17:10  peiwenjun  阅读(63)  评论(0)    收藏  举报

导航