当前位置: 首页 > news >正文

【笔记】树链剖分三题(洛谷 P3384 树剖模板 P2146 软件包管理器 P2486 染色)

我不会讲的很细,这个笔记更适合学过树剖的跟着我的题单快速复习。

这个题单挑了树剖最精华的三道题,省选前掌握这些就可以。

树剖基础可以看这个老师的,写的很详细:

树链剖分详解(洛谷模板 P3384) - ChinHhh - 博客园 (cnblogs.com)


1.模板题

P3384 【模板】重链剖分/树链剖分 - 洛谷 (luogu.com.cn)

树链剖分的核心思想是将一棵树分解成若干条重链

使得任意两点间的路径可以被拆分成至多 O(log n) 条连续的链。

我们将每条链映射到一个线性数组上,并用线段树等数据结构,

来高效地维护和查询这些链上的信息(如区间最大值、区间和等)。

因为重链都是子树节点数最大的点连起来构成的,所以整棵树封装重链修改查询就很省时。

整个代码分为以下步骤:

(1)第一次 dfs 遍历整棵树:记录父节点数组、深度、子树大小、重儿子。

(2)第二次 dfs 优先遍历重儿子:进行重链剖分,生成 dfs 序、反 dfs 序(序对应到节点),

         以及最重要的 top 数组,top[x] 即 x 所在重链的顶端

(3)根据 dfs 序建线段树:叶子节点的值 = dfs 序为 L 的节点的权值(节点初始权值题目给了)

         为什么建线段树有用?因为要查找的时候是一条条重链找的,而重链 dfs 序是连续的,

         一段连续的区间就可以通过线段树来维护。

(4)正常线段树的 change,get_sum 函数,以及懒标记、pushup、pushdown。

(5)查询 x 到 y 路径 or 加上 z:一条条重链的遍历,直到 top[x] = top[y]。

(6)查询 x 子树 or 加上 z:直接 dfn[x],dfn[x] - siz[x] + 1。

要注意的几个点:

(1)只有同一条重链上的点可以区间查询,修改。

(2)每个点都在一条重链上,即使那条重链的开头是自己,或者重链只有一个节点。

(3)第一次 dfs 记录的父节点数组,是给第二次 dfs 不要走回头路,

         和 query 时跳到重链头上一个节点的。

时间复杂度:

  • 预处理时间复杂度(dfs & 建树):O(n)
  • 单次修改查询操作时间复杂度(线段树):O(log n)
  • 单次跳重链时间复杂度:O(log n)
  • 总时间复杂度(对于 m 次操作):O(n + m log² n)

易错点:

(1)没时时刻刻 %P

(2)dep 和 dfn 弄混了

(3)没有初始化

(4)线段树边界打错

(5)自己一行行代码对着看吧早晚能看出来

还有些细节写代码里了:

