【前端刷题】132.分割回文串 II(HARD)

103 阅读1分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第12天,点击查看活动详情

题目(Palindrome Partitioning II)

链接:https://leetcode-cn.com/problems/palindrome-partitioning-ii
解决数:500
通过率:49.4%
标签:字符串 动态规划 
相关公司:google amazon bytedance 

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。

返回符合要求的 最少分割次数 。

 

示例 1:

输入: s = "aab"
输出: 1
解释: 只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。

示例 2:

输入: s = "a"
输出: 0

示例 3:

输入: s = "ab"
输出: 1

 

提示:

  • 1 <= s.length <= 2000
  • s 仅由小写英文字母组成

思路

看到这种求极值的问题应该不难想到dp

dp[i]dp[i] :索引 0 到 i 的子串 [0,i][0,i] 的最小分割数

题目求:dp[n1]dp[n-1] , n 为字符串 s 的长度

base case 是什么?如果 [0,i][0,i] 就是回文串,不用切割,此时 dp[i]=0dp[i] = 0

dp[i]dp[i] 对应的子串长度为 i+1i+1,最多能被分割 ii 次 所以我们初始化 dp[i]=idp[i] = i,包含了 base case:dp[0]=0dp[0] = 0

找出状态转移方程

我们尝试将子问题拆成规模小一点的子问题,找到它们之间的联系。

dp[i]dp[i] 表示 [0,i][0,i] 的最小分割数,我们用指针 j 去切分一下 [0,i][0,i],切一个规模小一点的 dp 子问题出来。

分成了两部分:[0,j][0,j][j+1,i][j+1,i],其中 [0,j][0,j] 的最小分割数是 dp[j]dp[j],它相对于 dp[i]dp[i] 是计算过的状态,我们要找出 dp[i]dp[i]dp[j]dp[j] 的递推关系。

对于 [j+1,i][j+1,i],如果它是回文串,就有递推关系:dp[i]=dp[j]+1dp[i] = dp[j] + 1

因为 j 指针是在扫 [0,i][0,i],j 在变,它切的 [j+1,i][j+1,i] 如果多次是回文串,dp[i]dp[i] 取最小的 dp[j]+1dp[j]+1 就好

两次 DP

因为我们需要判断 [j+1,i][j+1,i] 子串是否回文,就用到昨天的131题的 dp 解法:

用的 dp 二维数组存放每个 [i,j][i,j] 子串是否回文。所以这道题用了两次 dp,但两次都不复杂。

代码

func minCut(s string) int {
	n := len(s)
	isPali := make([][]bool, n) // isPali[i][j] 表示 [i,j] 子串是否回文
	for i := range isPali {
		isPali[i] = make([]bool, n)
	}
    // 计算isPali矩阵
	for j := 0; j < n; j++ {  // 我采用从左上开始的扫描方向,也可以选别的
		for i := 0; i <= j; i++ { 
			if i == j { 	 // 长度为1的子串 本身就是回文
				isPali[i][j] = true
			} else if j-i == 1 && s[i] == s[j] { // 长度为2的子串 两个字符需相同
				isPali[i][j] = true
			} else if j-i > 1 && s[i] == s[j] && isPali[i+1][j-1] {//剩余子串也回文
				isPali[i][j] = true  
			}                    
		}
	}
	// 第二次dp 
	dp := make([]int, n) // dp[i] 表示 [0,i] 子串的最小分割次数
	for i := 0; i < n; i++ { // 初始化dp 
		dp[i] = i
	}
	for i := 1; i < n; i++ { // 遍历计算 dp[i]
		if isPali[0][i] { // 如果[0,i]就是回文,不用分割,最小分割次数为0
			dp[i] = 0
			continue
		}
		for j := 0; j < i; j++ { // 用指针j去划分[0,i]
			if isPali[j+1][i] { // 如果[j+1,i]是回文 则有dp[i]=dp[j]+1
                if dp[j] + 1 < dp[i] {
                    dp[i] = dp[j] + 1 // 遍历结束时 dp[i]就是最小值了
                }
			}
		}
	}
	return dp[n-1] // 从0到len(s)-1的字符串s的最小分割次数
}
var minCut = function (s) {
    const n = s.length
    const isPali = new Array(n);
    for (let i = 0; i < n; i++) {
        isPali[i] = new Array(n);
    }
    
    for (let j = 0; j < n; j++) {
        for (let i = 0; i <= j; i++) {
            if (i == j) {
                isPali[i][j] = true
            } else if (j - i == 1 && s[i] == s[j]) {
                isPali[i][j] = true
            } else if (j-i > 1 && s[i] == s[j] && isPali[i+1][j-1]) {
                isPali[i][j] = true
            } else {
                isPali[i][j] = false
            }
        }
    }

    const dp = new Array(n);
    for (let i = 0; i < n; i++) {
        dp[i] = i;
    }
    for (let i = 0; i < n; i++) {
        if (isPali[0][i]) {
            dp[i] = 0;
            continue
        } 
        for (let j = 0; j < i; j++) {
            if (isPali[j + 1][i]) {
                dp[i] = Math.min(dp[i], dp[j] + 1);
            }
        }
    }
    return dp[n - 1];
};