给你一个由小写字母组成的字符串 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,t−1)+check(i+1,r))
其中
i
∈
(
l
,
r
)
i\in (l,r)
i∈(l,r),而check
函数用于检查将字符串s[i+1:r+1]
所需要的最少次数。
check
函数实现也非常容易,我们可以先定义两个指针l
和r
分别指向字符串的首尾,然后将左右指针向中间靠拢,判断左右指针指向的字符是不是相同,不相同的话就需要改变一次。
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,r−1)+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,r−1) 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=i−2r−1(f(t,r),f(t−1,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
如有问题,希望大家指出!!!