闇の連鎖
题目描述
核心思路
题意:有 n − 1 n-1 n−1条主要边构成一棵树,然后有 m m m条附加边,当把其中一条附加边添加到主要边构成的树中,则会与树上 x , y x,y x,y之间的路径上一起形成一个环。我们需要砍掉一条主要边和一条附加边,使得这棵树不再连通,成为两个独立的部分。我们需要统计有多少种不同的方案,使得这棵树不连通。
显然, “主要边” 构成一棵树,而一条 “附加边” 必然会和其两端的LCA形成环,如图所示:
那么,对于每一条主要边,存在三种情况:
-
没有被任何环覆盖
如下图所示:红色表示附加边,粉红色表示一条主要边,这条主要边并不在 这条附加边所形成的环中
-
只被一个环给覆盖
如下图所示:红色表示附加边,粉红色表示一条主要边,这条主要边在 这条附加边所形成的环中
-
被2个及以上的环覆盖
如下图所示:红色表示附加边,粉红色表示一条主要边,这条主要边在 两条附加边所形成的两个环中
- 对于情况一,枚举的那个主要边并不在环中,很明显,我们只要把这条主要边删除,那么这个图必然是不连通的。由于题目要求必须砍掉一条主要边和砍掉一条附加边。现在砍掉了这条枚举到的主要边,那么还需要砍掉一条附加边。由于有 m m m条附加边,因此我们有 m m m种选择,所以此时就有 m m m种方案。所以让答案ans累加上m即可, a n s + = m ans+=m ans+=m
- 对于情况二,枚举的那个主要边在环中,可以发现,如果我们把这条主要边删除,要想让图不连通,必然还要再删除这条附加边,因此这是唯一的一种方案了(即必然是砍掉这条枚举到的主要边和这个附加边),所以让答案ans累加上1即可, a n s + = 1 ans+=1 ans+=1
- 对于情况三,枚举的那个主要边在两个及以上的环中,以两个环为栗子,可以发现,如果我们把这条主要边砍掉,即使再砍掉一条附加边,这张图仍然是连通的,因此必须砍掉两条附加边,才能使得这张图变得不连通,但是题目要求只能砍掉一条主要边和砍掉一条附加边。因此,对于这种情况,不能得出方案,所以不需要累加答案。
那么我们怎么统计每条边被环覆盖的次数呢?也就是说,我们如何统计附加边 ( x , y ) (x,y) (x,y)所在的那个环中每条边上的权值呢?每条边上的权值表示被附加边覆盖的次数。
也就是我们如何让从点 x x x出发经过它俩的lca然后到达节点 y y y所经过的每一条边都+c呢?这其实可以用树上差分来做。
d [ x ] d[x] d[x]、 d [ y ] d[y] d[y] 会对它们到根节点上的每一条边都+c, d [ l c a ] d[lca] d[lca]会对它们到根节点上的每一条边都-2c,那么这样最终的效果就是:让 x x x到lca中和 y y y到lca中的每条边都+c
树上差分可以分为以下三种:
- 按点差分
- 按边差分
- 按深度差分
我们这里是按边差分,因为我们想知道每条边上的覆盖次数(可以认为是这条边的边权)。但是呢,一般按边差分不好做,我们都是将边权转换为点权,然后用按点差分来做。我们知道,每个节点最多只有1个父节点,也就是向上连的边只有1条,所以我们把一条边的边权下放到它的子节点的点权上(注意根节点没有点权,因为根节点上面没有边,所以没有边权下放给根节点),这样我们就转化到按点差分了。我们可以采用DFS对这棵树进行深搜,在回溯时,把子树 v v v中的所有节点上的权值都累加到节点 u u u上,设总和为 s u m sum sum,节点 v v v的父节点是 u u u,那么 s u m sum sum其实就是节点 u u u和节点 v v v之间的边权了,那么也就是覆盖次数了。
如下图所示
以节点 z z z所在的子树为栗子,将节点 z z z下面的所有点权 d [ i ] d[i] d[i]累加然后统计到节点 z z z上,设总和为 s u m sum sum,那么节点 p p p和节点 z z z之间的边权其实就是 s u m sum sum。
注意点:对于数组fa[][]
,因为点数最多是1e5,由于
2
16
<
1
e
5
<
2
17
2^{16}<1e5<2^{17}
216<1e5<217,所以第二维的大小应该取到17,这样才能弄完全部1e5个点。因此
f
a
fa
fa的第二维有0~16一共17个数,第二维需要设置为17。
虽然说题目中有附加边,但是我们建图时,只是把主要边给建立出来,并没有把附加边建到图中去。由于最多有 N N N个点,这是树,是无向边,因此最多有 M = 2 × N M=2\times N M=2×N条边。
算法设计:
- 先将主要边用图建立出来
- 进行bfs预处理出
fa[][]
- 枚举每一条附加边,对于附加边所在环上的所有主要边其两端的点权都+1,预处理出差分数组
d[]
- 进行dfs,枚举每一条主要边,进行树上差分得到每条主要边上的权值。查看每条主要边上的权值:
- 如果 c = 0 c=0 c=0,则说明这条主要边并不在附加边所形成的环中,就是情况一,所以答案 a n s + = m ans+=m ans+=m
- 如果 c = 1 c=1 c=1,则说明这条主要边在附加边所形成的环中,就是情况二,所以答案 a n s + = 1 ans+=1 ans+=1
- 如果 c > 1 c>1 c>1,则说明这条主要边处于多条附加边所形成的多个环中,就是情况三,不用累加答案
代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e5+10,M=2*N;
int n,m; //点数 附加边数
int h[N],e[M],ne[M],w[M],idx;
int depth[N];
int d[N]; //存储每个点的差分 树上差分,每个点上的权值,可以转化为边上的权值
int q[N]; //bfs使用到的队列
int fa[N][17];
int ans; //方案数
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
//预处理出fa[][]
void bfs(int root)
{
int hh=0,tt=0;
memset(depth,0x3f,sizeof depth);
depth[0]=0,depth[root]=1;
q[0]=root;
while(hh<=tt)
{
int t=q[hh++];
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
if(depth[j]>depth[t]+1)
{
depth[j]=depth[t]+1;
q[++tt]=j;
fa[j][0]=t;
for(int k=1;k<=16;k++)
fa[j][k]=fa[fa[j][k-1]][k-1];
}
}
}
}
int LCA(int a,int b)
{
if(depth[a]<depth[b])
swap(a,b);
for(int k=16;k>=0;k--)
{
if(depth[fa[a][k]]>=depth[b])
{
a=fa[a][k];
}
}
if(a==b)
return a;
for(int k=16;k>=0;k--)
{
if(fa[a][k]!=fa[b][k])
{
a=fa[a][k];
b=fa[b][k];
}
}
return fa[a][0];
}
//返回以节点u为根的子树中各节点的权值总和
int dfs(int u,int father)
{
int res=d[u]; //记录节点u的点权
//遍历节点u的所有邻接点
for(int i=h[u];~i;i=ne[i])
{
int j=e[i]; //邻接点j
if(j!=father) ///避免向上重复搜索已经遍历过的边
{
//递归搜索以j为根的子树中个节点的权值总和
int c=dfs(j,u);
//最终在回溯时就会算出以节点u为根的子树中各节点的权值总和c
//而这个c其实就是节点u与它的父节点之间的边权
if(c==0) //如果为0则说明这条主要边并不在附加边所形成的环中
ans+=m;
else if(c==1)//如果为1则说明这条主要边在附加边所形成的环中
ans++;
//相当于res=d[u]+dfs(j,u) 最开始res记录的是节点u的点权
//而dfs(j,u)就是把节点u以下的全部节点的点权都累加到节点u身上
//因此最终节点u的点权就是它自身拥有的d[u]+别上给它的c
res+=c;
}
}
//返回以节点u为根的子树中各节点的权值总和
//其实也就是返回节点u和它的父节点之间的这条边的边权 及被附加边覆盖的次数
return res;
}
int main()
{
int root=1; //树的根节点 我们设1号点为根节点 任意点都是可以的
memset(h,-1,sizeof h);
scanf("%d%d",&n,&m);
for(int i=0;i<n-1;i++) //读入n-1条 主要边
{
int a,b;
scanf("%d%d",&a,&b);
//建图时只包括主要边 而没有把附加边建出来
add(a,b),add(b,a); //无向图
}
//预处理出fa[][]
bfs(root);
//依次枚举每一条附加边
for(int i=0;i<m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
//找到当前枚举的这条附加边的两个端点的最近公共祖先
int lca=LCA(a,b);
//差分
d[a]++,d[b]++,d[lca]-=2;
}
dfs(1,-1);
printf("%lld\n",ans);
return 0;
}