S'S ALGORITHM

概念和适用范围

概念上来说,它关注的是将树节点,按照高度进行合并。对一棵用edges表述的树来说,具体合并过程,相当于将每一个子节点都不断的向上合并,直到所有的节点都被压扁到他们共享一个父亲节点。这样以来如果有任何想要连接子节点,而不共享父节点的边,都是那个构成环的边,也就是冗余的边。

适用问题:

现实世界问题:

很好,很复杂。并查集本身就是一个高级的主题。在实际应用中联想,完全不如在算法问题中这么明晰,重要的是在实际中,不断的找到算法的影子。将理论和实践结合。

总的来说,并查集,和联通,冗余,分组问题相关,在遇到这些问题的时候,可以考虑运用。

问题1:Redundant Connection

该问题中,tree被定义为,无向无环图。你被给定了一个图,有 n 个节点,从 1 到 n,但是在这个图中,被加入了一条冗余的边,这条边被 1 到 n 中两个不同的端点连接。返回可以删除的边,以便生成的图是 n 节点树。如果有多个答案,则返回输入列表中最后出现的符合条件的答案。

比如,给定edges = [[1, 2],[1, 3],[2, 3]], 输出[2, 3]

其实这是一个典型的,自己组装并查集的题,通过这道题正好可以切入并查集的基础结构。代码如下:

class Solution(object):
    def findRedundantConnection(self, edges):
        """
        :type edges: List[List[int]]
        :rtype: List[int]
        """
        # 初始化父节点为该节点自己
        par = [i for i in range(len(edges) + 1)]
        # 将每个节点的高度设置为1
        rank = [1] * (len(edges) + 1)

        def find(n):
            """
            helper函数,是为了在下面的步骤中,用于找到节点的父节点
            while的部分表示,当自己的父节点不是自己的时候,就不断的将父节点的父节点,设置为该节点的父节点
            是一种压缩树高度的行为,最终找到父节点,即为该节点自己
            """
            p = par[n]
            while p != par[p]:
                par[p] = par[par[p]]
                p = par[p]
            return p

        def union(n1, n2):
            """
            union是用来进行合并的部分
            找到这两个节点的父节点,然后比较树的高度,将低的树合并到高的树上,并重新计算高度
            如果他们已经共享父节点了就不需要何必了
            return的,是否进行了合并
            """
            p1, p2 = find(n1), find(n2)

            if p1 == p2:
                return False
            
            if rank[p1] > rank[p2]:
                par[p2] = p1
                rank[p1] += rank[p2]
            else:
                par[p1] = p2
                rank[p2] += p1
            return True

        # 通过union返回值判断两个节点是否已经有同样的父节点
        # 如果有的话说明再连接就冗余了,返回该节点即可
        for n1, n2 in edges:
            if not union(n1, n2):
                return [n1, n2]

笔记:这是一道逻辑非常清晰的题,不再赘述。时间复杂度和空间复杂度都是O(n)。

这是因为在进行find和union的时候,它的时间复杂度都是阿克曼反函数大小。

时间复杂度O(α(n))中的α(n)是阿克曼反函数(Ackermann inverse function)的渐进记号表示。

阿克曼函数是一个数论函数,增长非常快,被用来定义计算复杂性理论中所需的差值级。α(n)是阿克曼函数的反函数,它是一种极其缓慢增长的函数。

具体来说:

因此,O(α(n))被认为是计算复杂性理论中的一个很好的渐进记号,用来表示一个非常优秀、高效的算法,它的运行时间基本上可以认为是一个很小的常数。并查集的时间复杂度就是这个量级。

总之,O(α(n))反映了一种极其高效、接近最优的算法时间复杂度,实际应用意义重大。

问题2:Last Day Where You Can Still Cross

困难难度的力扣题。

给定一个由格子组成的二维地图,其中包含了陆地(用0表示)和水(用1表示)。在第一天都是陆地,但是每一天都有一个格子被水淹没。假设你可以在道路上自由移动,但不能斜着走。你从地图的最上方的某个起始位置出发,要前往地图的最下方。问题是在给定的淹水条件下,你哪天是最后能通过格子的。

比如cells = [[1, 1],[2, 1],[1, 2],[2, 2]],比如该cells给定的就是第index天,淹水的格子。结果应当返回2,这是因为在第三天开始无法通过格子。所以第二天是最后期限(第0天是初始天)。

同样是使用并查集思路:

以下是代码的思路步骤:

整体思路是利用并查集算法来合并连通分量,并通过维护每个连通分量的边界来判断是否满足条件。算法的核心是遍历格子并进行合并操作,以及检查合并后的连通分量的边界是否满足条件。

class Solution:
    def latestDayToCross(self, row, col, cells):
        n = row * col
        # root是根节点,每个节点都指向自己,left和right是最左和最右边界
        root, left, right = list(range(n)), [0] * n, [0] * n

        # 使用格子索引更新左右边界的列表,所谓边界是指格子所在位置的col
        for i in range(col):
            for j in range(row):
                # [0, 0, 1, 1]
                left[i * row + j] = i
                right[i * row + j] = i

        # find找到根节点
        def find(x):
            if x != root[x]:
                root[x] = find(root[x])
            return root[x]

        # 合并连通分量,返回左右边界
        def union(x, y):
            a, b = find(x), find(y)
            if a != b:
                root[a] = b
            left[b] = min(left[b], left[a])
            right[b] = max(right[b], right[a])

        # 开始搜索
        seen = set()
        dirs = ((1, 0), (0, 1), (-1, 0), (0, -1),
                (1, 1), (-1, 1), (1, -1), (-1, -1))

        for i, cell in enumerate(cells):
            cx, cy = cell[0] - 1, cell[1] - 1
            for dx, dy in dirs:
                x, y = cx + dx, cy + dy
                #检查相邻格子的有效性和是否见过
                if 0 <= x < row and 0 <= y < col and (x, y) in seen:
                    # union两个格子
                    union(cy * row + cx, y * row + x)
                    # 找到合并后的根节点
                    new = find(y * row + x)
                    # 检查新的组合是否可以跨越边界
                    if left[new] == 0 and right[new] == col - 1:
                        # 为真则返回天数
                        return i
            seen.add((cx, cy))

        return n

学习笔记:时间复杂度和空间复杂度O(mxn)。