(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 —— 单词拆分的主要内容,如果未能解决你的问题,请参考以下文章