看到标题估计大家也猜到了,其实和树链剖分所用到的重链剖分挺像。
重链剖分中,每个点所选取的重儿子是它儿子中子树最大的那一个儿子,他们之间的连线被称为重边;在整棵树中,许多重边组成的链即重链。重链相互不重合的划分了整棵树。
重剖和长剖唯一不同的是:重链剖分中一个点的重儿子是子树最大(管辖节点最多)的儿子,而长链剖分选择的是 子树深度最大的那个儿子(子树深度:一个点的子树中深度最大的点的深度)。
(图:重链剖分和长链剖分的对比)
看明白是长链剖分是怎么划分了后,我们先来了解一下长链剖分的两个性质。
性质1:
对树长链剖分后,树上所有长链的长度和为$n$
这个不必解释吧...
性质2:
对于树上任意一点$x$,它的$K$级祖先$y$所在长链的长度一定$>=K$
这个恐怕就没那么显然了..
不过还是好懂。如果$x$就在$y$所在的长链里,那么既然$x$上跳$K$步后都还在该长链里,这条长链的长度铁定$>=k$咯
那么如果$x$不在$y$所在的长链里呢?也简单,我们思考一下,$x$没在$y$所在的长链的原因是什么?就是因为x的深度没有$y$所在长链的 最大深度 大,所以才被分到了轻儿子里。那既然$y$所在长链的最大深度$>=$x的深度,这条长链的长度自然$>=K$了
(图:x不在y所在的长链的情况)
性质3:
任何一个点向上跳到根所经过的轻边不会超过$sqrt{n}$条
也不是那么的显然...
假设有轻边$u->v$,$u$是$v$的父亲。由长链剖分的性质我们可以知道,之所以会产生这条轻边,是因为$u$有其他儿子的子树深度大于等于$v$的子树深度。
上面这幅图即为该性质的最坏情况。$u$只有一个儿子,而且其子树深度刚好等于$v$的子树深度。
那么我们来看看这个最坏情况下,从树上一点跳到根可能有多少条轻边。
从左下的点开始向上跳,随着上跳,越来越多的点被压在下面。跳过的节点数$1+2+3+...$的不断增长,直到总和$=n$。
所以设上跳最多经过轻边$w$条,也就是$1+2+3+...$会一直加到$w$,有下面的等式:
$large sum_{i=1}^w i=n$
用等差数列的求和公式替换掉左边,得:
$large frac{w(w+1)}{2}=n$
$large w^2+w=2n$
所以,$w$最大为$O(sqrt{n})$,得证。
上面三个性质请仔细理解,后面时间复杂度的证明和算法正确性证明都会用到~
No.0:长链剖分基本信息求解
要求的几个信息:
$dep[x]$:点$x$的深度
$mxd[x]$:点$x$的子树深度(点$x$的子树中深度最大的点的深度)
$son[x]$:点$x$的重儿子——子树深度最大的那个儿子
$top[x]$:点$x$所属长链的顶端节点
$len[x]$:点$x$所属长链的长度(包含边数)
别看有一大堆要求的,实际上2个DFS就可以求出来,和重链剖分差不多啦~
int dep[MXN],mxd[MXN],son[MXN]; //每个点的深度、子树最大深度、重儿子 void DFS1(int x,int d){//将求出dep、mxd、son dep[x]=mxd[x]=d;son[x]=0; for(int i=last[x];i!=0;i=nxt[i]){//前向星AddEdge时存单向边,这里不需要判断父亲 int to=edge[i].v; DFS1(to,d+1); if(mxd[to]>mxd[son[x]])//通过mxd更新son son[x]=to; } mxd[x]=max(mxd[x],mxd[son[x]]);//更新mxd } int top[MXN],len[MXN];//该点所属重链的顶点和长度 void DFS2(int x,int tp){//将求出top、len top[x]=tp;len[x]=mxd[x]-dep[tp];//保存tp,算出len if(son[x]) DFS2(son[x],tp);//重儿子继承top for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v;if(to==son[x]) continue; DFS2(to,to);//轻儿子top为自身 } }
下面讲解几个常见的运用
No.1:求树上两点的最近公共祖先(LCA)
板题:P3379 【模板】最近公共祖先(LCA)
没啥好说的了吧..就是重链剖分的求法。根据性质3,这个算法是$O(n sqrt{n})$的,并没有树剖和倍增优秀(虽然实际跑起来还挺快的)
/* Problem:洛谷P3379:最近公共祖先(LCA) 长链剖分 by sun123zxy */ #include<iostream> #include<cmath> #include<cstring> #include<algorithm> #include<ctime> #include<cstdio> #include<cstdlib> #include<algorithm> #include<queue> using namespace std; const int PTN=500005,EDN=1000005,INF=999999999; //-----前向星----- struct Edge{ int u,v; }edge[EDN]; int last[PTN],nxt[EDN],graN,graM,root; void GraphInit(){ graM=0;for(int i=0;i<PTN;i++) last[i]=0; } void AddEdge(int u,int v){ edge[++graM]=(Edge){u,v}; nxt[graM]=last[u]; last[u]=graM; } //-----主体----- int fa[PTN],dep[PTN],mxd[PTN],son[PTN]; //每个点的父亲,的深度、子树最大深度、重儿子 void DFS1(int x,int f,int d){//将求出dep、mxd、son fa[x]=f;dep[x]=mxd[x]=d;son[x]=0; for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v;if(to==f) continue; DFS1(to,x,d+1); if(mxd[to]>mxd[son[x]])//通过mxd更新son son[x]=to; } mxd[x]=max(mxd[x],mxd[son[x]]);//更新mxd } int top[PTN];//该点所属重链的顶点 void DFS2(int x,int tp){//将求出top top[x]=tp; if(son[x]) DFS2(son[x],tp); for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v;if(to==fa[x]||to==son[x]) continue; DFS2(to,to);//轻儿子top为自己 } } int LCA(int x,int y){//长剖求LCA while(top[x]!=top[y]){ if(dep[top[x]]<dep[top[y]]) swap(x,y);//保证x的链顶比y的链顶更深 x=fa[top[x]];//上跳 } if(dep[x]<dep[y]) return x;//最后已经在同一条长链中了,找到深度最小的那一个返回 else return y; } int main(){ GraphInit(); int qn; cin>>graN>>qn>>root; for(int i=1;i<graN;i++){ int u,v;scanf("%d%d",&u,&v); AddEdge(u,v);AddEdge(v,u); } DFS1(root,0,1); DFS2(root,root); for(int i=1;i<=qn;i++){ int x,y;scanf("%d%d",&x,&y); printf("%d ",LCA(x,y)); } return 0; }
No.2:求树上某点的k级祖先
板题:Vijos-Bashu_OIers lxhgww的奇思妙想(bsoj5276 询问)
/* 描述 lxhgww 在树上玩耍时,LZX2019 走了过来。lxhgww 突然问道:“我现在的k级祖先是谁?” LZX2019 答道:“不是我吗?”。接着 lxhgww 就用教主之力让 LZX2019 消失了,现在他转过头准备向你求助。 格式 输入格式 第一行包含一个整数N,表示树的结点数。 接下来N-1行包含两个整数X,Y,表示第X个结点和第Y个结点间有一条边。(博主注:树根为1) 接下来1行包含一个整数M,表示询问个数。 接下来M行包含两个整数a,b,则x=a^lastans,k=b^lastans。 输出格式 输出包含M行,分别是M个询问的答案。 若没有k级祖先,则输出0 样例 样例输入 1 2 3 4 5 5 0 7 5 1 3 Copy 样例输出 1 1 Copy 限制 每个测试点1s,512MB 提示 数据范围: ≤ N ≤300000, M ≤ 1800000 ≤ x,k≤ N Source LZX2019 */
emm,你也许首先会想离线水过...DFS根据取出栈顶下$k$位的元素即可。这是$O(n)$的算法
但是你发现这道题是强制在线的...
于是你又想到了LCA,想到用$O(nlogn)$的时间先ST预处理一道,询问时倍增$O(logn)$得到答案
但是你又发现这道题的询问时百万级的,$O(nlogn+qlogn)$怕是会被卡常,,,
于是,我们需要一种单次询问耗时比$O(logn)$还优秀的算法,同时预处理的时间不能超过$O(nlogn)$
那只能是询问$O(1)$了..但做到$O(1)$谈何容易?
非常暴力的方法就是存储每一个节点的每一个祖先,这样直接$O(1)$调用即可,但是空间会炸呀
(口胡结束,下为正片)
用长链剖分的性质做到$O(1)$询问!
预处理
我们先写几个预处理,先别问为什么要写,用途和时间复杂度后面会讲
- 预处理1:ST预处理出每个点的$2^k$级祖先
-
预处理2:对于所有$K$($1$~$n$),$mxbit[K]=log2(K)$。e.g:$mxbit[5]=2$
显然有$2^{mxbit[K]}>=K/2$
- 预处理3:对于所有长链的顶端节点$tp$,我们记录$tp$上方和下方的$len[tp]$个节点。(“上方”指该点的祖先,“下方”指该点所属长链中比该点深度大的点,下同)(怎样记录后文会讲)
(图:预处理3 处理的节点)
处理询问
so,开始处理询问。设询问的点是$p$,要找它的$K$级祖先$q$
首先利用ST从询问点p上跳 $2^{mxbit[K]}$ 步,把上跳后这个点叫$x$。
那么因为$2^{mxbit[K]}>=K/2$,我们至少跳了$K/2$步。
此时根据性质2,x所在长链的长度$len[x]>=K/2$。
设tp为x所在长链的顶端节点,$len[tp]=len[x]>=K/2$。
(图:询问例图1)(图:询问例图2)
回顾一下要求的问题,我们刚刚跳到$x$的时候至少跳了$K/2$步,所以现在从$x$到$q$的距离$Dis(x,q)=K−2^{mxbit[K]}<=K/2$。
还记得我们的预处理3吗?我们为所有的链顶存储了其上下$len[tp]$个节点。这个预处理怎么用呢?
我们希望要求的$q$在$tp$的预处理中,这样就可以通过$tp$直接$O(1)$调用到$q$了.
也就是说现在要证明$q$在$tp$上面$len[tp]$个或下面$len[tp]$个。
分类讨论。如果$q$在$tp$下面,它一定被夹在$x$和$tp$之间,那么既然$x$在$tp$所属长链上,$Dis(x,tp)<=len[tp]$,q在x的上面,所以$Dis(q,tp)<=len[tp]$,得证。
如果$q$在$tp$上面。首先在上一页我们知道了$len[tp]>=K/2$,又知道了$Dis(x,q)<=K/2$,所以$len[tp]>=Dis(x,q)$。又因为$tp$是$x$上跳若干步的点,所以也有$len[tp]>=Dis(tp,q)$,得证。
且我们能够确定$q$在$tp$上方的第$(K−2^{mxbit[K]})−Dis(x,tp)$个点。(负数为下方,0就是tp本身)
时间复杂度分析:
- 预处理1:倍增显然$O(nlogn)$
-
预处理2:处理出了$mxbit[1$~$n]$,每个数使用函数log2(n),共$O(nlogn)$
- 预处理3:我们处理出了所有长链顶端节点$tp$上下$len[tp]$个节点,根据性质1——所有长链长度加起来为$n$,我们总共预处理出了$2n$个节点,所以时间复杂度$O(n)$
-
询问:
因为已经倍增预处理过,上跳$2^{mxbit[K]}$步可直接调用ST表,耗时$O(1)$
从点$x$找到$tp$,从$tp$找到$q$,这些都有预处理过,故耗时$O(1)$
所以,单个询问总耗时$O(1)$
综上,该算法总耗时$O(nlogn+q)$,非常优秀。
实现:
-
预处理1:直接ST乱搞就完了呀(别告诉我你不会LCA)
-
预处理2:套函数还需要说吗?
- 预处理3:
有点麻烦了。当然你可以选择用数组套上vector来乱搞,但这太玄学了233
所以自己yy出了一种存法
数组$sav[]$存储了具体的数据,$idx[]$表示该长链顶点在$sav[]$中的基准点。
(图:sav[]存储方式示意)
如图,在$sav[]数组里$
$idx[x]-len[x]$~$idx[x]-1$存储的是$x$下的$len[x]-1$个节点
$idx[x]+1$~$idx[x]+len[x]$则存储了$x$上的$len[x]-1$个节点
$idx[x]$就存储了$x$
这样就可以直接通过$idx[x]+K$调用到$x$上方第$K$个节点(K为负就是下面)
-
询问:
就是按照上文所的做就是了...
$Dis(x,tp)=(dep[x]-dep[tp])$
最后的答案是$sav[idx[tp]+K-2^{mxbit[K]}-(dep[x]-dep[tp])]$
呼~完了!详情看代码吧,都有详细注释
/* Problem:长链剖分板题:lxhgww的奇思妙想 by sun123zxy */ #include<iostream> #include<cmath> #include<cstring> #include<algorithm> #include<ctime> #include<cstdio> #include<cstdlib> #include<algorithm> #include<queue> using namespace std; const int PTN=300005,EDN=600005,INF=999999999; //-----前向星----- struct Edge{ int u,v; }edge[EDN]; int last[PTN],nxt[EDN],graN,graM,logN; void GraphInit(){ graM=0;for(int i=0;i<PTN;i++) last[i]=0; } void AddEdge(int u,int v){ edge[++graM]=(Edge){u,v}; nxt[graM]=last[u]; last[u]=graM; } //-----主体----- int jmp[PTN][25],dep[PTN],mxd[PTN],son[PTN]; //每个点的倍增祖先、深度、子树最大深度、重儿子 void DFS1(int x,int fa,int d){//将求出jmp、dep、mxd、son jmp[x][0]=fa;dep[x]=mxd[x]=d;son[x]=0; for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v;if(to==fa) continue; DFS1(to,x,d+1); if(mxd[to]>mxd[son[x]])//通过mxd更新son son[x]=to; } mxd[x]=max(mxd[x],mxd[son[x]]);//更新mxd } int top[PTN],len[PTN];//该点所属重链的顶点和长度 void DFS2(int x,int tp){//将求出top、len top[x]=tp;len[x]=mxd[x]-dep[tp]; if(son[x]) DFS2(son[x],tp); for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v;if(to==jmp[x][0]||to==son[x]) continue; DFS2(to,to); } } void ST(){//将求出jmp for(int j=1;j<=logN;j++) for(int i=1;i<=graN;i++) jmp[i][j]=jmp[jmp[i][j-1]][j-1]; } int mxbit[PTN];//记录该数二进制表示的位数 void MxbitInit(){//将求出mxbit for(int i=0;i<=graN;i++) mxbit[i]=-1;//位数从0开始 for(int i=0;i<=graN;i++){ int t=i; while(t) mxbit[i]++,t/=2; } } int sav[PTN*2],sn,idx[PTN]; //sav存储 所有top节点x 的上下共2*len[x]+1个节点 //idx[x]记录x的基准点,从idx[x]左右摊开len[x]个位置 bool vis[PTN]; void DivInit(){ mxd[0]=0;DFS1(1,0,1); DFS2(1,1); ST(); MxbitInit(); sn=0;for(int i=1;i<=graN;i++) vis[i]=0; for(int i=1;i<=graN;i++){ int tp=top[i]; if(vis[tp]) continue;//只遍历一次top节点 vis[tp]=1; idx[tp]=sn+len[tp]+1;sav[idx[tp]]=tp;//记录基准点,存放tp int gg=tp; for(int j=idx[tp]-1;j>=idx[tp]-len[tp];j--)//向左存放tp整条重链 gg=son[gg],sav[j]=gg; gg=tp; for(int j=idx[tp]+1;j<=idx[tp]+len[tp];j++)//向右存放tp的各级父亲 gg=jmp[gg][0],sav[j]=gg; sn+=2*len[tp]+1;//指针移动到该节点管辖范围末尾 } } int Query(int x,int K){//询问 if(K>dep[x]) return 0;//不存在该祖先 if(K==0) return x;//就是自己 x=jmp[x][mxbit[K]];K-=1<<mxbit[K];//上跳2^mxbit[K]个节点并修改K int tp=top[x]; return sav[idx[tp]+K-(dep[x]-dep[tp])];//tp与x的距离为dep[x]-dep[tp] //在x上面K个点,也就是在tp上面K-(dep[x]-dep[tp])个点(可能为负) } int main(){ GraphInit(); cin>>graN;logN=log2(graN); for(int i=1;i<graN;i++){ int u,v;scanf("%d%d",&u,&v); AddEdge(u,v);AddEdge(v,u); } DivInit(); int qn;cin>>qn; int LANS=0; for(int i=1;i<=qn;i++){ int x,K;scanf("%d%d",&x,&K); x^=LANS,K^=LANS;//强制在线 LANS=Query(x,K); printf("%d ",LANS); } return 0; }
No.3:长链剖分维护树上可合并深度信息
板题:CF1009F Dominant Indices(洛谷)
英文看不懂,翻译也不是人话!我简单写一下题意
给你一颗根为1的树,对于每个点$x$有一个距离$d$,$x$的子树中有一组点$p$满足$Dis(x,p)=d$。你需要给每个点$x$找到一个$d$,使其子树中离$x$距离为$d$的点最多。
输出所有点对应的$d$
$n<=1000000$
百万的数据,又是一道卡$O(nlogn)$的题目。所以我们需要想出一种$O(n)$的算法求出。
观察题目,很容易想到要维护一个二维数组$f[x][d]$,表示在$x$的子树中离$x$距离为$d$的点的个数。
于是我们得到了一个转移方程
$large f[u][d]=sum f[v][d-1]$(u->v是一条父子边)
好,那么我们获得了一个比较暴力的搞法。直接从根DFS后序遍历每一个节点,通过该点的子节点(遍历$f[v][d]$,$v$为子节点)更新获得该点的所有$f[u][d]$。答案即为$max(f[1][d])$。
时间复杂度大概是$O(n^2)$。
but $n<=1000000$,we need $O(n)$!
考虑每个点在合并子节点信息时进行的重复计算。我们发现我们更新节点的$f$是通过累加该点儿子的数据获得的,每个儿子$v$将信息累加到父亲消耗的时间是$O(mxd[v])$。
那么在最开始更新节点的$f$的时候,我们可以把这个累加操作看做是从该节点的某一个儿子$v$拷贝下来了一截长为$mxd[v]$的数据,然后位移一位存入该节点的$f$。
所以我们考虑优化这一个"拷贝" 的操作,让它可以$O(1)$实现。
那么怎么做呢?我们可以让该节点和它的某个儿子共用一段内存空间,儿子统计的信息存储在它父亲的一段内存空间内,相当于就直接被保存在了父亲$f$中,省去了这个拷贝的操作。对于其他儿子,直接暴力合并累加进来就可以了。
那么我们应该选择哪一个子节点共用内存空间,才能使时空消耗尽可能的小呢?
当然是儿子中$mxd$最大的那一个了呀!前面已经提到,每个儿子$v$将信息累加到父亲消耗的时间是$O(mxd[v])$,让$mxd$最大的那一个儿子与父亲共用空间$O(1)$转移,不就大幅减小时间消耗了吗
具体实现的话,我们首先开一个大的内存池(一个大数组),用来存放所有节点所有深度的$f$,然后从根向下DFS,先序开辟内存,划定该点$x$的存储范围(存储基准位置和长度$mxd[x]$),对于同一条重链上的节点,它们全部共用一段内存,每个点的存储范围随该点$mxd$的增加而减少,形成许多尾部重叠的后缀。对于其他轻儿子,就跳过当前重链占据的内存区间,为它们分别新开辟一片内存,DFS下去让它们自己分配。回溯时就暴力累加进来更新$f$就完了。
(这张大图可以充分满足你的需要)
好像很有道理的样子蛤
等等,但这么做时间复杂度就$O(n)$了?而且空间不会炸吗?
不会。时间是$O(n)$的,空间也是$O(n)$。可以简单的证明一下。
时空复杂度分析
时间:只有在遇到一个轻儿子,也就是在一条长链的链顶$x$上,我们才会用$O(mxd[x])$(也就是该重链的长度)的时间复杂度暴力转移,而根据性质一:所有重链的长度和为$n$,所以我们消耗的总也是$O(n)$的。
空间:只有在遇到一个轻儿子,也就是在一条长链的链顶$x$上,我们才会开辟$mxd[x]$(也就是该重链的长度)的新空间,而根据性质一:所有重链的长度和为$n$,所以我们开辟的总空间也是$O(n)$的。
是不是巨玄学?我们用的算法还是暴力,只是最大程度的节省了白耗的时间,最大程度的重用了空间,然后就得到了这个$O(n)$的长剖做法!
吐槽*1:网上看到的题解都用的指针写这道题?数组实现不是也很方便吗
吐槽*2:由于长剖资料较少,网上的dalao们也不屑于讲细,上面的东西有些是自己yy的,有错误还请指正
#include<iostream> #include<cmath> #include<cstring> #include<algorithm> #include<ctime> #include<cstdio> #include<cstdlib> #include<algorithm> #include<queue> using namespace std; const int PTN=1000005,EDN=2000005,INF=999999999; //-----前向星----- struct Edge{ int u,v; }edge[EDN]; int last[PTN],nxt[EDN],graN,graM,root; void GraphInit(){ graM=0;for(int i=0;i<PTN;i++) last[i]=0; } void AddEdge(int u,int v){ edge[++graM]=(Edge){u,v}; nxt[graM]=last[u]; last[u]=graM; } //-----主体----- int fa[PTN],dep[PTN],mxd[PTN],son[PTN]; int len[PTN];//这里的len重新定义为该点到该点所属链的链底的点的个数 void DFS1(int x,int f,int d){ fa[x]=f;dep[x]=mxd[x]=d;son[x]=0; for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v;if(to==f) continue; DFS1(to,x,d+1); if(mxd[to]>mxd[son[x]]) son[x]=to; } mxd[x]=max(mxd[x],mxd[son[x]]); len[x]=mxd[x]-dep[x]+1;//计算len } int sav[PTN],idx[PTN],ans[PTN],it;//注意:ans[x]存的是相对x的深度(下文所有深度描述默认相对于x) //sav[]是一个大内存池,idx[x]是点x在sav[]里的存储起点,it指向当前轻儿子存储起点,赋给idx[to]后向后跳指向下一个轻儿子的存储起点 //一个节点x的深度信息在sav[]里的存储范围是 idx[x]~(idx[x]+len[x]-1) void Sol_DFS(int x){ sav[idx[x]]=1;//深度0,只有自己(这里暂时不管ans,后面再来特判) if(son[x]){ idx[son[x]]=idx[x]+1;//让重儿子与自己共用一段存储空间 Sol_DFS(son[x]); ans[x]=ans[son[x]]+1;//初始化ans[x]为重儿子跑出来的答案 } for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v;if(to==fa[x]||to==son[x]) continue; idx[to]=it;it+=len[to];//把当前轻儿子应有的存储起点赋给idx[to],然后跳转到下一个轻儿子应有的存储起点 Sol_DFS(to); for(int j=0;j<len[to];j++){//更新深度(距离)范围为 0~len[to]-1 sav[idx[x]+j+1]+=sav[idx[to]+j];//把轻儿子的信息更新到x中对应深度存储中 if(sav[idx[x]+j+1]>sav[idx[x]+ans[x]] ||(sav[idx[x]+j+1]==sav[idx[x]+ans[x]]&&ans[x]>j+1))//本题要求如果节点数相同,选较小的答案 ans[x]=j+1;//如果找到当前节点x的更优解,更新ans[x] } } if(sav[idx[x]+ans[x]]==1) ans[x]=0;//特判:如果答案和深度0的答案一样,根据本题要求选择0 } void Solve(){ it=0; idx[root]=it;it+=len[root];//为根节点做存储初始化 Sol_DFS(root); } int main(){ GraphInit(); cin>>graN;root=1; for(int i=1;i<graN;i++){ int u,v;scanf("%d%d",&u,&v); AddEdge(u,v);AddEdge(v,u); } DFS1(root,0,1); Solve(); for(int i=1;i<=graN;i++) printf("%d ",ans[i]); return 0; }
No.4:其他玄学操作
例题1:bzoj3252 攻略
sto p9t6g 单手切掉该题 tql
是的,本题将长剖用到了出神入化的地步
给你一颗点权树,让你随意从根到叶子节点走K次,求K次经过点点权之和的最大值。(一个点重复经过多次不能重复加入答案,也就是只能被加入一次)
是不是很懵逼?我和p9t6g菊苣对着这道题瞎yy了半天(讨论过程可以看看下面放的ppt)
然后发现了这个玄学搞法
我们重新定义长链的长度为其包含的所有节点点权之和,然后按之前的方法剖掉整棵树。
然后直接把长度前K大的长链长度加起来output就完了。
对,完了。
(黑人问号.jpg)
这是什么玄学玩意???
转念一想,好像还是很有道理的蛤
你想,从根节点出发,如果走一次的话贪心的来说你肯定走那条从根开始的长链,要走第二次的话就从根节点的儿子里面又选一条最长的长链来走,第三次又找到一个与已经走过的点相邻的未走过点往下走它的长链...这样搞K次就完了
嗯,可以保证贪心是正确的,也就是说每次应该选择的最长的长链头必然是与已经走过的点组成的连通块相邻。
为啥?求得长链本身就是一个类似dp的过程,所以其实这个长链是包含其子问题的。剖出来一条长链,对于长链上的每一个点这种走法都是最优的。那么在走过一条长链后,剩下的就是稍次的走法,也就是当前最优的走法。
emm我可能有点语无伦次,我重新组织一道语言233
如果选了一条长链作为路径的下半部分(上半部分之前已经走过了),那么对于该长链链头的父亲,它一定没有更优的选择方案。因为如果有更优的方案,根据长剖的性质,必然会先选那个更优的。而这一条性质对于每个节点都适用,所以给长链按长度排个前K大加起来就完了。
将就看看感性理解一下吧..
于是屏幕前的你心情复杂的写下了代码并A了这道题
#include<iostream> #include<cmath> #include<cstring> #include<algorithm> #include<ctime> #include<cstdio> #include<cstdlib> #include<algorithm> #include<queue> using namespace std; typedef long long ll; const int MXN=2000005; //-----前向星----- struct Edge{ int u,v; }edge[MXN]; int last[MXN],nxt[MXN],graN,graM,logN; void GraphInit(){ graM=0;for(ll i=0;i<MXN;i++) last[i]=0; } void AddEdge(int u,int v){ edge[++graM]=(Edge){u,v}; nxt[graM]=last[u]; last[u]=graM; } //-----主体----- ll dep[MXN],mxd[MXN],ptv[MXN];int son[MXN],fa[MXN]; void DFS1(int x,int f,ll d){ fa[x]=f;dep[x]=mxd[x]=d;son[x]=0; for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v; DFS1(to,x,d+ptv[to]); if(mxd[to]>mxd[son[x]]) son[x]=to; } mxd[x]=max(mxd[x],mxd[son[x]]); } int top[MXN];ll len[MXN],sit=0; void DFS2(ll x,ll tp){ top[x]=tp;if(x==tp) len[++sit]=mxd[x]-dep[fa[tp]]; if(son[x]) DFS2(son[x],tp); for(int i=last[x];i!=0;i=nxt[i]){ int to=edge[i].v;if(to==son[x]) continue; DFS2(to,to); } } int main(){ GraphInit(); ll K;cin>>graN>>K; for(int i=1;i<=graN;i++) scanf("%lld",&ptv[i]); for(int i=1;i<graN;i++){ int u,v;scanf("%d%d",&u,&v); AddEdge(u,v); } DFS1(1,0,ptv[1]); DFS2(1,1); sort(len+1,len+1+sit); ll ans=0;int tt=0; for(int i=sit;i>=1&&tt<K;i--) tt++,ans+=len[i]; cout<<ans; return 0; }
No.5:没了
继续吐槽:
woc我怎么写了这么长!
长链剖分
可能以后再也不会了233
参考:
https://www.cnblogs.com/meowww/p/6403515.html