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

浅谈dsu on tree

前言

先学树剖。

讲讲启发式合并,最经典的就是并查集的按秩合并,这里不细讲。

常用的启发式合并就是小集合合并到大集合上,复杂度从 \(O(n^2)\) 优化至 \(O(n \log n)\)

例题

P3201 [HNOI2009] 梦幻布丁

题目描述

\(n\) 个布丁摆成一行,进行 \(m\) 次操作。每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色。

例如,颜色分别为 \(1,2,2,1\) 的四个布丁一共有 \(3\) 段颜色.

set或链表的启发式合并板。

考虑没有修改怎么做,显然统计一下有多少个 \(color[i]\ne color[i-1]\) 即可,复杂度 \(O(n)\)

考虑如何暴力修改,用set或链表维护每个颜色,修改时判断一下一个位置两边是不是要变的颜色,如果是 \(ans--\)

我们发现 \(x\)\(y\)\(y\)\(x\) 对答案没有影响,所以用 \(siz\) 小的链表合并到大的上面,复杂度 \(O(n \log n)\)

code

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N = 1e6 + 5;
int n, q, a[N];
int head[N], nxt[N], siz[N];
int ans;
int f[N * 10];inline void merge(int &x, int &y) {if (siz[x] > siz[y]) swap(x, y);if (!siz[x] || x == y) return ;for (int i = head[x]; i; i = nxt[i]) ans -= (a[i - 1] == y) + (a[i + 1] == y);for (int i = head[x]; i; i = nxt[i]) {a[i] = y;if (nxt[i] == 0) {nxt[i] = head[y];head[y] = head[x];break;}}siz[y] += siz[x];siz[x] = 0;head[x] = 0;
}signed main() {ios::sync_with_stdio(0);cin.tie(0), cout.tie(0);cin >> n >> q;for (int i = 1; i < N * 10; i++) f[i] = i;ans = 1;for (int i = 1; i <= n; i++) { cin >> a[i];nxt[i] = head[a[i]];head[a[i]] = i;siz[a[i]]++;if (nxt[i] != i - 1) ans++;}	int opt, x, y;while (q--) {cin >> opt;if (opt == 1) {cin >> x >> y;merge(f[x], f[y]);} else {cout << ans << endl;}}return 0;
}

dsu on tree

算法简介

考虑这样的一类树上统计问题,无修改且允许离线,统计子树信息。

这就是dsu on tree 处理的问题。

算法流程

考虑暴力怎么做,遍历每个子节点的贡献->处理答案->删除子节点贡献。

dsu on tree的实现 :

  1. 遍历所有轻儿子,递归时删除贡献。
  2. 遍历重儿子,保留贡献。
  3. 处理轻儿子的贡献。
  4. 处理当前节点的答案。
  5. 如果该点为轻儿子,删除该点贡献。

为什么不保留多个节点的贡献?

因为会混淆子树之间的信息,由于我们最后处理重儿子的答案,所以递归之后当前子树需要用到他的重儿子贡献,如果都保存的话,遍历下一个子树时会将当前子树的贡献也算进去,所以只能保留一个子树的答案,因为重儿子的子树大小最大,所以保存下之后递归回去需要处理的子节点数更少,从而达到降低复杂度的目的。

主体框架

void dfs1(int u) { // 重剖siz[u] = 1;for (int i = head[u]; i; i = e[i].next) {int v = e[i].v; dfs1(v); siz[u] += siz[v]; if (siz[son[u]] < siz[v]) son[u] = v;}
}
void dfs2(int u, int fa, int opt) { // opt=1表示保留贡献,否则表示删除for (int i = head[u]; i; i = e[i].next) {int v = e[i].v; if (v == son[u] || v == fa) continue ;dfs2(v, u, 0);//处理轻儿子答案}if (son[u]) dfs2(son[u], u, 1), Son = son[u];//处理重儿子答案并保存calc(u);//统计轻儿子贡献/*
处理当前节点答案*/ Son = 0;if (!opt) del(u); // 如果该点轻儿子,删除贡献
//注意:轻儿子的重子节点也要删
}

这里不懂没关系,结合例题再去理解。

例题

例题1

CF600E Lomsat gelral

题目描述

给你一棵以结点 \(1\) 为根的有根树,每个节点最开始都被涂上了颜色。

如果颜色 \(c\) 在以结点 \(v\) 为根的子树中出现次数最多,则称其在以结点 \(v\) 为根的子树中占重要地位。一棵树中可以有很多颜色同时占重要地位

\(v\) 为根的子树指结点 \(v\) 及其他到根结点的路径包含 \(v\) 的结点。

请输出对于每一个结点 \(v\),在其子树中占重要地位的颜色编号之和。

板子,没啥好说的。

