LeetCode | 一文帮你搞定BFS、DFS算法(python版)

模板方法

使用BFS,DFS的题目,在leetcode上一般标记为medium或者hard。但从思维逻辑上看,其难度定义偏高。可能从代码量来看,写一道BFS或者DFS的篇幅比其他类型的题目要多。
BFS,DFS既然思维固定,必然有其套路。套用模板方法,让我们的解题更加流畅。有如庖丁解牛,游刃有余。

BFS模板

BFS思路是先将邻居加进来,如果只是口述的话难免枯燥。
下面将以图上的搜索(找start_node到end_node之间的最短距离长度)为例,阐述怎样将思维变成代码。

得到图

正如链表其实质为节点和指向下一个节点的指针,图的实质也就是图上的节点和连接各个节点的边。
由此,对于图(这里对有向图和无向图不做区分)其实有两种表示方法:
LeetCode | 一文帮你搞定BFS、DFS算法(python版)_第1张图片

  • 用各个节点之间的边(edges)表示图。
    如我画的草图,则图可以由各个边表示,即为edges[[0,1],[0,2],[1,4],[2,3],[2,4]],每一条边连接两个顶点。根据题目,可以规定是否是有向图的边。
  • 用节点到节点之间的连接表示图
    假设图有n个节点,草图里面有5个。 则graph表示为[[1,2],[0,4],[0,3],[2],[1,2]]。graph中index表示节点的标号,即节点0和节点1、2相连,依次类推。此对应关系也可以使用字典表示。
    BFS第一步要得到图,可以使用这两种方式中任一种,视自己熟悉程度和解题的快捷程度而定。
    当然,可以根据edges构建图,不是很难:
def initial_graph(n, edges):
    dict_graph = {
     }

    for i in range(n):
        dict_graph[i] = []
        
    num_e = len(edges)
    for i in range(num_e):
        u = edges[i][0]
        v = edges[i][1]
        dict_graph[u].append(v)
        dict_graph[v].append(u)

    return dict_graph

使用队列

bfs适用于层级搜索,队列先进先出,量身定做。
python中队列两种导入方法为:

# 1.使用queue
from queue import Queue
q = Queue() # 定义,为什么是这样涉及python的设计,不是很懂
q.put(node) # 放入
q.get() # 出队
# 2.使用deque
import collections
q = collections.deque() # 双向队列
q.append() # 入队
q.popleft() # 出队 

节点入队

# 使用Queue()定义
q.put(start_node)
# 为防止无向图中回溯,使用set阻断
hash_set = set()
hash_set.add(start_node)

bfs主体,需要背诵的部分

step = 0
while not q.empty():
	size = len(q)
	step += 1
	for iter_num in range(size):
		node = q.get() # current node
		# get the neighbor
		for neighbor in dict_graph[node]: 
			if neighbor == end_node: # find it!!!!
				return step
			if neighbor in hash_set: 
				continue # avoid backtracking
			hast_set.add(node)
			q.put(neighbor)
	return 0 # can't find

小节

大体的bfs过程如上所示,一些题目难点在于bfs的细节实现,例如:

  • bfs的终止条件
    一些题目的终止条件可能不是找到某一终点,而是将图中的某种元素清除。如僵尸吃人的问题,需要将人全部变成僵尸。
  • 入队的元素表示
    可以将元组(node, step)入队,出队时进行拆包,从而少写一层循环,进行一定程度上加速。如wordladder的问题,case给定wordList其实很长。

DFS模板

DFS思路是一条路走到底,撞到了墙再回头。
同上述BFS解析,下面将以找数组num的所有子集为例,对dfs的过程进行阐述。

数组的子集

