启发式合并小记

适用范围

当题目中查询有关子树中的问题,而往往涉及类似莫队中每种值出现个数这类比较难用线段树快速维护的时候,我们可以考虑用启发式合并。

过程

启发式合并其实是优雅的暴力,具体思路就是:统计 u 子树的答案,我们先把 u 除了重儿子之外的所有儿子的答案统计了,然后再统计重儿子,但是对于重儿子我们不要清空贡献,最后再把轻儿子的贡献加上即可。

这个时间复杂度是 O(nlogn) 的。

模板题

CF600E Lomsat gelral

这里给一下代码。

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 1e5 + 5;
int n, m, c[MAXN] = {0};
vector<int> e[MAXN];
int cnt[MAXN] = {0}, maxCnt = 0;
long long sum[MAXN] = {0};
inline void add(int x) {
    sum[cnt[c[x]]] -= c[x];
    if (cnt[c[x]] == maxCnt)
        maxCnt++;
    cnt[c[x]]++;
    sum[cnt[c[x]]] += c[x];
}
inline void del(int x) {
    sum[cnt[c[x]]] -= c[x];
    if (cnt[c[x]] == maxCnt && sum[cnt[c[x]]] == 0)
        maxCnt--;
    cnt[c[x]]--;
    sum[cnt[c[x]]] += c[x];
}
int sz[MAXN] = {0}, chd[MAXN] = {0};
inline int srh(int x, int pr) {
    sz[x] = 1, chd[x] = -1;
    for (auto i: e[x])
        if (i != pr) {
            sz[x] += srh(i, x);
            if (chd[x] == -1 || sz[chd[x]] < sz[i])
                chd[x] = i;
        }
    return sz[x];
}
inline void insert(int x, int pr) {
    add(x);
    for (auto i: e[x])
        if (i != pr)
            insert(i, x);
}
inline void erase(int x, int pr) {
    del(x);
    for (auto i: e[x])
        if (i != pr)
            erase(i, x);
}
long long ans[MAXN] = {0};
inline void dfs(int x, int pr, bool flg) {
    for (auto i: e[x])
        if (i != pr && i != chd[x])
            dfs(i, x, true);
    if (chd[x] != -1)
        dfs(chd[x], x, false);

    for (auto i: e[x])
        if (i != pr && i != chd[x])
            insert(i, x);
    add(x);
    ans[x] = sum[maxCnt];
    if (flg) {
        for (auto i: e[x])
            if (i != pr)
                erase(i, x);
        del(x);
    }
}
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d", &c[i]);
    for (int i = 1, u, v; i < n; i++) {
        scanf("%d%d", &u, &v);
        e[u].push_back(v);
        e[v].push_back(u);
    }
    srh(1, 1);
    dfs(1, 1, true);
    for (int i = 1; i <= n; i++)
        printf("%lld ", ans[i]);
    return 0;
}
CF375D Tree and Queries

与上道题类似,离线统计即可。

CF1824C LuoTianyi and XOR-Tree

好题。

我们先观察性质,不难发现,答案的上界是叶子个数,因为我们可以通过改变所有叶子的权值使其满足异或和等于 0

我们进一步发现,最终状态下,每个节点的子树中所有叶子节点到这个节点的异或值都是相同的,因为祖先这一段是公共的。

我们这样就可以考虑从下往上递归解决,我们先把所有子节点内部贪心地调整成一样的,然后再看如何调整这些子节点的值使得异或和一样。

显然,我们贪心地希望只保留出现次数最多的异或和,其它都调整为这个。

但是一个子树内部可能有很多种情况,异或和不唯一。但是,我们可以断言,再最少调整次数下,这些可能的值构成一个集合 M,且 M 的大小不会超过子树大小。

所以我们只用求出所有 M 的并集然后求一下众数即可。这个就可以用启发式合并很轻松地解决了。

最后判断一下最终的集合中有没有 0,来判断要不要将答案加 1。所以,这道题其实最终异或为 0 并不是必须的。

点击查看代码
#include <iostream>
#include <cstdio>
#include <map> 
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;

int n;
int a[N] = {0};
vector<int> e[N];

map<int, int> mp[N], t;
int ans = 0;
void dfs(int x, int pr) {
	if ((int)e[x].size() == 1 && pr > 0) {
		mp[x][a[x]] = 1;
		return;
	}
	int mxCnt = 1, tot = 0;
	for (auto i: e[x])
		if (i != pr) {
			a[i] ^= a[x];
			dfs(i, x);
			if (mp[i].size() > mp[x].size())
				swap(mp[i], mp[x]);
			for (auto j: mp[i]) {
				mp[x][j.first] += j.second;
				mxCnt = max(mxCnt, mp[x][j.first]);
			}
			++tot;
		}
	if (mxCnt > 1) {
		t.clear();
		for (auto i: mp[x])
			if (i.second == mxCnt)
				t[i.first] = 1;
		swap(t, mp[x]);
	}
	ans += tot - mxCnt;
}
 
int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) 
		cin >> a[i];
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1, 0);
	cout << ans + (mp[1][0] != 0 ? 0 : 1) << endl;
	return 0;
}
CF1709E XOR Tree

好题。

我们依然需要观察性质,令 du 表示 u 到根节点的异或和,则 P(u,v)=dudvdlca(u,v),我们要求任意路径不为 0

对于树上问题,依然考虑从子树出发,我们发现:如果可以更改 au,则我们可以保证子树内不存在任何一条经过 u 的路径使得异或和不为 0。我们可以使的 au 有一个很高很高的位为 1,这样根据上面的式子,这一位消不掉。

所以我们可以从下往上地贪心,每次判断需不需要改 u,我们将子树中还没改的儿子节点的所有节点的 du 的集合拎出来,如果存在一对值异或和为 au,说明 au 必须被更改,否则不用。这个过程可以用启发式合并解决。

点击查看代码
#include <iostream>
#include <cstdio>
#include <set>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e5 + 5;

int n, a[N] = {0};
vector<int> e[N];
set<int> s[N];
int ans = 0;

void dfs(int x, int pr, int dth) {
	for (auto i: e[x])
		if (i != pr) 
			dfs(i, x, (dth ^ a[i]));
	bool flg = false;
	s[x].insert(dth);
	for (auto i: e[x])
		if (i != pr && !flg) {
			if (s[i].size() > s[x].size())
				swap(s[i], s[x]);
			for (auto j: s[i])
				if (s[x].find((j ^ a[x])) != s[x].end()) 
					flg = true;
			for (auto j: s[i]) 
				s[x].insert(j);
		}
	if (flg)
		ans++, s[x].clear();
}

int main() {
	cin >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1, 0, a[1]);
	cout << ans << endl;
	return 0; 
} 
posted @   rlc202204  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示