- LCA
- 树上倍增
- 树链剖分
- LCT(Link-Cut Tree)
- 玄♂妙的动态树问题
- 然而Link-Cut Tree并不能吃
- LCT之模样与概念
- LCT之核心操作
- 注意事项
- 例题1:BZOJ2049、luoguP2147.[SDOI2008]洞穴勘测
- 例题2:luoguP3203. [HNOI2010]弹飞绵羊
- 例题3:JZOJ2256.【ZJOI2008】树的统计
- 例题4:BZOJ3282、luoguP3690.【模板】Link Cut Tree (动态树)
- 例题5:JZOJ3754、luoguP2387.【NOI2014】魔法森林
- 例题6:JZOJ5466.【NOIP2017提高A组冲刺11.9】玩游戏
- 例题7:JZOJ4090.树上三角形(triangle)
- 尾声
LCA
what is LCA and LCA能干嘛
LCA(Lowest Common Ancestors),即最近公共祖先
在一棵树上,两个节点的深度最浅的公共祖先就是LCA (自己可以是自己的祖先)
很简单,我们可以快速地进入正题了
下图中的树,4和5的LCA为2,1和3的LCA是1(1是1的祖先)
除了你是刚学LCA、叫你直接求两点的LCA的模板题,没有题目会给你拍个裸code让你AC掉的这点不存在的~
那么LCA能干嘛?
其实LCA的用途很广,个人认为OI里用LCA最多的像是这样子的题
给你一棵树(或者是一个图求完MST的树,这种会比较常见)和树上的两点
然后找出这两点之间的路径的极值或权值和或其他东东
这时我们可以借助这两点的LCA作为中转点来轻松地解决这类题目
how to discover LCA
还是这棵树
4和3的LCA是1,这两点间的路径是下图标红的路径
怎么求LCA呢?
方法一
暴力dfs一遍,时间复杂度
,侮辱智商……
(逃)
方法二
模拟,从两点开始一起向上跳一步,跳过的点标记一下,时间复杂度
(逃)
方法三
用dfs记录每个点的深度,求LCA时先调至统一深度,再一起向上跳
如果这棵树是一个刚好分成两叉的树,时间复杂度
(逃)
以上三种方法都是小学生都会打的方法,不必讲了
(看不懂?右上角‘x’请)
想真正用有意义的LCA解决OIproblem,接下来的就才是重头戏
tarjan算法(离线)求LCA
求 有一种非常容易理解的方法就是tarjan算法预处理离线解决,时间复杂度 ,飞速
但个人觉得离线算法不适用于所有题目
(比如某些良心出题人丧病地要你强制在线,用tarjan你就100%GG了)
而在线算法基本可以解决所有的离线算法能做出来的题目,离线算法的缺陷就像我说的那样
所以在这里我不讲tarjan算法求LCA了,这里推荐一篇写的很棒的blog,自行理解一下吧
接下来的几个在线求LCA算法才是踏进这篇博客的门槛
ST(RMQ)算法(在线)求LCA
这种方法的思想,就是将LCA问题转化成RMQ问题
如果不会RMQ问题——ST算法的就戳这里吧
怎么把LCA转成RMQ问题?
这里有棵树
如何预处理呢?
我们用dfs遍历一次,得到一个dfs序(儿子节点回到父亲节点还要再算一遍)
dfs序就是这样的1->2->4->7->4->8->4->2->5->2->6->9->6->10->6->2->1->3->1
(一开始在root向儿子节点走,到了叶子节点就向另一个儿子走,最后回到root)
dfs预处理的时间复杂度为
看出dfs序的玄妙了吗?
设
表示
在这个dfs序当中第一次出现的位置,deep[x]表示x的深度。
如果求x和y的
,r[x]~r[y]这一段区间内一定有
,而且一定是区间中深度最小的那个点
(比如上面的dfs序中,第一个7和第一个5之间的序列里的深度最小的点是2,而2正是7和5的LCA!)
遍历以 为根的树时,不遍历完所有以 为根的树的节点是不会回到 的
还有就是明显地,想到达x再到y,必须上溯经过它们的LCA(两个点之间有且只有一条路径)
所以 的深度一定最小,无需过多解释
所以,我们直接用RMQ——ST表维护这个东西,求出来的最小值的点即为
注意事项
dfs序的长度是 ,用dfs 处理出r、deep和dfs序之后直接套上裸RMQ
(线段树?差不多的……别在意这个)设 表示dfs序中j~j+2^i-1的点当中,deep值最小的是哪个点 即可
每次询问时间复杂度 ,完美
code
声明一下就是我也是看别人blog学的RMQ解决LCA,所以code我就直接copy过来了
code有问题,不棱怪我~
啊还有一点就是这人的code贼丑
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
int n,_n,m,s;//_n是用来放元素进dfs序里,最终_n=2n-1
struct EDGE
{
int to;
EDGE* las;
}e[1000001];//前向星存边
EDGE* last[500001];
int sx[1000001];//顺序,为dfs序
int f[21][1000001];//用于ST算法
int deep[500001];//深度
int r[500001];//第一次出现的位置
int min(int x,int y)
{
return deep[x]<deep[y]?x:y;
}
void dfs(int t,int fa,int de)
{
sx[++_n]=t;
r[t]=_n;
deep[t]=de;
EDGE*ei;
for (ei=last[t];ei;ei=ei->las)
if (ei->to!=fa)
{
dfs(ei->to,t,de+1);
sx[++_n]=t;
}
}
int query(int l,int r)
{
if (l>r)
{
//交换
l^=r;
r^=l;
l^=r;
}
int k=int(log2(r-l+1));
return min(f[k][l],f[k][r-(1<<k)+1]);
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
int j=0,x,y;
for (int i=1;i<n;++i)
{
scanf("%d%d",&x,&y);
e[++j]={y,last[x]};
last[x]=e+j;
e[++j]={x,last[y]};
last[y]=e+j;
}
dfs(s,0,0);
//以下是ST算法
for (int i=1;i<=_n;++i)f[0][i]=sx[i];
int ni=int(log2(_n)),nj,tmp;
for (int i=1;i<=ni;++i)
{
nj=_n+1-(1<<i);
tmp=1<<i-1;
for (j=1;j<=nj;++j)
f[i][j]=min(f[i-1][j],f[i-1][j+tmp]);
}
//以下是询问,对于每次询问,可以O(1)回答
while (m--)
{
scanf("%d%d",&x,&y);
printf("%d\n",query(r[x],r[y]));
}
}
小结一下
如果你有比较认真地看了到这里为止的这篇blog,那么你应该已经掌握了基础的LCA知识了
LCA的运用是很广的,可能和线段树的考频有的一比,不掌握是会吃亏的
上面的是比较普通的方法求LCA,其实……树上倍增也能求LCA!
接下来,就该讲讲这丧心病狂(其实挺简单)的树上倍增了
树上倍增
树上倍增又是什么东东?
先不说树上倍增,说实话倍增这个东东严格来说就是种思想,只可意会不可言传,咳咳……
好了该正经一点了——
倍增,是根据已经得到了的信息,将考虑的范围扩大,从而加速操作的一种思想
使用了倍增思想的算法有
- 归并排序
- 快速幂
- 基于ST表的RMQ算法
- 树上倍增找LCA等
- FFT、后缀数组等高级算法
我们在这篇blog中接下来探讨的的是树上倍增
(sa什么的我怎么可能会呢……)
some advantages
树上倍增说实话和RMQ确实是比较相似的,都采用了二进制的思想,所以复杂度往往比较低
当OI的tree类题目做的多了以后,你会发现树上倍增和树链剖分用的是很多的
在处理树上问题时,树上倍增和树链剖分都是十分玄妙的,但倍增有着显而易见的优点——
比起树链剖分,树上倍增代码短,查错方便,时空复杂度优( )
只是功能欠缺了一些不要太在意
即使如此,树上倍增也能够解决大部分的树形题目
(某些犇:我XX宁愿打倍增都TM不打树剖)
树上倍增(在线)求LCA
预处理
怎么样预处理以达到在线单次询问 的时间呢?
我们需要构造倍增数组!
(和RMQ相似地)设
表示i节点的第2^j个祖先(或父亲)(祖先英文ancestor)
(
数组开到
,因为树上任意一点最多有
个祖先)
明显地发现这个东西完全可以代替不路径压缩的并查集,
(若
则说明
的第
祖先不存在)
然后,倍增的性质(DP方程)就清楚地出来了
用文字来表达它就是这样的:
i的第 个父亲是i的第 个父亲的第 个父亲
神奇不?
这样就是说本来我们暴力求i的第k个祖先的时间复杂度是
,现在变成了
同时,用一个dfs处理出每个点的深度
和
,时间复杂度
这样预处理的总的时间复杂度即为
预处理code
procedure dfs(x,y:longint);
var
now:longint;
begin
now:=last[x];
while now<>0 do
begin
if b[now]<>y then
begin
anc[b[now],0]:=x;
depth[b[now]]:=depth[x]+1;
dfs(b[now],x);
end;
now:=next[now];
end;
end;
for j:=1 to trunc(ln(n)/ln(2)) do
for i:=1 to n do
anc[i,j]:=anc[anc[i,j-1],j-1];
询问
请务必认认真真地阅读以下内容否则树上倍增你就学不会了
树上倍增的运用中,最简单最常见的就是求
了
先来一个简单一点的问题吧:现在我们需要求出
,怎么用树上倍增做?
(其实我不告诉你你也不知道……hh)
好了认真一点,还记得我在前面说过的三种脑残 复杂度的求 的方法的第三种吗?
用dfs记录每个点的深度,求LCA时先调至统一深度,再一起向上跳
其实树上倍增运用的就是这个思想!只不过时间复杂度降至了飞快的
来看看具体如何实现——
对于两个节点u和v,我们先把u和v调至同一深度以求
(若此时的u=v,那么原来两点的LCA即为当前点,明显)
如果 但 ,就说明 在更“浅”的地方
我们同时把 和 向上跳 步( ),直到
明显地,这种方法肯定能求出
,因为
和
一定会相遇的嘛
倍增比那种脑残方法优的是,脑残方法一步一步向上跳,倍增一次跳
步!
但其实还存在着一些问题——如果这样跳,跳到了LCA的祖先节点怎么破?
所以如果
和
向上跳
步到达的节点相同,那我们就不跳,让
和
的第
祖先不同即可跳
注意一点就是下一次向上跳的距离是上一次跳的
(
),直到
,
即已被求出
这样使
和
与
的距离每次缩短一半,不难看出时间复杂度是
的
如何把
和
调至同一深度?和上面其实是一样的,先把较深的那个点调浅就行了
看不懂,见code,再理解理解
询问code
function lca(x,y:longint):longint;
var
k:longint;
begin
if depth[x]<depth[y] then swap(x,y);
k:=trunc(ln(depth[x]-depth[y]+1)/ln(2));
while k>=0 do
begin
if depth[anc[x,k]]>depth[y] then x:=anc[x,k];
dec(k);
end;
if depth[x]<>depth[y] then x:=anc[x,0];
k:=trunc(ln(d[x])/ln(2));
while k>=0 do
begin
if anc[x,k]<>anc[y,k] then
begin
x:=anc[x,k];
y:=anc[y,k];
end;
dec(k);
end;
exit(x);
end;
以上给出的三段code已经能解决所有关于树上倍增求 的地方了
整个树上倍增求LCA的时间复杂度为 ,已经很优秀了
树上倍增的真正意义
我在前面说过,在OI比赛里面直接求
是没有什么用处的
其实,树上倍增的真正可怕之处是这个倍增的思想
不信?好来看看这种很常见的题目
给你一棵树和两个点 和 ,求这两点间唯一路径的路径最大/小点权
明显我们是要求
和
的
,因为唯一路径必须经过
求
比如说用
算法,然后?暴力
?不可能的
这种问题用树上倍增还是能用
的时间解决
我们可以设 表示i到他的第 个祖先的路径最大值,就可以边求LCA边求出两点距离
因为 也符合:
服不服?
还有求两点的路径上路径和呢?
- 设
表示i到他的第
个祖先的路径和,同时也可边求
边求和,因为:
服不服?
看这才是树上倍增的真正意义所在,像ST算法求
,是无法解决这类问题的
至此树上倍增讲的已经差不多了,该看看例题了
例题1:JZOJsenior1738Heatwave
problem
Description 给你N个点的无向连通图,图中有M条边,第j条边的长度为: d_j. 现在有 K个询问。 每个询问的格式是:A
B,表示询问从A点走到B点的所有路径中,最长的边最小值是多少?Input
文件名为heatwave.in 第一行: N, M, K。 第2..M+1行: 三个正整数:X, Y, and D (1 <=
X <=N; 1 <= Y <= N). 表示X与Y之间有一条长度为D的边。 第M+2..M+K+1行: 每行两个整数A
B,表示询问从A点走到B点的所有路径中,最长的边最小值是多少?Output
对每个询问,输出最长的边最小值是多少。
Sample Input
6 6 8 1 2 5 2 3 4 3 4 3 1 4 8 2 5 7 4 6 2 1 2 1 3 1 4 2 3 2 4 5 1 6 2
6 1Sample Output
5 5 5 4 4 7 4 5
Data Constraint
50% 1<=N,M<=3000 其中30% K<=5000 100% 1 <= N <= 15,000 1 <= M <=
30,000 1 <= d_j <= 1,000,000,000 1 <= K <= 20,000Hint
我其实写过这道题的blog
这里更加详细地讲一讲真相是我懒得两边统一了
think about other issues
先不考虑什么算法or数据结构,先考虑一下如何搞定题目所给的问题
注意这句话——
从A点走到B点的所有路径中,最长的边的最小值是多少?
想到什么了吗?
MST!既然让最长的边最短,那么我们就求这棵树的最小生成树即可
这样就可以保证两点间最长边最短(明显),这里直接用kruskal+并查集撸过去就行了
(还有,不要告诉我你不知道kruskal……太过简单的就不讲了吧)
由于这里一个节点的出度可能很大,所以用邻接表存下即可
(我才不会告诉你我打pascal直接用p的动态长度数组犯贱呢……)
analysis
求完 后,这道题是不是就是道裸题?维护 和 即可
照样用一个dfs遍历整棵树,维护出 、 和 ,之后 预处理搞定之后,我们对每一对 求 (设u深度更深)注意求 过程中记录每一次的max值即
- 即为答案
时间复杂度为
code
动态数组版code
var
a,edge:array[0..15000]of array of longint;
anc,dis:array[0..15000,0..15]of longint;
b:array[0..30000,0..3]of longint;
depth,father:array[0..15000]of longint;
n,m,q,i,j,x,y:longint;
function max(x,y:longint):longint;
begin
if x>y then exit(x);
exit(y);
end;
procedure swap(var x,y:longint);
var
z:longint;
begin
z:=x;
x:=y;
y:=z;
end;
procedure qsort(l,r:longint);
var
i,j,mid:longint;
begin
i:=l;
j:=r;
mid:=b[(l+r)div 2,3];
repeat
while b[i,3]<mid do inc(i);
while b[j,3]>mid do dec(j);
if i<=j then
begin
b[0]:=b[i];
b[i]:=b[j];
b[j]:=b[0];
inc(i);
dec(j);
end;
until i>j;
if l<j then qsort(l,j);
if i<r then qsort(i,r);
end;
function getfather(x:longint):longint;
begin
if father[x]=x then exit(x);
father[x]:=getfather(father[x]);
exit(father[x]);
end;
procedure dfs(x,y:longint);
var
i:longint;
begin
for i:=1 to a[x,0] do
if a[x,i]<>y then
begin
anc[a[x,i],0]:=x;
depth[a[x,i]]:=depth[x]+1;
dis[a[x,i],0]:=edge[x,i];
dfs(a[x,i],x);
end;
end;
function lca(x,y:longint):longint;
var
k:longint;
begin
lca:=0;
if depth[x]<depth[y] then swap(x,y);
k:=trunc(ln(depth[x]-depth[y]+1)/ln(2));
while k>=0 do
begin
if depth[anc[x,k]]>depth[y] then
begin
lca:=max(lca,dis[x,k]);
x:=anc[x,k];
end;
dec(k);
end;
if depth[x]<>depth[y] then
begin
lca:=max(lca,dis[x,0]);
x:=anc[x,0];
end;
k:=trunc(ln(depth[x])/ln(2));
while k>=0 do
begin
if anc[x,k]<>anc[y,k] then
begin
lca:=max(max(lca,dis[x,k]),dis[y,k]);
x:=anc[x,k];
y:=anc[y,k];
end;
dec(k);
end;
if x=y then exit(lca);
exit(max(lca,max(dis[x,0],dis[y,0])));
end;
begin
readln(n,m,q);
for i:=1 to n do
begin
father[i]:=i;
setlength(a[i],1);
a[i,0]:=0;
setlength(edge[i],1);
edge[i,0]:=0;
end;
for i:=1 to m do
begin
for j:=1 to 3 do read(b[i,j]);
readln;
end;
qsort(1,m);
for i:=1 to m do
begin
x:=b[i,1];
y:=b[i,2];
if getfather(x)<>getfather(y)then
begin
father[getfather(x)]:=getfather(y);
inc(a[x,0]);
setlength(a[x],a[x,0]+1);
a[x,a[x,0]]:=y;
inc(edge[x,0]);
setlength(edge[x],edge[x,0]+1);
edge[x,a[x,0]]:=b[i,3];
inc(a[y,0]);
setlength(a[y],a[y,0]+1);
a[y,a[y,0]]:=x;
inc(edge[y,0]);
setlength(edge[y],edge[y,0]+1);
edge[y,a[y,0]]:=b[i,3];
end;
end;
depth[1]:=1;
dfs(1,0);
for j:=1 to trunc(ln(n)/ln(2)) do
for i:=1 to n do
begin
anc[i,j]:=anc[anc[i,j-1],j-1];
dis[i,j]:=max(dis[i,j-1],dis[anc[i,j-1],j-1]);
end;
for i:=1 to q do
begin
readln(x,y);
writeln(lca(x,y));
end;
end.
喜欢犯贱的我承认动态数组确实比邻接表好打还方便很多
但pascal分配动态地址要多花一些时间,所以上面这段code比下面邻接表的慢十几ms
邻接表版code
var
b,c,next,last,father,depth:array[0..50000]of longint;
anc,dis:array[0..15000,0..16]of longint;
a:array[0..30000,0..3]of longint;
n,m,q,i,j,x,y,tot:longint;
procedure swap(var x,y:longint);
var
z:longint;
begin
z:=x;
x:=y;
y:=z;
end;
function max(x,y:longint):longint;
begin
if x>y then exit(x);
exit(y);
end;
procedure qsort(l,r:longint);
var
i,j,mid:longint;
begin
i:=l;
j:=r;
mid:=a[(l+r)div 2,3];
repeat
while a[i,3]<mid do inc(i);
while a[j,3]>mid do dec(j);
if i<=j then
begin
a[0]:=a[i];
a[i]:=a[j];
a[j]:=a[0];
inc(i);
dec(j);
end;
until i>j;
if l<j then qsort(l,j);
if i<r then qsort(i,r);
end;
function getfather(x:longint):longint;
begin
if father[x]=x then exit(x);
father[x]:=getfather(father[x]);
exit(father[x]);
end;
function judge(x,y:longint):boolean;
begin
exit(getfather(x)=getfather(y));
end;
procedure insert(x,y,z:longint);
begin
inc(tot);
b[tot]:=y;
next[tot]:=last[x];
last[x]:=tot;
c[tot]:=z;
end;
procedure dfs(x,y:longint);
var
now:longint;
begin
now:=last[x];
while now<>0 do
begin
if b[now]<>y then
begin
anc[b[now],0]:=x;
depth[b[now]]:=depth[x]+1;
dis[b[now],0]:=c[now];
dfs(b[now],x);
end;
now:=next[now];
end;
end;
function lca(x,y:longint):longint;
var
k:longint;
begin
lca:=0;
if depth[x]<depth[y] then swap(x,y);
k:=trunc(ln(depth[x]-depth[y]+1)/ln(2));
while k>=0 do
begin
if depth[anc[x,k]]>depth[y] then
begin
lca:=max(lca,dis[x,k]);
x:=anc[x,k];
end;
dec(k);
end;
if depth[x]<>depth[y] then
begin
lca:=max(lca,dis[x,0]);
x:=anc[x,0];
end;
k:=trunc(ln(depth[x])/ln(2));
while k>=0 do
begin
if anc[x,k]<>anc[y,k] then
begin
lca:=max(max(lca,dis[x,k]),dis[y,k]);
x:=anc[x,k];
y:=anc[y,k];
end;
dec(k);
end;
if x=y then exit(lca);
exit(max(lca,max(dis[x,0],dis[y,0])));
end;
begin
readln(n,m,q);
for i:=1 to n do father[i]:=i;
for i:=1 to m do readln(a[i,1],a[i,2],a[i,3]);
qsort(1,m);
for i:=1 to m do
begin
if (not judge(a[i,1],a[i,2])) then
begin
insert(a[i,1],a[i,2],a[i,3]);
insert(a[i,2],a[i,1],a[i,3]);
father[getfather(a[i,1])]:=getfather(a[i,2]);
end;
end;
depth[1]:=1;
dfs(1,0);
for j:=1 to trunc(ln(n)/ln(2)) do
for i:=1 to n do
begin
anc[i,j]:=anc[anc[i,j-1],j-1];
dis[i,j]:=max(dis[i,j-1],dis[anc[i,j-1],j-1]);
end;
for i:=1 to q do
begin
readln(x,y);
writeln(lca(x,y));
end;
end.
例题2:JZOJsenior2753.【2012东莞市选】树(tree)
problem
Description
在这个问题中,给定一个值S和一棵树。在树的每个节点有一个正整数,问有多少条路径的节点总和达到S。路径中节点的深度必须是升序的。假设节点1是根节点,根的深度是0,它的儿子节点的深度为1。路径不必一定从根节点开始。
Input
第一行是两个整数N和S,其中N是树的节点数。 第二行是N个正整数,第i个整数表示节点i的正整数。 接下来的N-1行每行是2个整数x和y,表示y是x的儿子。
Output
输出路径节点总和为S的路径数量。
Sample Input
3 3 1 2 3 1 2 1 3
Sample Output
2
Data Constraint
Hint
对于30%数据,N≤100;
对于60%数据,N≤1000;
对于100%数据,N≤100000,所有权值以及S都不超过1000。
analysis
相信这题应该难不住你了吧
正解即为树上倍增+二分
首先还是一样,用
的时间预处理
数组
至于权值,你可以顺便维护
数组,但为何不用简单的前缀和呢?
剩下来的就比较简单了啦
枚举节点 ,二分一个 表示 的第 个祖先
然后通过 数组用 的时间求出 的第 个祖先是哪个节点(设为是第 号)
若 则 , 则
就刚好找到了此时累加答案即可
时间复杂度为
code
#include<bits/stdc++.h>
#define MAXN 100001
using namespace std;
int last[MAXN],next[MAXN],tov[MAXN];
int a[MAXN],value[MAXN],depth[MAXN];
int f[MAXN][21];
int n,s,tot,ans;
void insert(int x,int y)
{
next[++tot]=last[x];
last[x]=tot;
tov[tot]=y;
}
void dfs(int x)
{
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
value[j]=value[x]+a[j];
f[j][0]=x;
depth[j]=depth[x]+1;
dfs(j);
}
}
int find(int x,int k)
{
int t=0;
while (k)
{
if(k&1)x=f[x][t];
t++;k/=2;
}
return x;
}
bool judge(int x)
{
int left=0,right=depth[x];
while (left<=right)
{
int mid=(left+right)/2,temp=find(x,mid);
if (value[x]-value[temp]==s)
{
return 1;
}
else
{
if (value[x]-value[temp]>s)
{
right=mid-1;
}
else
{
left=mid+1;
}
}
}
return 0;
}
int main()
{
scanf("%d%d",&n,&s);
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
for (int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
insert(x,y);
}
depth[1]=1,value[1]=a[1];
dfs(1);
for (int j=1;j<=floor(log(n)/log(2));j++)
{
for (int i=1;i<=n;i++)
{
f[i][j]=f[f[i][j-1]][j-1];
}
}
for (int i=1;i<=n;i++)
{
if (judge(i))ans++;
}
printf("%d\n",ans);
return 0;
}
一些感想
树链剖分不会有时不要紧,树上倍增就基本是那种一定要会的知识了
总之,倍增是一种较为基础的处理树的算法,一定要熟练掌握并会运用它
(不然你能把正解看成暴力……)
接下来,即将进入丧心病狂(其实比树上倍增还简单)的树链剖分
当然,树链剖分也能求出LCA!(滑稽.jpg)
树链剖分
介绍一下树链剖分
如果你是那种零树剖基础的OI选手,到这里为止你应该还是不懂树剖的
简要地说一下吧——
树链剖分的思想是维护树上路径信息
树链剖分先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链
- 然后再通过数据结构(树状数组、SBT、splay、线段树等)来维护每一条链
以上就是树链剖分的思想了,很少
其实看完这篇blog,你就会发现哇树剖怎么这么简单-_-!
问题是怎么分轻重边?怎么维护?接着往下看吧
more advantages
既然树上倍增又好打又好调,你咋还用树链剖分?
no,树上倍增的用途还就真没有树链剖分那么广
不信?再来一类看起来也很常见的题目
在一棵树上进行路径的权值修改,询问路径权值和、路径权值极值
看起来简单,但不简单,线段树、树上倍增什么的根本解决不了
why?后两个操作我们当然可以用树上倍增做
关键是修改操作,树上倍增都预处理好了,怎么修改?重新预处理?
不可能的
服不服?
这时我们就要用到高级一点的算法——树链剖分了
由于我们使用数据结构来维护,所以路径权值什么的当然是可以修改的啦
树上倍增和树链剖分确实用的是很多的,但并不是说树上倍增就能真正完全通用
树剖的用处比树上倍增多得多,树上倍增能做的题树剖一定能做,反过来就不对了
树剖的代码复杂度不算特别高,调试也不难,且功能多多
所以在高级的OI比赛里,树剖是必备知识
普通的树剖?大概150lines+(逃)
注意树剖有三种:重链剖分、长链剖分和实虚链剖分
实虚链剖分其实就是LCT
所以我就分开来写好了正式进入树链剖分的学习
重链剖分
请务必认认真真地阅读以下内容否则重链剖分你就学不会了
重链剖分的一些概念
轻儿子和重儿子
设
表示以x为根的子树的节点个数
对于每个非叶子节点y,y的儿子所有中
最大的儿子就是重儿子(两个或以上都最大的话,任意选一个)
而y其它的儿子,都是轻儿子
(轻儿子通常没有用,我们可以不去考虑niao它)
重边、轻边和重链
重儿子与其父亲之间的路径,就是重边
不是重边的边均为轻边
多条重边相连为一条重链
图中灰色的点为重儿子,加粗的边是重边,其他没有加粗的是轻边
这棵树中的长度大于1的重链有两条:2—5和1—3—6—10
所有轻儿子可视作一个长度为1的重链
something about properties
若x是轻儿子,则有
从根到某一点的路径上,不超过 条重链,不超过 条轻链
(不需要证明~)
预处理、维护和在线操作
其实和着概念一起看剩下三种东东,树剖还就真挺水的连模板都不用背
接下来的数据结构维护我们使用简单一点的线段树
预处理
首先用一个dfs遍历,求出每个点的 、 、 以及重儿子heavy_son
其次再用一个dfs剖分,第二个dfs维护什么呢?
- 以优先走重边的原则,遍历出来一个dfs序
- 表示x位于的重链上的顶端节点编号
- to_tree 表示x在线段树中的位置
- 对于线段树中的第y个位置,to_num 为它对应的dfs序中的位置
剖分完以后,每一条重链相当于一个区间,我们维护处理完的数据结构即可
我们把上面那棵树剖分完以后线段树中的点顺序是这样的:
1,3,6,10,8,9,7,2,5,4 (加粗的点是在重链上的点)
在同一条重链上的点,在线段树的顺序中要连在一起(否则怎么维护呢)
剖树的时间复杂度
维护以及在线操作
change
因为已经搞过to_tree了,通过to_tree的标号在线段树中用单点修改即可
不要告诉我你不会单点修改:D
时间复杂度是线段树修改的
query
询问
到
路径的极值(或者路径和等等),和求
一样边向上跳边记录答案
这里记录的答案通过查询数据结构得到
关于时间复杂度嘛……
关于时间复杂度?
对于我们这些蒟蒻来说最关键的还是时间复杂度的问题
用最普通的数据结构线段树来维护树链剖分的时间复杂度是
后面那个 什么东西?
由于求LCA是
的时间,统计答案的过程中还要查询数据结构又是
的时间
所以每个查询的时间复杂度就是
了
平常来说,对于树剖,时间应该是比树上倍增那个
的预处理优的
所以你看树剖还是非常有用哒!
(其实用LCT维护可以做到
的时间还有LCT什么的你现在怎么可能会呢:D)
重链剖分(在线)求LCA
树剖当然也能求LCA啦!并且比倍增还好打耶!
具体如下
- 若 ,说明 和 不在同一条重链上
- 记 和 两点中top的深度较深的那个点为z
- 把 跳到
- 重复以上步骤,直到 和 在同一条重链上(即 )
- 此时 和 中 小的点即为
其实很好理解,跳到
的原因是让z进入另一条重链免得程序卡住
而选择
浅的点而不是本身浅的点是防止跳到重链头超过
不懂滚粗去想一想,倍增那么难你都听懂了树剖的你听不懂就(✺ω✺)了
说实话树剖求
真的很简单哪!你看倍增花了我好多时间来讲,呼——
那么时间复杂度
code
当然求 没有各种权,是不用打线段树的啦!
#include<cstdio>
#define MAXN 30001
using namespace std;
int last[3*MAXN],next[3*MAXN],tov[3*MAXN];
int n,m,tot;
struct information
{
int depth,father,size,heavy_son,top,to_tree,value;
}a[30001];
void insert(int x,int y)
{
next[++tot]=last[x];
last[x]=tot;
tov[tot]=y;
}
void dfs1(int x,int father)
{
int mx=-1;
a[x].size=1;
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=father)
{
a[j].depth=a[x].depth+1;
a[j].father=x;
a[x].size+=a[j].size;
dfs1(j,x);
if (a[j].size>mx)
{
mx=a[j].size;
a[x].heavy_son=j;
}
}
}
}
void dfs2(int x,int father,int k)
{
if (x==0)return;
a[x].top=k;
dfs2(a[x].heavy_son,x,k);
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=father && j!=a[x].heavy_son && j!=0)
{
dfs2(j,x,j);
}
}
}
int lca(int x,int y)
{
while (a[x].top!=a[y].top)
{
if (a[a[x].top].depth>a[a[y].top].depth)
x=a[a[x].top].father;
else y=a[a[y].top].father;
}
return a[x].depth<a[y].depth?x:y;
}
int main()
{
scanf("%d",&n);
for (int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
insert(x,y);
insert(y,x);
}
a[1].depth=1;
a[1].father=0;
dfs1(1,0);
dfs2(1,0,1);
scanf("%d",&m);
for (int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
return 0;
}
还是来看看例题吧——
例题1:JZOJsenior1738Heatwave
problem
题目上面有所以就不重新贴了
analysis
我说过了倍增能做的题树剖都能做的嘛,所以树剖当然也能做这道题
其实在树剖里面这题算是模板题,而且树链剖分还看得很清楚
求完了
后,我们还是照样把这棵
剖分一下,用线段树维护区间最大值
这里我们求的是边权而不是点权,我们就把边权放到儿子节点上看成点权就可以了
求
时我们是一条重链一条重链地向上跳,这里我们是一样的——
每次跳一条重链的时候,用线段树维护好的区间最大值查询这条重链的点权最大值
(还有不要告诉我你不会线段树)
套上求 的意义也就是这样——
- 若 ,记 和 两点中top的深度较深的那个点为z
- 把 跳到
最后的 即为答案,每个查询是 的时间复杂度
其实想一想,线段树是把每条重链当做区间来维护,意义十分显然
(注意一点就是求最大值时一定不要算上 的值)
code
#include<cstdio>
#include<algorithm>
#define MAXN 15001
#define INF 1000000007
using namespace std;
int last[4*MAXN],next[4*MAXN],tov[4*MAXN],dis[4*MAXN];
int to_num[4*MAXN],father[MAXN];
int n,m,k,tot,total;
struct information
{
int depth,father,size,heavy_son,top,to_tree,value;
}a[4*MAXN];
struct point
{
int mx,ans;
}tree[4*MAXN];
struct dist
{
int u,v,w;
}line[4*MAXN];
bool cmp(dist a,dist b)
{
return a.w<b.w;
}
int getfather(int x)
{
if (father[x]==0)return x;
father[x]=getfather(father[x]);
return father[x];
}
void insert(int x,int y,int z)
{
next[++tot]=last[x];
last[x]=tot;
tov[tot]=y;
dis[tot]=z;
}
void dfs1(int x,int father)
{
int mx=-1;
a[x].size=1;
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=father)
{
a[j].depth=a[x].depth+1;
a[j].father=x;
a[j].value=dis[i];
dfs1(j,x);
a[x].size+=a[j].size;
if (a[j].size>mx)
{
mx=a[j].size;
a[x].heavy_son=j;
}
}
}
}
void dfs2(int x,int father,int top)
{
if (x==0)return;
a[x].top=top;
a[x].to_tree=++total;
to_num[total]=x;
dfs2(a[x].heavy_son,x,top);
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=father && j!=a[x].heavy_son && j!=0)
{
dfs2(j,x,j);
}
}
}
void maketree(int t,int l,int r)
{
if (l==r)
{
tree[t].mx=a[to_num[l]].value;
return;
}
int mid=(l+r)/2;
maketree(t*2,l,mid);
maketree(t*2+1,mid+1,r);
tree[t].mx=max(tree[t*2].mx,tree[t*2+1].mx);
}
int tree_query_max(int t,int l,int r,int x,int y)
{
if (l==x && r==y)
{
return tree[t].mx;
}
int mid=(l+r)/2;
if (y<=mid)
{
return tree_query_max(t*2,l,mid,x,y);
}
else if (x>mid)
{
return tree_query_max(t*2+1,mid+1,r,x,y);
}
else return max(tree_query_max(t*2,l,mid,x,mid),tree_query_max(t*2+1,mid+1,r,mid+1,y));
}
int query_max(int x,int y)
{
int temp=-1;
while (a[x].top!=a[y].top)
{
if (a[a[x].top].depth>a[a[y].top].depth)
{
temp=max(temp,tree_query_max(1,1,n,a[a[x].top].to_tree,a[x].to_tree));
x=a[a[x].top].father;
}
else
{
temp=max(temp,tree_query_max(1,1,n,a[a[y].top].to_tree,a[y].to_tree));
y=a[a[y].top].father;
}
}
if (x==y)return temp;
if (a[x].depth<a[y].depth)
{
temp=max(temp,tree_query_max(1,1,n,a[a[x].heavy_son].to_tree,a[y].to_tree));
}
else
{
temp=max(temp,tree_query_max(1,1,n,a[a[y].heavy_son].to_tree,a[x].to_tree));
}
return temp;
}
int main()
{
//freopen("readin.txt","r",stdin);
scanf("%d%d%d",&n,&m,&k);
for (int i=1;i<=m;i++)
{
scanf("%d%d%d",&line[i].u,&line[i].v,&line[i].w);
}
sort(line+1,line+m+1,cmp);
for (int i=1;i<=m;i++)
{
if (getfather(line[i].u)!=getfather(line[i].v))
{
father[getfather(line[i].u)]=getfather(line[i].v);
insert(line[i].u,line[i].v,line[i].w);
insert(line[i].v,line[i].u,line[i].w);
}
}
a[1].depth=1;
dfs1(1,0),dfs2(1,0,1);
maketree(1,1,n);
for (int i=1;i<=k;i++)
{
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",query_max(x,y));
}
return 0;
}
一定读懂这段代码再接着看下面的例题
否则你会爽炸天
例题2:JZOJsenior3534、luoguP1967.【NOIP2013提高组day1】货车运输
problem
Description
A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路。每一条道路对车辆都有重量限制,简称限重。现在有 q
辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的情况下,最多能运多重的货物。Input
第一行有两个用一个空格隔开的整数 n,m,表示 A 国有 n 座城市和 m 条道路。
接下来 m 行每行 3 个整数 x、y、z,每两个整数之间用一个空格隔开,表示从 x 号城市到 y 号城市有一条限重为 z 的道路。注意:x
不等于 y,两座城市之间可能有多条道路。接下来一行有一个整数 q,表示有 q 辆货车需要运货。
接下来 q 行,每行两个整数 x、y,之间用一个空格隔开,表示一辆货车需要从 x 城市运输货物到 y 城市,注意:x 不等于 y。
Output
输出共有 q 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。如果货车不能到达目的地,输出-1。
Sample Input
4 3
1 2 4
2 3 3
3 1 1
3
1 3
1 4
1 3
Sample Output
3
-1
3
Data Constraint
对于 30%的数据,0 < n < 1,000,0 < m < 10,000,0 < q < 1,000;
对于 60%的数据,0 < n < 1,000,0 < m < 50,000,0 < q < 1,000;
对于 100%的数据,0 < n < 10,000,0 < m < 50,000,0 < q < 30,000,0 ≤ z ≤
100,000。
analysis
又是一个满满套路的 配树链剖分
就是不想用树上倍增
和上面heatwave不同的是,这里的原图不一定连通,且两城之间有多条边
虽说不一定连通,还是直接上一次最大生成树即可
多条边就把多条边中最大权值的边留下,其他的边不必管
剩下线段树维护点权最小值即和heatwave的一模一样
但必须注意一点:判断联通性的问题
我们不能只从1节点开始剖一次树,而是必须从所有未访问到的节点开始剖树
至于判断联通性,一开始我去做洛谷的时候,是在求
途中判断是否连通,但是WA了
其实除了kruskal用的一次并查集外,我们可以再用一次并查集,把
上的边再搞一次
需要访问
城市时只需询问
即可
code
#include<bits/stdc++.h>
#define MAXN 10001
#define MAXM 50001
using namespace std;
int last[2*MAXM],next[2*MAXM],tov[2*MAXM],dis[2*MAXM];
bool visited[MAXN],flag[MAXM];
int to_num[MAXN],father[MAXN],tree[4*MAXN];
int n,m,q,tot,total;
struct array
{
int x,y,z;
}dist[2*MAXM];
struct information
{
int depth,father,size,heavy_son,top,to_tree,value;
}a[MAXN];
bool cmp(array a,array b)
{
return a.z>b.z;
}
int min(int x,int y)
{
return x<y?x:y;
}
int getfather(int x)
{
if (father[x]==0)return x;
return father[x]=getfather(father[x]);
}
void insert(int x,int y,int z)
{
next[++tot]=last[x];
last[x]=tot;
tov[tot]=y;
dis[tot]=z;
}
void dfs1(int x,int father)
{
visited[x]=0;
a[x].size=1;
int mx=-1;
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=father)
{
if (visited[j]==0)
{
a[j].value+=dis[i];
continue;
}
a[j].depth=a[x].depth+1;
a[j].value=dis[i];
a[j].father=x;
dfs1(j,x);
a[x].size+=a[j].size;
if (a[j].size>mx)
{
a[x].heavy_son=j;
mx=a[j].size;
}
}
}
}
void dfs2(int x,int top)
{
if (x==0)return;
a[x].top=top;
a[x].to_tree=++total;
to_num[total]=x;
dfs2(a[x].heavy_son,top);
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=a[x].father && j!=a[x].heavy_son && j!=0)
{
dfs2(j,j);
}
}
}
void maketree(int t,int l,int r)
{
if (l==r)
{
tree[t]=a[to_num[l]].value;
return;
}
int mid=(l+r)/2;
maketree(2*t,l,mid),maketree(2*t+1,mid+1,r);
tree[t]=min(tree[2*t],tree[2*t+1]);
}
bool judge(int x,int y)
{
while (a[x].top!=a[y].top)
{
if ((x==a[x].top && a[x].father==0)||(y==a[y].top && a[y].father==0))return 0;
if (a[a[x].top].depth>a[a[y].top].depth)
{
x=a[a[x].top].father;
}
else
{
y=a[a[y].top].father;
}
}
return 1;
}
int tree_query_min(int t,int l,int r,int x,int y)
{
if (l==x && y==r)
{
return tree[t];
}
int mid=(l+r)/2;
if (y<=mid)
{
return tree_query_min(t*2,l,mid,x,y);
}
else if (x>mid)
{
return tree_query_min(t*2+1,mid+1,r,x,y);
}
else
{
return min(tree_query_min(t*2,l,mid,x,mid),tree_query_min(t*2+1,mid+1,r,mid+1,y));
}
}
int query_min(int x,int y)
{
int temp=1000000007;
while (a[x].top!=a[y].top)
{
if (a[a[x].top].depth>a[a[y].top].depth)
{
temp=min(temp,tree_query_min(1,1,n,a[a[x].top].to_tree,a[x].to_tree));
x=a[a[x].top].father;
}
else
{
temp=min(temp,tree_query_min(1,1,n,a[a[y].top].to_tree,a[y].to_tree));
y=a[a[y].top].father;
}
}
if (x==y)return temp;
if (a[x].depth<a[y].depth)
{
return min(temp,tree_query_min(1,1,n,a[a[x].heavy_son].to_tree,a[y].to_tree));
}
else
{
return min(temp,tree_query_min(1,1,n,a[a[y].heavy_son].to_tree,a[x].to_tree));
}
}
int main()
{
//freopen("read.txt","r",stdin);
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
{
scanf("%d%d%d",&dist[i].x,&dist[i].y,&dist[i].z);
}
sort(dist+1,dist+m+1,cmp);
for (int i=1;i<=m;i++)
{
int u=dist[i].x,v=dist[i].y,w=dist[i].z;
if (getfather(u)!=getfather(v))
{
father[getfather(u)]=v;
insert(u,v,w),insert(v,u,w);
flag[i]=1;
}
}
memset(visited,1,sizeof(visited));
for (int i=1;i<=n;i++)
if (visited[i])
{
a[i].depth=1;
a[i].father=0;
dfs1(i,0),dfs2(i,i);
}
maketree(1,1,n);
memset(father,0,sizeof(father));
for (int i=1;i<=m;i++)
if (flag[i])
{
int u=dist[i].x,v=dist[i].y;
father[getfather(u)]=getfather(v);
}
scanf("%d",&q);
while (q--)
{
int x,y;
scanf("%d%d",&x,&y);
if (getfather(x)==getfather(y))
{
printf("%d\n",query_min(x,y));
}
else printf("-1\n");
}
return 0;
}
例题3:JZOJsenior2256.【ZJOI2008】树的统计
problem
Description
一棵树上有n个节点,编号分别为1到n,每个节点都有一个权值w。 我们将以下面的形式来要求你对这棵树完成一些操作:
I. CHANGE u t : 把结点u的权值改为t II. QMAX u v: 询问从点u到点v的路径上的节点的最大权值
III. QSUM u v: 询问从点u到点v的路径上的节点的权值和 注意:从点u到点v的路径上的节点包括u和v本身Input
输入文件的第一行为一个整数n,表示节点的个数。 接下来n – 1行,每行2个整数a和b,表示节点a和节点b之间有一条边相连。
接下来n行,每行一个整数,第i行的整数wi表示节点i的权值。 接下来1行,为一个整数q,表示操作的总数。
接下来q行,每行一个操作,以“CHANGE u t”或者“QMAX u v”或者“QSUM u v”的形式给出。Output
对于每个“QMAX”或者“QSUM”的操作,每行输出一个整数表示要求输出的结果。
Sample Input
QMAX 3 4
QMAX 3 3
QMAX 3 2
QMAX 2 3
QSUM 3 4
QSUM 2 1
CHANGE 1 5
QMAX 3 4
CHANGE 3 6
QMAX 3 4
QMAX 2 4
QSUM 3 4Sample Output
4 1 2 2 10 6 5 6 5 16
Data Constraint
Hint
【数据说明】
对于100%的数据,保证1<=n<=30000,0<=q<=200000;中途操作中保证每个节点的权值w在-30000到30000之间。
analysis
这题还是树剖经典例题
看懂heatwave的树剖做法,这题就比较简单啦(码农彼此彼此)
这里的线段树要维护和查询两个值了,是区间最大值和区间和不要告诉我你还不会线段树
还是一样地,求
的同时用线段树查询整条重链的区间最大值和区间和
所以,我懒得再讲两种查询了因为这篇blog的树剖部分已经够长了
至于change操作,由于我们已经把每个点在线段树中的编号(to_tree)处理出来了
所以我们直接用线段树的单点修改就可以在线段树里面直接修改指定节点
修改完以后,推回父亲节点,更新线段树即可,change成功解决
change的时间复杂度
是不是看起来很简单哼哼你错了你能把代码打到150行以下我吃粑粑
code
我打个树剖能打250行我也是醉了
#include<cstdio>
#define MAXN 30001
#define INF 1000000007
using namespace std;
int last[3*MAXN],next[3*MAXN],tov[3*MAXN];
int to_num[MAXN];
int n,m,tot,total;
char st[10];
struct information
{
int depth,father,size,heavy_son,top,to_tree,value;
}a[MAXN];
struct point
{
int mx,ans;
}tree[MAXN*4];
int max(int x,int y)
{
return x>y?x:y;
}
void insert(int x,int y)
{
next[++tot]=last[x];
last[x]=tot;
tov[tot]=y;
}
void dfs1(int x,int father)
{
int mx=-1;
a[x].size=1;
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=father)
{
a[j].depth=a[x].depth+1;
a[j].father=x;
dfs1(j,x);
a[x].size+=a[j].size;
if (a[j].size>mx)
{
mx=a[j].size;
a[x].heavy_son=j;
}
}
}
}
void dfs2(int x,int father,int k)
{
if (x==0)return;
a[x].top=k;
a[x].to_tree=++total;
to_num[total]=x;
dfs2(a[x].heavy_son,x,k);
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=father && j!=a[x].heavy_son && j!=0)
{
dfs2(j,x,j);
}
}
}
void maketree(int t,int l,int r)
{
if (l==r)
{
tree[t].ans=tree[t].mx=a[to_num[l]].value;
return;
}
int mid=(l+r)/2;
maketree(t*2,l,mid);
maketree(t*2+1,mid+1,r);
tree[t].ans=tree[t*2].ans+tree[t*2+1].ans;
tree[t].mx=max(tree[t*2].mx,tree[t*2+1].mx);
}
void change(int t,int l,int r,int x,int y)
{
if (l==r)
{
tree[t].ans=tree[t].mx=y;
return;
}
int mid=(l+r)/2;
if (x<=mid)
{
change(t*2,l,mid,x,y);
}
else
{
change(t*2+1,mid+1,r,x,y);
}
tree[t].mx=max(tree[t*2].mx,tree[t*2+1].mx);
tree[t].ans=tree[t*2].ans+tree[t*2+1].ans;
}
int tree_query_max(int t,int l,int r,int x,int y)
{
if (l==x && r==y)
{
return tree[t].mx;
}
int mid=(l+r)/2;
if (y<=mid)
{
return tree_query_max(t*2,l,mid,x,y);
}
else if (x>mid)
{
return tree_query_max(t*2+1,mid+1,r,x,y);
}
else
{
return max(tree_query_max(t*2,l,mid,x,mid),tree_query_max(t*2+1,mid+1,r,mid+1,y));
}
}
int tree_query_sum(int t,int l,int r,int x,int y)
{
if (l==x && r==y)
{
return tree[t].ans;
}
int mid=(l+r)/2;
if (y<=mid)
{
return tree_query_sum(t*2,l,mid,x,y);
}
else if (x>mid)
{
return tree_query_sum(t*2+1,mid+1,r,x,y);
}
else
{
return tree_query_sum(t*2,l,mid,x,mid)+tree_query_sum(t*2+1,mid+1,r,mid+1,y);
}
}
int query_max(int x,int y)
{
int temp=-INF;
while (a[x].top!=a[y].top)
{
if (a[a[x].top].depth>a[a[y].top].depth)
{
temp=max(temp,tree_query_max(1,1,n,a[a[x].top].to_tree,a[x].to_tree));
x=a[a[x].top].father;
}
else
{
temp=max(temp,tree_query_max(1,1,n,a[a[y].top].to_tree,a[y].to_tree));
y=a[a[y].top].father;
}
}
if (a[x].depth<a[y].depth)
{
temp=max(temp,tree_query_max(1,1,n,a[x].to_tree,a[y].to_tree));
}
else
{
temp=max(temp,tree_query_max(1,1,n,a[y].to_tree,a[x].to_tree));
}
return temp;
}
int query_sum(int x,int y)
{
int temp=0;
while (a[x].top!=a[y].top)
{
if (a[a[x].top].depth>a[a[y].top].depth)
{
temp+=tree_query_sum(1,1,n,a[a[x].top].to_tree,a[x].to_tree);
x=a[a[x].top].father;
}
else
{
temp+=tree_query_sum(1,1,n,a[a[y].top].to_tree,a[y].to_tree);
y=a[a[y].top].father;
}
}
if (a[x].depth<a[y].depth)
{
temp+=tree_query_sum(1,1,n,a[x].to_tree,a[y].to_tree);
}
else
{
temp+=tree_query_sum(1,1,n,a[y].to_tree,a[x].to_tree);
}
return temp;
}
int main()
{
//freopen("readin.txt","r",stdin);
scanf("%d",&n);
for (int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
insert(x,y);
insert(y,x);
}
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i].value);
}
a[1].depth=1,a[1].father=0;
dfs1(1,0),dfs2(1,0,1);
maketree(1,1,n);
scanf("%d",&m);
for (int i=1;i<=m;i++)
{
int x,y;
scanf("%s %d %d",&st,&x,&y);
if (st[1]=='M')
{
printf("%d\n",query_max(x,y));
}
else if (st[1]=='S')
{
printf("%d\n",query_sum(x,y));
}
else
{
change(1,1,n,a[x].to_tree,y);
}
}
return 0;
}
例题4:JZOJsenior1175、bzoj2238.【IOI2008】生成树
problem
Description
给出一个N个点M条边的无向带权图,以及Q个询问,每次询问在图中删掉一条边后图的最小生成树。(各询问间独立,每次询问不对之后的询问产生影响,即被删掉的边在下一条询问中依然存在)
Input
第一行两个正整数N,M(N<=50000,M<=100000)表示原图的顶点数和边数。
下面M行,每行三个整数X,Y,W描述了图的一条边(X,Y),其边权为W(W<=10000)。保证两点之间至多只有一条边。
接着一行一个正整数Q,表示询问数。(1<=Q<=100000)
下面Q行,每行一个询问,询问中包含一个正整数T,表示把编号为T的边删掉(边从1到M按输入顺序编号)。Output
Q行,对于每个询问输出对应最小生成树的边权和的值,如果图不连通则输出“Not connected”
Sample Input
4 4 1 2 3 1 3 5 2 3 9 2 4 1 4 1 2 3 4
Sample Output
15 13 9 Not connected
Data Constraint
Hint
数据规模: 10%的数据N,M,Q<=100。 另外30%的数据,N<=1000 100%的数据如题目。
analysis
- 当然还有第四道例题啦!
(不要看到“IOI”什么的就不敢做了)
正解还是——MST+树链剖分
奇怪的是静态树上问题MST+树剖是不是万能的我去……
想一想即知,如果删去的边不在原图的
上,对原答案没有影响
但是如果删去了
上的边,那么其他的
树边一定不变
那此时要找的只是一条非
上的边,且要求最短,显然是在树链上
我们在求完
后,用树链剖分维护每条非MST树边能够使多少条MST树边删去后连通
说着很拗口,但操作还是很simple的,标记永久化一下即可
当然原图就是不连通的话,
个操作全都输出“Not connected”
时间复杂度为
code
#include<bits/stdc++.h>
#define MAXN 50001
#define MAXM 100001
using namespace std;
int last[2*MAXM],next[2*MAXM],tov[2*MAXM],dis[2*MAXM];
int father[MAXN],where[MAXM],to_num[MAXN],tree[4*MAXN],ref[2*MAXN];
int n,m,q,tot,total,ans;
int min(int x,int y)
{
return x<y?x:y;
}
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
struct information
{
int depth,father,size,heavy_son,top,to_tree,value;
}a[MAXN];
struct dist
{
int x,y,z,num;
bool flag;
}line[MAXM];
bool cmp(dist a,dist b)
{
return a.z<b.z;
}
bool cmp1(dist a,dist b)
{
return a.num<b.num;
}
int getfather(int x)
{
return !father[x]?x:father[x]=getfather(father[x]);
}
void insert(int x,int y,int z)
{
next[++tot]=last[x];
last[x]=tot;
tov[tot]=y;
dis[tot]=z;
}
void dfs1(int x,int father)
{
a[x].size=1;
int mx=-1;
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=father && j!=0)
{
a[j].depth=a[x].depth+1;
a[j].father=x;
ref[dis[i]]=j;
dfs1(j,x);
a[x].size+=a[j].size;
if (a[j].size>mx)
{
mx=a[j].size;
a[x].heavy_son=j;
}
}
}
}
void dfs2(int x,int top)
{
if (x==0)return;
a[x].top=top;
a[x].to_tree=++total;
to_num[total]=x;
dfs2(a[x].heavy_son,top);
for (int i=last[x];i;i=next[i])
{
int j=tov[i];
if (j!=a[x].father && j!=a[x].heavy_son && j!=0)
{
dfs2(j,j);
}
}
}
void change(int t,int l,int r,int x,int y,int z)
{
if (l==x && y==r)
{
tree[t]=min(tree[t],z);
return;
}
int mid=(l+r)/2;
if (y<=mid)
{
change(t*2,l,mid,x,y,z);
}
else if (x>mid)
{
change(t*2+1,mid+1,r,x,y,z);
}
else
{
change(t*2,l,mid,x,mid,z);
change(t*2+1,mid+1,r,mid+1,y,z);
}
//tree[t]=min(tree[t*2],tree[t*2+1]);
}
int query(int t,int l,int r,int x)
{
if (l==r)
{
return tree[t];
}
int mid=(l+r)/2;
if (x<=mid)return min(tree[t],query(t*2,l,mid,x));
else return min(tree[t],query(t*2+1,mid+1,r,x));
}
void modify(int x,int y,int z)
{
while (a[x].top!=a[y].top)
{
if (a[a[x].top].depth>a[a[y].top].depth)
{
change(1,1,n,a[a[x].top].to_tree,a[x].to_tree,z);
x=a[a[x].top].father;
}
else
{
change(1,1,n,a[a[y].top].to_tree,a[y].to_tree,z);
y=a[a[y].top].father;
}
}
if (x==y)return;
if (a[x].depth>a[y].depth)
{
change(1,1,n,a[a[y].heavy_son].to_tree,a[x].to_tree,z);
}
else
{
change(1,1,n,a[a[x].heavy_son].to_tree,a[y].to_tree,z);
}
}
int main()
{
//freopen("read.txt","r",stdin);
n=read(),m=read();
for (int i=1;i<=m;i++)
{
line[i].x=read(),line[i].y=read(),line[i].z=read();
line[i].num=i;
}
if (m<n-1)
{
q=read();
while (q--)printf("Not connected\n");
return 0;
}
sort(line+1,line+m+1,cmp);
for (int i=1;i<=m;i++)
{
int u=line[i].x,v=line[i].y,w=line[i].z;
if (getfather(u)!=getfather(v))
{
father[getfather(u)]=v;
line[i].flag=1;
ans+=w;
insert(u,v,line[i].num);
insert(v,u,line[i].num);
}
}
a[1].depth=1;
a[1].father=0;
dfs1(1,0),dfs2(1,1);
memset(tree,0x3f,sizeof(tree));
sort(line+1,line+m+1,cmp1);
for (int i=1;i<=m;i++)
{
if (!line[i].flag)
{
modify(line[i].x,line[i].y,line[i].z);
}
}
q=read();
while (q--)
{
int x=read();
if (!line[x].flag)
{
printf("%d\n",ans);
}
else
{
int xx=query(1,1,n,a[ref[x]].to_tree);
if(xx==0x3f3f3f3f)
{
printf("Not connected\n");
}
else
{
printf("%d\n",ans-line[x].z+xx);
}
}
}
return 0;
}
长链剖分
树剖大法好
最近做校内比赛,每次只要一看到树上问题,不假思索开码树剖
(打树剖打傻了么……)
不管怎样,树剖对付静态树问题基本都能很完美地解决
树剖用处确实非常非常多,不仅要熟练运用,还要做到触类旁通、用老知识解决新题目,才最重要
静态树问题……动态树问题呢?树链剖分GG
接下来,树剖2.0——Link-Cut Tree出场
(LCT明明比树剖高级多了)
LCT(Link-Cut Tree)
玄♂妙的动态树问题
静态树问题在前面的blog里已经见识过啦
就是树的形态不发生变化,只是树上的点权变化,用数据结构维护的问题罢了
但是树的形态如果会变化呢?(例如添、删树边操作等)
这就是动态树问题(Dynamic Tree Problem)了
然而Link-Cut Tree并不能吃
杨哲的《QTREE 解法的一些研究》你应该听说过
由于百度像csdn一样要XX币才能下载那些文档,就百度云共享一波啦
这里下载,uuq0
LCT(Link-Cut Tree),解决动态树问题的一种算法
LCT≈splay+树剖,是解决动态树问题的一把利器
核心思想就是借鉴树剖的“轻重边”等概念,用比线段树灵活的splay来维护一棵树(森林)
意会就好
(真相是tarjan老爷子是在发明LCT之后才发明的splay)
the most advantages
动态树问题通常要求你对一棵树进行切割和拼接等操作,然后再在上面维护传统数据结构可维护的值
树剖做不了的软肋在于重儿子和重链早就剖分(预处理)好了
怎么添、删边?暴力重构所有重儿子和整个线段树?
100%GG
呵呵当然不用Link-Cut Tree也是能勉强做某些动态树问题了(注意是某些)
如果只有添边或只有删边且不要求强制在线的题目,用离线+树剖+splay/动态开点线段树是可以做的
虽说树剖勉勉强强支持删边操作,但这毕竟不是最通用、最完美的做法
什么东西能真正完美地解决这一类动态树问题呢?
Link-Cut Tree算法!!!
而且LCT能做的除了动态树问题还有
任何树剖题
可支持删边的并查集
可做不用排序的kruskal(动态加边的MST并查集)
等……
我之前不是说过树上倍增能做的题树剖一定能做吗
而树链剖分能做的题LCT也一定能做
那么LCT的优点我都不必多说了,一切树上问题真正的多面手
嗯?LCA呢?
LCT是不可以求动态树上的LCA的!!!
为什么不能求?这个等下再说吧
来进入本blog最后的真·毒瘤——LCT
LCT之模样与概念
看看样子总是要的
这就是一个LCT
LCT维护的其实算是一个连通性森林
些许概念
- Preferred Child:重儿子,重儿子与父亲节点在同一棵splay中,一个节点至多一个重儿子
- Preferred Edge:重边,连接父亲节点和重儿子的边
- Preferred Path :重链,由重边及重边连接的节点构成的链
你看这个明显和树剖的东东一个鬼样所以可以略过
原来的那些名称都叫什么“偏好”XX的,感觉用树剖名词就可以了
接下来是LCT里一个独♂特的东西
- Auxiliary Tree:辅助树
由一条重链上的所有节点所构成的Splay称作这条链的辅助树
每个点的键值为这个点的深度,即这棵Splay的中序遍历是这条链从链顶到链底的所有节点构成的序列
- 辅助树的根节点的父亲指向链顶的父亲节点,然而链顶的父亲节点的儿子并不指向辅助树的根节点
- (也就是说父亲不认轻儿子只认重儿子,儿子都认父亲)
- 这条性质为后来的操作提供了依据
上面框框里面是上网copy的,不知道怎么解释
必须注意辅助树(splay)的根≠原树的根
首先不要把LCT 真的当成一棵树
LCT不是树!!!是个森林OK?LCA都求不了这叫做树么?
在同一棵splay里面当然可以暴力求LCA,但这有意义么?其次,为什么LCT里的点不认轻重儿子?
在一个splay里的点不就是父亲和重儿子和重儿子的重儿子……么?对上面的定义简单点说,在同一条“重链”里的所有点在一个splay中
不懂就看上面那个 自己理解,如 与 和 是在一个splay里的
这个明明很简单啊splay过程中,树的形态可以变化如
但是原树是不会变的
LCT还有个特点,就是不用记录节点编号
因为一个点的右儿子的深度是比它的大的,不像splay一样需要查找什么的:D其实更值得一提的是上面提到的虚边(也就是对应树剖的轻边)的那个东西
虚边 储存在一个splay的根节点, 为 在LCT里的父亲(即在不在当前辅助树里的那个)
而 则是splay中 的父亲,不要把这个搞混
例子:图中 而 , 而
同样自己去看图理解懒得多说可以这样想:两个重链是用一条虚边连接的,没有虚边不就成一条重链了么?
看完这些,其实LCT还是很简单的……
那么,不看懂概念怎么可以去学LCT的操作呢?
LCT之核心操作
请务必认认真真地阅读以下内容否则LCT你就学不会了
一句话说了N遍
操作1:access
access操作是LCT里最基础、最重要的操作
定义 操作就是把 的重儿子与 断开,然后把从整个LCT的根到 之间的路径都变为重边
无图费口水,有图不用嘴
辣莫access操作其实是很简单的
若现在要 节点 ,每一次把 旋转到当前辅助树的根
此时 的右儿子深度比 大,即右儿子是重儿子,我们自然而然的就要把它给删掉
把 连上 所在的辅助树(splay)里,然后
简单点说就是把 的右儿子作为辅助树的新根,并连一条向 的虚边
重复以上操作,直到
不简单吗?
不会的给我去面壁脑补去!
时间复杂度呢?
tarjan老爷子说,splay一次的时间复杂度是
整个 要进行几次splay呢?明显是向上跳虚边的次数
tarjan老爷子又说,最多会跳 次
所以 操作的时间复杂度是
以后任何对access操作时间复杂度有疑问的请去找tarjan老爷子喝茶
问我真没什么用
code
cc大神的LCT写的确实漂-亮的紧
我已经把他万恶的恶意缩行给搞掉了赏心悦目
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void update(int x)
{
if(x)
{
a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
}
}
void downdata(int x)
{
while (x)st[++st[0]]=x,x=fa[x];
while (st[0])down(st[st[0]--]);
}
//从x到辅助树的根的整条路径上的边都要下传翻转标记
//……其实也就是暴力下传标记
int lr(int x)
{
return t[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],k=lr(x);
t[y][k]=t[x][!k];
if(t[x][!k])fa[t[x][!k]]=y;
fa[x]=fa[y];
if(fa[y])t[fa[y]][lr(y)]=x;
t[x][!k]=y;
fa[y]=x;
pf[x]=pf[y];
update(y),update(x);
}
void splay(int x,int y)
{
//downdata(x);
while(fa[x]!=y)
{
if(fa[fa[x]]!=y)
{
if(lr(x)==lr(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
void access(int x)
{
for (int y=0;x;update(x),y=x,x=pf[x])
{
splay(x,0);
fa[t[x][1]]=0;
pf[t[x][1]]=x;
t[x][1]=y;
fa[y]=x;
pf[y]=0;
}
}
操作2:makeroot
makeroot操作和access操作是LCT的最核心的操作
其他操作视题而定,但makeroot和access必不可少
定义 操作就是将 所在的splay翻转,使 变为原LCT树的根
大概这样吧……
不算 的话 只有splay一次的时间复杂度为
makeroot不就更简单了?
首先 后,此时的 是辅助树中深度最大的点,接着把 旋转(splay)到LCT的根
而此时在辅助树中,除了 的其他点都是其父亲的左儿子
然后再splay区间翻转即可,打上lazy-tag标记
然后 才成功解决,如上图的那种样子
嗯?不会splay区间翻转?
给我滚去学!
code
void swap(int &x,int &y)
{
int z=x;
x=y;
y=z;
}
void reverse(int x)
{
if(x)
{
a[x].rev^=1;
swap(t[x][0],t[x][1]);
}
}
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void makeroot(int x)
{
access(x);
splay(x,0);
reverse(x);
}
操作3:link
link和cut操作嘛……所以LCT要叫Link-Cut Tree啊
定义 操作就是将节点 连到节点 的儿子
link太简单了不想放图
link操作就太简单了
后,此时 是原LCT的根
此时 没有 ,而从 连向 会冲掉原来 的
所以从 向 连一条虚边,即
code
void link(int x,int y)
{
makeroot(x);
pf[x]=y;
}
操作4:cut
定义 操作就是将节点 (x是y的儿子)与父亲 断开
cut太简单了也不想放图
cut和link其实很类似
且 后, 是原树的根,且 和 在同一splay里
然后 ,保证 是根,又由于 连通,所以此时 是 的右儿子
所以 就好了
code
void cut(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
t[x][1]=fa[y]=pf[y]=0;
update(x);
}
操作5:getroot
定义 为 节点当前辅助树的根
退化版并查集路径压缩
小学生级别难度
暴力把 向上跳,直到
没了
code
int getroot(int x)
{
while (fa[x])x=fa[x];
return x;
}
操作6:judge
定义 ,若 联通则为 ,否则为
退化版并查集集合查询
比 稍微难一丁点
后,x 为原树根,y打通到了最顶上的点(不一定是原树根)
若x,y间联通,则getroot(y)=x
所以询问getroot(y)=x?即可
code
bool judge(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
return getroot(y)==x;
}
Link-Cut Tree常规套路
和树剖的差不多,LCT的题目还是问你x点到y点间的路径路径和或者极值什么的
树剖很好做静态树的嘛
LCT怎么解决这类问题呢?
其实有常规套路
比如询问x到y间路径和
makeroot(y)后,再access(x),splay(x,0),此时x和y就在同一棵splay里了
然后这条路径不就任你摆布了?
注意事项
LCT可以求LCA……才怪……
不考虑LCT求LCA的话会发现Link-Cut Tree 很有某些优势
代码复杂度的排序,应该是倍增优、LCT稍劣一点、树剖很差
功能的排序当然是LCT最多、树剖少、倍增最少
-
突然发现LCT很好打、很好用?
时间复杂度的排序就是树剖最快、倍增较快、LCT龟速……
由于splay带来的巨大常数因子,LCT的时间大约是树剖的几倍……
比如这个
这是【ZJOI2008】树的统计
上面是树剖下面LCT,比对一下代码长度和时间即可
所以不要听tarjan那家伙BB说LCT时间很好
而且还有一个硬伤是求不了LCA
静态树上的两个点,LCA是唯一确定的,维护的值怎么变都没有影响LCA
splay的LCA是确定的么……
所以比赛里有动态树求LCA就赶快弃掉
LCT的点权、边权
由于LCT使用灵活的splay维护数据,维护点权易如反掌
怎么维护LCT的边权呢?
可能先想到的是采取树剖思想,边权看成儿子的点权
- LCT也可以这么做么?
不行
由于LCT的splay旋转,儿子旋转后就成为了父亲
那维护的权值不都乱套了?
解决方法很简单
把一条连接x,y的边看成一个点,再把这个点和x,y分别连起来
如此就可以维护LCT的边权了
必须注意要开两倍空间!
事实证明LCT非常好用
所以需要海量例题来让你刻骨铭心
例题1:BZOJ2049、luoguP2147.[SDOI2008]洞穴勘测
problem
题目描述
辉辉热衷于洞穴勘测。
某天,他按照地图来到了一片被标记为JSZX的洞穴群地区。经过初步勘测,辉辉发现这片区域由n个洞穴(分别编号为1到n)以及若干通道组成,并且每条通道连接了恰好两个洞穴。假如两个洞穴可以通过一条或者多条通道按一定顺序连接起来,那么这两个洞穴就是连通的,按顺序连接在一起的这些通道则被称之为这两个洞穴之间的一条路径。
洞穴都十分坚固无法破坏,然而通道不太稳定,时常因为外界影响而发生改变,比如,根据有关仪器的监测结果,123号洞穴和127号洞穴之间有时会出现一条通道,有时这条通道又会因为某种稀奇古怪的原因被毁。辉辉有一台监测仪器可以实时将通道的每一次改变状况在辉辉手边的终端机上显示:
如果监测到洞穴u和洞穴v之间出现了一条通道,终端机上会显示一条指令 Connect u v
如果监测到洞穴u和洞穴v之间的通道被毁,终端机上会显示一条指令 Destroy u v
经过长期的艰苦卓绝的手工推算,辉辉发现一个奇怪的现象:无论通道怎么改变,任意时刻任意两个洞穴之间至多只有一条路径。
因而,辉辉坚信这是由于某种本质规律的支配导致的。因而,辉辉更加夜以继日地坚守在终端机之前,试图通过通道的改变情况来研究这条本质规律。
然而,终于有一天,辉辉在堆积成山的演算纸中崩溃了……他把终端机往地面一砸(终端机也足够坚固无法破坏),转而求助于你,说道:“你老兄把这程序写写吧”。辉辉希望能随时通过终端机发出指令 Query u v,向监测仪询问此时洞穴u和洞穴v是否连通。现在你要为他编写程序回答每一次询问。
已知在第一条指令显示之前,JSZX洞穴群中没有任何通道存在。输入输出格式
输入格式: 第一行为两个正整数n和m,分别表示洞穴的个数和终端机上出现过的指令的个数。
以下m行,依次表示终端机上出现的各条指令。每行开头是一个表示指令种类的字符串s(”Connect”、”Destroy”或者”Query”,区分大小写),之后有两个整数u和v
(1≤u, v≤n且u≠v) 分别表示两个洞穴的编号。输出格式: 对每个Query指令,输出洞穴u和洞穴v是否互相连通:是输出”Yes”,否则输出”No”。(不含双引号)
输入输出样例
输入样例#1: 复制 200 5 Query 123 127 Connect 123 127 Query 123 127 Destroy
127 123 Query 123 127 输出样例#1: 复制 No Yes No 输入样例#2: 复制 3 5 Connect 1 2
Connect 3 1 Query 2 3 Destroy 1 3 Query 2 3 输出样例#2: 复制 Yes No 说明数据说明
10%的数据满足n≤1000, m≤20000
20%的数据满足n≤2000, m≤40000
30%的数据满足n≤3000, m≤60000
40%的数据满足n≤4000, m≤80000
50%的数据满足n≤5000, m≤100000
60%的数据满足n≤6000, m≤120000
70%的数据满足n≤7000, m≤140000
80%的数据满足n≤8000, m≤160000
90%的数据满足n≤9000, m≤180000
100%的数据满足n≤10000, m≤200000
保证所有Destroy指令将摧毁的是一条存在的通道
本题输入、输出规模比较大,建议c\c++选手使用scanf和printf进行I\O操作以免超时
analysis
……智障题
这题是最普通的link、cut操作和判断联通
makeroot(x),access(y)后,判断getroot(y)=x?即可
不要告诉我说不知道怎么link和cut
code
#include<bits/stdc++.h>
#define MAXN 30001
using namespace std;
int t[MAXN][2];
int b[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
char s[10];
int n,m;
struct node
{
int val,sum,mx,size;
bool rev;
}a[MAXN];
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void reverse(int x)
{
if(x)
{
a[x].rev^=1;
swap(t[x][0],t[x][1]);
}
}
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void update(int x)
{
if (x)
{
a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
}
}
void downdata(int x)
{
st[0]=0;
while (x)st[++st[0]]=x,x=fa[x];
while (st[0])down(st[st[0]--]);
}
int lr(int x)
{
return t[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],k=lr(x);
t[y][k]=t[x][!k];
if (t[x][!k])fa[t[x][!k]]=y;
fa[x]=fa[y];
if (fa[y])t[fa[y]][lr(y)]=x;
t[x][!k]=y;
fa[y]=x,pf[x]=pf[y];
update(y),update(x);
}
void splay(int x, int y)
{
downdata(x);
while (fa[x]!=y)
{
if (fa[fa[x]]!=y)
{
if (lr(x)==lr(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
void access(int x)
{
for (int y=0;x;update(x),y=x,x=pf[x])
{
splay(x,0);
fa[t[x][1]]=0;
pf[t[x][1]]=x;
t[x][1]=y;
fa[y]=x;
pf[y]=0;
}
}
void makeroot(int x)
{
access(x);
splay(x,0);
reverse(x);
}
void link(int x,int y)
{
makeroot(x);
pf[x]=y;
}
void cut(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
t[x][1]=fa[y]=pf[y]=0;
update(x);
}
int getroot(int x)
{
while (fa[x])x=fa[x];
return x;
}
bool judge(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
return getroot(y)==x;
}
int main()
{
n=read(),m=read();
while (m--)
{
scanf("%s",&s);
int x=read(),y=read();
if (s[0]=='C')link(x,y);
else if (s[0]=='D')cut(x,y);
else (judge(x,y))?(printf("Yes\n")):(printf("No\n"));
}
}
例题2:luoguP3203. [HNOI2010]弹飞绵羊
problem
题目描述
某天,Lostmonkey发明了一种超级弹力装置,为了在他的绵羊朋友面前显摆,他邀请小绵羊一起玩个游戏。游戏一开始,Lostmonkey在地上沿着一条直线摆上n个装置,每个装置设定初始弹力系数ki,当绵羊达到第i个装置时,它会往后弹ki步,达到第i+ki个装置,若不存在第i+ki个装置,则绵羊被弹飞。绵羊想知道当它从第i个装置起步时,被弹几次后会被弹飞。为了使得游戏更有趣,Lostmonkey可以修改某个弹力装置的弹力系数,任何时候弹力系数均为正整数。
输入输出格式
输入格式: 第一行包含一个整数n,表示地上有n个装置,装置的编号从0到n-1。
接下来一行有n个正整数,依次为那n个装置的初始弹力系数。
第三行有一个正整数m,
接下来m行每行至少有两个数i、j,若i=1,你要输出从j出发被弹几次后被弹飞,若i=2则还会再输入一个正整数k,表示第j个弹力装置的系数被修改成k。
输出格式: 对于每个i=1的情况,你都要输出一个需要的步数,占一行。
输入输出样例
输入样例#1: 复制 4 1 2 1 1 3 1 1 2 1 1 1 1 输出样例#1: 复制 2 3 说明
对于20%的数据n,m<=10000,对于100%的数据n<=200000,m<=100000
analysis
LCT板子题
我们可以建一个n+1号节点,x号节点连到表示n+1号节点就表示被弹飞
若要更改,直接先cut再link一波,关键怎么询问弹飞几次呢?暴力splay?
我们可以makeroot(n+1),使n+1号节点变为原LCT的根
接着我们access(x)后splay(x,0),也就是把x旋转到原树的根
那么x号节点会弹xsize−1次被弹飞,但是为什么呢?
由于辅助树按照深度关键字排序,所以x的子树大小-1就是要被弹飞几次了
所以明显正确
code
#include<stdio.h>
#include<string.h>
#define MAXN 200001
using namespace std;
int t[MAXN][2];
int b[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
int n,m;
struct node
{
int size,rev;
}a[MAXN];
int min(int x,int y)
{
return x<y?x:y;
}
void swap(int &x,int &y)
{
int z=x;
x=y;
y=z;
}
void reverse(int x)
{
if(x)
{
a[x].rev^=1;
swap(t[x][0],t[x][1]);
}
}
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void update(int x)
{
if(x)
{
a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
}
}
void downdata(int x)
{
while (x)st[++st[0]]=x,x=fa[x];
while (st[0])down(st[st[0]--]);
}
int lr(int x)
{
return t[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],k=lr(x);
t[y][k]=t[x][!k];
if(t[x][!k])fa[t[x][!k]]=y;
fa[x]=fa[y];
if(fa[y])t[fa[y]][lr(y)]=x;
t[x][!k]=y;
fa[y]=x;
pf[x]=pf[y];
update(y),update(x);
}
void splay(int x, int y)
{
downdata(x);
while(fa[x]!=y)
{
if(fa[fa[x]]!=y)
{
if(lr(x)==lr(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
void access(int x)
{
for (int y=0;x;update(x),y=x,x=pf[x])
{
splay(x,0);
fa[t[x][1]]=0;
pf[t[x][1]]=x;
t[x][1]=y;
fa[y]=x;
pf[y]=0;
}
}
void makeroot(int x)
{
access(x);
splay(x,0);
reverse(x);
}
void link(int x,int y)
{
makeroot(x);
pf[x]=y;
}
void cut(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
t[x][1]=fa[y]=pf[y]=0;
update(x);
}
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%d",&b[i]);
link(i,min(n+1,i+b[i]));
}
scanf("%d",&m);
while (m--)
{
int z,x;
scanf("%d%d",&z,&x),x++;
if (z==1)
{
makeroot(n+1);
access(x);
splay(x,0);
printf("%d\n",a[x].size-1);
}
else
{
cut(x,min(x+b[x],n+1));
scanf("%d",&b[x]);
link(x,min(x+b[x],n+1));
}
}
}
例题3:JZOJ2256.【ZJOI2008】树的统计
problem
题目上面有所以就不重新贴了
analysis
前面是用树剖做的,现在用LCT重做了一次
其实LCT的代码复杂度简直秒杀树剖好伐都说了LCT能做所有树剖题,然而这题连cut操作都没有
-
那不就简单了?
像普通套路一样,询问x,y间的路径极值或权值和,就makeroot(y),access(x),splay(x,0)
然后直接输出a[x].XXX就好了
至于修改x节点的操作,把x旋到根,直接修改即可
-
然而WA90
为什么呢?
每次输入的时候,都要把当前节点旋到根,读入后再update!!!
这样才终于AC了
code
#include<bits/stdc++.h>
#define MAXN 30001
using namespace std;
int t[MAXN][2];
int b[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
char s[10];
int n,m;
struct node
{
int val,sum,mx,size,rev;
}a[MAXN];
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void reverse(int x)
{
if(x)
{
a[x].rev^=1;
swap(t[x][0],t[x][1]);
}
}
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void update(int x)
{
if (x)
{
a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
a[x].sum=a[x].val+a[t[x][0]].sum+a[t[x][1]].sum;
a[x].mx=max(a[x].val,max(a[t[x][0]].mx,a[t[x][1]].mx));
}
}
void downdata(int x)
{
st[0]=0;
while (x)st[++st[0]]=x,x=fa[x];
while (st[0])down(st[st[0]--]);
}
int lr(int x)
{
return t[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],k=lr(x);
t[y][k]=t[x][!k];
if (t[x][!k])fa[t[x][!k]]=y;
fa[x]=fa[y];
if (fa[y])t[fa[y]][lr(y)]=x;
t[x][!k]=y;
fa[y]=x,pf[x]=pf[y];
update(y),update(x);
}
void splay(int x, int y)
{
downdata(x);
while (fa[x]!=y)
{
if (fa[fa[x]]!=y)
{
if (lr(x)==lr(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
void access(int x)
{
for (int y=0;x;update(x),y=x,x=pf[x])
{
splay(x,0);
fa[t[x][1]]=0;
pf[t[x][1]]=x;
t[x][1]=y;
fa[y]=x;
pf[y]=0;
}
}
void makeroot(int x)
{
access(x);
splay(x,0);
reverse(x);
}
void link(int x,int y)
{
makeroot(x);
pf[x]=y;
}
int main()
{
n=read();
for (int i=1;i<n;i++)link(read(),read());
a[0].mx=-2147483647,a[0].sum=a[0].val=0;
for (int i=1;i<=n;i++)
{
splay(i,0);
a[i].mx=a[i].sum=a[i].val=read();
update(i);
}
m=read();
while (m--)
{
scanf("%s",&s);
int x=read(),y=read();
if (s[1]=='M')//max
{
makeroot(y);
access(x);
splay(x,0);
printf("%d\n",a[x].mx);
}
else if (s[1]=='S')//sum
{
makeroot(y);
access(x);
splay(x,0);
printf("%d\n",a[x].sum);
}
else //change
{
splay(x,0);
a[x].val=y;
update(x);
}
}
}
例题4:BZOJ3282、luoguP3690.【模板】Link Cut Tree (动态树)
problem
题目背景
动态树
题目描述
给定n个点以及每个点的权值,要你处理接下来的m个操作。操作有4种。操作从0到3编号。点从1到n编号。
0:后接两个整数(x,y),代表询问从x到y的路径上的点的权值的xor和。保证x到y是联通的。
1:后接两个整数(x,y),代表连接x到y,若x到y已经联通则无需连接。
2:后接两个整数(x,y),代表删除边(x,y),不保证边(x,y)存在。
3:后接两个整数(x,y),代表将点x上的权值变成y。
输入输出格式
输入格式: 第1行两个整数,分别为n和m,代表点数和操作数。
第2行到第n+1行,每行一个整数,整数在[1,10^9]内,代表每个点的权值。
第n+2行到第n+m+1行,每行三个整数,分别代表操作类型和操作所需的量。
输出格式: 对于每一个0号操作,你须输出x到y的路径上点权的xor和。
输入输出样例
输入样例#1: 复制 3 3 1 2 3 1 1 2 0 1 2 0 1 1 输出样例#1: 复制 3 1 说明
数据范围: 1 \leq N, M \leq 3 \cdot {10}^5 1≤N,M≤3⋅10 5
analysis
还是LCT板子题
差不多是板子在手天下我有注意两点就好
link,cut的两个点不一定联通,用judge特判一下
还有就是异或和的话a[i].sum=(a[i].val)xor(a[t[i][0]].sum)xor(a[t[i][1]].sum)
如此简单
code
#include<bits/stdc++.h>
#define MAXN 300001
using namespace std;
int t[MAXN][2];
int b[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
char s[10];
int n,m;
struct node
{
int val,sum,mx,size;
bool rev;
}a[MAXN];
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void reverse(int x)
{
if(x)
{
a[x].rev^=1;
swap(t[x][0],t[x][1]);
}
}
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void update(int x)
{
if (x)
{
a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
a[x].sum=a[x].val^a[t[x][0]].sum^a[t[x][1]].sum;
}
}
void downdata(int x)
{
st[0]=0;
while (x)st[++st[0]]=x,x=fa[x];
while (st[0])down(st[st[0]--]);
}
int lr(int x)
{
return t[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],k=lr(x);
t[y][k]=t[x][!k];
if (t[x][!k])fa[t[x][!k]]=y;
fa[x]=fa[y];
if (fa[y])t[fa[y]][lr(y)]=x;
t[x][!k]=y;
fa[y]=x,pf[x]=pf[y];
update(y),update(x);
}
void splay(int x, int y)
{
downdata(x);
while (fa[x]!=y)
{
if (fa[fa[x]]!=y)
{
if (lr(x)==lr(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
void access(int x)
{
for (int y=0;x;update(x),y=x,x=pf[x])
{
splay(x,0);
fa[t[x][1]]=0;
pf[t[x][1]]=x;
t[x][1]=y;
fa[y]=x;
pf[y]=0;
}
}
void makeroot(int x)
{
access(x);
splay(x,0);
reverse(x);
}
int getroot(int x)
{
while (fa[x])x=fa[x];
return x;
}
bool judge(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
return getroot(y)==x;
}
void link(int x,int y)
{
if (!judge(x,y))
{
makeroot(x);
pf[x]=y;
}
}
void cut(int x,int y)
{
if (judge(x,y))
{
makeroot(x);
access(y);
splay(x,0);
t[x][1]=fa[y]=pf[y]=0;
update(x);
}
}
int main()
{
//freopen("readin.txt","r",stdin);
n=read(),m=read();
a[0].val=a[0].sum=0;
for (int i=1;i<=n;i++)
{
splay(i,0);
a[i].val=read();
update(i);
}
while (m--)
{
int z=read(),x=read(),y=read();
if (z==0)
{
makeroot(y);
access(x);
splay(x,0);
printf("%d\n",a[x].sum);
}
else if (z==1)link(x,y);
else if (z==2)cut(x,y);
else
{
splay(x,0);
a[x].val=y;
update(x);
}
}
}
上面的都是裸的LCT题目,直接维护
所以接下来的例题不会再有裸题
例题5:JZOJ3754、luoguP2387.【NOI2014】魔法森林
problem
Description
为了得到书法大家的真传,小 E 同学下定决心去拜访住在魔法森林中的隐士。魔法森林可以被看成一个包含 n 个节点 m
条边的无向图,节点标号为1,2,3, … , n,边标号为 1,2,3, … , m。初始时小 E 同学在 1 号节点,隐士则住在 n
号节点。小 E 需要通过这一片魔法森林,才能够拜访到隐士。魔法森林中居住了一些妖怪。每当有人经过一条边的时候,这条边上的妖怪就会对其发起攻击。 幸运的是, 在 1 号节点住着两种守护精灵: A
型守护精灵与B 型守护精灵。小 E 可以借助它们的力量,达到自己的目的。只要小 E 带上足够多的守护精灵, 妖怪们就不会发起攻击了。具体来说, 无向图中的每一条边 ei 包含两个权值 ai 与 bi 。
若身上携带的 A 型守护精灵个数不少于 ai ,且 B 型守护精灵个数不少于 bi
,这条边上的妖怪就不会对通过这条边的人发起攻击。当且仅当通过这片魔法森林的过程中没有任意一条边的妖怪向小 E 发起攻击,他才能成功找到隐士。由于携带守护精灵是一件非常麻烦的事,小 E 想要知道, 要能够成功拜访到隐士,最少需要携带守护精灵的总个数。守护精灵的总个数为 A
型守护精灵的个数与 B 型守护精灵的个数之和。Input
输入文件的第 1 行包含两个整数 n, m,表示无向图共有 n 个节点, m 条边。
接下来 m 行,第 i + 1 行包含 4 个正整数 Xi, Yi, ai, bi, 描述第 i
条无向边。其中Xi与Yi为该边两个端点的标号,ai与bi的含义如题所述。注意数据中可能包含重边与自环。
Output
输出一行一个整数:如果小 E 可以成功拜访到隐士,输出小 E 最少需要携带的守护精灵的总个数;如果无论如何小 E
都无法拜访到隐士,输出“-1” (不含引号) 。Sample Input
【样例输入 1】
4 5
1 2 19 1
2 3 8 12
2 4 12 15
1 3 17 8
3 4 1 17
【样例输入 2】
3 1
1 2 1 1
Sample Output
【样例输出 1】
32
【样例输出 2】
-1
Data Constraint
Hint
【样例说明 1】
如果小 E 走路径 1→2→4,需要携带 19+15=34 个守护精灵;
如果小 E 走路径 1→3→4,需要携带 17+17=34 个守护精灵;
如果小 E 走路径 1→2→3→4,需要携带 19+17=36 个守护精灵;
如果小 E 走路径 1→3→2→4,需要携带 17+15=32 个守护精灵。
综上所述,小 E 最少需要携带 32 个守护精灵。
【样例说明 2】
小 E 无法从 1 号节点到达 3 号节点,故输出-1。
analysis
好像MST的样子,但思考一下就可以发现kruskal是错的
正解LCT,但听说SPFA也可AC?
维护动态增删边MST就可以了
注意此处维护边权,就按平常套路,把边看成点连起来,边权看成点权
首先将a升序排序,保证a最优,接下来维护b的MST,怎么弄呢?
我们重新定义一下a[i].max,为i节点子树内权值最大的点的标号
如果当前第j条边的b小于当前LCT里面b最大的边,我们不就可以把这条边换掉了?
通过查找querymax(x,y)来查找并切断那一条较劣的边即可
然后中途只要1和n是联通的,就统计答案ans=min(f[i].a+a[querymax(1,n)].val)
搞定
code
#include<bits/stdc++.h>
#define MAXN 200001
#define MAXM 500001
#define INF 1000000007
using namespace std;
int t[MAXN][2];
int fat[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
char s[10];
int n,m,ans;
struct node
{
int val,mx,size;
bool rev;
}a[MAXN];
struct edge
{
int x,y,a,b;
}f[MAXM];
bool cmp(edge x,edge y)
{
return x.a<y.a;
}
int getfa(int x)
{
return !fat[x]?x:fat[x]=getfa(fat[x]);
}
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void reverse(int x)
{
if(x)
{
a[x].rev^=1;
swap(t[x][0],t[x][1]);
}
}
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void update(int x)
{
if (x)
{
a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
a[x].mx=x;
if (a[a[t[x][0]].mx].val>a[a[x].mx].val)a[x].mx=a[t[x][0]].mx;
if (a[a[t[x][1]].mx].val>a[a[x].mx].val)a[x].mx=a[t[x][1]].mx;
}
}
void downdata(int x)
{
st[0]=0;
while (x)st[++st[0]]=x,x=fa[x];
while (st[0])down(st[st[0]--]);
}
int lr(int x)
{
return t[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],k=lr(x);
t[y][k]=t[x][!k];
if (t[x][!k])fa[t[x][!k]]=y;
fa[x]=fa[y];
if (fa[y])t[fa[y]][lr(y)]=x;
t[x][!k]=y;
fa[y]=x,pf[x]=pf[y];
update(y),update(x);
}
void splay(int x, int y)
{
downdata(x);
while (fa[x]!=y)
{
if (fa[fa[x]]!=y)
{
if (lr(x)==lr(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
void access(int x)
{
for (int y=0;x;update(x),y=x,x=pf[x])
{
splay(x,0);
fa[t[x][1]]=0;
pf[t[x][1]]=x;
t[x][1]=y;
fa[y]=x;
pf[y]=0;
}
}
void makeroot(int x)
{
access(x);
splay(x,0);
reverse(x);
}
int getroot(int x)
{
while (fa[x])x=fa[x];
return x;
}
bool judge(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
return getroot(y)==x;
}
void link(int x,int y)
{
makeroot(x);
pf[x]=y;
}
void cut(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
t[x][1]=fa[y]=pf[y]=0;
update(x);
}
int query_max(int x,int y)
{
makeroot(y);
access(x);
splay(x,0);
return a[x].mx;
}
int main()
{
n=read(),m=read();
for (int i=1;i<=m;i++)f[i].x=read(),f[i].y=read(),f[i].a=read(),f[i].b=read();
sort(f+1,f+m+1,cmp);
ans=INF;
for (int i=1;i<=m;i++)
{
int x=f[i].x,y=f[i].y;
if (getfa(x)==getfa(y))
{
int temp=query_max(x,y);
if (f[i].b<a[temp].val)cut(temp,f[temp-n].x),cut(temp,f[temp-n].y);
else
{
if (getfa(1)==getfa(n))ans=min(ans,f[i].a+a[query_max(1,n)].val);
continue;
}
}
else fat[getfa(x)]=getfa(y);
link(x,n+i),link(y,n+i);
a[n+i].mx=n+i,a[n+i].val=f[i].b;
if (getfa(1)==getfa(n))ans=min(ans,f[i].a+a[query_max(1,n)].val);
}
(ans==INF)?(printf("-1\n")):(printf("%d\n",ans));
return 0;
}
例题6:JZOJ5466.【NOIP2017提高A组冲刺11.9】玩游戏
problem
Description
小A得了忧郁综合症,小B正在想办法开导她。
机智的小B决定陪着小A玩游戏,他从魔法的世界里变出一张无向联通图,每条边上都有边权。小B定义一条路径的权值为所有经过边中的最大权值,小A则定义两点的最短路径为所有路径中权值最小的路径权。
每次小A和小B会选出k对点mi_1,mi_2,分别计算出mi_1,mi_2的最短路径ti,然后小B会拿出k堆灵魂宝石,每堆有ti个。然后小A先从一堆中选出若干个灵魂宝石拿走,接下来小B重复同样的操作,如此反复,直到取走最后一颗灵魂宝石,然后取走最后一颗宝石的人获胜。
小B认为这样游戏太简单,于是他会不定期向这张图上加上一些边,以增大游戏难度。
小A具有预知未来的能力,她看到了自己和小B在未来游戏中的选择,以及小B增加的边。现在对于每次游戏,小A想知道自己是否存在必胜的方法。但是预知未来已经消耗了她太多精力,出于疲惫她只好找到了你。Input
第一行三个数N和M和K,表示这张无向图初始的点数与边数,以及每次询问的点对的个数;
接下来M行,每行三个数u,v,q,表示点u和点v之间存在一条权值为q的 边; 接下来一行一个数Q,表示操作总数;
接下来Q行,表示操作,每行格式为下面两条中的一条:
1.add u v q:表示在u与v之间加上一条边权为q的边;
2.game m1_1 m1_2 … mk_1 mk_2:表示一次游戏中选择的k对点。 数据保证1≤u,v,mi_1,mi_2≤n,1≤q,mi_1≠mi_2Output
对于每个game输出一行,若小A存在必胜策略,则输出“madoka”,否则输出“Baozika”,以回车结尾
Sample Input
5 6 2 1 2 3 2 3 6 4 2 4 5 3 5 3 4 5 5 1 5 4 game 1 3 4 3 game 1 5 2 4
add 2 5 4 game 1 5 3 4Sample Output
Baozika madoka madoka
Data Constraint
Hint
样例解释: 对于第一轮,1至3的最短路径为5,4至3的最短路径为5 对于第二轮,1至5的最短路径为5,2至4的最短路径为4
再看加边之后: 对于第三轮,1至5的最短路径为4,3至4的最短路径为5
analysis
有点难的LCT
当然由于add操作很少暴力重构也能AC考虑必胜策略的问题
考场上我愚蠢地用了SG做
我不会证……k 堆石子,每堆石子数量为 ai,A 和 B 玩游戏,轮流从其中某一堆石子中取
出若干个石子,最后取完石子的人获胜。
结论:如 a1 xor a2 xor …xor ak=0,则先手输,否则先手赢,xor 表示异或
证明:若 a1 xor a2 xor …xor an!=0,则一定可以从其中某堆石子中取出一些石
子使得剩下的石子数异或结果为 0,若 a1 xor a2 xor …xor an=0,则进行一次取石
子操作后 a1 xor a2 xor …xor an 一定不等于 0,按照这样的操作下去,最后一定会
出现 a1=a2=…=an=0 的情况。
如 n=3,a1=2,a2=3,a3=4,2 xor 3 xor 4=5(101),第三位为 1,三堆石子中只有 4
的二进制第三位为 1,所以我们可以通过从第三堆石子中取出石子,使得异或结
果为 0,只要让 101 对应的第 1 和第 3 位取反就可以,4 的二进制位 100,第一
位第三位取反后为 001,从 100 变成 001,所以从第三堆石子 4 中取出 3 个石子
即可。用这样的策略可以确保先手必胜要求最大边权最小,还是先求一次最小生成树
接下来就像【NOI2014】魔法森林一样动态维护最小生成树即可
若新加入的边的权比LCT中权值最大的边要小,那么这条边肯定较劣
就把LCT中这条边cut掉,再link上新边,询问就非常简单了
注意long long!!
code
#include<bits/stdc++.h>
#define MAXN 20001
#define MAXM 200001
using namespace std;
int t[MAXN][2];
int fat[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
int n,m,k,q,tot;
long long ans;
char s[10];
struct node
{
int mx,size;
long long val;
bool rev;
}a[MAXN];
struct edge
{
int u,v;
long long q;
}f[MAXM];
int getfa(int x)
{
return !fat[x]?x:fat[x]=getfa(fat[x]);
}
bool cmp(edge x,edge y)
{
return x.q<y.q;
}
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
long long readll()
{
long long x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void reverse(int x)
{
if(x)
{
a[x].rev^=1;
swap(t[x][0],t[x][1]);
}
}
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void update(int x)
{
if (x)
{
a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
a[x].mx=x;
if (a[a[t[x][0]].mx].val>a[a[x].mx].val)a[x].mx=a[t[x][0]].mx;
if (a[a[t[x][1]].mx].val>a[a[x].mx].val)a[x].mx=a[t[x][1]].mx;
}
}
void downdata(int x)
{
st[0]=0;
while (x)st[++st[0]]=x,x=fa[x];
while (st[0])down(st[st[0]--]);
}
int lr(int x)
{
return t[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],k=lr(x);
t[y][k]=t[x][!k];
if (t[x][!k])fa[t[x][!k]]=y;
fa[x]=fa[y];
if (fa[y])t[fa[y]][lr(y)]=x;
t[x][!k]=y;
fa[y]=x,pf[x]=pf[y];
update(y),update(x);
}
void splay(int x, int y)
{
downdata(x);
while (fa[x]!=y)
{
if (fa[fa[x]]!=y)
{
if (lr(x)==lr(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
void access(int x)
{
for (int y=0;x;update(x),y=x,x=pf[x])
{
splay(x,0);
fa[t[x][1]]=0;
pf[t[x][1]]=x;
t[x][1]=y;
fa[y]=x;
pf[y]=0;
}
}
void makeroot(int x)
{
access(x);
splay(x,0);
reverse(x);
}
int getroot(int x)
{
while (fa[x])x=fa[x];
return x;
}
bool judge(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
return getroot(y)==x;
}
void link(int x,int y)
{
makeroot(x);
pf[x]=y;
}
void cut(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
t[x][1]=fa[y]=pf[y]=0;
update(x);
}
int query_max(int x,int y)
{
makeroot(y);
access(x);
splay(x,0);
return a[x].mx;
}
int main()
{
freopen("game.in","r",stdin);
freopen("game.out","w",stdout);
n=read(),m=read(),k=read();
for (int i=1;i<=m;i++)f[i].u=read(),f[i].v=read(),f[i].q=readll();
sort(f+1,f+m+1,cmp);
for (int i=1;i<=m;i++)
{
int x=f[i].u,y=f[i].v;
if (getfa(x)!=getfa(y))
{
fat[getfa(x)]=getfa(y);
link(x,n+i),link(y,n+i);
a[n+i].mx=n+i,a[n+i].val=f[i].q;
}
}
q=read();
while (q--)
{
scanf("%s",&s);
if (s[0]=='a')//add
{
int x=read(),y=read();
long long z=readll();
int temp=query_max(x,y);
if (z<a[temp].val)
{
cut(temp,f[temp-n].u),cut(temp,f[temp-n].v);
f[temp-n].u=x,f[temp-n].v=y,f[temp-n].q=z;
link(x,temp),link(y,temp);
a[temp].val=z,a[temp].mx=temp;
}
}
else //game
{
ans=a[query_max(read(),read())].val;
for (int i=1;i<k;i++)ans^=a[query_max(read(),read())].val;
printf(ans?"madoka\n":"Baozika\n");
}
}
return 0;
}
例题7:JZOJ4090.树上三角形(triangle)
problem
Description
给定一个n个点的以1为根的树,每个点有一个正整数点权。 有q个操作,每个操作为以下类型之一:
1. 1 u v 询问树上所有在u到v的简单路径的节点(含u,v)中,是否存在三个不同的节点,使得以这三个节点的点权为边长的三条边能够构成一个三角形。
2. 2 u v 将节点u的权值改成v。
3. 3 u v 若节点v不在以节点u为根的子树里,那么令u的父节点为v,否则令v的父节点为u,如果u=v那么忽略这一条操作。 所有操作按输入的顺序进行。 这个题面应该不会看不懂吧。。Input
第一行有两个正整数n,q,表示树的节点数和操作数。 第二行n个正整数,表示每个节点的点权。
第三行有n-1个正整数,第i个数表示节点i+1的父节点编号,保证所给定的是一棵树。 接下来q行,每行三个正整数t u
v,意义如问题描述中所示。Output
对每个询问输出一行表示答案,“Y”表示有解,“N”表示无解。
Sample Input
5 5 1 2 3 4 5 1 2 3 1 1 1 3 1 4 5 2 1 4 1 2 5 1 2 3
Sample Output
N Y Y N
Data Constraint
对于20%的数据,保证n,q<=1000 对于另外30%的数据,保证对于每个操作,t<=2
对于再另外的30%的数据,保证询问操作不超过20次。 对于100%的数据,保证n<=100000 q<=200000
每个节点的权值<=(2^31)-1
analysis
80%以下的数据都是模拟什么的
正解暴力LCT……
思考把x,y之间的节点权值拉出来升序排序,共有k个点
能够成三角形就满足a[i]+a[i+1]>a[i+2](1<=i<=k−2)那么最长不满足这样的东西就是斐波那契数列
而节点权值都在int范围内,所以x,y之间节点数超过50个就一定合法
维护查询什么的直接上LCT
判断x,y的话,就makeroot(y),access(x),再用getroot判断
查询x,y的话,就makeroot(y),access(x),splay(x,0),再bfs遍历整棵splay就好
code
#include<bits/stdc++.h>
#define MAXN 100001
using namespace std;
int t[MAXN][2];
int b[MAXN],fat[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
int n,q,tot;
struct node
{
long long val;
int size,rev;
}a[MAXN];
bool cmp(int x,int y)
{
return a[x].val<a[y].val;
}
int read()
{
int x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
long long readll()
{
long long x=0,f=1;
char ch=getchar();
while (ch<'0' || '9'<ch)
{
if (ch=='-')f=-1;
ch=getchar();
}
while ('0'<=ch && ch<='9')
{
x=x*10+ch-'0';
ch=getchar();
}
return x*f;
}
void reverse(int x)
{
if(x)
{
a[x].rev^=1;
swap(t[x][0],t[x][1]);
}
}
void down(int x)
{
if (a[x].rev)
{
reverse(t[x][0]),reverse(t[x][1]);
a[x].rev=0;
}
}
void update(int x)
{
if (x)
{
a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
}
}
void downdata(int x)
{
st[0]=0;
while (x)st[++st[0]]=x,x=fa[x];
while (st[0])down(st[st[0]--]);
}
int lr(int x)
{
return t[fa[x]][1]==x;
}
void rotate(int x)
{
int y=fa[x],k=lr(x);
t[y][k]=t[x][!k];
if (t[x][!k])fa[t[x][!k]]=y;
fa[x]=fa[y];
if (fa[y])t[fa[y]][lr(y)]=x;
t[x][!k]=y;
fa[y]=x,pf[x]=pf[y];
update(y),update(x);
}
void splay(int x, int y)
{
downdata(x);
while (fa[x]!=y)
{
if (fa[fa[x]]!=y)
{
if (lr(x)==lr(fa[x]))rotate(fa[x]);
else rotate(x);
}
rotate(x);
}
}
void access(int x)
{
for (int y=0;x;update(x),y=x,x=pf[x])
{
splay(x,0);
fa[t[x][1]]=0;
pf[t[x][1]]=x;
t[x][1]=y;
fa[y]=x;
pf[y]=0;
}
}
void makeroot(int x)
{
access(x);
splay(x,0);
reverse(x);
}
int getroot(int x)
{
while (fa[x])x=fa[x];
return x;
}
bool judge(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
return getroot(y)==x;
}
void link(int x,int y)
{
makeroot(x);
pf[x]=y;
}
void cut(int x,int y)
{
makeroot(x);
access(y);
splay(x,0);
t[x][1]=fa[y]=pf[y]=0;
update(x);
}
void bfs(int x)
{
queue<int> que;
while (!que.empty())que.pop();
que.push(x),b[tot=1]=x;
while (!que.empty())
{
int now=que.front();
que.pop();
for (int i=0;i<=1;i++)
{
if (t[now][i])que.push(t[now][i]),b[++tot]=t[now][i];
if (tot>50)break;
}
if (tot>50)break;
}
}
int main()
{
freopen("triangle.in","r",stdin);
freopen("triangle.out","w",stdout);
n=read(),q=read();
for (int i=1;i<=n;i++)a[i].val=readll();
for (int i=2;i<=n;i++)
{
fat[i]=read();
link(i,fat[i]);
update(i);
}
while (q--)
{
int z=read(),x=read();
if (z==1)
{
int y=read();
makeroot(y);
access(x);
splay(x,0);
bfs(x);
if (tot>50)printf("Y\n");
else
{
bool bz=0;
sort(b+1,b+tot+1,cmp);
for (int i=1;i<=tot-2;i++)
{
if (a[b[i]].val+a[b[i+1]].val>a[b[i+2]].val)
{
bz=1;
break;
}
}
printf(bz?"Y\n":"N\n");
}
}
else if (z==2)a[x].val=readll();
else
{
int y=read();
if (x==y)continue;
makeroot(1);
access(y);
splay(x,0);
if (getroot(y)==x)
{
cut(y,fat[y]),link(y,x);
fat[y]=x;
}
else
{
cut(x,fat[x]),link(x,y);
fat[x]=y;
}
}
}
return 0;
}
尾声
这坑总算是填完了,呼——
(貌似我还得继续填splay的坑)
想一想开这篇blog一个月前连求LCA都只会用暴力……
又涨了不少姿势
thanks for your watching!!