【并查集】力扣947.移除最多的同行或同列石头 (一维/二维)深度搜索&并查集(4种题解)

大概半年前在力扣上写了一个题,947.移除最多的同行或同列石头,里面用到了一个二维的并查集,一直以来我都只会一维的,看到这个二维的半天没想明白,后来终于懂了觉得挺有意思,所以半年后来写个博客记录一下…另外这个题还有深搜的解法,每种都可以用两种(一维/二维)方法来写,对应将其理解成点还是线。在此博客中汇总一下方法。这篇博客是小白向的,写的很详细。

#什么是并查集?

首先介绍一下并查集的概念。并查集一般用在图论中来计算连通分量的个数,它把每个连通分量看成一个集合,不在意内部究竟是如何连接的,结点之间只有“属于”和“不属于”同一连通分量的区别,所以无需在意点与点之间是单向连接还是双向连接。
并查集的主要用途是存储和查找某连通分量根结点。在并查集中,同一连通分量的根节点相同,即属于一个集合,可以用根节点的编号来表示这个集合。并查集中共有多少个这样的集合,就代表着图共有多少个连通分量。假设并查集为root[1..n],根结点i在并查集中的值为root[i]=i,子结点j在并查集中的值为他的父辈结点root[j]=j_father_node。要找到j的根节点,只需要根据它在并查集中的值不断向上索引即可,同一连通分量里的所有结点向上索引得到的结果都是根节点。示例如下:
在这里插入图片描述

如图有两个连通分量,根节点为红色结点,蓝色结点为子节点。将这个图转成并查集root,下方的0~6对应结点编号。
根据并查集来找结点3的根节点,首先root[3]=1,即它的父结点是1,但是root[1]=0,所以结点1不是根节点,且1的父结点是0,继续找0的父结点,root[0]=0,父结点是自己,即意味到达了根节点,因此结点3所在树的根结点是0。同理,{0,1,2,3}属于连通分量(集合)0,{4,5,6}属于连通分量4。
为了避免链式结构里面不断重复地找根节点,因此在查找根节点的时候需要使用路径压缩,即在查找过程中,每次都直接将子节点的值改成根节点。这段过程的代码如下:

def findroot(node):
    if(node!=root[node]):
         root[node]=findroot(root[node]) #路径压缩
    return root[node]  #注意不是node

在使用并查集时,一般先初始化并查集里的元素root[i]=i,意为在遍历边之前,每个结点都可以看做是一个连通分量(互相独立),然后根据边e连接的结点u和v,使用find函数找到它们的根节点fu和fv,如果根节点不同,那么要修改其中一个根节点的值另一个根节点的值(边e将两个连通分量连接起来,因此得到一个连通分量,于是只存在一个根节点),即root[fu]=fv或者root[fv]=fu。

#原文

题目
n块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。如果一块石头的同行或者同列上有其他石头存在,那么就可以移除这块石头。
给你一个长度为n的数组stones ,其中stones[i]=[xi,yi]表示第i块石头的位置,返回可以移除的石子的最大数量。
在这里插入图片描述
示例
输入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
输出:5
解释:一种移除 5 块石头的方法如下所示:
移除石头 [2,2] ,因为它和 [2,1] 同行。
移除石头 [2,1] ,因为它和 [0,1] 同列。
移除石头 [1,2] ,因为它和 [1,0] 同行。
移除石头 [1,0] ,因为它和 [0,0] 同列。
移除石头 [0,1] ,因为它和 [0,0] 同行。
石头 [0,0] 不能移除,因为它没有与另一块石头同行/列。

#题意理解

题目中要返回可以移除的石子的最大数量。这句话意味着移除石子的方法其实很多,那么最大是什么含义呢?
考虑下面两种移法:在这里插入图片描述
可以看到,对于横纵坐标中有一个相同的石头群,只要按照x/y方向移除,而不是留下对角线石头,最终只会剩下一个石头,这就是移除最大石头个数的情况,于是可知一个石头群最后只会有一个石头留下,其他都被移走。所以可以将抽象的题目转化成这个意思:给出一群石头的坐标,两块石头的横纵坐标中只要有一个相同,那么它们同属于一个石头群(连通分量)中。请计算石头个数-石头群的结果。因此,题目转化为计算连通分量的个数的问题。

#并查集版

并查集是用来计算图中连通分量个数的高效方法,在本题中由于结点之间的连接方式和通常情况下的不同,因此有两种解法。

#一维并查集版:

顺着题意,脑海里最先出现的应该是坐标轴上的点,这些点很自然的可以看成单个结点,所以我们可以把它当成普通的图来看。需要注意的是,本题中两个结点连通的定义是横纵坐标中一个值相同。
使用邻接表表示各点之间的连通情况。对于每个结点i,顺序遍历它后面的结点j(优化时间,并查集中只需要知道连通就行,无序在意是单向连接还是双向连接),如果横纵坐标中有一个值相同,那么graph[i]中加入j,意味i和j相通。
得到图的邻接表之后,依次遍历每个结点,对于结点i首先通过并查集查找和他连通的结点集合graph[i]中结点的根节点是否和节点i根节点一致,不一致修改其中一个根节点的值(按照之前并查集中所说)。当对所有结点遍历结束之后,只需查找并查集中root[i]=i的元素个数即可。

