使用Manacher算法解決最長回文子串問題


作者:Grey

原文地址:使用Manacher算法解決最長回文子串問題

要解決的問題

求一個字符串最長回文子串是什么。且時間復雜度 O(N)

具體描述可參考:

LeetCode_5_最長回文子串

LintCode_200_最長回文子串

暴力解法

以每個字符為中心向左右兩邊擴,直到擴不動為止,記錄下每個字符對應能擴的范圍大小。因為有每個位置左右兩邊能擴的最大范圍,我們可以很方便還原出最長回文子串是什么。

比如:AB1234321CD 這個字符串,以4字符為中心向左右兩邊能擴的位置最大,1234321 為最長回文子串。

如上解法有個問題,即針對類似1ABBA2這樣的字符串,如上算法會錯過最長回文子串ABBA, 因為ABBA不是以任何一個字符串向左右兩邊擴散得到的。所以,需要預處理一下原始字符串,預處理的方式如下:

在字符串的每兩個位置之間插入一個特殊字符,變成一個預處理后的字符串,比如我們可以以#作為特殊字符(特殊字符選哪個無所謂,不必非要是原始串中不含有的字符),將1ABBA2這個字符串預處理成1#A#B#B#A#2,用預處理串來跑這個暴力解法,會得到#A#B#B#A#這個是預處理串的最長回文子串,我們可以很方便把這個串還原成原始串的最長回文子串。

暴力解法時間復雜度為O(N^2)

暴力方法的示例代碼如下:

public class LeetCode_0005_LongestPalindromicSubstring {
    // 暴力解法
    public static String longestPalindrome1(String s) {
        if (s.length() == 1) {
            return s;
        }
        char[] str = s.toCharArray();
        char[] mStr = manacherStr(str);
        int max = 1; // 最大回文長度至少是1
        int lM = 0; // 記錄最長回文的左邊界的上一個位置
        int rM = 0; // 記錄最長回文的有邊界的下一個位置
        for (int i = 1; i < mStr.length; i++) {
            int curMax = 1; // 當前的最大回文長度至少是1
            int l = i - 1;
            int r = i + 1;
            while (l >= 0 && r < mStr.length) {
                if (mStr[l] == mStr[r]) {
                    // 暴力擴充
                    l--;
                    r++;
                } else {
                    break;
                }
            }
            curMax = r - l - 1;
            if (curMax > max) {
                // 當前最長回文長度已經超過了max了
                // 更新中心值
                // 更新max值
                max = curMax;
                lM = l;
                rM = r;
            }
        }
        StringBuilder sb = new StringBuilder();
        for (int i = lM + 2; i < rM; i += 2) {
            sb.append(mStr[i]);
        }
        return sb.toString();
    }

    public static char[] manacherStr(char[] str) {
        final char c = '#';
        char[] mStr = new char[(str.length << 1) | 1];
        mStr[0] = c;
        mStr[1] = str[0];
        int index = 1;
        for (int i = 2; i < mStr.length; i++) {
            if ((i & 1) != 1) {
                mStr[i] = c;
            } else {
                mStr[i] = str[index++];
            }
        }
        return mStr;
    }
}

Manacher算法

Manacher算法可以用O(N)時間復雜度解決這個問題。同樣的,Manacher算法也需要對原始字符串進行上述的預處理過程。

相關變量說明

pArr

一個整型數組,長度和預處理串一樣,存每個位置的最長回文半徑是多少。

比如#A#B#B#A#這個字符串,

位於數組2號位置的A的回文半徑是A#或者#A, 長度為2,則pArr[2] = 2

位於數組4號位置的#的回文半徑是#B#A#或者#A#B#, 長度為5, 則pArr[4] = 5

其他位置以此類推。

通過pArr的定義,我們顯然可以得到如下結論

pArr[0] = 1

i

整型,當前遍歷到的位置,因為pArr[0]=1, 所以i可以從1開始遍歷。

r

整型,回文最右邊界,只要某個位置能擴到超過這個位置,就更新r這個值,初始值為0,因為一個字符串回文字符串至少是1,可以以第0個字符為中心且以0為最右邊界(即:第0個字符本身作為一個回文串)

c

整型,就是擴到r位置的的中心點,即pArr[c] = r - c + 1,初始值為0,與r的初始值定為0一樣的考慮。

流程

考慮i, r, c三個變量之間的位置關系,無非有以下兩種情況

情況1. ir外,比如初始狀態下:i=1, r,c = 0

情況2. ir內或者i==r

關於情況1,流程如暴力解法一樣,以i位置為中心,左右兩邊擴到不能再擴的位置,更新pArr[i],c, r的值。

關於情況2,我們假設i'i關於c對稱的點,r'r關於c對稱的點,示例圖如下:

image

細分如下幾種情況:

情況2-1

i'自己的回文區域都在[r'...r]內。

