[Analysis] \color{blue}{\texttt{[Analysis]}} [Analysis]
删除,而且没有强制在线,一般就可以考虑时光倒流了。
离线,把删除改为加入。
在加入边前,整个图是由很多棵相互不联通的树组成的,两点间的最大长度就是所有树的直径长度的最大值。
求直径当然是 O ( n ) O(n) O(n) 的啦。
考虑加入一条边 ( u , v ) (u,v) (u,v) 对答案有什么影响。
假设加入边 ( u , v ) (u,v) (u,v) 前 u u u 所在子树的直径两端分别为 l u , r u l_{u},r_{u} lu,ru,同理, v v v 所在子树的直径两端分别为 l v , r v l_{v},r_{v} lv,rv。可以证明加入边 ( u , v ) (u,v) (u,v) 后这棵新树的直径两端一定是 l u , r u , l v , r v l_{u},r_{u},l_{v},r_{v} lu,ru,lv,rv 四个点中的两个。
证明可以考虑分类讨论:
- 如果新树的直径不经过 ( u , v ) (u,v) (u,v),那它的直径就是原来的两棵子树的直径较长的那一条;即端点要么是 l u , r u l_{u},r_{u} lu,ru,要么是 l v , r v l_{v},r_{v} lv,rv。
- 如果新树的直径经过 ( u , v ) (u,v) (u,v),在原来 u u u 的子树中离 u u u 最远的点一定是 l u l_{u} lu 或者 r u r_{u} ru,在原来 v v v 的子树中离 v v v 最远的点一定是 l v l_{v} lv 或者 r v r_{v} rv。所以新树的直径一定是一端是 l u l_{u} lu 或者 r u r_{u} ru,另一端是 l v l_{v} lv 或者 r v r_{v} rv。
这样利用 LCA 可以 O ( log n ) O(\log n) O(logn) 求出任意两点间的距离。即每次加入一条边的时间复杂度为 O ( log n ) O(\log n) O(logn)。
总的时间复杂度为 O ( q log n ) O(q \log n) O(qlogn)。
Code \color{blue}{\text{Code}} Code
struct UnionFind{
int Fa[N];
void clear(int n){
for(int i=1;i<=n;i++) Fa[i]=i;
}
int getf(int x){
if (Fa[x]==x) return x;
return Fa[x]=getf(Fa[x]);
}
void merge(int x,int y){
int u=getf(x),v=getf(y);
Fa[v]=u;//注意次序
}
}Reach;//封装一个并查集
struct edge{
int nxt,to,val;
int operator = (int n){
return nxt=to=val=n;
}
}e[N<<1],E[N<<1];
int h[N],ecnt,H[N],Ecnt;
void add(int u,int v,int w){
e[++ecnt]=(edge){h[u],v,w};h[u]=ecnt;
e[++ecnt]=(edge){h[v],u,w};h[v]=ecnt;
}
void Add(int u,int v,int w){
E[++Ecnt]=(edge){H[u],v,w};H[u]=Ecnt;
E[++Ecnt]=(edge){H[v],u,w};H[v]=Ecnt;
}
int Fa[22][N],n,m,dep[N],Log[N];
ll len[N];
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if (v==fa) continue;
len[v]=len[u]+e[i].val;
Fa[0][v]=u;
dfs(v,u);
}
}
void dispose(){
for(int i=1;i<=Log[n];i++)
for(int u=1;u<=n;u++)
Fa[i][u]=Fa[i-1][Fa[i-1][u]];
}
int LCA(int u,int v){
if (dep[u]<dep[v]) swap(u,v);
int k=dep[u]-dep[v];
for(int i=Log[k];i>=0;i--)
if (dep[u]-dep[v]>=(1<<i))
u=Fa[i][u];
if (u==v) return u;
for(int i=Log[dep[u]];i>=0;i--)
if (Fa[i][u]!=Fa[i][v]){
u=Fa[i][u];v=Fa[i][v];
}
return Fa[0][u];
}
ll distance(int u,int v){
return len[u]+len[v]-(len[LCA(u,v)]<<1);
}
ll dis[N];
int dp(int u,int fa){
int ret=u;
for(int i=H[u];i;i=E[i].nxt){
int v=E[i].to;
if (v==fa) continue;
dis[v]=dis[u]+E[i].val;
int tmp=dp(v,u);
if (dis[tmp]>dis[ret]) ret=tmp;
}
return ret;
}//求树中离 u 最远的点
ll ans,out[N];bool flag[N];
int del[N],l[N],r[N];
struct linkage{
int u,v,val;
}Link[N];
void merge(int u,int v){
u=Reach.getf(u);
v=Reach.getf(v);
ll d1=distance(l[u],r[u]);
ll d2=distance(l[v],r[v]);
ll d3=distance(l[u],r[v]);
ll d4=distance(l[v],r[u]);
ll d5=distance(l[u],l[v]);
ll d6=distance(r[u],r[v]);
ll D=max(d1,max(d2,max(d3,max(d4,max(d5,d6)))));
ans=max(ans,D);
if (d2==D){
l[u]=l[v];r[u]=r[v];
}//这里别漏情况
else if (d3==D){
l[u]=l[u];r[u]=r[v];
}
else if (d4==D){
l[u]=r[u];r[u]=l[v];
}
else if (d5==D){
l[u]=l[u];r[u]=l[v];
}
else if (d6==D){
l[u]=r[u];r[u]=r[v];
}
Reach.merge(u,v);//注意次序
}
void initdata(){
ecnt=Ecnt=ans=0;
for(int i=1;i<=(n<<1);i++)
E[i]=e[i]=0;
for(int i=1;i<=n;i++){
l[i]=r[i]=h[i]=H[i]=len[i]=dep[i]=0;
flag[i]=true;
}
for(int i=0;i<=20;i++)
for(int u=1;u<=n;u++)
Fa[i][u]=0;
}//类似于 Codeforces 里的要求,不能直接用 memset 清零
int main(){
Log[0]=-1;
for(int i=1;i<=1e5;i++)
Log[i]=Log[i>>1]+1;
for(int T=read();T;T--){
n=read();m=read();
initdata();
Reach.clear(n);
for(int i=1;i<n;i++){
Link[i].u=read();
Link[i].v=read();
Link[i].val=read();
add(Link[i].u,Link[i].v,Link[i].val);
}
dfs(1,0);
dispose();
for(int i=1;i<=m;i++){
del[i]=read();
flag[del[i]]=false;
}
for(int i=1;i<n;i++)
if (flag[i]){
Add(Link[i].u,Link[i].v,Link[i].val);
Reach.merge(Link[i].u,Link[i].v);
}
for(int i=1;i<=n;i++)
if (Reach.getf(i)==i){
dis[i]=0;l[i]=dp(i,0);
dis[l[i]]=0;r[i]=dp(l[i],0);
ans=max(ans,distance(l[i],r[i]));
}
for(int i=m;i>=1;i--){
out[i]=ans;
merge(Link[del[i]].u,Link[del[i]].v);
}
for(int i=1;i<=m;i++)
printf("%lld\n",out[i]);
}
return 0;
}
ll 表示 long long
这份代码能过,不过显得很冗长。因为最开始求各棵子树的直径和最后加边时求新直径用了两种不同的算法。
我们可以使用相同的算法减小代码长度:最开始求子树直径的时候也不一定要 O ( n ) O(n) O(n) 的专门算法。开始时视为整个图由 n n n 棵只有 1 1 1 个节点的树组成,在求答案之前把所有不需要删除的边逐条加入图中也可以得到最开始时各棵子树的直径的最大值。
这样做的话,这一部分代码的时间复杂度是线性对数的,不如第一份代码;但是这样做可以减少代码长度,方便调试。
struct UnionFind{
int Fa[N];
void clear(int n){
for(int i=1;i<=n;i++) Fa[i]=i;
}
int getf(int x){
if (Fa[x]==x) return x;
return Fa[x]=getf(Fa[x]);
}
void merge(int x,int y){
int u=getf(x),v=getf(y);
Fa[v]=u;//注意次序
}
}Reach;
struct edge{
int nxt,to,val;
int operator = (int n){
return nxt=to=val=n;
}
}e[N<<1];int h[N],ecnt;
void add(int u,int v,int w){
e[++ecnt]=(edge){h[u],v,w};h[u]=ecnt;
e[++ecnt]=(edge){h[v],u,w};h[v]=ecnt;
}
int Fa[22][N],n,m,dep[N],Log[N];
ll len[N];
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
for(int i=h[u];i;i=e[i].nxt){
int v=e[i].to;
if (v==fa) continue;
len[v]=len[u]+e[i].val;
Fa[0][v]=u;
dfs(v,u);
}
}
void dispose(){
for(int i=1;i<=Log[n];i++)
for(int u=1;u<=n;u++)
Fa[i][u]=Fa[i-1][Fa[i-1][u]];
}
int LCA(int u,int v){
if (dep[u]<dep[v]) swap(u,v);
int k=dep[u]-dep[v];
for(int i=Log[k];i>=0;i--)
if (dep[u]-dep[v]>=(1<<i))
u=Fa[i][u];
if (u==v) return u;
for(int i=Log[dep[u]];i>=0;i--)
if (Fa[i][u]!=Fa[i][v]){
u=Fa[i][u];v=Fa[i][v];
}
return Fa[0][u];
}
ll distance(int u,int v){
return len[u]+len[v]-(len[LCA(u,v)]<<1);
}
ll ans,out[N];bool flag[N];
int del[N],l[N],r[N];
struct linkage{
int u,v,val;
}Link[N];
void merge(int u,int v){
u=Reach.getf(u);
v=Reach.getf(v);
ll d1=distance(l[u],r[u]);
ll d2=distance(l[v],r[v]);
ll d3=distance(l[u],r[v]);
ll d4=distance(l[v],r[u]);
ll d5=distance(l[u],l[v]);
ll d6=distance(r[u],r[v]);
ll D=max(d1,max(d2,max(d3,max(d4,max(d5,d6)))));
ans=max(ans,D);
if (d2==D){
l[u]=l[v];r[u]=r[v];
}
else if (d3==D){
l[u]=l[u];r[u]=r[v];
}
else if (d4==D){
l[u]=r[u];r[u]=l[v];
}
else if (d5==D){
l[u]=l[u];r[u]=l[v];
}
else if (d6==D){
l[u]=r[u];r[u]=r[v];
}
Reach.merge(u,v);//注意次序
}
void initdata(){
ecnt=ans=0;
for(int i=1;i<=(n<<1);i++) e[i]=0;
for(int i=1;i<=n;i++){
l[i]=r[i]=i;
h[i]=len[i]=dep[i]=0;
flag[i]=true;
}
for(int i=0;i<=20;i++)
for(int u=1;u<=n;u++)
Fa[i][u]=0;
}
int main(){
Log[0]=-1;
for(int i=1;i<=1e5;i++)
Log[i]=Log[i>>1]+1;
for(int T=read();T;T--){
n=read();m=read();
initdata();
Reach.clear(n);
for(int i=1;i<n;i++){
Link[i].u=read();
Link[i].v=read();
Link[i].val=read();
add(Link[i].u,Link[i].v,Link[i].val);
}
dfs(1,0);
dispose();
for(int i=1;i<=m;i++){
del[i]=read();
flag[del[i]]=false;
}
for(int i=1;i<n;i++)//这里是改变的地方
if (flag[i]) merge(Link[i].u,Link[i].v);
for(int i=m;i>=1;i--){
out[i]=ans;
merge(Link[del[i]].u,Link[del[i]].v);
}
for(int i=1;i<=m;i++)
printf("%lld\n",out[i]);
}
return 0;
}