LeetCode(8):word-break

题目描述

      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".

思考

      从本题知识点中可以知道这道题要我们使用动态规划方法解题,所以我们先对动态规划方法做一个回顾,个人推荐阅读博文《动态规划:从新手到专家》。

      动态规划可以说是一种思维方式,其基本指导思想是“最优性原理”。最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。

      引用《计算机算法设计与分析》中的话:“动态规划法与分治法类似,它们均是将待求解的问题分解成若干子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,以致于最后解决原问题需要耗费指数时间。然而,不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,从而得到多项式时间算法。为了达到此目的,可以用一个表来记录所有已解决的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思想。具体的动态规划算法多种多样,但它们具有相同的填表格式。”


      我们先通过一个简单的例子来理解一下动态规划算法的基本要素及解题步骤。

      假如我们有面值1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?

      乍一看好像可以用贪心算法解决,但是仔细想想会发现这并不是一个有效的解决办法。如果我们将题目换成:“假如我们有面值2元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?”此时使用贪心算法将会得不到解,具体分析为:首先我们将会选出一个面值不超过11元的最大硬币,即5元,然后选出面值不超过11 - 5 = 6元的最大硬币,仍选择5元,最后选出面值不超过6 - 5 = 1元的最大硬币,此时无解。而显然我们可以轻易地找出一组解(3, 3, 5),所以这道题并不适于用贪心算法解决。

      首先我们来分析一下这道例题,题目问的是“如何用最少的硬币凑够11元”,为什么要强调“最少”?因为一个“最”字为我们提供了一种递推的结构。我们假设需要凑够i元,当i = 0,则问题为我们最少需要多少个硬币凑够0元,因为1、3、5都大于0,所以凑够0元最少需要0个硬币。记“凑够i元最少需要j个硬币”为d(i) = j,于是现在我们有d(0) = 0;当i = 1时,我们拿出一个1元的硬币,此时我们还需要凑够i - 1 = 0元,用式子表示即d(1) = d(1 - 1) + 1 = d(0) + 1 = 1;当i = 2时,有d(2) = d(2 - 1) + 1 = d(1) + 1 = 2;当i = 3时,我们有两种选择,d(3) = d(3 - 3) + 1或d(3) = d(3 - 1) + 1 = d(2) + 1 = 2 + 1 = 3,那我们应该选择哪种方案呢?最少的,用式子表述是:d(3) = min{d(3 - 3) + 1, d(3 - 1) + 1} = min{1, 3} = 1。如果题目中不强调“最少”,则我们无法得到一个确定的d(3)的值,从而无法向更大的i递推。

      现在我们想要解决“用最少的硬币凑够11元”这个问题,也即求解d(11),与上面一样,可以列式进行表述,d(11) = min{d(11 - 5) + 1, d(11 - 3) + 1, d(11 - 1) + 1} = min{d(6) + 1, d(8) + 1, d(10) + 1},如果我们已经算出了d(6)、d(8)以及d(10)的话,就可以通过这个式子很快的计算得到d(11)了。在这里我们提出最优子结构重叠子问题的概念,这是动态规划算法的两大基本要素。当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。比如说例题中d(11)包含了d(6)、d(8)和d(10),而d(6)、d(8)、d(10)分别为凑够6、8、10元需要的最少硬币数,因此称该问题具有最优子结构性质。在用递推方法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次,这就叫重叠子问题。我们求d(6)、d(8)、d(10)时,可以看出它们的计算过程中有着一些重复的部分,比如d(6) = min{d(6 - 5) + 1, d(6 - 3) + 1, d(6 - 1) + 1} = min{d(1) + 1, d(3) + 1, d(5) + 1},d(8) = min{d(8 - 5) + 1, d(8 - 3) + 1, d(8 - 1) + 1} = min{d(3) + 1, d(5) + 1, d(7) + 1},其中都用到了d(3),也就是说d(3)是d(6)和d(8)的一个重叠子问题。

      下面我们给出求解例题的伪代码:

Set Min[i] equal to Infinity for all of i
Min[0] = 0

