DSU on array - 反向操作区间合并

上节我们讲到可以将 DSU 用于数组元素的删除,而有些时候我们删除一个元素时要维护的信息量巨大,那么就可以考虑反向操作。即依次激活每一个元素并合并区间。伪代码如下:

if Active(i) :
    active[i] = true;
    if active[i - 1] : merge(i, i - 1);
    if active[i + 1] : merge(i + 1, i);

适用场景

1. 反向操作、倒序构造 —— 由「删除」变「添加」

正向操作是「删除一个点」—— 破坏区间结构,难以维护。

反向操作是「添加一个点」—— 激活它并与邻接点合并,DSU 能保持区间结构。

因此:

  • 将所有操作倒序。

  • 初始所有点未被激活。

  • 每次添加一个点,更新 DSU.

  • 利用 DSU 的段信息倒序回答问题。

适用条件:

  • 每次操作对结构影响不可逆或难以正向维护。

  • 删除操作逆向后变成容易维护的添加操作。

  • 问题允许离线。

2. 维护连续段性质(sum / max / length / 条件计数)

假如题目要求:

  • 每个连续 1 段长度是多少?

  • 当前所有连续段中最大值是多少?

  • 每段的 sum 要维护?

  • 是否存在长度为 M 的连续段?

那么可以在 DSU 合并时在根节点维护这些信息。

3. 逐步构造可达性 / 连通区域

有些题出现 “某个条件满足时,该点才可用”,当条件变化时会出现大段的可达区域。

例如:某些区间合并、河流结冰、道路开放类建模。

常用模板

并查集基本操作:

vector<int> fa(n + 1);
iota(fa.begin(), fa.end(), 0);

function<int(int)> find = [&](int x) {
    while (x != fa[x]) x = fa[x] = fa[fa[x]];
    return x;
};

function<void(int, int)> merge = [&](int x, int y) {
    x = find(x), y = find(y);
    if (x != y) {
        fa[y] = x;
        /* 维护其他信息 */
    }
};

倒序激活操作:

vector<bool> vis(n + 1); // 记录该点是否被激活
for (int i = n; i >= 1; i -- ) {
    int pos = p[i]; // 当前激活的是哪一个点
    vis[pos] = true;
    /* 根据题目要求维护信息 */
    if (pos + 1 <= n && vis[pos + 1]) { // 合并右区间
        merge(pos + 1, pos);
    }
    if (pos - 1 >= 1 && vis[pos - 1]) { // 合并左区间
        merge(pos, pos - 1);
    }
    /* 根据题目要求维护信息 */
}

例题 - CF722C

题目大意:

给你一个由 \(n\) 个非负整数组成的数列 \(a_1, a_2, \cdots, a_n\).

你将要一个一个地摧毁这个数列中的数。并且,现在给你一个由 \(1\)\(n\) 组成的序列来告诉你每个数被摧毁的时间顺序。

当一个元素被摧毁时,你需要找到这个当前数列中的未被摧毁的数组成的和最大的连续子序列。另外,如果当前剩余的序列为空,最大和即为 \(0\).

思路:

当我们依照题意模拟摧毁过程时,我们会发现每次摧毁都可能产生新的区间,而且会对原始区间的信息产生修改。这种修改是难以维护的,考虑正难则反。

我们考虑当所有点都被摧毁时,答案肯定为 \(0\),接着我们从最后一个被摧毁的点开始逐步恢复每一个点。此时的情况就变成了,激活一个点,只考虑是否要合并左右区间,并维护区间信息和答案。可以用并查集解决。

code:

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

#define endl '\n'
#define i64 long long

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr), cout.tie(nullptr);

    int n;
    cin >> n;

    vector<int> a(n + 1);
    vector<i64> sum(n + 1);
    for (int i = 1; i <= n; i ++ ) {
        cin >> a[i];
        sum[i] = a[i];
    }

    vector<int> p(n + 1);
    for (int i = 1; i <= n; i ++ ) {
        cin >> p[i];
    }

    vector<int> fa(n + 1);
    iota(fa.begin(), fa.end(), 0);

    function<int(int)> find = [&](int x) {
        while (x != fa[x]) x = fa[x] = fa[fa[x]];
        return x;
    };

    function<void(int, int)> merge = [&](int x, int y) {
        x = find(x), y = find(y);
        if (x != y) {
            fa[y] = x;
            sum[x] += sum[y];
        }
    };

    i64 mx = 0;
    vector<i64> ans(n + 1);
    vector<bool> vis(n + 1);
    for (int i = n; i >= 1; i -- ) {
        int pos = p[i];
        ans[i] = mx;
        vis[pos] = true;
        if (pos + 1 <= n && vis[pos + 1]) {
            merge(pos + 1, pos);
        }
        if (pos - 1 >= 1 && vis[pos - 1]) {
            merge(pos, pos - 1);
        }
        mx = max(mx, sum[find(pos)]);
    }

    for (int i = 1; i <= n; i ++ ) cout << ans[i] << endl;
}

练习

LC2382: https://leetcode.cn/problems/maximum-segment-sum-after-removals/description/
LC1562: https://leetcode.cn/problems/find-latest-group-of-size-m/description/
CF2126G2: https://codeforces.com/problemset/problem/2126/G2
Luogu P4269: https://www.luogu.com.cn/problem/P4269

posted @ 2025-12-10 21:44  算法蒟蒻沐小白  阅读(29)  评论(0)    收藏  举报