代码如下:

class Solution:
    def removeStones(self, stones: List[List[int]]) -> int:
        #把每个坐标看成单独的结点,一维并查集版
        lens=len(stones)
        graph=[[] for i in range(lens)] #存图,graph[i]是和节点i在同一横坐标和纵坐标的结点结合
        root=[i for i in range(lens)] #并查集
        cnt=0 #连通分量个数
        #建表
        for i in range(lens):
            for j in range(i+1,lens):
                if(stones[i][0]==stones[j][0] or stones[i][1]==stones[j][1]):
                    graph[i].append(j)
        #并查集找树根
        def findroot(node):
            if(node!=root[node]):
                root[node]=findroot(root[node]) #路径压缩
            return root[node]  #注意不是node
        #合并树
        for u in range(lens):
            for v in graph[u]:
                fu=findroot(u)
                fv=findroot(v)
                if(fu!=fv):
                    root[fv]=fu
        #计算连通分量
        for i in range(lens):
            if(root[i]==i):
                cnt+=1
        return lens-cnt

#二维并查集版

上面的一维并查集建图的时候用了一个二重循环,如果石头个数很多会有很大的时间开销,因此考虑二维的并查集,也就是不建图,直接根据石头坐标查找根节点。
刚刚看这种题解的时候我一直都没法从过去常见的图论中抽离,搞不懂单独根据一个坐标要怎么使用并查集,很久才想通。 说几个我理解的重点:

  • 必须从本质上理解并查集的作用,并查集要用来查找节点的根节点。假如情况从一维变成二维、三维的,它也必须能够满足多维查找,因此不管有几维,都必须只能使用一个并查集,才满足跨维度查找的需要(否则不同维度之间的结点没法对比和合并),但是不同维度之间需要加以区分,x=1和y=1在并查集中应该处于不同的位置,一般将其中一个维度往后推一个举距离就行。,以此题为例,0≤x/y≤1e4,将y往后移动1e4+1就可以避免和x冲突,坐标(1,1)在并查集中可以写为root[1],root[1+1e4+1]。
  • 多维中,可能不存在通常意义的“点”,而是以线的思想去理解。此时需要根据具体题目来分析怎样才可以属于同一个连通分量。在本题中,单个点一定在一个连通分量中,两个坐标只要xy其中一个值相同就属于一个连通分量,把它转化为维度的思想就是:一个坐标的x/y轴这两条线属于同一连通分量,所以它们(合并之后)在并查集中的值root[x],root[y+1e4+1]要相同,对于其他的坐标,如果它与这个坐标的x/y轴值有一个相同,假设是x,那么这三条线x,y1,y2都属于同一个连通分量,即合并之后root[x],root[y1+1e4+1],root[y2+1e4+1]值相同

由于使用二维写法,那么root[i](0≤i≤1e4)表示x=i的根节点,root[i](1e4+1≤i≤1e4*2+1)表示y=i的根节点,这些线不饱和,即其中一些x轴上的线和y轴上的线可能不存在,所以不能直接初始化并查集root的每个元素为它的下标,因为一旦这么写了,就意味着一个不存在的线也被当成一个连通分量。所以使用-1来表示不存在的线在并查集root中的值。
对于新的线x的初始化放在查找函数中,如果它还没有被放入并查集中,就修改root[x]的值-1为x,表示它为一个连通分量的根节点。
详细代码如下:

class Solution:
    def removeStones(self, stones: List[List[int]]) -> int:
        #二维并查集
        lens=len(stones)
        cnt=0    #连通分量个数
        root=[-1 for i in range(int((1e4+1)*2))] #xy轴都通过root来查找根节点
        def findroot(x):
            if(root[x]==-1):  #新的线,初始化它为自己的根节点
                root[x]=x
            if(x!=root[x]):  #查找根节点
                root[x]=findroot(root[x])
            return root[x]
            
        #一个新的点会在这个操作中将x和y归到同一连通分量中
        for i in range(lens):
            fx=findroot(stones[i][0])  #找线x的根节点
            fy=findroot(stones[i][1]+int(1e4+1))  #找线y的根节点
            if(fx!=fy):    #合并
                root[fy]=fx
        
        for i in range(int((1e4+1)*2)):  #查找连通分量个数
            if(root[i]==i):
                cnt+=1
                
        return lens-cnt

上面的写法中,并查集使用列表,但是这个表不一定饱和所以有一定的空间浪费,且最后还有一层遍历计算连通分量个数,其实在合并连通分量的时候就可以计算了。对这两点优化后,代码如下:

