冰茶姬(一种饮料?)

并查集(正经

并查集是一种支持动态 link 的,能够快速判断两元素是否处于同一集合的数据结构。

(如果需要 cut,请出门右转 LCT)

那么其基本操作就包括:

  1. 连边。在两个点之间连一条边,使得两个点所在的两个集合合并为一个集合。

  2. 查询。查询两个点是否处在同一个集合中。

这也是普通并查集的所有操作了。

题单

暴力并查集

模板

核心

当连边的时候,如果两个点本来就在一个集合里,是不需要再连边的。因此整个并查集的形态便是森林。

一般而言,树都是有根的,那么确定两个点分别属于哪个集合的时候,就可以找到并判断它们的根是否相等。

而连边的时候,也仅需要将两个根连接起来,因此找根是并查集的核心操作。

既然要找根,那么也就需要知道点与点之间的父(子)关系(确定父关系足矣)。

初始的时候,每个点各自为营,那么其父亲就是他们自己(我当我的爹)。

for (int i = 1; i <= n; i++)
    fa[i] = i;

每次操作的时候,不管是连边还是查询,都需要先找到两个点所在树的根。

一个很朴素的想法就是,既然知道父关系,那么就根据父关系一路往上跳。

根据初始化,直到当前节点的父亲为自己,就是找到根了。

代码如下:

int find(int x)
{
    if (x == fa[x]) return x;
    return find(fa[x]);
}

找到根之后,这两个操作也就很容易实现了:

for (int i = 1; i <= m; i++)
{
    int op, u, v;
    cin >> op >> u >> v;
    int ru = find(u), rv = find(v);
    if (op == 1)
        fa[ru] = rv;
    else
        cout << (ru == rv ? "Y" : "N") << '\n';
}

时间复杂度

一般而言,时间复杂度都会有平均、最优、最差等几种。

最优一般是没有讨论意义的,因为总有好(du)人(liu)卡极限数据,那么此处就讨论一下极限数据时的最差情况。

在极限数据下,会出现这种情况:

每次将当前最大的树的根连在一个单节点树上,最终这棵树的深度则为 \(n\),此时最坏情况下的单次查询的复杂度为 \(O(n)\),总复杂度为 \(O(qn)\)。

这时候也就需要有一个优化。

路径压缩

由上可以发现,其实我们只期望知道根而不关心路径上通过了哪些点。

那么是否可以每次将我们所经过的点的父亲都更新成根呢?

当然可以,只需要将我们的 find 改成这样:

int find(int x)
{
    if (x == fa[x]) return x;
    return fa[x] = find(fa[x]);
}

相当于是一次记忆化搜索,核心就在于 fa[x] = find(fa[x]),在递归的过程中,不断更新经过的点的父亲,以达到路径压缩的目的,下次找 x 点的时候便能直接指向其根。

时间复杂度

这个优化计算时间复杂度是通过势能分析得到的均摊复杂度,证明在这(反正我是看不懂)。

按秩合并 / 启发式合并

除了路径压缩,还有一个很简单的想法,每次将小的合并到大的身上,这样就可以避免出现深度过深的情况了。

按秩合并和启发式合并是两种计算大小的方法,一种是计算树的大小,另一种是计算树的深度。

由于二者十分相似,一般不会太过细致的区分,不同的文章写的也不一样。

按大小(秩)的话比较简单,直接比较两个树的 siz 大小,将小的合并到大的上即可。

int find(int x)
{
    if (fa[x] == x) return x;
    return (find(fa[x]));
}
void merge(int x, int y)
{
    x = find(x), y = find(y);
    if (siz[x] > siz[y])
        swap(x, y);
    fa[x] = y;
    siz[y] += siz[x];
}

但是实测按大小合并的复杂度并不优秀。

而按深度合并(启发式)则稍微复杂一些,因为如果两个树的深度不相等的话,小树合并到大树是不会影响大树的深度的,而两树深度相同的时候,合并才会使整体的深度 \(+1\)。

int find(int x)
{
    if (fa[x] == x) return x;
    return (find(fa[x]));
}
void merge(int x, int y)
{
    x = find(x), y = find(y);
    if (dep[x] > dep[y])
        swap(x, y);
    fa[x] = y;
    if (dep[x] == dep[y])
        dep[y]++;
}

这两种方法的 find 则不需要使用路径压缩。

当然,这两种优化方式是可以一起使用的,就是启发式合并的时候也使用路径压缩。但是对于大多数数据而言,单独使用两种优化也是足够优秀的,所以同时使用两种优化并不比单独使用其中一种优秀很多。

完整代码自己打(懒

一道好题。

需要用到离散化。

基础应用

并查集的应用有最小生成树,虽然一些其他算法也有用到并查集(比如某些 DP,左偏树),但是 kruskal 可以说是其代表了。

边带权

例题

思路:

显然这是一道并查集判连通的题目,但是不同之处在于,这道题不仅需要判断两个点是否处于同一连通块,查询时需要知道两个点之间的距离。

如果用一个二维数组去储存两点之间的距离的话,需要的空间是 \(O(n^2)\) 的,显然不允许。

思考并查集有一个什么特点。没错,每个点都能很容易的找到自己的根节点。那么如果我们储存每个点到其根节点的距离,如果两个点在同一个集合里时,就可以通过到根节点的距离之差来代表两个点之间的距离,空间复杂度 \(O(n)\),可以接受。

这种需要考虑点之间距离的并查集,就叫做边带权。

用 dis 来代表当前结点到其父结点节点的距离,siz 代表当前并查集的大小。

找根时,除了需要更新 fa,还需要更新 dis:

int find(int x)
{
    if (fa[x] == x) return x;
    dis[x] += dis[fa[x]];
    return fa[x] = find(fa[x]);
}

可以发现,让路径压缩之后,dis 便可认为是当前结点到并查集的根结点的距离。

合并的时候,同样需要更新 dis,不过只需要更新根结点。同时更新根结点的 siz:

void merge(int x, int y)
{
    x = find(x), y = find(y);
    fa[y] = x;
    dis[y] = siz[x];
    siz[x] += siz[y];
}

查询操作则最为简单,此处略。

拓展域

模板

拓展域一般用于解决一个点有多种性质的问题,如上。

一个人可以分为两个域:朋友域和敌人域。

根据朋友的朋友是朋友,可以得到朋友域处于同一个集合代表这些人互为是朋友。

而根据敌人的敌人是朋友,如果小甲和小乙互为敌人,小乙和小丙互为敌人,那么小甲和小丙应该是朋友,但是小甲和小丙不能直接通过朋友域连接,而是应该通过小乙的敌人域进行连接。

所以敌人域不会直接相连,它仅仅起到对朋友域进行间接连接的作用。

理论知识比较简单,而代码实现时,一般用 i,i+n 等来代表第 \(i\) 个人的不同域。

完整代码:

vector<int> fa;
int find(int x)
{
    if (fa[x] == x)
        return x;
    return fa[x] = find(fa[x]);
}
int main()
{
    int n, m;
    cin >> n >> m;
    fa.resize(n * 2 + 7);
    for (int i = 1; i <= n * 2; i++)
        fa[i] = i;
    for (int i = 1; i <= m; i++)
    {
        string op;
        int u, v;
        cin >> op >> u >> v;
        if (op == "F")
            fa[find(u)] = find(v);
        else
        {
            fa[find(u)] = find(v + n);
            fa[find(v)] = find(u + n);
        }
    }
    vector<bool> vis(n * 2 + 7);
    int ans = 0;
    for (int i = 1; i <= n; i++)
    {
        int rot = find(i);
        if (vis[rot])
            continue;
        vis[rot] = 1;
        ans++;
    }
    cout << ans << '\n';
    return 0;
}

(这个图可以点的啊喂)

可持久化

其实仅仅是用可持久化数组优化了并查集的 fa 和 dep 数组,使得并查集变得可持久化。

posted @ 2023-11-20 19:44  Zvelig1205  阅读(196)  评论(0)    收藏  举报