分割回文串2

161 阅读4分钟

题目

给定一个字符串 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 --;
                }
            }
        }
    }



}

基本思路

  1. 利用动态规划得到已知字符串, 任意子串是否是回文串, 方便判断

  2. 遍历字符串, 得到所有回文串的组合, 利用count计数

缺点

  1. 字符串很长时, 会超时

递归 + 动态规划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--;
            }
        }

    }
}

基本思路

  1. 本身是想着不要遍历字符串而是去研究已经生成的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];
    }
}

基本思路

  1. 动态规划得到一个用来判断索引范围内是否是回文串的dptable

  2. 将原问题分割, 原问题是求一个回文串最少能被分割几次, 首先的想法是从一个字符开始, 长度为2的字符串是否和长度为1的字符串的结果有关, 仔细一想发现不一定有关系, 似乎没有状态转移方程.

但是!!! 假设我们求aabb, 现在我们已经得到啊a, aa, aab这三个子串的最小分割次数, 现在多了一个b, 如果认为aabb是由aab得到, 那么求出来不对, 但是如果认为aabb由aa得到就对了, 所以我们要考虑的就是最后新添加的b和前面的字符串, 一共有多少种组成回文的可能, 即b能b组成bb, 只有这一种, 那么我们就可以由aa的可能 + 1得到aabb的结果

  1. 系统的来说就是一个字符串索引从 0...j...到n, 我们求0...j的最小分割次数的时候, 需要枚举出j之前所有包含j的回文子串, 例如2...j是一个回文串, 那么dp[j] = dp[2] +1; 我们只需要知道2之前的最小分割次数即可. 但是j之前的所有包含j的回文串有多个, 因此需要取一个最小值.

  2. 同时 如果0...j已经是一个回文了 就不需要枚举其它的了, 因为对于本问题, dp[j] = 0就是最小值了