深度优先搜索(DFS)和广度优先搜索(BFS),用代码讲原理

以图文的形式对深度搜索和广度搜索的理论进行讲解时,可能会对一些概念有些模糊,且不太清楚怎么把该理论用程序的方式进行复现并解决这一搜索问题(这说的就是本人)。所以后面我看完了一份实现这两种搜索方法的代码,在这做一个笔记,希望对大家有所帮助。


深度搜索和广度搜索的区别

两者本质的区别就是一个找下一个节点时是做的堆栈(先进后出),一个是做队列(先进先出)。我们用for循环和一个列表来简单说明理解一下。这里可能说的不好,如果给大家说模糊了可以不用看

li = []
for i in range(5):
    li.append(i)
print(li) # ==>结果[0,1,2,3,4]

 1.1 深度优先搜索的堆栈

上面的for循环程序中,列表里面的0是第一个进去的,4是最后一个进去的,所以我们在选择下一个点时就会选择4。选择过程也是取列表的最后一位,并将其从列表中删除

state = li[-1] # 选择下一步的位置状态(当作是坐标和行动方向)
li = li[:-1] # 选中后的状态会进入程序运算,所以这从列表中删除选择后的点
print(li) # ==> [0,1,2,3]

 1.2 广度优先搜索的队列

我们的队列,简单的说就是排队,谁先进入列表,那么在下一次选择动作节点时就优先被选中。

state = li[0] # 取列表的第一个节点状态(同坐标和行动方向)
li = li[-1] # 选取后进入程序运算,并从列表中删除该状态点
print(li) # ==> [1,2,3,4]

探索集 

在程序搜索过程中保存我们已经探索过的所有节点坐标,在程序中会一直对这个集合进行判断,保证不会在子节点和父节点之间来回探索,创建形式如下代码。

explore = set() # 创建探索集合

深度优先搜索

2.1  数据形式及处理

本文所采用的数据形式为txt文本,文本中内容如下:

 *****

 ** **
 **B**
 *  **
 * ***
A  ***

数据由A、B、*号和空格组成,其中*号表示不可通行的墙,A是我们的起始点,B是我们的目标点,空格表示可通行的点。

用代码实现数据处理并转换为坐标形式,如下所示:

class Maze():

    def __init__(self, filename):

        # Read file and set height and width of maze
        """
        初始化操作,找到文件中的起点a和终点b是否存在
        存在进行行列的遍历,找到A,B在文本中的坐标点
        :param filename:我们传入的txt文本,文本内容如上所示 

        """
        with open(filename) as f:
            contents = f.read()

        # Validate start and goal 判断是否存在起始点和目标点位,不存在退出程序
        if contents.count("A") != 1:
            raise Exception("maze must have exactly one start point")
        if contents.count("B") != 1:
            raise Exception("maze must have exactly one goal")

        # Determine height and width of maze 找到文本的长宽,做循环起到记录坐标点的作用
        contents = contents.splitlines()
        self.height = len(contents)
        self.width = max(len(line) for line in contents)

        # Keep track of walls
        self.walls = []
        # 这里循环找到所有坐标,及A、B、*号和空格所在的坐标点并保存
        for i in range(self.height):
            row = []
            for j in range(self.width):
                try:
                    if contents[i][j] == "A":
                        self.start = (i, j)
                        row.append(False)
                    elif contents[i][j] == "B":
                        self.goal = (i, j)
                        row.append(False)
                    elif contents[i][j] == " ":
                        row.append(False)
                    else:
                        row.append(True)
                except IndexError:
                    row.append(False)
            self.walls.append(row)

        self.solution = None

代码解释(有编程经验的看上面代码即可 ):我们向Maze类里面传入我们的txt文本,打开并读取其中的内容。对内容进行判断(是否存在起始点和目标点)。获取内容的高度和宽度,本文所传入的高宽为 7*5 (最上面那个很多*号的就是)。循环高度和宽度,找到A点和B点的坐标,将空格和A、B点在row列表中保存为False,*号保存为True,最后将每一行的row列表保存到self.wall的列表当中。在后续找点时判断self.wall[i][j]及第 i 行第 j 列的布尔值是否为True,是则代表无法通行。

2.2节点保存、路径搜索及路径回溯

