Python - 深夜数据结构与算法之 Graph

news2024/9/29 1:16:11

目录

一.引言

二.图的简介

1.Graph 图

2.Undirected graph 无向图

3.Directed Graph 有向图

4.DFS / BFS 遍历

三.经典算法实战

1.Num-Islands [200]

2.Land-Perimeter [463]

3.Largest-Island [827]

四.总结


一.引言

Graph 无论是应用还是算法题目在日常生活中比较少见,但是其应用非常广泛,现在社交关系网络很多内容都是基于 Graph 构建的,例如我们常见的各类 GraphEmbedding,其就是基于社交关系网络图进行游走生成对应 Node Embedding。

二.图的简介

1.Graph 图

由点和边构成的数据结构即可称之为图,其在代码中一班以 Graph(V, E) 来表示,其中 V 代表点,是顶点的有穷非空集合,E 代表边,是 V 中顶点偶对的有穷集合,偶对我们可以理解为每一条边都有左右两个端点,所以需要偶对。点有入度和出度之分,即从该点出发有几条路径,如果是无向图二者相等。边除了方向外,有的场景还有权重,例如社交场景评估两个相邻用户的亲密度 E 就是有权重的,默认情况下所有边的权重均为 1,即众生平等。

2.Undirected graph 无向图

◆ 无向无权图

图一般使用邻接矩阵或邻接列表的形式表示点之间的关系,边的关系则蕴含在两点之间。

◆ 无向有权图

默认情况下,边的权重都唯一,实际工业场景下,边之间往往通过 weight 来描述两个点之间的关联程度,如果使用邻接矩阵,那么直接在对应位置赋值 weight 即可,如果是邻接列表则可以使用一个二元组进行表示。注意: 不论是无向有权还是无向无权,其对应的邻接矩阵是对称矩阵,因为 0-1 相连必然 1-0 也相连。

3.Directed Graph 有向图

有向图只会在存在的方向上表示,例如 0->1,那么邻接矩阵 [0, 1] 是有值的,但 [1, 0] 为 0,此时邻接矩阵就不在是对称矩阵,除非每个边都是双向的。有向有权图可以参考上面的无向有权图,我们只需给矩阵修改 weights,数组转换为二元组即可。 

4.DFS / BFS 遍历

◆ DFS

深度优先遍历,这里与二叉树的遍历有一点不同是图可能存在环,从而导致元素重复,所以需要进行节点的去重。其实现遵循递归,也满足递归的三要素:

- 边界条件 判断节点是否被访问

- 处理逻辑 添加当前处理的点

- 自身调用 继续处理未处理的点 

处理中不断向 next_node 出发,所以是深度优先。

◆ BFS

广度优先遍历,其会向两边扩展搜索,二叉树的层数遍历就可以通过 BFS 实现,下面方法中 generate_related_nodes 其实就是搜索两边或者周围节点的伪代码,后续我们会带来 DFS、BFS 更详细的介绍和代码。大家在这里只要明确图遍历中,DFS 和 BFS 是很重要的两种方法即可。

三.经典算法实战

1.Num-Islands [200]

岛屿数量: https://leetcode.cn/problems/number-of-islands/

◆ 题目分析

海岛的形态与数字 0、1 的关系,我们需要遍历一个点周围的情况判断,以 [0, 1] 的点为例,我们以其为起点进行遍历,对于二叉树而言,其拥有 left 和 right 两个遍历方向,而对于网格问题,其拥有上下左右四个方向,遇到 1 就继续遍历,遇到 0 则停止遍历,当无法继续扩展时,代表遍历完成,此时就生成一个岛屿。这里还有一个问题,就是 for 循环遍历岛中的节点,会有重复的情况,例如遍历 [0, 1] 和 [1, 1] 的点都可以生成岛屿 1,所以这里我们还要对遍历过的岛屿进行标记,防止其他节点再扩展,这样重复的问题也可以解决。

