启发式合并

入门

例题

[ABC329F] Colored Ball

  • 题意

给定 \(N\) 个盒子,每个盒子里面有一个颜色为 \(C_i\) 的小球。有 \(Q\) 次操作,每次操作将第 \(a_i\) 个盒子中的球都放到第 \(b_i\) 个盒子里面,你需要在每次操作后输出当前操作结束后第 \(b_i\) 个盒子里面有多少个不同颜色的小球。

如果盒子为空,输出 \(0\) 即可。

首先看到对答案有贡献的只有小球的颜色,即种类。因此可以联想到 STL set 实现的自动去重功能。

考虑按题意模拟。若构造一组形如由极小集合合并至极大集合的数据,此算法的最劣复杂度是 \(O(nq\log n)\) 的。

此时考虑启发式合并。

我们考虑让集合中元素个数数量小的合并至大的中。此时可以证明时间复杂度是 \(O(n\log ^2 n)\) 的。

初看上可能感觉这就是个暴力。但是我们分析一下每个元素被 insert 了多少次。

一个集合中的元素被放入另一个集合中会被 insert 一次。但是这个元素所在的集合的大小至少扩大了一倍。所以一个元素最多被 insert \(O(\log n)\) 次。加上 set 本身带有的 \(O(\log n)\) 的复杂度,最终复杂度是 \(O(n\log ^2 n)\) 的。

在这里,对于两个大小不一样的集合,我们将小的集合合并到大的集合中,而不是将大的集合合并到小的集合中。

为什么呢?这个集合的大小可以认为是集合的高度(在正常情况下),而我们将集合高度小的并到高度大的显然有助于我们找到父亲。

让高度小的树成为高度较大的树的子树,这个优化可以做到单次 \(O(\log n)\)

while(q--) {
		cin >> x >> y;
		if(s[x].size() < s[y].size()) {
			for(auto i : s[x])
				s[y].insert(i);
			s[x].clear();
			cout << s[y].size() << '\n';
		}
		else {
			for(auto i : s[y])
				s[x].insert(i);
			s[y].clear(), swap(s[x], s[y]);
			cout << s[y].size() << '\n';
		}
	}

P3201 [HNOI2009] 梦幻布丁

dsu on tree

使用场景:需统计子树内节点的信息。

算法原理:对于一个一 \(x\) 为根的子树,总是先统计轻子树,然后清空,最后统计重子树并保留信息。

时间复杂度分析:瓶颈在于清空,显然任意一颗轻子树大小小于二分之一原子树,即最劣时间复杂度 \(O(n \log n)\)

例题

[ABC350G] Mediator

算是 ABC329F 的延申。

首先一个点是否与询问点对相邻,可以转换为对父亲的讨论。

下列情况是有解的:

  • \(fa_u = fa_v\)\(u, v\) 不是根节点,答案为 \(fa_u\)
  • \(fa_{fa_u} = v\),答案为 \(fa_u\)
  • \(fa_{fa_v} = u\),答案为 \(fa_v\)

画图分析较为易懂。

于是我们只需要维护父亲即可,考虑启发式合并。

每次将连通块大小较小的合并至较大的连通块内,对于每次这种操作暴力 dfs 修改 \(fa\) 即可。

连通块大小可以用并查集轻松维护。

证明复杂度:每次连通块大小最多扩大一倍,所以复杂度 \(O(n\log n)\)

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 1e5 + 5;
const int mod = 998244353;
int n, Q, u, v, op, ans, fa[N];
vector<int> g[N];

inline void dfs(int x, int last) {
	fa[x] = last;
	
	for(auto u : g[x])
		if(u != last) dfs(u, x);
	
	return ;
}

namespace USF {
	int Fa[N], sz[N];
	
	inline void init() {
		for(int i = 1 ; i < N ; ++ i)
			Fa[i] = i, sz[i] = 1;
		
		return ;
	}
	
	inline int find(int x) {
		if(x != Fa[x]) Fa[x] = find(Fa[x]);
		
		return Fa[x];
	}
	
	inline void merge(int x, int y) {
		int fx = find(x), fy = find(y);
		
		if(fx == fy) return ;
		
		if(sz[fx] < sz[fy]) swap(x, y), swap(fx, fy);
		
		dfs(y, x);
		
		sz[fx] += sz[fy], Fa[fy] = fx;
		
		g[x].pb(y), g[y].pb(x);
		
		return ;
	}
}

