[LeetCode] - Word Break

Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

For example, given
s = "leetcode",
dict = ["leet", "code"].

Return true because "leetcode" can be segmented as "leet code".

这周对着geeks上面好几道经典的DP题研究了一下,对于DP的题目稍微找到了一点儿感觉。只要是DP问题,就一定要先找到递推关系式,这是最最关键的一步。之后便可以根据递推关系式把原问题简化为子问题进行处理,并通过使用额外的内存空间来储存计算结果,以此来消除重复计算带来的时间上的消耗,也就是所谓的空间换时间。对于空间的使用,应该说有两种主要的方法。

1. 储存在一个table中。根据递推式构建这个table,它一般是一维或者二维的。然后我们便可以按照bottom-up的顺序对table的内容进行填充。所谓bottom-up,就是从table的小的index开始,逐渐往index大的方向进行填充。题目需要的结果一般就是table中的最后一个元素的值。使用table的话,一般可以写出来iterative的程序,我觉得相对于recursive而言,稍微好理解一些。缺点就是,要对各种边界条件考虑充分并进行适当的处理。我的经验是,对于字符串类型的DP题目,table的大小往往要比字符串的长度大1,也就是说,在table中,字符串是1-based index。这样往往会方便处理一些。

2. 储存在一个cache中,然后用recursive的方式来求解。每次进行计算之前,查找cache,看看当前要计算的结果在cache里面是否存在。如果存在,便可以直接利用了。这样,就成功去掉了重复计算。对我个人而言,table+iterative的方式更容易理解一点儿,尤其是对于这道word break的题目。


第一段程序是以前写的,用的cache+recursive的方法。当然,这也是一种DFS。依次取当前字符串的每一个prefix,如果它在dict中,那么检查cache,看看字符串相对于当前prefix而言的suffix是否在cache之中,如果不在,对其进行计算,然后将结果加入到cache之中。如果结果为true,那么证明这个word是可以被break的,返回true。若为false,则证明当前的拆分不可行,继续构建下一个prefix然后进行同样步骤的检查。这样可以保证在整个过程中消除重复的计算。

public class Solution {
    public boolean wordBreak(String s, Set<String> dict) {
        if(s==null) return false;
        HashMap<String, Boolean> cache = new HashMap<String, Boolean>();
        return wordBreakHelper(s, dict, cache);
    }
    
    public boolean wordBreakHelper(String s, Set<String> dict, HashMap<String, Boolean> cache) {
        if(s.length()==0) return true;
        for(int i=1; i<=s.length(); i++) {
            String firstHalf=s.substring(0, i), secondHalf=s.substring(i, s.length());
            if(dict.contains(firstHalf)) {
                if(!cache.containsKey(secondHalf)) {
                    cache.put(secondHalf, wordBreakHelper(secondHalf, dict, cache));
                }
                if(cache.get(secondHalf)) return true;
            }
        }
        return false;
    }
}


第二段程序是这周写的,用的是table+iterative的方法。我构建了一个一维的table,word[s.length()+1],它存储boolean值。所以,word[i]就很自然的用来表示string(1, i)是否可以被break。注意,需要将word[0]初始化为true,因为假设空字符串是可以被break的,后面对table的构建也需要用到这个假设。

好了,有了这个table,接下来可以用bottom-up的方式对它进行填充了。假设word是1-based index。所以for循环从1开始一直到word.length()结束。对于每一个位置i,依次取每一个字串word(j, i),注意j是从0开始而且是要小于i的。如果发现了一个字串,它可以是的word[j]为true,且同时是的word(j, i)在dict中,那么word[i]就为true,否则为false。这里对于i,j的选取和substring函数的使用稍微有点儿绕,要小心。将table从前到后走一遍之后,最终的结果将被保存在table的最后一个元素之中,return它即可。

我个人认为,较之第一种方法,第二种方法更好理解一点儿,代码也更清晰一些。当然了,都会肯定是最好的。

public class Solution {
    public boolean wordBreak(String s, Set<String> dict) {
        if(s==null) return false;
        
        // word[i] -> is s(1, i) breakable?
        boolean[] word = new boolean[s.length()+1];     // fault as default
        word[0] = true;
        for(int i=1; i<=s.length(); i++) {
            for(int j=0; j<i; j++) {
                if(word[j] && dict.contains(s.substring(j, i))) {
                    word[i] = true;
                    break;
                }
            }
        }
        return word[s.length()];
    }
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章