一次模拟赛的\(T3\):传送门html
只会\(O(n^2)\)的我就\(gg\)了,而且对于题解提供的\(\text{dsu on tree}\)的作法一脸懵逼。node
看网上的其余大佬写的笔记,我本身画图看了一天才看懂(我太蒻了),因而就有了这篇学习笔记。git
如今考虑这样一类树上统计问题:github
无修改操做,询问容许离线算法
对子树信息进行统计(链上的信息在某些条件下也能够统计)数组
树上莫队?点分治?数据结构
\(\text{dsu on tree}\)能够把它们吊起来打!学习
\(\text{dsu on tree}\)运用树剖中的轻重链剖分,将轻边子树信息累加到重链上进行统计,拥有\(O(nlogn)\)的优秀复杂度,常数还贼TM小,你值得拥有!优化
//虽然说是dsu on tree,但某个毒瘤@noip说这是静态链分治 //还有其余的数据结构神du仙liu说它能够被当作是静态的树剖(由于其在树上有强大的统计信息的能力,但不能支持修改操做),与正常的树链剖分相对 //因此我同时保留这几种说法,但愿数据结构神du仙liu们不要喷我这个juruo
遍历全部轻儿子,递归结束时消除它们的贡献ui
遍历全部重儿子,保留它的贡献
再计算当前子树中全部轻子树的贡献
更新答案
若是当前点是轻儿子,消除当前子树的贡献
那么这里有人可能就要问了,为何不保留求出的全部答案呢?这样复杂度就更优了啊
若是这样的话,当你处理完一颗子树的信息时,再递归去求解另外一颗子树时,
已有的答案就会与当前子树信息相混淆,就会产生错误答案。
因此,从这咱们看出,一个节点只能选择一个子节点来保留答案
其它的都要去暴力求解
那么选择哪个节点能使复杂度最优呢?
显然,咱们要尽可能均衡答案被保留的子树和不被保存的子树的大小
这是否是就很像树链剖分划分轻重儿子了呢?
由于窝太蒻了一开始没怎么理解它,因此有了图解这个环节23333。
同理,\(2\)号点也可进行相似操做,由于它的重儿子\(6\)号节点已保存了这颗子树的答案,只需上传便可,
不用再从\(6\)这个位置再往下走统计答案,惟一会暴力统计答案的只有它的轻儿子\(5\)号节点
inline void calc(int x,int fa,int val) { ...................... /* 针对不一样的问题 采起各类操做 */ for(rg int i=0;i<(int)G[x].size();++i) { int v=G[x][i]; if(vis[v] || v==fa) continue; calc(v,x,val); } } inline void dfs(int x,int fa,int keep)//keep表示当前是否为重儿子 { for(int i=0;i<(int)G[x].size();++i) { int v=G[x][i].v; if(v==fa || v==son[x]) continue; dfs(v,x,0); } if(son[x]) dfs(son[x],x,1),vis[son[x]]=true;//标记重儿子 calc(x,fa,1);vis[son[x]]=false;//计算贡献 ans[x]=....;//记录答案 if(!keep) calc(x,fa,-1);//不是重儿子,撤销其影响 }
若是是维护路径上的信息,大概还能够这么写:(若是有错,请大佬指出)
ps:关于\(\text{dsu on tree}\)对路径上信息进行维护的精彩应用,能够看最后\(3\)道例题
inline void dfs(int x,int fa) { siz[x]=1,dep[x]=dep[fa]+1,nid[rev[x]=++idx]=x; //再次借助树剖的思想,子树内节点顺序转为线性 for(rg int i=0;i<(int)G[x].size();++i) { int v=G[x][i].v,w=G[x][i].w; if(v==fa) continue; dfs(v,x),siz[x]+=siz[v]; if(!son[x] || siz[v]>siz[son[x]]) son[x]=v; } } inline void calc(int x,int val) {//对x这一节点进行单独处理 if(val>0) //计算贡献 else //撤销影响 } inline void dfs2(int x,int fa,int keep) { for(rg int i=0;i<(int)G[x].size();++i) { int v=G[x][i].v; if(v==fa || v==son[x]) continue; dfs2(v,x,0); } if(son[x]) dfs2(son[x],x,1); for(rg int i=0;i<(int)G[x].size();++i) { int v=G[x][i].v; if(v==fa || v==son[x]) continue; for(rg int j=0;j<siz[v];++j) { int vv=nid[rev[v]+j]; .......... //更新答案 } for(rg int j=0;j<siz[v];++j) calc(nid[rev[v]+j],1); } calc(x,1); ..........//更新答案 if(!keep) for(rg int i=0;i<siz[x];++i) calc(nid[rev[x]+i],-1); }
不感兴趣的大佬能够跳过这一段。(蒟蒻本身乱\(yy\)的证实,若是有错请大佬指出)
显然,根据上面的图解,一个点只有在它到根节点的路径上遇到一条轻边的时候,本身的信息才会被祖先节点暴力统计一遍
而根据树剖相关理论,每一个点到根的路径上有\(logn\)条轻边和\(logn\)条重链
即一个点的信息只会上传\(logn\)次
若是一个点的信息修改是\(O(1)\)的,那么总复杂度就是\(O(nlogn)\)
此题来自洛咕日报第\(65\)篇做者\(\text{codesonic}\)。
咱们能够维护一个全局数组\(cnt\),表明正在被计算的子树的每种颜色的数量
每次计算子树贡献的时候,把节点信息往里面加就好了,若是一个颜色第一次出现,则颜色种类数\(top++\)
对于须要撤销影响的子树,把信息从里面丢出来便可,若是被删除的颜色只有这一个,则颜色种类数\(top--\)
公认的\(\text{dsu on tree}\)模板题,相比于上题只是增长了对每种数量的颜色和的统计。
咱们能够维护\(cnt\)数组,表示某个颜色出现的次数;再维护一个\(sum\)数组,表示当前子树出现了\(x\)次的颜色的编号和
对节点信息统计时,先把它在\(sum\)数组里的贡献删掉,更新了\(cnt\)数组后再添回去
而后别忘了开\(long \, long\)(血的教训)
\[\color{orange}{\texttt{-> 原题传送门 <-}}\]
窝太菜了,不会二进制优化,只会\(O(26*nlogn)\)
首先,由于要造成回文串、又能够对字符进行任意排列,因此最多只能有一种字母的出现次数为奇数
而后咱们维护一个\(cnt\)数组,统计每一个深度全部字母的出现次数:
cnt[dep[x]][s[x]-'a']+=val;
\[\color{orange}{\texttt{-> 原题传送门 <-}}\]
首先用\(map\)把给的全部名字哈希成\(1\)到\(n\)的数字
题目就能够转化为求出每一个深度有多少不一样的数
一样,对每一个深度开个\(set\)去重并统计
而后就是套板子的事情了
\[\color{orange}{\texttt{-> 原题传送门 <-}}\]
显然原问题能够转化为求该点的\(k\)级祖先有多少个\(k\)级儿子(若是没有\(k\)级祖先,答案就是0)
而一个点\(x\)的\(k\)级儿子即为在以\(x\)为根节点的子树中有多少点的\(dep\)为\(dep[x] + k\)
把全部询问读进来,求出相关的点的\(k\)级祖先(能够离线\(O(n)\)处理,也能够倍增\(O(nlogn)\)搞;若是时空限制比较紧,就采起前者吧)
而后由于是统计节点数,因此开一个普通的\(cnt\)数组维护便可。最后答案别忘了\(-1\),由于算了本身
扔一个增强版的(\(N \le 10^6\),\(128MB,1s\)):\(\color{#66ccff}{\texttt{-> 传送门 <-}}\)
友情提醒:上面这道良心题不只卡空间,还卡时间(若是你用dsu on tree)
\[\color{orange}{\texttt{-> 原题传送门 <-}}\]
点分治的题怎么能用点分治呢?并且这仍是dsu on tree学习笔记
首先,这道题是对链的信息进行统计,就不能再像对子树的统计方法去搞♂了,因此须要一些奇技淫巧
思路与点分治同样,对于每一个节点\(x\),统计通过\(x\)的路径的信息
注意到这道题链上的信息是可加减的,因此咱们能够不保存\(x\)的子孙\(\rightarrow x\)的信息,而是保存每一个节点到根节点的信息,在统计的时候在减去\(x \rightarrow\)根节点的信息
而后咱们考虑如何统计,咱们能够在每一个节点维护一个桶\(cnt\),记录从这个点\(x\)往下走的全部路径中,能造成的每种路径权值和以及其所须要的最少的边的数量:
对于\(v_{ij}\),计算出其到\(x\)的距离\(dis\)及深度差\(d\)(能够当作路径上的节点数),并用\(d\) \(+\) \(cnt[\)k−dis\(]\)来更新答案。
而后用刚才获得的\(dis\)对应的\(d\)来更新\(cnt[dis]\)的值。
这样就至关于,用每一个\(v_{ij}\)到\(x\)的链,与以前桶中所保存某条链的路径权值和之和恰为\(k\)的拼成一条路径,并更新答案。而后,再把它也加入桶中
再套上\(\text{dsu on tree}\)的板子,每一个节点保存它的重儿子的 桶的信息便可
虽然是\(O(nlog^2n)\)的,但常数小,咱不慌
可是窝太菜了,用\(map\)做桶不开\(O2\)会\(T \, 3\)个点(毕竟用了\(STL\),还有两只\(log\)),有空再重写一遍233
貌似用\(unodered_{}map\)不开\(O2\)也卡得过去。。
\[\color{orange}{\texttt{-> 原题传送门 <-}}\]
考虑在第一段路径上一点\(u\)能观测到该玩家的条件是:\(dep[S] - dep[u] = w[u]\)
同理,在第二段路径上一点\(u\)能观测到该玩家的条件是:\(dep[T] - dep[u] = dis(S,T) - w[u]\),即\(dep[S] - 2 \times dep[lca(S,T)] = w[u] - dep[u]\)
而后能够用差分的思想,对每一个节点开两个桶\(up\)、\(down\)进行统计
在\(S\)的\(up\)中插入\(dep[S]\)
在\(T\)的\(down\)中插入\(dep[S] - 2 \times dep[lca(S,T)]\)
由于\(lca(S,T)\)会对\(S \rightarrow T\)和\(T \rightarrow S\)都进行统计,因此在其\(up\)中删除\(dep[S]\)
同理,在\(fa[lca(S,T)]\)的\(down\)中删除\(dep[S] - 2 \times dep[lca(S,T)]\)
而后用\(\text{dsu on tree}\)统计便可,答案为\(up[w[u]+dep[u]] + down[w[u] - dep[u]]\)
注意到\(w[u] - dep[u]\)可能小于零,为了不负数下标、又不想套\(map\),咱们可使用以下\(trick\)
int up[N],CNT[N<<1],*down=&CNT[N]; //把donw[0]指向CNT[N],这样就能够给负数和正数都分配大小为N的空间
跑的虽然没有普通的差分快,不过吊打线段树合并仍是绰绰有余的
\[\color{orange}{\texttt{-> 原题传送门 <-}}\]
跟每天爱跑步差很少,就不画图了(~懒)
同上题,用差分的思想,对每一个节点的增长和删除开两个桶统计
同时,这题要维护每一个点出现的最多物品的种类,直接开个线段树维护就行了
\(O(nlog^2n)\),常数应该和树剖差很少,不过由于每一个点都要进行增长删除两个操做,常数大了一倍,并且还用了线段树,因此\(\cdots\)
不过依然比部分线段树合并跑的快2333
由以上三题,咱们能够看出,在必定条件下,\(\text{dsu on tree}\)也是能够在链上搞♂事情的
好比\(Race\)知足链上信息可加减性,后两道题能够用差分将链上的修改/询问转化为点上的修改/询问
但\(\text{dsu on tree}\)能够应用的条件确定不止以上两种,由于窝太蒻了,只见识了这些题,之后看到其余类型的也会补上来
\[\color{orange}{\texttt{-> 提交地址 <-}}\]
如今终于能够回过头来解决这个题了
留给你们思考吧,要代码的话能够私信我
虽然有不少大佬会线段树合并或虚树上\(dp\)秒切这道题,不过仍是但愿用\(dsu \; AC\)
之后还会不按期地添加\(\text{dsu on tree}\)的相关题目~
若是有须要,我会把最后那道题的代码贴出来