Palindrome Partitioning II

Given a string s, partition s such that every substring of the partition is a palindrome.

Return the minimum cuts needed for a palindrome partitioning of s.

For example, given s = "aab",
Return 1 since the palindrome partitioning ["aa","b"] could be produced using 1 cut.

解题思路:

这是一道比较难的题目,我也是参考了网友的思路后,自己写出一个方法,但是还是超时。还是说一下思考的过程吧。

这道题和上一道题类似,但是不是求所有的palindrome partition解,而是求这么多解中的最优解,即最小的cut数量。一般求最优解,都是用动态规划,这是一个大思路。因为如果像 Palindrome Partitioning ,用dfs回溯,求出所有解后,再返回最小的,肯定是可以的。但是,十有八九会超时,因为多了很多重复的判断,每次回溯后,都要再判断下面什么位置是可能被partition的。于是,可以先借助isPalindrome的方法,用O(n^2)的时间给原字符串构造一个记录是否为palindrome的字典,dict[i][j] == 1就表示s.substring(i, j)是palindrome,所以这里length设置为s.length() + 1。

第二步,定义本题关注的dp状态,dp[i]为s.substring(0, i)的最小cut数量。所以这里dp的大小也必须是s.length() + 1。下面我们来看子状态的递推关系。给定一个子字符串S[0...i],循环去判断S[0...j - 1]和S[j...i - 1],如果后者是一个palindrome,那么整体的cut数量就是在子串S[0...j - 1]的cut数量上+1,即dp[j] + 1。所以,dp[i]就是这个循环内的最优解(最小值)。

故有状态转换方程:dp[i] = min(dp[j] + 1) (S.substring(0, j)为palindrome)。

这个状态转换的方式,和前面做过的一道题 Word Break 很像,都是在以当前下标为结尾的子字符串内,往前回溯,一步步找可能的解,或者最优解。 Word Break 寻找的是可能解,这里寻找的是最优解。然后在dp最后一个位置记录的是总体的最优解。这里和 Jump Game 的题目差不多,也有一个局部最优和总体最优的概念。

public class Solution {

    public int minCut(String s) {

        if(s == null || s.length() == 0 || s.length() == 1) {

            return 0;

        }

        int[][] dict = getPalindromeDict(s);

        int[] dp = new int[s.length() + 1];

        for(int i = 0; i < dp.length; i++) {

            dp[i] = -1;

        }

        dp[0] = 0;

        for(int i = 1; i < s.length() + 1; i++) {

            int min = -1, temp = -1;

            for(int j = 0; j < i; j++) {

                if(dict[j][i] == 1 && dp[j] != -1) {

                    temp = dp[j] + 1;

                    min = min == -1 ? temp : Math.min(min, temp);

                }

            }

            dp[i] = min;

        }

        return dp[s.length()] - 1;

    }

    

    public int[][] getPalindromeDict(String s) {

        int[][] dict = new int[s.length() + 1][s.length() + 1];

        for(int i = 0; i < s.length(); i++) {

            for(int j = i + 1; j < s.length() + 1; j++) {

                if(isPalindrome(s.substring(i, j))) {

                    dict[i][j] = 1;

                }

            }

        }

        return dict;

    }

    

    public boolean isPalindrome(String s) {

        if(s.length() == 0) {

            return true;

        }

        int start = 0, end = s.length() - 1;

        while(start < end) {

            if(s.charAt(start) != s.charAt(end)){

                return false;

            }

            start++;

            end--;

        }

        return true;

    }

}

结果还是超时,用了dp还是超时,只能再看大牛的解法。原来是getPalindromeDict的方法,也应该用dp去做。定义dict[i][j] == 1说明s.substring(i, j + 1)是palindrome,那么dict[i - 1][j + 1]就可以在dict[i][j]的基础上去判断,只要s.charAt(i - 1) == s.charAt(j + 1)。

这样改动后,计算dict的时间复杂度变为O(n^2)了。回头去看上面的方法,虽然循环也是两层,但是内层却调用了isPalindrome,再加了一层循环,时间复杂度达到了O(n^3)。

