LeetCode刷题总结 --- 记忆化搜索框架

文章目录

  • 记忆化搜索
    • 导言
    • 1. 框架
    • 2. 实例
      • 2.1 两个字符串的删除操作
      • 2.2 正则表达式匹配 (2.1 的升级版)
      • 2.3 编辑距离
      • 2.4 最长回文子序列
      • 2.5 猜数字大小 Ⅱ
      • 2.6 有效的括号字符串
    • 3. 注意点
    • 4. 练习题

记忆化搜索

导言

  1. 以下代码都存放于 我的GitHub仓库 ,如果小伙伴觉得有用,请给我颗星星哈。
  2. 以下代码都是提交过的,正确性可以保证。

1. 框架

var isVisit map[int]int // 保留已经得到的结果,该结构相当于一个备忘录

// 记忆化搜索函数调用者
func memorySearchCaller() {
	/* 1. 进行一些预处理 */
	/* 2. 开始调用记忆化搜索函数,返回记忆化搜索结果 */
}

// 记忆化搜索函数
func memorySearch() {
	/* 3. 判断是否需要返回结果以及进行一些剪枝  (特殊情况处理) */

	// 如果该问题已经求解过了,那么直接返回结果
	if x, ok := isVisit[key]; ok {
		return x
	}
	
	/* 4. 如果没求解,则继续调用记忆化搜索函数,得出结果  (一般情况处理) */
	
	// 记录该问题的结果,加入备忘录
	isVisit[key] = ans
	return ans
}

2. 实例

2.1 两个字符串的删除操作

583. 两个字符串的删除操作

var isVisit map[string]int // 保留已经得到的结果,该结构相当于一个备忘录

// 记忆化搜索函数调用者
func minDistance(word1 string, word2 string) int {
	/* 1. 进行一些预处理 */
	isVisit = make(map[string]int)

	/* 2. 开始调用记忆化搜索函数,返回记忆化搜索结果 */
	return minDistanceExec(word1, word2)
}

// 记忆化搜索函数
func minDistanceExec(word1 string, word2 string) int {
	/* 3. 判断是否需要返回结果以及进行一些剪枝  (特殊情况处理) */
	if len(word1) == 0 {
		return len(word2)
	}
	if len(word2) == 0 {
		return len(word1)
	}

	// 如果该问题已经求解过了,那么直接返回结果
	hashVal := hash(word1, word2)
	if x, ok := isVisit[hashVal]; ok {
		return x
	}

	/* 4. 如果没求解,则继续调用记忆化搜索函数,得出结果  (一般情况处理) */
	ans := 0
	if word1[len(word1)-1] == word2[len(word2)-1] {
		ans = minDistanceExec(word1[:len(word1)-1], word2[:len(word2)-1])
	} else {
		a := minDistanceExec(word1[:len(word1)-1], word2)
		b := minDistanceExec(word1, word2[:len(word2)-1])
		ans = min(a, b) + 1
	}

	// 记录该问题的结果,加入备忘录
	isVisit[hashVal] = ans
	return ans
}

// 由于备忘录的键值是 1 个字符串,而记忆化搜索函数需要 2 个字符串参数才能唯一标识一个子问题,
// 所以,这里采用哈希的方式,把两个参数进行哈希,生成一个键值来唯一的标识这个参数组合,
// 即: 用「1个字符串」 唯一标识 「1个子问题」。
func hash(a, b string) string {
	return a + "|" + b
}

func min(a, b int) int {
	if a > b {
		return b
	}
	return a
}

2.2 正则表达式匹配 (2.1 的升级版)

10. 正则表达式匹配

var hasResult map[string]bool // 保留已经得到的结果,该结构相当于一个备忘录

// 记忆化搜索函数调用者
func isMatch(s string, p string) bool {
	/* 1. 进行一些预处理 */
	hasResult = make(map[string]bool)

	/* 2. 开始调用记忆化搜索函数,返回记忆化搜索结果 */
	return isMatchExec(s, p)
}

