BFS使用场景
- 连通块问题:(通过一个点找到图中连通的所有点)
- 分层遍历:(图的层次遍历、简单图的最短路径)
- 拓扑排序:(求任意拓扑排序、求是否有拓扑排序、求字典序最小的拓扑序、求是否唯一拓扑序)
树的BFS
图的BFS
解决最短路径的管法有哪些
简单图 | 复杂图 |
---|
BFS | SPFA, Floyd, Dijkstra, Bellman-ford面试中一般不考复杂图最短路径问题 |
什么是简单图 ? 没有方向 (undirected) ? 没有权重 (unweighted) ? 两点之间最多只有一条边 (no multiple edges) ? 一个点没有一条边直接连着自己 (no graph loops,这里的graph loop指的是自己直接指向自己的loop)
BFS 算法的通用模板
Queue<Node> queue = new ArrayDeque<>();
HashMap<Node, Integer> distance = new HashMap<>();
queue.offer(node);
distance.put(node, 0);
while (!queue.isEmpty()) {
Node node = queue.poll();
for (Node neighbor : node.getNeighbors()) {
if (distance.containsKey(neighbor)) {
continue;
}
distance.put(neighbor, distance.get(node) + 1);
queue.offer(neighbor);
}
}
题例
克隆一张无向图. 无向图的每个节点包含一个 label 和一个列表 neighbors. 保证每个节点的 label 互不相同. 你的程序需要返回一个经过深度拷贝的新图. 新图和原图具有同样的结构, 并且对新图的任何改动不会对原图造成任何影响 你需要返回与给定节点具有相同 label 的那个节点
输入
{1,2,4#2,1,4#4,1,2}
输出
{1,2,4#2,1,4#4,1,2}
解释
1 => 2, 4 2 => 1, 4 4 => 1, 2 原图和新图的点和边一模一样
题解
将整个算法分解为三个步骤:1.找到所有点 2.复制所有点 3.复制所有边
public class Solution {
public UndirectedGraphNode cloneGraph(UndirectedGraphNode node) {
if (node == null) {
return null;
}
List<UndirectedGraphNode> nodes = findNodesByBFS(node);
Map<UndirectedGraphNode, UndirectedGraphNode> mapping = copyNodes(nodes);
copyEdges(nodes, mapping);
return mapping.get(node);
}
private List<UndirectedGraphNode> findNodesByBFS(UndirectedGraphNode node) {
Queue<UndirectedGraphNode> queue = new LinkedList<>();
Set<UndirectedGraphNode> visited = new HashSet<>();
queue.offer(node);
visited.add(node);
while (!queue.isEmpty()) {
UndirectedGraphNode curNode = queue.poll();
for (UndirectedGraphNode neighbor : curNode.neighbors) {
if (visited.contains(neighbor)) {
continue;
}
visited.add(neighbor);
queue.offer(neighbor);
}
}
return new LinkedList<>(visited);
}
private Map<UndirectedGraphNode, UndirectedGraphNode> copyNodes(List<UndirectedGraphNode> nodes) {
Map<UndirectedGraphNode, UndirectedGraphNode> mapping = new HashMap<>();
for (UndirectedGraphNode node : nodes) {
mapping.put(node, new UndirectedGraphNode(node.label));
}
return mapping;
}
private void copyEdges(List<UndirectedGraphNode> nodes, Map<UndirectedGraphNode, UndirectedGraphNode> mapping){
for (UndirectedGraphNode node : nodes) {
UndirectedGraphNode newNode = mapping.get(node);
for (UndirectedGraphNode neighbor : node.neighbors) {
UndirectedGraphNode newNeighbor = mapping.get(neighbor);
newNode.neighbors.add(newNeighbor);
}
}
}
}
题例
出两个单词(start和end)和一个字典,找出从start到end的最短转换序列,输出最短序列的长度。
变换规则如下: 每次只能改变一个字母。 变换过程中的中间单词必须在字典中出现。(起始单词和结束单词不需要出现在字典中)
样例
输入:
start = "a"
end = "c"
dict =["a","b","c"]
输出:
2
解释:
"a"->"c"
题解一
public class Solution {
public int ladderLength(String start, String end, Set<String> dict) {
dict.add(end);
HashSet<String> visited = new HashSet<String>();
Queue<String> queue = new LinkedList<String>();
queue.offer(start);
visited.add(start);
int length = 1;
while (!queue.isEmpty()) {
length++;
int size = queue.size();
for (int i = 0; i < size; i++) {
String word = queue.poll();
for (String nextWord : getNextWords(word, dict)) {
if (visited.contains(nextWord)) {
continue;
}
if (nextWord.equals(end)) {
return length;
}
visited.add(nextWord);
queue.offer(nextWord);
}
}
}
return 0;
}
private ArrayList<String> getNextWords(String word, Set<String> dict) {
ArrayList<String> nextWords = new ArrayList<String>();
for (String nextWord : dict) {
boolean hashOneDiff = false;
for (int i = 0; i < word.length(); i++) {
if (nextWord.charAt(i) != word.charAt(i)) {
if (hashOneDiff) {
hashOneDiff = false;
break;
}
hashOneDiff = true;
}
}
if (hashOneDiff) {
nextWords.add(nextWord);
}
}
return nextWords;
}
private String replace(String s, int index, char c) {
char[] chars = s.toCharArray();
chars[index] = c;
return new String(chars);
}
}
可以对getNextWords进行优化
public class Solution {
public int ladderLength(String start, String end, Set<String> dict) {
dict.add(end);
HashSet<String> visited = new HashSet<String>();
Queue<String> queue = new LinkedList<String>();
queue.offer(start);
visited.add(start);
int length = 1;
while (!queue.isEmpty()) {
length++;
int size = queue.size();
for (int i = 0; i < size; i++) {
String word = queue.poll();
for (String nextWord : getNextWords(word, dict)) {
if (visited.contains(nextWord)) {
continue;
}
if (nextWord.equals(end)) {
return length;
}
visited.add(nextWord);
queue.offer(nextWord);
}
}
}
return 0;
}
private String replace(String s, int index, char c) {
char[] chars = s.toCharArray();
chars[index] = c;
return new String(chars);
}
private ArrayList<String> getNextWords(String word, Set<String> dict) {
ArrayList<String> nextWords = new ArrayList<String>();
for (char c = 'a'; c <= 'z'; c++) {
for (int i = 0; i < word.length(); i++) {
if (c == word.charAt(i)) {
continue;
}
String nextWord = replace(word, i, c);
if (dict.contains(nextWord)) {
nextWords.add(nextWord);
}
}
}
return nextWords;
}
}
矩阵中的宽度优先搜索
描述
给一个 01 矩阵,求不同的岛屿的个数。
0 代表海,1 代表岛,如果两个 1 相邻,那么这两个 1 属于同一个岛。我们只考虑上下左右为相邻。
样例
输入:
[
[1,1,0,0,0],
[0,1,0,0,1],
[0,0,0,1,1],
[0,0,0,0,0],
[0,0,0,0,1]
]
输出:
3
题解
class Coordinate {
int x, y;
public Coordinate(int x, int y) {
this.x = x;
this.y = y;
}
}
public class Solution {
int[] deltaX = {0, 1, -1, 0};
int[] deltaY = {1, 0, 0, -1};
public int numIslands(boolean[][] grid) {
if (grid == null || grid.length == 0 || grid[0] == null || grid[0].length == 0) {
return 0;
}
int islands = 0;
int row = grid.length, col = grid[0].length;
boolean[][] visited = new boolean[row][col];
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (grid[i][j] && !visited[i][j]) {
bfs(grid, i, j, visited);
islands++;
}
}
}
return islands;
}
private void bfs(boolean[][] grid, int x, int y, boolean[][] visited) {
Queue<Coordinate> queue = new ArrayDeque<>();
queue.offer(new Coordinate(x, y));
visited[x][y] = true;
while (!queue.isEmpty()) {
Coordinate coor = queue.poll();
for (int direction = 0; direction < 4; direction++) {
int newX = coor.x + deltaX[direction];
int newY = coor.y + deltaY[direction];
if (!isVaild(grid, newX, newY, visited)) {
continue;
}
queue.offer(new Coordinate(newX, newY));
visited[newX][newY] = true;
}
}
}
private boolean isVaild(boolean[][] grid, int x, int y, boolean[][] visited) {
int n =grid.length, m = grid[0].length;
if (x < 0 || x >= n || y < 0 || y >= m) {
return false;
}
if (visited[x][y]) {
return false;
}
return grid[x][y];
}
}
拓扑排序 Topological Sorting
入度(In-degree): 有向图(Directed Graph)中指向当前节点的点的个数(或指向当前节点的边的条数) 算法描述:
- 统计每个点的入度
- 将每个入度为 0 的点放入队列(Queue)中作为起始节点
- 不断从队列中拿出一个点,去掉这个点的所有连边(指向其他点的边),其他点的相应的入度 - 1
- 一旦发现新的入度为 0 的点,丢回队列中
拓扑排序并不是传统的排序算法 一个图可能存在多个拓扑序(Topological Order),也可能不存在任何拓扑序
题例
你需要去上n门课才能获得offer,这些课被标号为 0 到 n-1 。有一些课程需要“前置课程”,比如如果你要上课程0,你需要先学课程1,我们用一个匹配来表示他们: [0,1] 给你课程的总数量和一些前置课程的需求,返回你为了学完所有课程所安排的学习顺序。 可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
样例
输入: n = 2, prerequisites = [[1,0]] 输出: [0,1]
题解
统计每个点的入度 将每个入度为0的点放入队列(Queue)中作为起始节点。0、1入读均为0,所以可以把0、1(不分前后)入队列 不断从队列中拿出一个点,去掉这个点的所有连边(指向其他点的边),其他点的相应的入度-1。0的连边为4、5相应的入度-1,此时4、5的度分别为为0、2. 一旦发现新的入读为零的点,将其入队。 5出队后出现新的度为0的4,将其入队。 按上述规则进行,知道结束
public class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
List[] graph = new ArrayList[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new ArrayList<Integer>();
}
int[] inDegree = new int[numCourses];
for (int[] edge : prerequisites) {
graph[edge[1]].add(edge[0]);
inDegree[edge[0]]++;
}
Queue queue = new LinkedList<>();
for (int i = 0; i < inDegree.length; i++) {
if (inDegree[i] == 0) {
queue.add(i);
}
}
int numChoose = 0;
int[] topoOrder = new int[numCourses];
while (!queue.isEmpty()) {
int nowPos = (int)queue.poll();
topoOrder[numChoose] = nowPos;
numChoose++;
for (int i = 0; i < graph[nowPos].size(); i++) {
int nextPos = (int)graph[nowPos].get(i);
inDegree[nextPos]--;
if (inDegree[nextPos] == 0) {
queue.add(nextPos);
}
}
}
return (numChoose == numCourses) ? topoOrder : new int[0];
}
}
|