这里dict的下标变了,是s.substring(i, j + 1)而不是s.substring(i, j),所以长度只需要S.length()。

public class Solution {

    public int minCut(String s) {

        if(s == null || s.length() == 0 || s.length() == 1) {

            return 0;

        }

        int[][] dict = getPalindromeDict(s);

        int[] dp = new int[s.length() + 1];

        for(int i = 0; i < dp.length; i++) {

            dp[i] = -1;

        }

        dp[0] = 0;

        for(int i = 1; i < s.length() + 1; i++) {

            int min = -1, temp = -1;

            for(int j = 0; j < i; j++) {

                if(dict[j][i - 1] == 1 && dp[j] != -1) {

                    temp = dp[j] + 1;

                    min = min == -1 ? temp : Math.min(min, temp);

                }

            }

            dp[i] = min;

        }

        return dp[s.length()] - 1;

    }

    private int[][] getPalindromeDict(String s) {

        int[][] dict = new int[s.length()][s.length()];

        for(int i = 0; i < s.length(); i++) {

            for(int j = i; j >= 0; j--) {

                if(i == j) {

                    dict[j][i] = 1;

                }else if(i - j == 1) {

                    if(s.charAt(i) == s.charAt(j)) {

                        dict[j][i] = 1;

                    }

                }else {

                    if(s.charAt(i) == s.charAt(j) && dict[j + 1][i - 1] == 1) {

                        dict[j][i] = 1;

                    }

                }

            }

        }

        return dict;

    }

}

上面代码的运行结果是AC的。

下面又优化了一点。将dp数组初始化赋值为-1是不必要的,因为对于任意下标i,dp[j]一定是>0的,这里j<i。因为再不济,每个字符cut一下也可以,所以不要借助-1来判断。之前主要是脑子秀逗了,条件判断为S[j...i]是palindrome,而且S[0...j]可以被palindrome partitioning。废话,S[0...j]是一定可以被palindrome partitioning的啊!

于是,有了以下的代码。

public class Solution {

    public int minCut(String s) {

        if(s == null || s.length() == 0 || s.length() == 1) {

            return 0;

        }

        //dict[i][j] == 1,s.substring(i, j + 1)是palindrome

        int[][] dict = getPalindromeDict(s);

        //dp[i],s.substring(0, i)的最小cut数量 + 1

        int[] dp = new int[s.length() + 1];

        dp[0] = 0;

        for(int i = 1; i < s.length() + 1; i++) {

            int min = -1, temp = -1;

            for(int j = 0; j < i; j++) {

                if(dict[j][i - 1] == 1) {

                    temp = dp[j] + 1;

                    min = min == -1 ? temp : Math.min(min, temp);

                }

            }

            dp[i] = min;

        }

        return dp[s.length()] - 1;

    }

    private int[][] getPalindromeDict(String s) {

        int[][] dict = new int[s.length()][s.length()];

        for(int i = 0; i < s.length(); i++) {

            for(int j = i; j >= 0; j--) {

                if(i == j) {

                    dict[j][i] = 1;

                }else if(i - j == 1) {

                    if(s.charAt(i) == s.charAt(j)) {

                        dict[j][i] = 1;

                    }

                }else {

                    if(s.charAt(i) == s.charAt(j) && dict[j + 1][i - 1] == 1) {

                        dict[j][i] = 1;

                    }

                }

            }

        }

        return dict;

    }

}

总的来说,这道题用了两次dp。这种二维的dp难点主要在于子状态的建模,和状态转换方程的寻找。

这里我上面总结了,对于每一个下标i,去循环寻找0<=j<=i,根据左右两段子字符串的状态来写状态转换方程。同时,还维护了一个当前最优和全体最优的解。

getPalindromeDict这个方法也是一个dp,而且是一个二维dp。借助前面存下来的结果,不断向两边拓展解,可以省去一重循环。应该说,和上面比是很直观的,也比较巧妙。

总的来说,还需要努力,只能通过做题去寻找规律了。

你可能感兴趣的:(partition)