启发式合并与集合操作浅谈

启发式合并及集合操作浅谈

引言

An advanced form of brute-force.

一种优雅的暴力。

2024 年 4 月 26 日,在得知自己写的一个长达 162 行的树上启发式合并 + 线段树由于一个字母错误而痛失 \(5\) 分(要不是数据太弱还会扣更多)后,回想起去年 CSP-S 中同样的原因使我从二等跌落至三等,以至于失去参加 NOIP 等一系列赛事的机会后,我决定写一篇浅谈以作发泄。

简介

启发式合并,是一种维护多个集合并进行合并操作时的优化,它可以在使用 STL 纯暴力合并的情况下使最劣时间复杂度从 \(O(n^2 \log n)\) 下降至 \(O(n\log n)\)

其基本思路为,在合并两个不同大小的集合时,将更小的集合合并至更大的集合中。对于其对时间复杂度的影响及其证明,可以参考 oi-wiki 上的这篇文章

该优化可以极大地降低各个集合合并相关题目暴力解法的耗时。

实战

1. 洛谷 - 可并堆模板

维护一个小根堆集合,支持合并操作、删除操作、检验某个数是否存在。

该题目的难度为蓝,其根本原因为洛谷的此类模板题的意义在于在不依靠 C++ 内置提供的红黑树以及 pbds 等内置数据结构的前提下独自理解并手打底层结构,从而将模板定向的知识点理解透彻。

此处由于重点不在底层,故忽略这个限制。

std::multiset 是 C++ 提供的一种内置数据结构,可在 \(O(\log n)\) 的时间复杂度内完成插入、删除、检查某元素是否存在、自动排序等操作。

创建 \(n\)multiset ,维护各个小根堆,并使用并查集辅助判断两数是否在同一个堆内。

利用 multiset 自动排序功能,我们可以通过将元素编号信息添加至元素内来快速选择值相同且编号最小的元素。

按照以上思路,本题就完成了。

代码

#include <iostream>
#include <set>
using namespace std;
const int N = 1e5 + 10;
using pii = pair<int, int>;
multiset<pii> ms[N], del;
multiset<pii>::iterator it[N];
int n, m, f[N];
inline int find(int x)
{
    return x == f[x] ? x : f[x] = find(f[x]);
}
inline void merge(int x, int y)
{
    if (it[x] == del.begin() or it[y] == del.begin())
        return;
    x = find(x), y = find(y);
    if (x == y)
        return;
    if (ms[x].size() < ms[y].size())
        swap(x, y);
    for (auto &i : ms[y])
    {
        it[i.second] = ms[x].insert(i);
    }
    ms[y].clear();
    f[y] = x;
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1, x; i <= n; i++)
    {
        scanf("%d", &x);
        f[i] = i;
        it[i] = ms[i].insert({x, i});
    }
    for (int i = 1, op, x, y; i <= m; i++)
    {
        scanf("%d%d", &op, &x);
        if (op == 1)
        {
            scanf("%d", &y);
            merge(x, y);
            continue;
        }
        if (it[x] == del.begin())
        {
            puts("-1");
            continue;
        }
        printf("%d\n", ms[find(x)].begin()->first);
        it[ms[find(x)].begin()->second] = del.begin();
        ms[find(x)].erase(ms[find(x)].begin());
    }
}

2. 洛谷 - 中位数

求前缀中位数。

本题既可以使用线段树完成,也可以使用树状数组完成。但是,由于这篇文章的标题包含“集合操作”,故这道题可以使用 multiset 解决。

multiset::iterator++ 操作符的意义是跳转到集合中下一个不小于目前迭代器指向元素的元素。结合 multiset 对元素自动排序的特性,我们可以在插入的过程中动态确定指向集合中中位数的迭代器位置。

由此,此题解决。

代码

#include <iostream>
#include <set>
using namespace std;
const int N = 1e5 + 10;
int n, a[N];
multiset<int> sto;
multiset<int>::iterator it;
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", a + i);
    }
    sto.insert(a[1]);
    printf("%d\n", a[1]);
    it = sto.begin();
    for (int i = 3; i <= n; i += 2)
    {
        sto.insert(a[i - 1]), sto.insert(a[i]);
        if (a[i] > *it and a[i - 1] > *it)
            it++;
        else if (a[i] <= *it and a[i - 1] <= *it)
            it--;
        printf("%d\n", *it);
    }
}
posted @ 2024-04-26 23:46  丝羽绫华  阅读(10)  评论(0编辑  收藏  举报