Question
Given two words (beginWord and endWord), and a dictionary’s word list, find the length of shortest transformation sequence from beginWord to endWord, such that:
Only one letter can be changed at a time
Each intermediate word must exist in the word list
For example,
Given:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog"
,
return its length 5
.
Note:
Return 0 if there is no such transformation sequence.
All words have the same length.
All words contain only lowercase alphabetic characters.
本题难度Medium。有2种算法分别是: BFS 和 Two End BFS
1、BFS
【复杂度】
时间 O(N) 空间 O(N)
【思路】
因为要求最短路径,如果我们用深度优先搜索的话必须遍历所有的路径才能确定哪个是最短的,而用广度优先搜索的话,一旦搜到目标就可以提前终止了,而且根据广度优先的性质,我们肯定是先通过较短的路径搜到目标。另外,为了避免产生环路和重复计算,我们找到一个存在于字典的新的词时,就要把它从字典中移去。这么做是因为根据广度优先,我们第一次发现词A的路径一定是从初始词到词A最短的路径,对于其他可能再经过词A的路径,我们都没有必要再计算了。(在这里,为了寻求单词之间的路径关系,会遍历单词的每个位置,对其做从'a'
到'z'
的替换)
【注意】
base case为队列q.size()==0
而不是wordList.size()==0
,因为并不是wordList
中所有的单词都会被BFS到。
【代码】
public class Solution {
public int ladderLength(String beginWord, String endWord, Set<String> wordList) {
//require
Queue<String> q=new LinkedList<>();
q.offer(beginWord);
//invariant
return bfs(2,q,endWord,wordList);
}
private int bfs(int length,Queue<String> q,String endWord, Set<String> wordList){
//base case
if(q.size()==0){
return 0;
}
Queue<String> nextQ=new LinkedList<>();
while(!q.isEmpty()){
String word=q.poll();
char[] cArray = word.toCharArray();
for(int i=0;i<word.length();i++){
for(char c='a';c<='z';c++){
char ci=cArray[i];
cArray[i]=c;
String tmp=new String(cArray);
//包括word本身也要进行检查
if(tmp.equals(endWord))
return length;
else if(wordList.contains(tmp)){
nextQ.offer(tmp);
wordList.remove(tmp);
}
cArray[i]=ci;
}
}
}
return bfs(length+1,nextQ,endWord,wordList);
}
}
2、Two End BFS
【复杂度】
时间 O(N) 空间 O(N)
【思路】
如果把各个单词之间的路径画出来中你会发现有个特点,它是个“纺锤体”:
通过第一个方法我们知道查找单词之间的路径的方法是遍历:
String word=q.poll();
char[] cArray = word.toCharArray();
for(int i=0;i<word.length();i++){
for(char c='a';c<='z';c++){
char ci=cArray[i];
cArray[i]=c;
String tmp=new String(cArray);
if(tmp.equals(endWord))
return length;
else if(wordList.contains(tmp)){
nextQ.offer(tmp);
wordList.remove(tmp);
}
cArray[i]=ci;
}
}
也就是说每个word
都要耗费traversal time = word.length()*26
次的遍历时间。traversal time
是无法降低的,但是我们可以通过降低word
的个数来提高性能,这也正是两端BFS名称的由来。通过产生两个集合ss
和ls
,保证ss
是单词较少的那个,遍历ss
中的单词cur
,只要ls
中有cur
就返回;如果没有就遍历cur
在wordList
中的下一个单词并加入到集合ns
中。结束后将ls
和ns
中小的给ss
,大的给ls
。实际上我们可以看出在第一次迭代中ss
存放的是beginWord
,而第二次迭代中ss
存放的是endWord
(这就是两端BFS的核心所在)。
【代码】
public class Solution {
public Set<String> getNeighbors(String cur, Set<String> dict) {
Set<String> neighbors = new HashSet<>();
char[] cArray = cur.toCharArray();
for (int i = 0; i < cArray.length; ++i) {
char ci= cArray[i];
for (char c = 'a'; c <= 'z'; ++c) {
if (c == ci) {
continue;
}
cArray[i] = c;
String s = new String(cArray);
if (dict.contains(s)) {
neighbors.add(s);
}
}
cArray[i] = ci;
}
return neighbors;
}
public int ladderLength(String beginWord, String endWord, Set<String> wordList) {
Set<String> s1 = new HashSet<>();
Set<String> s2 = new HashSet<>();
s1.add(beginWord);
s2.add(endWord);
// choose the set has the smaller size
boolean isS1 = s1.size() < s2.size() ? true : false;
Set<String> ss = isS1 ? s1 : s2;
Set<String> ls = isS1 ? s2 : s1;
int res = 0;
while (!ss.isEmpty()){
++res;
// remove for better performance and solve the visited problem
wordList.removeAll(ss);
// similar as queue.poll() in BFS
Set<String> ns = new HashSet<>();
for (String cur : ss) {
if (ls.contains(cur)) {
return res;
}
for (String neighbor : getNeighbors(cur, wordList)) {
ns.add(neighbor);
}
}
// choose the set has the smaller size
isS1 = ns.size() < ls.size() ? true : false;
ss = isS1 ? ns : ls;
ls = isS1 ? ls : ns;
}
return 0;
}
}
【附】
- 本题对于beginWord与endWord相等的情况,length 可以等于
1
也可以等于2
。 - 方法1是先对
word
进行变形再检查是否等于endWord
(其中还包括word
自己),是一种映射方法;方法2是先检查ss
中的cur
是否包含于ls
,再变形cur
,是一种集合方法。