动态规划1312.让字符串成为回文串的最少插入次数

给你一个字符串s,每一次操作你都可以在字符串的任意位置插入字符。
请你返回让s成为回文串的最少操作次数。

示例 1:

输入:s = “zzazz”
输出:0
解释:字符串 “zzazz” 已经是回文串了,所以不需要做任何插入操作。
示例 2:

输入:s = “mbadm”
输出:2
解释:字符串可变为 “mbdadbm” 或者 “mdbabdm” 。
示例 3:

输入:s = “leetcode”
输出:5
解释:插入 5 个字符后字符串变为 “leetcodocteel” 。

官方题解(很长):
设我们通过最少的操作次数将字符串s变成了回文串s’,根据s’长度的奇偶性,会有如下的两种情况:
若s’的长度为奇数,那么它的回文中心字符c。例如当s’ = “adgda"时,它的回文中心为单个字符"g”。我们可以断定,回文中心c一定是原字符串s中的字符,否则如果c是通过操作添加的字符,那么我们可以舍弃这一步操作,此时s’成为长度为偶数的字符串,并且它仍是回文串(在例子中,即"adgda"->“adda”)。

若s’的长度为奇数,那么它的回文中心为单个字符c。例如当s’ = “adgda"时,它的回文中心为单个字符“g”。我们可以断定,回文中心c一定是原字符串中的字符,否则如果c是通过操作添加的字符,那么我们可以舍弃这一步操作,此时s’成为长度为偶数的字符串,并且它仍是回文串(在例子中,即”adgda"->“adda”)。

若s’的长度为偶数,那么它的回文中心为两个字符cc,例如当s’ = “adggda"时,它的回文中心为两个字符“gg”。我们同样可以断定,回文中心cc一定是原字符串中的两个字符,否则如果cc中有至少一个是通过操作添加的字符,那么我们可以舍弃这些操作,此时s’成为长度为偶数(舍弃一次操作)或奇数(舍弃两次操作)的字符串,并且它仍是回文串(在例子中,即“adggda”->“adgda"或”adggda”->“adda”)。

根据此断定,我们还可以得到一条推论,即回文中心cc一定是原字符串中的两个连续的字符。这是因为我们的操作只能添加字符而不能删除字符,因此在回文中心cc是原字符串中的两个字符的情况下,它们一定也是连续的。

这样以来,我们可以首先枚举回文中心(单个字符或两个字符),再对回文中心左侧的字符串p和右侧的字符串q进行相应的操作。具体地,我们希望通过最少的操作次数(添加最少的字符),使得inv_p和q变成相同的字符串,其中inv_p表示将字符串p翻转之后得到的字符串,例如当p = "abcd"时, inv§ = “dcba”。

那么如何解决这个子问题呢?我们首先用inv_p代替p,这样我们的子问题变成:添加最少的字符,使得p和q变成相同的字符串。我们只需要得到p和q的最长公共子序列,设其长度为l,那么最少添加|p| + |q| - l * 2个字符,就可以将p和q变成相同的字符串。例如:
当 p = “abcde”,q = “adefg"时,他们的最长公共子序列为"ade”,长度为3。此时我们可以将p,q和它们的最长公共子序列写出如下的形式:
p = a b c d e
q = a d e f g
a d e
可以看出,以最长公共子序列为基础,我们只需要在"a"和"d"之间添加字符"bc",在"d"之后添加字符"fg",得到的字符串"abcdefg"就是p和q变成的相同字符串,即我们在p和q中分别添加2个字符,就可以得到该字符串。另一方面,|p|+|q| - l = 5 + 5 - 3 * 2 = 4,即我们一共需要添加4个字符,这两个值相等。
枚举回文中心的时间复杂度为O(N),而计算两个字符串的最长公共子序列的时间复杂度为O(N ^ 2),那么整个算法的时间复杂度为O(N^3),无法在规定的时间内通过本题。我们必须要对算法优化。

优化:
仔细回想一下算法的过程,我们依次进行了如下的两个步骤:
**
1 枚举回文中心,并得到回文中心左右两侧的字符串p和q;
2 计算inv_p和q的最长公共子序列。
**

我们能否把这两个步骤合并起来呢?这两个不走到底得到了什么结果?
如果我们将inv_p和q的最长公共子序列设为r,那么在这两个步骤之后,我们在inv_p中得到了inv_rq中得到了r,并且得到了回文中心c或cc。我们将这三个部分拼在一起,实际上得到了一个回文串inv_r + c/cc + r,并且它是原字符串s中找到一个最长回文子序列,若其长度为l,那么我们只需要添加|s| - l个字符,就可以将s变为回文串。

如何从直观上来理解它呢?当我们在原字符串s中找到最长回文子序列后,对于在s中单不在子序列中的那些字符,如果其在回文中心的左侧,我们就在右侧对应的位置添加一个相同的字符;如果其在回文中心的右侧,我们就在左侧对应的位置添加一个相同的字符。例如:
当 s = “dabca"时,它的最长回文子序列为"aba”,我们将s写成如下的形式:
a b a (回文中心为b)
s = d a b c a
s = d a b c a d (字符d在回文中心左侧,那么在右侧对应位置添加一个相同的字符)
s = d a c b c a d

我们添加了2个字符将s变为回文串。另一方面,|s| - l = 5 - 3 = 2,这两个值相等。
那么如何求出s的最长回文子序列sPA呢?实际上,sPA就等同于s和inv_s的最长公共子序列,即sPA即是s的子序列,也是inv_s的子序列(这样就保证了sPA是一个回文的子序列)。所以,我们只需要在O(N ^ 2)的时间求出s和inv_s的最长公共子序列,根据它的长度l,通过|s| - l就可以得到答案。

下面是代码

    public int minInsertions(String s) {
        int n = s.length();
        StringBuilder reversedBuilder = new StringBuilder(s);
        String t = reversedBuilder.reverse().toString();

        int[][]dp = new int[n+1][n+1];
        for(int i = 1; i <= n; ++ i){
            for(int j = 1; j <= n; ++j){
                dp[i][j] = Math.max(dp[i - 1][j],dp[i][j-1]);
                if(s.charAt(i - 1) == t.charAt(j - 1)){
                    dp[i][j] = Math.max(dp[i][j],dp[i - 1][j - 1]+1);
                }
            }
        }
        return n - dp[n][n];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值