5月qbxtD6T3
边权带修,查询距离某个点最远点的距离。
错误的想法:树上动态DP
正确的想法:用树链剖分维护。
链剖的套路似乎是维护一下自己轻儿子的某些信息(树上动态DP也有这个操作?)
对于这个题来说,要维护的信息是:维护每个点轻子树中,深度最大的点的深度-这个点自己的深度的二倍(注意只考虑了轻子树)
我:???
意思就是说,因为u到v的路径长为
查询时,
查询时,我们树剖之后,我们不是把u到根的路径剖成重链和轻边嘛
蓝色边为重链,粉色边为轻边。
我们现在的查询的u设为最下面的那个点。
我们考虑它的lca是什么。当lca为重链上的点时,那可能成为v的点一定是在lca的轻儿子里(因为u就在lca的重儿子里)。
直接利用我们上面对每个点维护的
当lca为轻边上的点(就是粉色的那个点),这时你不能直接用
这,,其实是并不难做的。
因为轻边只有log条,所以这种情况只会遇到log次。
假设Q子树的dfs序为[l1,r1],R为[l2,r2],那么v的取值范围就是
再开个线段树,下标为dfs序,维护区间深度的最大值即可。
捋一下思路:我们维护了两套东西:对于重链上的点,维护
每次查询直接插这条重链的最大值即可。
对于轻边上的点(呃具体来说应该是某条轻边较靠上的点)
v的可能取值是
用另一个线段树维护区间深度最大值即可。
那带修时,我们也考虑怎么维护这两套东西。
假设修改了一条边,首先,第二个线段树是好维护的。
因为那个线段树只是对dfs序维护了每个点的深度。
你把一条边边权改变后,只有那条边下面的子树里所有点的深度会变化相同的值,直接做区间加即可。
对于
既然只有log个点的值变化了,也可以相对暴力得维护:还是利用第二棵线段树:
和查询时处理轻儿子的情况类似,你考虑目前v的取值是这个点子树的dfs序区间
查询区间深度最大值即可。
于是这个题就做完了。
另一种做法:
众所周知,距离树上某个点最远的点,一定是任意一条直径的两端点其中的一个。
老师讲了一个证明,贴在最后面8!
那如果我们能维护边带修的动态直径(的两个端点U,V),那就可以容易地解决这个问题了!
等等,先考虑这么个问题:假设我们已经求出U,V了,怎么求x到U的距离和u到V的距离?
你的边带修了啊,暴力的想法是直接树剖,但其实不需要。
开一个线段树,下标为dfs序,维护每个点的深度,那u到U的距离就是
边修改其实是改了一个子树内所有点的dep
(其实就是第一种做法的“第二棵线段树”嘛)
不过这里你甚至不需要求区间最大值,只需要区间加和单点查询即可。
如何动态维护直径?
(如果你会这道题的话,有个结论是一条边边权改变,直径最多只有一个端点移动,于是你分别查到直径俩端点距离最长的点即可。)
如果不会这道题,还有另一种方法:
首先,定义一下树上任意一个点集S的直径。
定义为:
也就是说,直径可以经过不在点集上的点,只要求端点必须在点集内。
然后又有个性质:假设
有了这个性质,可以直接对dfs序新建一棵线段树,维护的是区间里所有点这个点集的直径的俩端点。
于是做完了。
update:写博客的时候口胡就好了,真正写的时候发现有亿些问题:
怎么modify啊?
当我们修改一条边边权的时候怎么modify啊?
好像既不是单点修改也不是正常的区间修改啊。。然后老师也没有给代码,网上也找不到有关资料。。。
然后自己脑补了一下:
注意咱们线段树维护的是一段dfn区间上各点形成的点集的直径。
假设修改了<u,v>这条边(v深度大)
如果这个点集完全在v子树内,或者和v子树完全没交集,那它们的直径一定不会有变化(因为简单路径嘛,你总不能经过<u,v>再从<u,v>走回来。)
也就是说改变的是,和v子树有交集,却没被v子树完全包含的区间。
对应到dfn序上,就形如右图。(黑色的是v子树在dfn上范围。蓝色的不可行,粉色的可行)。
那我们的做法很暴力——修改线段树上每个这样的区间,如果不需要修改return,否则递归处理。(如下图,×的是不递归,√的是递归(叶子结点直接return了))
复杂度看上去很窒息——但我们仔细想想,这个过程似乎和我们普通线段树上区间查询很像——:
如果完全包含直接return答案,如果完全没交压根不递归(或者说return0/INF一类的事情对吧,对答案无影响即可)
这里不也一样吗,所以看起来只需要修改log个区间。(这个idea可不可以拿来出题啊hhh
但,,并过不了,TTTTT
我感jio复杂度是对的……吧
可能是因为这个代码的巨巨巨巨巨大常数。
然后我拿下来拍了拍,大概能在6-7s内出结果(可是我树剖版本在本机也差不多这么慢啊hhhhh为啥树剖过了这个过不了。。。哭)
或许可以继承一下子树直径长度,这样每次update只需要求4条路径长度而不是6条(不知有木有用。。。。)
证明树上到一个点距离最远的点之一一定是任意直径的某一个端点
老师:画个图就非常简单了!
现在要求距离X最远的点,
分类讨论一下,可以发现,可能成为距离X最远的点,本质不同的只有下图y点z点两种情况
Case 1:
假设y为距离x最远的点。
那么有
那毁了,可以推出
Case 2:
假设z为距离x最远的点,与Case1类似
可以推出
那么
因而就得证了。
(似乎我当时也是这么想的)
证明两点集的并的直径,端点一定是两点集内部直径的四个端点中的两个:假设红色的点在一个点集,直径为(u1,v1);蓝色的点在一个点集,直径为(u2,v2),分类讨论:
Case1:
如果两个集合相交了,相交于P
如果(u1,x)是并集的直径,那么有
Case2:
如果两集合不相交,但他们必定还是有边相连的
如果(u1,x)是并集的直径,那么有
可以注意到,上面的讨论并没有用到点集联通之类的信息,于是可以证明了。
并且上面的分类讨论应该是不完全的,,但,,反正就是这种感觉啦hhh
第一种方法的代码
写一点调一点还好,要是让我整个写完再调可能需要调到下辈子hhh
(6kb,写过的人都说好
有个细节是,上面讨论的做法只考虑了u向上走的情况,向下走的(也即在u子树内部走的),随便处理一下即可。。。
#include<algorithm>
#include<iostream>
#include<cstdio>
#define INF 0x3f3f3f3f3f3f3f3f
#define I inline
#define R register
#define Root 1,n,1
#define Lson l,mid,rt<<1
#define Rson mid+1,r,rt<<1|1
#define Ls rt<<1
#define Rs rt<<1|1
using std::cout;
using std::endl;
using std::max;
using std::min;
int read(){
int h=0;char c=getchar();
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9')h=(h<<1)+(h<<3)+c-'0',c=getchar();
return h;
}
//longlong
const int MAXN=404000;
int n,m;
struct Edge{
int to;
int nxt;
int dis;
}edge[MAXN];
int frm[MAXN];
int cnt_e;
void insert(int u,int v,int w){
cnt_e++;
edge[cnt_e].to=v;
edge[cnt_e].nxt=frm[u];
edge[cnt_e].dis=w;
frm[u]=cnt_e;
}
int dep[MAXN];long long depth[MAXN];//dep为不带权深度,树剖时用;depth为带权深度,求两点距离时用
int fa[MAXN];
int siz[MAXN];
int mson[MAXN];//维护这四个
void dfs1(int x,int f,int w){
dep[x]=dep[f]+1;depth[x]=depth[f]+w;
fa[x]=f;
siz[x]=1;
int mxn=0;
for(int i=frm[x];i;i=edge[i].nxt){
int v=edge[i].to;
if(v!=f){
dfs1(v,x,edge[i].dis);
siz[x]+=siz[v];
if(siz[v]>mxn){
mson[x]=v;
mxn=siz[v];
}
}
}
}
int dfn[MAXN];//dfs序
int id[MAXN];//原编号
int top[MAXN];//链头
int cnt_d;
long long ww[MAXN];//初始状态depth[v]-2*depth[x]的最大值,其中v是x的轻儿子子树中任意点,建t2时用
//特别地,如果x没有轻子树,那就把它赋成-depth[x]。看着就很有道理对吧
long long mxd[MAXN];//初始状态,每个点所有儿子depth的最大值。求ww时用。
//两数组下表均为原编号
void dfs2(int x,int tp){
dfn[x]=++cnt_d,id[cnt_d]=x;
top[x]=tp;
mxd[x]=depth[x],ww[x]=-depth[x];//ww赋这样的初值,是因为depth[v]-2*depth[x]一定比 -depth[x]大,如果有轻儿子一定会被更新
if(!mson[x])return;
dfs2(mson[x],tp);//先递归重儿子
mxd[x]=max(mxd[x],mxd[mson[x]]);
for(int i=frm[x];i;i=edge[i].nxt){
int v=edge[i].to;
if(v!=fa[x]&&v!=mson[x]){
dfs2(v,v);
mxd[x]=max(mxd[x],mxd[v]);
ww[x]=max(ww[x],mxd[v]-2*depth[x]);
}
}
}
//t1对dfs序维护深度。支持单点查询(可用区间查最值实现),区间查最值,和区间加减。
//t2下标还是dfs序(树剖嘛),对每个点x维护depth[v]-2*depth[x]的最大值,其中v是x的轻儿子子树中任意点;支持单点赋值和区间求最大值
struct Segment_Tree1{
long long mx[MAXN<<2];//区间最大值
long long col[MAXN<<2];//区间加标记
void build(int l,int r,int rt,long long* A){
if(l==r){
mx[rt]=A[id[l]];//注意depth/ww的下标是原编号
return;
}
int mid=(l+r)>>1;
build(Lson,A);build(Rson,A);
mx[rt]=max(mx[Ls],mx[Rs]);
}
I void push_down(int rt){
col[Ls]+=col[rt],col[Rs]+=col[rt];
mx[Ls]+=col[rt],mx[Rs]+=col[rt];//区间每个数都加嘛w
col[rt]=0;
}
long long query(int l,int r,int rt,int nowl,int nowr){//查询区间最大值
if(nowr<nowl)return -INF;//这种情况在代码里是可能出现的hhh
if(nowl<=l&&r<=nowr)return mx[rt];
if(col[rt])push_down(rt);
int mid=(l+r)>>1;
if(nowl<=mid){
if(nowr>mid)return max(query(Lson,nowl,nowr),query(Rson,nowl,nowr));
return query(Lson,nowl,nowr);
}
return query(Rson,nowl,nowr);
}
void modify1(int l,int r,int rt,int nowl,int nowr,int k){//区间加
if(nowl<=l&&r<=nowr){
mx[rt]+=k,col[rt]+=k;
return;
}
push_down(rt);
int mid=(l+r)>>1;
if(nowl<=mid)modify1(Lson,nowl,nowr,k);
if(nowr>mid)modify1(Rson,nowl,nowr,k);
mx[rt]=max(mx[Ls],mx[Rs]);//差点忘了updatehh
}
void modify2(int l,int r,int rt,int pos,long long k){//单点赋值
if(l==r){
mx[rt]=k;
return;
}
push_down(rt);
int mid=(l+r)>>1;
if(pos<=mid)modify2(Lson,pos,k);
if(pos>mid)modify2(Rson,pos,k);
mx[rt]=max(mx[Ls],mx[Rs]);
}
}t1,t2;
I long long get_d(int x){//得到x这个点当前的带权深度 //所以还是多整点小函数方便啊hhh
return t1.query(Root,dfn[x],dfn[x]);
}
I int ed(int x){return dfn[x]+siz[x]-1;}//x子树dfs序最靠右的点
I long long get_d2(int x,int v){//得到x除了v这个儿子,之外的子树,带权深度的最大值
return max(t1.query(Root,dfn[x],dfn[v]-1),t1.query(Root,ed(v)+1,ed(x)));
}
I long long get_ans(int u){
long long anss=0;
long long dep_u=get_d(u);//u目前的带权深度
int x=u;
while(x){//还没跳到祖先
// cout<<"x"<<x<<endl;
int tpp=top[x];
anss=max(anss,dep_u+t2.query(Root,dfn[tpp],dfn[x]-1));//别忘了top[u]在上面
//为啥这里dfn[x]要-1呢?因为此时x在链的底端,它就是上一次走到的“轻边上端的点”,不能统计它的dep[v]-2*dep[x]
//但这样就会丢失u向下走的答案,需要最后统计一下。
int ftp=fa[tpp];
if(!ftp)break;//啊,得判掉。。
anss=max(anss,dep_u+get_d2(ftp,tpp)-get_d(ftp)*2);
x=ftp;
}
anss=max(anss,t1.query(Root,dfn[x],ed(x))-dep_u);
return anss;
}
I void Modify(int u){
int x=u;
while(x){
int tpp=top[x],ftp=fa[tpp];
if(!ftp)break;
long long val=get_d2(ftp,mson[ftp])-2*get_d(ftp);
t2.modify2(Root,dfn[ftp],val);
x=ftp;
}
}
int main(){
// freopen("ex_road2.in","r",stdin);//终于过了大样例,赶紧交上去,0pts,心态崩了,然后发现没关文件hhh
// freopen("zj.out","w",stdout);
n=read(),m=read();
for(int i=1;i<=n-1;i++){
int u=read(),v=read(),w=read();
insert(u,v,w);
insert(v,u,w);
}
dfs1(1,0,0);
dfs2(1,1);
t1.build(Root,depth);
t2.build(Root,ww);
for(int i=1;i<=m;i++){
if(read()==1){//查询操作,查u到所有点距离的最大值。
printf("%lld\n",get_ans(read()));
}
else{
//对于<u,v>这条边,设u在下,变大了d
// 要改两部分:t1的u子树(包括u),它们的深度都会增加d
// t2:1.u子树内,看看dep[v]-2*dep[x]就可以看出,因为dep[v],dep[x]都变大了d,因此u子树内所有点的值都会-d
// 2.u往上到根路径上,每条轻边靠上的点,它的值会改变;直接利用t1的信息重新计算即可。
int ee=read();int u=edge[(ee<<1)-1].to,v=edge[(ee<<1)].to,org=edge[ee<<1].dis;//org是原来的值
if(dep[u]<dep[v])u^=v^=u^=v;
int ww=read();
t1.modify1(Root,dfn[u],ed(u),ww-org);//1.1
t2.modify1(Root,dfn[u],ed(u),org-ww);//2.1
Modify(u);//修改u向上路径上轻边靠上的点的权值(u也可能被修改)//2.2
edge[ee<<1].dis=ww,edge[(ee<<1)-1].dis=ww;//居然往把这个改了hhh太傻了,,还是拍了拍才发现/哭泣
// printf("%lld\n",get_d(7));
}
}
return 0;
}
动态维护直径的做法
#include<algorithm>
#include<iostream>
#include<cstdio>
#define INF 0x3f3f3f3f3f3f3f3f
#define I inline
#define RR register
#define Root 1,n,1
#define Lson l,mid,rt<<1
#define Rson mid+1,r,rt<<1|1
#define Ls rt<<1
#define Rs rt<<1|1
#define lowbit(x) (x&-x)
using std::cout;
using std::endl;
using std::max;
using std::min;
int read(){
int h=0;char c=getchar();
while(c<'0'||c>'9')c=getchar();
while(c>='0'&&c<='9')h=(h<<1)+(h<<3)+c-'0',c=getchar();
return h;
}
//longlong
const int MAXN=404000;
int n,m;
struct Edge{
int to;
int nxt;
int dis;
}edge[MAXN];
int frm[MAXN];
int cnt_e;
void insert(int u,int v,int w){
cnt_e++;
edge[cnt_e].to=v;
edge[cnt_e].nxt=frm[u];
edge[cnt_e].dis=w;
frm[u]=cnt_e;
}
int dfn[MAXN],id[MAXN],cnt_d;
int dep[MAXN];//真实深度
long long depth[MAXN];//带权深度
int siz[MAXN];
int f[MAXN>>1][19];
void dfs(int x,int fa,int w){
dfn[x]=++cnt_d;id[cnt_d]=x;
f[x][0]=fa;
siz[x]=1;
for(int i=1;i<=15;i++)f[x][i]=f[f[x][i-1]][i-1];
dep[x]=dep[fa]+1,depth[x]=depth[fa]+w;
for(int i=frm[x];i;i=edge[i].nxt){
int v=edge[i].to;
if(v!=fa)dfs(v,x,edge[i].dis),siz[x]+=siz[v];//我窒息了,,这写成了dfs(v,x,edge[i].to)(*&%7W%&*(^
}
}
struct BIT{//树状数组对dfs维护深度,支持区间加,单点查询(线段树常数有点太自闭。。
long long z[MAXN];
void modify(int x,long long k){//如果是x==n+1的话,它压根不会进这个循环
for(int i=x;i<=n;i+=lowbit(i)){
z[i]+=k;
}
}
long long query(int x){
long long anss=0;
for(int i=x;i;i-=lowbit(i)){
anss+=z[i];
}
return anss;
}
}t1;
int get_lca(int x,int y){
if(dep[x]>dep[y])x^=y^=x^=y;
for(int i=15;i>=0;i--){//啊这常数太窒息了吧
if(dep[f[y][i]]>=dep[x])
y=f[y][i];
}
if(x==y)return x;
for(int i=15;i>=0;i--){
if(f[y][i]!=f[x][i]){
y=f[y][i],x=f[x][i];
}
}
return f[y][0];
}
I long long get_d(int x){return t1.query(dfn[x]);}//别忘了转dfs序
long long get_dis(int x,int y){//得到原编号为x,y的点的距离
int lca=get_lca(x,y);
return get_d(x)+get_d(y)-2*get_d(lca);
}
bool ch_ck(int L,int R,int l,int r){//[L,R],[l,r]有交集,并且交不为[L,R]
if(l<=L&&R<=r)return false;
if(R<l||L>r)return false;
return true;
}
struct Segmen_Tree2{//对dfs序维护直径
int u[MAXN<<3],v[MAXN<<3];//直径俩端点的原编号
void update(int rt){
int pt[4];
pt[0]=u[Ls],pt[1]=v[Ls],pt[2]=u[Rs],pt[3]=v[Rs];//常数有点窒息啊。。
long long mxn=-1;//边权有0!!!!
for(int i=0;i<=2;i++){
for(int j=i+1;j<=3;j++){
long long hh=get_dis(pt[i],pt[j]);
if(hh>mxn){
mxn=hh;
u[rt]=pt[i],v[rt]=pt[j];
}
}
}
// cout<<rt<<":";for(int i=0;i<=3;i++)cout<<pt[i]<<' ';puts("");
// cout<<"ANS:"<<u[rt]<<' '<<v[rt]<<endl;
}
void build(int l,int r,int rt){
if(l==r){
u[rt]=v[rt]=id[l];
return;
}
int mid=(l+r)>>1;
build(Lson);build(Rson);
update(rt);
}
void modify(int l,int r,int rt,int x,int y){//修改了连接xy两点边的边权。
// cout<<l<<' '<<r<<' '<<id[l]<<' '<<id[r]<<' '<<x<<' '<<x+siz[id[x]]-1<<endl;
if(l==r)return;
if(ch_ck(l,r,x,x+siz[id[x]]-1)==false){
return;
}
int mid=(l+r)>>1;
modify(Lson,x,y);modify(Rson,x,y);
update(rt);
// cout<<l<<' '<<r<<' '<<id[l]<<' '<<id[r]<<' '<<u[rt]<<' '<<v[rt]<<endl;
}
}t2;
long long get_ans(int x){
long long anss=max(get_dis(x,t2.u[1]),get_dis(x,t2.v[1]));
return anss;
}
int main(){
// freopen("ex_road2.in","r",stdin);//终于过了大样例,赶紧交上去,0pts,心态崩了,然后发现没关文件hhh
// freopen("zj.out","w",stdout);
n=read(),m=read();
for(int i=1;i<=n-1;i++){
int u=read(),v=read(),w=read();
insert(u,v,w);
insert(v,u,w);
}
dfs(1,0,0);
for(int i=1;i<=n;i++){
t1.modify(dfn[i],depth[i]);
t1.modify(dfn[i]+1,-depth[i]);
}
t2.build(Root);
for(int i=1;i<=m;i++){
if(read()==1){
printf("%lld\n",get_ans(read()));
}
else{
int ee=read(),ww=read();
int u=edge[ee<<1].to,v=edge[(ee<<1)-1].to;int org=edge[ee<<1].dis;
if(dep[u]<dep[v])u^=v^=u^=v;
t1.modify(dfn[u],ww-org);t1.modify(dfn[u]+siz[u],-(ww-org));//深度要修改的就是u子树内的信息。
t2.modify(Root,dfn[u],dfn[v]);
edge[ee<<1].dis=ww,edge[(ee<<1)-1].dis=ww;
// cout<<t2.u[1]<<' '<<t2.v[1]<<endl;
}
}
return 0;
}