题目
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的最少分割次数。
示例:
输入: "aab" 输出: 1 解释: 进行一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
回溯 + 动态规划 1
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.minCut("aab");
}
String s;
boolean [][] dpTable;
int minCount = Integer.MAX_VALUE;
public int minCut(String s) {
this.s = s;
// 首先利用动态规划 得到字符串中哪些字符串能够作为回文串
dpTable = new boolean[s.length()][s.length()];
for (int l = 0; l < s.length(); l ++) {
int left = 0;
int right = left + l;
while (right < s.length()) {
if (l == 0) {
dpTable[left][right] = Boolean.TRUE;
}
if (l == 1) {
dpTable[left][right] = s.charAt(left) == s.charAt(right);
}
if ( l > 1) {
dpTable[left][right] = dpTable[left + 1][right - 1] && (s.charAt(left) == s.charAt(right));
}
left ++;
right ++;
}
}
// 再利用回溯法得到所有可能的组合
huisu(0, 0);
return minCount - 1;
}
public void huisu(int count, int start) {
if (count > minCount ) {
return;
}
if (start == s.length()) {
minCount = Math.min(minCount, count);
return;
}
// 选择列表为start开始的所有子串
for (int i = start; i < s.length(); i ++) {
if (dpTable[start][i]) {
// 当前子串是回文才有继续递归下去的必要
count ++;
huisu(count, i + 1);
// 撤销选择
if (count > 0) {
count --;
}
}
}
}
}
基本思路
-
利用动态规划得到已知字符串, 任意子串是否是回文串, 方便判断
-
遍历字符串, 得到所有回文串的组合, 利用count计数
缺点
- 字符串很长时, 会超时
递归 + 动态规划2
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.minCut("aab");
}
String s;
boolean [][] dpTable;
int minCount = Integer.MAX_VALUE;
public int minCut(String s) {
this.s = s;
dpTable = new boolean[s.length()][s.length()];
for (int l = 0; l < s.length(); l ++) {
int left = 0;
int right = left + l;
while (right < s.length()) {
if (l == 0) {
dpTable[left][right] = Boolean.TRUE;
}
if (l == 1) {
dpTable[left][right] = s.charAt(left) == s.charAt(right);
}
if ( l > 1) {
dpTable[left][right] = dpTable[left + 1][right - 1] && (s.charAt(left) == s.charAt(right));
}
left ++;
right ++;
}
}
// 得到dptable后, 就得到一个二维矩阵, 想象成一个表格
// 如果能从从(0, 0)到达最后一列, 就说明这个字符串能够被拆分成全是回文子串组成的
// 首先一个字符串一定能, 全部拆分为单个字符就可以, 这对应二维矩阵就是(0, 0), (1, 1), (2, 2)...(n - 1, n - 1)位置上的元素为1
// 这种情况就是拆分次数最多的情况, 一共需要n - 1次。
// 那么本题就可以转化成,求一个表格里面只有0和1, 什么情况下能从(0, 0)到达最后一列(x, n - 1)
// 得到最小的x, 那么最小拆分次数就得到了
digui(0, 0);
return minCount;
}
public void digui(int row, int count) {
boolean [] rowData = dpTable[row];
if (row == s.length() - 1 || rowData[s.length() - 1]) {
minCount = Math.min(minCount, count);
return;
}
for (int col = row; col < s.length(); col ++) {
if (rowData[col]) {
// 跳转去col + 1行
count ++;
digui(col + 1, count);
count--;
}
}
}
}
基本思路
- 本身是想着不要遍历字符串而是去研究已经生成的dptable, 结果发现写出来之后和遍历字符串是一样, 依旧会超时
动态规划
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.minCut("aab");
}
public int minCut(String s) {
boolean [][] dpTable = new boolean[s.length()][s.length()];
for (int l = 0; l < s.length(); l ++) {
int left = 0;
int right = left + l;
while (right < s.length()) {
if (l == 0) {
dpTable[left][right] = Boolean.TRUE;
}
if (l == 1) {
dpTable[left][right] = s.charAt(left) == s.charAt(right);
}
if ( l > 1) {
dpTable[left][right] = dpTable[left + 1][right - 1] && (s.charAt(left) == s.charAt(right));
}
left ++;
right ++;
}
}
int [] dp = new int[s.length()];
// 初始化dp
for (int i = 0; i < s.length(); i++) {
dp[i] = i;
}
for (int i = 1; i < s.length(); i ++) {
if (dpTable[0][i]) {
dp[i] = 0;
continue;
}
for (int j = 0; j < i; j ++) {
if (dpTable[j + 1][i]) {
dp[i] = Math.min(dp[i], dp[j] + 1);
}
}
}
return dp[s.length() - 1];
}
}
基本思路
-
动态规划得到一个用来判断索引范围内是否是回文串的dptable
-
将原问题分割, 原问题是求一个回文串最少能被分割几次, 首先的想法是从一个字符开始, 长度为2的字符串是否和长度为1的字符串的结果有关, 仔细一想发现不一定有关系, 似乎没有状态转移方程.
但是!!! 假设我们求aabb, 现在我们已经得到啊a, aa, aab这三个子串的最小分割次数, 现在多了一个b, 如果认为aabb是由aab得到, 那么求出来不对, 但是如果认为aabb由aa得到就对了, 所以我们要考虑的就是最后新添加的b和前面的字符串, 一共有多少种组成回文的可能, 即b能b组成bb, 只有这一种, 那么我们就可以由aa的可能 + 1得到aabb的结果
-
系统的来说就是一个字符串索引从 0...j...到n, 我们求0...j的最小分割次数的时候, 需要枚举出j之前所有包含j的回文子串, 例如2...j是一个回文串, 那么dp[j] = dp[2] +1; 我们只需要知道2之前的最小分割次数即可. 但是j之前的所有包含j的回文串有多个, 因此需要取一个最小值.
-
同时 如果0...j已经是一个回文了 就不需要枚举其它的了, 因为对于本问题, dp[j] = 0就是最小值了