• dsu on tree 学习笔记


    一、概述

    (dsu on tree)通常用于解决子树上的问题,要求无修改操作且允许离线。

    对于这样的问题,我们以前学过了像树上莫队、点分治等做法,但(dsu on tree)的复杂度远优于他们

    虽然(dsu on tree)复杂度优秀,但其实,它就是一个优雅的暴力:

    遇到子树问题,最暴力的做法毫无疑问就是暴力枚举子树上的所有点统计答案,实际上(dsu on tree)就是这样做的。

    只不过,(dsu on tree)有着优雅的思想:轻重链剖分,它借用对每个点轻儿子与重儿子贡献的分别处理,达到了(mathcal O(nlog(n)))的复杂度。

    二、实现

    • 将询问离线,记录在子树的根节点上
    • 遍历整棵树,对于节点(u),先计算它轻儿子的答案,计算后删除信息
    • 计算它重儿子的答案,不删除信息
    • 将重子树的信息合并到(u)
    • 暴力遍历(u)的轻子树,将轻子树的信息合并到(u)
    • 处理(u)处的询问
    • 根据(u)是否是重儿子选择是否删除(u)的信息

    这就是(dsu on tree)的思想了,大家可能不太理解,我们从一道例题来感受一下:

    三、例题

    CF600E

    题意:

    • 给定一棵(n)个节点的以(1)为根的树,每个节点都有一个颜色。

    • 如果一种颜色在以(x)为根的子树内出现次数最多,称其在以(x)为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。

    • 你的任务是对于每一个(i in[1,n]),求出以(i) 为根的子树中,占主导地位的颜色的编号和。

    题解:

    这是一道经典的(dsu on tree)题目了

    考虑如何暴力做:显然可以维护(w[i])表示(i)这种颜色出现的次数,同时记录(ret)表示目前处理的节点中出现次数最多的颜色的编号和。

    当遍历到(u)时,首先我们枚举它的轻儿子递归下去,遍历轻儿子后,要删除轻子树节点对于(w)的影响

    接着遍历重子树,这次我们保留这些节点的贡献

    那么全局变量中已经保存了重子树的信息了,轻子树的信息我们直接暴力枚举,修改(w)(ret)

    遍历完后,该节点的答案就是(u)的答案,最后,删除该节点的贡献,也是暴力枚举它的子树中的所有节点并删去。

    代码如下:

    int hson[N],siz[N],w[N],mx,son;
    ll ret,ans[N];
    inline void dfs(int u,int f){
    	siz[u]=1;
    	for(int i=first[u];i;i=e[i].nxt){
    		int v=e[i].v;
    		if(v==f) continue;
    		dfs(v,u);
    		siz[u]+=siz[v];if(siz[v]>siz[hson[u]]) hson[u]=v;
    	}//轻重链剖分模板
    }
    inline void work(int u,int f,int tp){
    	w[col[u]]+=tp;//tp=1表示要增加这个节点的贡献,-1则是减去该节点的贡献
    	if(w[col[u]]>mx) mx=w[col[u]],ret=col[u];
    	else if(w[col[u]]==mx) ret+=col[u];//更新ret
    	for(int i=first[u];i;i=e[i].nxt){
    		int v=e[i].v;
    		if(v==f||v==son) continue;//son保存的是重儿子,不要遍历到重儿子去
    		work(v,u,tp);
    	}
    }
    inline void dsu(int u,int f,int tp){//tp=1表示不删除信息,tp=0表示要删除
    	for(int i=first[u];i;i=e[i].nxt){
    		int v=e[i].v;
    		if(v==f||v==hson[u]) continue;
    		dsu(v,u,0); //处理轻儿子,要删除信息
    	}
    	if(hson[u]) dsu(hson[u],u,1),son=hson[u];//遍历重儿子,不删除信息
    	work(u,f,1);//暴力遍历轻子树
        son=0;//接下来删除贡献是暴力遍历整个子树而不仅是轻子树了
    	ans[u]=ret;
    	if(!tp) work(u,f,-1),mx=0,ret=0;//直接删除所有节点的贡献
    }
    

    看起来十分暴力吧?它的复杂度其实确实是(mathcal O(nlog(n)))的,这里给出了粗略的证明:

    首先,根据轻重链剖分的性质,每一个点到根的路径上至多有(mathcal O(log(n)))条轻边。

    考虑一个点(u)在什么时候会被遍历到:

    (u)被一个祖先节点(x)统计当且仅当(u)(x)的轻子树上,也就是说(x-u)的这条链第一条边是轻边,那么唯一一条轻边对应唯一一个(x),所以至多被统计(mathcal O(log(n)))

    (u)被一个祖先节点遍历以删除贡献当且仅当(x)是一个轻儿子,在(u)的祖先中,轻儿子的数量不超过(mathcal O(log(n)))个,于是也只会被遍历(mathcal O(log(n)))

    综上,因为每个点的信息修改是(mathcal O(1))的,所以总复杂度是(mathcal O(nlog(n)))

    四、更多例题

  • 相关阅读:
    工作中问题的总结1
    linux问题故障
    时间转换
    Tips
    总结
    方向
    同步&异步-阻塞&非阻塞
    IO 之 mark()、reset()
    GC日志分析
    JDK 部分工具使用方法
  • 原文地址:https://www.cnblogs.com/tqxboomzero/p/14255655.html
Copyright © 2020-2023  润新知