我们可以通过PIL库中的Image, ImageDraw,将以上处理结果转换为图片的形式,红色代表起始点,绿色代表目标点,白色为可以通过的节点。我们在后续过程都以下图为例。

2.2.1 节点保存

我们使用class类的方式保存当前状态坐标、行动的方向和父节点(父节点也是这个类,也存着坐标、行动和父节点),代码如下:

class Node():
    def __init__(self, state, parent, action):
        self.state = state
        self.parent = parent
        self.action = action

 我们每在后面while循环实例化该类时,都会在内存中有记录。可以看python类的基础知识

2.2.2 路径搜索

    def neighbors(self, state):
        """
        在调用这个函数的时候会传入当前状态的坐标点。也就是我们在程序中
        自己定义下一步优先走的方向,在candidates列表里面就是上下左右的
        顺序进行移动 以点(0,0)为例,得到的candidates列表就是
        [
        "up",(-1,0),
        "down",(1,0),
        "left",(0,-1),
        "right",(0,1)    
        ]
        得到行动列表后开始进行循环判断,判断是否超出长和宽,以及self.wall[r][c]是否为True,
        如果都不满足就代表可以行动,添加到result列表
        """
        row, col = state
        candidates = [
            ("up", (row - 1, col)),
            ("down", (row + 1, col)),
            ("left", (row, col - 1)),
            ("right", (row, col + 1))
        ]

        result = []
        # 保存可以走的方向, 判断是否在边界以及是否有墙 self.wall列表中的是bool
        for action, (r, c) in candidates:
            if 0 <= r < self.height and 0 <= c < self.width and not self.walls[r][c]:
                result.append((action, (r, c)))
        return result


    def solve(self):
        """Finds a solution to maze, if one exists."""

        # Keep track of number of states explored
        self.num_explored = 0

        # Initialize frontier to just the starting position
        # 初始化起始点的节点,并将其保存到StackFrontier()类里面的frontier列表当中(保存的是一个对象)
        
        start = Node(state=self.start, parent=None, action=None)
        frontier = StackFrontier()
        frontier.add(start)

        # Initialize an empty explored set 初始化探索集
        self.explored = set()

        # Keep looping until solution found 循环查找路径
        while True:
            # If nothing left in frontier, then no path
            if frontier.empty():
                raise Exception("no solution")

            # Choose a node from the frontier
  
            # 获取并子节点(坐标和行动方向,以及父节点)并从边界列表中删除该节点(具体看StackFrontier类的remove函数),其过程与最开始的堆栈列表过程相同
            node = frontier.remove()
            self.num_explored += 1

            # If node is the goal, then we have a solution
            if node.state == self.goal:
                actions = []
                cells = []
                # while循环对路径进行回溯(过程在文中进行简绍)
                while node.parent is not None:
                    actions.append(node.action)
                    cells.append(node.state)
                    node = node.parent
                print(cells)
                actions.reverse()
                cells.reverse()
                self.solution = (actions, cells)
                return

            # Mark node as explored
            self.explored.add(node.state)

            # Add neighbors to frontier 返回的执行列表是 先上 后下
            for action, state in self.neighbors(node.state):
                if not frontier.contains_state(state) and state not in self.explored:
                    # state 和 action保存的是子节点的行动和状态,而parent保存了父节点的所有状态,比如从起始点到第二个点,parent保存的就是起始状态的所有状态
                    child = Node(state=state, parent=node, action=action)

                    frontier.add(child)
class StackFrontier():
    def  __init__(self):
        self.frontier = []
    def add(self, node):
        self.frontier.append(node)

    def contains_state(self, state):
        return any(node.state == state for node in self.frontier)

    def empty(self):
        return len(self.frontier) == 0

    def  remove(self):
        # 堆栈,先进后出 深度搜索
        if self.empty():
            raise Exception("empty frontier")
        else:
            # 取列表中最后一个对象,并将边界列表的最后一个删除,返回结果为对象(对象中有当前节点、运动方向和他的父节点)
            node = self.frontier[-1]
            self.frontier = self.frontier[:-1]
            return node

上述代码是存在于Maze类当中,函数neighbors是查找当前坐标点上下左右4个方向的坐标,并判断是否可以通过,如果可以就保存到result列表当中,并返回result列表。

 solve函数是判断是否到达目标点的函数, 可以看程序中的注释进行了解。我们运行整体程序后得到一下结果的路径图,我们看图讲解一下过程。

