【算法】树链剖分

news/2024/10/25 5:16:35/文章来源:https://www.cnblogs.com/Daniel-yao/p/18489776

1. 算法简介

树链剖分为将树分割成若干条链,维护树上信息的思想。通常将其分为链后能用数据结构维护。

树链剖分分为重链剖分,长链剖分,实链剖分。通常重链剖分最常用,本文主要介绍重链剖分。

重链剖分可将树划分为一个个长度不超过 \(O(\log n)\) 的链,并且保证每条链内的 \(dfs\) 序是连续的。这样就可以用线性数据结构维护树内的信息了。

比如:

  • 动态修改树的节点(边)权值,路径信息,子树信息;
  • 查询树上路径信息,子树信息;
  • 求解 \(LCA\)

2. 算法实现

给出一些定义:

  1. 重儿子:表示在该节点中子树最大的子节点(若有多个重儿子,选其一)。
  2. 轻儿子:表示在该节点中不是重儿子的子节点。
  3. 重边:与重儿子相连的边;
  4. 轻边:与轻儿子相连的边;
  5. 重链:由若干重边组成的链;
  6. 链顶:树链中节点深度最小的节点编号,链顶节点与其父节点的连边为轻边,根节点一定为链顶节点

给出一些需要维护的数据:

  1. \(fa_x\) 表示 \(x\) 的父节点;
  2. \(siz_x\) 表示 \(x\) 的子树大小;
  3. \(dep_x\) 表示 \(x\) 的节点深度;
  4. \(dfn_x\) 表示节点 \(x\) 对应的 \(dfs\) 序;
  5. \(id_x\) 表示 \(dfs\)\(x\) 对应的节点编号;
  6. \(son_x\) 表示节点 \(x\) 的重儿子编号;
  7. \(top_x\) 表示节点 \(x\) 所在链的链顶节点的编号;

HLD

通过两边 \(dfs\) 维护出上述信息:

第一遍 \(dfs\) 维护出树的 \(fa,siz,dep,son\) 的信息。

void dfs(int x, int f) {dep[x] = dep[f] + 1, siz[x] = 1;fa[x] = f;int maxi = 0;for (int i = h[x]; i; i = e[i].nx) {int y = e[i].v;if(y == fa[x]) continue;w[y] = e[i].w;s[y] = s[x] ^ w[y];dfs(y, x);siz[x] += siz[y];if(maxi < siz[y]) {maxi = siz[y];son[x] = y;}}
}

第二遍 \(dfs\) 优先走重儿子,再走轻儿子,并在期间依次定 \(dfn,id\),并更新 \(top\) 的信息。

void dfs1(int x, int tp) {top[x] = tp;dfn[x] = ++idx;id[idx] = x;if(son[x]) dfs1(son[x], tp);for (int i = h[x]; i; i = e[i].nx) {int y = e[i].v;if(y == fa[x] || y == son[x]) continue;dfs1(y, y);}
}

走重儿子的时候链顶节点为原链顶节点,而走轻儿子的时候由于会重新划分一条链,所以将链顶节点信息 \(tp\) 更新为子节点后再进行遍历。最后遍历得到的 \(dfs\) 序在一条链内一定为连续的。并且保证树上每个节点都属于且仅属于一条重链

基于此性质,可以使用线性数据结构将树上路径转化为区间,并进行相应的操作。

可以发现,每一次向下经过一条轻边时,所在子树大小会至少减半。

所以,我们可以很快的胡出求解 \(LCA\) 的方法。

2.1 求 LCA

对于 \(u, v\),若它们所在的链顶为同一个,即它们在同一条链中,则 \(LCA(u,v)\) 为深度较小的那个节点。

否则,我们需要让它们跳到同一个链中去。先比较当前的 \(dep_{top_u}\)\(dep_{top_v}\),肯定是将所在链顶更深的那个节点跳到链顶,再跳出本链到达更浅链。如此往复直到它们在同一条链内。

int lca(int u, int v) {while(top[u] != top[v]) {if(dep[top[u]] < dep[top[v]]) swap(u, v);u = fa[top[u]];}if(dep[u] > dep[v]) swap(u, v);return u;
}

2.2 路径修改/查询

和求解 \(LCA\) 类似,不过这次需要维护路径的信息。于是在跳链顶的父节点之前对本链的贡献进行修改:

  1. 未在同一条链中时,在数据结构中修改区间 \([dfn_{top_x},dfn_x]\),因为保证了 \(dfn_{top_x}<dfn_x\)
  2. 在同一条链中,在数据结构中修改区间 \([dfn_v,dfn_x]\),前提要保证 \(dfn_v<dfn_x\)
void upd(int x, int y, int k) {while(top[x] != top[y]) {if(dep[top[x]] < dep[top[y]]) swap(x, y);update(1, dfn[top[x]], dfn[x], k);x = fa[top[x]];}if(dep[x] < dep[y]) swap(x, y);update(1, dfn[y], dfn[x], k);
}

