Python wide search (BFS) and deep search (DFS)

Posted by corrupshun on Thu, 17 Feb 2022 12:45:46 +0100

BFS

What is BFS

BFS is called breadth first search, also called breadth first search. Its alias is BFS, which belongs to a blind search method.
Dijkstra single source shortest path algorithm and Prim minimum spanning tree algorithm adopt the idea similar to width first search.
The purpose is to systematically expand and check all nodes in the diagram to find the results.
In other words, it does not consider the possible location of the result and searches the whole graph thoroughly until it finds the result.
Its idea is to start from a vertex V0 and traverse the wider area around it radially, so it is named.
Breadth first search is realized by using queue, which has the characteristics of first in first out FIFO(First Input First Output).

Working process and principle of BPS

BFS operation steps are as follows:
1. Put the starting point into the queue;
2. Repeat the following 2 steps until the queue is empty:
1) Take out the queue header from the queue;
2) Find the points adjacent to this point that have not been traversed, mark them, and then put them all into the queue.

(1) Put the starting node 1 into the queue and mark it as traversed:

(2) Take node 1 of the queue header from the queue, find nodes 2 and 3 adjacent to node 1, mark them as traversed, and then put them into the queue.

(3) Take node 2 of the queue header from the queue and find nodes 1, 4 and 5 adjacent to node 2. Since node 1 has been traversed, it is excluded; It is traversed and marked as 4,5.

(4) Take the node 3 of the queue header from the queue and find the nodes 1, 6 and 7 adjacent to node 3. Since node 1 has been traversed, it is excluded; Mark 6 and 7 as traversed and put them into the queue.

(5) Take out the node 4 of the queue header from the queue, find out the nodes 2, 8 and 2 adjacent to the node 4, which belong to the traversed points and are excluded; Therefore, mark node 8 as traversed and put it into the queue.

(6) Take out the node 5 of the queue header from the queue, and find out the nodes 2, 8, 2 and 8 adjacent to the node 5, which belong to the traversed points, and do not do the next operation.

(7) Take out the node 6 of the queue header from the queue, find out that the nodes 3, 8, 9, 3 and 8 adjacent to the node 6 belong to the traversed points, and exclude them; Therefore, mark node 9 as traversed and put it into the queue.

(8) Take out the node 7 of the queue header from the queue, and find out that the nodes 3, 9, 3 and 9 adjacent to the node 7 belong to the traversed points, so do not do the next operation.

(9) Take out the node 8 of the queue header from the queue, and find out that the nodes 4, 5, 6, 4, 5 and 6 adjacent to the node 8 belong to the traversed points, so do not do the next operation.

(10) Take out the node 9 of the queue header from the queue, and find out the nodes 6, 7 and 6, 7 adjacent to the node 9, which belong to the traversed points and do not do the next operation.

(11) If the queue is empty, BFS traversal ends.

BFS application scenario

  • Two scenarios of BFS: sequence traversal and shortest path
  • Find the shortest path from a certain point, but the path is not unique. The shortest path is the first one to meet the conditions
  • Extensive search
  • Words like "shortest" and "least" can be given priority

Implementing BFS in python (tree and graph)

Trees are special graphs

BFS diagram of python implementation

import os
import sys

'''
python realization BFS Graph of
2022-01-15
'''

# Create a dictionary for storing graphs. A dictionary is equivalent to a mapping relationship and is read through key value pairs. It is applicable to only a small number of points in the graph. If there is too much data, it is more appropriate to use python class
graph = {
    "A": ["B", "C"],
    "B": ["A", "C", "D"],
    "C": ["A", "B", "D", "E"],
    "D": ["B", "C", "E", "F"],
    "E": ["C", "D"],
    "F": ["D"]
}