本图为深度优化搜索得到的结果,如果了解了上面对程序的解释,那么我们看后面的内容应该会比较容易。

同理红色代表我们的起始点A,绿色代表我们的目标点B,蓝色是我们深度优化搜索算法得到的路径,白色为为探索的路径。  我们来讲一下程序在这图片中的路径搜索过程。

以起始点为例A(6,0),经过初始化节点的Node类保存起始点的坐标(起始点无父节点和行动方向),将A点传入以下程序中寻找可移动的方向和节点

    def neighbors(self, state):
        """
        在调用这个函数的时候会传入当前状态的坐标点。也就是我们在程序中
        自己定义下一步优先走的方向,在candidates列表里面就是上下左右的
        顺序进行移动 以点(0,0)为例,得到的candidates列表就是
        [
        "up",(-1,0),
        "down",(1,0),
        "left",(0,-1),
        "right",(0,1)    
        ]
        得到行动列表后开始进行循环判断,判断是否超出长和宽,以及self.wall[r][c]是否为True,
        如果都不满足就代表可以行动,添加到result列表
        """
        row, col = state
        candidates = [
            ("up", (row - 1, col)),
            ("down", (row + 1, col)),
            ("left", (row, col - 1)),
            ("right", (row, col + 1))
        ]

        result = []
        # 保存可以走的方向, 判断是否在边界以及是否有墙 self.wall列表中的是bool
        for action, (r, c) in candidates:
            if 0 <= r < self.height and 0 <= c < self.width and not self.walls[r][c]:
                result.append((action, (r, c)))
        return result

       得到列表结果为:  [ ("up",(5,0)),( "down",(7,0)),("left",(6,-1)), ("right",(6,1)) ],

将得到的列表再经过for循环判断是否超出原文本的长宽,以及是否为墙,如果满足其中一项就表示该节点无法通过,比如节点 (“down”,(7,0))超出了我们整体的高度及超出了二维矩阵的索引(高度是从0开始到6结尾,整体高为7)这个节点就不会被保存进result列表

因此从A点开始可以通过的节点为 result = [("up",(5,0)),("right",(6,1))]。得到的结果返回到for循环中,如下程序:

             for action, state in self.neighbors(node.state):
                if not frontier.contains_state(state) and state not in self.explored:
                    # state 和 action保存的是子节点的行动和状态,而parent保存了父节点的所有状态,比如从起始点到第二个点,parent保存的就是起始状态的所有状态
                    child = Node(state=state, parent=node, action=action)

                    frontier.add(child)

在本段程序中会将两个子节点保存到Node类中,并将child这个对象保存到边界列表中(边界列表在StackFrontier类中)。深度优先搜索会在下一次的while循环中去找StackFrontier类中边界列表中最后一个对象,并赋予变量node,然后重置边界列表(就是删除取出来的最后一位对象)。然后开始重复上方可行动节点的搜索,找到便保存到边界列表,没有就会在边界列表中已有的对象去查找可移动节点。

 2.2.3 回溯

 我们已经知道每找到一个节点在重新中都会对其做一次对象保存(保存在Node类里),其中保存的内容为该节点的坐标和父节点运动的该节点的方向,以及父节点的对象。我们看下方程序,就是在while循环判断是否存在父节点,存在的情况下找到当前节点的坐标,并将该节点的父节点重新赋值给到node变量,直到回溯到我们的起始点节点。

回溯程序

       # 判罚是否到达我们的目标点  
       if node.state == self.goal:
                actions = []
                cells = []
                # 开是while循环判断回溯,判断我们类里面是否有父节点(在起始点的时候我们没有父节点的)
                while node.parent is not None:
                    # 将回溯节点进行保存
                    actions.append(node.action)
                    cells.append(node.state)
                    # 重新对变量node进行赋值,得到当前节点父节点
                    node = node.parent

起始点的节点

# 在循环查找路径前我们就已经将我们的起始点进行保存(parent代表我们的父节点,在这里是None,代表没有,起到while回溯退出的作用)
start = Node(state=self.start, parent=None, action=None)

 看一看回溯过程

"""
    以起始点和他的一个子节点为例
"""

start = Node(state=(6,0),parent=None,action=None)

# parent保存的是节点对象
child = Node(state=(6,1),parent=start,action='right')

node = child 

action = []
cells = []