已知数组num[1,2,3](假设数组已经排序),要求他的所有子集,dfs的过程如草图2所示,这和人的思维方式几乎一模一样。
LeetCode | 一文帮你搞定BFS、DFS算法(python版)_第2张图片
从空集开始,然后将1加入,1加入之后,下一次加入2,之后加入3,这时startindex指针移出数组范围,开始回溯。将3移除后,回溯到2,(这里内部循环已经结束,执行到函数底部),2回溯之后,到1,1可以再加入3。这是程序的过程,关键是怎么写代码。

dfs函数定义

因为dfs涉及到程序的递归调用,一般dfs不嵌入到程序内部过程。在主函数中,将dfs作为辅助函数调用,故此将dfs函数命名为dfsHelper。

#python中,一般定义如下:
def dfsHelper(self,
				input parameter..., 
				startIndex, 
				tmp, 
				result):
"""
其中:
input parameter一般指不变的已知量:比如数组num,在矩阵的搜索中指矩阵grid,在图中指图graph和目标节点end_node;
startIndex用来标记当前指针在数组(矩阵或者图中的位置),随着递归而改变;
tmp:用来暂存递归过程中的结构,随着递归而改变;
result:一般为全局变量,用来保存所有的结果。
在程序撰写中,可以定义很多经常使用的量为常量,从而使dfsHelper函数更加短小精悍。
"""

dfs递归的过程,需要背诵

递归的过程就是先在tmp中加入当前元素,然后调用自身(这中间指针后移),最后在tmp中移除当前元素。
若不看dfsHelper的循环调用,可以这样理解;当tmp=[]时,将startIndex后移是依次将1,2,3加入tmp,也就是搜索的第一层。将"1"pop掉即保持了当前层的纯粹,没有这个操作会导致递归层次混乱。

for i in range(startIndex, len(nums)):
    tmp.append(nums[i])
    self.dfsHelper(nums, i + 1, tmp, result)
    tmp.pop()

结果的暂存

由于是求所有的子集,再每次递归时其实tmp保存了不同的子集,故在dfs函数的最开始要考虑将tmp加入到result。由于tmp其实是指向一个地址的指针,在递归中可能随时改变,因此在加入之前先将tmp的内容拷贝一份而不是直接拿出,保证运行结果的正确。
这里c++是否可以直接取值?有时间再研究下。

tmp_copy = tmp.copy()
result.append(tmp_copy)

dfs的出口

dfs的出口是这类题目的难点,因其变化多端。当定义dfsHelper函数中使用startIndex,很明显需要借助此标志进行判定。所以有:

if startIndex == len(nums):
    return

在其他的问题中,比如二维矩阵中,可能是index越界,可能是定义了visited而恰好当前位置访问过,这些针对特定的问题需要特殊对待。

一窥全貌

将此问题的完整解法列在下面,对此问题便更进一步理解了。

class Solution:
    """
    @param nums: A set of numbers
    @return: A list of lists
    """
    def subsets(self, nums):
        # 从空集开始搜索,每次将nums的节点加入空集中
        result = []
        tmp = []
        
        if nums is None:
            return None
        
        nums.sort()
        
        startIndex = 0    
        self.dfsHelper(nums, startIndex, tmp, result)
        
        return result
        
    def dfsHelper(self, nums, startIndex, tmp, result):
        # dfs出口
        tmp_copy = tmp.copy() # 拷贝一份
        result.append(tmp_copy)
        
        if startIndex == len(nums):
            return
        
        for i in range(startIndex, len(nums)):
            tmp.append(nums[i])
            self.dfsHelper(nums, i + 1, tmp, result)
            tmp.pop()
        return

小结

个人觉得,DFS比BFS要难,因其在定义dfsHelper函数时多种多样,可以很灵活地根据自己的需求和习惯定义。但,换了个马甲,其内部的思维并没有变,计算量(所说的复杂度)大致没有变。
所谓的算法,更侧重于“法”,而非“算”,即它更多的是一种方法,而不是一种算术。

更多精彩文章,欢迎关注公众号“Li的白日呓语”。

你可能感兴趣的:(leetcode,指针,队列,python,算法,leetcode)