Idiot-maker

  :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

https://leetcode.com/problems/word-ladder/

Given two words (start and end), and a dictionary, find the length of shortest transformation sequence from start to end, 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.

解题思路:

先分析题意。从start到end,每次只能改一个字母,要求改完的词都在dict中。有点像dfs,我可以每次从dict里找一个词,同时维护一个visited的set,用来判断这个词是不是已经被使用过了。如果,这个词没有被使用过,而且与前一个词只相差一个字母,就可以继续拓展,否则剪枝。这样直到可以抵达end,回溯。我们将所有结果都加入一个结果集。最后算这个结果集里面最小的那个就行了。

代码如下。

public class Solution {
    public int ladderLength(String start, String end, Set<String> dict) {
        Set<String> visited = new HashSet<String>();
        visited.add(start);
        List<String> current = new ArrayList<String>();
        current.add(start);
        List<Integer> result = new ArrayList<Integer>();
        dfs(end, dict, visited, current, result);
        if(result.size() == 0) {
            return 0;
        }
        Collections.sort(result);
        return result.get(0);
    }
    
    public void dfs(String end, Set<String> dict, Set<String> visited, List<String> current, List<Integer> result) {
        if(differsOnlyOne(current.get(current.size() - 1), end)) {
            result.add(current.size() + 1);
            return;
        }
        for (String str : dict) {
            if(differsOnlyOne(current.get(current.size() - 1), str) && !visited.contains(str)) {
                current.add(str);
                visited.add(str);
                dfs(end, dict, visited, current, result);
                current.remove(current.size() - 1);
                visited.remove(str);
            }
        }
    }
    
    public boolean differsOnlyOne(String str1, String str2) {
        int differNum = 0;
        for (int i = 0; i < str1.length(); i++) {
            if(str1.charAt(i) != str2.charAt(i)) {
                differNum++;
            }
        }
        if(differNum == 1) {
            return true;
        }else {
            return false;
        }
    }
}

DFS,结果超时+内存溢出。因为本题要求的最小步数,无论如何都会感觉到brute dfs有些不对,因为肯定没必要把每个可能的解都算出来。前面提到过,一般求最优解的,可以使用动态规划,这里似乎也不行。

而且,使用上面的dfs,令dict的size为n,对于每个词,都有n个拓展的可能,时间复杂度会比较高。

看了tag,提示使用BFS。想想有道理,因为求的是最小的步数,相当于是最浅的高度,或者是最短路径!回忆一下前面经常写的BFS,几层循环相套。第一层是可能的最长路径,第二层是当前存在的解的个数,第三层循环是针对每个接继续向下拓展。

public class Solution {
    public int ladderLength(String start, String end, Set<String> dict) {
        List<List<String>> result = new ArrayList<List<String>>();
        List<String> current = new ArrayList<String>();
        current.add(start);
        result.add(current);

        int num = 1;
        for (int i = 0; i < dict.size(); i++) {
            List<List<String>> temp = new ArrayList<List<String>>();
            for (List<String> list : result) {
                if(differsOnlyOne(list.get(list.size() - 1), end)) {
                    return list.size() + 1;
                }
                for (String str : dict) {
                    List<String> temp1 = new ArrayList<String>(list);
                    if(differsOnlyOne(temp1.get(temp1.size() - 1), str) && !temp1.contains(str)) {
                        temp1.add(str);
                        temp.add(temp1);
                    }
                }
            }
            result = temp;
        }
        return 0;
    }
    
    public boolean differsOnlyOne(String str1, String str2) {
        int differNum = 0;
        for (int i = 0; i < str1.length(); i++) {
            if(str1.charAt(i) != str2.charAt(i)) {
                differNum++;
            }
        }
        if(differNum == 1) {
            return true;
        }else {
            return false;
        }
    }
}

结果换成BFS,还是超时。网上有人说,最内层的针对set的循环是非常耗时间的,如果set很大的话。不如对当前word的每位换字符,这样因为只有26个字母,word的长度也是恒定的,这个耗时就是一个常数的时间,而不会因为set过大而增长。结果,还是超时,代码就不贴了。

最后,AC的解法是下面的样子的。写出来才恍然大悟,其实就和二叉树的bfs一模一样。用了一个size的变量来记录当前已经遍历到第几层。

public class Solution {
    public int ladderLength(String start, String end, Set<String> dict) {
        Set<String> visited = new HashSet<String>();
        LinkedList<String> queue = new LinkedList<String>();
        queue.offer(start);
        visited.add(start);
        
        int level = 1;
        int size = queue.size();
        while (queue.size() > 0) {
            size = queue.size();
            while(size > 0) {
                String last = queue.poll();
                for(int j = 0; j < last.length(); j++) {
                    for(char k = 'a'; k <= 'z'; k++) {
                        String newString = last.substring(0, j) + k + last.substring(j + 1);
                        if(newString.equals(end)) {
                            return level + 1;
                        }
                        if(dict.contains(newString) && !visited.contains(newString)) {
                            visited.add(newString);
                            queue.offer(newString);
                        }
                    }
                }
                size--;
            }
            level++;
        }
        return 0;
    }
}

上面的解法是AC了,不过还有两个问题。

第一,这个visited的set是全局用的,难道不是应该每个路径一个set吗?因为逻辑上来讲,只要这条路径前面没用过这个词就行了,其他路径用过的,当前为啥排除了也能得到正确的解?我们假设start=abc,往下一层有adc、aec、afc等,再往下,adc可以变成aec,因为aec没有在abc-adc里出现过,但是必然不可能是最短解。因为abc可以直接到aec,已经比adb-adc-aec短了。