例如下圖中[6...10]i'的最長回文區域,左邊界並未超過r'

image

由此可以推出,由於i位置和i'位置是關於c位置對稱的,則i位置的回文區域至少包括[14...19]這一段,如下圖

image

pArr[i']至少等於pArr[i],接下來考慮i能否繼續擴散,即考慮19位置的值是否等於13位置的值,

我們可以假設19位置的值和13位置的值相等,同時,有如下兩個顯而易見的結論

  1. 19位置的值等於5位置的值。

  2. 13位置的值等於11位置的值。

推出5位置的值和11位置的值相等,那么由於我們前面假設i'只能擴散到最左6位置以及最右10位置,所以,推出的結論和我們的假設矛盾,所以,19位置的值不等於13位置的值

所以情況2-1的結論是:i的最長回文區域長度和i'的答案一樣, 即:pArr[i'] = pArr[i]

情況2-2

i'自己的回文區域在[r'...r]

如下圖

image

其中[2...14]范圍是以i'為中心的最長回文區域。

在情況2-2下,我們可以得到如下幾個結論:

  1. 根據ii'的關系,以i為中心,從[13...19]至少是回文的。

  2. 根據i'的回文區域,12位置的值等於4位置的值,以c為中心,4位置的值又等於20位置的值,所以12位置的值等於20位置的值,即以i為中心,最長回文區域還可以擴展到[12...20]

  3. 根據i'的回文區域,13位置的值等於3位置的值,以c為中心,13位置的值又等於11位置的值,3位置的值等於21位置上的值,所以11位置的值等於21位置的值,即以i為中心,最長回文區域還可以擴展到[11...21]

  4. 繼續判斷以i為中心,是否可以繼續擴散,即要繼續判斷10位置的值是否等於22位置的值,我們假設10位置的值等於22位置的值,以c為中心,10位置的值等於14位置的值,以i'為中心,14位置的值等於2位置的值,所以10位置的值等於2位置的值,根據我們的假設,2位置的值會等於22位置的值。這個與我們的前提矛盾了,因為我們的前提是c只能擴展到[3...21]這個區域,即:2位置的值不可能等於22位置的值,所以我們的假設不成立,所以10位置的值不等於22位置的值。

所以,情況2-2的結論是:ir的距離就是i的回文半徑,即:pArr[i] = r - i + 1

情況2-3

i'自己的回文區域左邊界和r'壓線

如下圖

image

其中[3...13]區域為以i'為中心能擴的最大回文區域。

有了情況2-2的鋪墊,i在情況2-3條件下至少可以擴充的范圍是[11...21], 但是接下來是否可以繼續擴充,還需要逐個判斷。

自此,所有情況考慮完畢。

由於i在遍歷過程中,始終不回退,所以,Manacher算法時間復雜度O(N)

完整代碼

public class LeetCode_0005_LongestPalindromicSubstring {
    
    public static String longestPalindrome(String s) {
        if (s == null || s.length() <= 1) {
            return s;
        }
        char[] str = s.toCharArray();
        char[] strs = manacherStr(str);
        int[] pArr = new int[strs.length];
        int c = 0;
        int r = 0;
        int i = 1;
        int len = strs.length;
        int max = 1;
        while (i < len) {
            // pArr[i] 至少不需要擴的大小
            pArr[i] = i < r ? Math.min(r - i, pArr[c - (i - c)]) : 1;
            // 暴力擴
            while (i + pArr[i] < len && i - pArr[i] >= 0) {
                if (strs[i + pArr[i]] == strs[i - pArr[i]]) {
                    pArr[i]++;
                } else {
                    break;
                }
            }
            // 擴散的位置能否更新回文有邊界R
            // 如果可以更新,則更新R,且把C置於當前的i,因為是當前的i讓回文右邊界擴散的
            if (i + pArr[i] > r) {
                r = i + pArr[i];
                c = i;
            }
            max = Math.max(pArr[i++], max);
        }

        // 定位最大回文有邊界的回文中心是哪個
        int n = 0;
        for (; n < len; n++) {
            if (pArr[n] == max) {
                break;
            }
        }

        // 構造最大回文子串
        StringBuilder sb = new StringBuilder();
        for (i = n - max + 2; i < n + max; i += 2) {
            sb.append(strs[i]);
        }
        return sb.toString();
    }

    public static char[] manacherStr(char[] str) {
        char[] strs = new char[str.length << 1 | 1];
        for (int i = 0; i < strs.length; i++) {
            strs[i] = ((i & 1) == 1) ? str[i >> 1] : '#';
        }
        return strs;
    }
}

相關習題

LeetCode_0005_LongestPalindromicSubstring

LintCode_0200_LongestPalindromicSubstring

LeetCode_0647_PalindromicSubstrings

LeetCode_0214_ShortestPalindrome

更多

算法和數據結構筆記

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM