AcWing352 闇の連鎖(暗之连锁)
涉及知识点:LCA,树上差分
题意
题目乱七八槽的说了一大通,但实际上抽象出来就是:
有两种边,一种是主要边,一种是附加边,主要边构成一颗树,附加边为连接树上节点的非树边(注意可以有自环)。首先剪断一条主要边,再剪断一条附加边,使得整张图变为不连通的两部分,问有几种方案(就算第一步就斩断了,第二步也得挑一条边来切)
思路
我们看到切断后变成两个强连通分量,立马就会想到有关连通性的割边算法,但实际上本题是可以切两次,用割边并不是很好做。我们要抓住题目给出的特殊性质:
- 主要边为一颗树
- 先切主要边,然后切附加边
我们以这张图为例来分析(上图仅为主要边)
感性理解
假设我们现在有一条从x连到y的附加边,虽然按照题意x到y的附加边是直接相连的,但我们可以尝试将树上x到y的路径”覆盖“上一层,那么最后对于一条主要边的覆盖情况可以分成三种情况
- 没有被覆盖 这种情况说明当这条边被切断后,没有附加边可以让这条边的两端连通分量再连回来,后面切的附加边不影响答案,因此再切断任意附加边
走走流程即可 - 被覆盖一层 这种情况说明当这条边被切断后,有一条附加边可以把两端连通分量又接回来,那么后面把这条附加边又切断即可
- 被覆盖两次及以上 这种情况即使切断该条主要边、以及一条连两端连通分量的附加边,也至少存在一条边还能把它们连回来,因此不能割这条主要边
但为什么说被覆盖了几层就代表有几条附加边在连这条主要边两端的联通分量呢?请继续看
理性分析
我们在大胆猜测过后,一定要小心求证
从主要边分析
首先我们从主要边组成的图来看,可以很容易发现任意一条主要边被切断后,整个树就不连通了,而且如果令被切断的边的两端为u和v,u为v的父亲,那么此时图分为两个部分,一个是v的子树,一个是不属于v的子树的其他部分,在”感性分析“中我们将这两个部分称为这条边两端的连通分量,如图
从附加边分析
当我们将主要边分为两个部分后,对于一条附加边的连法就可以归纳为3种情况
为方便阐述,我们将v的子树记为V,将主要边构成的树除了v的子树的部分记为U
- 从U连到U的附加边 假设从4连到7,覆盖情况如下图
这时候从4到7的路径不会经过u到v(3到6)这条被删的边
- 从V连到V的附加边 假设从10连到14,覆盖情况如下图
我们同样可以发现这段路径是不经过u到v的
- 从U连到V(从V连到U,因为是无向边)的附加边 假设从1连到11,覆盖情况如下图
这时候这段路径经过u到v,覆盖层数+1
结论:若附加边分别连在一条主要边两端的连通分量中的点,这条附加边两端点的路径一定经过主要边(注意附加边两端点不一定为主要边两端点,只要附加边两端点分别在两个连通分量里即可)
按照上面分析的思路,我们得到重要性质:一条边被覆盖0次时对答案的贡献为所有附加边数(因为切哪条附加边无所谓),被覆盖1次时对答案贡献为1,被覆盖2次及以上对答案没有贡献,最后累加即可。
如何寻找路径
LCA即可,此处不再赘述
优化:树上差分
那我们该如何维护路径的覆盖数呢?如果对于每个附加边都对路径打上标记,这样的做法是约为\(O(n^2)\)的。但是我们可以发现一个性质,树上的路径一定是一条类似”倒V字型“的路径,即\(x\)到\(y\)的路径为\(x\)到\(LCA(x,y)\),\(LCA(x,y)\)再到\(y\)(特殊情况为x或y就是LCA(x,y),此时为一条链),整条路径的转折点最多仅为3个,那么我们能否采类似差分的化区间修改为单点修改的思路来优化呢?
答案是肯定的,这种做法叫树上差分,树上差分分为对于点和对于边两种差分,此处因为我们修改的是边权,因此我们只选讲对于边的树上差分
以此图为例,比如说我们给3号节点到2号节点的路径全部+3(当然这是举例,实际上只用+1),那么我们就给3号和2号节点+3,然后在他们的LCA要减回去,这里与普通差分的区别在于由于它的儿子加了两次3,因此总共要减6。最后对于某个点(比如说4号节点),它到它父亲(4到5)的边的边权便为它的子树和(3+3+(-6))
其余请读者自行举例,此处不再赘述
因此对于每条附加边\((u,v)\),设差分值为\(d[i]\),我们将\(d[u]\)和\(d[v]\)加上1,然后给\(d[LCA(u,v)]\)减去2即可。最后dfs统计每个点到它父亲的边的覆盖层数,再利用之前的结论计数。
代码实现
鄙人这篇代码写的不是很好,常数挺大,内存炸飞,仅供帮助大家理解思路,不要学习我(doge
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
const int MAXN=100005,MAXM=200005;
vector<int>g[MAXN];
vector<pii>q[MAXN];
int n,m,lca[MAXM],que[MAXM][2],w[MAXN]={0},f[MAXN];
short vis[MAXN]={0};
long long ans=0;
class UFS{
private:
int fa[MAXN];
public:
inline void init(){
for(int i=1;i<=n;i++){
fa[i]=i;
}
}
int get(int x){
if(x==fa[x]) return x;
else return fa[x]=get(fa[x]);
}
inline void mer(int x,int y){
fa[y]=x;
}
}ufs;
void tarjan(int x){//tarjan lca
vis[x]=1;
for(auto it:g[x]){
if(vis[it]) continue;
tarjan(it);
ufs.mer(x,it);//这里顺序很重要,并查集的根必须是深度较浅的
}
for(auto it:q[x])
if(vis[it.first]==2) lca[it.second]=ufs.get(it.first);
vis[x]=2;
}
void dfs(int x,int fat){
f[x]=w[x];
for(auto it:g[x]){
if(fat==it) continue;
dfs(it,x);
f[x]+=f[it];
}
// cout<<x<<' '<<f[x]<<endl;
if(x!=1){
if(f[x]==1) ans+=1;//cout<<x<<' '<<fat<<" 1"<<endl;
else if(f[x]==0) ans+=m;//cout<<x<<' '<<fat<<' '<<m<<endl;
}
}
int main(){
cin>>n>>m;
ufs.init();
for(int i=1,x,y;i<n;i++){
cin>>x>>y;
g[x].push_back(y);
g[y].push_back(x);
}
for(int i=1,x,y;i<=m;i++){
cin>>x>>y;
que[i][0]=x;que[i][1]=y;
if(x==y) continue;
q[x].emplace_back(y,i);
q[y].emplace_back(x,i);
}
tarjan(1);
for(int i=1;i<=m;i++){//树上差分
if(que[i][0]==que[i][1]) continue;
// cout<<que[i][0]<<' '<<que[i][1]<<' '<<lca[i]<<endl;
w[lca[i]]-=2;
w[que[i][0]]+=1;
w[que[i][1]]+=1;
}
dfs(1,0);
cout<<ans<<endl;
return 0;
}