765. Couples Holding Hands+399. Evaluate Division+Union-Find(并查集算法)详细探讨!!!!!!

本文先给出Union-Find算法的模板,然后结合该算法,解决两道题即Couples Holding Hands和Evaluate Division并给出详细的结题思路过程,虽然可能有点绕,但是对开阔思路,尤其是帮助我们对Union-Find算法的灵活使用都有很大的收益,这两道题都有别的解法,在一定程度上也更好理解,但是我们使用Union-Find其意义更重要的是在于可以借此学习一下Union-Find算法(很重要),以后对于一些问题没有思路的时候,说不定其就是一个很好的解决方法呢?

说一下Couples Holding Hands这道题可以使用贪心法,代码也很少如下,Evaluate Division这道题同样的道理,但是笔者这里要使用另外一种方法即图连通性算法Union-Find,之前也看过有人使用该方法,但是解释的不够详细,所以本篇将详细探讨第二种方法。

class Solution {
public:
    int minSwapsCouples(vector<int>& row) {
        unordered_map<int, int> hash;   // map from person id to position
        for (int i = 0; i < row.size(); ++i) {
            hash[row[i]] = i;
        }
        int ret = 0;
        for (int i = 0; i < row.size(); i += 2) {
            int p1 = row[i];
            int p2 = p1 % 2 == 0 ? p1 + 1 : p1 - 1; // its couple's id
            if (row[i + 1] != p2) {
                int p2_pos = hash[p2];
                hash[row[i + 1]] = p2_pos;
                hash[p2] = i + 1;
                swap(row[i + 1], row[p2_pos]);
                ++ret;
            }
        }
        return ret;
    }
};

这里的hash记录的就是id为row[i]的人在的位置,思路很简单就是看p1的下一个位置是否是其配偶p2,不是的话就寻找p2的位置,然后将两者调换,调换次数ret+1,同时更新Hash即可

下面我们重点说第二种方法:

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

首先说一下Union-Find的问题

它是解决图连通性问题的,即判断图中两个节点是否连通,关于它的原理,推荐一篇讲的很好的博客:

https://blog.csdn.net/dm_vincent/article/details/7655764

通过借鉴博主,笔者这里写了一个python版本:

class UF:
    def __init__(self, N):
        self.id = list(range(N))
        self.sz = list(1 for i in range(N))
        self.count = N
    def GetCount(self):
        return self.count
    def find(self,p):
        while p!=self.id[p]:
            self.id[p] = self.id[self.id[p]]
            p = self.id[p]
        return p
    def union(self,p,q):
        i = self.find(p)
        j = self.find(q)
        if i==j:
            return 
        else:
            if self.sz[i]<self.sz[j]:
                self.id[i] = j
                self.sz[j]+=self.sz[i]
            else:
                self.id[j] = i
                self.sz[i]+=self.sz[j]
        self.count-=1

需要注意一点的就是,在实际中当节点的标签并不是数字即0,1,2的时候

比如用A,B,C,D,E来表示,我们可以使用一个映射函数比如Hash,将其Hash(A)=0,Hash(B)=1,Hash(C)=2,Hash(D)=3,Hash(E)=4

那么就可以继续愉快的使用该模板啦!!!!!!!!!!!!!

博客中博主已经讲的非常清楚了,这里再简单总结一下其思想:

将能连通的节点划分为一个组,那我们在判断两个节点是否连通就可以转化为判断两个节点是否在一个组(代码中count就是代表最后构建完图有多少组)

随着一对对连通节点的输入,首先判断它们是否在现有图下已经能够连通,如果是,那么什么也不做,否者进行两者连通操作union。

最后我们要判断两个节点是否连通只需要调用find即可

假如我们现在知道一个图,是由[0,1,2,3,4,5,6,7]节点组成的他们之间的连通比如是:

1,2

2,4

2,5

3,6

5,6

1,6

那么我们怎么做呢?

很简单,就是先声明一个UF类,初始化为8,因为一共是8个节点嘛

