动态规划的思想来求解字符串分割问题

简介:

LeetCode WordBreak原题

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-1] 能否被分割,可先判断 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     }
复制代码
相关文章
|
1月前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
48 2
|
2月前
动态规划——斐波那契模型
本文介绍了动态规划的基本步骤及其在几个典型问题中的应用。首先概述了动态规划的四个关键步骤:状态表示、状态转移方程、初始化及填表顺序,并说明了如何确定返回值。接着通过具体实例讲解了第 N 个泰波那契数、三步问题、使用最小花费爬楼梯以及解码方法等问题的求解过程,详细阐述了每一步的具体实现方法与代码示例。最后还讨论了如何优化初始化过程以简化编码。
32 4
动态规划——斐波那契模型
|
7月前
|
算法
动态规划求解超详细介绍 例题+代码
动态规划求解超详细介绍 例题+代码
|
7月前
|
算法 测试技术 C++
【动态规划】 【字典树】C++算法:472 连接词
【动态规划】 【字典树】C++算法:472 连接词
|
7月前
|
算法
算法修炼-动态规划之斐波那契数列模型
算法修炼-动态规划之斐波那契数列模型
|
7月前
|
存储 缓存 算法
【算法训练-动态规划 零】动态规划解题框架
【算法训练-动态规划 零】动态规划解题框架
105 0
|
机器学习/深度学习 Go C语言
921. 使括号有效的最少添加:简单贪心思想
这是 力扣上的 921. 使括号有效的最少添加,难度为 中等。
|
机器学习/深度学习 算法 Python
Python实现回溯算法求解括号生成
Python实现回溯算法求解括号生成
65 0
|
算法
算法设计初步之动态规划
算法设计初步之动态规划