Labels

Monday, January 26, 2015

Word Ladder II


Word Ladder II



 


Given two words (start and end), and a dictionary, find all shortest transformation sequence(s) 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"]

Return

  [
    ["hit","hot","dot","dog","cog"],
    ["hit","hot","lot","log","cog"]
  ]
Note:

  • All words have the same length.
  • All words contain only lowercase alphabetic characters.

Naive Way:This question is hard. I tried at least 100 times but only get one of my solution passed the OJ in 1800+ms. Hard problem can always classify people, so I need to pay more attention to this question. In  word-ladder , I use a BFS approach, which is easy and quick to generate. What is different this time is that whether BFS or DFS, we need to mark each node that is visited. Consider this case:

start = 'red'
end = 'tax'
dict = ['ted', rad', 'tad']

since we cannot use a node twice, whether BFS or DFS will give us
either red->ted->tad->tax
or red->rad->tad->tax
because 'tad' is a common word in two paths.

A DFS with back-tracing is able to deal with that. But only DFS cannot ensure minimum steps. Thus, I apply level-order BFS first to put every word in a List<Set<String>> layer structure container, with the size of outer list equal to the minimum step. And apply DFS on this container to get each path. This is my first solution that get accepted. It takes 1400+ ms, while the average run time for this question is around 700ms.

 public class Solution {  
   public List<List<String>> findLadders(String start, String end, Set<String> dict) {  
     List<List<String>> rslt = new ArrayList<List<String>>();  
     List<Set<String>> tree = new ArrayList<Set<String>>();  
     boolean found = false;  
       
     // initialize first layer of the tree  
     Set<String> first_layer = new HashSet<String>();  
     first_layer.add(start);  
     if(dict.contains(start)) dict.remove(start);  
     tree.add(first_layer);  
       
     // add end to dictionary  
     dict.add(end);  
       
     // level-order traversal to construct the tree  
     while(!found && tree.get(tree.size()-1).size()!=0){  
       Set<String> new_layer = new HashSet<String>();  
       Set<String> cur_layer = tree.get(tree.size()-1);  
       Iterator<String> iter = cur_layer.iterator();  
       while(iter.hasNext()){  
         String s = iter.next();  
         char[] chars = s.toCharArray();  
         for(int j = 0;j < s.length();j++){  
           char original = chars[j];  
           for(char c = 'a';c <= 'z';c++){  
             chars[j] = c;  
             String t = new String(chars);  
             if(t.equals(end)) found = true;  
             if(dict.contains(t)){  
               if(!t.equals(end)) dict.remove(t);  
               new_layer.add(t);  
             }  
           }  
           chars[j] = original;  
         }  
       }  
       tree.add(new_layer);  
     }  
       
     // dfs to construct paths  
     Stack<String> path = new Stack<String>();  
     path.push(start);  
     dfs(start, end, 1, path, tree, rslt);  
       
     return rslt;  
   }  
     
   private void dfs(String s, String end, int index, Stack<String> path, List<Set<String>> tree, List<List<String>> rslt){  
     if(s.equals(end)){  
       List<String> validPath = new ArrayList<String>();  
       validPath.addAll(path);  
       rslt.add(validPath);  
       return;  
     }  
     if(index >= tree.size()) return;  
     Set<String> set = tree.get(index);  
     char[] chars = s.toCharArray();  
     for(int j = 0;j < s.length();j++){  
       char original = chars[j];  
       for(char c = 'a';c <= 'z';c++){  
         chars[j] = c;  
         String t = new String(chars);  
         if(set.contains(t)){  
           path.push(t);  
           dfs(t, end, index+1, path, tree, rslt);  
           path.pop();  
         }  
       }  
       chars[j] = original;  
     }  
   }  
 }  

Also, it is after several observation of others' code, I found that when listing the neighbors of a particular word, first convert it to char[] array and then replace a char instead of doing s.substring(0,i)+c+s.substring(i+1,s.length()) will save much time. It is probably because doing substring is initializing a new String each time, which is costly.

After seeing this post https://oj.leetcode.com/discuss/21902/java-solution-with-iteration on Discuss, I realized that DFS is not necessary. If I do a level-order traversal, delete the whole level from dict before traversal next level, I can efficiently deal with the case where a word is shared by to paths. Because a word shared by two paths must be at same location.

I give it a second trial using only BFS. And the code get accepted in 1000+ms.

 public class Solution {  
   public List<List<String>> findLadders(String start, String end, Set<String> dict) {  
     List<List<String>> rslt = new ArrayList<List<String>>();  
     Deque<List<String>> paths = new LinkedList<List<String>>();  
     boolean found = false;  
       
     // initialize path  
     List<String> path = new ArrayList<String>();  
     path.add(start);  
     paths.offerLast(path);  
       
     // add end to dictionary, remove start from dict  
     dict.add(end);  
     if(dict.contains(start)) dict.remove(start);  
       
     // BFS  
     while(!found && !paths.isEmpty()){  
       Set<String> set = new HashSet<String>();  
       int k = paths.size();  
       for(int i = 0;i < k;i++){  
         List<String> list = paths.pollFirst();  
         String s = list.get(list.size()-1);  
         for(String t : neighbors(s, dict)){  
           set.add(t);  
           List<String> newList = new ArrayList<String>(list);  
           newList.add(t);  
           paths.offerLast(newList);  
           if(t.equals(end)){  
             found = true;  
             rslt.add(newList);  
           }  
         }  
       }  
       dict.removeAll(set);  
     }  
     return rslt;  
   }  
     
   private List<String> neighbors(String s, Set<String> dict){  
     List<String> list = new ArrayList<String>();  
     char[] chars = s.toCharArray();  
     for(int j = 0;j < s.length();j++){  
       char original = chars[j];  
       for(char c = 'a';c <= 'z';c++){  
         chars[j] = c;  
         String t = new String(chars);  
         if(dict.contains(t)) list.add(t);  
       }  
       chars[j] = original;  
     }  
     return list;  
   }  
 }  

Improved Way: That is not enough. The highest run time distribution is around 700ms. I am far away from that yet. I looked into several posts about Word Ladder II. This two I found most helpful.
https://oj.leetcode.com/discuss/25970/java-modified-bfs-to-find-end-followed-dfs-reconstruct-paths
and http://yucoding.blogspot.com/2014/01/leetcode-question-word-ladder-ii.html (C++).

I found that the common point of their methods is to store the parents for each string instead of what is more straightforward, the children of each string. This reason for doing this is probably because storing the parents is using a lot less space than storing the children. (I tried storing children instead f parents, got MLE). Just Considering each word could have length * 26 at most children, while it will always have less than length*26 parents, since its neighbors selected  by its parent cannot become its parent.

To put it simple, parent selects children, only when two parents are alike, they can select same children. Thus, given each word, the size of its parents is much more less than the size of its children.

The following code applies mapping a word to its parents and got accept in 600+ ms. I keep the finding neighbor function and adding a dfs to find paths based on the parent relationship map.

 public class Solution {  
   public List<List<String>> findLadders(String start, String end, Set<String> dict) {  
     List<List<String>> rslt = new ArrayList<List<String>>();  
     Map<String, List<String>> parents = new HashMap<String, List<String>>();  
     boolean found = false;  
       
     // initialize  
     Set<String> cur_layer = new HashSet<String>();  
     cur_layer.add(start);  
     if(dict.contains(start)) dict.remove(start);  
     dict.add(end);  
       
     // BFS construct map  
     while(!found && !cur_layer.isEmpty()){  
       Set<String> new_layer = new HashSet<String>();  
       Iterator<String> iter = cur_layer.iterator();  
       while(iter.hasNext()){  
         String s = iter.next();  
         for(String t: neighbors(s, dict)){  
              new_layer.add(t);  
             if(!parents.containsKey(t)){  
               List<String> list = new ArrayList<String>();  
               list.add(s);  
               parents.put(t,list);  
             }else{  
               List<String> list = parents.get(t);  
               list.add(s);  
             }  
             if(t.equals(end)) found = true;  
         }  
       }  
       dict.removeAll(new_layer);  
       cur_layer = new_layer;  
     }  
       
     // DFS construct paths  
     Stack<String> path = new Stack<String>();  
     path.push(end);  
     dfs(start, end, path, parents, rslt);  
       
     return rslt;  
   }  
     
   private void dfs(String start, String s, Stack<String> path, Map<String, List<String>> parents, List<List<String>> rslt){  
        // base case  
     if(s.equals(start)){  
       List<String> list = new ArrayList<String>();  
       list.addAll(path);  
       Collections.reverse(list);  
       rslt.add(list);  
       return;  
     }  
     // edge case  
        if(!parents.containsKey(s)) return;  
     // recursion  
     for(String t: parents.get(s)){  
       path.push(t);  
       dfs(start, t, path, parents, rslt);  
       path.pop();  
     }  
   }  
     
   private List<String> neighbors(String s, Set<String> dict){   
     List<String> list = new ArrayList<String>();   
     char[] chars = s.toCharArray();   
     for(int j = 0;j < s.length();j++){   
       char original = chars[j];   
       for(char c = 'a';c <= 'z';c++){   
         chars[j] = c;   
         String t = new String(chars);   
         if(!t.equals(s) && dict.contains(t)) list.add(t);   
       }   
       chars[j] = original;   
     }   
     return list;   
   }   
 }  

 

Word Ladder


 



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.

Naive way: 第一想法是BFS。但这题好像是Dijkstra算法 的典型。仔细想了想,BFS是每一次将与目标为邻的word 压进队列,最多有 n个邻居,(n是字典的大小)。那么BFS的算法复杂度就是O(n^2),如果考虑到是对字符的处理,最多有26种变化,每个位置都替换一次是O(k), (k是word的长度),那么算法复杂度就是O(nk).一般情况下,O(k) << O(n),而且,替换单个字符去主动匹配 比 对字典中每一个word进行遍历 要高效的多。两种的space complexity都是O(n)。

那么Dijkstra呢。我们首先需要构建一个图,保存整个字典的信息,这首先需要O(n)空间,然后构建图需要O(n^2)的遍历。运行Dijikstra算法需要O(n^2)的run time。

最关键的是这道题只要求输出步长,不要求中间节点的信息,所以BFS更优。后面有个这道题的变形要求输出每一步的word,可能Dijkstra会更实用。最重要的一点是,这里每一个节点之间的距离都是1,用Dijkstra最后会变成BFS。


// 用了一个Map去存层数,并使用原字典是Set的特性,每次遍历过的直接从字典中删除。

**我曾经想过会不会出现这种情况**
hit->hot->...-> cog
cot->hot->..-> cog
如果一开始hot在hit后面遍历掉被删除出字典,等到遍历cot的邻居就没有hit了。这种情况写出来就很明了。
Because                         layer(hit) <= layer(cot)
And       steps(hot to cog | start from hit) = step(hot to cog | start from cot)
Thus      layer(cog | going through hit) <= layer(cog | going through cot).



public class Solution {
    public int ladderLength(String start, String end, Set<String> dict) {
        // BFS
        Map<String, Integer> layers = new HashMap<String, Integer>();
        Queue<String> queue = new LinkedList<String>();
        queue.add(start);
        layers.put(start,1);
        while(!queue.isEmpty()){
            String s = queue.poll();
            int layer = layers.get(s);
            for(int i = 0;i < s.length();i++){
                for(char c = 'a'; c <= 'z';c++){
                    String temp = s.substring(0,i) + c + s.substring(i+1,s.length());
                    if(temp.equals(end))
                        return layer+1;
                    if(dict.contains(temp)){
                        queue.add(temp);
                        layers.put(temp,layer+1);
                        dict.remove(temp);
                    }
                }
            }
        }
        return 0;
    }
}

 

Search in Rotated Sorted Array


Search in Rotated Sorted Array



 


Suppose a sorted array is rotated at some pivot unknown to you beforehand.
(i.e., 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2).
You are given a target value to search. If found in the array return its index, otherwise return -1.
You may assume no duplicate exists in the array.

Naive Way: 这是我算法课期中考试的一道原题,日,当时我没做出来。在一个sorted array里查找Binary search是O(logn), 那么它如果rotated了一下还能否O(logn)呢。(关于binary search做法的题目,这章文章最下面有链接。)画一张图就很清楚了,有以下两种情况:


示例图
  1.  当处于第一种情况时, target 介于[b1,e1)之间,对前半部分进行search, target <= e2 和 target >= b2 ,对后半分布进行search. 注意到e1, b2是连着的,e2,b1也是连着的。
  2. 当处于第二种情况时, target介于(b2, e2]之间, 对后半部分search,否则search 前半部分。
      如何区分这两种情况,可以比较e1和e2,也可以比较b1和b2。


public class Solution {
    public int search(int[] A, int target) {
        return search(A,0,A.length-1,target);
    }
    
    private int search(int[] A, int begin, int end, int target){
        if(begin > end)
            return -1;
        int middle = (begin+end)/2;
        if(A[middle]==target)
            return middle;
        if(A[middle] > A[end]){
            if(A[middle] > target && A[begin] <= target)
                return search(A, begin, middle-1, target);
            else
                return search(A, middle+1, end, target);
        }else{
            if(A[middle] < target && A[end] >= target)
                return search(A, middle+1, end, target);
            else
                return search(A, begin, middle-1, target);
        }
    }
}

关于binary search的题目还有Search-in-rotated-sorted-array-ii, Search-insert-position

 

Add Binary


Add Binary



Given two binary strings, return their sum (also a binary string).
For example,
a = "11"
b = "1"
Return "100".

Naive Way: 直观的感受就是直接加,像列竖式一样,用一个变量记录进位。唯一的区别可能就是是否使用extra space. 如果说用原来的某一个String作载体承接输出String, 看似没有使用extra space,但由于在Java中String 是immutable的(什么是immutable),对String的某一位置的改动都必须重新创建一个新的String来承接,所以就算用原有的String也无法做到no extra space。我做的时候考虑了一下StringBuilder.insert()好像也要O(n),就先用一个Stack存下字符,最后一并推出,但实际上和用StringBuilder.insert()的实际时间比没有任何什么差别。

之所以不用String 而用StringBuilder是因为String每次改动都要创建一个新的String对象,这样就需要很多的extra space。


// 这是我第二次写的
public class Solution {
    public String addBinary(String a, String b) {
        Stack<Character> stack = new Stack<Character>();
        StringBuilder s = new StringBuilder();
        if(a.length() < b.length())
            return addBinary(b,a);
        int carry = 0;
        int j = a.length()-1,i = b.length()-1;
        while(j >= 0 && i >= 0){
            carry += a.charAt(j--)=='1'?1:0;
            carry += b.charAt(i--)=='1'?1:0;
            stack.push((char)(carry%2 + '0'));
            carry /= 2;
        }
        while(j >= 0){
            carry += a.charAt(j--)=='1'?1:0;
            stack.push((char)(carry%2 + '0'));
            carry /= 2;
        }
        if(carry==1)
            stack.push('1');
        while(!stack.isEmpty()){
            s.append(stack.pop());
        }
        return s.toString();
    }
}


// 这是我第一次写的,用了一个reverse()的方法
public String addBinary(String a, String b) {
            boolean forward =false;
            String rslt = "";
            int i = a.length()-1;
            int j = b.length()-1;
            while(i >= 0 && j >= 0){
                if(a.charAt(i) == '1' && b.charAt(j) == '1' && forward){
                    rslt += '1';
                    forward = true;
                }else if(((a.charAt(i) == '1' || b.charAt(j) == '1') && forward) || (a.charAt(i) == '1' && b.charAt(j) == '1')){
                    rslt += '0';
                    forward = true;
                }else if(a.charAt(i) == '1' || b.charAt(j) == '1' || forward){
                    rslt += '1';
                    forward = false;
                }else{
                    rslt += '0';
                    forward = false;
                }
                i--;
                j--;
            }
            while(i >= 0){
                if(a.charAt(i) == '1' && forward){
                    rslt += '0';
                    forward = true;
                }else{
                    rslt += (a.charAt(i) == '1'||forward)?'1':'0';
                    forward = false;
                }
                i--;
            }
            while(j >= 0){
                if(b.charAt(j) == '1' && forward){
                    rslt += '0';
                    forward = true;
                }else{
                    rslt += (b.charAt(j) == '1'||forward)?'1':'0';
                    forward = false;
                }
                j--;
            }
            if(forward){rslt += '1';}
           
            // reverse the rslt
            String reverse = new StringBuffer(rslt).reverse().toString();
            return reverse;
        }

Remove Duplicates from Sorted Array


 



Remove Duplicates from Sorted Array



 


Given a sorted array, remove the duplicates in place such that each element appear only once and return the new length.
Do not allocate extra space for another array, you must do this in place with constant memory.
For example,
Given input array A = [1,1,2],
Your function should return length = 2, and A is now [1,2].


Naive Way:直观的可以在遍历的时候keep一个同样大小的数组来记录每个数是否出现过,然后第二次遍历将数进行移动, 或者说再简历一个新数组。这样的做法有点不动脑。应该算是最naive的方法。但是直观觉得很可行。

Improved Way:题目既然要求了constant memory,那么这是一个big hint,说明我们可以不借助多余数组完成remove。比较直觉的是这种在数组上处理数据的,应该可以通过keep 指针来帮助记录关键位置来实现。文章最后有其他类似题目的链接。

//这是我第二次做的代码,两个指针记录有效的部分和下一个可能的数的位置。

    public int removeDuplicates(int[] A) {
        int p1 = 0;
        int p2 = 0;
        int begin, end;
        if(A.length==0){return 0;}
        // points p2 to a next element;
        while(p2 < A.length){
            if(A[p1]!=A[p2])
                break;
            p2++;
        }
        end = p2;
       
        // set p1 ~ end
        begin = p1+1;
        while(begin < end && p2 < A.length){
            if(A[begin]==A[p1]){
                A[begin] = A[p2];
                // shift p2
                int temp = A[p2];
                while(p2 < A.length){
                    if(A[p2] != temp)
                        break;
                    p2++;
                }
            }else{
                p1 = begin;
            }
            begin++;
        }
       
        // set the rest according to p2
        while(p2 < A.length){
            A[begin++] = A[p2];
            int temp = A[p2];
            while(p2 < A.length){
                if(A[p2] != temp)
                    break;
                p2++;
            }
        }
       
        return begin;
    }



//这是我第一次写得代码,思想更简单,坚信两个指针足矣。(居然如此简洁,难道在后退,日)

public int removeDuplicates(int[] A) {
        if(A.length == 0){return 0;}
        int i, j;
        i = 1;
        j = 0;
        while(i < A.length){
            if(A[i] == A[j]){
                i++;
            }else{
                A[++j]=A[i++];
            }
        }
        return j+1;
    }

类似的要求在constant space的情况下完成对数组的处理的题目还有



 

Max Points on a Line


Max Points on a Line


Given n points on a 2D plane, find the maximum number of points that lie on the same straight line.



Naive Way: 每两个点确定一条直线 ,每确定一条直线遍历所有点,每次用经过的点个数和最大值作比较。这样的话就需要O(n^3) run time。其中,有直线平行于x-axis或y-axis,两个点重叠的edge case可能会造成遗漏。并且考虑到k和b是小数可能带来误差,比较时采用取差值在一定小的范围内的方法。


/**
 * Definition for a point.
 * class Point {
 *     int x;
 *     int y;
 *     Point() { x = 0; y = 0; }
 *     Point(int a, int b) { x = a; y = b; }
 * }
 */

public class Solution {
    static final double zero = 0.0001;
    public int maxPoints(Point[] points) {
    // O(n^3)
        int max = 1;
        for(int i = 0;i < points.length-1;i++){
            for(int j = i+1;j < points.length;j++){
                int count = 0;
                // case 1: no y parallel
                if(points[i].x!=points[j].x){
                    double k = (points[i].y - points[j].y)/(double)(points[i].x-points[j].x);
                    double b = (double)points[i].y - k*points[i].x;
                    for(int t = 0;t < points.length;t++){
                        if(Math.abs(k*points[t].x+b-points[t].y) < zero)
                            count++;
                    }
                    max = Math.max(max, count);
                }else{ // case 2: parallel to y axis
                    for(int t = 0;t < points.length;t++){
                        if(points[t].x == points[i].x)
                            count++;
                    }
                    max = Math.max(max, count);
                }
            }
        }
        return points.length==0?0:max;
    }
}

Improved Way: 由于最坏的情况是没有三个点在同一条直线上,那么至少需要运行O(n^2)次来遍历所有可能的直线,所以最低的run time应该也要O(n^2)。 一个比较直观的感觉就是如果可以记录每一条直线,存入一个Map中,每次得到新的直线都先看Map中是否已有,有就增加Map的value,没有就加入新的直线,该法基于Map可以O(1)的读存, 使run time 提升到O(n^2),但同时也带来了O(n^2)的extra space.

但其实这种方法有难度,难在如何记录一条直线上。我自己想到的方法是构建一个新类表示一条线。采用两个参数 k 和 b, 分别表示 y = kx+b中中的两个参数。这样会带来一个问题,表示x = c时会无法表示。可以多设立一个c参数和一个boolean值来区分是否是平行于x-axis。

这里写的时候才发现了另一个问题,同样参数的两个新类,Map不会设别成同一个Key,必须得自己重写equals函数和hashCode函数,具体我是看了一个教程教你如何用Java写Equal函数 How to Write a Equality Method in Java,很有用, 具体自己的hashCode函数是rounding的,肯定很有误差,但是估计leetcode的样本很少,还是能通过。




public class Solution {
        static final double zero = 0.0001;
        public int maxPoints(Point[] points) { 
            // O(n^2)
            int max = 1;
        Map<Line, Set<Integer>> map = new HashMap<Line, Set<Integer>>();
        for(int i = 0; i < points.length-1;i++){
            for(int j = i+1; j < points.length;j++){
                if(points[i].x==points[j].x){
                    Line line = new Line(points[i].x);
                    if(!map.containsKey(line)){
                        Set<Integer> set = new HashSet<Integer>();
                        set.add(i);
                        set.add(j);
                        map.put(line, set);
                    }else{
                        Set<Integer> set = map.get(line);
                        set.add(i);
                        set.add(j);
                        map.put(line, set);
                    }
                }else{
                    double k = (points[i].y - points[j].y)/(double)(points[i].x-points[j].x);
                    double b = (double)points[i].y - k*points[i].x;
                    Line line = new Line(k,b);
                    if(!map.containsKey(line)){
                        Set<Integer> set = new HashSet<Integer>();
                        set.add(i);
                        set.add(j);
                        map.put(line, set);
                    }else{
                        Set<Integer> set = map.get(line);
                        set.add(i);
                        set.add(j);
                        map.put(line, set);
                    }
                }
            }
        }
        for(Map.Entry<Line, Set<Integer>> entry: map.entrySet())
            max = Math.max(max, entry.getValue().size());
        return points.length==0?0:max;
        }
       
        class Line{
            double k;
            double b;
            boolean xp;
            int xValue;
            Line(double m, double n){
                k = m;
                b = n;
                xp = false;
                xValue = 0;
            }
            Line(int v){
                k = Integer.MAX_VALUE;
                b = 0;
                xp = true;
                xValue = v;
            }
            @Override
            public int hashCode(){
                return (int)Math.round(k) << 16|(int)Math.round(b) + xValue;
            }
            @Override
            public boolean equals(Object o){
                if(o == this)
                    return true;
                if(!(o instanceof Line))
                    return false;
                Line l = (Line)o;
                if(xp && l.xp)
                    return xValue==l.xValue;
                else
                    return Math.abs(k-l.k) < zero && Math.abs(b-l.b) < zero;
            }
        }
    }



Other Ways: 在网上看到一个很好的方法,是采用两个Map和找最大公约数。对于一条直线y=kx+b, 每次得到k和b通过求他们最大公约数可以把他们化成最小量值,这样再次出现一样斜率的直线,就可以找相同的k,那么第一个map就是 k->b 的map。每一个斜率对应不同偏量。 第二个map就是 b->#points 的map, 每个确定的直线对应点的个数,最后就是一个Map<Integer, Map<Integer, Integer>>的形式,在这里,大家可以去看一看具体的code,写得好呀。