所以这个visited的set完全可以所有路径共用一个。再想想,其实这就是图的BFS方法。一个图,能直接到的点,肯定是比从其他点绕一圈,再过来要短的。所谓,不走回头路,也不要绕路。

第二,第一次写的BFS是维护了一个List<List<String>>的类型的,因为以前都是要BFS出所有结果,所以要保留前面所有的路径前驱。而这里,只要求解的深度就行了。所以其实只要像树的bfs那样,用一个queue,维护最近一次的所有前一个结点即可。前面所有的前驱放在visited这个set里就可以了。

第一点,对于问题的解决是关键,可以说大大降低了问题的时间复杂度。第二点,可以使得空间复杂度降低。

我这里尝试着,用List<List<String>>的BFS,改进visited的set,而不用queue,也是可以AC的。代码如下。

说明时间复杂度的关键,第一,在针对每个词进行拓展的时候,不要遍历整个dict,而要每个位置的每个字母进行拓展,因为可能性只有26种。第二,就是这个set。

public class Solution {
    public int ladderLength(String start, String end, Set<String> dict) {
        List<List<String>> result = new ArrayList<List<String>>();
        List<String> current = new ArrayList<String>();
        Set<String> visited = new HashSet<String>();
        visited.add(start);
        current.add(start);
        result.add(current);

        int num = 1;
        for (int i = 0; i < dict.size(); i++) {
            List<List<String>> temp = new ArrayList<List<String>>();
            for (List<String> list : result) {
                String last = list.get(list.size() - 1);
                if(differsOnlyOne(last, end)) {
                    return list.size() + 1;
                }
                for(int j = 0; j < last.length(); j++) {
                    for(char k = 'a'; k <= 'z'; k++) {
                        String newString = last.substring(0, j) + k + last.substring(j + 1);
                        List<String> temp1 = new ArrayList<String>(list);
                        if(dict.contains(newString) && !visited.contains(newString)) {
                            temp1.add(newString);
                            visited.add(newString);
                            temp.add(temp1);
                        }
                    }
                }
            }
            result = temp;
        }
        return 0;
    }
    
    public boolean differsOnlyOne(String str1, String str2) {
        int differNum = 0;
        for (int i = 0; i < str1.length(); i++) {
            if(str1.charAt(i) != str2.charAt(i)) {
                differNum++;
            }
        }
        if(differNum == 1) {
            return true;
        }else {
            return false;
        }
    }
}

 //20180930

题目本身做了一些更改,首先参数不是set而是list了,其次,如果endWord不在wordList里面,也被认为是不可以的。

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Set<String> visited = new HashSet<String>();
        Set<String> wordListSet = new HashSet<String>(wordList);
        
        if (!wordListSet.contains(endWord)) {
            return 0;
        }
        
        LinkedList<String> queue = new LinkedList<String>();
        queue.offer(beginWord);
        int level = 1;
        
        // queue里面是所有层的所有可能的结果,他们之间的层次是通过sizeForThisLevel来得知的
        while (queue.size() > 0) {
            int sizeForThisLevel = queue.size();
            while (sizeForThisLevel > 0) {
                // 对于本层的所有词(所有的候选词),尝试所有的变换
                // 所以这里的lastword要不断的poll,直到sizeForThisLevel == 0.
                String lastWord = queue.poll();
                for (int i = 0; i < lastWord.length(); i++) {
                    for (char c = 'a'; c <= 'z'; c++) {
                        String candidate = lastWord.substring(0, i) + c + lastWord.substring(i + 1, lastWord.length());
                        if (candidate.equals(endWord)) {
                            return level + 1;
                        }
                        if (!visited.contains(candidate) && wordListSet.contains(candidate)) {
                            queue.offer(candidate);
                            visited.add(candidate);                            
                        }
                    }
                }
                sizeForThisLevel--;
            }
            // sizeForThisLevel == 0 代表这里层都试完了还没有结果,进入下一层尝试。
            level++;            
        }
        return 0;
    }
}

 另一种方法

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        Set<String> visited = new HashSet<String>();
        Set<String> wordListSet = new HashSet<String>(wordList);
        
        if (!wordListSet.contains(endWord)) {
            return 0;
        }
        
        List<String> cur = new ArrayList<String>();
        List<List<String>> res = new ArrayList<List<String>>();
        cur.add(beginWord);
        visited.add(beginWord);
        res.add(cur);
        
        // 最多的变换长度就是 wordList.size()
        for (int level = 0; level < wordList.size(); level++) {
            // temp就是这一层穷举后所有可能的candidate变换list
            // 所以从空开始,然后用上一层的穷举结果res继续往下尝试
            List<List<String>> temp = new ArrayList<List<String>>();
            
            for (List<String> list : res) {
                String lastWord = list.get(list.size() - 1);  
                
                for (int i = 0; i < lastWord.length(); i++) {
                    for (char c = 'a'; c <= 'z'; c++) {
                        String candidate = lastWord.substring(0, i) + c + lastWord.substring(i + 1, lastWord.length());
                        if (candidate.equals(endWord)) {
                            return list.size() + 1;
                        }
                        if (!visited.contains(candidate) && wordListSet.contains(candidate)) {
                            List<String> cur2 = new ArrayList<String>(list);
                            cur2.add(candidate);
                            temp.add(cur2);
                            visited.add(candidate);                            
                        }
                    }

                }
            }
            // 本层的穷举结果temp取代上一层的结果res
            res = temp;
        } 
        return 0;
    }
}

 

posted on 2015-04-14 15:43  NickyYe  阅读(179)  评论(0编辑  收藏  举报