update函数为数据结构修改操作,以模版题为例,则为带标记的区间加。

修改操作同理:

int qry(int x, int y) {int ans = 0;while(top[x] != top[y]) {if(dep[top[x]] < dep[top[y]]) swap(x, y);ans = (ans % mod + query(1, dfn[top[x]], dfn[x]) % mod) % mod;x = fa[top[x]];}if(dep[x] < dep[y]) swap(x, y);ans = (ans % mod + query(1, dfn[y], dfn[x]) % mod) % mod;return ans;
}

以模版题为例的区间加和,query函数为数据结构查询操作。

2.3 子树修改/查询

可以发现,子树内的 \(dfn\) 序是连续的,为 \([dfn_x,dfn_x+siz_x-1]\)。于是直接在数据结构上查询/修改 \([dfn_x,dfn_x+siz_x-1]\) 即可。

void Upd(int x, int k) {update(1, dfn[x], dfn[x] + siz[x] - 1, k);
}int Qry(int x) {return query(1, dfn[x], dfn[x] + siz[x] - 1) % mod;
}

以上的所有的树上跳点操作均为 \(O(\log n)\),即单次求 \(LCA\)\(O(\log n)\)。而套上数据结构维护树上信息,以线段树为例,则为 \(O(\log ^2 n)\)。但是由于树上跳点的 \(O(\log n)\) 常数很小,并且理论上限即为 \(\log n\) 严重跑不满。所以一般 \(n\le 5\times 10^5\) 是很稳定。

加上操作数 \(m\) 总时间复杂度为 \(O(m\log^2 n)\)

P3384 【模板】重链剖分/树链剖分

将所有操作拼起来即可。

#include <bits/stdc++.h>
#define int long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003-
#define ls p<<1
#define rs p<<1|1using namespace std;namespace Read {template <typename T>inline void read(T &x) {x=0;T f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}x*=f;}template <typename T, typename... Args>inline void read(T &t, Args&... args) {read(t), read(args...);}
}using namespace Read;void print(int x){if(x<0){putchar('-');x=-x;}if(x>9){print(x/10);putchar(x%10+'0');}else putchar(x+'0');return;
}const int N = 1e5 + 10;struct Node {int v, nx;
} e[N << 1];struct Tree {int l, r, val, add;
} t[N << 2];int n, m, r, mod, h[N], tot, w[N], dep[N], fa[N], dfn[N], id[N], son[N], top[N], siz[N], idx;void add(int u, int v) {e[++tot] = (Node) {v, h[u]};h[u] = tot;
}void dfs(int x, int f) {dep[x] = dep[f] + 1;fa[x] = f, siz[x] = 1;int maxi = -1;for (int i = h[x]; i; i = e[i].nx) {int y = e[i].v;if(y == f) continue;dfs(y, x);siz[x] += siz[y]; if(maxi < siz[y]) {maxi = siz[y];son[x] = y;}}
}void dfs1(int x, int tp) {top[x] = tp;dfn[x] = ++idx;id[idx] = x;if(son[x]) dfs1(son[x], tp);for (int i = h[x]; i; i = e[i].nx) {int y = e[i].v;if(y == fa[x] || y == son[x]) continue;dfs1(y, y);}
}void pushup(int p) {t[p].val = (t[ls].val % mod + t[rs].val % mod) % mod;
}void pushdown(int p) {if(t[p].add) {t[ls].val = (t[ls].val % mod + t[p].add * (t[ls].r - t[ls].l + 1) % mod) % mod;t[rs].val = (t[rs].val % mod + t[p].add * (t[rs].r - t[rs].l + 1) % mod) % mod;t[ls].add = (t[ls].add % mod + t[p].add % mod) % mod;t[rs].add = (t[rs].add % mod + t[p].add % mod) % mod;t[p].add = 0; }
}void build(int p, int l, int r) {t[p].l = l, t[p].r = r;if(l == r) {t[p].val = w[id[l]] % mod;return ; }int mid = (l + r) >> 1;build(ls, l, mid);build(rs, mid + 1, r);pushup(p); 
}void update(int p, int l, int r, int k) {if(l <= t[p].l && t[p].r <= r) {t[p].val = (t[p].val % mod + k * (t[p].r - t[p].l + 1) % mod) % mod;t[p].add = (t[p].add % mod + k % mod) % mod;return ;  }pushdown(p);int mid = (t[p].l + t[p].r) >> 1;if(l <= mid) update(ls, l, r, k);if(r > mid) update(rs, l, r, k);pushup(p);
}int query(int p, int l, int r) {if(l <= t[p].l && t[p].r <= r) {return t[p].val % mod;}pushdown(p);int mid = (t[p].l + t[p].r) >> 1, ans = 0;if(l <= mid) ans = (ans + query(ls, l, r) % mod) % mod;if(r > mid) ans = (ans + query(rs, l, r) % mod) % mod;return ans;
}void upd(int x, int y, int k) {while(top[x] != top[y]) {if(dep[top[x]] < dep[top[y]]) swap(x, y);update(1, dfn[top[x]], dfn[x], k);x = fa[top[x]];}if(dep[x] < dep[y]) swap(x, y);update(1, dfn[y], dfn[x], k);
}int qry(int x, int y) {int ans = 0;while(top[x] != top[y]) {if(dep[top[x]] < dep[top[y]]) swap(x, y);ans = (ans % mod + query(1, dfn[top[x]], dfn[x]) % mod) % mod;x = fa[top[x]];}if(dep[x] < dep[y]) swap(x, y);ans = (ans % mod + query(1, dfn[y], dfn[x]) % mod) % mod;return ans;
}void Upd(int x, int k) {update(1, dfn[x], dfn[x] + siz[x] - 1, k);
}int Qry(int x) {return query(1, dfn[x], dfn[x] + siz[x] - 1) % mod;
}signed main() {read(n, m, r, mod);For(i,1,n) read(w[i]);For(i,1,n-1) {int u, v;read(u, v);add(u, v);add(v, u);}dfs(r, 0);dfs1(r, r);build(1, 1, n);while(m--) {int op, x, y, z;read(op);if(op == 1) {read(x, y, z);z %= mod;upd(x, y, z);} else if(op == 2) {read(x, y);cout << qry(x, y) << '\n';} else if(op == 3) {read(x, z);z %= mod;Upd(x, z);} else {read(x);cout << Qry(x) << '\n';}}return 0;
}

