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)

【思路】
如果把各个单词之间的路径画出来中你会发现有个特点,它是个“纺锤体”:

(复习)[LeetCode]Word Ladder_ci

通过第一个方法我们知道查找单词之间的路径的方法是遍历:

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;
}
}

【附】

  1. 本题对于beginWord与endWord相等的情况,length 可以等于​​1​​​也可以等于​​2​​。
  2. 方法1是先对​​word​​​进行变形再检查是否等于​​endWord​​​(其中还包括​​word​​​自己),是一种映射方法;方法2是先检查​​ss​​​中的​​cur​​​是否包含于​​ls​​​,再变形​​cur​​,是一种集合方法。

参考

​​[Leetcode] Word Ladder 单词爬梯​​

​beat 95% two end BFS java soluction​