记忆化搜索
摘要
- 三种适用动态规划的场景
- 三种不适用动态规划的场景
- 用记忆化搜索解决字符串相关的 DP 问题
什么是记忆化搜索
在函数返回前,记录函数的返回结果
在下一次以同样参数访问函数时直接返回记录下的结果
记忆化搜索函数的三个特点
- 函数有返回值
- 函数返回结果只和输入参数相关,和其他全局状态无关
- 参数列表中传入哈希表或者其他用于记录计算结果的数据结构
记忆化搜索 vs 动态规划
记忆化搜索是动态规划的一种实现方式
动态规划的另外一种实现方式是多重循环 (下节课)
所以记忆化搜索就是动态规划
动态规划的核心思想:由大化小
动态规划的算法思想:大规模问题的依赖于小规模问题的计算结果
类似思想算法的还有:递归,分治法
独孤九剑 —— 破箭式
三种适用动态规划的场景
- 求最优值
- dp[] 的值的类型是最优值的类型
- dp[大问题] = max{dp[小问题 1], dp[小问题 2], …}
- dp[大问题] = min{dp[小问题 1], dp[小问题 2], …}
- 求方案数
- dp[] 的值的类型是方案数 (整数)
- dp[大问题] = ∑(dp[小问题 1], dp[小问题 2],…)
- ∑=sum
- 求可行性
- dp[] 的值是 true / false
- dp[大问题] = dp[小问题 1] or dp[小问题 2] or …
- 代码通常用 for 小问题 if dp[小问题] == true then break 的形式实现
三种不适用动态规划的场景
- 求出所有的具体方案
- http://www.lintcode.com/problem/palindrome-partitioning/
- 只求出一个具体方案还是可以用 DP 来做的 (下节课)
- 该判断标准成功率 99%
- 输入数据是无序的
- http://www.lintcode.com/problem/longest-consecutive-sequence/
- 背包类动态规划不适用此判断条件
- 除去背包问题后,该判断标准成功率 60-70%,有一些题可以先排序之后按序处理
- 暴力算法的复杂度已经是多项式级别
- http://www.lintcode.com/problem/largest-rectangle-in-histogram/
- 动态规划擅长与优化指数级别复杂度 (2^n, n!) 到多项式级别复杂度 (n^2, n^3)
- 不擅长优化 n^3 到 n^2
- 该判断标准成功率 80%
192 · 通配符匹配
类别:匹配型动态规划
适用场景:求可行性
func IsMatch(s string, p string) bool {
memo := make([][]bool, len(s))
visited := make([][]bool, len(s))
for i := range memo {
memo[i] = make([]bool, len(p))
visited[i] = make([]bool, len(p))
}
return isMatchHelper(s, 0, p, 0, memo, visited)
}
func isMatchHelper(s string, sIndex int, p string, pIndex int, memo [][]bool, visited [][]bool) bool {
// 如果 p 从pIdex开始是空字符串了,那么 s 也必须从 sIndex 是空才能匹配上
if pIndex == len(p) {
return sIndex == len(s)
}
// 如果 s 从 sIndex 是空,那么p 必须全是 *
if sIndex == len(s) {
return allStar(p, pIndex)
}
if visited[sIndex][pIndex] {
return memo[sIndex][pIndex]
}
sChar := s[sIndex]
pChar := p[pIndex]
match := false
if pChar == '*' {
match = isMatchHelper(s, sIndex, p, pIndex+1, memo, visited) ||
isMatchHelper(s, sIndex+1, p, pIndex, memo, visited)
} else {
match = charMatch(sChar, pChar) &&
isMatchHelper(s, sIndex+1, p, pIndex+1, memo, visited)
}
visited[sIndex][pIndex] = true
memo[sIndex][pIndex] = match
return match
}
func charMatch(sChar, pChar byte) bool {
return sChar == pChar || pChar == '?'
}
func allStar(p string, pIndex int) bool {
for i := pIndex; i < len(p); i++ {
if p[i] != '*' {
return false
}
}
return true
}
154 · 正则表达式匹配
func IsMatchRE(s string, p string) bool {
memo := make([][]bool, len(s)) // 记忆搜索结果
visited := make([][]bool, len(s)) // 标记是否访问
for i := range memo {
memo[i] = make([]bool, len(p))
visited[i] = make([]bool, len(p))
}
return isMatchHelperRE(s, 0, p, 0, memo, visited)
}
func isMatchHelperRE(s string, sIndex int, p string, pIndex int, memo [][]bool, visited [][]bool) bool {
// "" == ""
if pIndex == len(p) { // 如果p已经匹配完毕
return sIndex == len(s) // 根据s是否匹配完毕即可
}
if sIndex == len(s) { // 如果s匹配完毕
return isEmpty(p, pIndex)
}
if visited[sIndex][pIndex] {
return memo[sIndex][pIndex]
}
sChar := s[sIndex]
pChar := p[pIndex]
match := false
// Consider a* as a bundle
if pIndex+1 < len(p) && p[pIndex+1] == '*' { // 如果为'*',有两种方案
match = isMatchHelperRE(s, sIndex, p, pIndex+2, memo, visited) || // '*'不去匹配字符
charMatchRE(sChar, pChar) && isMatchHelperRE(s, sIndex+1, p, pIndex, memo, visited) // '*'重复前面一个字符去匹配s
} else {
match = charMatchRE(sChar, pChar) && // 如果当前两字符匹配
isMatchHelperRE(s, sIndex+1, p, pIndex+1, memo, visited) // 继续下一个字符匹配
}
visited[sIndex][pIndex] = true // 搜索完成就标记
memo[sIndex][pIndex] = match // 存储搜索结果
return match
}
func charMatchRE(sChar, pChar byte) bool { // 判断两字符是否匹配
return sChar == pChar || pChar == '.'
}
func isEmpty(p string, pIndex int) bool { // 形如"x*x*"形式
for i := pIndex; i < len(p); i += 2 {
if i+1 >= len(p) || p[i+1] != '*' { // 如果不是'*',无法匹配
return false
}
}
return true
}
记忆化搜索时间复杂度
= 动态规划的时间复杂度
= O (状态总数 * 计算每个状态的时间耗费)
829 · 字模式 II
这个题不能使用动态规划或者记忆化搜索,因为参数列表中 mapping 和 used 无法记录到记忆化的哈希表中。
func WordPatternMatch(pattern string, str string) bool {
return isMatch(pattern, str, map[rune]string{}, map[string]struct{}{})
}
func isMatch(pattern string, str string, mapping map[rune]string, used map[string]struct{}) bool {
if len(pattern) == 0 {
return len(str) == 0
}
char := rune(pattern[0])
if word, ok := mapping[char]; ok {
if !strings.HasPrefix(str, word) {
return false
}
return isMatch(pattern[1:], str[len(word):], mapping, used)
}
for i := 0; i < len(str); i++ {
word := str[:i+1]
if _, exists := used[word]; exists {
continue
}
used[word] = struct{}{}
mapping[char] = word
if isMatch(pattern[1:], str[i+1:], mapping, used) {
return true
}
delete(mapping, char)
delete(used, word)
}
return false
}
107 · 单词拆分(一)
给定字符串 s 和单词字典 dict,确定 s 是否可以分成一个或多个以空格分隔的字串,并且这些字串都在字典中存在
记忆化搜索的缺陷:递归深度太深,导致 StackOverflow
划分型动态规划算法
state: dp[i] 表示前 i 个字符是否能够被划分为若干个单词 function: dp[i] = or{dp[j] and j + 1~i 是一个单词}
func WordBreak(s string, wordSet map[string]struct{}) bool {
if s == "" {
return true
}
n := len(s)
dp := make([]bool, n+1)
dp[0] = true
maxLength := 0
for word := range wordSet {
if len(word) > maxLength {
maxLength = len(word)
}
}
for i := 1; i <= n; i++ {
for l := 1; l <= maxLength; l++ {
if i < l {
break
}
if !dp[i-l] {
continue
}
word := s[i-l : i]
if _, ok := wordSet[word]; ok {
dp[i] = true
break
}
}
}
return dp[n]
}
136 · 分割回文串
一个类似 Word Break II 的题
但是使用记忆化搜索优化效果甚微
683 · 单词拆分 III
类别:前缀型/划分型动态规划
适用场景:求方案总数
设 dp[i][j]表示从字典 dict 中组成子串 str[i:j+1]有多少种方法。
func WordBreak 3 (s string, dict map[string]struct{}) int {
n := len (s)
lowerS := strings.ToLower (s)
lowerDict := make (map[string]struct{})
for str := range dict {
lowerDict[strings.ToLower (str)] = struct{}{}
}
dp := make ([][]int, n)
for i := range dp {
dp[i] = make ([]int, n)
}
for i := 0; i < n; i++ {
for j := i; j < n; j++ {
sub := lowerS[i : j+1]
if _, exists := lowerDict[sub]; exists {
dp[i][j] = 1
}
}
}
for i := 0; i < n; i++ {
for j := i; j < n; j++ {
for k := i; k < j; k++ {
dp[i][j] += dp[i][k] * dp[k+1][j]
}
}
}
return dp[0][n-1]
}
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。