// 记忆化搜索函数
func isMatchExec(s string, p string) bool {
	/* 3. 判断是否需要返回结果以及进行一些剪枝  (特殊情况处理) */
	if s == p {
		return true
	}
	if p == "" {
		return s == ""
	}
	ends, endp := len(s)-1, len(p)-1
	if s == "" {
		if p[endp] == '*' {
			return isMatchExec(s, p[:endp-1])
		}
		return false
	}

	// 如果该问题已经求解过了,那么直接返回结果
	key := hash(s, p)
	if x, ok := hasResult[key]; ok {
		return x
	}

	/* 4. 如果没求解,则继续调用记忆化搜索函数,得出结果  (一般情况处理) */
	ans := false
	if s[ends] == p[endp] || p[endp] == '.' {
		ans = isMatchExec(s[:ends], p[:endp])
	} else {
		if p[endp] == '*' {
			if p[endp-1] == s[ends] || p[endp-1] == '.' {
				ans = isMatchExec(s, p[:endp]) || isMatchExec(s[:ends], p) || isMatchExec(s, p[:endp-1])
			} else {
				ans = isMatchExec(s, p[:endp-1])
			}
		}
	}

	// 记录该问题的结果,加入备忘录
	hasResult[key] = ans
	return ans
}

// 由于备忘录的键值是 1 个字符串,而记忆化搜索函数需要 2 个字符串参数才能唯一标识一个子问题,
// 所以,这里采用哈希的方式,把两个参数进行哈希,生成一个键值来唯一的标识这个参数组合,
// 即: 用「1个字符串」 唯一标识 「1个子问题」。
func hash(s, p string) string {
	return s + "|" + p
}

2.3 编辑距离

72. 编辑距离

var isVisit map[string]int // 保留已经得到的结果,该结构相当于一个备忘录

// 记忆化搜索函数调用者
func minDistance(word1 string, word2 string) int {
	/* 1. 进行一些预处理 */
	isVisit = make(map[string]int)

	/* 2. 开始调用记忆化搜索函数,返回记忆化搜索结果 */
	return minDistanceExec(word1, word2)
}

// 记忆化搜索函数
func minDistanceExec(word1 string, word2 string) int {
	/* 3. 判断是否需要返回结果以及进行一些剪枝  (特殊情况处理) */
	if len(word1) == 0 {
		return len(word2)
	}
	if len(word2) == 0 {
		return len(word1)
	}

	// 如果该问题已经求解过了,那么直接返回结果
	hashVal := hash(word1, word2)
	if x, ok := isVisit[hashVal]; ok {
		return x
	}

	/* 4. 如果没求解,则继续调用记忆化搜索函数,得出结果  (一般情况处理) */
	ans := 0
	if word1[len(word1)-1] == word2[len(word2)-1] {
		ans = minDistanceExec(word1[:len(word1)-1], word2[:len(word2)-1])
	} else {
		a := minDistanceExec(word1[:len(word1)-1], word2)
		b := minDistanceExec(word1[:len(word1)-1], word2[:len(word2)-1])
		c := minDistanceExec(word1, word2[:len(word2)-1])
		ans = min(a, b, c) + 1
	}

	// 记录该问题的结果,加入备忘录
	isVisit[hashVal] = ans
	return ans
}

// 由于备忘录的键值是 1 个字符串,而记忆化搜索函数需要 2 个字符串参数才能唯一标识一个子问题,
// 所以,这里采用哈希的方式,把两个参数进行哈希,生成一个键值来唯一的标识这个参数组合,
// 即: 用「1个字符串」 唯一标识 「1个子问题」。
func hash(a, b string) string {
	return a + "|" + b
}

// 这里我重写了min函数,让它可以计算n个参数的最小值
func min(arr ...int) int {
	if len(arr) == 1 {
		return arr[0]
	}
	a, b := arr[0], min(arr[1:]...)
	if a > b {
		return b
	}
	return a
}

2.4 最长回文子序列

516. 最长回文子序列

2.5 猜数字大小 Ⅱ

375. 猜数字大小 Ⅱ

var inf int				// 无穷大
var amount map[int]int 	// 保留已经得到的结果,该结构相当于一个备忘录

// 记忆化搜索函数调用者
func getMoneyAmount(n int) int {
	/* 1. 进行一些预处理 */
	amount = make(map[int]int)
	inf = 100000000000

	/* 2. 开始调用记忆化搜索函数,返回记忆化搜索结果 */
	return getMoneyAmountExec(1, n)
}

