Word Break II

本文探讨LeetCode上的单词拆分II问题,介绍两种解决方法:递归回溯的Top-down方式与更高效的Bottom-up递归算法,并详细解析Bottom-up算法如何通过哈希表避免重复计算,提高效率。

https://oj.leetcode.com/problems/word-break-ii/

Given a string s and a dictionary of words dict, add spaces ins to construct a sentence where each word is a valid dictionary word.

Return all such possible sentences.

For example, given
s = "catsanddog",
dict = ["cat", "cats", "and", "sand", "dog"].

A solution is ["cats and dog", "cat sand dog"].

public List<String> wordBreak(String s, Set<String> dict)


这一题的解集数目实际上是指数级的,所以解法也只能用指数级的DFS递归来做了。这一题可以有两种方法,第一种是top-down的方式。

也就是递归加back-trace的做法。这种做法是每一层进行循环,找当前匹配字典的字符串,然后构成临时答案向下一层递归传递,当递归到底的时候直接把答案放进List的解集中。

这一种做法相对废效率,因为会递归出很多无效的临时解。所以,会过不了最坏的情况。下面给出代码:

    public List<String> wordBreak(String s, Set<String> dict) {
        StringBuilder sb = new StringBuilder();
        List<String> res = new LinkedList<String>();
        if(wordBreakTest(s, dict))
            Helper(s, dict, sb, 0, res);
        return res;
    }
    public boolean wordBreakTest(String s, Set<String> dict) {
        if(s == null || s.isEmpty())
            return true;
        boolean[] dp = new boolean[s.length()];
        dp[0] = true;
        for(int i = 0; i < s.length(); i++){
            if(!dp[i])
                continue;
            if(dict.contains(s.substring(i)))
                return true;
            for(int j = i + 1; j < s.length(); j++){
                if(dict.contains(s.substring(i,j)))
                    dp[j] = true;
            }
        }
        return false;
    }
    public void Helper(String s, Set<String> dict, StringBuilder curres, int index, List<String> res){
        if(index == s.length()){
            res.add(curres.toString());
        }else{
            for(int i = index + 1; i <= s.length(); i++){
                String candidate = s.substring(index, i);
                if(dict.contains(candidate)){
                    StringBuilder next_res = new StringBuilder(curres);
                    if(i == s.length()){
                        next_res.append(candidate);
                    }else{
                        next_res.append(candidate + " ");
                    }
                    Helper(s, dict, next_res, i, res);
                }
            }
        }
    }

这一段代码的wordBreakTest实际上就是为了躲过最坏的那个情况的。

实际上这一题bottom-up是一个更好的解,因为不存在往下传递的无效解的情况,相反,自下往上收集的必然都是有效解的临时解集。做法和上面那个差不了太多,但是我们当我们每当找到一个字典里能匹配的子字符串的时候,我们不往下传,相反是往下一层要有效解集,只有当递归到最后一层的时候才会作为一个base case返回子串作为最初的有效解集,只有当下一层返回有效解集的时候这一层的有效字串才会加入子集中往上返回,当下一层返回的是一个空集(表示找不到任何有效子解集)时,当前的子串也直接在当前递归层放弃。然后继续循环匹配。同时,top-down的做法往往难以形成截枝的情况,相反bottom-up就容易得多,下面注意一下我的HashMap的用法,那就是用来截枝用的,避免重复递归结果而浪费了效率。下面就给出对应此算法的代码,大家可以比较一下:

    public List<String> wordBreak(String s, Set<String> dict) {
        return DFShelper(s, dict, new HashMap<Integer, List<String>>(), 0);
    }
    
    public List<String> DFShelper(String s, Set<String> dict, HashMap<Integer, List<String>> cached, int cur_pos){
        if(cur_pos == s.length()){
            return new LinkedList<String>();
        }else{
            List<String> cur_res = new LinkedList<String>();
            for(int i = cur_pos; i < s.length(); i++){
                String candidate = s.substring(cur_pos, i + 1);
                if(dict.contains(candidate)){
                    List<String> next_res = null;
                    if(i + 1 == s.length()){
                        cur_res.add(candidate);
                    }else{
                        if(cached.containsKey(i + 1)){
                            next_res = cached.get(i + 1);
                        }else{
                            next_res = DFShelper(s, dict, cached, i + 1);
                        }
                        for(String next_level : next_res){
                            cur_res.add(candidate + " " + next_level);
                        }
                    }
                }
            }
            cached.put(cur_pos, cur_res);
            return cur_res;
        }
    }

这种算法不需要取巧可以直接过leetcode哪怕是最坏的结果。

更新一下截枝的做法,下面这段代码稍微简洁易懂一些

    public List<String> wordBreak(String s, Set<String> wordDict) {
        HashMap<String, List<String>> cachedMap = new HashMap<String, List<String>>();
        return helper(s, wordDict, cachedMap);
    }
    
    public List<String> helper(String s, Set<String> wordDict, HashMap<String, List<String>> cachedMap){
        if(cachedMap.containsKey(s))
            return cachedMap.get(s);
        List<String> currentResult = new LinkedList<String>();
        for(int i = 0; i < s.length(); i++){
            String currentSubString = s.substring(0, i + 1);
            if(wordDict.contains(currentSubString)){
                if(i == s.length() - 1){
                    currentResult.add(currentSubString);
                }else{
                    List<String> nextlevel = helper(s.substring(i + 1), wordDict, cachedMap);
                    for(String subResult : nextlevel){
                        currentResult.add(currentSubString + " " + subResult);
                    }
                }
            }
        }
        cachedMap.put(s, currentResult);
        return currentResult;
    }

2018-01-21 Updated:

这次不update代码了,上面那个截肢的解法已经很好了。但是我认为我之前的解读是有错误的。无论是top-down还是bottom-up,都是会出现无效解的分支的。但是top-down没办法做的是截肢,bottom-up却是可以做到的。这估计就是这题为何是hard的原因。top-down是涉及不到dp的,但是bottom-up却是dp的一种初级和不常见的运用。dp的本质就是memorize subresult。但我们一般见到的dp都是通过数组来实现的,这里的所谓的dp就是通过哈希表来记录的。所以这样的截肢其实也是属于dp的一种。当然这一题也是可以通过数组,把HashMap<Integer, List<String>>换成 List<String>[] dp = new List<String>[s.length()]即可。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值