zoukankan      html  css  js  c++  java
  • (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",那么先走到"apple",权值消耗掉目标节点的字符串"apple",变为"penapple";走到"pen"节点,消耗掉"pen",权值剩下"apple";之后向"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;
        }
    }
  • 相关阅读:
    2016.5.15——leetcode:Number of 1 Bits ,
    2016.5.14——leetcode-HappyNumber,House Robber
    记录学习过程
    npm 模块安装机制简介
    搭建Vue.js开发环境(window10)
    pwd 显示当前所在的工作路径
    Lucene 6.5.0 入门Demo
    java.lang.UnsupportedClassVersionError
    window.onload 和 $(document).ready(function(){}) 的区别
    plsql + 客户端 连接oracle数据库
  • 原文地址:https://www.cnblogs.com/tengdai/p/9260942.html
Copyright © 2011-2022 走看看