D156 最近公共祖先+树上差分 P10931 闇の連鎖

D156 最近公共祖先+树上差分 P10931 闇の連鎖_哔哩哔哩_bilibili

 

P10931 闇の連鎖 - 洛谷       U143800 暗之连锁 - 洛谷

N 个点,N-1 条边为树边,M 条边为非树边,先砍断一条树边,再砍断一条非树边,使图分成两部分的方案数。

思路

如左图,添加一条非树边,非树边就与最近公共祖先构成环

每一条树边存在三种情况:1. 没有被任何环覆盖;  2. 只被一个环给覆盖;  3. 被 $2$ 个及以上的环覆盖

第一种情况,切掉一条树边后已经将整张图分成了两部分,再随便切掉一条非树边,此时的方案数为非树边的条数 $M$

第二种情况,切掉一条树边后,由于它是在一个环中,所以只能切掉它所在环中的唯一一条非树边,此时的方案数为 $1$

第三种情况,切掉一条树边后,会使覆盖它的两个环合并成一个环,再切一刀非树边,不能切开整张图,此时的方案数为 $0$

怎样统计每条边被环覆盖的次数(即边做加法的次数)呢?

可以用树上边差分来做,用 $d[x]$ 表示节点 $x$ 到根节点的距离,初始时各节点的点权均为 $0$

每次添加一条非树边 $(x,y)$ 时,其两端的 $d[x],d[y]$ 均 $+1$,即该点到根节点的路径上所有边的边权都 $+1$,也就是覆盖次数 $+1$,它们的最近公共祖先的 $d[lca]$ 要 $-2$,如中图

然后用 $dfs$ 求出每个节点的子树的 $d[x]$ 的累加和,它就表示节点 $x$ 与它父亲节点之间的边被环覆盖的次数,如右图

最后,依次统计每条树边能产生的方案贡献,累加起来

相关板子:

D09【模板】最近公共祖先 倍增算法 - 董晓 - 博客园

A11【模板】树上差分 - 董晓 - 博客园

 

// LCA+树上差分 O(mlogn)
#include<bits/stdc++.h>
using namespace std;

const int N=100010,M=200010;
int h[N],to[M],ne[M],idx;
void add(int u,int v){
  to[++idx]=v,ne[idx]=h[u],h[u]=idx;
}
int n,m,ans;
int dep[N],fa[N][17],d[N];

void dfs(int u,int f){ //预处理dep,fa数组
  dep[u]=dep[f]+1; fa[u][0]=f;
  for(int i=1; i<=16; i++) fa[u][i]=fa[fa[u][i-1]][i-1];
  for(int i=h[u]; i; i=ne[i]){
    int v=to[i];
    if(v!=f) dfs(v,u);
  }
}
int lca(int u,int v){ //求lca
  if(dep[u]<dep[v]) swap(u,v);
  for(int k=16; k>=0; k--)if(dep[fa[u][k]]>=dep[v]) u=fa[u][k];
  if(u==v) return u;
  for(int k=16; k>=0; k--)if(fa[u][k]!=fa[v][k]) u=fa[u][k],v=fa[v][k];
  return fa[u][0];
}
int dfs2(int u,int f){ //对子树的差分求和
  int sum=d[u];
  for(int i=h[u]; i; i=ne[i]){
    int v=to[i];
    if(v==f) continue;
    int s=dfs2(v,u);
    if(s==0) ans+=m;
    else if(s==1) ans++; //树边(u,v)的贡献
    sum+=s;
  }
  return sum; //u子树的结点权值和,即边(u,fa)被覆盖次数
}
int main(){
  scanf("%d%d",&n,&m);
  for(int i=0,u,v; i<n-1; i++){
    scanf("%d%d",&u,&v);
    add(u,v),add(v,u);
  }
  dfs(1,0);
  for(int i=0,u,v; i<m; i++){
    scanf("%d%d",&u,&v);
    d[u]++,d[v]++,d[lca(u,v)]-=2; //树上差分
  }
  dfs2(1,0); //差分求和
  printf("%d\n",ans);
}

 

posted @ 2026-05-16 22:39  董晓  阅读(67)  评论(0)    收藏  举报