Hi 你们好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 周赛题解。git
这里是第 170 期的第 4 题,也是题目列表中的第 1312 题 -- 『让字符串成为回文串的最少插入次数』github
给你一个字符串 s
,每一次操做你均可以在字符串的任意位置插入任意字符。算法
请你返回让 s
成为回文串的 最少操做次数。shell
「回文串」是正读和反读都相同的字符串。segmentfault
示例 1:数组
输入:s = "zzazz" 输出:0 解释:字符串 "zzazz" 已是回文串了,因此不须要作任何插入操做。
示例 2:优化
输入:s = "mbadm" 输出:2 解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。
示例 3:spa
输入:s = "leetcode" 输出:5 解释:插入 5 个字符后字符串变为 "leetcodocteel" 。
示例 4:code
输入:s = "g" 输出:0
示例 5:blog
输入:s = "no" 输出:1
提示:
1 <= s.length <= 500
s
中全部字符都是小写字母。HARD
一看到回文字符串,脑中第一反应就是那个马拉车算法,而后瞬间就不想作这道题了,小猪闹脾气,哼 T_T
不过冷静下来以后仍是先仔细看一下题的内容。题目要求为给定一个字符串,咱们能够向里面任意位置插入任何字符,最终要使得该字符串变成一个回文字符串。须要获得这个最小的被插入的字符数量。
其实个人第一反应是要不要找到当前的最长回文子字符串,而后基于它来判断两边的差别从而获得结果。但是这样会有一个问题,那就是基于最长回文子字符串做为中点来进行填充,真的是最优解么?对于这个疑问咱们能够看一下这个例子。
对于 "abccdec" 这个字符串,咱们能够获得最长的回文子字符串是 "cc"。若是咱们基于它做为中点,那么因为左右彻底不同,须要 5 个字符来进行补齐,例如 "abcedccdecba"。但其实咱们能够作到只用 4 个字符进行补齐,即 "abcedcdecba"。这是因为对于奇数长度的字符串,中点其实不须要进行补齐,而咱们也利用了左右一对匹配的 "c" 字符,因此能够减小一个补齐字符的需求。
那么咱们尝试换一个思路,回到这个字符串自己的特性。若是它最后要变成一个回文字符串,那么它最终的最左侧和最右侧的字符必定要是相同的。因此反推回来,若是当前最左侧和最右侧的字符同样,即可继续遍历。可是若是不同呢?这时候咱们能够进行填补。但是填补的方式有两种:
对于这两种填补方式,它们的填补消耗都是 1 个字符,咱们也没法肯定哪种是最优解。因此只有继续推导,直到最终遍历完成后即可获得全局最优解。
基于上述思路,咱们不难想到这里能够利用动态规划的方式来进行实现,或者说动态规划是对于这种思路方式的一种比较不错的实现。
那么什么是动态规划呢?详细的内容仍是期待新坑吧,哈哈哈哈,小猪这里就不展开啦。咱们这里先基于这道题目的内容进行分析和说明就行了。
如上述思路中提到的内容,若是咱们想知道区间 [left, right]
范围里的最优解,那么可能存在两种状况,即 s[left] === s[right]
或者 s[left] !== s[right]
。针对这两种状况,咱们能够获得两种对应的结果,即 0 + [left + 1, right - 1]
和 1 + min([left + 1, right], [left, right - 1])
。若是写成一个递推公式的话能够是:
f(left, right) = s[left] === s[right] ? f(left + 1, right - 1) : 1 + min(f(left + 1, right), f(left, right - 1))
那么接下来按照动态规划的惯例,咱们使用一个数组来记录递推的过程和中间值。具体流程以下:
[0, s.length - 1]
对应的值就是咱们的结果。根据上述流程,咱们能够实现相似下面的代码:
const minInsertions = s => { const LEN = s.length; const dp = []; for (let i = 0; i < LEN; ++i) { dp[i] = new Uint16Array(LEN); dp[i][i + 1] = s[i] === s[i + 1] ? 0 : 1; } for (let i = 2; i < s.length; ++i) { for (j = 0; j < s.length - i; ++j) { dp[j][j + i] = s[j] === s[j + i] ? dp[j + 1][j + i - 1] : 1 + Math.min(dp[j + 1][j + i], dp[j][j + i - 1]); } } return dp[0][s.length - 1]; };
上面的代码时间复杂度 O(n^2),空间复杂度也是 O(n^2)。那么其实按照经验,咱们能够尝试一下把空间复杂度压缩到 O(n),即不是用二维数组,只是用一维数组来记录递推的中间值。
不过这里要注意的是,因为咱们没法保存全部历史的中间值,因此咱们的遍历递推方向作出了一点调整。具体的代码以下:
const minInsertions = s => { const LEN = s.length; const dp = new Uint16Array(LEN); for (let i = LEN - 2; i >= 0; i--) { let prev = 0; for (let j = i; j < LEN; j++) { const tmp = dp[j]; dp[j] = s[i] == s[j] ? prev : 1 + Math.min(dp[j], dp[j - 1]); prev = tmp; } } return dp[s.length - 1]; };
这段代码跑出了 60ms,暂时 beats 100%。
这道题的风格可能更偏向于科班一点,特别是其中关于动态规划的部分,包含着满满的套路感。不过我以为这里面比较重要的部分在与,整个推导过程当中前者和后者的关系,也就是如何基于当前值衍生出下一个值。一旦有了这个推导公式,咱们便能较为容易的写出对应的代码实现了。