LeetCode Union-Find(并查集) 专题(二)

来源:互联网 发布:mac常用办公软件 编辑:程序博客网 时间:2024/05/17 16:15

LeetCode Union-Find(并查集) 专题(二)


并查集 Union-Find 的适用情况

如何考虑使用并查集

从并查集的功能入手, 显然题目要求有 “查询”, “合并” 操作时可以考虑使用。重点是合并。(单考虑查询用map即可)并且注意到这种合并应该是不需要记录合并路径的。(这是由于路径压缩会破坏原有路径, 破坏了原有的树结构)

从我做到的题来看, 并查集的使用具有很明显的特点。 一是经常和BFS或DFS相似。即看上去可以用BFS或者DFS解决。 二是具有数据分类的特性。 这里的分类比较抽象。 对于一些问题可能会直接给出一个关系对向量或者一个关系矩阵, 有些问题则比较隐晦一些。 例如下面将要谈到的区间上的合并(线段合并),合并的条件是隐性的: 相邻的数字都是同类。


并查集的优势

的确很多并查集的题目都可以利用BFS + DFS得到解决。 但是有一些问题是不能够或者说不便于利用BFS或DFS。 下面举一个例子。

ProblemA: 给出一个大小为n*n的关系矩阵, 要求输出合并后的连通块数。

对于上面的问题, DFS 和 BFS 完全可以胜任。 复杂度即为 O(N2).
反而用并查集不能够做到很好。 之后的详细的复杂度分析可以知道, 即使使用并查集除非做了很好的优化, 否则达到O(1)复杂度的Union操作是不现实的。

然后继续:

ProblemB: 给出一个大小为n的关系对向量, 要求输出合并后的连通块数。

这题和上面的题相对比, 只是信息给出的方式不同。但是这种做法避免了冗余信息(即关系矩阵中的0)。这样情况下, DFS, BFS和并查集的复杂度都下降, 其中DFS, BFS复杂度为 O(N). 并查集则还是像刚才说过的那样, 复杂度只能达到近似 O(N)

接着继续:

ProblemC: 给出一个大小为n的操作序列,每次操作可能有两种形式, 如下:
1. [0, i, j] -> 在点 i, j 之间建立关系
2. [1, k]-> 查询在第 k 次建立关系操作后 当前联通块的数目。

要求输出每次查询操作结果构成的结果向量。

这下如何? DFS, BFS即使使用了记忆化, 也需要复杂度为 O(kN). 这里主要的问题就在于 :对于拓扑结构的改变, BFS, DFS 很难在原有的基础上进行更新, 而必须重新计算。 这里的原因又在于 BFS和DFS都只返回出最后的结果, 但是没有对整个关系的内容做任何保存。

再考虑并查集, 由于它是有对当前结构做一个记录的, 因此只需要在原有保存的结构上再加入一条关系的信息即可。 然后再采用记忆化, 就可以在 O(N)的复杂度上解决这个问题。

个人感觉这种动态性的查询才是并查集的精髓。。。但是暂时没有找到对应的例题, 找到了再回来补上。

下面讲一个区间上的并查集使用例题。


区间上的并查集应用举例 —— LeetCode 128. Longest Consecutive Sequence

题目描述

Given an unsorted array of integers, find the length of the longest consecutive elements sequence.

For example,
Given [100, 4, 200, 1, 3, 2],
The longest consecutive elements sequence is [1, 2, 3, 4]. Return its length: 4.

Your algorithm should run in O(n) complexity.

思路分析

题目就很生硬的告诉我们复杂度要求就是要 O(N)。 直接不用分析了。

这里的合并信息的隐性的: 相关条件是两个数相邻。 先不看具体实现, 可以想到的是, 随着每个单点逐渐合并,成为线段, 主要的问题在于线段之间的合并。这里并查集问题的性质就体现出来了。

直接就现有条件做并查集也是可行的。 但是如果多考虑一点发现我们并不用做那么多。如果我们人为的规定了并查集中, 根的位置。

比如固定根位于区间的最左端, 下面考虑一个情况: 当前需要进行合并的数i,右侧i + 1是一个根节点。 那么我们只需要将father[i + 1] = father[i]即可。这里省去了一步Find()操作的时间。

又由于省去了这个位置的Find()函数的使用, 因此整个Union()中只有一处需要Find()。因此可以直接在Union()函数中内嵌入Find()函数的代码即可。

其实更进一步考虑,由于整个Union Find只有一个函数, 所以这个结构可以只存在于我们脑子里不必实际建立。。。不过考虑到写一下会让思路更清晰一点(最重要的是写之前我怎么可能想到那么多), 所以在代码中还是写上了。

代码实现

//BLOG VERSIONstruct UF{    UF() {        maximum = 0;    }    void Union(int i) {        if (range[i].size() > 0) return ; // has occured.        range[i] = vector<int>({i, i});        father[i] = i;               //update range only at root point.        // let root always at the mostleft in the range.         int t = i;        if (range[i - 1].size() > 0) {            t = i - 1;            while (father[t] != t) t = father[t]; // t is the root for i - 1            if (range[t][1] == i - 1) {                //can merge.                range[t][1] = i; father[i] = t;                //comdense the path.                int j = i - 1;                while (j != father[j]) {                    int temp = father[j];                    father[j] = t;                    j = father[j];                }            }        }        if (range[i + 1].size() > 0) {            //this condition is enough.            range[t][1] = range[i + 1][1]; father[i + 1] = t; // since no more info. can not comdence some path now.        }        if (range[t][1] - range[t][0] > maximum) maximum = range[t][1] - range[t][0];    }    unordered_map<int, vector<int>> range;// record the range.    unordered_map<int ,int> father; //recored the uf struct.    int maximum;};class Solution {public:      int longestConsecutive(vector<int>& nums) {        if (nums.empty()) return 0;        UF uf_set;        for (int i = 0; i < nums.size(); ++i) {            uf_set.Union(nums[i]);        }        return uf_set.maximum + 1;    }};

其他细节

这道题用并查集做自然是个很好的办法,但其实也有别的选择。 重要的一点是: 这道题的合并只会在区间的端点处进行。因此一条线段中我们只需要记录在其端点记录其长度即可描述出该合并线段当前的信息。

这种思路的做法推荐:https://discuss.leetcode.com/topic/5333/possibly-shortest-cpp-solution-only-6-lines/10
做法非常简洁。


The End.