DSU on Tree(树上启发式合并)学习笔记

引入

有这样一个问题:给定一棵结点数为 n n n 的树,求其长度刚好为 K K K 的简单路径数量, n n n K K K 都是 1 0 5 10^5 105 级别。

此题有多种解法,如点分治,长链剖分,最暴力的一种就是直接 dp,设 f u , i f_{u,i} fu,i 表示 u u u 的子树中到 u u u 长度为 i i i 的路径数量,然后暴力合并并计算答案。时间复杂度 O ( n 2 ) O(n^2) O(n2)

这里介绍一种 O ( n log ⁡ n ) O(n\log n) O(nlogn) 的做法。

DSU on Tree

首先想想我们需要维护什么,如果对于当前维护的 u u u 为根的联通块,知道 f u , i f_{u,i} fu,i,那么就可以将 u u u 的一个不在联通块内的子树内所有点,和联通块内的点构成点对并计算答案,然后合并到联通块。

什么意思呢,首先是计算答案,若子树内一个点到 u u u 的距离为 d d d,那联通块内对应的点到 u u u 的距离就是 K − d K-d Kd,如果我们知道联通块内到 u u u 距离为 K − d K-d Kd 的点的数量的话,就能将这个数量加到答案里。计算完后,把这个子树合并到联通块中,同时维护 f u f_{u} fu

不过到一个点的距离不好维护,可以改成维护深度,记当前联通块深度为 i i i 的点的数量为 c n t i cnt_i cnti,这样一减就是两个点的距离。

但这样计算答案并合并,就好把子树遍历一遍,是 O ( n 2 ) O(n^2) O(n2) 的,这时就需要用 dsu on tree。

我们可以先进行重链剖分,在递归计算答案时,对于轻儿子维护出来的 c n t cnt cnt,直接丢掉,对于重儿子维护出来的 c n t cnt cnt,把它留着。先将重儿子的子树作为初始联通块,然后把所有轻儿子的子树依次对联通块计算答案,接着合并。没错,遍历一遍轻儿子的子树,暴力合并。

众所周知,重链剖分后每个点到根节点的轻边数量是 O ( log ⁡ n ) O(\log n) O(logn) 的,每次合并是 O ( n ) O(n) O(n),所以总复杂度是 O ( n log ⁡ n ) O(n\log n) O(nlogn) 的!

看起来有点抽象?这里有道例题。

例题 1:[CF161D] Distance in Tree

这题数据范围并不强,但也可以用来练手,题目就是上面所说的。代码如下:

/*
    Program: CF161D.cpp
    Author: 1l6suj7
    DateTime: 2024-01-26 18:04:43
    Description:
*/

#include <bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lp(i, j, n) for(int i = j; i <= n; ++i)
#define dlp(i, n, j) for(int i = n; i >= j; --i)
#define mst(n, v) memset(n, v, sizeof(n))
#define mcy(n, v) memcpy(n, v, sizeof(v))
#define INF 1e18
#define MAX4 0x3f3f3f3f
#define MAX8 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define pll pair<ll, ll>
#define co(x) cerr << (x) << ' '
#define cod(x) cerr << (x) << endl
#define fi first
#define se second
#define eps 1e-8
#define lc(x) ((x) << 1)
#define rc(x) ((x) << 1 ^ 1)
#define pb(x) emplace_back(x)

using namespace std;

const int N = 50010, M = 510;

struct edge { int v, nxt; } E[N << 1];
int en, hd[N];

void add(int u, int v) { E[++en] = { v, hd[u] }, hd[u] = en; }

int n, K;
ll ans;

