树上差分
算法流程
设差分数组为
快速修改点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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理