LeetCode每日一题(2021-3-7 & 3-8 分割回文串 I & II)
题目 I 描述
这两道题是一个系列(还有III和IV,暂时先不管),两道题的区别在于,I是返回 s 所有可能的分割方案(输入输出示例如下);II是返回符合要求的最少分割次数。

解题思路
先说I,一般这种求所有可能方案的题目,都可以用回溯法来暴力搜索。回溯法实际上就是一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就回溯返回,尝试别的路径。
回溯法通常可以用递归来实现。回溯法的一般模板:
public void backtrack(未探索区域, ans, path) {
if (终止条件) {
ans.add(path);
return;
}
for (选择:未探索区域中所有元素) {
if(满足条件){
path.add(当前选择);
backtrack(路径,选择列表); // 递归
path.remove(当前选择); //回溯
}
}
}
接下来的思路其实就比较明了了,我们可以从 i 开始,从小到大依次枚举 j。对于当前枚举的 j 值,我们判断 s[i…j] 是否为回文串。如果 s[i…j]是回文串,那么就将其加入答案数组 ans 中,并以 j+1作为新的 i 进行下一层搜索,并在未来的回溯时将 s[i…j] 从 ans 中移除。
这里判断回文串的方法,我原本是打算按照老方法,从字符串两端开始比较是否相等,相等就继续向中间移动,不过这种方法肯定很耗时。结果看了官方题解之后,才发现自己真是菜得抠脚。
我们可以将字符串 s 的每个子串 s[i…j] 是否为回文串预处理出来,使用动态规划即可。设 f(i, j) 表示 s[i…j] 是否为回文串,那么有状态转移方程:

预处理完成之后,我们只需要 O(1) 的时间就可以判断任意 s[i…j] 是否为回文串了。具体代码如下:
class Solution {
//回溯+动态规划预处理
//回溯枚举所有可能的方案。动态规划快速判断子串是否是回文串
int n;
boolean[][] dp;
List<List<String>> ans = new ArrayList<>();
List<String> temp = new ArrayList<>();
public List<List<String>> partition(String s) {
this.n = s.length();
//dp[i][j]表示字符s[i...j]是否为回文串
dp = new boolean[n][n];
for(int i = 0; i < n; i++){
Arrays.fill(dp[i], true);
}
//因为dp[i][j]的值与dp[i + 1][j - 1]有关,所以i要从后往前搜索
for(int i = n - 1; i >= 0; i--){
for(int j = i + 1; j < n; j++){
dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i + 1][j - 1];
}
}
dfs(s, 0);
return ans;
}
//回溯标准模板,一般都是利用深度优先搜索实现回溯
public void dfs(String s, int idx){
if(idx == n){
ans.add(new ArrayList<>(temp));
return;
}
for(int i = idx; i < n; i++){
if(dp[idx][i]){
temp.add(s.substring(idx, i + 1));
dfs(s, i + 1);
temp.remove(temp.size() - 1);
}
}
}
}

题目 II 描述

解题思路
刚看到这题的时候,我还觉得和昨天的题目没区别,直接把昨天代码拿过来一用,返回所有可能分割方案里,长度最短的那个方案的长度减一。自信满满提交,好家伙,直接超时了。昨天题目的字符串长度最大还是16,今天的题目直接变2000了。
因此考虑使用动态规划,设 f[i] 表示字符串的前缀 s[0..i]s[0..i]s[0..i] 的最少分割次数。要想得出 f[i] 的值,我们可以考虑枚举 s[0..i]s[0..i]s[0..i] 分割出的最后一个回文串,这样我们就可以写出状态转移方程:

即我们枚举最后一个回文串的起始位置 j+1,保证 s[j+1..i]s[j+1..i]s[j+1..i] 是一个回文串,那么 f[i]f[i]f[i] 就可以从 f[j]f[j]f[j] 转移而来,附加 1 次额外的分割次数。代码如下:
class Solution {
public int minCut(String s) {
int n = s.length();
//动态规划预处理数组,dp[i][j]表示字符s[i...j]是否为回文串
boolean[][] dp = new boolean[n][n];
for(int i = 0; i < n; i++){
Arrays.fill(dp[i], true);
}
//因为dp[i][j]的值与dp[i + 1][j - 1]有关,所以i要从后往前搜索
for(int i = n - 1; i >= 0; i--){
for(int j = i + 1; j < n; j++){
dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i + 1][j - 1];
}
}
//d[i]表示字符串前缀s[0...i]的最少分割次数
int[] d = new int[n];
Arrays.fill(d, Integer.MAX_VALUE);
for(int i = 0; i < n; i++){
if(dp[0][i]){
d[i] = 0;
}
else{
for(int j = 0; j < i; j++){
if(dp[j + 1][i]){
d[i] = Math.min(d[i], d[j] + 1);
}
}
}
}
return d[n - 1];
}
}

本文介绍LeetCode上的分割回文串I&II题目的解题思路及代码实现,采用回溯法和动态规划预处理解决分割回文串的所有方案及最少分割次数问题。

被折叠的 条评论
为什么被折叠?