#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N = 1e5 + 10;LL num[N], P;
vector<int> G[N];
int fa[N], dep[N], siz[N], son[N];void fir_dfs(int x, int x_fa) {dep[x] = dep[x_fa] + 1;fa[x] = x_fa;siz[x] = 1;son[x] = -1;for (int y : G[x]) if (y != x_fa) {fir_dfs(y, x);if ( (son[x] == -1) || (siz[son[x]] < siz[y]) ) {  // 判断 -1 记得写前面,不然会越界 son[x] = y;}siz[x] += siz[y];}
}int dfn[N], r_dfn[N], top[N], tsp;void sec_dfs(int x, int tp) {   // tp 是 x 节点所在的重链头 tsp ++; dfn[x] = tsp;       // 别写成 dep 那样加了 r_dfn[dfn[x]] = x;top[x] = tp;if (son[x] != -1) {sec_dfs(son[x], tp);   // 优先遍历重儿子,与 x 同属一个重链头 }for (int y : G[x]) if (y != son[x] && y != fa[x]) {sec_dfs(y, y);       // 不是重儿子的儿子重链头只能是自己了 }
}#define lc(p) p << 1
#define rc(p) (p << 1) | 1struct node {int l, r;LL sum, lazy;    // 区间和,懒标记 
} tr[4 * N];void pushup(int p) {tr[p].sum = (tr[lc(p)].sum + tr[rc(p)].sum) % P;
}void pushdown(int p) {if (tr[p].lazy) { // 有懒标记才下放// 先更新左子树的 sumtr[lc(p)].sum = (tr[lc(p)].sum + (tr[lc(p)].r - tr[lc(p)].l + 1) * tr[p].lazy % P) % P;tr[lc(p)].lazy = (tr[lc(p)].lazy + tr[p].lazy) % P;// 再更新右子树的 sumtr[rc(p)].sum = (tr[rc(p)].sum + (tr[rc(p)].r - tr[rc(p)].l + 1) * tr[p].lazy % P) % P;tr[rc(p)].lazy = (tr[rc(p)].lazy + tr[p].lazy) % P;tr[p].lazy = 0;}
}void build(int p, int l, int r) {tr[p] = {l, r, 0, 0};if (l == r) {tr[p].sum = num[r_dfn[l]] % P;return ;}int mid = (l + r) >> 1;build(lc(p), l, mid);build(rc(p), mid + 1, r);pushup(p);             // 别忘了更新 p 
} void change(int p, int l, int r, LL c) {if ( (tr[p].l > r) || (tr[p].r < l) ) {   // 根本不沾边 return ;}if (l <= tr[p].l && tr[p].r <= r) { // 如果 p 节点管辖的范围夹在修改范围的中间 tr[p].sum = ( tr[p].sum + (tr[p].r - tr[p].l + 1) * c % P ) % P;tr[p].lazy = (tr[p].lazy + c) % P;return ;}pushdown(p);     // 下放懒标记 change(lc(p), l, r, c);change(rc(p), l, r, c);pushup(p);       // 改了就得更新 
}LL get_sum(int p, int l, int r) { if ( (l > tr[p].r) || (r < tr[p].l) ) {   // 不沾边 return 0;} if (l <= tr[p].l && tr[p].r <= r) {   // 如果 p 节点管辖的范围夹在查询范围的中间 return tr[p].sum;}pushdown(p);    // 注意!!这里虽然没有修改,但不一定放了懒标记,需要再放一次 return (get_sum(lc(p), l, r) + get_sum(rc(p), l, r)) % P;// 没改不用 pushup
}void change_path(int x, int y, LL z) {while (top[x] != top[y]) {if (dep[top[x]] < dep[top[y]]) {     // 保持 top[x] 的层数比 top[y] 大,也就是在树的下面应该跳上来 swap(x, y);}change(1, dfn[top[x]], dfn[x], z);     // 跳了一整条重链 x = fa[top[x]];      // x 跳跳跳到重链头的父节点 }// 现在 x 跳到了 y 所在的重链 if (dep[x] > dep[y]) {     // 这时候要 x 的层数比 y 小,又因为两者在一条重链上,也就是 dfn[x] < dfn[y]swap(x, y);}change(1, dfn[x], dfn[y], z);
}LL query_path(int x, int y) {LL res = 0;while (top[x] != top[y]) {if (dep[top[x]] < dep[top[y]]) {swap(x, y);}res = ( res + get_sum(1, dfn[top[x]], dfn[x]) ) % P;x = fa[top[x]];}if (dep[x] > dep[y]) {swap(x, y);}res = ( res + get_sum(1, dfn[x], dfn[y]) ) % P;return res;
}void change_subtree(int x, LL z) {change(1, dfn[x], dfn[x] + siz[x] - 1, z);   // 子树的 dfs 序都是连着的,直接改就行 
}LL query_subtree(int x) {return get_sum(1, dfn[x], dfn[x] + siz[x] - 1);
}int main () {ios::sync_with_stdio(false);cin.tie(0);int n, m, root;cin >> n >> m >> root >> P;for (int i = 1; i <= n; i ++) {cin >> num[i];}for (int i = 1; i < n; i ++) {int x, y;cin >> x >> y;G[x].push_back(y);G[y].push_back(x);}dep[0] = 0;fir_dfs(root, 0);tsp = 0;sec_dfs(root, root);build(1, 1, n);     // 这里传进去的 1 是 dfs 序,不是根节点的节点编号 for (int i = 1; i <= m; i ++) {int sig;cin >> sig;if (sig == 1) {int x, y; LL z;cin >> x >> y >> z;z %= P;change_path(x, y, z);}if (sig == 2) {int x, y;cin >> x >> y;cout << query_path(x, y) << "\n";}if (sig == 3) {int x; LL z;cin >> x >> z;z %= P;change_subtree(x, z);}if (sig == 4) {int x;cin >> x;cout << query_subtree(x) << "\n";}}return 0;
}

然后是一些比较板的练习:

P2590 [ZJOI2008] 树的统计 - 洛谷 (luogu.com.cn)

非常板,但是名题,可以当双倍经验(注意题目中节点的负权值)。

P3178 [HAOI2015] 树上操作 - 洛谷 (luogu.com.cn)

板,不想做可以不做。

 2.软件包管理器

P2146 [NOI2015] 软件包管理器 - 洛谷 (luogu.com.cn)

看上去和树剖没关系,但仔细看发现软件包之间的依赖关系就像一棵树(根节点为 0)。

(觉得 0 不好处理就全部节点编号 + 1)

那就好办了,线段树结构体里面 sum(这段范围里安装的软件包个数)、

标识(全打开为 1,全关为 0,混乱为 -1)安排上就行。

要注意的点:

(1)安装软件包是 x 到根节点全部为 1,卸载软件包是 x 的子树全部为 0。

(2)标识和懒标记不一样,请谨慎对待。

(3)如果你选择 + 1,记得题目全部读入都要 + 1。

详见代码:

#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N = 1e5 + 10;vector<int> G[N];
int fa[N], dep[N], siz[N], son[N];void fir_dfs(int x, int x_fa) {dep[x] = dep[x_fa] + 1;fa[x] = x_fa;siz[x] = 1;son[x] = -1;for (int y : G[x]) if (y != x_fa) {fir_dfs(y, x);if ( (son[x] == -1) || (siz[son[x]] < siz[y]) ) {  son[x] = y;}siz[x] += siz[y];}
}int dfn[N], r_dfn[N], top[N], tsp;void sec_dfs(int x, int tp) {tsp ++; dfn[x] = tsp;   r_dfn[dfn[x]] = x;top[x] = tp;if (son[x] != -1) {sec_dfs(son[x], tp); }for (int y : G[x]) if (y != son[x] && y != fa[x]) {sec_dfs(y, y);    }
}#define lc(p) p << 1
#define rc(p) (p << 1) | 1struct node {int l, r;LL sum, sig;    // [l, r] 打开了的软件包的个数,标识(全打开为 1,全关为 0,混乱为 -1) 
} tr[4 * N];void pushup(int p) {tr[p].sum = tr[lc(p)].sum + tr[rc(p)].sum;if (tr[lc(p)].sig == tr[rc(p)].sig) {tr[p].sig = tr[lc(p)].sig;}else {tr[p].sig = -1;}
}void pushdown(int p) {if (tr[p].sig != -1) {tr[lc(p)].sig = tr[rc(p)].sig = tr[p].sig;tr[lc(p)].sum = tr[lc(p)].sig * (tr[lc(p)].r - tr[lc(p)].l + 1);tr[rc(p)].sum = tr[rc(p)].sig * (tr[rc(p)].r - tr[rc(p)].l + 1);}
}void build(int p, int l, int r) {tr[p] = {l, r, 0, 0};if (l == r) {return ;}int mid = (l + r) >> 1;build(lc(p), l, mid);build(rc(p), mid + 1, r);pushup(p);         
} int res;void change(int p, int l, int r, int c) { if ( (l > tr[p].r) || (r < tr[p].l) ) { return ;} if (l <= tr[p].l && tr[p].r <= r) {   if (c == 0) {res += tr[p].sum;} else {res += (tr[p].r - tr[p].l + 1) - tr[p].sum;}tr[p].sig = c;tr[p].sum = c * (tr[p].r - tr[p].l + 1);return ;}pushdown(p);   change(lc(p), l, r, c);change(rc(p), l, r, c);pushup(p);
}int main () {ios::sync_with_stdio(false);cin.tie(0);int n;cin >> n;for (int i = 2; i <= n; i ++) {int x;cin >> x;x ++;G[x].push_back(i);}dep[0] = 0;fir_dfs(1, 0);tsp = 0;sec_dfs(1, 1);build(1, 1, n);   int m;cin >> m;for (int i = 1; i <= m; i ++) {char s[20];cin >> s;if (s[0] == 'i') {int x;cin >> x;x ++;res = 0;while (x != 0) {change(1, dfn[top[x]], dfn[x], 1);x = fa[top[x]];}cout << res << "\n";}else {int x;cin >> x;x ++;res = 0;change(1, dfn[x], dfn[x] + siz[x] - 1, 0);cout << res << "\n";}}return 0;
}

3. 染色

P2486 [SDOI2011] 染色 - 洛谷 (luogu.com.cn)

难点在如何用线段树实现染色。

(以下讲述,端点是 dfs 树上的点,节点的线段树上的点)

线段树结构体里面多定义 l_col 和 r_col,分别代表管辖范围左右端点的颜色

再定义个 cnt 表示当前节点管辖范围的颜色段数量

更新的时候比较左节点的 r_col 和右节点 l_col,如果一样 tr[p].cnt = 左右节点 cnt 和 - 1。

不然就 tr[p].cnt = 左右节点 cnt。

你以为这样就完了?

发挥惊人的注意力,发现 get_sum 函数没那么简单,要返回一个结构体。

node get_node(int p, int l, int r) {if (tr[p].r < l || tr[p].l > r) {return (node){0, 0, -1, -1, 0, -1};}if (l <= tr[p].l && tr[p].r <= r) {return tr[p];}pushdown(p);node res;node a = get_node(lc(p), l, r);node b = get_node(rc(p), l, r);res = merge(a, b);return res;
}

我们考虑构造一个函数 merge,负责合并两个节点,和 pushup 差不多。

node merge(node a, node b) {node res;if (a.cnt == 0) {return b;}if (b.cnt == 0) {return a;}res.l_col = a.l_col;res.r_col = b.r_col;res.cnt = a.cnt + b.cnt;if (a.r_col == b.l_col) {res.cnt --;}return res;
}

同时 pushup 函数也应该保留,防止混乱。

再来看询问路径函数:

int query_path(int x, int y) {node t_x = {0, 0, -1, -1, 0, -1};node t_y = {0, 0, -1, -1, 0, -1};while (top[x] != top[y]) {if (dep[top[x]] > dep[top[y]]) {node no = get_node(1, dfn[top[x]], dfn[x]);t_x = merge(no, t_x);x = fa[top[x]];}else {node no = get_node(1, dfn[top[y]], dfn[y]);t_y = merge(no, t_y);y = fa[top[y]];}}if (dep[x] > dep[y]) {swap(x, y);swap(t_x, t_y);}node no = get_node(1, dfn[x], dfn[y]);node res = merge(no, t_y);swap(t_x.l_col, t_x.r_col);res = merge(t_x, res);return res.cnt;
}

我们需要建两个结构体分别记录 x 和 y 的重链,防止混淆。

最后合并时优先把同端点的合并。

我们设一开始的 x y 就是 x y,while 之后的 x y 是 tx 和 ty。

那么 t_x 就是 tx -> x 的链, t_y 就是 ty -> y 的链。

(因为默认求重链就是 dfn 小的 -> dfn 大的)

no 就是 tx - > ty 的链。

那么我们先把 no 和 y 连起来变成 tx - > y,

再把 t_x 的前后翻转,变成 x -> tx。

最后 t_x 和 no 合并,x -> tx -> ty -> y。

最后注意颜色初始化要等于 -1,懒标记只有不等于 -1 的时候才 pushup,

pushup 和 pushdown 不要忘写了或者写多了,这道题应该就没问题。

完整代码:

#include<bits/stdc++.h>
using namespace std;typedef long long LL;
const int N = 1e5 + 10;int num[N];
vector<int> G[N];
int fa[N], siz[N], son[N], dep[N];void fir_dfs(int x, int x_fa) {fa[x] = x_fa;siz[x] = 1;son[x] = -1;dep[x] = dep[x_fa] + 1;for (int y : G[x]) if (y != x_fa) {fir_dfs(y, x);if ( (son[x] == -1) || (siz[son[x]] < siz[y]) ) {son[x] = y;}siz[x] += siz[y];}
}int tsp, dfn[N], r_dfn[N], top[N];void sec_dfs(int x, int tp) {tsp ++;dfn[x] = tsp;r_dfn[dfn[x]] = x;top[x] = tp;if (son[x] != -1) {sec_dfs(son[x], tp);}for (int y : G[x]) if (y != son[x] && y != fa[x]) {sec_dfs(y, y);}
}#define lc(p) p << 1
#define rc(p) (p << 1) | 1struct node {int l, r;int l_col, r_col;    // 左右端点的颜色 int cnt, lazy;    // 颜色段数量,懒标记
} tr[4 * N];void pushup(int p) {tr[p].l_col = tr[lc(p)].l_col;tr[p].r_col = tr[rc(p)].r_col;tr[p].cnt = tr[lc(p)].cnt + tr[rc(p)].cnt;if (tr[lc(p)].r_col == tr[rc(p)].l_col) {tr[p].cnt --;}
}void pushdown(int p) {if (tr[p].lazy != -1) {tr[lc(p)].l_col = tr[lc(p)].r_col = tr[p].lazy;tr[lc(p)].cnt = 1;tr[lc(p)].lazy = tr[p].lazy;tr[rc(p)].l_col = tr[rc(p)].r_col = tr[p].lazy;tr[rc(p)].cnt = 1;tr[rc(p)].lazy = tr[p].lazy;tr[p].lazy = -1; }
}void build(int p, int l, int r) {tr[p] = {l, r, -1, -1, r - l + 1, -1};   // 有关颜色的都一开始等于 -1 if (l == r) {tr[p].l_col = tr[p].r_col = num[r_dfn[l]];return ; }int mid = (l + r) >> 1;build(lc(p), l, mid);build(rc(p), mid + 1, r);pushup(p);
}void change(int p, int l, int r, int c) {if (tr[p].r < l || tr[p].l > r) {return ;}if (l <= tr[p].l && tr[p].r <= r) {tr[p].l_col = tr[p].r_col = c;tr[p].cnt = 1;tr[p].lazy = c;return ;}pushdown(p);change(lc(p), l, r, c);change(rc(p), l, r, c);pushup(p);
}node merge(node a, node b) {node res;if (a.cnt == 0) {return b;}if (b.cnt == 0) {return a;}res.l_col = a.l_col;res.r_col = b.r_col;res.cnt = a.cnt + b.cnt;if (a.r_col == b.l_col) {res.cnt --;}return res;
}node get_node(int p, int l, int r) {if (tr[p].r < l || tr[p].l > r) {return (node){0, 0, -1, -1, 0, -1};}if (l <= tr[p].l && tr[p].r <= r) {return tr[p];}pushdown(p);node res;node a = get_node(lc(p), l, r);node b = get_node(rc(p), l, r);res = merge(a, b);return res;
}void change_path(int x, int y, int c) {while (top[x] != top[y]) {if (dep[top[x]] < dep[top[y]]) {swap(x, y);}change(1, dfn[top[x]], dfn[x], c);x = fa[top[x]];}if (dep[x] > dep[y]) {swap(x, y);}change(1, dfn[x], dfn[y], c);
}int query_path(int x, int y) {node t_x = {0, 0, -1, -1, 0, -1};node t_y = {0, 0, -1, -1, 0, -1};while (top[x] != top[y]) {if (dep[top[x]] > dep[top[y]]) {node no = get_node(1, dfn[top[x]], dfn[x]);t_x = merge(no, t_x);x = fa[top[x]];}else {node no = get_node(1, dfn[top[y]], dfn[y]);t_y = merge(no, t_y);y = fa[top[y]];}}if (dep[x] > dep[y]) {swap(x, y);swap(t_x, t_y);}node no = get_node(1, dfn[x], dfn[y]);node res = merge(no, t_y);swap(t_x.l_col, t_x.r_col);res = merge(t_x, res);return res.cnt;
}int main () {ios::sync_with_stdio(false);cin.tie(0);int n, m;cin >> n >> m;for (int i = 1; i <= n; i ++) {cin >> num[i];}for (int i = 1; i < n; i ++) {int x, y;cin >> x >> y;G[x].push_back(y);G[y].push_back(x);}dep[0] = 0;fir_dfs(1, 0);tsp = 0;sec_dfs(1, 1);build(1, 1, n);for (int i = 1; i <= m; i ++) {char s[5];cin >> s;if (s[0] == 'C') {int x, y, c;cin >> x >> y >> c;change_path(x, y, c);}else {int x, y;cin >> x >> y;cout << query_path(x, y) << "\n";}}return 0;
}

http://www.dtcms.com/a/461600.html

相关文章:

  • 建设银行网站用户名忘了怎么办wordpress标签链接优化
  • 文献阅读:A Survey of Edge Caching: Key Issues and Challenges
  • 信号140上岸山东师范经验。
  • 力扣面试经典150题day1第一题(lc88),第二题(lc27)
  • asp 网站开发 软件做期货主要看哪几个财经网站
  • JavaScript实现防抖、节流【带思路】
  • 汇川高压变频器故障解析F79 F90
  • kanass入门到实战(13) - 如何通过评审,有效保障需求和用例的质量
  • 深度解析:Redis缓存三大核心问题(穿透/击穿/雪崩)的技术原理与企业级解决方案
  • 最专业网站建设哪家好微网站微名片
  • 上海兆越通讯闪耀第二十五届中国国际工业博览会
  • 车库到双子星:惠普的百年科技传奇
  • 网站防止恶意注册dedecms菜谱网站源码
  • 基于IoT的智能温控空调系统设计与实现
  • 网站开发常用的框架营销到底是干嘛的
  • 老题新解|组合数问题
  • Java 工具类详解:Arrays、Collections、Objects 一篇通关
  • Cucumber自学导航
  • docker案例
  • 网站如何做提现功能上海市城乡和住房建设厅网站
  • 南宁 网站建设 公司老吕爱分享 wordpress
  • python 矩阵中寻找就接近的目标值 (矩阵-中等)含源码(八)
  • 嵌入式Linux:线程中信号处理
  • docker启动容器慢,很慢,特别慢的坑
  • C#基础14-非泛型集合
  • 【22.1-决策树的构建1】
  • asp制作网站wordpress使用端口
  • 【机器学习】(一)实用入门指南——如何快速搭建自己的模型
  • 【数值分析】插值法实验
  • 地方门户网站的前途搜索引擎大全全搜网