至于上下左右的界定,我们针对 (x, y) ± 1 即可,需要注意出格的行为,即 (x, y) 超出岛屿范围。和二叉树类比的话,停止条件就是遇到 0 或者遇到访问过的点 Visted Point 或者出格 Out of Bound,就好比是 root == None;而遍历 left、rigth 则变成 (r-1, c)、.....、(r+1, c)。

◆ DFS

class Solution(object):
    def numIslands(self, grid):
        """
        :type grid: List[List[str]]
        :rtype: int
        """
        # 异常情况
        if not grid:
            return 0

        # 记录岛屿数量
        count = 0

        # 遍历岛屿
        for row in range(len(grid)):
            for col in range(len(grid[0])):
                if grid[row][col] == "1":
                    # 沿 (row, col) 递归遍历上下左右
                    self.dfs(grid, row, col)
                    count += 1
                    
        return count
    
    def inArea(self, grid, row, col):
        return 0 <= row < len(grid) and 0 <= col < len(grid[0])

    def dfs(self, grid, row, col):
        # 不在网格 -> 返回
        if not self.inArea(grid, row, col):
            return

        # 不是岛屿->返回        
        if grid[row][col] != "1":
            return
    
        # 已遍历标记
        grid[row][col] = "2"

        self.dfs(grid, row - 1, col) # 上
        self.dfs(grid, row + 1, col) # 下
        self.dfs(grid, row, col - 1) # 左
        self.dfs(grid, row, col + 1) # 右

inArea 函数负责判断当前点是否在 grid 网格区域内,超出网格则停止 DFS,其次针对遍历过的 "1" 即陆地,为了防止 DFS 重复循环,我们需要将其换一个标记从而避免重复,最后上下左右探索即可。每探索一次,如果能够发现陆地则陆地全部被标记为已走,则下次无法遍历,从而划分出一块一块土地。

如上图所示,遍历到 [0, 1] 位置时会扩展出岛屿1,遍历到 [0, 3] 位置时扩展出岛屿 2,以此类推,每次 count += 1 即可,当 grid 里没有 "1" 即陆地时,遍历结束。

2.Land-Perimeter [463]

岛屿的周长: https://leetcode.cn/problems/island-perimeter/description/

◆ 题目分析 

和上题很像,也是 grid 中的岛屿,这不过这里有一个限定即只有一个岛屿,所以我们 BFS 一次就能 get 到整个岛屿。观察图像可以发现,对于 (row, col) 而言如果下一个点在海里或者 grid 外,这里便存在一个边即属于周长的边,画个图理解下,红色箭头是出 grid 的边,蓝色箭头是入海流,正所谓红头依山尽,蓝头入海流:

◆ DFS

