启发式合并 & dsu on tree
简介
启发式合并指在合并时,\(size\)小的集合/数据结构向\(size\)大的合并,这样,每次\(size\)必定增加之前的2倍,从而每个元素最多被合并\(logn\)次,因而所有元素最多插入\(nlogn\)次,从而用暴力的想法解决题目。
dsu on tree
指:我们在暴力统计子树答案的时候,先暴力统计轻儿子的答案,最后统计重儿子的答案,在统计完轻儿子的答案后,我们消除轻儿子的影响,而重儿子由于最后统计,不需要消除影响,这样父亲结点就可以使用重儿子保留的信息,不需要再次访问重儿子,由于树链剖分后轻儿子所占结点每次至少除以二,这样复杂度也被优化到\(nlogn\)从而用暴力解决树上子树问题。
dsu on tree一般用来解决树上询问问题,这类问题一般有两个特征:
1. 没有修改
2. 只有对子树的询问
下面直接通过题目具体学习一下
BZOJ-2809 dispatching
题意
每个节点有两个信息(x,y),给定n个结点和m,输入每行包含3个数,第一个表示该结点的父亲,后面两个数表示(x,y)
要求:
求出最大的\(u.y \times size(v)\), 其中\(v\)是u的子树中的结点,且满足\(\sum_{i \in v} x_i < m\)
\(n \le 10^5\)
样例
Sample Input
5 4
0 3 3
1 3 5
2 2 2
1 2 4
2 3 1
Sample Output
6
题解
我们很容易想到一种解法,对于每个点维护一个优先队列,按x排序,队头是最大的x,同时记录一个队列中的元素和,当元素和大于m时便不断出队直到元素和小于等于m。
我们用启发式合并,就可以将这个合并的复杂度降到\(logn\),加上优先队列的复杂度,总复杂度\(nlog^2n\)可以通过此题
注意在合并的时候,一定要记录一下队列被合并到哪个点了,也就是说,子节点信息存储的编号不一定是v
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 50;
struct node {
ll x, y;
} p[N];
vector<int> G[N];
priority_queue<int> q[N];
ll sum[N];
ll now[N];
ll ans = 0;
int n; ll m;
void dfs(int u) {
q[u].push(p[u].x);
sum[u] = p[u].x;
if (sum[u] > m) {
q[u].pop();
sum[u] = 0;
}
now[u] = u;//用now数组存储一下合并到哪个点了
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
dfs(v);
if (q[now[u]].size() < q[now[v]].size()) swap(now[u], now[v]);
while (!q[now[v]].empty()) {
ll t = q[now[v]].top();
q[now[v]].pop();
q[now[u]].push(t);
sum[now[u]] += t;
}
sum[now[v]] = 0;
while (!q[now[u]].empty() && sum[now[u]] > m) {
sum[now[u]] -= q[now[u]].top();
q[now[u]].pop();
}
}
ans = max(ans, p[u].y * q[now[u]].size());
}
int main() {
scanf("%d%lld", &n, &m);
for (int i = 1; i <= n; i++) {
int f; ll x, y;
scanf("%d%lld%lld", &f, &x, &y);
p[i].x = x;
p[i].y = y;
if (f) G[f].push_back(i);
}
dfs(1);
printf("%lld\n", ans);
return 0;
}
HDU-1512 Monkey King
题意
现在有n只猴子,每只猴子有一个力量值,初始时每个猴子自己独处一个集合,猴子所处集合间会发生冲突,每次冲突,两边猴子所处集合力量最大的猴子出来打架,打完后两边集合力量最大的两只猴子力量减半,且集合合并,问每次冲突完毕后,合并的集合里力量最大的猴子力量是多少,如果两只猴子处于同一集合,输出-1
样例
Sample Input
5
20
16
10
10
4
5
2 3
3 4
3 5
4 5
1 5
Sample Output
8
5
5
-1
10
题解
和上题一样,每只猴子开一个优先队列,开一个并查集维护猴子的集合,冲突时先查询猴子是不是同一集合,是输出-1,否则取出最大力量的两只猴子力量减半后,启发式合并,输出队首即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 50;
int n, m;
priority_queue<int> q[N];
int a[N];
int f[N];
int find(int x) {
return x == f[x] ? x : f[x] = find(f[x]);
}
int main() {
while (~scanf("%d", &n)) {
for (int i = 1; i <= n; i++) {
while (!q[i].empty()) q[i].pop();
}
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
q[i].push(a[i]);
f[i] = i;
}
int m;
scanf("%d", &m);
for (int i = 1; i <= m; i++) {
int a, b;
scanf("%d%d", &a, &b);
int x = find(a), y = find(b);
if (x == y) {
puts("-1");
}
else {
if (q[x].size() < q[y].size()) {
swap(x, y);
}
int u = q[x].top();
q[x].pop();
q[x].push(u / 2);
u = q[y].top();
q[y].pop();
q[y].push(u / 2);
while (!q[y].empty()) {
q[x].push(q[y].top());
q[y].pop();
}
f[y] = x;
printf("%d\n", q[x].top());
}
}
}
return 0;
}
HDU-6109 数据分割
题意
给定L行数据,之后L行,每行一个约束条件\((i,j,op)\),op=1表示\(x_i=x_j\),op=0表示\(x_i!=x_j\),问一共有多少组约束条件,数据保证每组条件都是不可满足的,且去掉每组最后一个约束条件,是可以满足的。
样例
Sample Input
6
2 2 1
2 2 1
1 1 1
3 1 1
1 3 1
1 3 0
Sample Output
1
6
题解
维护两个vector和一个并查集,一个vector表示相等关系,另一个vector表示不等关系,并查集维护相等关系的集合。合并两个相等关系时,先查询被合并的相等关系集合是否有对应的不等关系在另一个集合里,如果有说明矛盾了,直接退出,清空,记录答案,处理下一组,没有则合并。(均指启发式合并)对于不等关系,我们直接查询两个数是否处于同一集合,如果处于同一并查集说明矛盾了,直接退出,清空,记录答案,处理下一组,否则处理不等关系数组。
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 50;
int f[2 * N];
vector<int> e[N];
vector<int> ne[N];
int find(int x) {
return x == f[x] ? x : f[x] = find(f[x]);
}
vector<int> ans;
vector<int> lis;
int main() {
int l;
scanf("%d", &l);
for (int i = 1; i <= 1e5 + 5; i++) {
f[i] = i;
e[i].push_back(i);
}
int last = 0;
bool flag = true;
int cnt = 0;
while (l--) {
cnt++;
int a, b, op;
scanf("%d%d%d", &a, &b, &op);
lis.push_back(a);
lis.push_back(b);
if (op) {
int x = find(a), y = find(b);
if (x == y) continue;
if (e[x].size() < e[y].size()) {
swap(x, y);
}
for (int i = 0; i < e[y].size(); i++) {
if (!flag) break;
int t = e[y][i];
for (int j = 0; j < ne[t].size(); j++) {
int o = find(ne[t][j]);
if (o == x) {
flag = false;
break;
}
}
}
if (!flag) {
ans.push_back(cnt - last);
last = cnt;
flag = true;
for (int i = 0; i < lis.size(); i++) {
f[lis[i]] = lis[i];
e[lis[i]].clear();
e[lis[i]].push_back(lis[i]);
ne[lis[i]].clear();
}
lis.clear();
}
else {
f[y] = x;
for (int i = 0; i < e[y].size(); i++) {
e[x].push_back(e[y][i]);
}
e[y].clear();
}
}
else {
int x = find(a), y = find(b);
if (x == y) {
ans.push_back(cnt - last);
last = cnt;
for (int i = 0; i < lis.size(); i++) {
f[lis[i]] = lis[i];
e[lis[i]].clear();
e[lis[i]].push_back(lis[i]);
ne[lis[i]].clear();
}
lis.clear();
}
else {
ne[x].push_back(y);
ne[y].push_back(x);
}
}
}
printf("%d\n", ans.size());
for (int i = 0; i < ans.size(); i++) {
printf("%d\n", ans[i]);
}
return 0;
}
CSU-1811 Tree Intersection
题意
一棵树,每个点有一个颜色,对于每条边,去掉这条边后形成两颗树,求两棵树颜色交集的大小.多组数据
样例
Sample Input
4
1 2 2 1
1 2
2 3
3 4
5
1 1 2 1 2
1 3
2 3
3 5
4 5
Sample Output
1
2
1
1
1
2
1
题解
对于求颜色的交集,我们可以转化为求子树中某个颜色的个数,如果某种颜色的个数等于它的总个数,说明这个颜色都处于这个子树中,所以交集中不存在这个颜色,如果小于,则说明交集中有这个颜色.
对于每个点维护一个map,合并时某种元素第一次出现计算一次,等于总个数时减去一次
注意,如果map中某个元素可能没插入过,千万不要直接用下标调用,一定要map.count
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 50;
int c[N];
struct node {
int v, id;
node(int v = 0, int id = 0): v(v), id(id) {}
} edge[N];
vector<node> G[N];
map<int, int> mp[N];
int now[N];
int cnt[N];
int ans[N];
int tans[N];
void dfs(int u, int f, int id) {
now[u] = u;
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i].v;
int ee = G[u][i].id;
if (v == f) continue;
dfs(v, u, ee);
if (mp[now[u]].size() < mp[now[v]].size()) {
swap(now[u], now[v]);
}
for (auto e : mp[now[v]]) {
int x = e.first, y = e.second;
if (mp[now[u]].count(x) == 0) {
if (y != cnt[x]) ans[now[u]]++;
}
else if (mp[now[u]][x] + y == cnt[x]) ans[now[u]]--;
mp[now[u]][x] += y;
}
mp[now[v]].clear();
}
//printf("--%d %d %d %d--\n", u, c[u], now[u], mp[now[u]].count(c[u]));
if (mp[now[u]].count(c[u]) == 0) {
if (cnt[c[u]] != 1) ans[now[u]]++;
}
else if (mp[now[u]][c[u]] + 1 == cnt[c[u]]) ans[now[u]]--;
mp[now[u]][c[u]]++;
tans[id] = ans[now[u]];
}
int main() {
int n;
while (~scanf("%d", &n)) {
for (int i = 1; i <= n; i++) {
mp[i].clear();
G[i].clear();
ans[i] = 0;
tans[i] = 0;
cnt[i] = 0;
}
for (int i = 1; i <= n; i++) {
scanf("%d", &c[i]);
cnt[c[i]]++;
}
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(node(v, i));
G[v].push_back(node(u, i));
}
dfs(1, 0, 0);
for (int i = 1; i < n; i++) {
printf("%d\n", tans[i]);
}
}
return 0;
}
下面这两题都是dsu on tree
Codeforces-600E Lomsat gelral
题意
给定一颗树,n个点,每个点有一个颜色,求对于每个子树,颜色出现次数最多的是什么,如果多种颜色出现次数相同则求和.
样例
Input
4
1 2 3 4
1 2
2 3
2 4
Output
10 9 3 4
Input
15
1 2 3 1 2 3 3 1 1 3 2 2 1 2 3
1 2
1 3
1 4
1 14
1 15
2 5
2 6
2 7
3 8
3 9
3 10
4 11
4 12
4 13
Output
6 5 4 3 2 3 3 1 1 3 2 2 1 2 3
题解
我们就像之前说的那样先dfs轻儿子,消除轻儿子影响,再dfs重儿子,不消除影响就可以了,可以直接看代码,类似于模板
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 50;
typedef long long ll;
vector<int> G[N];
int n;
ll c[N];
int dep[N];
int sze[N];
int son[N];
int Son;
void dfs1(int u, int f) {
sze[u] = 1;
dep[u] = dep[f] + 1;
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (v == f) continue;
dfs1(v, u);
sze[u] += sze[v];
if (sze[v] > sze[son[u]]) son[u] = v;
}
}
ll cnt[N];
ll maxv = 0;
ll sum = 0;
ll ans[N];
void calc(int u, int f, int val) {
cnt[c[u]] += val;//不同的题目这个地方有所不同
if (cnt[c[u]] > maxv) maxv = cnt[c[u]], sum = c[u];
else if (cnt[c[u]] == maxv) sum += c[u];
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (v == f || v == Son) continue;
calc(v, u, val);
}
}
void dfs2(int u, int f, int op) {
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (v == f || v == son[u]) continue;
dfs2(v, u, 0);
}
if (son[u]) {
dfs2(son[u], u, 1);
Son = son[u];
}
calc(u, f, 1); Son = 0;
ans[u] = sum;
if (!op) {
calc(u, f, -1);
sum = 0;
maxv = 0;
}
}
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &c[i]);
}
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
dep[0] = 0;
dfs1(1, 0);
dfs2(1, 0, 0);
for (int i = 1; i <= n; i++) {
printf("%lld ", ans[i]);
}
return 0;
}
Codeforces-1009F Dominant Indices
题意
求一棵树的子树中结点最多的是第几层,这里的第几层是相对于该子树的根节点而言的
样例
Input
4
1 2
2 3
3 4
Output
0
0
0
0
Input
4
1 2
1 3
1 4
Output
1
0
0
0
Input
4
1 2
2 3
2 4
Output
2
1
0
0
题解
同上一题,只需要改一下calc函数即可
代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 50;
vector<int> G[N];
int dep[N];
int son[N];
int sze[N];
void dfs1(int u, int f) {
sze[u] = 1;
dep[u] = dep[f] + 1;
son[u] = 0;
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (v == f) continue;
dfs1(v, u);
sze[u] += sze[v];
if (sze[v] > sze[son[u]]) son[u] = v;
}
}
int c[N];
int maxv;
int now;
int ans[N];
int Son;
void calc(int u, int f, int val) {
c[dep[u]] += val;
if (c[dep[u]] > maxv) {
now = dep[u];
maxv = c[dep[u]];
}
else if (c[dep[u]] == maxv) {
if (dep[u] < now) now = dep[u];
}
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (v == f || v == Son) continue;
calc(v, u, val);
}
}
void dfs2(int u, int f, int op) {
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (v == f || v == son[u]) continue;
dfs2(v, u, 0);
}
if (son[u]) {
dfs2(son[u], u, 1);
Son = son[u];
}
calc(u, f, 1);
Son = 0;
ans[u] = now - dep[u];
if (!op) {
calc(u, f, -1);
now = 0;
maxv = 0;
}
}
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
dep[0] = 0;
dfs1(1, 0);
dfs2(1, 0, 0);
for (int i = 1; i <= n; i++) {
printf("%d\n", ans[i]);
}
return 0;
}