5. Longest Palindromic Substring

求一个字符串中的最长回文字符串。

这道题卡了我很长时间。。。看到这道题目,第一反应就是dp。结果证明我的直觉很正确,确实可以用dp来解,但我却找错了状态方程。。。

当时我的定义是:dp(i)表示以char[i]字符结尾的最长回文字符串的长度。于是dp(0)、dp(1)都很好求出,但如何从dp(i)推导出dp(i+1)?当时我只考虑到一种情况:if(char[i+1]==char[i-dp(i)]) then dp(i+1)=dp(i)+2,但不止这一种情况啊。。。其他情况下dp(i+1)的值不仅仅依赖于以前的状态,想求出dp(i+1)就要遍历以前的字符,这就违反了dp的基本原则。沿着这个思路走下去,于是纠结了很长时间。。。

正确的做法是用二维dp:dp(i,j)==1表示从char[i]到char[j]是回文,否则dp(i,j)==0。我们可以认为这是一个i*j的矩阵,要算出这个矩阵中所有的元素的值。目的就是找到一个dp(i,j)==1的元素,使j-i+1最大。

状态转移方程:

if(dp[i][j]==0)
    dp[i-1][j+1]=0;
else if(char[i-1]==char[j+1])
    dp[i-1][j+1]=1;
else
    dp[i-1][j+1]=0;

画个图可能好理解些:在这个i*j的矩阵中,所有的dp(i,i)都必定是1,因为单独的一个字符都被认为是回文;所有的dp(i,i+1)也可以很简单的求出,因为只有2个字符,如果这两个字符相同,那么dp(i,i+1)=1,否则dp(i,i+1)=0。这个就是初始状态。

接下来就要根据初始状态和状态转移方程,求出矩阵中其他元素的值。由状态转移方程可知每个元素的值只跟它左下方的相邻元素有关。所以我们可以从已知的元素出发,向右上角“移动”,求出所有元素的值。矩阵的下半部份是不用关心的,因为i<j没有意义。

在实际计算过程中,没必要求出所有元素,否则容易TLE。如果当前元素是0,那么也不用往右上角“移动”了,肯定都是0。

public class Solution {
    // dp矩阵
    int[][] cache = null;

    public String longestPalindrome(String s) {
        if (s == null || s.trim().length() == 0)
            return null;

        char[] array = s.toCharArray();
        cache = new int[array.length][array.length];

        // 存储最长回文字符串的开始下标和长度
        int begin = 0, length = 0;
        // 遍历一次,计算初始状态,就是计算所有的dp(i,i)和dp(i,i+1)
        for (int i = 0; i < array.length; i++) {
            cache[i][i] = 1;
            if (i + 1 < array.length && array[i] == array[i + 1]) {
                cache[i][i + 1] = 1;
                begin = i;
                length = 2;
            }
        }

        if (length == 0) {
            begin = 0;
            length = 1;
        }

        // 开始计算矩阵中其他元素的值
        for (int i = 0; i < array.length; i++) {
            // 先尝试从dp(i,i)向右上角移动
            int trueStep = trueStep(array, i, i);  // 最多可以向右上角移动多少步?
            int trueDist = trueStep * 2 + 1;  // 这里利用了一个特性,每向右上角移动一步,长度+2
            if (trueDist > length) {
                begin = i - trueStep;
                length = trueDist;
            }

            // 再尝试从dp(i,i+1)向右上角移动
            if (i + 1 < array.length) {
                trueStep = trueStep(array, i, i + 1);
                trueDist = trueStep * 2 + 2;
                if (trueDist > length) {
                    begin = i - trueStep;
                    length = trueDist;
                }
            }
        }

        return s.substring(begin, length + begin);
    }

    /**从dp(row,col)开始,最多可以向右上角移动几步,碰到0或者越界就返回*/
    private int trueStep(char[] array, int row, int col) {
        // 如果当前元素是0,直接返回
        if (cache[row][col] != 1) {
            return 0;
        }
        int step = 0;
        while (row >= 0 && col < array.length) {
            if (array[row] == array[col]) {
                // dp(row,col)=1,说明从row到col是回文
                step++;
            } else {
                // 如果一个元素是0,那么接下来的也必然都是0,不用再算了
                break;
            }
            row--;
            col++;
        }
        return step - 1;
    }
}

其实还是有些可以优化的地方的,比如从某个元素向右上角移动时,可以算出最大可能移动多少步,如果最大可能步数都小于当前的trueStep,这一条线上的元素都不用计算了。

被dp折磨之后,我又google了下其他解法。其实这道题不一定要dp的,比如下面这个:

public class Solution {
    public String longestPalindrome(String s) {
        if (s == null) {
            return "";
        }
        char[] arr = s.toCharArray();
        int max = 0;
        int maxi = 0;
        int maxj = 0;

        for (int i = 0; i < arr.length;) {
            // 这里利用了回文字符串的特性:中间是一个或多个相同元素,然后加上对称的“两翼”,类似abcdxxxxxdcba
            // 一个中心两个基本点么。。。
            int i1 = getFarestSameElementIndex(arr, i);
            int dist = getDistance(arr, i, i1);   // 求出“两翼”的长度
            int index1 = i - dist;
            int index2 = i1 + dist;
            int l = index2 - index1;   // 这是中心包含arr[i]元素的,最大回文字符串的,长度
            if (l > max) {
                max = l;
                maxi = index1;
                maxj = index2;
            }
            i = i1 + 1;  // 这是最关键的一步,为何i=i1+1,而不是i++?
        }

        return s.substring(maxi, maxj + 1);
    }

    // 从index1/index2开始向左/右“扩展”,找到最大的回文字符串长度
    // 从arr[index1]到arr[index2]都是相同的元素
    private int getDistance(char[] arr, int index1, int index2) {
        int i1 = index1 - 1;
        int i2 = index2 + 1;
        int dist = 0;
        while (i1 >= 0 && i2 < arr.length) {
            if (arr[i1] == arr[i2]) {
                dist++;
            } else {
                break;
            }
            i1--;
            i2++;
        }
        return dist;
    }

    // 从index开始向右,找到最远的、相同元素的下标
    private int getFarestSameElementIndex(char[] arr, int index) {
        for (int i = index + 1; i < arr.length; i++) {
            if (arr[i] != arr[index]) {
                return i - 1;
            }
        }
        return arr.length - 1;
    }
}

这个解法比DP快很多。关键在于利用了回文字符串的特性,比较难想到。i = i1 + 1那步是关键,可以减少很多无用的计算量。

results matching ""

    No results matching ""