while node.parent is not None:
    action.append(node.action)
    cells.append(node.state)
    node = node.parent  # node.parent = Node(state=(6,0),parent=None,action=None)

 广度优先搜索

我们的广度优先搜索的处理过程和深度优先搜索相同,唯一不同的地方是在下方StackFrontier类的remove函数当中,需要更改为取列表的第一位,及索引0

class StackFrontier():
    def  __init__(self):
        self.frontier = []
    def add(self, node):
        self.frontier.append(node)

    def contains_state(self, state):
        return any(node.state == state for node in self.frontier)

    def empty(self):
        return len(self.frontier) == 0

    def  remove(self):
        # 堆栈,先进后出 深度搜索
        if self.empty():
            raise Exception("empty frontier")
        else:
            # 取列表中最后一个对象,并将边界列表的最后一个删除,返回结果为对象(对象中有当前节点、运动方向和他的父节点)
            node = self.frontier[-1]
            self.frontier = self.frontier[:-1]
            return node

函数更改,将上方remove函数改为以下代码:

    def remove(self):
        if self.empty():
            raise Exception("empty frontier")
        else:
            # 这里取了边界列表的第一位
            node = self.frontier[0]
            print(node)
            # 这里删除了边界列表的第一位
            self.frontier = self.frontier[1:]
            return node

 在寻找节点的时候和深度搜索相同,只是按照所有节点进入边界列表的顺序来进行下一步的行动。

下图是广度优先搜索得出的结果图:

 粉红色为探索过的路径

 总结

写完感觉整篇文章的逻辑和内容有点混乱,本人对两者算法的理解也有限,可能存在理解错误的地方,如果导致您感到困惑地方还望理解。

完整代码如下

##############################
#Maze.py
#Introdution:
#IDE:
#From
#By:qin,2023....
##############################

import sys
import matplotlib.pyplot as plt


#类:
class Node():
    def __init__(self, state, parent, action):
        self.state = state
        self.parent = parent
        self.action = action


class StackFrontier():
    def  __init__(self):
        self.frontier = []
    def add(self, node):
        self.frontier.append(node)

    def contains_state(self, state):
        return any(node.state == state for node in self.frontier)

    def empty(self):
        return len(self.frontier) == 0

    # def  remove(self):
    #     # 堆栈,先进后出 深度搜索
    #     if self.empty():
    #         raise Exception("empty frontier")
    #     else:
    #         node = self.frontier[-1]
    #         self.frontier = self.frontier[:-1]
    #         return node
    def remove(self):
        if self.empty():
            raise Exception("empty frontier")
        else:
            node = self.frontier[0]
            print(node)
            self.frontier = self.frontier[1:]
            return node


class QueueFrontier(StackFrontier):
    # 队列 先进先出,进行广度搜索
    def remove(self):
        if self.empty():
            raise Exception("empty frontier")
        else:
            node = self.frontier[0]
            print(node)
            self.frontier = self.frontier[1:]
            return node

