树上前缀和与差分

news/2024/10/7 22:30:07

树上前缀和

\(sum_i\) 表示根节点到节点 \(i\) 的权值总和。
则有:

  • 对于点权,\(x,y\) 路径上的和为 \(sum_x + sum_y - sum_{lca} - sum_{fa_{lca}}\)
  • 对于边权,\(x,y\) 路径上的和为 \(sum_x + sum_y - 2 \times sum_{lca}\)

习题:P4427 [BJOI2018] 求和

解题思路

预处理出 \(sum_{i,k}\) 表示根节点到节点 \(i\) 的深度的 \(k\) 次方和,这个过程的时间复杂度为 \(O(nk)\),后面即为树上点权前缀和问题。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using std::swap;
using std::vector;
const int N = 3e5 + 5;
const int K = 55;
const int LOG = 19;
const int MOD = 998244353;
vector<int> tree[N];
int fa[N][LOG], depth[N], sum[N][K];
void dfs(int u, int pre) {depth[u] = depth[pre] + 1;int d = 1;for (int i = 0; i < K; i++) {sum[u][i] = (sum[pre][i] + d) % MOD;d = 1ll * d * depth[u] % MOD;} fa[u][0] = pre;for (int v : tree[u]) {if (v == pre) continue;dfs(v, u);}
}
int lca(int x, int y) {if (depth[x] < depth[y]) swap(x, y);int delta = depth[x] - depth[y];for (int i = LOG - 1; i >= 0; i--)if (delta & (1 << i)) x = fa[x][i];if (x == y) return x;for (int i = LOG - 1; i >= 0; i--) {if (fa[x][i] != fa[y][i]) {x = fa[x][i]; y = fa[y][i];}}return fa[x][0];
}
int main()
{int n; scanf("%d", &n);for (int i = 1; i < n; i++) {int x, y; scanf("%d%d", &x, &y);tree[x].push_back(y);tree[y].push_back(x);}depth[0] = -1;dfs(1, 0);for (int i = 1; i < LOG; i++) {for (int j = 1; j <= n; j++) fa[j][i] = fa[fa[j][i - 1]][i - 1];}int m; scanf("%d", &m);while (m--) {int i, j, k; scanf("%d%d%d", &i, &j, &k);int lca_ij = lca(i, j), f = fa[lca_ij][0];int ans1 = (sum[i][k] + MOD - sum[f][k]) % MOD;int ans2 = (sum[j][k] + MOD - sum[lca_ij][k]) % MOD;printf("%d\n", (ans1 + ans2) % MOD);}return 0;
}

树上差分

树上差分可以理解为对树上的某一段路径进行差分操作,这里的路径可以类比一维数组的区间进行理解。例如在对树上的一些路径进行频繁操作,并且询问某条边或者某个点在经过操作后的值的时候,就可以运用树上差分思想。

点差分

例题:P3128 [USACO15DEC] Max Flow P

问题描述:有 \(n\) 个节点,用 \(n-1\) 条边连接,所有节点都连通。给出 \(k\) 条路径,第 \(i\) 条路径为节点 \(s_i\)\(t_i\)。每给出一条路径,路径上所有节点的权值加 \(1\)。输出最大权值点的权值。
数据范围:\(2 \le n \le 50000, 1 \le k \le 100000\)

树上两点 \(u,v\) 的路径指的是最短路径。可以把 \(u \rightarrow v\) 的路径分为两个部分:\(u \rightarrow LCA(u,v)\)\(LCA(u,v) \rightarrow v\)

先考虑简单的思路。首先对每条路径求 LCA,分别以 \(u\)\(v\) 为起点到 LCA,把路径上每个节点的权值加 \(1\);然后对所有路径进行类似操作。把路径上每个节点加 \(1\) 的操作的复杂度为 \(O(n)\),共 \(k\) 次操作,会超时。

本题的关键是如何记录路径上每个节点的修改。显然,如果真的对每个节点都记录修改,肯定会超时。我们可以利用差分,因为差分的用途是“把区间问题转换为断电问题”,适用这种情况。

给定数组 \(a\),定义差分数组 \(D[k]=a[k]-a[k-1]\),即数组相邻元素的差。

从差分数组的定义可以推出:\(a[k]=D[1]+D[2]+ \cdots + D[k] = \sum\limits_{i=1}^{k} D[i]\)

这个式子描述了 \(a\)\(D\) 的关系,即“差分是前缀和的逆运算” ,它把求 \(a[k]\) 转换为求 \(D\) 的前缀和。

对于区间 \([L,R]\) 的修改问题,比如把区间内每个元素都加上 \(d\),则可以对区间的两个端点 \(L\)\(R+1\) 做以下操作:

  1. \(D[L]\) 加上 \(d\)
  2. \(D[R+1]\) 减去 \(d\)

