• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

RomanLin

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

【记忆化搜索】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/ ,本文仅学习使用,若造成侵权,可以联系删除。

记忆化搜索是一种带存储答案功能的递归搜索,是动态规划的一种优雅实现方式。记忆化搜索的核心是:先递归求解,但把算过的子问题答案存起来,下次遇到直接取用,不再重复计算。

它的三个关键点:

  1. 自顶向下:从原始大问题开始,递归地分解成小问题。
  2. 有备忘录:通常用一个数组或哈希表来存储每个状态(如dp[状态])的答案。
  3. 先查后算:每次递归开头先查备忘录,如果算过就立即返回;没算过才真正计算,并将结果存入备忘录。

记忆化搜索的优点:思维简单(顺着问题自然递归),只计算需要的子问题(可能比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] 的最优解
    }
};

posted on 2026-07-30 21:25  RomanLin  阅读(5)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3