Go猜想录
大道至简,悟者天成
记忆化搜索

摘要

  • 三种适用动态规划的场景
  • 三种不适用动态规划的场景
  • 用记忆化搜索解决字符串相关的 DP 问题

什么是记忆化搜索

在函数返回前,记录函数的返回结果

在下一次以同样参数访问函数时直接返回记录下的结果

记忆化搜索函数的三个特点

  • 函数有返回值
  • 函数返回结果只和输入参数相关,和其他全局状态无关
  • 参数列表中传入哈希表或者其他用于记录计算结果的数据结构

记忆化搜索 vs 动态规划

记忆化搜索是动态规划的一种实现方式

动态规划的另外一种实现方式是多重循环 (下节课)

所以记忆化搜索就是动态规划

动态规划的核心思想:由大化小

动态规划的算法思想:大规模问题的依赖于小规模问题的计算结果

类似思想算法的还有:递归,分治法

独孤九剑 —— 破箭式

三种适用动态规划的场景

  1. 求最优值
    • dp[] 的值的类型是最优值的类型
    • dp[大问题] = max{dp[小问题 1], dp[小问题 2], …}
    • dp[大问题] = min{dp[小问题 1], dp[小问题 2], …}
  2. 求方案数
    • dp[] 的值的类型是方案数 (整数)
    • dp[大问题] = ∑(dp[小问题 1], dp[小问题 2],…)
    • ∑=sum
  3. 求可行性
    • dp[] 的值是 true / false
    • dp[大问题] = dp[小问题 1] or dp[小问题 2] or …
    • 代码通常用 for 小问题 if dp[小问题] == true then break 的形式实现

三种不适用动态规划的场景

  1. 求出所有的具体方案
  2. 输入数据是无序的
  3. 暴力算法的复杂度已经是多项式级别

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 国际许可协议进行许可。