[每日一题]140. Word Break II

2,014 阅读4分钟

题目描述

本题难度:Hard
做题日期:2017年3月26日
本题地址: leetcode.com/problems/wo…

Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, add spaces in s to construct a sentence where each word is a valid dictionary word. You may assume the dictionary does not contain duplicate words.

Return all such possible sentences.

For example, given
s = "catsanddog",
dict = ["cat", "cats", "and", "sand", "dog"].

A solution is ["cats and dog", "cat sand dog"].

分析和解答

这道题是本周关于回溯的 Hard 题。

暴力法

首先能想到的是暴力法,遍历所有的可能。

以题意中的例子做分析

s = "catsanddog",
dict = ["cat", "cats", "and", "sand", "dog"].



图一

我们从左到右遍历 catsanddog:

  1. 当前子串为 c,不符合要求
  2. 当前子串为 ca,不符合要求
  3. 当前子串为 cat,符合要求,接着求 sanddog 子串的结果, 如图二所示
  4. 当前子串是 cats, 符合要求...


图二

步骤 3 ,一直递归下去,我们可以得到结果 ["cat sand dog"]

代码如下:


图三

图三的代码是暴力搜索所有的可能,会引起 超时异常(黑话是 TLE, 全称 Time Limit Exceeded),如下图所示的case。



图四

经过观察,我们发现图四的test case有一个特点,就是有很多重复的字符: 有很多重复的子串,在暴力算法中有大量的重复计算。

所以,我们可以在图三的代码的基础上做优化:缓存结果。

DP = 记忆优化 + 剪枝

在图一和图二的分析中,我们可以知道,遍历所有的可能解的过程中 ,有许多重复的分支。

s = "aaaaaaaaaa", word_dic = ["aa", "aaa", "aaaa", "aaaaa"] 为例子



图五

如图五所示,在某一次递归求解的过程中,子串 "aa" 会被被重复的计算 5 次(如果是多次递归,计算的次数会更多)。我们可以将 "aa" 的结果缓存到一个 HashTable 中,这样就可以减少大量重复的计算。

Tips: 缓存一般都是用哈希,数组用的比较少,因为用数组会有空间浪费。



图六

Tips: 自顶向下的DP其实就是暴力搜索 + 缓存。

自底向上的思路

在上面的分析中,我们可以发现一个规律:在递归的过程中,我们其实是将字符串分成两个子串:

  1. 将10个a, 分成2个a 和 8 个a 的结果的组合: "aa" + "aaaaaaaa"
  2. 8 个 a 又可以分成 2 个 a 和 6 个 a的组合: "aa" + "aaaaaa"
  3. ... ...

所以,我们可以得到如下的思路:假设 字符串当前的位置是 nf(n) 表示所有的可能解, w(x) 表示 从位置 xn 的子串:

f(n) = w(n) f(n-1) + w(n-1) f(n-2) + ... + w(0)

以 catsanddog 为例子,如下图所示



图七

代码实现如下

Tips 1: 自底向上的DP有一个小窍门:逆向思考。比如我们在分析该问题的时候,一般是考虑从起始点 0 到 结束点 i 的子串结果,这不利于推导DP公式。我们应该这样思考:以 i 为结尾的子串作为处理的单元。这么思考的好处,我们会自动的思考 i - 1 与 i 之间的关系,并且方便的推导他们之间的关系。

最佳提交

关于我们

每日一道算法题是一个纯粹的算法学习社区:通过每天一起做一道算法题来提升我们的算法能力。

每日一题算法群现在一共有3000位小伙伴:有来自北美和国内顶尖名校的本科生、研究生和博士生;也有来自国内外各大互联网公司(Google, Facebook, 亚马逊, 百度, 阿里, 腾讯等)的经验丰富的开发者。

大家聚集只为一个目的:每天一起学习算法!

我们坚信:学习算法是一种信仰!

长按下面的二维码,关注每日一道算法题公众号,跟我们一起学习算法!

附录