Longest Palindromic SubString(最長迴文)

Longest Palindromic SubString(最長迴文)

最長迴文是指給定一個字符串,找出其中最長的迴文。Longest Palindromic SubString介紹了幾種算法,翻譯記錄一下。

方法1:最長公共子串

將字符串翻轉,然後找出兩個字符串的最長公共子串,這個子串同時也是最長迴文。
這個方法看似正確,其實有缺陷。
如,S=”caba”,S’=”abac”,最長子串是aba,求解正確。
但是,對於S=”abacdfgdcaba”,S’=”abacdgfdcaba”,最長公共子串是abacd,顯然,這個不是迴文。
從中可以看出,錯誤原因是由於非迴文的子串正好是原字符串的一部分。爲了修正這個錯誤,需要在發現最長子串的時候,檢查子串的下標是不是與翻轉後的原始下標一致(原文是we check if the substring’s indices are the same as the reversed substring’s original indices,我的理解是對於最長公共子串,如果是合法的迴文,相對於S的頭部,S’的末尾的偏移位置應是一樣的,如果不一樣,則意味着該子串只是公共子串,並不具備迴文屬性),如果一致,則更新最長迴文,否則跳過該回文。該算法使用動態規劃算法,時間消耗O(n2) 。詳見Longest Common SubString。該算法使用的是最長後綴算法,比我設想的動態規劃要簡潔許多,需要多複習。

方法2: 暴力破解

最簡單最粗暴的算法,遍歷所有可能,找到一個答案。
複雜性分析:

  • 時間複雜度: O(n3) .假設n是輸入字符串的長度,則共有n(n-1)/2種子串。檢查是否迴文需要花費O(n),總的複雜度就是O(n3)
  • 空間複雜度: O(1)

方法3: 動態規劃

爲改進暴力破解方案,我們需要首先觀察如何避免迴文檢查過程中的不必要的重複計算。如字符串ababa,如果我們已經知道bab是個迴文,顯然ababa也是一個迴文,因爲左邊和右邊的字符是相同的。
我們定義P(i,j)如下:

