(Java) LeetCode 139. Word Break —— 单词拆分

Posted tengdai

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(Java) LeetCode 139. Word Break —— 单词拆分相关的知识,希望对你有一定的参考价值。

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, determine if s can be segmented into a space-separated sequence of one or more dictionary words.

Note:

  • The same word in the dictionary may be reused multiple times in the segmentation.
  • You may assume the dictionary does not contain duplicate words.

Example 1:

Input: s = "leetcode", wordDict = ["leet", "code"]
Output: true
Explanation: Return true because "leetcode" can be segmented as "leet code".  

Example 2:

Input: s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
Explanation: Return true because "applepenapple" can be segmented as "apple pen apple".
             Note that you are allowed to reuse a dictionary word.

Example 3:

Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
Output: false

 

解法一:

但凡是能把问题规模缩小的都应该想到用动态规划求解。例如本题,如果我知道给定字符串的0到i子串可以用字典中的单词表达,那么我只需要知道i+1到末尾的子串能否被字典表达即可知道整个字符串能否被字典表达。所以随着i的增大,问题规模逐渐的缩小,且之前求解过的结果可以为接下来的求解提供帮助,这就是动态规划了。设dp[i]代表s.substring(0, i)能否被字典表达,此刻我们知道dp[0]~dp[i-1]的结果。而dp[i]的结果由两部分组成,一部分是dp[j](j < i),已知;另一部分是j到i之间的字符串是不是在字典里。当这两个部分都为真的时候,dp[i]即为真。而一旦dp[i]为真,就不用继续迭代了。测试的时候发现倒着遍历会比正着遍历速度稍稍快一点,大概是因为test case的字典里长度较长的单词要比长度较短的单词多。

 

解法二(BFS)、解法三(DFS):

观察例子2,我想知道"applepenapple"能否被字典分割,首先肯定是要从前缀开始找。碰到的第一个前缀"apple"恰好在字典里,那么只需要知道剩下的字符串"penapple"能不能被字典分割即可。而步骤和之前一样,还是要从前缀开始找,碰到的第一个前缀"pen"恰好在字典里,继而问题规模再度缩小。到最后只要找"apple"是否能被字典分割即可。整个过程有两个关键,第一个是循环,即每一次都是在做同样的事情——找前缀;第二个是如何把剩下的字符串存起来后再拿出来。想到这里,就不难想到可以用一个循环和一个队列来完成这两个关键。而用到循环和队列的算法是什么呢?广度优先搜索!而另一种方法是不用队列,而采用回溯寻找的方式来处理剩下的字符串,即广度优先搜索!想到这里就发现这道题其实和之前做过的第39题并没有什么区别。如果把字符串想成target,字典想成数组,那么就是要在字典中寻找合适的组合来拼接成目标字符串。很trick的部分是到底如何模型化这个图。首先是节点,很明显节点就是字典中的字符串以及目标字符串。额外的,要加上一个空字符串""。对于第二个例子来说,节点就是"","apple","pen"以及"applepenapple"四个节点。确定好节点之后,再来看边。首先本题一定是有自环的,因为可以用多个数字组成最后的结果。其次,所有的节点一定是互相联通的,即任何节点之间一定都有边,而且是有向边。最后最关键的权值,很抽象。边的权值是从该节点出发到达目标节点的过程中,需要在前缀位置“消耗”掉的目标节点内的字符串。之所以是消耗,是因为可以把本题想象成从节点"applepenapple"通向节点""且权值恰好依次消耗掉源节点字符串的路径。见下图例子(省略了自环以及目标"applepenapple"连接到""的边)。

由此可见,如果想从"applepenapple"节点走向""节点,且权值恰好依次消耗完所有的"applepenapple",那么先走到"apple",权值消耗掉目标节点的字符串"apple",变为"penapple";向右走到"pen"节点,消耗掉"pen",权值剩下"apple";之后向左走,消耗掉"apple",权值变为"";那么最后走向""节点,恰好消耗完所有的权值。

整个过程中,必须要按照权值等于前缀的顺序走,才会形成有效拼接。如果不是,比如"abcd",{"bc, "ad"}。如果先走"bc",最后还是剩下了"ad",但这不是一个有效拼接。所以拼接必须要按前缀的顺序走。

理清了模型,剩下的就是BFS和DFS算法的实现了。这其中最重要的问题是,自环状态下已访问节点要如何标记。其实在这里并不是标记节点本身,而是标记当前消耗掉前缀的位置。仍然拿"applepenapple"举例,这个字符串总共有13位,也就是总共有13个位置可能产生前缀。已经访问过的前缀是不需要再访问的,因为我们已经知道了从那个前缀位置出的所有路径。扫清一切障碍之后,BFS(见解法二代码)和DFS(见解法三代码)就都能实现了。

 


解法一(Java)

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = i - 1; j >= 0 && !dp[i]; j--) {
                String check = s.substring(j, i);
                dp[i] = dp[j] && wordDict.contains(check);
            }
        }
        return dp[s.length()];   
    }
}

 

解法二(Java)

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        Queue<Integer> q = new LinkedList<>(); //构建队列,存储前缀位置
        boolean[] visited = new boolean[s.length() + 1]; //总共有s.length()个位置可能产生前缀
        for (int i = 0; i < wordDict.size(); i++) //找到源节点的相邻节点,即可以通过前缀访问的节点
            if (s.length() >= wordDict.get(i).length() && s.indexOf(wordDict.get(i)) == 0)
                q.add(wordDict.get(i).length());
        visited[0] = true; //标记起始位置
        while (!q.isEmpty()) {
            int start = q.poll(); //取出即将访问的前缀位置
            if (start == s.length()) return true;
            if (!visited[start]) { 
                visited[start] = true; //标记前缀位置为已访问
                String sub = s.substring(start); //依据前缀位置更新权值
                for (int i = 0; i < wordDict.size(); i++) //根据权值,访问具有相同前缀的下一位置
                    if (sub.length() >= wordDict.get(i).length() && sub.indexOf(wordDict.get(i)) == 0)
                        q.add(start + wordDict.get(i).length());
            }
        }
        return false;
    }
}

 

解法三(Java)

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean[] visited = new boolean[s.length()+1]; //总共有s.length()个位置可能产生前缀
        return dfs(wordDict, s, s, 0, visited);
    }
    
    private boolean dfs(List<String> wordDict, String target, String sub, int start, boolean[] visited) {
        if (start == target.length()) return true; //如果前缀的位置在target末尾,证明达到目标节点
        boolean mark = false;
        for (int p = 0; p < wordDict.size(); p++) {
            String word = wordDict.get(p);
            if (word.length() > sub.length()) continue;
            if (sub.indexOf(word) == 0) { //查询前缀
                int next = word.length(); //记录找到的前缀的长度            
                if (!visited[next + start]) { //即将要访问的前缀位置为当前位置start加上前缀长度next
                    visited[next + start] = true; //标记前缀位置为已访问
                    mark = mark || dfs(wordDict, target, sub.substring(next), next + start, visited); //更新权值后,访问下一位置
                }
            }
        }
        return mark;
    }
}

 

以上是关于(Java) LeetCode 139. Word Break —— 单词拆分的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode 139. Word Break

LeetCode 139 Word Break

[LeetCode] 139 Word Break

LeetCode #139. Word Break C#

Leetcode 139. Word Break

LeetCode 139: Word Break