Skip to content

Commit a673b57

Browse files
committed
word ladder2
1 parent e806719 commit a673b57

File tree

2 files changed

+224
-7
lines changed

2 files changed

+224
-7
lines changed

src/main/java/joshua/leetcode/bfs/WordLadder2.java

Lines changed: 215 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
package joshua.leetcode.bfs;
44

5-
import java.util.ArrayList;
6-
import java.util.LinkedList;
7-
import java.util.List;
8-
import java.util.Set;
5+
import java.util.*;
96

107
/**
118
* 126. Word Ladder II</br>
@@ -53,7 +50,10 @@ public abstract class WordLadder2 {
5350
public static class Solution1 extends WordLadder2 {
5451

5552
/**
56-
* BFS算法,但是需要对每个节点保存已走过的路径。
53+
* 最基本的BFS算法
54+
* <p/>
55+
* 缺点:需要对每个节点保存已走过的路径,空间复杂度取决于branch factor,即每个节点的分支数量。
56+
* 由于需要复制路径,并且会遇到branch factor过大的情况,所有会Time Limit Exceeded。
5757
*
5858
* @param beginWord
5959
* @param endWord
@@ -122,4 +122,214 @@ public QueueItem(String word, List<String> path) {
122122
}
123123
}
124124
}
125+
126+
/**
127+
* 对BFS的优化。
128+
* <p/>
129+
* 如果单纯的将word抽象为节点,word之间diff为1认为可以到达,那么可以形成一个无向有环图。
130+
* 但是如果使用BFS找到连接beginWord和endWord的所有最短路径,则可以形成一个有向无环图。
131+
* 这里重要的是确定出发点和结束点之后求的是最短路径,因此形成的是有先后顺序的有向关系。
132+
* <p/>
133+
* 同时在具体的搜索过程中,每轮以路径递增的顺序搜索,在某轮出现了到达endWord的路径之后,就可以在完成
134+
* 本轮搜索(找到具有相同长度的最短路径)之后结束整个BFS过程了。
135+
* <p/>
136+
* 同时在以上BFS过程中,对于所有搜寻到的点,保存所有以该点为结束点的路径,即以逆邻接表的方式方式存储。
137+
* <p/>
138+
* 因为以这种方式建立的图,并不是所有节点都在某条最短路径中,因此为了降低最后搜寻最短路径的复杂度,
139+
* 是从endWord出发来逆序搜寻到startWord的路径的,这里就解释了上面为什么用逆邻接表来存储了。
140+
*/
141+
public static class Solution2 extends WordLadder2 {
142+
143+
/**
144+
* 用于存储最短路径的反向邻接表,key的value是所有以key为结束边的出发点的集合。
145+
* 之所以用反向邻接表存储是因为方便最后从endWord出发DFS搜寻所有的最短路径。
146+
*/
147+
private Map<String, List<String>> inversedAdjTable = new HashMap<String, List<String>>();
148+
149+
private List<List<String>> shortestPaths = new ArrayList<List<String>>();
150+
151+
@Override
152+
public List<List<String>> findLadders(String beginWord, String endWord, Set<String> wordList) {
153+
// generate the DAG constructed by all the shortest path.
154+
genDAG(beginWord, endWord, wordList);
155+
if (!inversedAdjTable.containsKey(endWord)) {
156+
return shortestPaths;
157+
}
158+
LinkedList<String> path = new LinkedList<String>();
159+
path.addFirst(endWord);
160+
genPaths(beginWord, endWord, path);
161+
return shortestPaths;
162+
}
163+
164+
private void genDAG(String startWord, String endWord, Set<String> set) {
165+
set.add(endWord);
166+
Queue<String> stepsAtLevel = new LinkedList<String>();
167+
stepsAtLevel.add(startWord);
168+
boolean found = false;
169+
//BFS, every iteration is for one level.
170+
int currentLevelSize = 1;
171+
while (currentLevelSize > 0) {
172+
int nextLevelSize = 0;
173+
Set<String> visitedWords = new HashSet<String>();
174+
while (currentLevelSize-- > 0) {
175+
String stepWord = stepsAtLevel.poll();
176+
char[] chars = stepWord.toCharArray();
177+
boolean reachEndWord = false;
178+
for (int i = 0; i < chars.length; i++) {
179+
char originalChar = chars[i];
180+
for (char ch = 'a'; ch <= 'z'; ch++) {
181+
if (originalChar != ch) {
182+
chars[i] = ch;
183+
String ladderWord = new String(chars);
184+
//if visitedWords contains the ladderword means the shortest path from beginWord to this
185+
//ladderword has been found at previous rounds, so no need to update the adjacent edges
186+
//for this word, as DAG only need to includes edges involved in those shortest paths.
187+
if (set.contains(ladderWord)) {
188+
if (!inversedAdjTable.containsKey(ladderWord)) {
189+
inversedAdjTable.put(ladderWord, new ArrayList<String>());
190+
}
191+
inversedAdjTable.get(ladderWord).add(stepWord);
192+
if (endWord.equals(ladderWord)) {
193+
// shortest path found, so we can stop stepWord's transition flagged
194+
// by reachEndWord variable and the whole iteration flagged by found variable.
195+
reachEndWord = found = true;
196+
break;
197+
}
198+
if (visitedWords.add(ladderWord)) {
199+
stepsAtLevel.offer(ladderWord);
200+
nextLevelSize++;
201+
}
202+
}
203+
}
204+
}
205+
if (reachEndWord) {
206+
// no need to try more possible transition for current step word after having proved that
207+
// it reached the end word. 'cause there is only at most one possible transition.
208+
break;
209+
} else {
210+
chars[i] = originalChar;
211+
}
212+
}
213+
}
214+
if (found) {
215+
break;
216+
} else {
217+
set.removeAll(visitedWords);
218+
currentLevelSize = nextLevelSize;
219+
}
220+
}
221+
}
222+
223+
/**
224+
* generate all the shorted path from the DAG's inverse adjacent table.
225+
* DFS from the endWord.
226+
*
227+
* @param beginWord
228+
* @param endWord
229+
*/
230+
private void genPaths(String beginWord, String endWord, LinkedList<String> path) {
231+
List<String> adjacentComingWords = inversedAdjTable.get(endWord);
232+
for (String word : adjacentComingWords) {
233+
path.addFirst(word);
234+
if (word.equals(beginWord)) {
235+
List<String> shortestPath = new ArrayList<String>(path);
236+
shortestPaths.add(shortestPath);
237+
} else {
238+
genPaths(beginWord, word, path);
239+
}
240+
path.removeFirst();
241+
}
242+
}
243+
}
244+
245+
/**
246+
* 对{@link Solution2}的进一步改进。
247+
* Solution2的弱点是如果从startWord出发的branch factor很大,意思是说startWord可以跳到100个ladderWord,然后这100个ladderword又
248+
* 可以跳到1000个ladderword,这样需要的存储空间就很大,list的插入也是时间开销。
249+
* 但是相反如果从endWord回溯的ladderword只有3个,其实可以考虑从endword出发。
250+
* <p/>
251+
* 所以这里的优化点是双端BFS,每次从较小的branch set出发去BFS,直到两个branch set相遇,表示找到了最短路径。
252+
*/
253+
public static class Solution3 extends Solution2 {
254+
255+
/**
256+
* 用于存储最短路径的反向邻接表,key的value是所有以key为结束边的出发点的集合。
257+
* 之所以用反向邻接表存储是因为方便最后从endWord出发DFS搜寻所有的最短路径。
258+
*/
259+
private Map<String, List<String>> inverseAdjTable = new HashMap<String, List<String>>();
260+
private List<List<String>> shortestPaths = new ArrayList<List<String>>();
261+
262+
@Override
263+
public List<List<String>> findLadders(String beginWord, String endWord, Set<String> wordList) {
264+
Set<String> startSet = new HashSet<String>();
265+
startSet.add(beginWord);
266+
Set<String> endSet = new HashSet<String>();
267+
endSet.add(endWord);
268+
wordList.remove(beginWord);
269+
wordList.remove(endWord);
270+
genDAG(startSet, endSet, wordList, true);
271+
genPaths(beginWord, endWord, new ArrayList<String>());
272+
return shortestPaths;
273+
}
274+
275+
private void genDAG(Set<String> startSet, Set<String> endSet, Set<String> wordList, boolean fromBeginWord) {
276+
if (startSet.size() == 0) {
277+
return;
278+
}
279+
if (startSet.size() > endSet.size()) {
280+
genDAG(endSet, startSet, wordList, !fromBeginWord);
281+
return;
282+
}
283+
boolean met = false;
284+
Set<String> visitedWords = new HashSet<String>();
285+
for (String stepWord : startSet) {
286+
char[] chars = stepWord.toCharArray();
287+
for (int i = 0; i < chars.length; i++) {
288+
char originalChar = chars[i];
289+
for (char ch = 'a'; ch <= 'z'; ch++) {
290+
if (originalChar != ch) {
291+
chars[i] = ch;
292+
String ladderWord = new String(chars);
293+
String key = fromBeginWord ? ladderWord : stepWord;
294+
String value = fromBeginWord ? stepWord : ladderWord;
295+
if (endSet.contains(ladderWord)) {
296+
met = true;
297+
if (!inverseAdjTable.containsKey(key)) {
298+
inverseAdjTable.put(key, new ArrayList<String>());
299+
}
300+
inverseAdjTable.get(key).add(value);
301+
}
302+
if (!met && wordList.contains(ladderWord)) {
303+
visitedWords.add(ladderWord);
304+
if (!inverseAdjTable.containsKey(key)) {
305+
inverseAdjTable.put(key, new ArrayList<String>());
306+
}
307+
inverseAdjTable.get(key).add(value);
308+
}
309+
}
310+
}
311+
chars[i] = originalChar;
312+
}
313+
}
314+
if (!met) {
315+
wordList.removeAll(visitedWords);
316+
genDAG(visitedWords, endSet, wordList, fromBeginWord);
317+
}
318+
}
319+
320+
private void genPaths(String beginWord, String endWord, List<String> path) {
321+
path.add(0, endWord);
322+
if (endWord.equals(beginWord)) {
323+
shortestPaths.add(new LinkedList<String>(path));
324+
} else {
325+
List<String> words = inverseAdjTable.get(endWord);
326+
if (words != null) {
327+
for (String word : words) {
328+
genPaths(beginWord, word, path);
329+
}
330+
}
331+
}
332+
path.remove(0);
333+
}
334+
}
125335
}

src/test/java/joshua/leetcode/bfs/WordLadder2Test.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,27 @@
55
import java.util.List;
66
import java.util.Set;
77

8+
import org.junit.Before;
89
import org.junit.Test;
910

1011
import com.google.common.collect.Sets;
1112

1213
public class WordLadder2Test {
1314

15+
private WordLadder2 solution;
16+
17+
@Before
18+
public void setUp () {
19+
// solution = new WordLadder2.Solution2();
20+
solution = new WordLadder2.Solution3();
21+
}
22+
1423
@Test
1524
public void testSolution() {
1625
String beginWord = "hit";
1726
String endWord = "cog" ;
1827
Set<String> wordList = Sets.newHashSet("hot","dot","dog","lot","log");
19-
WordLadder2 solution = new WordLadder2.Solution1();
2028
List<List<String>> result = solution.findLadders(beginWord, endWord, wordList);
2129
System.out.println(result);
2230
}
23-
2431
}

0 commit comments

Comments
 (0)