// 记忆化搜索函数
func getMoneyAmountExec(l, r int) int {
	/* 3. 判断是否需要返回结果以及进行一些剪枝  (特殊情况处理) */
	if l >= r {
		return 0
	}

	// 如果该问题已经求解过了,那么直接返回结果
	hashNumber := hash(l,r)
	if x, ok := amount[hashNumber]; ok {
		return x
	}

	/* 4. 如果没求解,则继续调用记忆化搜索函数,得出结果  (一般情况处理) */
	ans := inf
	for i := l; i <= r; i++ {
		left := getMoneyAmountExec(l, i-1)
		right := getMoneyAmountExec(i+1, r)
		ans = min(ans, max(left, right)+i)
	}

	// 记录该问题的结果,加入备忘录
	amount[hashNumber] = ans
	return ans
}

// 由于备忘录的键值是 1 个整数,而记忆化搜索函数需要 2 个整数参数才能唯一标识一个子问题,
// 所以,这里采用哈希的方式,把两个参数进行哈希,生成一个键值来唯一的标识这个参数组合,
// 即: 用「1个整数」 唯一标识 「1个子问题」。
func hash(l,r int) int{
	off := 10
	return (r << off) | l
}

func min(a, b int) int {
	if a > b {
		return b
	}
	return a
}
func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

2.6 有效的括号字符串

678. 有效的括号字符串

var isVisit map[int]bool // 保留已经得到的结果,该结构相当于一个备忘录

// 记忆化搜索函数调用者
func checkValidString(s string) bool {
	/* 1. 进行一些预处理 */
	isVisit = make(map[int]bool)

	/* 2. 开始调用记忆化搜索函数,返回记忆化搜索结果 */
	return checkValidStringExec(s, len(s)-1, 0, 0)
}

// 记忆化搜索函数调用者
// s[: nowIndex+1]为当前处理的字符串
// left, right 表示此时的左右括号数量
func checkValidStringExec(s string, nowIndex int, left, right int) bool {
	/* 3. 判断是否需要返回结果以及进行一些剪枝  (特殊情况处理) */
	if nowIndex == -1 {
		return left == right
	}
	if left > right {
		return false
	}

	// 如果该问题已经求解过了,那么直接返回结果
	hashNumber := hash(nowIndex, left, right)
	if x, ok := isVisit[hashNumber]; ok {
		return x
	}

	/* 4. 如果没求解,则继续调用记忆化搜索函数,得出结果  (一般情况处理) */
	ans := false
	lastChar := s[nowIndex]
	if lastChar == '(' || lastChar == '*'{
		ans = ans || checkValidStringExec(s, nowIndex-1, left+1, right)
	}
	if lastChar == ')' || lastChar == '*'{
		ans = ans || checkValidStringExec(s, nowIndex-1, left, right+1)
	}
	if lastChar == '*' {
		ans = ans || checkValidStringExec(s, nowIndex-1, left, right)
	}

	// 记录该问题的结果,加入备忘录
	isVisit[hashNumber] = ans
	return ans
}

// 由于备忘录的键值是 1 个整数,而记忆化搜索函数需要 3 个整数参数才能唯一标识一个子问题,
// 所以,这里采用哈希的方式,把三个参数进行哈希,生成一个键值来唯一的标识这个参数组合,
// 即: 用「1个数字」 唯一标识 「1个子问题」。
func hash(a, b, c int) int {
	return (a << 20) + (b << 10) + c
}

3. 注意点

  • 在设置备忘录时,必须知道备忘录要传入什么信息作为键值,以及备忘录要记录什么信息。
  • 该框架第 4 步涉及到状态转移。
  • 「记忆化搜索」是动态规划思想的递归实现,而一般情况所说的「动态规划」是动态规划思想的迭代实现。
  • 从设计难度上看,「记忆化搜索」易于「动态规划」。
  • 从时空效率上看,「记忆化搜索」差于「动态规划」。 (它们的时间复杂度是一样的)

4. 练习题

  • 10. 正则表达式匹配
  • 72. 编辑距离
  • 375. 猜数字大小 Ⅱ
  • 516. 最长回文子序列
  • 583. 两个字符串的删除操作
  • 678. 有效的括号字符串

你可能感兴趣的:(LeetCode,算法,go语言)