LeetCode刷題總結 --- 記憶化搜索框架

記憶化搜索

導言

  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. 練習題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章