P(i,j)={true,Si...Sj is palindrome0,otherelse

因此:
P(i,j) = (P(i+1, j-1) and Si == Sj)
初始條件:
P(i,i) = true
P(i, i+1) = (Si == Si+1)

這是個很直接的DP算法,我們首先初始化一個和兩個字符的迴文,然後計算3個或者更多.

複雜度分析:

  • 時間複雜度: O(n2)
  • 空間負雜度:O(n2)

方法4: 中心擴展(Expand Around Center)

我們可以用常量空間,O(n2) 的時間來解決這個問題。

我們觀察到迴文總是圍繞某個中心的一個鏡像。因此,迴文可以從某個中心擴展,共有2n-1箇中心。

爲什麼會有2n-1箇中心?因爲中心也可能會在兩個字符中間。如abba,中心就在兩個b中間。

public String longestPalindrome(String s) {
    int start = 0, end = 0;
    for (int i = 0; i < s.length(); i++) {
        int len1 = expandAroundCenter(s, i, i);
        int len2 = expandAroundCenter(s, i, i + 1);
        int len = Math.max(len1, len2);
        if (len > end - start) {
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

private int expandAroundCenter(String s, int left, int right) {
    int L = left, R = right;
    while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
        L--;
        R++;
    }
    return R - L - 1;   
}

複雜度分析:

  • 時間複雜度: O(n2)
  • 空間複雜度: O(1)

方法5: Manacher算法

Manacher算法複雜度是O(n),詳見Manacher’s algorithm. (還沒來的及研究,存檔先)

附上我實現的幾個方法:

版本1: 暴力破解

public String longestPalindrome(String s) {
    if (s == null || s.length() <= 1) {
        return s;
    }
    int size = 0;
    boolean isVliad = false;
    String sub = "";

    for (int i = 0; i < s.length(); i++) {
        for (int k = size; k < s.length() - i; k++) {
            isVliad = true;

            for (int x = i,y = i + k; x <= y; x++,y--) {
                if (s.charAt(x) == s.charAt(y)) {
                    System.out.println(String.format("%d-%d:%s", x, y, s.charAt(x)));
                } else {
                    isVliad = false;
                    break;
                }
            }

            if (isVliad && (k + 1) > size) {
                size = k + 1;
                sub = s.substring(i, i + k + 1);
                System.out.println(String.format("%d----%d:%s", i, k, sub));
            }
        }
    }
    System.out.println(sub);
    return sub;
}

毫無懸念,提交後直接超時。

版本2: 改進型的暴力破解

public String longestPalindrome(String s) {
    if (s == null || s.length() <= 1) {
        return s;
    }
    Map<Character, List<Integer>> charMap = new HashMap<>();
    for (int i = 0; i < s.length(); i++) {
        Character k = s.charAt(i);
        List<Integer> set = charMap.get(k);
        if (set == null) {
            set = new ArrayList<>();
            set.add(i);
            charMap.put(k, set);
        } else {
            set.add(i);
        }
    }
    boolean isValid = false;
    int size = 1;
    String sub = s.substring(0, 1);
    int tail = 0;

    for (int i = 0; i < s.length(); i++) {
        List<Integer> set = charMap.get(s.charAt(i));

        for (int k = set.size() - 1; set.get(k) > i; k--) {
            isValid = true;
            tail = set.get(k);

            if (size > (tail - i + 1)) {
                break;
            }

            for (int x = i,y = tail; x < y; x++,y--) {
                 if (s.charAt(x) == s.charAt(y)) {
    //                         System.out.println(String.format("%d-%d:%s", x, y, s.charAt(x)));
                 } else {
                     isValid = false;
                     break;
                 }
            }

            if (isValid) {
                 if (size < (tail - i + 1)) {
                     size = tail - i + 1;
                     sub = s.substring(i, tail + 1);
                     System.out.println(String.format("%d----%d:%s", i, k, sub));
                 }

                 break;
            }
        }
    }
    System.out.println(sub);
    return sub;
}

只是用了一些輔助手段,減少了遍歷次數,提交通過,耗時144ms。(當然,我給不出複雜度分析。這是不是從一個側面說明了工程方法可以解決算法問題?。。。)

版本3: 動態規劃算法

public String longestPalindrome(String s) {
    if (s == null || s.length() <= 1) {
        return s;
    }

    boolean track[][] = new boolean[s.length()][s.length()];
    String longest = s.substring(0, 1);

    for (int i = 0; i < s.length(); i++) {
        track[i][i] = true;
    }

    int j = 0;
    for (int k = 1; k < s.length(); k++) {
        for (int i = 0; i < s.length() - k; i++) {
            j = i + k;
            if (k == 1 || track[i+1][j-1]) {
                if (s.charAt(i) == s.charAt(j)) {
                    track[i][j] = true;

                    if (j - i + 1 > longest.length()) {
                        longest = s.substring(i, j + 1);
                    }
                }
            }
        }
    }

    return longest;
}

不負衆望,60ms。

版本4: 最長公共子串

public String longestPalindrome(String s) {
    if (s == null || s.length() <= 1) {
        return s;
    }

    StringBuilder sb = new StringBuilder();
    for (int i = s.length() - 1; i >= 0; i--) {
        sb.append(s.charAt(i));
    }

    String r = sb.toString();
    int track[][] = new int[s.length()][s.length()];
    String sub = s.substring(0, 1);
    String rsub = "";
    String tmp = "";

    for (int i = 0; i < s.length(); i++) {
        for (int j = 0; j < s.length(); j++) {
            if (s.charAt(i) == r.charAt(j)) {
                if (i == 0 || j == 0) {
                    track[i][j] = 1;
                } else {
                    track[i][j] = track[i-1][j-1] + 1;
                }

                if (track[i][j] > sub.length()) {
                    int start = i - track[i][j] + 1;
                    tmp = s.substring(start, i + 1);
                    rsub = r.substring(r.length() - i - 1, r.length() - start);

//                        System.out.println(String.format("%d-%d :: %d-%d", start, i + 1, r.length() - i - 1, r.length() - start- 1));
//                        System.out.println(tmp + "--" + rsub);
                    if (tmp.equals(rsub)) {
                        sub = tmp;
                        System.out.println(sub);
                    }

                }
            }
        }
    }

    return sub;
}

有點意外,同樣是O(n2) 算法,耗時是366ms,比我的改進型暴力破解算法還要多。估計是字符串截取和比較導致多餘的消耗。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章