# Start BFS traversal
# Graph is the graph data and s is the starting point of the graph
def BFS(graph, s):
    # Create an array as a queue to store points that have not been accessed
    queue = []
    # Put in starting point
    queue.append(s)
    # Create a collection to hold the points that have been read in, such as the starting point s
    seen = set()
    seen.add(s)
    # Loop read queue
    while (len(queue) > 0):
        # Via queue Pop (0) reads the queue head, that is, each point
        vertex = queue.pop(0)
        # Read the points adjacent to each point
        nodes = graph[vertex]
        # Weight judgment: cycle to judge whether adjacent points have been read
        for w in nodes:
            if w not in seen:
                queue.append(w)
                seen.add(w)
        # Output traversal node
        print(vertex, end= ' ')

BFS(graph, "A")
'''
# Operation results
A B C D E F
'''

Shortest path of python implementation diagram


Improved from the above code

import os
import sys

'''
python realization BFS Graph of
2022-01-15
'''

# Create a dictionary for storing graphs. A dictionary is equivalent to a mapping relationship and is read through key value pairs. It is applicable to only a small number of points in the graph. If there is too much data, it is more appropriate to use python class
graph = {
    "A": ["B", "C"],
    "B": ["A", "C", "D"],
    "C": ["A", "B", "D", "E"],
    "D": ["B", "C", "E", "F"],
    "E": ["C", "D"],
    "F": ["D"]
}

# Start BFS traversal
# Graph is the graph data and s is the starting point of the graph
def BFS(graph, s):
    # Create an array as a queue to store points that have not been accessed
    queue = []
    # Put in starting point
    queue.append(s)
    # Create a collection to hold the points that have been read in, such as the starting point s
    seen = set()
    seen.add(s)
    # Path restore, which corresponds the accessed point to its previous point to form a key value pair, and uses it to complete the output of the shortest path
    parent = {s: None}
    # Loop read queue
    while (len(queue) > 0):
        # Via queue Pop (0) reads the queue head, that is, each point
        vertex = queue.pop(0)
        # Read the points adjacent to each point
        nodes = graph[vertex]
        # Weight judgment: cycle to judge whether adjacent points have been read
        for w in nodes:
            if w not in seen:
                queue.append(w)
                seen.add(w)
                parent[w] = vertex
        # Output traversal node
        # print(vertex, end= ' ')
    return parent


parent = BFS(graph, "A")
# Output the shortest path you want
# Set v as the end point
v = 'F'
# Loop find v
while v != None:
    print(v, end=' ')
    v = parent[v]
'''
# Operation results
F D B A 
'''

Implementing BFS tree in python

# Define the nodes of a tree
class TreeNode:
    def __init__(self, x):
        self.val = x  # value
        self.left = None  # Left node
        self.right = None  # Right node


def level_order_tree(root):
    if not root:  # A tree has only the root node and returns a null value
        return root
    queue = []
    result = []
    queue.append(root)  # Queue root node
    result.append(root.val)  # Add root node value
    while len(queue) > 0:
        node = queue.pop(0)  # Take the head of the line
        result.append(node.val)
        # Judge left and right subtrees
        if node.left:  # Add children from the left subtree to the queue
            queue.append(node.left)
        if node.right:  # Add children from the right subtree to the queue
            queue.append(node.right)
    return result


if __name__ == "__main__":
    tree = TreeNode(4)
    tree.left = TreeNode(9)
    tree.right = TreeNode(0)
    tree.left.left = TreeNode(5)
    tree.left.right = TreeNode(1)

    print(level_order_tree(tree))
    # [4, 9, 0, 5, 1, 3, 2, 10, 7]

DFS

What is DFS

  1. Depth first search (DFS) is an algorithm used to traverse or search trees or graphs. In brief, the process is to go deep into each possible branch path to the point where it can no longer go deep, and each node can only access it once.
  2. When the edge of node v has been explored or the node does not meet the conditions when searching, the search will be traced back to the starting node of the edge where node v is found. The whole process repeats until all nodes are accessed.
  3. For the same tree or graph, the DFS order is not necessarily unique.
  4. Depth first search is realized by stack, which has the characteristics of first in and last out.

DFS working process and principle

DFS working process and principle

