|
2 | 2 |
|
3 | 3 | package joshua.leetcode.bfs;
|
4 | 4 |
|
5 |
| -import java.util.ArrayList; |
6 |
| -import java.util.LinkedList; |
7 |
| -import java.util.List; |
8 |
| -import java.util.Set; |
| 5 | +import java.util.*; |
9 | 6 |
|
10 | 7 | /**
|
11 | 8 | * 126. Word Ladder II</br>
|
@@ -53,7 +50,10 @@ public abstract class WordLadder2 {
|
53 | 50 | public static class Solution1 extends WordLadder2 {
|
54 | 51 |
|
55 | 52 | /**
|
56 |
| - * BFS算法,但是需要对每个节点保存已走过的路径。 |
| 53 | + * 最基本的BFS算法 |
| 54 | + * <p/> |
| 55 | + * 缺点:需要对每个节点保存已走过的路径,空间复杂度取决于branch factor,即每个节点的分支数量。 |
| 56 | + * 由于需要复制路径,并且会遇到branch factor过大的情况,所有会Time Limit Exceeded。 |
57 | 57 | *
|
58 | 58 | * @param beginWord
|
59 | 59 | * @param endWord
|
@@ -122,4 +122,214 @@ public QueueItem(String word, List<String> path) {
|
122 | 122 | }
|
123 | 123 | }
|
124 | 124 | }
|
| 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 | + } |
125 | 335 | }
|
0 commit comments