题目地址
https://leetcode-cn.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/
题目描述
给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s 成为回文串的 最少操作次数 。
「回文串」是正读和反读都相同的字符串。
示例 1:
输入:s = "zzazz"
输出:0
解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。
示例 2:
输入:s = "mbadm"
输出:2
解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。
示例 3:
输入:s = "leetcode"
输出:5
解释:插入 5 个字符后字符串变为 "leetcodocteel" 。
示例 4:
输入:s = "g"
输出:0
示例 5:
输入:s = "no"
输出:1
提示:
1 <= s.length <= 500
s 中所有字符都是小写字母。
函数签名
// 返回字符串 s 变成回文串的最少操作次数
public int minInsertions(String s);
思考
解题之前先来思考几个问题:
如何构造一个回文串?
构造回文串的步骤:
- (1) 一个空串
""
或者一个字符的字符串,比如"a"
- (2) 在步骤(1)的基础上,首尾追加单个相同的字符,比如
"a" => "bab"
- (3) 不断重复步骤(2)的操作
回文串的具体定义是什么?
- (1) 空串
""
是一个回文串 - (2) 单个字符组成的字符串是一个回文串,比如
"a"
- (3) 两个相同字符组成的字符串是一个回文串,比如
"aa"
- (4) 去掉首尾两个相同的字符后的字符串仍然是一个回文串。比如
"bab"
和"baab"
题解
穷举法
class Solution {
public int minInsertions(String s) {
if (s.length() == 0 || s.length() == 1) {
return 0;
}
if (s.charAt(0) == s.charAt(s.length() - 1)) {
return minInsertions(s.substring(1, s.length() - 1));
}
return Math.min(minInsertions(s.substring(1)), minInsertions(s.substring(0, s.length() - 1))) + 1;
}
}
递归算法的时间复杂度怎么计算?子问题总数 解决一个子问题需要的时间。
递归算法的问题规模不断缩减,因此可以看成一棵树,每个问题是一个节点,每个问题的子问题相当于其子节点。
这里最坏的情况是每次都从【问题规模 】分裂为两个【问题规模 】的子问题,对应代码的最后一行。所以可以看成是一个深度为 的二叉树,其总节点数量为 。
解决一个问题需要的时间是 ,所以这里的时间复杂度为 ——指数级别。
剪枝
因为字符串是一个有序集合,长度为 的字符串其子串的数量为 。
推导过程如下所示:
n: 长度为 1 的子串数量(单个字符串,假设其字符都不相同。一共有 n 个)
(n-1):长度为 2 的子串数量
(n-2):长度为 3 的子串数量
...
2: 长度为 n-1 的子串数量(字符串去掉首字符,字符串去掉尾字符)
1: 长度为 n 的子串数量(就是字符串自己)
// 运用【等差数列求和公式】可知
n + (n-1) + (n-2) + ... + 2 + 1
= (n + 1) * n / 2
显而易见,每个子串是一个独立的子问题,子问题总数等于所有子串的数量和。
所以穷举法存在大量的重复子问题。
因此可以通过备忘录的形式进行剪枝,代码如下:
class Solution {
public int minInsertions(String s) {
if (memo.get(s) != null) {
return memo.get(s);
}
if (s.length() == 0 || s.length() == 1) {
return 0;
}
if (s.charAt(0) == s.charAt(s.length() - 1)) {
return minInsertions(s.substring(1, s.length() - 1));
}
int result = Math.min(minInsertions(s.substring(1)), minInsertions(s.substring(0, s.length() - 1))) + 1;
memo.put(s, result);
return result;
}
private Map memo = new HashMap<>();
}
上述算法的时间复杂度为:。
因为 substring()
的调用十分耗时,所以可以通过引入首位索引指针的形式减少 substring()
的调用开销。
首先定义函数签名:
// 返回字符串 s[left:right] 变成回文串的最少操作次数
public int minInsertions(String s, int left, int right)
代码实现如下所示:
class Solution {
public int minInsertions(String s) {
return minInsertions(s, 0, s.length() - 1);
}
public int minInsertions(String s, int left, int right) {
int key = getKey(left, right);
if (memo.get(key) != null) {
return memo.get(key);
}
if (left >= right) {
return 0;
}
if (s.charAt(left) == s.charAt(right)) {
return minInsertions(s, left + 1, right - 1);
}
int result = Math.min(
minInsertions(s, left + 1, right),
minInsertions(s, left, right - 1)
) + 1;
memo.put(key, result);
return result;
}
private Map memo = new HashMap<>();
// 1 <= s.length <= 500
// 所以可以通过 left * 1000 的形式让后三位表示 right,前三位表示 left 索引指针的值
private int getKey(int left, int right) {
return left * 1000 + right;
}
}
拓展
返回任意一个回文串结果
如果需要获取上述操作后生成的一个回文串,该如何处理呢?
首先定义函数签名:
// 返回字符串 s 通过插入操作变成回文串后的最短长度回文串
public String minInsertions(String s)
// 返回字符串 s[left:right] 通过插入操作变成回文串后的最短长度回文串
public String minInsertions(String s, int left, int right)
代码实现如下所示:
class Solution {
public String minInsertions(String s) {
return minInsertions(s, 0, s.length() - 1);
}
public String minInsertions(String s, int left, int right) {
int key = getKey(left, right);
if (memo.get(key) != null) {
return memo.get(key);
}
if (left >= right) {
memo.put(key, s.substring(left, right + 1));
return memo.get(key);
}
if (s.charAt(left) == s.charAt(right)) {
String subPart = minInsertions(s, left + 1, right - 1);
memo.put(key, s.charAt(left) + subPart + s.charAt(right));
return memo.get(key);
}
String leftPart = minInsertions(s, left + 1, right);
String rightPart = minInsertions(s, left, right - 1);
if (leftPart.length() > rightPart.length()) {
memo.put(key, s.charAt(right) + rightPart + s.charAt(right));
} else {
memo.put(key, s.charAt(left) + leftPart + s.charAt(left));
}
return memo.get(key);
}
private Map memo = new HashMap<>();
// 1 <= s.length <= 500
// 所以可以通过 left * 1000 的形式让后三位表示 right,前三位表示 left 索引指针的值
private int getKey(int left, int right) {
return left * 1000 + right;
}
}
返回所有的回文串结果
如果需要获取上述操作后生成的所有回文串集合,该如何处理呢?
首先定义函数签名:
// 返回字符串 s 通过插入操作变成回文串后的最短长度回文串集合
public List minInsertions(String s)
// 返回字符串 s[left:right] 通过插入操作变成回文串后的最短长度回文串集合
public List minInsertions(String s, int left, int right)
代码实现如下所示:
class Solution {
public List minInsertions(String s) {
return minInsertions(s, 0, s.length() - 1);
}
public List minInsertions(String s, int left, int right) {
int key = getKey(left, right);
if (memo.get(key) != null) {
return memo.get(key);
}
if (left >= right) {
putMemo(key, s.substring(left, right + 1));
return memo.get(key);
}
if (s.charAt(left) == s.charAt(right)) {
List subParts = minInsertions(s, left + 1, right - 1);
putMemo(key, subParts, s.charAt(left));
return memo.get(key);
}
List leftParts = minInsertions(s, left + 1, right);
List rightParts = minInsertions(s, left, right - 1);
int leftLength = leftParts.get(0).length();
int rightLength = rightParts.get(0).length();
if (leftLength > rightLength) {
putMemo(key, rightParts, s.charAt(right));
} else if (leftLength < rightLength) {
putMemo(key, leftParts, s.charAt(left));
} else {
// leftLength == rightLength
putMemo(key, rightParts, s.charAt(right));
putMemo(key, leftParts, s.charAt(left));
}
return memo.get(key);
}
private Map> memo = new HashMap<>();
// 1 <= s.length <= 500
// 所以可以通过 left * 1000 的形式让后三位表示 right,前三位表示 left 索引指针的值
private int getKey(int left, int right) {
return left * 1000 + right;
}
private void putMemo(int key, String value) {
memo.put(key, Arrays.asList(value));
}
private void putMemo(int key, List subParts, char addon) {
List newValues = subParts.stream()
.map(string -> addon + string + addon)
.collect(Collectors.toList());
List oldValues = memo.get(key);
if (oldValues != null) {
newValues.addAll(oldValues);
}
memo.put(key, newValues);
}
}
递归算法的时间复杂度怎么计算?子问题总数 x 解决一个子问题需要的时间。
这里的子问题总数仍然是 。
但是一个子问题需要的时间等于 putMemo(int key, List
的耗时,这个和当前字符串的子串拥有的回文串数量相关,没有一个很好的计算方式。
通过实测 s = "tldjbqjdogipebqsohdypcxjqkrqla"
(字符串长度为 ),发现其回文串数量为 。
由此可知其时间复杂度是远远大于 的。