树上差分总结

原文链接
一二维差分和前缀和

算法流程

设差分数组为 d [ i ] d[i] d[i]
快速修改点a与点b之间的边权

d[a] += c;
d[b] += c;
d[lca(a, b)] -= 2 * c;

以一点作为根节点的子树上的点权和即为连接该点边的修改值
eg:设差分数组初始值为0,对于下图,++ d[3],++d[4], d[2] -= 2
对于边b,3号点的值为1,即b的改变量为1;对于边c,4号点的值为1,即c的改变量为1;对于边a,以2号点为根节点的子树点权和为0,所以a的改变量为0

例题

解题思路
题目描述比较绕,简单地说对于一个图,存在树边和非树边,询问存在多少方案能够去掉一条树边和非树边能将图转变为非连通图
通过分析可以将问题转变为每一条树边存在于多少非树边所在的环路上,即要能够快速对每一条非树边的两端点间的边上的计数器加1,从而转变为差分问题

代码实现

/**
 * 主要边选一条,附加边选一条
 * 如果不加入两条边之前图是不连通的,加上两条边之后变连通了,则为一种方案
 * 即使判断图是否连通是O(1)的,两条边的枚举是O(N * M)的,会TLE
 * 
 * 以上方法不但TLE,实现也较为困难,如何判断加上某条边后图是否连通?
 * 对于任意一个非树边,如果其所在环路上的树边仅处在这么一条非树边所在的环上,
 * 那么去掉这条树边和非树边即可达到切断的目的
 * 如果换上某边处在两个非树边所在环上,那么如果想要通过切断该树边切断,需要将2条非树边均切除,是不符合题意得
 * 即统计每条树边在几个非树边所在的环上,对于<=1的边累计答案
 * 非树边所在环即非树边两端点a,b形成的环路,a -> a,b最近公共祖先p -> b
 * 快速修改一段区间的数采用差分,在树上实现故为书上差分,方法为a+=c, b+=c, p-=2c
 */
 
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>

using namespace std;

const int N = 1e5 + 10, M = 2e5 + 10, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[N * 2], ne[N * 2], idx;
int depth[N], f[N][17];
int d[N];
int ans = 0;

void bfs()
{
    queue<int> q;

    q.push(1);
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[1] = 1;

    while (q.size())
    {
        int t = q.front();
        q.pop();

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (depth[j] == INF)
            {
                q.push(j);
                depth[j] = depth[t] + 1;
                f[j][0] = t;

                for (int k = 1; k <= 16; ++ k)
                    f[j][k] = f[f[j][k - 1]][k - 1];
            }
        }
    }
}
int lca(int a, int b)
{
    if (depth[a] < depth[b]) swap(a, b);

    for (int k = 16; ~k; -- k)
        if (depth[f[a][k]] >= depth[b])
            a = f[a][k];
    
    if (a == b) return a;
    for (int k = 16; ~k; --k )
        if (f[a][k] != f[b][k])
        {
            a = f[a][k];
            b = f[b][k];
        }
    return f[a][0];
}
void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}
/**
 * 每个点的权值均作为连接以该点为根节点的子树的那条边的权值
 * 如果为0,说明该边没有任何限制,去掉后再去掉任意一条非树边即可达到目的,故答案累加m
 * 如果为1,说明该边受到1条非树边的限制,则必须去掉对应的那条非树边,故答案累加1
 * 如果>1,说明该边受到多条非树边的限制,无法通过仅去除一条非树边达到目的,故答案累加0
 * 
 * 计算以u为根节点的子树的差分和
 * 因为树是双向边,f防止走回头路
 */
int dfs(int u, int f)
{
    int res = d[u];
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j != f)
        {
            int s = dfs(j, u);
            if (s == 0) ans += m;
            else if (s == 1) ans += 1;
            res += s;
        }
    }

    return res;
}
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 0; i < n - 1; ++ i)
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);
    }
    
    bfs();

    for (int i = 0; i < m; ++ i)
    {
        int a, b;
        cin >> a >> b;
        ++ d[a];
        ++ d[b];
        d[lca(a, b)] -= 2; 
    }

    dfs(1, -1);
    
    cout << ans << endl;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值