定义
- 广度优先搜索(Breadth-First Search)算法属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。广度优先搜索一般需要队列的配合实现。典型的广度优先搜索例子有:拓扑排序、树的层序遍历。
- 深度优先搜索(Depth-First Search)过程简要来说是对每一个可能的分支路径深入到不能再深入为止,当访问某个结点到尽头时,返回上一个还没访问的结点继续进行深度优先搜索,所以深度优先搜索常用栈配合实现。
题目
207. 课程表
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
思路分析
问题可转化为判断以课程为结点组成的图是否存在环。于是,问题的重点在于,怎样判断图中有环。由此可以联想到图的拓扑排序:拓扑排序每次将一个入度为0的结点输出。如果一个图中若干个结点形成环,那么这几个结点将不会被输出到拓扑序列中,因为它们互相为对方提供入度。所以维护一个表记录每个结点的入度(这里入度是先修课程),当一个结点没有入度时,我们将它放到一个队列中缓存。每次从队列中取出一个结点,将课程数numCourse减一,然后查找prerequisites数组,如果某一课程以该结点为前驱,则将该课程的入度减一。如果一个课程入度为零,就将它加入到队列中缓存。最后判断numCourses是否等于零。
复杂度分析
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] indegrees = new int[numCourses];
for(int[] e: prerequisites)
indegrees[e[0]]++;
Queue<Integer> buffer = new LinkedList<>();
for(int i=0;i<indegrees.length;i++)
if(indegrees[i]==0)
buffer.offer(i);
while(!buffer.isEmpty()) {
int cur = buffer.poll();
numCourses--;
for(int[] e: prerequisites) {
if(e[1] == cur) {
indegrees[e[0]]--;
if(indegrees[e[0]] == 0)
buffer.offer(e[0]);
}
}
}
return numCourses == 0;
}
}
改进
只要在构建入度表的同时将每个课程的先修课程以及当前课程放入一个哈希表,每次从队列中取出结点不用遍历一次prerequisite数组就能找到该结点的后继。
class Solution{
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] indegrees = new int[numCourses];
Map<Integer, List<Integer>> successors = new HashMap<>();
for(int[] e: prerequisites){
indegrees[e[0]]++;
if(!successors.containsKey(e[1]))
successors.put(e[1], new ArrayList<>());
successors.get(e[1]).add(e[0]);
}
Queue<Integer> buffer = new LinkedList<>();
for(int i=0;i<indegrees.length;i++)
if(indegrees[i]==0)
buffer.offer(i);
while(!buffer.isEmpty()) {
int cur = buffer.poll();
numCourses--;
List<Integer> temp = successors.get(cur); //得到以cur为先修课程的所有课程
if(temp == null)
continue;
for(int e: temp) {
indegrees[e]--;
if(indegrees[e]==0)
buffer.offer(e);
}
}
return numCourses == 0;
}
}
复杂度分析
- 时间复杂度:构建入度表遍历prerequisite花费O(V),在遍历结点过程中每个结点及每条边都被访问了一次,总共花费O(V+E)。
- 空间复杂度:构建入度表O(V),构建哈希表O(V+E)。
深度优先搜索
只要判断出由课程关系构成的图有环,即可断定不能完成课程学习。如果采用深度优先搜索,设置数组visited,visited[i]的三个值含义分别为:
- -1:之前某次函数调用已经访问过该结点
- 1:本次函数调用过程中已经访问过该结点
- 0:该结点未被访问过
每次访问一个结点将相应visited[i]设置为1,如果在深度优先搜索递归过程中发现一个结点visited[j] == 1,则说明这个结点已经被访问过,说明图中有环。注意在递归函数退出前将visited[i]设置为-1,表明该结点已经访问过,且没有成环。
由上述分析可见,要进行深度优先搜索,要记录结点的后继信息,故设置一个哈希表,记录每个结点的后继结点。
class Solution{
public boolean canFinish(int numCourses, int[][] prerequisites) {
Map<Integer, List<Integer>> successors = new HashMap<>();
int[] visited = new int[numCourses];
for(int[] e: prerequisites) {
if(!successors.containsKey(e[1]))
successors.put(e[1], new ArrayList<Integer>());
successors.get(e[1]).add(e[0]);
}
for(int i=0;i<numCourses;i++)
if(isCircular(successors, visited, i))
return false;
return true;
}
private boolean isCircular(Map<Integer,List<Integer>> adj, int[] visited, int i) {
if(visited[i] == 1)
return true;
if(visited[i] == -1)
return false;
visited[i] = 1;
List<Integer> temp = adj.get(i);
if(temp == null) {
visited[i] = -1;
return false;
}
for(int e: temp) {
if(isCircular(adj, visited, e))
return true;
}
visited[i] = -1;
return false;
}
}
同样的思路,可以解决210.课程表II:
210.课程表II
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
广度优先搜索
class Solution{
public int[] findOrder(int numCourses, int[][] prerequisites) {
int[] res = new int[numCourses];
int index = 0;
//设置一个队列缓存入度为零的结点
Queue<Integer> queue = new LinkedList<>();
//设置一个哈希表记录每个结点的后继结点
Map<Integer, List<Integer>> successors = new HashMap<>();
//设置入度表记录每个结点的入度
int[] indegrees = new int[numCourses];
//初始化入度表和哈希表
for(int[] e: prerequisites) {
if(!successors.containsKey(e[1]))
successors.put(e[1], new ArrayList<Integer>());
successors.get(e[1]).add(e[0]);
indegrees[e[0]]++;
}
//初始化queue,将入度为零的结点放入queue
for(int i=0;i<indegrees.length;i++) {
if(indegrees[i] == 0)
queue.offer(i);
}
while(!queue.isEmpty()) {
int cur = queue.poll();
numCourses--;
res[index++] = cur;
List<Integer> temp = successors.get(cur);
if(temp == null)
continue;
for(int e: temp) {
indegrees[e]--;
if(indegrees[e] == 0)
queue.offer(e);
}
}
if(numCourses == 0)
return res;
else return new int[0];
}
}
深度优先搜索
深度优先搜索中,注意将输出结果的语句放在方法递归遍历之后,在方法返回之前。于是每次总是递归到最深层,即当一个结点没有后继时才开始输出,之后方法不断返回、不断将调用它的方法所遍历的结点输出。
public int[] findOrder(int numCourses, int[][] prerequisites) {
res = new int[numCourses];
for(int i=0;i<res.length;i++) {
res[i] = -1;
}
index = res.length-1;
Map<Integer, List<Integer>> successors = new HashMap<>();
int[] visited = new int[numCourses];
int[] indegrees = new int[numCourses];
for(int[] e: prerequisites) {
if(!successors.containsKey(e[1]))
successors.put(e[1], new ArrayList<Integer>());
successors.get(e[1]).add(e[0]);
indegrees[e[0]]++;
}
for(int i=0;i<indegrees.length;i++) {
if(indegrees[i] == 0) {
boolean hasCircle = topologicalSort(successors, visited, i);
if(hasCircle == true)
return new int[0];
}
}
if(res[0] == -1)
return new int[0];
return res;
}
private int[] res;
private int index;
//return true if there's circle
private boolean topologicalSort(Map<Integer, List<Integer>> successors, int[] visited, int i) {
if(visited[i] == 1)
return true;
if(visited[i] == -1)
return false;
visited[i] = 1;
List<Integer> temp = successors.get(i);
if(temp != null) {
for(int e: temp) {
if(topologicalSort(successors, visited, e) == true)
return true;
}
}
res[index--] = i; //这时结点已经没有后继了,或者后继已经输出完了
visited[i] = -1;
return false;
}
752. 打开转盘锁
题目描述
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转:例如把 ‘9’ 变为 ‘0’,‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 ‘0000’ ,一个代表四个拨轮的数字的字符串。
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出最小的旋转次数,如果无论如何不能解锁,返回 -1。
示例 1:
输入:deadends = [“0201”,“0101”,“0102”,“1212”,“2002”], target = “0202”
输出:6
解释:
可能的移动序列为 “0000” -> “1000” -> “1100” -> “1200” -> “1201” -> “1202” -> “0202”。
注意 “0000” -> “0001” -> “0002” -> “0102” -> “0202” 这样的序列是不能解锁的,
因为当拨动到 “0102” 时这个锁就会被锁定。
思路分析
转盘锁从“0000”开始,对此我们进行广度优先搜索,从一次转动可能出现的所有结果中,选取既还没访问,又不在deadends中的结果,将它们加入到一个队列中缓存。
class Solution{
public int openLock(String[] deadends, String target) {
int turningTimes = 0;
if(target == "0000")
return 0;
Set<String> deads = new HashSet<>();
Queue<String> buffer = new LinkedList<>();
Set<String> visited = new HashSet<>();
for(String s: deadends)
deads.add(s);
if(deads.contains("0000"))
return -1;
buffer.offer("0000");
visited.add("0000");
while(!buffer.isEmpty()) {
int count = buffer.size();
turningTimes++;
while(count > 0) {
String cur = buffer.poll();
for(int i=0;i<cur.length();i++) {
for(int j=-1;j<=1;j+=2) {
int digit = Integer.parseInt(cur.charAt(i)+"");
digit = (digit + j + 10) % 10;
StringBuilder temp = new StringBuilder(cur);
temp.replace(i, i+1, digit+"");
String adj = temp.toString();
if(!visited.contains(adj) && !deads.contains(adj)) {
if(adj.equals(target))
return turningTimes;
buffer.offer(adj);
visited.add(adj);
}
}
}
count--;
}
}
return -1;
}
}
同样的套路,稍加处理就可以解决其他类似问题。
773.滑动谜题
题目描述
在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示.
一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换.
最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。
给出一个谜板的初始状态,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1 。
示例:
输入:board = [[1,2,3],[4,0,5]]
输出:1
解释:交换 0 和 5 ,1 步完成
思路分析
将二维数组board转化为一个字符串,然后进行广度优先搜索。用一个哈希表visited记录已经访问过的字符串,用一个队列buffer缓存当前生成的字符串。特别注意对字符串的修改。
代码
public int slidingPuzzle(int[][] board) {
final String TARGET = "123450";
Queue<String> buffer = new LinkedList<>();
Set<String> visited = new HashSet<>();
int movingTimes = 0;
String initial = generateInitialBoard(board);
if(initial.equals(TARGET))
return 0;
buffer.offer(initial);
visited.add(initial);
while(!buffer.isEmpty()) {
int count = buffer.size();
movingTimes++;
while(count > 0) {
String cur = buffer.poll();
for(int i=-3;i<=3;i+=6) {
if(!isLegal(cur, i))
continue;
String next = generateNext(cur, i);
if(next.equals(TARGET))
return movingTimes;
if(!visited.contains(next)) {
buffer.offer(next);
visited.add(next);
}
}
for(int j=-1;j<=1;j+=2) {
if(!isLegal(cur, j))
continue;
String next = generateNext(cur, j);
if(next.equals(TARGET))
return movingTimes;
if(!visited.contains(next)) {
buffer.offer(next);
visited.add(next);
}
}
count--;
}
}
return -1;
}
private String generateInitialBoard(int[][] board) {
StringBuilder initial = new StringBuilder();
for(int i=0;i<board.length;i++) {
for(int j=0;j<board[0].length;j++) {
initial.append(board[i][j]);
}
}
return initial.toString();
}
private boolean isLegal(String cur, int i) {
int pos = cur.indexOf("0");
if(pos == 2 && i == 1 || pos == 3 && i == -1)
return false;
return pos + i >= 0 && pos + i < cur.length();
}
private String generateNext(String cur, int i) {
StringBuilder temp = new StringBuilder(cur);
int pos = temp.indexOf("0");
char c = temp.charAt(pos + i);
temp.replace(pos + i, pos + i + 1, "0");
temp.replace(pos, pos + 1, c+"");
return temp.toString();
}