说道并查集,不得不提的是最小生成树,因为并查集的最经典的应用就是解决最小生成树的Kruskal算法。
有两个经典的算法可以用来解决最小生成树问题:Kruskal算法和Prim算法。其中Kruskal算法中便应用了并查集这种数据结构。
实现最小生成树还有一种算法叫做Prime算法,Prime算法维护的是顶点的集合,而Kruskal维护的是边的集合。
Prime算法过程:
Kruskal算法中需要判断两个节点是否在同一个连通分量中,如何判断是否是一个连通分量呢??也就判断是否加入新边之后,是否会和原来已经添加的边形成环路,并查集正是高效的实现了这个功能。
如何通过以上操作判断某条边是否会与原来的边集形成环路呢?
684.Redundant Connection这道题目实际上就是要找到一个无向图中形成环路的最后那条边(输入保证了所有边会形成回路)
vector findRedundantConnection(vector>& edges) {
vector root(2000, 0);
for(int i=0; i<2000; i++)
root[i]=i;
vector res;
for(auto edge : edges)
{
int a = edge[0];
int b = edge[1];
#find
while(root[a]!=a)
a = root[a];
while(root[b]!=b)
b = root[b];
#union
if(a==b)
res = edge;
else
root[a]=b;
}
return res;
}
首先初始化,将每个顶点设为单独的连通分量,每个顶点的根节点为自己。
每次find操作的时间复杂度为 O ( n ) O(n) O(n)。
每次union的时间复杂度为 O ( 1 ) O(1) O(1):
将b结点所在连通子图的根节点当做a结点所在联通子图的根节点的根节点,也就是将a结点所在的联通子图当做b的根节点的子树。这样就将两个连通子图连通为一个连通子图。
所以总的时间复杂度为 O ( m n ) O(mn) O(mn),那么有没有一种改进总体时间复杂度的方法呢?
Path compression:
将连通分量看作为一棵树,在循环的find操作中,将树中的每个结点都连接到parent结点,从而降低树的高度。
降低树的高度就是能够降低查找的时间复杂度,从 O ( n ) O(n) O(n)降为了 O ( l o g n ) O(logn) O(logn),因为原来的递归搜索实际上是在每个结点只有一个子节点的树上进行搜索,树的高度即为结点的个数,而通过path compression则能够有效的降低树的高度。
Union by rank:
另外一个问题就是进行Union操作时,需要将高度低的树连接到高度教高的树上,目的是减少union后的整棵树的高度。rank代表的就是树的高度。
采用了path compression和union by rank之后,find的时间复杂度变为了 O ( l o g n ) O(logn) O(logn),union的时间复杂度为 O ( 1 ) O(1) O(1),因此总的时间复杂度为 O ( m l o g n ) O(mlogn) O(mlogn), m m m为边的数目,而 n n n为点的数目。改进后的代码如下:
int root[1001];
int rank[1001];
int find(int node)
{
int ans = node;
while(root[node]!=node)
node = root[node];
int r = node;
while(root[ans]!=r)
{
int tmp = ans;
ans = root[ans];
root[tmp] = r;
}
return r;
}
vector findRedundantConnection(vector>& edges)
{
for(int i=0; i<1000; i++)
{
root[i]=i;
rank[i]=0;
}
vector res;
for(auto edge : edges)
{
int a = edge[0];
int b = edge[1];
#find
int p1 = find(a);
int p2 = find(b);
#union
if(p1==p2)
res = edge;
else if(rank[p1]>rank[p2])
{
root[p2] = p1;
}
else if(rank[p1]
685. Redundant Connection ||从前面的无向图升级到了有向图,对应的要求从原来的仅要求不形成环路升级到在不形成环路的基础上,拓扑必须要是一棵合法树,也就是每个点只能有一个父节点,例如 [[2,1],[3,1]] 这两条边虽然没有形成环路,但是 1 有两个父亲节点(2和3),因此不是一棵合法的树。
由于题目说明了输入只有一条不合法的边,因此首先可以统计一下这些边中是否存在某个点有两个父亲节点,假如有,则需要移除的边必定为连着这个点的两条边中的一条,通过上面 Union-find 的方法,可以判断出假如移除掉连着这个点的第一条边时,是否会形成回路。如果会,则说明需要移除第二条边,否则直接移除第一条边。
721. Accounts-merge该任务的任务是连接同一个account的email,这非非常适用于并查集来实现。为了将这些emails进行group,每个group需要有一个代表(父节点)。在最初, 每个email是其自己的代表。每个accont中的emails很自然的属于同一个group,应该被分配到相同的parent。选择每个account中的第一个email作为父节点。在其后进行find和union操作进行查找和更新父节点。
class Solution {
public:
vector> accountsMerge(vector>& acts) {
map owner;
map parents;
map> unions;
for (int i = 0; i < acts.size(); i++) {
for (int j = 1; j < acts[i].size(); j++) {
parents[acts[i][j]] = acts[i][j];
owner[acts[i][j]] = acts[i][0];
}
}
for (int i = 0; i < acts.size(); i++) {
string p = find(acts[i][1], parents);
for (int j = 2; j < acts[i].size(); j++)
parents[find(acts[i][j], parents)] = p;
}
for (int i = 0; i < acts.size(); i++)
for (int j = 1; j < acts[i].size(); j++)
unions[find(acts[i][j], parents)].insert(acts[i][j]);
vector> res;
for (pair> p : unions) {
vector emails(p.second.begin(), p.second.end());
emails.insert(emails.begin(), owner[p.first]);
res.push_back(emails);
}
return res;
}
private:
string find(string s, map& p) {
return p[s] == s ? s : find(p[s], p);
}
};
吴良超的leetcode解题报告