image

\(D\) 求前缀和,则可得到 \(a\) 数组,以上的更新相当于:

  1. \(1 \le x < L\)\(a[x]\) 不变;
  2. \(L \le x \le R\)\(a[x]\) 增加了 \(d\)
  3. \(R < x \le N\)\(a[x]\) 不变,因为被 \(D[R+1]\) 中减去的 \(d\) 抵消了。

利用差分能够把区间修改问题转换为只用端点做记录。如果不用差分数组,区间内每个元素都需要修改,时间复杂度为 \(O(n)\);转换为只修改两个端点后,时间复杂度降到 \(O(1)\),这就是差分的重要作用。

把差分思想用到树上,只需要把树上路径转换为区间即可。把一条路径 \(u \rightarrow v\) 分为两部分:\(u \rightarrow LCA(u,v)\)\(LCA(u,v) \rightarrow v\),这样每条路径都可以当成一个区间处理。

\(LCA(u,v)=R\),并记 \(R\) 的父节点为 \(F=fa[R]\),要把路径上每个节点权值加 \(1\),有:

  1. 路径 \(u \rightarrow R\) 这个区间上,\(D[u]++\)\(D[F]--\)
  2. 路径 \(v \rightarrow R\) 这个区间上,\(D[v]++\)\(D[F]--\)

经过以上操作,能通过 \(D\) 计算出 \(u \rightarrow v\) 上每个节点的权值。不过,由于两条路径在 \(R\)\(F\) 这里重合了,这两个步骤把 \(D[R]\) 加了两次,把 \(D[F]\) 减了两次,需要调整为 \(D[R]--\)\(D[F]--\)

image

在本题中,对每条路径都用倍增法求一次 LCA,并做一次差分操作。当对于所有路径都操作完成后,再做一次 DFS,求出每个节点的权值,所有权值中的最大值即为答案。

\(k\) 次 LCA 的时间复杂度为 \(O(n \log n + k \log n)\);最后做一次 DFS,时间复杂度为 \(O(n)\);总的时间复杂度为 \(O((n+k) \log n)\)

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 50005;
const int LOG = 16;
vector<int> tree[N];
int d[N], fa[N][LOG], a[N], ans;
void dfs(int cur, int pre) {d[cur] = d[pre] + 1;fa[cur][0] = pre;for (int i = 1; i < LOG; i++) fa[cur][i] = fa[fa[cur][i - 1]][i - 1];for (int nxt : tree[cur])if (nxt != pre) dfs(nxt, cur);
}
int lca(int x, int y) {if (d[x] < d[y]) swap(x, y);int len = d[x] - d[y];for (int i = LOG - 1; i >= 0; i--) if (1 << i <= len) {x = fa[x][i]; len -= 1 << i;}if (x == y) return x;for (int i = LOG - 1; i >= 0; i--) if (fa[x][i] != fa[y][i]) {x = fa[x][i]; y = fa[y][i];}return fa[x][0];
}
void calc(int cur, int pre) {for (int nxt : tree[cur])if (nxt != pre) {calc(nxt, cur); a[cur] += a[nxt];} ans = max(ans, a[cur]);
}
int main()
{int n, k;scanf("%d%d", &n, &k);for (int i = 1; i < n; i++) {int x, y;scanf("%d%d", &x, &y);tree[x].push_back(y); tree[y].push_back(x);}dfs(1, 0); // 计算每个节点的深度并预处理fa数组while (k--) {int s, t;scanf("%d%d", &s, &t);int r = lca(s, t);a[s]++; a[t]++; a[r]--; a[fa[r][0]]--; // 树上差分}  calc(1, 0); // 用差分数组求每个节点的权值printf("%d\n", ans);return 0;
}

边差分