Please don't forget that DFS order is not necessarily unique.

  1. Take A as the starting node and access A. the adjacent points connected with A are B and C.
  2. At this point, you can access B and C. which one you access depends on the order in which you press the stack.
  3. Visit B, and the adjacent points connected to B are C and D.
  4. Access D, and the adjacent points connected to D are E and F.
  5. Access F, f has no adjacency point, and the nearest adjacency point D when the original path returns.
  6. The adjacency point e connected with D has not been accessed. Access e, and the adjacency points connected with E are C and D.
  7. Access C. the adjacency points of C include A, B, D and E. all have been visited. The original path returns to D. at this time, the adjacency points of d have been visited. The original path returns to B and then to A.

DFS application scenario

  • Seeking subsequence and subset
  • Find all possible solutions and assemble the results layer by layer (not much different from the first one)

Implementing DFS with python

DFS diagram implemented in python

import os
import sys

'''
python realization DFS Graph of
2022-01-15
'''

# Create a dictionary for storing graphs. A dictionary is equivalent to a mapping relationship and is read through key value pairs. It is applicable to only a small number of points in the graph. If there is too much data, it is more appropriate to use python class
graph = {
    "A": ["B", "C"],
    "B": ["A", "C", "D"],
    "C": ["A", "B", "D", "E"],
    "D": ["B", "C", "E", "F"],
    "E": ["C", "D"],
    "F": ["D"]
}

# Start DFS traversal
# Graph is the graph data and s is the starting point of the graph
def BFS(graph, s):
    # Create an array as a stack to store points that have not been accessed
    stack = []
    # Put in starting point
    stack.append(s)
    # Create a collection to hold the points that have been read in, such as the starting point s
    seen = set()
    seen.add(s)
    # Cyclic read stack
    while (len(stack) > 0):
        # Via queue Pop() reads the last on the stack
        vertex = stack.pop()
        # Read the points adjacent to each point
        nodes = graph[vertex]
        # Weight judgment: cycle to judge whether adjacent points have been read
        for w in nodes:
            if w not in seen:
                stack.append(w)
                seen.add(w)
        # Output traversal node
        print(vertex, end= ' ')

BFS(graph, "A")
'''
# Operation results
A C E D F B 
'''

Implementing DFS tree in python

# Create tree node class val: value, left: left subtree, right: right subtree
class TreeNode:
    def __init__(self, value):
        self.val = value
        self.left = None
        self.right = None


# Deep search traversal binary tree
def DFS(root):
    # Create a storage result array
    res = []
    # Create a stack. The stack has the characteristics of last in first out. First put it into the root node
    stack = [root]
    # Loop stack
    while stack:
        # Take out the last element of the stack
        currentNode = stack.pop()
        # Get the value of the element
        res.append(currentNode.val)
        # Include right subtree
        if currentNode.right:
            # Add right subtree to stack
            stack.append(currentNode.right)
        # Whether to include left subtree
        if currentNode.left:
            # Add left subtree to stack
            stack.append(currentNode.left)

    # Return results
    return res


if __name__ == "__main__":
    tree = TreeNode(4)  # Add root node
    tree.left = TreeNode(9)  # Add root left subtree
    tree.right = TreeNode(0)  # Add root right subtree, and so on
    tree.left.left = TreeNode(5)
    tree.left.right = TreeNode(1)
    tree.right.left = TreeNode(3)
    tree.right.right = TreeNode(2)
    tree.left.left.left = TreeNode(10)
    tree.right.left.right = TreeNode(7)

    print(DFS(tree))
    # [4, 9, 5, 10, 1, 0, 3, 7, 2]
'''
Operation results:
[4, 9, 5, 10, 1, 0, 3, 7, 2]
'''