P4114 Qtree1

需要维护边权而不是点权,于是边权转点权维护即可。(边权打在深儿子上)。

细节注意的是转点权之后 \(u,v\)\(LCA\) 的点权其实为 \(LCA\) 的父节点与 \(LCA\) 相连的边权。所以在修改/查询时不能将 \(LCA\) 节点的点权算入其中。

#include<bits/stdc++.h>
#define int long long
#define ls p<<1
#define rs p<<1|1
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)using namespace std;const int N = 1e5 + 10;struct node {int l, r, val;
} t[N << 2];struct edge {int u, v;
} E[N];struct Node {int v, w, nx;
} e[N << 1];int n, h[N], tot, fa[N], son[N], siz[N], dep[N], top[N], dfn[N], id[N], w[N], idx;void add(int u, int v, int w) {e[++tot] = (Node){v, w, h[u]};h[u] = tot;
}void dfs(int x, int f) {fa[x] = f, siz[x] = 1;dep[x] = dep[f] + 1;int maxi = 0;for (int i = h[x]; i; i = e[i].nx) {int y = e[i].v;if(y == f) continue;w[y] = e[i].w;dfs(y, x);siz[x] += siz[y];if(maxi < siz[y]) {son[x] = y;maxi = siz[y];}}
}void dfs1(int x, int tp) {top[x] = tp;dfn[x] = ++idx;id[idx] = x;if(son[x]) dfs1(son[x], tp);for (int i = h[x]; i; i = e[i].nx) {int y = e[i].v;if(y == fa[x] || y == son[x]) continue;dfs1(y, y);}
}void pushup(int p) {t[p].val = max(t[ls].val, t[rs].val);
}void build(int p, int l, int r) {t[p].l = l, t[p].r = r;if(l == r) {t[p].val = w[id[l]];return ;}int mid = l + r >> 1;build(ls, l, mid);build(rs, mid + 1, r);pushup(p);
}void upd(int p, int x, int k) {if(t[p].l == t[p].r) {t[p].val = k;return ;}int mid = t[p].l + t[p].r >> 1;if(x <= mid) upd(ls, x, k);else upd(rs, x, k);pushup(p);
}int qry(int p, int l, int r) {if(l <= t[p].l && t[p].r <= r) {return t[p].val;}int mid = t[p].l + t[p].r >> 1, ans = 0;if(l <= mid) ans = max(ans, qry(ls, l, r));if(r > mid) ans = max(ans, qry(rs, l, r));return ans;
}int query(int x, int y) {int ans = 0;while(top[x] != top[y]) {if(dep[top[x]] < dep[top[y]]) swap(x, y);ans = max(ans, qry(1, dfn[top[x]], dfn[x]));x = fa[top[x]];}if(dep[x] < dep[y]) swap(x, y);ans = max(ans, qry(1, dfn[y]+1, dfn[x]));return ans;
}signed main() {ios::sync_with_stdio(0);cin.tie(0), cout.tie(0);cin >> n;For(i,1,n-1) {int u, v, w;cin >> u >> v >> w;add(u, v, w);add(v, u, w);E[i] = (edge){u, v};}dfs(1, 0);dfs1(1, 1);build(1, 1, idx);string op;while(cin >> op) {if(op == "DONE") break;int u, v;cin >> u >> v;if(op == "QUERY") {if(u == v) cout << 0 << '\n';else cout << query(u, v) << '\n';} else {if(dep[E[u].u] < dep[E[u].v]) swap(E[u].u, E[u].v);upd(1, dfn[E[u].u], v);}}return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Meta 最新 SPIRIT-LM:语音文本无缝转换还能懂情绪;字节回应实习生破坏大模型训练:网传损失不实丨 RTE 开发者日报

开发者朋友们大家好:这里是「RTE 开发者日报」,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文章 」、「有看点的 会议 」,但内容仅代表编辑的…

通义灵码实践教程——编码使用实践

点击此处,立即下载通义灵码!https://tongyi.aliyun.com/lingma/ 通义灵码最佳使用实践参考 通义灵码是JetBrains或VSCode集成开发环境(IDE)中嵌入的一款智能开发助手工具,旨在通过人工智能技术简化软件开发过程,提升开发效率。本文将介绍在开发过程中如何深度体验多种辅助…

通义灵码实践教程——单元测试

通义灵码加持的单元测试实践 本文首先讲述了什么是单元测试、单元测试的价值、一个好的单元测试所具备的原则,进而引入如何去编写一个好的单元测试,通义灵码是如何快速生成单元测试的。 什么是单元测试? 单元测试是一种软件测试方法,通过编写代码来验证应用程序中最小的可测…

2024秋软工实践 银河战舰队展示与选题报告

作业所属课程 班级的链接作业要求 https://edu.cnblogs.com/campus/fzu/SE2024/homework/13290作业的目标 开发一个基于LLM大模型接口的软件,为传统的软件赋予全新的体验和功能。团队名称 银河战舰团队成员学号-名字 102202129-林伟宏 102202131-林鑫 102202109-木合塔拉提 10…

geojson的下载与展示

下载地址:https://datav.aliyun.com/portal/school/atlas/area_selecto 展示地址:https://l7editor.antv.antgroup.com/

VMware低版本打开高版本虚拟机

前言全局说明VMware低版本打开高版本虚拟机一、说明 环境: Windows 11 家庭版 23H2 22631.3737二、注意修改前,备份虚拟机文件 为了数据安全,最好不要手动修改虚拟机配置信息 最好使用对应版本创建的虚拟机三、版本对应 https://www.cnblogs.com/wutou/p/17712402.html四、修…

【验证码识别专栏】大炮打麻雀 — CLIP 图文多模态模型,人均通杀 AIGC 六、九宫格验证码

前言 近期有粉丝私信,不知道如何训练某讯系点选验证码,之前星球群也有不少粉丝讨论相关问题,为满足粉丝们的需求,本文将对这型验证码的训练进行讲解, 文末可以下载相关的工具,包括 文章配套标注工具 + 文章配套训练代码 + 部分学习数据集(少量类目,仅供学习使用,不设计…

数据库—多表查询、事务

1.多表查询: 例:点击查看代码 # 创建部门表 CREATE TABLE dept( did INT PRIMARY KEY AUTO_INCREMENT, dname VARCHAR(20) );# 创建员工表 CREATE TABLE emp ( id INT PRIMARY KEY AUTO_INCREMENT, NAME VARCHAR(10), gender CHAR(1), -- 性别 salary DOUBLE, -- 工资 join_d…

021 天气案例

@click后面也可以写一些简单语句,这样就不用配置methods了

通义灵码操作指南——插件配置指南

点击链接,立即下载通义灵码插件:https://tongyi.aliyun.com/lingma/ 通义灵码支持在 Visual Studio Code、JetBrains IDEs 中修改常用快捷键、进行行间生成的启用/禁用等功能开关配置。 Visual Studio Code 中配置通义灵码 准备工作 如果需要在 Visual Studio Code 中使用通义…

1200PLC通过NODERED,将数据发布到阿里云物联网平台

配置要求:1,电脑上需要安装有博图软件,我这里使用的是TIA Portal V16版本 2,电脑上需要安装NODE_RED 3,已经有阿里云物联网平台账号。新建PLC项目,编写PLC程序, *新建PLC项目,我这里硬件为cpu1214,dcdc_R| | | | | ---- | ---- | ---- | | | …

织梦数据库主表?dedecms数据库包含那些表

以下是织梦CMS (DedeCMS) 数据库表的汇总表格,包括主要表及其用途:表名 用途dede_admin 管理员信息表,存储管理员账号、密码、权限等信息。dede_addonarticle 附加文章表,存储文章的详细内容。dede_arctype 栏目类型表,存储网站栏目的分类信息。dede_archives 文档主表,存…