前言
先学树剖。
讲讲启发式合并,最经典的就是并查集的按秩合并,这里不细讲。
常用的启发式合并就是小集合合并到大集合上,复杂度从 \(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的实现 :
- 遍历所有轻儿子,递归时删除贡献。
- 遍历重儿子,保留贡献。
- 处理轻儿子的贡献。
- 处理当前节点的答案。
- 如果该点为轻儿子,删除该点贡献。
为什么不保留多个节点的贡献?
因为会混淆子树之间的信息,由于我们最后处理重儿子的答案,所以递归之后当前子树需要用到他的重儿子贡献,如果都保存的话,遍历下一个子树时会将当前子树的贡献也算进去,所以只能保留一个子树的答案,因为重儿子的子树大小最大,所以保存下之后递归回去需要处理的子节点数更少,从而达到降低复杂度的目的。
主体框架
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;
}