using namespace USF;

inline int query(int u, int v) {
	if(fa[u] == fa[v] && fa[u]) return fa[u];
	if(fa[fa[u]] == v) return fa[u];
	if(fa[fa[v]] == u) return fa[v];
	
	return 0;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	cin >> n >> Q;
	
	init();
	
	while(Q --) {
		cin >> op >> u >> v;
		
		op = 1 + ((op * (1 + ans)) % mod) % 2;
		u = 1 + ((u * (1 + ans)) % mod) % n;
		v = 1 + ((v * (1 + ans)) % mod) % n;
		
		if(op == 1) merge(u, v);
		else {
			ans = query(u, v);
			
			cout << ans << '\n';
		}
	}
	
	return 0;
}

CF375D Tree and Queries

算是很典的题了。

分析:

1.离线,将询问挂到每个点上

2.轻重链剖分,对于点 \(x\),统计子树大小,并将最大的子树称为 \(x\) 的重儿子,其余为轻儿子。

3.每个除了叶子的节点都有重儿子,从根节点开始一直走重儿子形成的链称之为重链

4.对于任意点 \(x\),dfs 先遍历轻儿子并统计节点颜色,处理轻儿子询问,遍历后清空桶

5.对于点 \(x\),最后遍历重儿子 \(son_x\),统计答案并保留

6.若点 \(x\) 为点 \(fa_x\) 的轻儿子,清空整个 \(x\) 的子树

不难发现 dsu on tree 有一个固定的流程。即:轻儿子 \(\to\) 重儿子 \(\to\) 自己。

于是这类 dsu 的题目其实是有一个较为固定的模板的。

代码:

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 1e5 + 5;
int n, u, v, Q, heavy, c[N], sz[N], son[N], ans[N], cnt[N], num[N];
struct Node {
	int id, k;
	Node(int iid = 0, int kk = 0) {
		id = iid, k = kk;
	}
};
vector<int> g[N];
vector<Node> q[N]; 

inline void dfs1(int x, int last) {
	sz[x] = 1;
	
	for(auto u : g[x])
		if(u != last) {
			dfs1(u, x);
			
			sz[x] += sz[u];
			
			if(sz[son[x]] < sz[u]) son[x] = u;
		}
	
	return ;
}

inline void add(int x, int last, int val) {
	if(val == -1) -- num[cnt[c[x]]];
	
	cnt[c[x]] += val;
	
	if(val == 1) ++ num[cnt[c[x]]];
	
	for(auto u : g[x])
		if(u != last && u != heavy) add(u, x, val);
	
	return ;
}

inline void dfs2(int x, int last, bool flag) {
	for(auto u : g[x])
		if(u != last && u != son[x]) dfs2(u, x, 0);
	
	if(son[x]) dfs2(son[x], x, 1), heavy = son[x];
	
	add(x, last, 1);
	
	for(auto i : q[x])
		ans[i.id] = num[i.k];
	
	heavy = 0;
	
	if(! flag) add(x, last, -1);
	
	return ;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr); 
	
	cin >> n >> Q;
	for(int i = 1 ; i <= n ; ++ i)
		cin >> c[i];
	for(int i = 1 ; i < n ; ++ i) {
		cin >> u >> v;
		
		g[u].pb(v), g[v].pb(u);
	}
	for(int i = 1 ; i <= Q ; ++ i) {
		cin >> u >> v;
		
		q[u].pb(Node(i, v));
	}
	
	dfs1(1, -1);
	dfs2(1, -1, 0);
	
	
	for(int i = 1 ; i <= Q ; ++ i)
		cout << ans[i] << '\n';
	
	return 0;
}

其中 dfs1 dfs2 add 可以直接固定住形成一个较为稳定的板子。

CF570D Tree Requests

改一下 add 即可,没啥变化。

#include <bits/stdc++.h>
#define pb push_back
using namespace std;