uf = UF(8)

然后一对对输入union进行构建图(也就是说我们已经知道了部分节点之间可以连通这一信息,将其输入我们的算法

union(1,2)  ,union(2,4) ,union(2,5) ,union(3,6) ,union(5,6) ,union(1,6) 

上面就算构建完图了,之后判断任意两个节点只需要调用find函数即可

同时通过上面算法,我们可以发现一个有趣的现象,依据

1,2

2,4

2,5

3,6

5,6

1,6

我们可得原始无向图为:

再经过union后其实图并不是这样了,为什么呢?

再去看一下union函数会发现其是先判断两个节点是否能够连通,如果不能连通才进行合并,否则便不会,也就是说当我们进行到union(1,6)的时候,已经构建的图应该是:

即1和6在当前构建的图中已经能够连通,即find(1)==find(6),所以会直接返回,并不会合并,所以最后构件的图并不会有红线。对应的代码部分就是:

 i = self.find(p)
 j = self.find(q)
 if i==j:
     return

所以最后可以得到这么一个小小的结论,那就是已经能够连通了,那我就不再增加链路了,即最后的图是无向无环图

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

下面进入到实践环节,也就是Couples Holding Hands这道题

本题的最后目的就是要让情侣们都坐在一起,看看需要调动的最少次数。

现在我们这样来考虑这个问题:

我们将一对情侣看成是一个节点即(0,1)看成是图中一个节点,(2,3)看成是一个节点,,,,,,

那么一共就是 N = len(row)/2个节点对吧

假设现在节点1中是(3,5)节点7中是(2,9)

2和3是一对情侣,所以我们认为节点1和节点7是有联系的,就将这两个节点认为是一个连通对,试想如果有一个节点中是(0,1)即恰好是一对情侣,那么最后体现在图上就应该是孤零零一个节点,没有任何节点与其相连。假设我们最后构建完图为:

可以看到节点7和节点8中其实已经是情侣了,不需要再去调动了。

那么我们的目标是什么呢?就是通过调动使图中所有节点都是孤零零的,即彼此之间没有任何连通,这样不就实现了所有情侣都坐在一起了吗?

那么需要调动多少次呢?

很简单,那就是有多少边数就需要调动多少次!!!!!!!!!!!!!

对应到上面的图,这里有5条边,那就需要调动5次(为什么呢?因为每个节点都有元素需要去移动,即每条边都需要动)

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

这里需要注意一个问题就是,当是无向有环图的时候,结论就并不是如此了即:

假设AB,CD,EF是三对情侣

这时候我们不需要动三次,我们只需要动两次,即先将A和E交换,再将C和D交换即可

可以看到有环的时候,交换的次数并不是边数,所幸的是Union-Find算法构造的图并没,并没有环,具体原因我们上面已经讨论过了,用Union-Find去构造上面图的话大概会是:

当然啦,随着输入union的顺序不同,可能结果还会是:

等等,但总之不会出现环,即总会是2条边,当然了最后结果也就是变动2次了

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

那我们怎么利用Union-Find去计算有多少边呢?还记得Union-Find模板中的count吗?它的含义是多少组,对应

就是3组

边数=节点数-组数

对应到这里就是5=8-3

所以最后返回(总节点数-count)即可

下面给一下解决该题的完整代码:

class UF:
    def __init__(self, N):
        self.id = list(range(N))
        self.sz = list(1 for i in range(N))
        self.count = N
    def GetCount(self):
        return self.count
    def find(self,p):
        while p!=self.id[p]:
            self.id[p] = self.id[self.id[p]]
            p = self.id[p]
        return p
    def union(self,p,q):
        i = self.find(p)
        j = self.find(q)
        if i==j:
            return 
        else:
            if self.sz[i]<self.sz[j]:
                self.id[i] = j
                self.sz[j]+=self.id[i]
            else:
                self.id[j] = i
                self.sz[i]+=self.id[j]
        self.count-=1
    

class Solution(object):
    def minSwapsCouples(self, row):
        """
        :type row: List[int]
        :rtype: int
        """  
        pairs = int(len(row)/2)
        uf = UF(pairs)
        for i in range(0,pairs):
            m = row[2*i]
            n = row[2*i+1]
            uf.union(int(m/2),int(n/2))
        return pairs-uf.GetCount()

上半部分就是Union-Find算法,这里不再叙述

我们将(0,1)这对情侣归为节点0

(2,3)这对情侣归为节点1

(4,5)这对情侣归为节点2

,,,,,,,,

(这里节点下标我们从0开始而不是1)

每次循环取两个相邻位置的人即m,n依次取

位置0和位置1的人

位置2和位置3的人

位置4和位置5的人

,,,,,,,

int(m/2)和int(n/2)就是可以得到m和n这两个人对应的节点

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

举个例子,id是5的这个人对应在图上是哪个节点呢?

int(5/2)=2即是节点2

id是9的人呢

int(9/2)=4即是节点4

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

接下来就是调用Union-Find算法的union方法去不断的构建图

可以预见当m和n是一对情侣的时候通过int(m/2)和int(n/2)的计算,其值必然相同,即属于同一个节点,比如m=6,n=7那么

int(m/2)=3,int(n/2)=3,所以union(3,3)即对应图上就是节点3自己,当m=6,n=8时,那么int(m/2)=3,int(n/2)=4,那么就会将节点3和节点4进行相连,当然啦,正如我们之前讨论的还是会先判断一下在已构建的图上是否可以相连

for循环结束,即图构建完毕,返回总节点数-count即可

最后的最后感叹一下吧!这里在使用Union-Find构建图确实很巧妙!!!!!!!!!!!!

当m和n即相邻两个座位是情侣的时候,通过int(m/2),int(n/2)正好可以使两者相同,进而归并到一个节点,当不是情侣的时候则两者不相同,属于不同的节点,我们要让两者可以连通即使用union,在这里m和n不是情侣就是我们知道的部分节点可以连通的信息

int(m/2),int(n/2)的巧妙使用在于题目使用了相邻的数字作为情侣的标签,倘若使用别的比如说ABCDEF字母呢?

那么我们可能就要采取别的手段了无非就是一个映射函数比如是hash,通过该hash可以实现hash(A)=0,hash(B)=0,hash(C)=1,hash(D)=1。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

下面我们来看第二道题:

Union-Find算法同样适用这道题

思路是这样演化的:

相较于上面的Union-Find算法,我们这里还要考虑图的权值,即连接节点与节点之间的边有了权值

我们将equations中的每个字母作为一个节点,values作为权值,对应到题目给的例子,图大概是这个样子:

那么a/c怎么算呢?那就是2*3=6,很直观吧,就是边上的权值进行相乘即可。如果利用Union-Find算法的find函数发现两个节点没有连通直接返回-1

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

比如告诉:

a/b = 2

b/c = 3

a/k = 6

d/m = 7

让求 d/k,显然没有解,体现在图上就是节点d和节点k没有连通。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------

说到这里好像问题解决了,但是你会发现有几个问题需要注意:

一:计算c/a呢?答案应该是1/6,也就是说体现在图上的话,图貌似应该是有向图

二:因为前面我们使用Union-Find算法的时候,并没有权值这一说,现在应该怎么将边的权值这一信息融合到Union-Find算法中

现在我们两个问题一起解决

定义一个字典dict

他的样子是这样的,dict[p] = [q,45],代表节点p到节点q需要走的权值是45,再比如上面例子中的应该就是dict[a] = [c,6]

dict初始化就是将其设为自身,且边的权值是1,比如:

dict = {a:[a,1] , b:[b,1] , c:[c,1] }即Union-Find算法的初始化就是:

for node1,node2 in equations:
      dict[node1] = [node1,1]
      dict[node2] = [node2,1]

下面就是定义find和union函数了

def find(p):
      value = 1
      while p!=dict[p][0]:
            value*=dict[p][1]
            p = dict[p][0]
            return p,value
       
def union(p,q,value):
      root1,value1 = find(p)
      root2,value2 = find(q)
      dict[root1] = [root2,float(value*value2/value1)]

通过union可以看到,每次是将p的根节点归并到q的根节点,换句话说就是将p划分到q组

那value*value2/value1是怎么来的呢?看一下:假如equations中有一对是[p,q],其对应的values中的值是value,现在我们要合并p和q

通过

root1,value1 = find(p)
root2,value2 = find(q)

我们分别找到了p的组号也即其根节点root1,以及其到root1的权值value1

                            q的组号也即其根节点root2,以及其到root2的权值value2

通过:

dict[root1] = [root2,float(value*value2/value1)]

即root1到root2,也就是root1的根节点不再是是自身了,而是变成了root2,进而导致左半部分所有节点使用find的时候,最后返回的不再是root1而是root2。

那么root1到root2的权值是多少呢?从图上可以看到,假如现在我们find(m),依照定义的find函数可得其还是先一路到达root1比如这一过程是权值temp1=value3*value1,然后由root1->root2的权值是temp2,那么m到其所属组号也就是root2一共的权值就是temp1*temp2对吧

从图中可得真实值应该是value3*value*value2

temp2 = (value3*value*value2)/(value3*value1) = value*value2/value1

看到了吧,这就是其来历,再说的直接点就是,我们现在都要走红线了,find(m),就是从m出发,直奔p,然后通过红线,到达root2,只不过由于之前root1是我们的老大,我们还是要先去拜访一下root1,然后再原路返回,去的时候乘以了路上的权值,后来的时候除以这一权值,那么就相当于没走了,进而实现我们从m到root2真实距离

那么这样就解决了有向图问题吗?是的

试想我们现在计算m/q和q/m,怎么做呢就是:

 des1,value1 = find(q)
 des2,value2 = find(m)

用float(value1/value2)就是q/m的值对应到图上就是:

value2/(value3*value*value2)

当求m/q呢,用float(value2/value1)即可对应到图上就是:

(value3*value*value2)/value2

看到了吧,这就体现了有序性

当然了我们还是想要判断一下des1和des2是否相同,不相同的话肯定都不连通,直接返回-1即可

对应到这里des1和des2肯定都是root2啦

还有就是我们看queries中是否有dict关键字中不存在的元素,有的话,直接返回-1就是好,比如

要求求["a", "e"]我们的dict关键字只有abc,没有e,所以不可能有解的。

最后给一下完整代码:


class Solution(object):
    def calcEquation(self, equations, values, queries):
        """
        :type equations: List[List[str]]
        :type values: List[float]
        :type queries: List[List[str]]
        :rtype: List[float]
        """
       
        dict = {}
        result = []
        #UF的init初始化,每个节点的根节点为自身,根节点就是组号
        for node1,node2 in equations:
            dict[node1] = [node1,1]
            dict[node2] = [node2,1]
        def find(p):
            value = 1
            while p!=dict[p][0]:
                value*=dict[p][1]
                p = dict[p][0]
            return p,value
        def union(p,q,value):
            root1,value1 = find(p)
            root2,value2 = find(q)
            dict[root1] = [root2,float(value*value2/value1)]
        #构建图
        for i in range(len(values)):
            union(equations[i][0],equations[i][1],values[i])
        for querie in queries:
            if querie[0] not in  dict or querie[1] not in  dict:
                result.append(-1.0)
                continue
            des1,value1 = find(querie[0])
            des2,value2 = find(querie[1])
            if des1==des2:
                result.append(float(value1/value2))
            else:
                result.append(-1.0)
        return result

通过这道题,我们应该学习了在Union-Find算法中如何融入权值!!!!!!!!!!!!

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

每天进步一点点,结束!!!!!!!!!!!!!!!!!!!!!!

猜你喜欢

转载自blog.csdn.net/weixin_42001089/article/details/84346327
今日推荐