When to use deep search and wide search

  1. BFS is used to search the solution of the shortest path, which is more appropriate, such as the solution with the least steps and the solution with the least exchange times. Because the solution encountered in the process of BFS search must be the closest to the root, the solution encountered must be the optimal solution. At this time, the search algorithm can be terminated. It is not appropriate to use DFS at this time, because the solution searched by DFS is not necessarily the closest to the root. Only after the global search is completed can we find the nearest solution to the root from all solutions. (of course, the deficiency of DFS can be made up by using iterative deepening search ID-DFS).
  2. In terms of space, DFS has advantages. DFS does not need to save the state in the search process, while BFS needs to save the searched state in the search process, and generally a queue is needed to record.
  3. DFS is suitable for searching all solutions, because to search all solutions, it is useless to encounter the solution closest to the root in the BFS search process. You must traverse the entire search tree, and DFS search will also search all solutions. However, compared with DFS, DFS does not need to record too much information, so DFS is obviously more suitable for searching all solutions.

python implements the traversal of the tree before, during and after the sequence

Preorder traversal

# Define the nodes of a tree
class TreeNode:
    def __init__(self, x):
        self.val = x  # value
        self.left = None  # Left node
        self.right = None  # Right node


# Preorder traversal. lis is the passed in list type parameter. In order to get a traversal result of list recursively
def preorder(root, result):
    result.append(root.val)  # Add root node value
    if root.left:
        preorder(root.left, result)  # Recursive search for left subtree
    if root.right:
        preorder(root.right, result)  # Recursive search for right subtree
    return result


if __name__ == "__main__":
    tree = TreeNode(4)  # Add root node
    tree.left = TreeNode(9)  # Add root left subtree
    tree.right = TreeNode(0)  # Add root right subtree, and so on
    tree.left.left = TreeNode(5)
    tree.left.right = TreeNode(1)
    tree.right.left = TreeNode(3)
    tree.right.right = TreeNode(2)
    tree.left.left.left = TreeNode(10)
    tree.right.left.right = TreeNode(7)

    print(preorder(tree, []))
    # [4, 9, 5, 10, 1, 0, 3, 7, 2]

Middle order traversal

# Define the nodes of a tree
class TreeNode:
    def __init__(self, x):
        self.val = x  # value
        self.left = None  # Left node
        self.right = None  # Right node


# Medium order traversal. lis is the passed in list parameter. In order to get a traversal result of list recursively
def infix_order(root, result):
    if root.left:
        infix_order(root.left, result)
    result.append(root.val)
    if root.right:
        infix_order(root.right, result)
    return result


if __name__ == "__main__":
    tree = TreeNode(4)  # Add root node
    tree.left = TreeNode(9)  # Add root left subtree
    tree.right = TreeNode(0)  # Add root right subtree, and so on
    tree.left.left = TreeNode(5)
    tree.left.right = TreeNode(1)
    tree.right.left = TreeNode(3)
    tree.right.right = TreeNode(2)
    tree.left.left.left = TreeNode(10)
    tree.right.left.right = TreeNode(7)

    print(infix_order(tree, []))
    # [10, 5, 9, 1, 4, 3, 7, 0, 2]

Postorder traversal

# Define the nodes of a tree
class TreeNode:
    def __init__(self, x):
        self.val = x  # value
        self.left = None  # Left node
        self.right = None  # Right node


# Post order traversal. Result is the passed in list type parameter. In order to get a traversal list result recursively
def epilogue(root, result):
    if root.left:
        epilogue(root.left, result)
    if root.right:
        epilogue(root.right, result)
    result.append(root.val)
    return result


if __name__ == "__main__":
    tree = TreeNode(4)  # Add root node
    tree.left = TreeNode(9)  # Add root left subtree
    tree.right = TreeNode(0)  # Add root right subtree, and so on
    tree.left.left = TreeNode(5)
    tree.left.right = TreeNode(1)
    tree.right.left = TreeNode(3)
    tree.right.right = TreeNode(2)
    tree.left.left.left = TreeNode(10)
    tree.right.left.right = TreeNode(7)

    print(epilogue(tree, []))
    # [10, 5, 1, 9, 7, 3, 2, 0, 4]

reference material

Topics: Python