例题:P6869 [COCI2019-2020#5] Putovanje

显然针对每一条边只会考虑购买单程票和多程票的一种,这取决于该条边被经过的次数 \(k\),这样一来这条边上的最少花费是 \(\min (k c_1, c_2)\)

这里需要根据若干条路径计算出每条边经过的次数,可以借助差分思想,注意它和点差分不同。对于边相关的问题,一般我们会将每个点与它父亲节点相连的边与该点绑定,从而将边上信息的维护转化为对点的信息的维护

image

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 200005;
const int LOG = 19;
vector<int> tree[N];
int d[N], fa[N][LOG], cnt[N], a[N], b[N], c1[N], c2[N];
void dfs(int cur, int pre) {d[cur] = d[pre] + 1;fa[cur][0] = pre;for (int i = 1; i < LOG; i++) fa[cur][i] = fa[fa[cur][i - 1]][i - 1];for (int nxt : tree[cur]) if (nxt != pre) dfs(nxt, cur);
}
int lca(int x, int y) {if (d[x] < d[y]) swap(x, y);int len = d[x] - d[y];for (int i = LOG - 1; i >= 0; i--) if ((1 << i) <= len) {x = fa[x][i]; len -= 1 << i;}if (x == y) return x;for (int i = LOG - 1; i >= 0; i--)if (fa[x][i] != fa[y][i]) {x = fa[x][i]; y = fa[y][i];}return fa[x][0];
}
void calc(int cur, int pre) {for (int nxt : tree[cur]) if (nxt != pre) {calc(nxt, cur);cnt[cur] += cnt[nxt];}
}
int main()
{int n;scanf("%d", &n);for (int i = 1; i < n; i++) {scanf("%d%d%d%d", &a[i], &b[i], &c1[i], &c2[i]);tree[a[i]].push_back(b[i]);tree[b[i]].push_back(a[i]);}dfs(1, 0);for (int i = 1; i < n; i++) {int r = lca(i, i + 1);cnt[i]++; cnt[i + 1]++; cnt[r] -= 2;}calc(1, 0);LL ans = 0;for (int i = 1; i < n; i++) {if (d[a[i]] > d[b[i]]) ans += min(1ll * c1[i] * cnt[a[i]], 1ll * c2[i]);else ans += min(1ll * c1[i] * cnt[b[i]], 1ll * c2[i]);}printf("%lld\n", ans);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hjln.cn/news/42711.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

SynProject 介绍---(synopse理解的版本控制和文档自动化生成)

SynProject 介绍---(synopse理解的版本控制和文档自动化生成) Synopse SynProject是一个用于Delphi项目的源代码版本控制和自动化文档生成的开源应用程序。它在GPL许可下发布。 有关其全部功能的完整列表,请参阅SynProject功能。源代码可从本源代码存储库获取。请选择上方的…

品牌全域人群资产增长飞轮

品牌人群资产是指品牌通过一系列营销活动和用户互动所积累的、对品牌有一定认知、情感和忠诚度的用户群体。这些资产是品牌的无形资产,对于品牌的长期成功和市场份额的扩张至关重要。 品牌人群资产可以模拟成一个多层次、多维度的球形结构体,其中核心用户群位于中心,随着用户…

TCP传输控制协议

网络编程 TCP传输控制协议 传输层中最为常见的两个协议分别是传输控制协议TCP(Transmission Control Protocol)和用户数据报协议UDP(User Datagram Protocol) 一、TCP协议的特点:TCP是面向连接的,端对端(一对一)的可靠的协议,可修复损坏数据,全双工的连接。 1.TCP协议判…

NPU与超异构计算杂谈

NPU与超异构计算杂谈 NPU 基础 近年来,随着人工智能技术的飞速发展,AI 专用处理器如 NPU(Neural Processing Unit)和 TPU(Tensor Processing Unit)也应运而生。这些处理器旨在加速深度学习和机器学习任务,相比传统的 CPU 和 GPU,它们在处理 AI 任务时表现出更高的效率和…

植物大战僵尸杂交版 官方版(作者:b站潜艇伟伟迷) 植物大战僵尸杂交版v2.0.88安装程序.exe

植物大战僵尸杂交版 官方版(作者:b站潜艇伟伟迷) 植物大战僵尸杂交版v2.0.88安装程序.exe阿里云盘链接: https://www.alipan.com/s/HgmmLFjHzj7 提取码: 15yc 【点击链接保存,或者复制本段内容,打开「阿里云盘」APP ,无需下载极速在线查看,视频原画倍速播放。】夸克网盘…

网络编程知识点

网络编程 两台主机的进程实现通信的方式 同一台主机中的实现进程间通信的方式有很多,比如管道、信号、消息队列、信号量集、共享内存等,如果现在需要两台主机间的进程实现数据传输,则想要用到套接字文件(socket)的,它的作用则是用于实现不同主机中的进程间通信的。 IP协议 …

MySQL存储

MySQL存储关系型数据库是基于关系模型的数据库,而关系模型是通过二维表来保存的,所以它的存储方式就是行列组成的表,每一列是一个字段,每一行是一条记录。表可以看作某个实体的集合,实体之间存在联系需要表与表之间的关联关系来体现,如主键外键的关联关系。多个表组成一个…

VMware-Ubuntu20.04配置双网卡解决远程连接的2种场景

需求场景环境:VMware Workstation下的虚拟机Ubuntu20.04 LTS 需求1:网卡1:桥接模式,在物理局域网与宿主机拥有同网段的IP,便于局域网通信 需求2:网卡2:NAT模式,假如不在家中局域网,宿主机未连入局域网时,Ubuntu使用NAT模式连接宿主机,共享使用宿主机网络,此时需要宿…