[LeetCode#127]Word Ladder

Problem:

Given two words (beginWord and endWord), and a dictionary, find the length of shortest transformation sequence from beginWord to endWord, such that:

  1. Only one letter can be changed at a time
  2. Each intermediate word must exist in the dictionary

For example,

Given:
start = "hit"
end = "cog"
dict = ["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.

Analysis:

This problem is no-trival, but it would help you throughly understand the difference between BFS and DFS.
Typically, it is easy to come up with the solution of DFS, since once your design the recursive function well, the problem is not hard to implement.
But for this case, DFS is actually not a good way to solve this problem, it would need to traverse all pathes until we could confidently calculate the shortest path. 

For a problem, If you have to traverse all all possible pathes, you could use DFS. However, if just certain part of the path, you may try to come up with another solution.

Let us analyze the DFS solution at first.

Solution 1:

public int ladderLength(String beginWord, String endWord, Set<String> wordDict) {
        String start = beginWord;
        String end = endWord;
        Set<String> dict = wordDict;
        List<List<String>> ret = new ArrayList<List<String>> ();
        if (start == null || end == null || dict == null)
            throw new IllegalArgumentException("The passed in arguments is illegal");
        ArrayList<String> path = new ArrayList<String> ();
        ArrayList<Integer> min = new ArrayList<Integer> ();
        min.add(Integer.MAX_VALUE);
        HashSet<String> visited = new HashSet<String> (); 
        visited.add(start);
        path.add(start);
        findPath(start, end, dict, path, visited, min);
        return min.get(0);
    }
    
    private void findPath(String cur_str, String end, Set<String> dict, ArrayList<String> path, Set<String> visited, ArrayList<Integer> min) {
        if (cur_str.equals(end)) {
            if (path.size() < min.get(0))
                min.set(0, path.size());
            return;
        }
        for (int pos = 0; pos < end.length(); pos++) {
            char[] cur_array = cur_str.toCharArray();
            for (int i = 0; i < 26; i++) {
                cur_array[pos] = (char)('a' + i);
                String new_str = new String(cur_array);
                if (dict.contains(new_str) && !visited.contains(new_str) && path.size() <= min.get(0) - 1) {
                    visited.add(new_str);
                    path.add(new_str);
                    findPath(new_str, end, dict, path, visited, min);
                    path.remove(path.size()-1);
                    visited.remove(new_str);
                }
            }
        }
    }

Improvement Analysis:

The code structure is easy. But you should make sure that:
At each word, we have 26 * word_len possible children to follow, rather than 26 charcters to try for next index.
findPath(new_str, index, end, dict, path, visited, min). 
The above recursive call format is wrong. When you try to use DFS to solve a problem, we must analyz it clearly in a graph, understand the out degree clearly. (the structure of the graph clearly)

for (int pos = 0; pos < end.length(); pos++) {
    char[] cur_array = cur_str.toCharArray();
    for (int i = 0; i < 26; i++) {
        cur_array[pos] = (char)('a' + i);
        String new_str = new String(cur_array);
       ...
    }
}

What's more, since this graph include circle(which does not appear in a tree), we must avoid the circle and use a visited array(for a certain path). 
if (dict.contains(new_str) && !visited.contains(new_str) && path.size() <= min.get(0) - 1) {
    visited.add(new_str);
    path.add(new_str);
    findPath(new_str, end, dict, path, visited, min);
    path.remove(path.size()-1);
    visited.remove(new_str);
}
Note: when a search path shorter than the current minimum path, we shoud should cut-off that search branch. 
if (path.size() <= min.get(0) - 1)

------------------------------------------------------------------------------------------------------------
Even the above solution is workable, it is still too complex to solve this problem. Actually, in a graph, BFS search can always find the shortest path from a start point to a target point. (The first time to find the target point, the related path is the shortest path to reach the target point.) 
Cause the shortest path must be the first valid path, we could tag every new nodes we have encountered during the search process, it could greatly reduce the search space. 

Basic idea:
We use a queue for the search process, once we found out a new node, rather than search along the the new direction, we just enqueue it into the queue, and dequeue it later. Once we encounter the target word, we return the target word's depth back. The process is just like level-traverse a binary tree. The difference are:
1. the out-degree of each node is 26*word_len, but most of them would be cut-off through dict.
2. there could be a circle in the graph, we must use a visited hashset to avoid circle search. 

The idea is simple:
use cur_num, next_num, and level properly. For this part, we could refer the analysis for level traverse. 

The most dangerous part at this problem is to properly avoid the repeately search during the search process.
Principle: Once a word was tagged as used, every search path reach it after that time could be cut-off, since we are only care about the shortest depth. 
A error:
------------------------------------------------------------
String cur_word = queue.poll();
....
visisted.add(cur_word);
if (wordDict.contains(temp) && !visited.contains(temp)) {
        queue.offer(temp);
        next_num++;
}
---------------------------------------------------------------
This is very typical error in using BFS, tage a word when it was dequeued from queue. It is right, but could exceed time limit.
Case:
start: hot
target: dog
dict: [hot, hog, hof, dog]
        hot
    hof     hog
hog             dog
step 1: "hot" was dequeued, "hot" was tagged as visited
step 2: "hof" and "hog" were enqueued 
step 3: "hof" was dequeued, "hof" was tagged,
step 4, "hog" was enqueued. 
(repeat happens, "hog" has already been enqueued at step2, but since it has not been dequeued, it was not tagged as "visited")

To avoid this sitution, the guidance is:
Reference:https://en.wikipedia.org/wiki/Breadth-first_search

This non-recursive implementation is similar to the non-recursive implementation of depth-first search, but differs from it in two ways:
1. it uses a queue instead of a stack and
2. it checks whether a vertex has been discovered before enqueueing the vertex rather than delaying this check until the vertex is dequeued from the queue.

The fix method for this problem is:
visited.add(start);
...
if (wordDict.contains(temp) && !visited.contains(temp)) {
    queue.offer(temp);
    next_num++;
    visited.add(temp);
}

Solution 2:

public class Solution {
    public int ladderLength(String beginWord, String endWord, Set<String> wordDict) {
        if (beginWord == null || endWord == null || wordDict == null || beginWord.length() != endWord.length())
            throw new IllegalArgumentException("The passed in arguments in illegal!");
        Queue<String> queue = new LinkedList<String> ();
        HashSet<String> visited = new HashSet<String> ();
        queue.offer(beginWord);
        visited.add(beginWord);
        int cur_num = 1;
        int next_num = 0;
        int level = 1;
        int word_len = beginWord.length();
        while (!queue.isEmpty()){
            String cur_word = queue.poll();
            cur_num--;
            char[] char_array = cur_word.toCharArray();
            for (int i = 0; i < word_len; i++) {
                char copy = char_array[i];
                for (char c = 'a'; c <= 'z'; c++) {
                    char_array[i] = c;
                    String temp = new String(char_array);
                    if (temp.equals(endWord))
                        return level+1;
                    if (wordDict.contains(temp) && !visited.contains(temp)) {
                        queue.offer(temp);
                        next_num++;
                        visited.add(temp);
                    }
                }
                char_array[i] = copy;
            }
            if (cur_num == 0) {
                cur_num = next_num;
                next_num = 0;
                level++;
            }
        }
        return 0;
    }
}

 

posted @ 2015-08-30 11:03  airforce  阅读(191)  评论(0编辑  收藏  举报