class Maze():

    def __init__(self, filename):

        # Read file and set height and width of maze
        """
        初始化操作,找到文件中的起点a和终点b是否存在
        存在进行行列的遍历,找到A,B在文本中的坐标点
        :param filename:

        """
        with open(filename) as f:
            contents = f.read()

        # Validate start and goal
        if contents.count("A") != 1:
            raise Exception("maze must have exactly one start point")
        if contents.count("B") != 1:
            raise Exception("maze must have exactly one goal")

        # Determine height and width of maze
        contents = contents.splitlines()
        self.height = len(contents)
        self.width = max(len(line) for line in contents)

        # Keep track of walls
        self.walls = []
        for i in range(self.height):
            row = []
            for j in range(self.width):
                try:
                    if contents[i][j] == "A":
                        self.start = (i, j)
                        row.append(False)
                    elif contents[i][j] == "B":
                        self.goal = (i, j)
                        row.append(False)
                    elif contents[i][j] == " ":
                        row.append(False)
                    else:
                        row.append(True)
                except IndexError:
                    row.append(False)
            self.walls.append(row)

        self.solution = None


    def print(self):
        solution = self.solution[1] if self.solution is not None else None
        print()
        for i, row in enumerate(self.walls):
            for j, col in enumerate(row):
                if col:
                    print("█", end="")
                elif (i, j) == self.start:
                    print("A", end="")
                elif (i, j) == self.goal:
                    print("B", end="")
                elif solution is not None and (i, j) in solution:
                    print("*", end="")
                else:
                    print(" ", end="")
            print()
        print()


    def neighbors(self, state):
        row, col = state
        candidates = [
            ("up", (row - 1, col)),
            ("down", (row + 1, col)),
            ("left", (row, col - 1)),
            ("right", (row, col + 1))
        ]

        result = []
        # 保存可以走的方向, 判断是否在边界以及是否有墙 self.wall列表中的是bool
        for action, (r, c) in candidates:
            if 0 <= r < self.height and 0 <= c < self.width and not self.walls[r][c]:
                result.append((action, (r, c)))
        return result


    def solve(self):
        """Finds a solution to maze, if one exists."""

        # Keep track of number of states explored
        self.num_explored = 0

        # Initialize frontier to just the starting position
        start = Node(state=self.start, parent=None, action=None)
        print("起始点的坐标",start.state)
        frontier = StackFrontier()
        frontier.add(start)

        # Initialize an empty explored set
        self.explored = set()

        # Keep looping until solution found
        while True:
            # If nothing left in frontier, then no path
            if frontier.empty():
                raise Exception("no solution")

            # Choose a node from the frontier
            r = QueueFrontier()
            node = frontier.remove()
            self.num_explored += 1

            # If node is the goal, then we have a solution
            if node.state == self.goal:
                actions = []
                cells = []
                while node.parent is not None:
                    actions.append(node.action)
                    cells.append(node.state)
                    node = node.parent
                print(cells)
                actions.reverse()
                cells.reverse()
                self.solution = (actions, cells)
                return

            # Mark node as explored
            self.explored.add(node.state)

            # Add neighbors to frontier 返回的执行列表是 先上 后下
            for action, state in self.neighbors(node.state):
                if not frontier.contains_state(state) and state not in self.explored:
                    # state 和 action保存的是子节点的行动和状态,而parent保存了父节点的所有状态,比如从起始点到第二个点,parent保存的就是起始状态的所有状态
                    child = Node(state=state, parent=node, action=action)

                    frontier.add(child)


    def output_image(self, filename, show_solution=True, show_explored=False):
        from PIL import Image, ImageDraw
        cell_size = 50
        cell_border = 2
        # 创建图像画布,将start 、goal、wall和路径用不同的像素表示
        # Create a blank canvas
        img = Image.new(
            "RGBA",
            (self.width * cell_size, self.height * cell_size),
            "black"
        )
        draw = ImageDraw.Draw(img)

        solution = self.solution[1] if self.solution is not None else None
        for i, row in enumerate(self.walls):
            for j, col in enumerate(row):
                # plt.clf()

                # Walls
                if col:
                    fill = (40, 40, 40)

                # Start
                elif (i, j) == self.start:
                    fill = (255, 0, 0)
                    # plt.imshow(img)
                    # plt.pause(0.4)

                # Goal
                elif (i, j) == self.goal:
                    fill = (0, 171, 28)

                # Solution
                elif solution is not None and show_solution and (i, j) in solution:
                    fill = (120, 200, 252)
                    # plt.imshow(img)
                    # plt.pause(0.4)

                # Explored
                elif solution is not None and show_explored and (i, j) in self.explored:
                    fill = (255, 100, 120)

                # Empty cell
                else:
                    fill = (237, 255, 252)

                # Draw cell
                draw.rectangle(
                    ([(j * cell_size + cell_border, i * cell_size + cell_border),
                      ((j + 1) * cell_size - cell_border, (i + 1) * cell_size - cell_border)]),
                    fill=fill
                )
                # plt.imshow(img)
                # plt.pause(0.4)
        # plt.ioff()
        # plt.show()
        img.save(filename)

# for i in range(4):
#     sys.argv.append(f"./maze{i+1}.txt")
# 
# 
# if len(sys.argv) < 2:
#     sys.exit("Usage: python maze.py maze.txt1")
# 
# 
# print((sys.argv))


# path = sys.argv[j+1]
m = Maze('./maze1.txt')
print("Maze:")
m.print()
print("Solving...")
m.solve()
print("States Explored:", m.num_explored)
print("Solution:")
m.print()
m.output_image(f"maze.png", show_explored=True)

上述代码非本人编写,仅用于讲述与教学

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

seeyou1228

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值