回文子串的最小切割数

目录

给定一个字符串 str,返回把 str 全部切成回文子串的最小切割数。

【举例】

str = “ABA” 不需要切割,str 本身就是回文串,所以返回 0

str = “ACDCDCDAD” 最少需要切 2 次变成 3 个回文子串,比如“A”、“CDCDC” 和 “DAD”,所以返回 2.

方案一:暴力递归

动态规划:从左向右尝试模型。不是字符每个位置都可以作为切分点。左边部分必须是一个回文子串,才有资格作为切分点。

f( i ) 定义: str[ i: ] 子串中回文子串的个数。

最终结果:f(0) - 1。整个字符串中回文子串的个数 = 最小切割数 + 1

可能性分析:

如下图:str[ :j ] 是回文(个数是1),此时 j 就是一个切分点,f ( j ) 是后半部分包含的回文子串的个数,因此整体个数时:f ( j ) + 1。题意要求最小切割数,所以最终结果是:所有切分点的最小值。

import sys

def min_cut(string):
    return f(string, 0) - 1

def f(string, index):
    if index == len(string): return 0

    res = sys.maxsize
    for i in range(index, len(string)):
        if valid(string, index, i):
            res = min(res, f(string, i + 1) + 1)

    return res

def valid(string, i, j):
    while i <= j:
        if string[i] != string[j]:
            return False
        i += 1
        j -= 1
    return True

# a|cdcdc|ada
print(min_cut("acdcdcada"))
# aabaa|kck
print(min_cut("aabaakck"))

方案二:动态规划

时间复杂度:$O(N^3)$

空间复杂度:$O(N)$

def min_cut2(string):
    if not string: return 0
    dp = [0] * (len(string) + 1)
    for i in range(len(string) - 1, -1, -1):
        res = sys.maxsize
        for j in range(i, len(string)):
            if valid(string, i, j):
                res = min(res, dp[j + 1] + 1)
        dp[i] = res
    return dp[0] - 1
  
def valid(string, i, j):
    while i <= j:
        if string[i] != string[j]:
            return False
        i += 1
        j -= 1
    return True

方案三:动态规划–优化判断是否是回文串

在判断 str 每个子串是不是回文时,都需要 O(N) 的时间复杂度。其实我们可以提前计算好,在使用时直接查询结果。

生成回文串矩阵,dp[i][j] 为 True 表示以 i 开头,以 j 结尾的 str 是回文串,dp[i][j] 为 False 表示不是回文串。

dp 的生成使用了在动态规划的范围探索模型。

可能性分析:

$f(i,j)=\begin{cases} dp[i+1][j-1] &str[i]==str[j] \False &str[i]!=str[j] \end{cases}$

  • 如果 str[i] == str[j],str[ i: j+1 ] 是否是回文串取决于去掉 i 和 j 后的子串是不是回文串。
  • str[i] != str[j],str[ i: j+1 ] 一定不是回文串

basecase:

  • 只有一个字符时,一定为回文串:dp[i][i] = True
  • 只有两个个字符时,这两个字符相等为回文串,不相等就不是回文串。

时间复杂度:$O(N^2)$

空间复杂度:$O(N^2)$

def palindrome(string):
    n = len(string)
    dp = [[False] * n for _ in range(n)]
    # base case
    dp[-1][-1] = True
    for i in range(n - 1):
        dp[i][i] = True
        dp[i][i + 1] = string[i] == string[i + 1]

    for row in range(n - 3, -1, -1):
        for col in range(row + 2, n):
            dp[row][col] = string[row] == string[col] and dp[row + 1][col - 1]

    return dp

将 valid 调用改为从提前计算好的 palindrome_dp 中查询。

时间复杂度:$O(N^2)$

空间复杂度:$O(N^2)$

def min_cut3(string):
    if not string: return 0
    dp = [0] * (len(string) + 1)

    palindrome_dp = palindrome(string)
    for i in range(len(string) - 1, -1, -1):
        res = sys.maxsize
        for j in range(i, len(string)):
            if palindrome_dp[i][j]:
                res = min(res, dp[j + 1] + 1)
        dp[i] = res
    return dp[0] - 1

对数器

import random

def generator_random_str(max_size):
    alphabet = [chr(i) for i in range(97, 123)]
    size = int(random.random() * max_size)
    return ''.join([random.sample(alphabet, 1)[0] for _ in range(size)])

def check():
    max_size = 10
    for i in range(500):
        stirng1 = generator_random_str(max_size)

        res1 = min_cut(stirng1)
        res2 = min_cut2(stirng1)
        res3 = min_cut3(stirng1)

        if res1 != res2 or res2 != res3:
            print("ERROR", stirng1, "res1=", res1, "res2=", res2, "res3=", res3)
    print("OVER")

check()

总结:本题在求回文子串的最小切割数时,使用动态规划从左向右尝试模型。但是在判断子串是不是回文串时,又使用了动态规划的范围尝试模型。本题综合使用了两种尝试模型。