For i = 1 to N
  For j = 0 to M - 1
    If (Vj <= i AND Min[i - Vj] + 1< Min[i])
      Min[i] = Min[i - Vj] + 1

Output Min[N]

      其实上述伪代码的主体就一个式子:d(i) = min{d(i - Vj) + 1},其中i - Vj >= 0,Vj表示第j个硬币的面值。

      总结上面的解题过程,我们的思路是:先分析最优解的结构,本题中为d(i)是可以用d(j)的最优解来递归求解的;然后建立递归关系,即d(i) = min{d(i - Vj) + 1};最后计算问题的最优值即可,当然有时候我们可能还要寻求最优解,比如11可以由5 + 5 + 1得到,这叫做最优解。


      好了,通过一个简单的例题热身之后,我们来解决word-break问题。

      这道题其实和分硬币的例题思路一致,我们可以将dict看做硬币集,s看做我们想要凑够的钱数,如此一来就有思路了,对于给出的例子,s = "leetcode",dict = ["leet", "code"],我们可以这样做:

      a)创建vector<bool> v(len + 1, false),设置v[0] = true,表示空字符串可以由dict中的元素构成,这里的v[i] = true表示s的前i个字母组成的子串可以用dict中的元素进行表示。

      b)判断v[0] = true,然后判断s = "l"是否可以匹配dict中某个元素,显然不可以,所以设置v[1] = false。

      c)判断v[1] = false,所以不用对s = "e"是否可以匹配dict中某个元素进行判断;判断v[0] = ture,接着判断s = "le"是否可以匹配dict中的某个元素,不可以,所以设置v[2] = false。

      ......

      d)判断v[3] = false,所以不用判断s = "t"是否可以匹配dict中的某个元素;继续判断v[2] = false,所以也不用判断s = "et"是否可以匹配dict中的某个元素;继续判断v[1] = false,仍然不用判断s = "eet"是否可以匹配dict中的某个元素;最后判断v[0] = true,所以我们判断s = "leet"是否可以匹配dict中的某个元素,可以,因而v[4] = true。

      e)判断v[4] = true,继续判断s = "c"是否可以匹配dict中的某个元素,不可以;而对于v[3]、v[2]、v[1]均为false,所以不需要对s = "tc"/"etc"/"eetc"进行判断,而v[0] = true,所以对"leetc"是否为dict中的元素进行判断,发现不是,所以v[5] = true。

      ......

      f)判断v[7]、v[6]、v[5]均为false,所以不用判断s = "e"/"de"/"ode",而v[4] = true,所以继续判断s = "code"是否在dict中,发现是的,所以v[8] = true。

      g)返回v[8]。

      给出代码如下:

class Solution {
public:
    bool wordBreak(string s, unordered_set<string> &dict) {
        int len = s.length();
        vector<bool> v(len + 1, false);
        v[0] = true;
        for(int i = 1; i <= len; ++i){
            for(int j = i - 1; j >= 0; --j){
                if(v[j] && dict.find(s.substr(j, i - j)) != dict.end()){
                    v[i] = true;
                    break;
                }
            }
        }
        return v[len];
    }
};

      具体的代码分析思路可以参考前面我们对本题的思路分析,不过有一点需要注意,我们在语句if(v[j] && dict.find(s.substr(j, i - j)) != dict.end())中,将v[j]是否为true的判断放在前面,从而简化计算,因为如果v[j]为false的话,就不需要再对后面的部分进行计算与判断了。此外这里用到了几个函数,一是unordered_set类型的成员函数find与end,其中find(s)函数将在dict中寻找s的匹配项,如果没有则返回unordered_set::end;二十string类型对象的substr成员函数,它的第一个参数是子串在对象s中的起始点,第二个参数则是子串长度。

      继续思考,为什么这里不是先判断s = "l"是否在dict中,然后判断s = "le"是否在dict中并依次类推呢?因为这样并没有办法定义出子结构的递归式,或者说我们没有办法像上面一样去找到一个v[i],从而将问题分解为子问题。


      本文就说到这里了,有机会再见~




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值