启发式合并
\(\text{codeforces-600e}\)
给你一棵以结点 \(1\) 为根的有根树,每个节点最开始都被涂上了颜色。
如果颜色 \(c\) 在以结点 \(v\) 为根的子树中出现次数最多,则称其在以结点 \(v\) 为根的子树中占重要地位。一棵树中可以有很多颜色同时占重要地位。
以 \(v\) 为根的子树指结点 \(v\) 及其他到根结点的路径包含 \(v\) 的结点。
请输出对于每一个结点 \(v\),在其子树中占重要地位的颜色编号之和。
\(1 \le n \le 10 ^ 5\),\(1 \le c _ i \le n\)。
树上启发式合并模板题。
以下部分题解来自于 题解 CF600E 【Lomsat gelral】 - 洛谷专栏
\(\text{dsu on tree}\) 一般用来解决一类不带修的子树查询问题。
其核心思想为:利用重链剖分的性质优化子树贡献的计算。
具体流程如下:
- 跑一边搜索,预处理出每个点的重儿子,重儿子的定义顾名思义就是,所在子树节点个数最多的儿子。
- 对于一个点 \(x\),先计算他所有轻儿子的贡献,而且计算完要清空贡献,接下来处理重儿子 \(son_x\) 的贡献,并保留 \(st(son_x)\),\(st(x)\) 表示 \(x\) 的子树。
- 之后暴力加入 \(x\) 的轻儿子所在子树的贡献,此时即可得到 \(ans_x\)。
该算法的时间复杂度为 \(O(n \log n)\),可用重链剖分的性质证明。
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 100005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, a[MAXN], sz[MAXN], son[MAXN], cnt[MAXN];
long long ans[MAXN], p[MAXN], t, res, tp;
vector<long long> v[MAXN];
void dfs(long long x, long long fa) {
sz[x] = 1;
for(auto y : v[x]) if(y != fa) {
dfs(y, x), sz[x] += sz[y];
if(sz[y] > sz[son[x]]) son[x] = y;
}
return;
}
void Clear() {
while(t) cnt[p[t --]] = 0;
res = tp = 0; return;
}
void insert(long long x) {
cnt[p[++ t] = a[x]] ++;
if(cnt[a[x]] > tp) res = a[x], tp = cnt[res];
else if(cnt[a[x]] == tp) res += a[x];
return;
}
void add(long long x, long long fa) {
insert(x);
for(auto y : v[x]) if(y != fa) add(y, x);
return;
}
void dsu(long long x, long long fa) {
for(auto y : v[x]) if(y != fa && y != son[x]) dsu(y, x), Clear();
if(son[x]) dsu(son[x], x);
for(auto y : v[x]) if(y != fa && y != son[x]) add(y, x);
insert(x), ans[x] = res;
return;
}
int main() {
n = read();
for(int i = 1; i <= n; i ++) a[i] = read();
for(int i = 1; i < n; i ++) {
long long x = read(), y = read();
v[x].push_back(y), v[y].push_back(x);
}
dfs(1, 0), dsu(1, 0);
for(int i = 1; i <= n; i ++) cout << ans[i] << " ";
cout << "\n";
return 0;
}
\(\text{luogu-5290}\)
给定一棵 \(n\) 个点的树,其中 \(1\) 为根节点,每个点有点权 \(a_i\)。
你需要将这棵树上的节点划分到若干个段中,满足每个段中任意两个点不存在祖先-后代关系。
一个段的价值为段中点的点权最大值,求所有段的价值和的最小值。
\(1 \le n \le 2 \times 10^5\),\(1 \le \max a_i \le 10^9\)。
我们从叶子节点开始考虑,对于只有一个叶子节点时,显然只能把叶子节点划分成一个段。
接着向上考虑,此时有一个节点和他的若干儿子,那么显然把所有儿子合并成一个段更优。
接着向上推广,此时一个节点有若干子树,那么子树间的段可以合并。
合并时不需要把一整个段合并,只需要合并段中的最大值即可。
但暴力合并每次是 \(O(\max(sz_x, sz_y))\) 的,总时间复杂度最坏 \(O(n^2)\)。
考虑启发式合并,本质上就是把 \(sz_i\) 小的向大的里合并。
对于两个排好序的堆合并,我们只需要让 \(sz_i\) 小的和另一个的前 \(sz_i\) 个取 \(\max\) 合并。
剩下的不需要管,也就是不参与合并。
于是最多合并 \(O(n)\) 次,但因为是堆,所以时间复杂度为 \(O(n \log n)\)。
#include<iostream>
#include<cstdio>
#include<queue>
#include<vector>
using namespace std;
#define MAXN 200005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
priority_queue<long long> q[MAXN];
vector<long long> v[MAXN], t;
long long n, a[MAXN];
void merge(long long x, long long y) {
if(q[x].size() < q[y].size()) swap(q[x], q[y]);
while(!q[y].empty()) {
t.push_back(max(q[x].top(), q[y].top()));
q[x].pop(), q[y].pop();
}
while(!t.empty()) q[x].push(t.back()), t.pop_back();
return;
}
void dsu(long long x) {
for(auto y : v[x]) dsu(y), merge(x, y);
q[x].push(a[x]); return;
}
int main() {
n = read();
for(int i = 1; i <= n; i ++) a[i] = read();
for(int i = 2; i <= n; i ++) v[read()].push_back(i);
dsu(1); long long ans = 0;
while(!q[1].empty()) ans += q[1].top(), q[1].pop();
cout << ans << "\n";
return 0;
}
\(\text{codeforces-1709e}\)
给定一棵包含 \(n\) 个顶点的树。每个顶点上写有一个数字,第 \(i\) 个顶点上的数字为 \(a_i\)。
我们称一条简单路径为每个顶点最多访问一次的路径。路径的权值定义为该路径上所有顶点的值的按位异或。我们称一棵树是“好”的,如果不存在权值为 \(0\) 的简单路径。
你可以进行如下操作任意次(也可以不进行):选择树上的一个顶点,将其上的值替换为任意正整数。请问,最少需要进行多少次操作,才能使这棵树变为“好”的?
\(1 \le n \le 2 \times 10^5\),\(1 \le a_i < 2^{30}\),\(1 \le x,y \le n\),\(x \ne y\)。
以下部分题解来自于 CF1709E XOR Tree(set 启发式合并) - 洛谷专栏。
令 \(a_u\) 表示 \(u\) 的点权,\(d_u\) 表示树上 \(1\) 到 \(u\) 简单路径上所有点的点权异或和。
题目中 \(u\) 到 \(v\) 的简单路径上的所有点点权异或和 \(=0\) 等价于 \(d_u\oplus d_v\oplus a_{\text{lca}(u,v)}=0\)。
可以证明如果允许改变 \(a_u\),那么一定可以使以 \(u\) 为根的子树中不存在异或和为 \(0\) 的简单路径(一种构造方法是令 \(a_u=2^{u+11^{4514}}\))。
考虑对每个点开一个 set,记录其子树中(包括自身)所有点的 \(d\)。特别的,若一个点被改变,则其 set 为空 \(^{[1]}\)。
在枚举一个点 \(rt\) 的所有儿子时,设现有集合为 \(S\),该儿子的集合为 \(T\),则枚举 \(T\) 中的所有元素。设当前枚举元素为 \(T_i\),在 \(S\) 中查找是否存在 \(a_{rt}\oplus T_i\),若存在,则子树中一定存在两个点 \(u,v\) 满足 \(d_u\oplus d_v\oplus a_{rt}=0\)。因此一定要改变 \(a_{rt}\)。
\({[1]}\):若改变了 \(a_{rt}\),那么以 \(rt\) 为根的子树中的所有点就不必参与 \(rt\) 祖先点中的讨论,因为 \(a_{rt}=2^{u+11^{4514}}\),不可能有以 以 \(rt\) 为根的子树中的点 为端点的简单路径的异或和为 \(0\),因此清空 \(S_{rt}\)。
复杂度 \(O(n^2\log n)\),用 启发式合并 可以使复杂度降低到 \(O(n\log^2n)\)。
#include<iostream>
#include<cstdio>
#include<vector>
#include<set>
using namespace std;
#define MAXN 200005
long long read() {
long long x = 0, f = 1;
char c = getchar();
while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
return x * f;
}
long long n, a[MAXN], b[MAXN], ans;
vector<long long> v[MAXN];
set<long long> s[MAXN];
void dsu(long long x, long long fa) {
s[x].insert(b[x]); bool fg = 0;
for(auto y : v[x]) if(y != fa) {
b[y] = b[x] ^ a[y], dsu(y, x);
if(s[x].size() < s[y].size()) swap(s[x], s[y]);
for(auto t : s[y]) if(s[x].find(a[x] ^ t) != s[x].end()) fg = 1;
for(auto t : s[y]) s[x].insert(t);
}
if(fg) ans ++, s[x].clear();
return;
}
int main() {
n = read();
for(int i = 1; i <= n; i ++) a[i] = read();
for(int i = 1; i < n; i ++) {
long long x = read(), y = read();
v[x].push_back(y), v[y].push_back(x);
}
dsu(1, 0); cout << ans << "\n";
return 0;
}
本文来自博客园,作者:So_noSlack,转载请注明原文链接:https://www.cnblogs.com/So-noSlack/p/19489322

浙公网安备 33010602011771号