class Solution:
    def removeStones(self, stones: List[List[int]]) -> int:
        #二维并查集
        lens=len(stones)
        cnt=0    #连通分量个数
        root={}  #xy轴都通过root来查找根节点,key:value x/y轴值:所属连通分量标号
        def findroot(x):
            nonlocal cnt
            if(x not in root):
                root[x]=x
                cnt+=1  #新出现一个“独立的结点”,连通数+1
            if(x!=root[x]):
                root[x]=findroot(root[x])
            return root[x]
        
        #一个新的点会在这个操作中将x和y归到同一连通分量中
        for i in range(lens):
            fx=findroot(stones[i][0])
            fy=findroot(stones[i][1]+int(1e4+1))
            if(fx!=fy):
                root[fy]=fx
                cnt-=1  #两个连通分量结合,连通数-1

        return lens-cnt

#深搜版

看题解的时候看到了根据xy轴搜索的dfs方法,觉得很有意思,然后想到把它按照单个节点看也可以使用dfs解,因此深搜版也有两种方法。

#一维(结点)深度搜索版

该题解中,把每个stone看成一个结点,因此有多少块石头就有多少个结点,按照之前建图的方式建立双向连接图。
图建好以后,依次遍历结点,对每个结点进行深搜,从而找到跟这个结点属于同一连通分量的点,标记已访问,之后就不会再被访问了。每个最初能进行dfs的结点就是它所在连通分量的根节点,计算这种结点的个数就知道有多少个连通分量,代码如下:

class Solution:
    def removeStones(self, stones: List[List[int]]) -> int:
        #把每个坐标看成单独的结点,一维并查集版
        lens=len(stones)
        graph=[[] for i in range(lens)] #存图,graph[i]是和节点i在同一横坐标和纵坐标的结点结合
        cnt=0 #连通分量个数
        vis=set()
        #建表,必须是双向连接,否则会有遗漏
        for i in range(lens):
            for j in range(lens):
                if(stones[i][0]==stones[j][0] or stones[i][1]==stones[j][1]):
                    graph[i].append(j)
        #通过结点标号来搜索
        def dfs(node):
            vis.add(node)
            for next_node in graph[node]:
                if(next_node not in vis):
                    dfs(next_node)

        for node in range(lens):  
            if(node not in vis):
                cnt+=1
                dfs(node)

        return lens-cnt

#二维(坐标轴)深度搜索版:

二维的深搜用的也是线的思想,它通过坐标来找到相关联的x/y轴,然后把这些搜索到的x/y轴上的线都归为一组,计算一共有多少组即可。
在这一版中,图的建立有所不同。因为是以线为单位查找,因此需要两个字典来分别存储与x、y相关联的y、x。xdict={key / value : i / [1…j]}用来存x=i这条线上石头的纵坐标j集合,ydict={key / value : i / [1…j]}用来存y=i这条线上石头的横坐标j集合
深搜时使用坐标进行,例:此时通过坐标(1,1)来搜索与x=1和y=1“相连”的其他线。首先最先开始深搜的结点(1,1)是根节点,连通分量+1,然后dfs((1,1))。从xdict中找到在x=1上的石头的纵坐标集合,假设为2,4,然后依次dfs((1,2)),dfs((1,4)),纵坐标遍历结束后,从ydict中找到在y=1上的石头的横坐标集合,假设为5,7,依次dfs((5,1)),dfs((7,1)),对这些结点都继续递归搜索,最终能找到所有属于同一连通分量的石头,每次遍历到的石头坐标都加入vis集合以防无限循环搜索。
具体代码如下:

class Solution:
    def removeStones(self, stones: List[List[int]]) -> int:
        # 二维深搜版
        lens=len(stones)
        xdict={}   # key:坐标x  value:横坐标为x的纵坐标集合
        ydict={}   # key:坐标y  value:纵坐标为y的横坐标集合     
        vis=set()  #visit集合,防重复访问
        cnt=0      #连通分量个数
        #建表
        for i in range(lens): 
            if(stones[i][0] not in xdict):
                xdict[stones[i][0]]=[stones[i][1]]
            else:
                xdict[stones[i][0]].append(stones[i][1])
            if(stones[i][1] not in ydict):
                ydict[stones[i][1]]=[stones[i][0]]
            else:
                ydict[stones[i][1]].append(stones[i][0])
        #深搜
        def dfs(node):  #node为tuple
            vis.add(node)
            #横坐标相同的点为一个连通分量
            for y in xdict[node[0]]:
                next_node=(node[0],y)
                if(next_node not in vis):
                    dfs(next_node)
            #纵坐标相同的点为一个连通分量
            for x in ydict[node[1]]:
                next_node=(x,node[1])
                if(next_node not in vis):
                    dfs(next_node)
        #计算连通分量
        for i in range(lens):
            node=tuple(stones[i]) #set内要是可哈希对象,所以转成元组,不能用列表
            # print(node)
            if(node not in vis):
                cnt+=1
                dfs(node)
        return lens-cnt

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值