图论:BFS与DFS,拓扑排序,前缀树Trie

深度优先搜索depth-first search,dfs

1、定义

这是一种用于遍历或搜索树/图的算法。简单来说,从起始节点开始,沿着路径尽可能深/远地搜索,知道到达叶子节点,然后回溯到上一个节点,继续探索未访问的路径。

2、方法

递归或栈

3、举例:岛屿数量

代码:(注释有解释逻辑)

# 深度优先搜索
'''
逻辑:扫描整个二维网格--》如果遇到‘1’,则以它为起始节点进行深度优先搜索
     --》每个搜索到的‘1’变成‘0’--》进行深度优先搜索的次数=岛屿数量

'''

class Solution(object):
    def numIslands(self, grid):

        # 定义递归函数
        def dfs(grid,i,j):
            grid[i][j]='0'  # 让搜索到的节点为0

            m=len(grid) # (矩阵grid)行
            n=len(grid[0])  # 列

            for x,y in [(i-1,j),(i+1,j),(i,j-1),(i,j+1)]:     # 搜索起始节点的上下左右四个节点
                if x>=0 and x<m and y>=0 and y<n and grid[x][y]=='1':    # 保证搜索的范围不超过矩阵的大小
                    dfs(grid,x,y)  # 递归(回到这个函数定义的第一行)
        
        if not grid:   # 异常检查
            return 0

        ans=0
        for i in range(len(grid)):
            for j in range(len(grid[0])):  # 扫描矩阵
                if grid[i][j]=='1':
                    ans+=1           # 深度优先搜索的次数 == 岛屿数量 
                    dfs(grid,i,j)
        return ans
        

广度优先搜索breadth-first search,bfs

1、定义

从起始节点开始,首先访问所有与起始节点【相邻】的节点,然后【逐层】向外扩展搜索,直到找到目标节点或者遍历完整个图。

2、方法

队列

3、举例:腐烂的橘子

代码:

代码中使用了一个队列来存储腐烂橘子的坐标,并在每次循环中对这个队列进行了多次出队和入队操作。

class Solution(object):
    def orangesRotting(self, grid):
        row=len(grid)
        col=len(grid[0])

        # 异常检查1:如果全是空单元格或者烂橘子,返回0
        check=all(element !=1 for item in grid  for element in item)
        if check :
            return 0


        '''
        找出所有腐烂橘子的坐标--》遍历腐烂橘子的队列--》
        搜索每一个腐烂橘子的上下左右橘子,如果符合条件,将该腐烂橘子变成2,并且将搜索到的腐烂橘子坐标记录在队列中--》最后判断还剩下多少新鲜橘子--》剩下新鲜橘子,返回0,没剩下新鲜橘子,返回time
        '''
        queue=[]
        for i in range(row):
            for j in range(col):
                if grid[i][j]==2:
                    queue.append((i,j))
        
        time=-1   # 注意:初始化时间应该是-1而不是0
        while queue:
            current_len=len(queue)  # 在此处定义queue长度的变量,而不直接在range中使用,是因为下面的小循环中queue的长度可能有变化,为了防止循环出错,所以定义变量。
            for _ in range(current_len):
                i,j=queue.pop(0)   # 处理一个腐烂橘子就从queue踢掉这个坐标,下次不再处理这个坐标
                for x,y in [(1,0),(-1,0),(0,1),(0,-1)]:  # 固定写法:上下左右搜索
                    temp_i=i+x
                    temp_j=j+y
                    if temp_i>=0 and temp_i<row and temp_j>=0 and temp_j < col:  # 坐标必须在矩阵grid内
                        grid[temp_i][temp_j]=2
                        queue.append((temp_i,temp_j))  # 搜索到新的腐烂橘子坐标就加入queue
            time +=1  #遍历一个腐烂橘子,时间加1
        
        for item in grid:
            if 1 in item:   # 异常检查2:如果全部搜索玩还剩下新鲜橘子,那么就返回-1
                return -1
        return time    # 否则返回遍历的时间

        
                    

        

BFS与DFS的区别

总的来说区别不是很大,但有些细节要注意区分。

1、搜索顺序不同

2、搜索策略不同

dfs需要设置适当的终止条件,不然可能会陷入无限循环或长路径;

bfs可以保证找到的路径是最短路径。

3、适用场景不太相同

dfs适合解决图的遍历问题,比如判断图是否连通、解决路径规划等问题;

bfs解决图的最短路径问题、状态转移图的搜索问题(迷宫问题、八数码问题等)。

有向图-拓扑排序算法

