动态规划的思想来求解字符串分割问题
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"
.
0,问题介绍
给定一个包含了若干单词的字典以及一个字符串,将该字符串分割,分割后的得到的单词必须由字典中的单词组成。
1,动态规划思想的介绍---参照《算法导论》中关于动态规划的分析来分析此问题
①最优子结构
要判断整个字符串 s[0..length] 能否被分割,可先判断 s[0...length -1] 能否被分割;而判断 s[0...length -1] 能否被分割,可先判 s[0...length-2] 能否被分割,……直至判断 s[0] 能否被分割,而 s[0] 能否被分割是显而易见的---(查找 s[0] 在不在字典中即可--dict.contains(s[0])???)
递归表达式如下:
②子问题的总个数是多少?每个子问题可以有多少种选择?算法的时间复杂度是多少?
子问题的个数一个有 n 个,n为字符串的长度。每个子问题有 2 种选择,即要么可以分割,要么不可以分割。从这个角度来看,算法的时间复杂度为 O(n)。但是,这里没有考虑每个子问题做选择时,需要执行多少步骤,代价是多大?从下面代码的第 18 行 for 循环中可以看出,第 i 个子问题 需要循环 i 次,那么时间复杂度为 1+2+3+……+n = O(n^2)。
③用到了 动态规划中的重叠子问题
动态规划中的重叠子问题是指,在将原问题分解的过程中,得到了若干子问题,再将这些子问题分解时,又得到若干更小子问题……,这些子问题中,有很多是重复的。这个特点与分治算法分解问题时不同,分治算法分解得到的子问题一般是独立的,各个子问题之间没有太多联系。基于动态规划子问题的重复性,因此,在求解出某个子问题之后,将它的结果记录下来,当下一次再碰到此问题时,直接查找它的结果,而不是再一次计算该子问题。
在 wordBreak 问题中的第2点动态规划求解思路分析(下面 第2点)中,求解 match[i] 的值可能需要用到 match[i-1]、match[i-2]、……match[0]的值;
求解match[i-1]的值 可能需要用到 match[i-2]、match[i-3]、match[0]。match[i] 即对应 s[0..i-1]能否分割这个问题,再结合动态规划自底向上求解问题的性质,把"底层"问题的求解结果记录下来,在求解到“上层”问题时,查找“底层”问题的结果,这种方式可以提高算法的运行效率。这也是《算法导论》中求LCS问题中提到的“查表”。具体的代码体现如下:求mathc[i]的值时,需要查找match[j]的值是多少。
for (int i = 1; i < length + 1; i++) { for (int j = 0; j < i; j++) { if (match[j] && wordDict.contains(s.substring(j, i))) { match[i] = true;
2,用动态规划求解的思路
①match[s.length] 用来表示字符串 s[0...length-1] 每部分能否分割。初始时,match[0]=true; match[i] 表示 s[0...i-1] 这段字符能否分割。
match[s.length] 则表示整个字符串(s[0...length-1])能否分割。
②若match[i]=true,表示 s[0...i-1]能够分割,则需要满足以下条件中的任一 一个:
a)match[0]==true && s[0...i-1] 在字典中;b)match[1] == true && s[1...i-1] 在字典中;c)match[2] == true && s[2...i-1]在字典中;.....
d)match[i-1]==true && s[i-1] 在字典中。 s[i...j]在字典中表示:字符串s中由下标为 i 到 j 的子字符串是字典中的某个单词。
具体分析:设 s = "aaaaa",dict = ["aaaa","aaa"]
由于 "a" 不在dict中,故match[1] = false; "aa" 不在dict中 且 (match[1]=false && "a" 不在dict中),故match[2]=false,对于 match[3]的值,
先判断 "aaa" 是否在dict中,由于 "aaa"在dict 中,故 match[3] = true,对于match[4],由于"aaaa"在dict中故 match[4] = true,
对于 match[5],先判断 "aaaaa",由于它不属于dict ;再继续判断,(match[1] = true?) and (s[1...5] exist in dict?),虽然,s[1...5]="aaaa" exist in dict 为true,但是 match[1] = false,故此时还不能判断match[5];
将 "aaaaa" 先分成 "a" ,再分成 "aaaa" 是否可以? 由于 "a" 不在 dict中 因为,match[1] == false 尽管 "aaaa" 在 dict 中,但还是故不能这样分,因为 "a" 不在 dict 中
故match[2] 赋值为 false
再继续判断,(match[2] = true?) and (s[2...5] exist in dict?)....
将 "aaaaa" 先分成 "aa",再分成 "aaa",是否可以? 由于 "aa" 不在 dict 中,因为判断match[2] == false. 尽管 "aaa" 在dict 中,但还是不能这样分 故match[3] 赋值为 false
直至判断到
match[4]==true? and s[4...4] == "a" exist in dict?? 此时,尽管match[4] == true 但是 s[4]== "a" 为 false,故match[5]= false.
即对于 设 s = "aaaaa",dict = ["aaaa","aaa"],程序将返回false.
3,完整代码如下:
1 import java.util.HashSet; 2 import java.util.Set; 3 4 public class Solution { 5 6 public boolean wordBreak(String s, Set<String> wordDict) { 7 // 参数校验 8 if (s == null || s.length() < 1 || wordDict == null || wordDict.size() < 1) { 9 return false; 10 } 11 12 // 标记是否匹配,match[i]表示 str[0, i-1]可以分割 13 int length = s.length(); 14 boolean[] match = new boolean[length + 1]; 15 match[0] = true; 16 17 for (int i = 1; i < length + 1; i++) { 18 for (int j = 0; j < i; j++) { 19 if (match[j] && wordDict.contains(s.substring(j, i))) { 20 // s(0,n) = s(0,i) + s(i,j) + s(j,n) 21 match[i] = true; 22 break; 23 } 24 } 25 } 26 return match[length]; 27 } 28 public static void main(String[] args) { 29 Solution s = new Solution(); 30 Set<String> set = new HashSet<String>(); 31 set.add("aaaa"); 32 set.add("aaa"); 33 boolean result = s.wordBreak("aaaaa", set); 34 System.out.println(result); 35 } 36 37 }