【记忆化搜索】LeetCode 1312. 让字符串成为回文串的最少插入次数
前言
(本题使用二维数组的递推写法时间表现上会优秀很多,本文主要旨在讲解记忆化搜索,可根据自身需求选择是否观看)
本文参考了题解 https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/solutions/3844666/ling-shen-dp-mo-ban-si-lu-cong-ji-yi-hua-r4cm/ ,本文仅学习使用,若造成侵权,可以联系删除。
记忆化搜索是一种带存储答案功能的递归搜索,是动态规划的一种优雅实现方式。记忆化搜索的核心是:先递归求解,但把算过的子问题答案存起来,下次遇到直接取用,不再重复计算。
它的三个关键点:
- 自顶向下:从原始大问题开始,递归地分解成小问题。
- 有备忘录:通常用一个数组或哈希表来存储每个状态(如dp[状态])的答案。
- 先查后算:每次递归开头先查备忘录,如果算过就立即返回;没算过才真正计算,并将结果存入备忘录。
记忆化搜索的优点:思维简单(顺着问题自然递归),只计算需要的子问题(可能比DP算得少),代码易写
记忆化搜索的缺点:递归有栈溢出风险(深度大时),且频繁查表会有微小开销。
总之,当遇到一个可以分解成重叠子问题的递归问题时,加个数组存答案,就是记忆化搜索。它和递推式 DP 等价,只是实现风格不同(一个自顶向下,一个自底向上)。
题目
https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/description/
题解
寻找子问题
考虑从字符串两端开始思考,进行分类讨论:
- 如果 \(s[0]==s[n-1]\),首位字符相同则无需插入,因此问题变为求 \(s[1:n-2]\) 的最少插入次数,这是一个子问题
- 如果 \(s[0]!=s[n-1]\),此时有如下两种选择,均需增加一次插入次数
- 在 \(s[0]\) 之后插入一次,问题变为求解 \(s[1,n-1]\) 的最少插入次数的子问题
- 在 \(s[n-1]\) 之前插入一次,问题就变为求解 \(s[0,n-2]\) 的最少插入次数的子问题
这些问题都是和原问题相似的、规模更小的子问题,可以用递归解决。
状态定义与状态转移方程
根据对问题的分析,定义 \(dfs(i,j)\) 表示从 \(s[i]\) 到 \(s[j]\) 形成回文串的最少插入次数。
分类讨论:
- 如果 \(s[i]==s[j]\),\(dfs(i,j)=dfs(i+1,j-1)\)
- 如果 \(s[i]!=s[j]\),\(dfs(i,j)=min(dfs(i+1,j),dfs(i,j-1))\)
递归边界:\(i > j\)
递归入口:\(dfs(0,n-1)\),同时这也是答案
递归搜索+保存递归返回值=记忆化搜索
考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 cache 数组中。
如果一个状态不是第一次遇到(cache 中保存的结果不等于 cache 的初始值),那么可以直接返回 cache 中保存的结果。
值得注意的是,cache 数组初始化的值一定不能等于任何记忆化过程中可能记忆的值。
时间复杂度:\(O(n^2)\)
空间复杂度:\(O(n^2)\)。保存多少状态,就需要多少空间。
参考代码
class Solution {
private:
int n;// 字符串长度
unordered_map<int, int> cache;// 记忆化搜索,key 由区间计算得来,value 表示该区间的最优解
int getKey(int l, int r) {// 计算出区间 [l. r] 的 key
return l * n + r;
}
int dfs(string &s, int l, int r) {
if (l >= r) return 0;
int key = getKey(l, r);// 获取区间 [l, r] 的key
if (cache.find(key) != cache.end()) {// 判断是否已经记忆化过
return cache[key];// 直接返回区间 [l, r] 记忆化的最优解
} // 未曾记忆化过
int ans = 0;
if (s[l] == s[r]) {
// 若 s[l] == s[r] 已经成立,则无需插入任何字符
// 直接递归到区间 [l+1, r-1] 进行计算
ans = dfs(s, l + 1, r - 1);
} else {
// 否则要么需要在位置 r 之后插入一个字符 s[l],要么需要在位置 l 之前插入一个字符 s[r]
// 递归计算出区间 [l+1, r] 和区间 [l, r-1] 的最优解,取较小者作为最优解
ans = min(dfs(s, l + 1, r), dfs(s, l, r - 1)) + 1;
}
cache[key] = ans;// 记忆化区间 [l, r] 的最优解
return ans;
}
public:
int minInsertions(string s) {
n = s.size();
return dfs(s, 0, n - 1);// 返回区间 [0, n - 1] 的最优解
}
};
浙公网安备 33010602011771号