https://labuladong.gitee.io/algo/4/29/108/
After reading this article, you can not only learn the algorithm routine, but also win the following topics on LeetCode:
111. Minimum depth of binary tree (simple)
752. Open the turntable lock (medium)
-—–
Many people in the background asked about the framework of BFS and DFS. Let's talk about it today.
First of all, you have to say that I haven't written a BFS framework. That's right. You'll be finished by writing a framework today. But if you say you haven't written a DFS framework, you're really wrong. In fact, DFS algorithm is a backtracking algorithm, as we mentioned earlier Detailed explanation of backtracking algorithm framework routine I've written it, and it's not generally good. It's recommended to review it well. Hey, hey~
The core idea of BFS should not be difficult to understand. It is to abstract some problems into a graph, start from one point and spread around. Generally speaking, we use the data structure of "queue" to write BFS algorithm, adding all nodes around one node to the queue at a time.
The main difference between BFS and DFS is that the path found by BFS must be the shortest, but the cost is that the spatial complexity may be much larger than DFS. As for why, it is easy to see when we introduce the framework later.
This paper will write two typical topics of BFS from simple to deep, namely "minimum height of binary tree" and "minimum steps to open password lock". I will teach you how to write BFS algorithm by hand.
1, Algorithm framework
To talk about the framework, let's first give an example of the common scenes of BFS. Well, the essence of the problem is to let you find the starting point in a "picture" start To the end target This example sounds very boring, but BFS algorithm problems are actually doing this. Understand the essence of boredom, and then appreciate the packaging of various problems.
This broad description can have various variants, such as walking in a maze. Some grids are fences. What is the shortest distance from the beginning to the end? What if the labyrinth belt "portal" can be transported instantly?
For another example, two words require you to change one of them into another through some replacement. You can only replace one character at a time. How many times should you replace it at least?
Another example is watching the game repeatedly. The condition for the elimination of two squares is not only the same pattern, but also to ensure that the shortest connection between two squares cannot be more than two inflection points. You play repeatedly and click on two coordinates. How does the game judge how many inflection points there are in the shortest connection between them?
Another example is
These questions are nothing fancy. They are essentially a "picture" that allows you to ask the shortest path from a starting point to the end. This is the essence of BFS. If the framework is clear, just write it directly.
Just remember the following framework:
// Calculate the nearest distance from start to target int BFS(Node start, Node target) { Queue<Node> q; // Core data structure Set<Node> visited; // Avoid going back q.offer(start); // Add starting point to queue visited.add(start); int step = 0; // Record the number of diffusion steps while (q not empty) { int sz = q.size(); /* Spread all nodes in the current queue around */ for (int i = 0; i < sz; i++) { Node cur = q.poll(); /* Key points: judge whether to reach the end point here */ if (cur is target) return step; /* Add the adjacent nodes of cur to the queue */ for (Node x : cur.adj()) if (x not in visited) { q.offer(x); visited.add(x); } } /* Key points: here are the update steps */ step++; } }
queue q Not to mention, the core data structure of BFS; cur.adj() General reference cur Adjacent nodes, such as cur in a two-dimensional array The positions of upper, lower, left and right sides are adjacent nodes; visited The main function of is to prevent turning back, which is necessary most of the time. However, like the general binary tree structure, there is no pointer from the child node to the parent node, so it is not necessary if you don't turn back visited.
2, Minimum height of binary tree
Let's start with a simple question. Let's practice the BFS framework and judge the minimum height of a binary tree. This is also question 111 of LeetCode. Let's take a look at the topic:
How to put it into the framework of BFS? First, clarify the starting point start And end point target What is it and how to judge the end?
Obviously, the starting point is root The end point of the root node is the "leaf node" closest to the root node. The leaf node is both child nodes null Selected nodes:
if (cur.left == null && cur.right == null) // Reach leaf node
Then, we can write the solution according to the above framework with a little modification:
int minDepth(TreeNode root) { if (root == null) return 0; Queue<TreeNode> q = new LinkedList<>(); q.offer(root); // root itself is a layer, and depth is initialized to 1 int depth = 1; while (!q.isEmpty()) { int sz = q.size(); /* Spread all nodes in the current queue around */ for (int i = 0; i < sz; i++) { TreeNode cur = q.poll(); /* Judge whether to reach the end point */ if (cur.left == null && cur.right == null) return depth; /* Add the adjacent nodes of cur to the queue */ if (cur.left != null) q.offer(cur.left); if (cur.right != null) q.offer(cur.right); } /* Increase the number of steps here */ depth++; } return depth; }
Binary tree is a very simple data structure. I think you can understand the above code. In fact, other complex problems are deformations of this framework. Before discussing complex problems, let's answer two questions:
1. Why can BFS find the shortest distance, but can't DFS?
First, look at the logic of BFS, depth Each time the queue is added, all nodes in the queue take a step forward, which ensures that the number of steps taken is the least when reaching the destination for the first time.
DFS can't find the shortest path? In fact, it is OK, but the time complexity is much higher. You think, DFS actually relies on recursive stack records. If you want to find the shortest path, you must explore all the branches in the binary tree to compare the length of the shortest path, right? BFS can "go hand in hand" step by step with the help of queue, which can find the shortest distance without traversing the whole tree.
In terms of image, DFS is a line and BFS is a surface; DFS is fighting alone and BFS is collective action. This should be easier to understand.
2. Since BFS is so good, why does DFS still exist?
BFS can find the shortest distance, but the spatial complexity is high, while DFS has low spatial complexity.
Let's take the example of dealing with the binary tree problem just now. Suppose the binary tree given to you is a full binary tree with the number of nodes N. For DFS algorithm, the spatial complexity is nothing more than recursive stack. In the worst case, it is at most the height of the tree, that is O(logN).
But if you think about the BFS algorithm, the nodes at the first level of the binary tree are stored in the queue every time. In this case, the spatial complexity should be the number of nodes at the bottom of the tree in the worst case, that is N/2, expressed by Big O, that is O(N).
From this point of view, BFS still has a price. Generally speaking, BFS is used when looking for the shortest path, and DFS is used more in other times (mainly recursive code is easy to write).
Well, now that you know enough about BFS, let's take a more difficult topic and deepen your understanding of the framework.
3, Minimum number of times to unlock the password lock
This LeetCode question is question 752, which is more interesting:
What is described in the title is the common password lock in our life. If there are no constraints, it is easy to calculate the minimum number of dials, just as we usually open the password lock and go straight to the password dialing.
But the difficulty now is that it can't appear Deands, how should we calculate the minimum number of rotations?
The first step, we ignore all the restrictions, regardless of deadends and target If you were asked to design an algorithm and enumerate all possible password combinations, what would you do?
Exhaustive, a little simpler. If you just turn the lock, how many possibilities are there? There are four positions in total. Each position can turn up or down, that is, there are eight possibilities, right.
For example, from "0000" Start, turn once, you can cite "1000", "9000", "0100", "0900"... There are 8 passwords in total. Then, based on these 8 kinds of passwords, turn each password again and enumerate all the possibilities
If you think about it carefully, it can be abstracted into a picture. Each node has 8 adjacent nodes, which allows you to seek the shortest distance. This is not a typical BFS. The framework can come in handy. First write a "simple" BFS framework code, and then say something else:
// Move s[j] up once String plusOne(String s, int j) { char[] ch = s.toCharArray(); if (ch[j] == '9') ch[j] = '0'; else ch[j] += 1; return new String(ch); } // Move s[i] down once String minusOne(String s, int j) { char[] ch = s.toCharArray(); if (ch[j] == '0') ch[j] = '9'; else ch[j] -= 1; return new String(ch); } // BFS framework, print out all possible passwords void BFS(String target) { Queue<String> q = new LinkedList<>(); q.offer("0000"); while (!q.isEmpty()) { int sz = q.size(); /* Spread all nodes in the current queue around */ for (int i = 0; i < sz; i++) { String cur = q.poll(); /* Judge whether to reach the end point */ System.out.println(cur); /* Queue the adjacent nodes of a node */ for (int j = 0; j < 4; j++) { String up = plusOne(cur, j); String down = minusOne(cur, j); q.offer(up); q.offer(down); } } /* Add steps here */ } return; }
PS: of course, there are many problems in this code, but we can't solve the algorithm problem overnight, but from simple to perfect. Don't be perfectionist. Let's take our time, okay.
This BFS code can enumerate all possible password combinations, but obviously it can not complete the problem. The following problems need to be solved:
1. Will go back. For example, we're from "0000" Dial to "1000", but wait to take it out of the queue "1000" When, one will be dialed "0000", in this case, there will be a cycle of life and death.
2. There are no termination conditions. According to the requirements of the topic, we found target It should end and return the number of dials.
3. No, right deadends According to the principle, these "death passwords" cannot appear, that is, you need to skip these passwords when you encounter them.
If you can understand the above code, I really applaud you. You can fix these problems by slightly modifying the corresponding position according to the BFS framework:
int openLock(String[] deadends, String target) { // Record the death password that needs to be skipped Set<String> deads = new HashSet<>(); for (String s : deadends) deads.add(s); // Record the passwords that have been exhausted to prevent turning back Set<String> visited = new HashSet<>(); Queue<String> q = new LinkedList<>(); // Start breadth first search from the starting point int step = 0; q.offer("0000"); visited.add("0000"); while (!q.isEmpty()) { int sz = q.size(); /* Spread all nodes in the current queue around */ for (int i = 0; i < sz; i++) { String cur = q.poll(); /* Judge whether to reach the end point */ if (deads.contains(cur)) continue; if (cur.equals(target)) return step; /* Add the non traversal adjacent nodes of a node to the queue */ for (int j = 0; j < 4; j++) { String up = plusOne(cur, j); if (!visited.contains(up)) { q.offer(up); visited.add(up); } String down = minusOne(cur, j); if (!visited.contains(down)) { q.offer(down); visited.add(down); } } } /* Add steps here */ step++; } // If you can't find the target password after exhausting, you can't find it return -1; }
So far, we have solved this problem. There is a small Optimization: you don't need it dead This hash set can directly initialize these elements to visited In the collection, the effect is the same and may be more elegant.
4, Bidirectional BFS optimization
You think the BFS algorithm is over here? On the contrary. BFS algorithm also has a slightly higher optimization idea: bidirectional BFS, which can further improve the efficiency of the algorithm.
Due to space constraints, here are the differences: the traditional BFS framework starts from the starting point and spreads around, and stops when it meets the end point; The two-way BFS starts from the starting point and the end point at the same time, and stops when there is intersection on both sides.
Why can this improve efficiency? In fact, if the algorithm complexity is analyzed from the Big O representation, the worst complexity of both of them is O(N), but in fact, two-way BFS is faster. I'll draw two pictures for you to see:
In the tree structure shown in the figure, if the end point is at the bottom, the nodes of the whole tree will be searched according to the strategy of the traditional BFS algorithm, and finally found target; In fact, two-way BFS only traverses half a tree, and there is an intersection, that is, the shortest distance is found. From this example, we can intuitively feel that bidirectional BFS is more efficient than traditional BFS.
However, two-way BFS also has limitations, because you must know where the end point is. For example, we just discussed the minimum height of binary tree. You don't know where the end point is at the beginning, so you can't use two-way BFS; However, for the second problem of password lock, two-way BFS algorithm can be used to improve efficiency. The code can be modified slightly:
int openLock(String[] deadends, String target) { Set<String> deads = new HashSet<>(); for (String s : deadends) deads.add(s); // Using a collection without a queue can quickly determine whether an element exists Set<String> q1 = new HashSet<>(); Set<String> q2 = new HashSet<>(); Set<String> visited = new HashSet<>(); int step = 0; q1.add("0000"); q2.add(target); while (!q1.isEmpty() && !q2.isEmpty()) { // The hash set cannot be modified during traversal. temp is used to store the diffusion results Set<String> temp = new HashSet<>(); /* Spread all nodes in q1 around */ for (String cur : q1) { /* Judge whether to reach the end point */ if (deads.contains(cur)) continue; if (q2.contains(cur)) return step; visited.add(cur); /* Add the non traversal adjacent nodes of a node to the collection */ for (int j = 0; j < 4; j++) { String up = plusOne(cur, j); if (!visited.contains(up)) temp.add(up); String down = minusOne(cur, j); if (!visited.contains(down)) temp.add(down); } } /* Add steps here */ step++; // temp is equivalent to q1 // Exchange Q1 and q2 here, and the next round of while is diffusion q2 q1 = q2; q2 = temp; } return -1; }
Bidirectional BFS still follows the BFS algorithm framework, but instead of using queues, HashSet is used to quickly judge whether two sets have intersection.
Another skill point is The last exchange of the while loop q1 and q2 Content, so as long as the default diffusion q1 It's equivalent to rotating diffusion q1 and q2.
In fact, there is another optimization of bidirectional BFS, which is to make a judgment at the beginning of the while cycle:
// ... while (!q1.isEmpty() && !q2.isEmpty()) { if (q1.size() > q2.size()) { // Swap q1 and q2 temp = q1; q1 = q2; q2 = temp; } // ...
Why is this an optimization?
Because according to the logic of BFS, the more elements in the queue (set), the more elements in the new queue (set) after diffusion; In the bidirectional BFS algorithm, if we choose a smaller set for diffusion every time, the growth rate of occupied space will be slower and the efficiency will be higher.
However, whether traditional BFS or two-way BFS is optimized or not, the time complexity is the same from the Big O measurement standard. It can only be said that two-way BFS is a kind of trick, and the running speed of the algorithm will be relatively fast. It doesn't matter whether you master it or not. The most important thing is to write down the BFS general framework. Anyway, all BFS algorithms can use it to set out the solution.
_____________