int sz[N], son[N], dep[N], L[N], R[N], nd[N], dfc, cnt[N];
/*
sz:子树大小
son:重儿子
dep:到根节点距离
L:子树内最小 dfn
R: 子树内最大 dfn
nd:dfn 对应的结点
dfc:记录当前 dfn
cnt[i]:当前联通块深度为 i 的结点数量
*/
void dfs1(int u, int fa) {	// 预处理
    sz[u] = 1, L[u] = R[u] = ++dfc, nd[dfc] = u;
    for(int i = hd[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if(v == fa) continue;
        dep[v] = dep[u] + 1, dfs1(v, u), sz[u] += sz[v], R[u] = max(R[u], R[v]);
        if(sz[v] > sz[son[u]]) son[u] = v;
    }
}

void calc(int grd, int u) {		// 计算子树内的一个点与联通块内点构成路径的答案
    int t = dep[grd] + K - dep[u] + dep[grd];	// 联通块内点的深度
    if(t >= 0 && t <= n) ans += cnt[t];
}

void Add(int u) { ++cnt[dep[u]]; }	// 向联通块内添加点

void Del(int u) { --cnt[dep[u]]; }	// 删除点

void dfs2(int u, int fa, bool kp) {		// kp 表示最后是否丢掉维护的东西
    for(int i = hd[u]; i; i = E[i].nxt) {	// 先计算轻儿子
        int v = E[i].v;
        if(v == fa || v == son[u]) continue;
        dfs2(v, u, 0);
    }
    if(son[u]) dfs2(son[u], u, 1);		// 计算重儿子
    for(int i = hd[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if(v == fa || v == son[u]) continue;
        lp(j, L[v], R[v]) calc(u, nd[j]);	// 先记录答案
        lp(j, L[v], R[v]) Add(nd[j]);		// 暴力合并
    } calc(u, u), Add(u);
    if(kp) return;
    lp(i, L[u], R[u]) Del(nd[i]);	// 丢掉维护的东西
}

signed main() {
    // freopen(".in", "r", stdin);
    // freopen(".out", "w", stdout);
#ifndef READ
    ios::sync_with_stdio(false);
    cin.tie(0);
#endif
    cin >> n >> K;
    int u, v;
    lp(i, 1, n - 1) cin >> u >> v, add(u, v), add(v, u);
    dfs1(1, 0), dfs2(1, 0, 0);
    cout << ans << endl;
    return 0;
}

例题 2:P3806 【模板】点分治 1

要求树上距离为 k k k 的点对是否存在,只不过有边权,深度会很大,所以用 map 来维护(实际上 map 很可能会比 unordered_map 跑得快)。

代码如下:

/*
    Program: P3806.cpp
    Author: 1l6suj7
    DateTime: 2024-01-26 20:14:36
    Description:
*/

#include <bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lp(i, j, n) for(int i = j; i <= n; ++i)
#define dlp(i, n, j) for(int i = n; i >= j; --i)
#define mst(n, v) memset(n, v, sizeof(n))
#define mcy(n, v) memcpy(n, v, sizeof(v))
#define INF 1e18
#define MAX4 0x3f3f3f3f
#define MAX8 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define pll pair<ll, ll>
#define co(x) cerr << (x) << ' '
#define cod(x) cerr << (x) << endl
#define fi first
#define se second
#define eps 1e-8
#define lc(x) ((x) << 1)
#define rc(x) ((x) << 1 ^ 1)
#define pb(x) emplace_back(x)

using namespace std;

const int N = 10010;

struct edge { int v, nxt, w; } E[N << 1];
int en, hd[N];

void add(int u, int v, int w) { E[++en] = { v, hd[u], w }, hd[u] = en; }

int n, m, qs[N], ans[N];

int sz[N], dep[N], son[N], dfc, L[N], R[N], nd[N];
map<int, int> cnt;
void dfs1(int u, int fa) {
    sz[u] = 1, L[u] = R[u] = ++dfc, nd[dfc] = u;
    for(int i = hd[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if(v == fa) continue;
        dep[v] = dep[u] + E[i].w, dfs1(v, u), sz[u] += sz[v], R[u] = max(R[u], R[v]);
        if(sz[v] > sz[son[u]]) son[u] = v;
    }
}

void calc(int grd, int u) {
    lp(i, 1, m) {
        int t = dep[grd] + qs[i] - dep[u] + dep[grd];
        ans[i] |= cnt.count(t);
    }
}

void add(int u) { ++cnt[dep[u]]; }

void del(int u) { int t = --cnt[dep[u]]; if(!t) cnt.erase(dep[u]); }

void dfs2(int u, int fa, int kp) {
    for(int i = hd[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if(v == fa || v == son[u]) continue;
        dfs2(v, u, 0);
    }
    if(son[u]) dfs2(son[u], u, 1);
    for(int i = hd[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if(v == fa || v == son[u]) continue;
        lp(j, L[v], R[v]) calc(u, nd[j]);
        lp(j, L[v], R[v]) add(nd[j]);
    } calc(u, u), add(u);
    if(kp) return;
    lp(i, L[u], R[u]) del(nd[i]);
}

signed main() {
    // freopen("P3806.in", "r", stdin);
    // freopen("P3806.out", "w", stdout);
#ifndef READ
    ios::sync_with_stdio(false);
    cin.tie(0);
#endif
    cin >> n >> m;
    int u, v, w;
    lp(i, 1, n - 1) cin >> u >> v >> w, add(u, v, w), add(v, u, w);
    lp(i, 1, m) cin >> qs[i];
    dfs1(1, 0), dfs2(1, 0, 0);
    lp(i, 1, m) cout << (ans[i] ? "AYE" : "NAY") << endl;
    return 0;
}

例题 3:P2634 [国家集训队] 聪聪可可

很板,就不放代码了。

例题 4:[IOI2011] Race

也很板。

题意:一棵结点数为 n n n 的树,求其长度为 k k k 的简单路径上边的数量的最小值,若不存在输出 -1。

可以维护到根节点距离为 d d d 的结点到根节点边的数量的最小值,计算答案与合并显然,不过由于加点时要取 min,所以加点前要做个备份,删点时直接倒着扫一遍备份数组即可。

代码如下:

/*
    Program: P4149.cpp
    Author: 1l6suj7
    DateTime: 2024-01-26 21:14:54
    Description:
*/

#include <bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lp(i, j, n) for(int i = j; i <= n; ++i)
#define dlp(i, n, j) for(int i = n; i >= j; --i)
#define mst(n, v) memset(n, v, sizeof(n))
#define mcy(n, v) memcpy(n, v, sizeof(v))
#define INF 1e18
#define MAX4 0x3f3f3f3f
#define MAX8 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define pll pair<ll, ll>
#define co(x) cerr << (x) << ' '
#define cod(x) cerr << (x) << endl
#define fi first
#define se second
#define eps 1e-8
#define lc(x) ((x) << 1)
#define rc(x) ((x) << 1 ^ 1)
#define pb(x) emplace_back(x)

using namespace std;

const int N = 200010, M = 1000010;

int n, K, ans = MAX4;

struct edge { int v, nxt; ll w; } E[N << 1];
int en, hd[N];

void add(int u, int v, ll w) { E[++en] = { v, hd[u], w }, hd[u] = en; }

int sz[N], son[N], dfc, L[N], R[N], nd[N], cnt[N]; ll dep[N];
map<ll, int> res;
void dfs1(int u, int fa) {
    sz[u] = 1, L[u] = R[u] = ++dfc, nd[dfc] = u;
    for(int i = hd[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if(v == fa) continue;
        dep[v] = dep[u] + E[i].w, cnt[v] = cnt[u] + 1, dfs1(v, u), sz[v] += sz[u], R[u] = max(R[u], R[v]);
        if(sz[v] > sz[son[u]]) son[u] = v;
    }
}

void calc(int grd, int u) {
    int t = dep[grd] + K - dep[u] + dep[grd];
    if(res.count(t)) ans = min(ans, cnt[u] - cnt[grd] + res[t] - cnt[grd]);
}

pll bck[N];
int bn;
void add(int u) {
    int & t = res[dep[u]];
    if(!t || cnt[u] < t) bck[++bn] = { dep[u], t }, t = cnt[u];
}

void dfs2(int u, int fa, int kp, int pre) {
    for(int i = hd[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if(v == fa || v == son[u]) continue;
        dfs2(v, u, 0, bn + 1);
    }
    if(son[u]) dfs2(son[u], u, 1, bn);
    for(int i = hd[u]; i; i = E[i].nxt) {
        int v = E[i].v;
        if(v == fa || v == son[u]) continue;
        lp(j, L[v], R[v]) calc(u, nd[j]);
        lp(j, L[v], R[v]) add(nd[j]);
    } calc(u, u), add(u);
    if(kp) return;
    dlp(i, bn, pre) {
        int & t = res[bck[i].fi]; t = bck[i].se;
        if(!t) res.erase(bck[i].fi);
    }
    bn = 0;
}

signed main() {
    // freopen("P4149.in", "r", stdin);
    // freopen(".out", "w", stdout);
#ifndef READ
    ios::sync_with_stdio(false);
    cin.tie(0);
#endif
    cin >> n >> K;
    int u, v; ll w;
    lp(i, 1, n - 1) cin >> u >> v >> w, ++u, ++v, add(u, v, w), add(v, u, w);
    dep[1] = 1, dfs1(1, 0), dfs2(1, 0, 0, 1);
    if(ans ^ MAX4) cout << ans << endl;
    else cout << -1 << endl;
    return 0;
}

其他题目

[CF600E] Lomsat gelral

[CF1009F] Dominant Indices

[CF375D] Tree and Queries

  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值