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 transformed word must exist in the word list. Note that beginWord is not a transformed word.
For example,
Given:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log","cog"]
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.
- You may assume no duplicates in the word list.
- You may assume beginWord and endWord are non-empty and are not the same.
这道题本质上是一个求无向无权图的单源最短路径问题,首选的解法是广度优先遍历(BFS)。
beginWord相当于路径的源点,而endWord相当于路径的终点。dictionary中的每个单词相当于一个节点,相互之间只差一个字符的两个单词其对应节点相互邻接,从而构成了一个无向无权图。
为了进行广度优先遍历,我们需要一个队列Queue作为辅助的数据结构。在进入循环之前,先把beginWord添加到队列中以初始化队列;当Queue为空时,结束循环。
由于题目要求的是路径的长度,我们可以用一个变量来记录广度优先遍历的当前层次数,并用一个内循环来完成一层遍历。这样,当找到endWord时,结束遍历,此时该变量的值就是路径的长度。
这道题的场景与图论中求单源最短路径不同之处在于,图论中遍历一个节点的邻接节点通常可以直接从该节点的属性中读取,而这道题中无法直接得到一个单词的邻接单词。
对于这个问题,有两种可以参考的解决思路:
(1)用一个Set来记录已经处理过的单词。每次要求一个单词A的邻接单词时,就遍历字典——如果当前单词不在Set(即还没有被处理过),就把当前单词入队列Queue;否则,表明该单词已经处理过,直接略过。
这种求邻接单词的方法需要遍历整个字典,因此时间复杂度为O(n),其中n表示字典的规模。在最坏情况下,要对字典中的每一个单词都处理之后才能得到这道题的最终结果,因此这种解法的总的时间复杂度是O(n^2)。用到了两个辅助的数据结构——Set和Queue,最坏情形下需要2*n的辅助空间,因此空间复杂度为O(n)。
对应的代码如下:
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> known = new HashSet<>();
Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
for(int i = 2; !queue.isEmpty(); i++) {
int size = queue.size();
for(int j = 0; j < size; j++) {
String str = queue.poll();
known.add(str);
wordList.remove(str);
for(String s : wordList) {
if(!known.contains(s) && isNeighbor(str, s)) {
if(s.equals(endWord)) {
return i; //Found it!
} else {
queue.offer(s);
}
}
}
}
}
return 0;
}
private boolean isNeighbor(String str1, String str2) {
int length = str1.length();
int diff = 0;
for(int i = 0; i < length; i++) {
if(str1.charAt(i) != str2.charAt(i)) {
diff++;
}
}
return diff == 1;
}
}
(2)用一个Set来记录没有入过队列Queue的单词。每次要求一个单词A的邻接单词时,就遍历A中的所有字符,逐个尝试用a~z替换当前字符,看得到的新单词是否在Set中——如果在,就把新单词入队列Queue,同时从Set中移除该单词;否则,忽略这个新单词。
这里求邻接单词时,遍历了单词中的每个字符,需要进行k * 26次操作,因此其时间复杂度为O(k),其中k表示单词的长度。因此,这种解法的总时间复杂度为O(nk)。空间复杂度也是O(n)。
对应的代码如下:
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> toQueue = new HashSet<>(wordList);
Queue<String> queue = new LinkedList<>();
queue.offer(beginWord);
for(int i = 1; !queue.isEmpty(); i++) {
int size = queue.size();
for(int j = 0; j < size; j++) {
String str = queue.poll();
if(str.equals(endWord)) {
return i;
} else {
findNeighbors(str, toQueue, queue);
}
}
}
return 0;
}
private void findNeighbors(String word, Set<String> toQueue, Queue<String> queue) {
for(int i = 0; i < word.length(); i++) {
for(int j = 0; j < 26; j++) {
char[] wordCharArray = word.toCharArray();
wordCharArray[i] = (char)('a' + j);
String s = String.valueOf(wordCharArray);
if(toQueue.contains(s)) {
toQueue.remove(s);
queue.offer(s);
}
}
}
}
}
这两种思路的选择取决于字典的特点:如果字典中的单词长度明显比字典规模大,那么选用第1种思路会有比较好的效果;反之,如果字典规模明显比单词长度大,那么应该选用第2种思路。