Leetcode 1278:分割回文串 III(超详细的解法!!!)

给你一个由小写字母组成的字符串 s,和一个整数 k

请你按下面的要求分割字符串:

  • 首先,你可以将 s 中的部分字符修改为其他的小写英文字母。
  • 接着,你需要把 s 分割成 k 个非空且不相交的子串,并且每个子串都是回文串。

请返回以这种方式分割字符串所需修改的最少字符数。

示例 1:

输入:s = "abc", k = 2
输出:1
解释:你可以把字符串分割成 "ab" 和 "c",并修改 "ab" 中的 1 个字符,将它变成回文串。

示例 2:

输入:s = "aabbc", k = 3
输出:0
解释:你可以把字符串分割成 "aa"、"bb" 和 "c",它们都是回文串。

示例 3:

输入:s = "leetcode", k = 8
输出:0

提示:

  • 1 <= k <= s.length <= 100
  • s 中只含有小写英文字母。

解题思路

这个问题显然可以通过递归记忆化的方式来解。我们定义函数 f ( l , r , t ) f(l,r,t) f(l,r,t)表示字符串s[l:r+1]分给成t段最少需要的次数,那么

  • f ( l , r , t ) = m i n ( f ( l , i , t − 1 ) + c h e c k ( i + 1 , r ) ) f(l,r,t)=min(f(l,i,t-1)+check(i+1,r)) f(l,r,t)=min(f(l,i,t1)+check(i+1,r))

其中 i ∈ ( l , r ) i\in (l,r) i(l,r),而check函数用于检查将字符串s[i+1:r+1]所需要的最少次数。

check函数实现也非常容易,我们可以先定义两个指针lr分别指向字符串的首尾,然后将左右指针向中间靠拢,判断左右指针指向的字符是不是相同,不相同的话就需要改变一次。

from functools import lru_cache
class Solution:
    def palindromePartition(self, s: str, k: int) -> int:
        @lru_cache(None)
        def check(l, r):
            res = 0
            while l <= r:
                if s[l] != s[r]:
                    res += 1
                l += 1
                r -= 1
            return res
        
        @lru_cache(None)
        def dfs(l, r, t):
            if t == 1:
                return check(l, r)
            res = float("inf")
            for i in range(l, r):
                res = min(res, dfs(l, i, t - 1) + check(i+1, r))
            return res
        
        return dfs(0, len(s)-1, k)

上面代码可以继续优化,我们要将字符串s[l:r+1]分给成t段,那么字符串的最短长度就要大于等于t,所以可以这样写:

from functools import lru_cache
class Solution:
    def palindromePartition(self, s: str, k: int) -> int:
        @lru_cache(None)
        def check(l, r):
            res = 0
            while l <= r:
                if s[l] != s[r]:
                    res += 1
                l += 1
                r -= 1
            return res
        
        @lru_cache(None)
        def dfs(l, r, t):
            if t == 1:
                return check(l, r)
            res = float("inf")
            for i in range(l + t - 2, r): # 优化
                res = min(res, dfs(l, i, t - 1) + check(i+1, r))
            return res
        
        return dfs(0, len(s)-1, k)

我们发现优化完的代码比之前快了一倍。

也可以使用动态规划的方式去写。我们需要进行两次动态规划计算,一次是计算check的值,另一次计算 f ( l , r , t ) f(l,r,t) f(l,r,t)。此时的check函数可以写成如下的递归形式:

  • c h e c k ( l , r ) = c h e c k ( l + 1 , r − 1 ) + 1   i f   s [ l ] ≠ s [ r ] check(l,r)=check(l+1,r-1)+1\ if\ s[l]\neq s[r] check(l,r)=check(l+1,r1)+1 if s[l]=s[r]
  • c h e c k ( l , r ) = c h e c k ( l + 1 , r − 1 )   i f   s [ l ] = s [ r ] check(l,r)=check(l+1,r-1)\ if\ s[l]= s[r] check(l,r)=check(l+1,r1) if s[l]=s[r]

f ( l , r , t ) f(l,r,t) f(l,r,t)初始化为inf,当t=1的时候, f ( l , r , 1 ) = c h e c k ( l , r ) f(l,r,1)=check(l,r) f(l,r,1)=check(l,r)。采用这种做法的话,我们的空间复杂度就是O(len(s)^2*k),还可以继续优化。

实际上观察前面的代码,我们可以发现l始终是0,也就是l这个参数是无用的,那么我们可以将其去掉。我们可以定义函数 f ( t , r ) f(t,r) f(t,r)表示以位置r结尾的字符串且分为t块的最少替换次数,那么:

  • f ( t , r ) = m i n l = i − 2 r − 1 ( f ( t , r ) , f ( t − 1 , l ) + c h e c k ( l + 1 , r ) ) f(t,r)=min_{l=i-2}^{r-1}(f(t,r),f(t-1,l)+check(l+1,r)) f(t,r)=minl=i2r1(f(t,r),f(t1,l)+check(l+1,r))

考虑边界情况,当t=1的时候, f ( 1 , r ) = c h e c k ( 0 , r ) f(1,r)=check(0,r) f(1,r)=check(0,r)

class Solution:
    def palindromePartition(self, s: str, k: int) -> int:
        n = len(s)
        p = [[0] * n for _ in range(n)]
        for j in range(n):
            for i in range(j - 1, -1, -1):
                p[i][j] = 0 if s[i] == s[j] else 1
                if i + 1 < j:
                    p[i][j] += p[i+1][j-1]

        f = [[float("inf")]*n for _ in range(k + 1)]
        for i in range(n):
            f[1][i] = p[0][i]
            
        for i in range(2, k + 1):
            for r in range(i - 1, n):
                for l in range(i - 2, r):
                    f[i][r] = min(f[i][r], p[l + 1][r] + f[i - 1][l])
        return f[k][n-1]

我将该问题的其他语言版本添加到了我的GitHub Leetcode

如有问题,希望大家指出!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值