题目描述
本题难度: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:
- 当前子串为 c,不符合要求
- 当前子串为 ca,不符合要求
- 当前子串为 cat,符合要求,接着求 sanddog 子串的结果, 如图二所示
- 当前子串是 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其实就是暴力搜索 + 缓存。
自底向上的思路
在上面的分析中,我们可以发现一个规律:在递归的过程中,我们其实是将字符串分成两个子串:
- 将10个a, 分成2个a 和 8 个a 的结果的组合: "aa" + "aaaaaaaa"
- 8 个 a 又可以分成 2 个 a 和 6 个 a的组合: "aa" + "aaaaaa"
- ... ...
所以,我们可以得到如下的思路:假设 字符串当前的位置是 n
,f(n)
表示所有的可能解, w(x)
表示 从位置 x
到 n
的子串:
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, 亚马逊, 百度, 阿里, 腾讯等)的经验丰富的开发者。
大家聚集只为一个目的:每天一起学习算法!
我们坚信:学习算法是一种信仰!
长按下面的二维码,关注每日一道算法题公众号,跟我们一起学习算法!