思路:
求最短路径问题,很自然地想到要用广度优先遍历
代码:
class Solution {
public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
//先将wordList放到哈希表中
Set<String> wordSet=new HashSet<>(wordList);
List<List<String>> res=new ArrayList<>();
if(wordSet.size()==0||!wordSet.contains(endWord)){
return res;
}
//第1步,使用广度优先,得到后继节点列表successors
Map<String,Set<String>> successors=new HashMap<>();
boolean found=bfs(beginWord,endWord,wordSet,successors);
//如果没找到,就返回空的res
if(!found){
return res;
}
//第2步,使用回溯算法得到所有的最短路径列表,要基于后继节点successors
Deque<String> path=new ArrayDeque<>();
path.addLast(beginWord);
dfs(beginWord,endWord,successors,path,res);
return res;
}
private boolean bfs(String beginWord,String endWord,Set<String> wordSet,Map<String,Set<String>>successors){
//使用一个队列,先进先出
Queue<String> queue=new LinkedList<>();
//初始化队列,把第一个单词加入队列
queue.offer(beginWord);
//利用一个Set哈希来存储是否访问过visited
Set<String> visited=new HashSet<>();
//visited妙,防止多次加入
visited.add(beginWord);
boolean found=false;
int wordLen=beginWord.length();
//将当前单词访问过的节点添加到nextLevelVisited
Set<String> nextLevelVisited=new HashSet<>();
while(!queue.isEmpty()){
//是为了记录,下面queue的size会改变
int currentSize=queue.size();
//每一个单词
for(int i=0;i<currentSize;i++){
//是poll,则每一次都不会重复遍历之前的
String currentWord=queue.poll();
char[] charArray=currentWord.toCharArray();
//每一个字母
for(int j=0;j<wordLen;j++){
//记录下当前的字母
char originChar=charArray[j];
//新奇的遍历字母的方式,仍然是k++
//把当前字母的25种形式都换一遍
for(char k='a';k<='z';k++){
if(charArray[j]==k){
continue;
}
//更改
charArray[j]=k;
String nextWord=new String(charArray);
//单词列表里要包含该更改的字母
//下面是一次循环
if(wordSet.contains(nextWord)){
//要没有访问过
//visited妙
if(!visited.contains(nextWord)){
//如果刚好此单词与最后一个单词一致,则找到了一个
if(nextWord.equals(endWord)){
found=true;
}
//不管是不是最后一个单词,都算查到了
//加入到该单词的查找列表中
nextLevelVisited.add(nextWord);
//加入到队列中再进行循环
queue.offer(nextWord);
//更新successor
//下面是java 1.8之后支持的形式
//意思是如果存在当前单词的哈希表,则直接用之前的,否则新建
successors.computeIfAbsent(currentWord,a->new HashSet<>());
//将每一个符合的单词加入,其实相当于nextLevelVisited
successors.get(currentWord).add(nextWord);
}
}
}
//将更改的字母变回原来的那个字母,还原原来的单词
charArray[j]=originChar;
}
}
//一个单词的一个字符遍历结束,开始下一个字符
if(found){
break;
}
//visited妙
visited.addAll(nextLevelVisited);
//HashSet-->clear()方法
nextLevelVisited.clear();
}
return found;
}
private void dfs(
String beginWord
,String endWord
,Map<String,Set<String>> successors
,Deque<String> path
,List<List<String>> res){
//回溯终止条件
if(beginWord.equals(endWord)){
res.add(new ArrayList<>(path));
return;
}
if(!successors.containsKey(beginWord)){
return;
}
//得到该单词的同一层单词
Set<String> successorWords=successors.get(beginWord);
for(String nextWord:successorWords){
path.addLast(nextWord);
dfs(nextWord,endWord,successors,path,res);
//最后得到了endWord或没得到都把path弹出去,查找别的情况
path.removeLast();
}
}
}
分解:
1)第一步:使用广度优先遍历(bfs)求得每个字符串的邻接表。并且bfs方法里还要设置一个flag,判断是否找到一个结果,若没有找到一个,则不需要再往下进行
2)找到每个单词的邻接表后,利用回溯法(dfs),求得符合条件的多条最短路径
3)广度优先遍历利用的是队列这个数据结构,bfs里要设置:
i)同层字符串nextLevel
ii)记录是否访问过,利用HashSet形式(visited)
iii)设置flag判断是否找到至少一组以上(found)
iv)核心:
1.当队列不为空时,就不断poll出字符串
2.记录每次弹出时当时队列中的字符串个数
3.分别改变每个字符串的每个字母,将它替换为其他25个字母。遍历过一遍后将更改过的字母还原为原来的,然后继续超前遍历该字符串的别的字母
4.多个if判断新字符串:i)单词列表中有 ii)没被访问过(visited里没有) iii#)刚好此单词为最后一个单词endWord,则设置flag为true,找到一个
5.更新邻接表:
下面是java 1.8之后支持的形式
意思是如果存在当前单词的哈希表,则直接用之前的,否则新建
successor.computeIfAbsent(currentWord,a->new HashSet<>());
successor.get(currentWord).add(nextWord);
6.在每遍历完一个字符串后,要将nextLevel存入visited并清空nextLevel
4)回溯法(dfs):
回溯终止条件
if(beginWord.equals(endWord)){
res.add(new ArrayList<>(path));
return;
}
if(!successors.containsKey(beginWord)){
return;
}
核心代码:
//得到该单词的同一层单词
Set<String> successorWords=successors.get(beginWord);
for(String nextWord:successorWords){
path.addLast(nextWord);
dfs(nextWord,endWord,successors,path,res);
//最后得到了endWord或没得到都把path弹出去,查找别的情况
path.removeLast();
}