树上差分思想与例题

前言

笔者今天遇到一道题目,用到了树上边差分;

记录一下个人学习树上差分的思考;

引入

我们想想数列的差分;

比如想给区间 [ 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被经过的次数 cntii

结论

先放结论;

比如我们要求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;
}

参考资料

大佬的博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值