题目地址:
https://www.lintcode.com/problem/word-ladder-ii/description
给定两个字符串
s
s
s和
e
e
e,再给定一个含字符串的哈希表,从
s
s
s开始出发,每次允许改变其中的一个字母,问多少至少步能走到
e
e
e,并要求按字典序返回所有最短路径,并且要求路径中所有字符串都包含于哈希表内(起点终点不必)。
关于BFS找路径的标准做法,参考https://blog.csdn.net/qq_46105170/article/details/108415255。下面介绍另一种做法。
思路是BFS建图 + DFS。如果起点终点一样,情形是平凡的。题目本质是隐式图搜索,而且要求最短路径,所以要用BFS分层遍历,并且要维护层与层之间的路径(要注意排除掉层内部单词的路径,这些路径是无效的)。我们可以采用逆向遍历的方式,从 e e e一路BFS到 s s s,建一个从 s s s到 e e e的一个拓扑图,接下来只需要做一遍DFS就可以了(其实BFS的方向无所谓,都可以做,只要最后建出来的图DFS方便即可)。由于要求路径按字典序排列,所以在建图的时候可以用邻接表,并且value是一个TreeSet,这样一个顶点的邻接点会按照字典序排序,DFS的时候会将路径按字典序由小到大遍历出来,最后就不用再排序了。代码如下:
import java.util.*;
public class Solution {
/*
* @param start: a string
* @param end: a string
* @param dict: a set of string
* @return: a list of lists of string
*/
public List<List<String>> findLadders(String start, String end, Set<String> dict) {
// write your code here
List<List<String>> res = new ArrayList<>();
// 特判
if (start.equals(end)) {
List<String> list = new ArrayList<>();
list.add(start);
res.add(list);
return res;
}
// 为了方便,把起点和终点都加入dict
dict.add(start);
dict.add(end);
// 开一个哈希表用作邻接表建图
Map<String, Set<String>> graph = new HashMap<>();
// 为了BFS建一个队列和哈希表,并将end加入;从end开始做BFS
Queue<String> queue = new LinkedList<>();
queue.offer(end);
Set<String> visited = new HashSet<>();
visited.add(end);
boolean found = false;
while (!queue.isEmpty()) {
// 先把queue里这一层的节点全标记为visited,这样在扩展下一层的时候可以避免本层顶点被纳入进来
visited.addAll(queue);
// 存下一层要入队的字符串,由于下一层字符串可能有重复,这里用个哈希表去重
Set<String> nextLayer = new HashSet<>();
// 分层BFS需要记录队列的size
int size = queue.size();
for (int i = 0; i < size; i++) {
String cur = queue.poll();
// 返回cur的不在本层的邻接顶点
for (String next : getNexts(cur, dict, visited)) {
// 开始从next向cur连一条边
graph.putIfAbsent(next, new TreeSet<>());
graph.get(next).add(cur);
// 如果到达了start,就标记一下
if (next.equals(start)) {
found = true;
}
nextLayer.add(next);
}
}
// 建图结束,不要再向后建了,否则DFS的时候会产生一个更长的路径
if (found) {
break;
}
// 把下一层的节点加入队列
queue.addAll(nextLayer);
}
dfs(start, end, graph, new ArrayList<>(), res);
return res;
}
// 从cur开始一路DFS到end,并将所有路径加入res
// 这里DFS的时候并不需要记录visited,原因是图本身已经是分好层的,也就是没有环,类似于树的形状(只是类似,但并不是)
private void dfs(String cur, String end, Map<String, Set<String>> graph, List<String> list, List<List<String>> res) {
list.add(cur);
if (cur.equals(end)) {
res.add(new ArrayList<>(list));
// 回溯的时候恢复现场
list.remove(list.size() - 1);
return;
}
if (graph.containsKey(cur)) {
for (String next : graph.get(cur)) {
dfs(next, end, graph, list, res);
}
}
// 回溯的时候恢复现场
list.remove(list.size() - 1);
}
private Set<String> getNexts(String cur, Set<String> dict, Set<String> visited) {
Set<String> nexts = new HashSet<>();
char[] chs = cur.toCharArray();
for (int i = 0; i < chs.length; i++) {
char ch = chs[i];
for (char c = 'a'; c <= 'z'; c++) {
chs[i] = c;
String next = String.valueOf(chs);
if (dict.contains(next) && !visited.contains(next)) {
nexts.add(next);
}
}
chs[i] = ch;
}
return nexts;
}
}
时空复杂度 O ( V log V + E ) O(V\log V+E) O(VlogV+E)。 log V \log V logV的原因是用了TreeSet而不是普通的HashSet。
注解:
1、关键的一步是,在BFS的时候,一开始就要将queue里记录的当前层的所有顶点都加入哈希表里,这样getNexts的时候就不会得到当前层的顶点,而只会得到下一层的顶点了。可以说visited.addAll(queue);
这一句所处的位置相当的关键。如果和普通的BFS一样,将这一句放在遍历next的循环里,就会漏掉一部分路径,也就是会漏解;
2、nextLayer
这个变量是用来存储下一层要入队的节点的,这样可以防止同一个字符串重复入队,可以提高效率;
3、found
变量主要用来记录是否到达了start
,一旦到达就说明找到了最短路了,这时候建完从start
到本层的cur
的边后就不能继续建图了,否则会产生一条更长的路径,DFS的时候就会产生错误的结果(当然也可以记录步数,在DFS的时候排除掉更长的路径)。