class Solution(object):
    def islandPerimeter(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """
        # 寻找小岛的起点
        for row in range(len(grid)):
            for col in range(len(grid[0])):
                if grid[row][col] == 1:
                    return self.dfs(grid, row, col)

        return 0

    # 是否在 grid 网格内
    def inArea(self, grid, row, col):
        return 0 <= row < len(grid) and 0 <= col < len(grid[0])

    def dfs(self, grid, row, col):
        # 蓝色箭头 -> 出格 -> 边长 + 1
        if not self.inArea(grid, row, col):
            return 1
        
        # 红色箭头 -> 出海 -> 边长 + 1
        if grid[row][col] == 0:
            return 1
        
        # 只剩 == "2" 的已经遍历的情况了,忽略
        if grid[row][col] != 1:
            return 0

        grid[row][col] = 2

        return self.dfs(grid, row - 1, col) + self.dfs(grid, row + 1, col) + self.dfs(grid, row, col - 1) + self.dfs(grid, row, col + 1)

红箭头出海,蓝箭头出格,这两个情况 +1 其余情况说拜拜即可,dfs 遍历思路与上面相同,也是上下左右出发。

3.Largest-Island [827]

造海填路问题: https://leetcode.cn/problems/making-a-large-island/

◆ 题目分析 

grid 网格内有多个岛屿,通过将一块海洋改变陆地,求填海造地后最大的面积,这里我们可以借助第一题的思路,先把陆地都照出来,然后遍历 "0" 即海洋,看哪个海洋变成陆地后,可以连接更多土地。 除此之外,我们还需要对相连的陆地给与记号,保证面积不会重复累加。

◆ DFS

#!/usr/bin/python
# -*- coding: UTF-8 -*-

class Solution(object):

    def largestIsland(self, grid):
        """
        :type grid: List[List[int]]
        :rtype: int
        """

        if not grid:
            return 0

        # 标记访问节点,记录每个岛屿面积
        index = 2
        location = {}
        visited = set()

        for row in range(len(grid)):
            for col in range(len(grid[0])):
                # 每块陆地标记为一个记号
                if grid[row][col] == 1:
                    self.dfs(grid, row, col, index, location, visited)
                    index += 1
        
        # 通过原始岛屿信息更新 max
        if location:
            max_island = max(location.values())
        else:
            max_island = 0

        for row in range(len(grid)):
            for col in range(len(grid[0])):
                # 找到海洋并计算联通面积
                if grid[row][col] == 0:
                    max_island = max(self.getAroundArea(grid, row, col, location), max_island)

        return max_island

    def getAroundArea(self, grid, row, col, location):

        # 记录最大面积
        res = 1
        # 同一块陆地算一次
        area = set()
        # 上下左右找陆地
        if self.inArea(grid, row + 1, col) and grid[row + 1][col] != 0:
            area.add(grid[row + 1][col])
        if self.inArea(grid, row - 1, col) and grid[row - 1][col] != 0:
            area.add(grid[row - 1][col])
        if self.inArea(grid, row, col + 1) and grid[row][col + 1] != 0:
            area.add(grid[row][col + 1])
        if self.inArea(grid, row, col - 1) and grid[row][col - 1] != 0:
            area.add(grid[row][col - 1])


        # 土地去重相加
        if location and area:
            for index in area:
                res += location[index]

        return res

    def dfs(self, grid, row, col, index, location, visted):

        # 不在网格 -> 返回
        if not self.inArea(grid, row, col):
            return

        # 不是岛屿->返回
        if grid[row][col] != 1:
            return

        # 已遍历标记
        grid[row][col] = index
        if index not in location:
            location[index] = 0
        if (row, col) not in visted:
            visted.add((row, col))
            location[index] += 1

        self.dfs(grid, row - 1, col, index, location, visted)  # 上
        self.dfs(grid, row + 1, col, index, location, visted)  # 下
        self.dfs(grid, row, col - 1, index, location, visted)  # 左
        self.dfs(grid, row, col + 1, index, location, visted)  # 右

    # 是否在 grid 网格内
    def inArea(self, grid, row, col):
        return 0 <= row < len(grid) and 0 <= col < len(grid[0])

dfs 负责在 grid 中寻找土地并标记,getAroundArea 负责遍历每一片海,并在海的上下左右寻找可能存在的岛屿,如果存在则将此地造海 res = 1,再加上发现的陆地的面积,最后更新 max 即可。这个方法的优点是思路很好理解,但是本题边界情况很多,需要判断的空 set、dict 也很多,而且遍历多次时间复杂度也较高。

四.总结

上面介绍了图 Graph<V,E> 的一般概念,以及通过 BFS 解决图的一些相关问题,图的题目整体考察不多,但是 DFS、BFS 的用法还是要熟悉。这里关于上面图 DFS 遍历的算法,推荐大家参考乐扣大神的题解,思路非常清晰: 岛屿相关问题 DFS 思路。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1333812.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

股市中的Santa Claus Rally (圣诞节行情)

圣诞节行情 Santa Claus Rally Santa Claus Rally 是指 12 月 25 日圣诞节前后股市的持续上涨这样一个现象。《股票交易员年鉴》的创始人 Yale Hirsch 于 1972 年创造了这个定义&#xff0c;他将当年最后五个交易日和次年前两个交易日的时间范围定义为反弹日期。 根据 CFRA Re…

OpenAI科学家Hyung Won Chung演讲精华版

文章目录 第一个观点&#xff1a;涌现第二个观点&#xff1a;如何扩大规模1、标记化2、嵌入3、计算4、评估&#xff08;损失函数&#xff09;5、反向传播 最近从Google跳槽到OpenAI的AI科学家 Hyung Won Chung 比较拗口&#xff0c;我就简称尚哥了 他最近做了一个技术演讲 …

GPU性能实时监测的实用工具

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

ElasticSearch入门介绍和实战

目录 1.ElasticSearch简介 1.1 ElasticSearch&#xff08;简称ES&#xff09; 1.2 ElasticSearch与Lucene的关系 1.3 哪些公司在使用Elasticsearch 1.4 ES vs Solr比较 1.4.1 ES vs Solr 检索速度 2. Lucene全文检索框架 2.1 什么是全文检索 2.2 分词原理之倒排索引…

Unity-Shader-渲染队列

Unity-Shader-渲染队列 渲染简介Unity中的几种渲染队列Background (1000)最早被渲染的物体的队列。Geometry (2000) 不透明物体的渲染队列。大多数物体都应该使用该队列进行渲染&#xff0c;也就是Unity Shader中默认的渲染队列。AlphaTest (2450) 有透明通道&#xff0c;需要进…

Python画皮卡丘

代码&#xff1a; import turtledef getPosition(x, y):turtle.setx(x)turtle.sety(y)print(x, y)class Pikachu:def __init__(self):self.t turtle.Turtle()t self.tt.pensize(3)t.speed(9)t.ondrag(getPosition)def noTrace_goto(self, x, y):self.t.penup()self.t.goto(…

Python - 深夜数据结构与算法之 Heap Binary Heap

目录 一.引言 二.堆与二叉堆介绍 1.Heap 堆 2.Binary Heap 二叉堆 3.HeapifyUp 添加节点 4.HeapifyDown 删除节点 5.Heap 时间复杂度 6.Insert & Delete 代码实现 三.经典算法实战 1.Smallest-K [M14] 2.Sliding-Window-Max [239] 3.Ugly-Number [264] 4.Top-…

机器学习或深度学习的数据读取工作(大数据处理)

机器学习或深度学习的数据读取工作&#xff08;大数据处理&#xff09;主要是.split和re.findall和glob.glob运用。 读取文件的路径&#xff08;为了获得文件内容&#xff09;和提取文件路径中感兴趣的东西(标签) 1&#xff0c;“glob.glob”用于读取文件路径 2&#xff0c;“.…

靠谱免费的MAC苹果电脑杀毒软件CleanMyMac X2024

您是否曾经为Mac电脑的性能下降、存储空间不足而烦恼&#xff1f;是否希望有一个简单而高效的解决方案来优化您的Mac系统&#xff1f;那么&#xff0c;我向您介绍一款非常出色的工具&#xff1a;CleanMyMac X。它能够轻松处理这些问题&#xff0c;并让您的Mac恢复到最佳状态。 …

新版IDEA中Git的使用(三)

说明&#xff1a;前面介绍了在新版IDEA中Git的基本操作、分支操作&#xff0c;本文介绍一下在新版IDEA中&#xff0c;如何回滚代码&#xff1b; 分以下三个阶段来介绍&#xff1a; 未Commit的文件&#xff1b; 已经Commit&#xff0c;但未Push的文件&#xff1b; 已经Push的…

常见的Ubuntu命令30条(二)

Ubuntu命令是指在Ubuntu操作系统中用于执行各种任务和操作的命令行指令。这些命令可以用于管理系统、配置网络、安装软件、浏览文件等。Ubuntu命令通常在终端&#xff08;Terminal&#xff09;应用程序中输入并执行。 history&#xff1a;显示命令行历史记录。grep&#xff1a…

Spark编程语言选择:Scala、Java和Python

在大数据处理和分析领域&#xff0c;Apache Spark已经成为一种非常流行的工具。它提供了丰富的API和强大的性能&#xff0c;同时支持多种编程语言&#xff0c;包括Scala、Java和Python。选择合适的编程语言可以直接影响Spark应用程序的性能、可维护性和开发效率。在本文中&…

jvm_下篇_补充:浅堆深堆与内存泄露

笔记来源&#xff1a;尚硅谷 JVM 全套教程&#xff0c;百万播放&#xff0c;全网巅峰&#xff08;宋红康详解 java 虚拟机&#xff09; 同步更新&#xff1a;https://gitee.com/vectorx/NOTE_JVM https://codechina.csdn.net/qq_35925558/NOTE_JVM https://github.com/uxiahnan…

shell 如何调用多个脚本

简介 这篇文章主要描述如何通过主脚本去调用其他脚本中的方法&#xff0c;调用的过程中可能出现哪些坑&#xff0c;如何避免。 目录 1. 主脚本调用其他脚本的方法 1.1. bash方法 1.2. source方法 2. 避坑技巧 2.1. 路径配置无效 2.2. source变量冲突 3. 总结 1. 主脚本调…

工具系列:TensorFlow决策森林_(5)使用文本和神经网络特征

文章目录 设置使用原始文本作为特征使用预训练的文本嵌入同时训练决策树和神经网络构建模型训练和评估模型 欢迎来到 TensorFlow决策森林&#xff08; TF-DF&#xff09;的 中级教程。 在本文中&#xff0c;您将学习有关 TF-DF的一些更高级的功能&#xff0c;包括如何处理自…

SQL进阶理论篇(二十一):基于SQLMap的自动化SQL注入

文章目录 简介获取当前数据库和用户信息获取MySQL中的所有数据库名称查询wucai数据库中的所有数据表查看heros数据表中的所有字段查询heros表中的英雄信息总结参考文献 简介 从上一小节&#xff0c;可以发现&#xff0c;如果我们编写的代码存在着SQL注入的漏洞&#xff0c;后果…

HarmonyOS的装饰器之BuilderParam 理解

BuilderParam 装饰器 使用时间&#xff1a;当定义了一个子组件&#xff0c;并且子组件的build()中有一个布局在不同的父组件&#xff0c;实现效果不一样的时候&#xff0c;可以在子组件中用这个BuilderParam装饰器&#xff0c; 在父组件用Builder 装饰器进行实现&#xff0c;然…

Lua的垃圾回收机制详解

Lua 是一种轻量级的编程语言&#xff0c;广泛用于嵌入到其他应用程序中&#xff0c;尤其是在游戏开发领域。Lua 的内存管理机制采用了自动垃圾收集&#xff08;Garbage Collection&#xff09;的方法。以下是Lua内存管理的一些关键方面&#xff1a; 垃圾收集原理概述 Lua 使用…

我的软考之路

缘起 2016年&#xff0c;入职了一家业务相对稳定的公司。技术栈的切换使得刚入职的时光格外忙碌。然而当所有工作所需技术逐步掌握并渐渐精通&#xff0c;摸鱼的时间也相对多了起来。 这样的日子一多&#xff0c;危机感开始蔓延&#xff0c;毕竟35是谁都绕不过的一道坎。程序猿…

SQL实践篇(一):使用WebSQL在H5中存储一个本地数据库

文章目录 简介本地存储都有哪些&#xff1f;如何使用WebSQL打开数据库事务操作SQL执行 在浏览器端做一个英雄的查询页面如何删除本地存储参考文献 简介 WebSQL是一种操作本地数据库的网页API接口&#xff0c;通过它&#xff0c;我们可以操作客户端的本地存储。 WebSQL曾经是H…