A.有向图常见概念
  1. 顶点(Vertex):有向图中的基本单位,表示图中的节点或元素。通常用不同的符号或标签来表示各个顶点。

  2. 边(Edge):连接两个顶点的有向边,具有方向性,表示从一个顶点到另一个顶点的有向关系。有向边通常用箭头来表示方向。

  3. 入度和出度(In-degree and Out-degree):对于有向图中的每个顶点,其入度表示指向该顶点的边的数量,出度表示从该顶点指出的边的数量。

  4. 路径(Path):顶点序列构成的有向边序列,表示从一个顶点到另一个顶点的一系列连续边的集合。

  5. 有向环(Directed Cycle):在有向图中,如果存在一条路径,使得起点和终点相同,并且路径中至少包含一条有向边,那么这条路径就称为有向环。

  6. 拓扑排序(Topological Sorting):有向图的一种排序方法,它可以将图中的顶点线性排序,使得对于图中的每一条有向边 (u, v),在排序中顶点 u 都出现在顶点 v 的前面。拓扑排序常用于任务调度、课程选修等问题中。

  7. 强连通图(Strongly Connected Graph):在有向图中,如果对于图中的任意两个顶点 u 和 v,都存在从 u 到 v 和从 v 到 u 的路径,那么这个图就是强连通图。

  8. 强连通分量(Strongly Connected Components,SCC):有向图中的极大强连通子图(即在子图内,任意两个顶点都是强连通的),强连通分量是有向图中一种重要的结构。

B.拓扑排序详解

拓扑排序的算法可以通过深度优先搜索(DFS)或广度优先搜索(BFS)实现。算法的基本思想是遍历图中的每个顶点,并递归地将顶点标记为已访问,然后将其所有邻接顶点加入到排序结果中。在实际应用中,如果存在循环依赖(即图中存在环),则无法进行拓扑排序。

拓扑排序有多种实现方法,包括 Kahn 算法、DFS 算法等。其中 Kahn 算法是一种基于入度(顶点的入边数量)的贪心算法,它通过不断删除入度为 0 的顶点并更新其邻接顶点的入度来实现拓扑排序。

C.举例:leetcode207‘课程表’

关于这道题,官方解析

207. 课程表 Course Schedule 【LeetCode 力扣官方题解】_哔哩哔哩_bilibili

做的动画非常清晰易懂,总而言之就是根据入度数逐个判断。需要补充collections的一些用法知识。

代码:

class Solution(object):
    def canFinish(self, numCourses, prerequisites):
        edges=collections.defaultdict(list)  # 存储顶点信息
        indeg=[0]*numCourses    # 创建入度数列表
        res=0    # 已修完的课程数

        for info in prerequisites:     
            edges[info[1]].append(info[0])    # 更新修课程顺序信息,比如修完0可以修1,2,修完1可以修3 ,e.g. {0:[1,2],1:[3]}
            indeg[info[0]] +=1         # 更新入度数列表
        
        q=collections.deque([u for u in range(numCourses) if indeg[u]==0])  #创建入度数=0的双端队列
        
        while q:  # 首先修完入度数=0的课程,因为这些课程不需要提前修其他的课程
            u=q.popleft()  # 修完一门就从q中移除,下次不做处理
            res +=1        
            for v in edges[u]:   # 检索该课程修完之后可以修的课程有哪些?
                indeg[v]-=1      # 然后把相应的课程入度数-1
                if indeg[v]==0:  # 如果-1之后入度数=0,那么将该课程放入q中,下次处理
                    q.append(v)
        
        return res==numCourses   # 如果拓扑排序之后顶点数=课程数,代表True,否则返回False

前缀树Trie

1、定义

顾名思义,trie就是每个样本都从头节点开始,根据字符或前缀数字建出来的一棵大树。没有路了就新建节点,有路就复用节点。每个节点只存储pass和end两种信息,字符信息只在‘路’上传递。

2、优点、缺点和实现方法

(1)优点:根据前缀信息来选择树上的信息,可以节省大量时间。常见于搜索引擎的自动补全、word里面的拼写检查等。

(2)缺点:比较浪费空间,查询时和字符数量和种类有关。

(3)实现方法:类描述,静态数组(推荐)。

(内心os: 概念不难懂,但是代码有点点绕,如果想未来能手撕,建议多打打代码熟悉下,知道前缀树到底是如何实现它说的那些规则的,虽然网上说静态方法更适合比赛和笔试,但是你要是想搞透这个知识点,两种方法都敲敲)

3、举例:leetcode208

代码:

class Trie(object):
    '''
    模板,背吧
    '''
    def __init__(self):
        '''
        初始化你的前缀树结构:子节点+树枝
        '''
        self.child=dict()
        self.isword=False


    def insert(self, word):
        rt=self    ##########相当于c++的this指针!!!
        for w in word:
            if w not in rt.child:  # 没有就新建
                rt.child[w]=Trie()
            rt=rt.child[w]   # 往树的下面走
        rt.isword=True  
        
     

    def search(self, word):
        rt=self
        for w in word:
            if w not in rt.child:    # 有字母不在这条path上,断了
                return False
            rt=rt.child[w]     #沿着path往下走
        return rt.isword==True    #看isword位


    def startsWith(self, prefix):
        rt=self
        for w in prefix:
            if w not in rt.child:    #path断了
                return False
            rt=rt.child[w]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值