细节见代码 code

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, c[N], siz[N], son[N], mx, sum, cnt[N], Son, ans[N];
vector<int> g[N];
void dfs1(int u, int fa) {siz[u] = 1;for (auto v : g[u]) {if (v == fa) continue ;dfs1(v, u);siz[u] += siz[v];if (siz[son[u]] < siz[v]) son[u] = v;}
}
inline void add(int u, int fa, int val) {cnt[c[u]] += val;if (cnt[c[u]] > mx) mx = cnt[c[u]], sum = c[u];else if (cnt[c[u]] == mx) sum += c[u];for (auto v : g[u]) {if (v == fa || v == Son) continue ;add(v, u, val);}
}
void dfs2(int u, int fa, int opt) {for (auto v : g[u]) {if (v == fa || v == son[u]) continue ;dfs2(v, u, 0);}if (son[u]) dfs2(son[u], u, 1), Son = son[u];add(u, fa, 1), Son = 0;ans[u] = sum;if (!opt) add(u, fa, -1), sum = 0, mx = 0;
}
signed main() {ios::sync_with_stdio(0);cin.tie(0), cout.tie(0);cin >> n;for (int i = 1; i <= n; i++) cin >> c[i];for (int i = 1, u, v; i < n; i++) {cin >> u >> v;g[u].push_back(v), g[v].push_back(u);}dfs1(1, 0), dfs2(1, 0, 0);for (int i = 1; i <= n; i++) cout << ans[i] << " ";return 0;
}

例题2

P9233 [蓝桥杯 2023 省 A] 颜色平衡树

题目描述

给定一棵树,结点由 \(1\)\(n\) 编号,其中结点 \(1\) 是树根。树的每个点有一个颜色 \(C_i\)

如果一棵树中存在的每种颜色的结点个数都相同,则我们称它是一棵颜色平衡树。

求出这棵树中有多少个子树是颜色平衡树。

也是模板,用一个桶维护每种颜色出现了多少次,再维护一下出现的颜色数量,如果 \(cnt_c \times tot == siz_u\),答案加 \(1\)

其中 \(cnt_c\) 为颜色 \(c\) 的出现次数,\(tot\) 为颜色数,\(siz_u\) 为当前节点 \(u\) 的子树大小。

code

#include <bits/stdc++.h>
#define endl '\n'
#define int long long
#define rd read()
using namespace std;
const int N = 2e5 + 5;
struct Edge {int v, next;
} e[N];
inline int read() {int x = 0; char ch = getchar();while (ch < '0' || ch > '9') ch = getchar();while (ch >= '0' && ch <= '9') x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();return x;
}
int ans, c[N], n, head[N], cnt, siz[N], son[N], Son, col[N], tot;
inline void add(int u, int v) {e[++cnt] = {v, head[u]}; head[u] = cnt;}
void dfs1(int u) {siz[u] = 1;for (int i = head[u]; i; i = e[i].next) {int v = e[i].v; dfs1(v); siz[u] += siz[v]; if (siz[son[u]] < siz[v]) son[u] = v;}
}
inline void calc(int u, int val) {col[c[u]] += val;if (col[c[u]] == 1 && val == 1) tot++; if (col[c[u]] == 0 && val == -1) tot--;for (int i = head[u]; i; i = e[i].next) {int v = e[i].v; if (v == Son) continue ;calc(v, val);}
}
void dfs2(int u, int opt) {for (int i = head[u]; i; i = e[i].next) {int v = e[i].v; if (v == son[u]) continue ;dfs2(v, 0);}if (son[u]) dfs2(son[u], 1), Son = son[u];calc(u, 1);if (tot * col[c[u]] == siz[u]) ans++;Son = 0;if (!opt) calc(u, -1), tot = 0;
}
signed main() {n = rd; for (int i = 1; i <= n; i++) c[i] = rd, add(rd, i);dfs1(1); dfs2(1, 0);cout << ans;return 0;
}
http://www.hskmm.com/?act=detail&tid=18854

相关文章:

  • JavaDay10
  • 昇腾多机推理极速上手:10倍简化的 DeepSeek R1 超大规模模型部署
  • python开始exe应用程序初级教程
  • B站油管抖音一键笔记
  • 介绍自己
  • pycharm更换国内源
  • 基于Python+Vue开发的反诈视频宣传管理系统源码+运行步骤
  • 0voice-2.2.1-服务器百万并发实现
  • 微服务去掉认证的功能
  • INNER JOIN LEFT JOIN, RIGHT JOIN, FULL OUTER JOIN
  • 进程调度的时机,切换与过程
  • 深入解析:六维力传感器材质选择:影响性能与精度的关键因素
  • 按键精灵安卓/ios辅助工具,脚本开发新手教程ui界面介绍 - 教程
  • P3197fwx - FanWenxuan
  • 2025年AI大模型赋能智能座舱研究报告:技术、资本与市场|附20+份报告PDF、数据仪表盘汇总下载
  • 专题:2025年AI Agent智能体行业洞察报告|附110+份报告PDF、数据仪表盘汇总下载
  • 开启我的Java旅程
  • MYSQL: 时间戳演示
  • 自动化测试用例结构分析
  • 谷歌新款具身智能模型 Gemini Robotics 1.5 和 Gemini Robotics-ER 1.5
  • 完整教程:测试自动化教程:Parasoft如何流重定向与单元测试自动化
  • 用 Zig 实现英文数字验证码识别
  • 用 Crystal 实现英文数字验证码识别工具
  • 基于 Nim 的英文数字验证码识别工具实现
  • 完整教程:数组(Java基础语法)
  • AI信任心理学:构建可信赖人工智能系统的实用指南
  • 英语_阅读_Robot
  • 模仿Teamcenter(UIHealthDetector) 实现 系统托盘
  • 一个纯净的自动微分框架—autograd
  • PHP 8.2 vs PHP 8.3 对比:新功能、性能提升和迁移技巧