前言
笔者今天遇到一道题目,用到了树上边差分;
记录一下个人学习树上差分的思考;
引入
我们想想数列的差分;
比如想给区间 [ L , R ] + C [L,R]+C [L,R]+C,那么是在 差 分 数 组 b [ L ] + C , b [ R + 1 ] − C 差分数组b[L]+C,b[R+1]-C 差分数组b[L]+C,b[R+1]−C;
也就是只会影响我们想要操作的区间,不会导致其他的数受影响;
树上差分&数列差分
我个人感觉,树上的差分可以看成从叶子到当前节点的一条链;
如下图所示;
树上点差分
我们定义 c n t i 表 示 点 i 被 经 过 的 次 数 cnt_i表示点i被经过的次数 cnti表示点i被经过的次数
结论
先放结论;
比如我们要求s到t这条路径上点被经过的次数;
我们只需要让
c
n
t
s
+
+
,
c
n
t
t
+
+
cnt_s++,cnt_t++
cnts++,cntt++
c
n
t
l
c
a
−
−
,
c
n
t
f
a
t
h
e
r
(
l
c
a
)
−
−
cnt_{lca}--,cnt_{father(lca)}--
cntlca−−,cntfather(lca)−−即可;
思考
假设一开始点的权值都是0,那么按我们刚刚那样操作以后,现在变成这样;
对于当前节点u,我们会将其所有子树的值全部加上;
即 c n t u + = c n t u ( s o n ) cnt_u+=cnt_{u(son)} cntu+=cntu(son);
当图中的LCA加完它所有的子树以后,我们发现值为1;是符合我们的预期的;
那我们不经担忧,LCA的父节点会不会受到影响?
显然我们可以发现,LCA的父节点受影响说明LCA的父节点被经过了一次;
那么我们只需要提前给它减掉一次即可;
这样就得到了我们的结论;
例题
还没遇到哦;
树上边差分
我们是将边权赋值到深处的点权上;
简单来说就是将边塞给了点;
注意,塞给的是儿子节点;
正常给边赋权值的话,图是这样的;
我们将边塞进点,那么图就是这样的;
结论
和之前点差分的时候一样,我们要求s到t这条路径上边的次数;
先放结论,我们只需要
c
n
t
s
+
+
,
c
n
t
t
+
+
,
c
n
t
l
c
a
−
=
2
cnt_s++,cnt_t++,cnt_{lca}-=2
cnts++,cntt++,cntlca−=2即可
思考
为什么和点差分的时候不同了呢?
因为我们并没有经过LCA的父节点到LCA这条路;
因此我们需要在LCA这个点消除影响;
其他的思考和点差分是一致的;
例题
题意
题意是给我们一个无向图,有两种边,树边以及附加边;
允许我们砍两刀,一刀砍给树边,另一刀砍给非树边;
问有多少种方案可以将图切成两半(彼此不连通);
思考
假设我们连了这样一条红色的附加边;
那么对于环上的绿色边来说,要想将图切为两半;
切掉树边以后还需要去砍掉特定的红色边;
现在我们又连了一条红色边;
对于绿色边来说,为1代表在砍掉当前树边后,只需要再砍一条特定的红色边;
为2则说明需要再砍两条特定的红色边才能使得图被砍成两半;
我们记 m m m为附加边的数量;
如果当前边的边权为0,也就是说我们可以砍掉任意一条附加边;那么 a n s + = m ans+=m ans+=m;
如果当前边的边权为1,我们需要砍掉一条特定的附加边;那么 a n s + + ans++ ans++;
如果当前边的边权大于1,那么已经超出题目给我们砍的次数(无法砍为两半),则 a n s ans ans不变;
现在问题转变成了,我们如何快速的给环上的绿色边加一以及求每条树边的边权;
这就是我们上面提到的树的边差分;
如果快速的给环上的绿色边加一呢?
只需要 c n t s + + , c n t t + + , c n t l c a − = 2 cnt_s++,cnt_t++,cnt_{lca}-=2 cnts++,cntt++,cntlca−=2即可
那么求某条树边的边权呢?
只需要 d f s dfs dfs累加一下某个节点其所有子树的点权之和即可(因为我们将边权塞给点权了);
再次提醒,边权塞给点权是向下塞的;
Code
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 100000 + 10;
struct Edge{
int to,next;
}e[N<<1];
int head[N],cnt,n,m,depth[N],fa[N][20],ans;
int d[N<<1];//树上差分数组
queue<int> q;
void add(int u,int v){
e[++cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
}
void init_lca(int root){
memset(depth,0x3f,sizeof depth);
depth[0] = 0,depth[root] = 1;
q.push(root);
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = head[u];i;i=e[i].next){
int to = e[i].to;
if(depth[to] > depth[u]+1){
depth[to] = depth[u] + 1;
q.push(to);
fa[to][0] = u;
for(int k=1,anc;k<=19;++k){
anc = fa[to][k-1];
fa[to][k] = fa[anc][k-1];
}
}
}
}
}
int LCA(int p,int q){
if(depth[p] < depth[q]){
swap(p,q);
}
for(int k=19;k>=0;--k){
if(depth[fa[p][k]]>=depth[q]){
p = fa[p][k];
}
}
if(p == q) return p;
//一起往上跳
for(int k=19;k>=0;--k){
if(fa[p][k] != fa[q][k]){
p = fa[p][k];
q = fa[q][k];
}
}
return fa[p][0];
}
int dfs(int u,int father){
int res = d[u];
for(int i=head[u];i;i=e[i].next){
int to = e[i].to;
if(father != to){
int ret = dfs(to,u);//ret表示这颗子树的值,因为是差分数组,因此是变成一个点的值
if(ret == 0) ans += m;
else if(ret == 1) ++ans;
//res > 1 说明无法变成不连通了
res += ret;
}
}
return res;
}
int main(){
cin >> n >> m;
for(int i=1,a,b;i<=n-1;++i){
cin >> a >> b;
add(a,b);
add(b,a);
}
init_lca(1);
for(int i=1,a,b;i<=m;++i){
cin >> a >> b;
++d[a],++d[b];
int p = LCA(a,b);
d[p]-=2;
}
dfs(1,-1);//根节点的值不需要加
cout << ans << '\n';
return 0;
}