const int N = 5e5 + 5;
int n, u, v, Q, heavy, sz[N], son[N], dep[N], cnt[N][27];
bool ans[N];
char c[N];
struct Node {
	int id, k;
	Node(int iid = 0, int kk = 0) {
		id = iid, k = kk;
	}
};
vector<int> g[N];
vector<Node> q[N]; 

inline void dfs1(int x, int last) {
	sz[x] = 1;
	
	for(auto u : g[x])
		if(u != last) {
			dep[u] = dep[x] + 1;
			
			dfs1(u, x);
			
			sz[x] += sz[u];
			
			if(sz[son[x]] < sz[u]) son[x] = u;
		}
	
	return ;
}

inline void add(int x, int last, int val) {
	cnt[dep[x]][c[x] - 'a' + 1] += val;
	
	for(auto u : g[x])
		if(u != last && u != heavy) add(u, x, val);
	
	return ;
}

inline void dfs2(int x, int last, bool flag) {
	for(auto u : g[x])
		if(u != last && u != son[x]) dfs2(u, x, 0);
	
	if(son[x]) dfs2(son[x], x, 1), heavy = son[x];
	
	add(x, last, 1);
	
	for(auto i : q[x]) {
		int sum = 0;
		
		for(int j = 1 ; j <= 26 ; ++ j)
			sum += (cnt[i.k][j] & 1);
		
		ans[i.id] = (sum <= 1);
	}
	
	heavy = 0;
	
	if(! flag) add(x, last, -1);
	
	return ;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr); 
	
	cin >> n >> Q;
	for(int i = 2 ; i <= n ; ++ i) {
		cin >> u;
		
		g[u].pb(i), g[i].pb(u);
	}
	for(int i = 1 ; i <= n ; ++ i)
		cin >> c[i];
	for(int i = 1 ; i <= Q ; ++ i) {
		cin >> u >> v;
		
		q[u].pb(Node(i, v));
	}
	
	dep[1] = 1;
	
	dfs1(1, -1);
	dfs2(1, -1, 0);
	
	for(int i = 1 ; i <= Q ; ++ i)
		if(ans[i]) cout << "Yes\n";
		else cout << "No\n";
	
	return 0;
}

CF246E Blood Cousins Return

在每个深度上维护 std :: set 即可。

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 5e5 + 5;
int n, u, v, Q, heavy, sz[N], son[N], dep[N], cnt[N], ans[N];
struct Node {
	int id, k;
	Node(int iid = 0, int kk = 0) {
		id = iid, k = kk;
	}
};
string s[N];
vector<int> g[N];
vector<Node> q[N];
set<string> S[N];

inline void dfs1(int x, int last) {
	sz[x] = 1;
	
	for(auto u : g[x])
		if(u != last) {
			dep[u] = dep[x] + 1;
			
			dfs1(u, x);
			
			sz[x] += sz[u];
			
			if(sz[son[x]] < sz[u]) son[x] = u;
		}
	
	return ;
}

inline void add(int x, int last, int val) {
	if(val == 1) S[dep[x]].insert(s[x]);
	else S[dep[x]].clear();
	
	for(auto u : g[x])
		if(u != last && u != heavy) add(u, x, val);
	
	return ;
}

inline void dfs2(int x, int last, bool flag) {
	for(auto u : g[x])
		if(u != last && u != son[x]) dfs2(u, x, 0);
	
	if(son[x]) dfs2(son[x], x, 1), heavy = son[x];
	
	add(x, last, 1);
	
	for(auto i : q[x])
		if(i.k + dep[x] <= n) ans[i.id] = S[i.k + dep[x]].size();
	
	heavy = 0;
	
	if(! flag) add(x, last, -1);
	
	return ;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr); 
	
	cin >> n;
	for(int i = 1 ; i <= n ; ++ i) {
		cin >> s[i] >> u;
		
		 g[u].pb(i), g[i].pb(u);
	}
	cin >> Q;
	for(int i = 1 ; i <= Q ; ++ i) {
		cin >> u >> v;
		
		q[u].pb(Node(i, v));
	}
	
	dfs1(0, -1);
	dfs2(0, -1, 0);
	
	for(int i = 1 ; i <= Q ; ++ i)
		cout << ans[i] << '\n';
	
	return 0;
}
posted @ 2024-10-08 14:08  end_switch  阅读(10)  评论(1编辑  收藏  举报