演算法之字串——最長迴文子串

it_was發表於2020-10-02

難度中等:smirk:
給定一個字串 s,找到 s 中最長的迴文子串。你可以假設 s 的最大長度為 1000。

眾所周知,迴文子串是以某一軸為中心,左右呈映象對稱,如何找到 s 中最長的迴文子串呢?

暴力破解是最容易想到的一種解法,即以每個字元為中心,向左右兩邊擴,看擴出去的範圍有多大,同時記錄一個最大值。比如 s = "abhba",最大回文子串即以 字元 h 為中心,左右擴到底。然而這種方法有個問題,無法應對偶迴文:cold_sweat:,當 s ="abba"的時候,以每個字元為中心擴結果得到的最長迴文子串長度為 1 !明顯是錯誤的!
所以為了解決上述無法應對偶迴文的情況,需要引入特殊字元來解決,即在每一個字元左右新增上一個特殊字元進行佔位:smiley:,這樣就可以解決這個問題
上述的 s ="abba" 就變成了 s ="#a#b#b#a#",這樣我們就可以透過暴力法解決!

以下為暴力法的程式碼

public char[] getSPString(String s){
        int len = s.length();
        char[] arr = new char[2 * len + 1];
        for(int i = 0 ;i < len;i++){
            arr[2 * i] = '#';
            arr[2 * i + 1] = s.charAt(i);
        }
        arr[2 * len] = '#';
        return  arr;
    }
    public String longestPalindrome(String s) {
        if(s == null || s.length() <= 1){
            return s;
        }
        char[] res = getSPString(s);
        int len = res.length;
        int C = 0;   //最長迴文串中心
        int max = 1;  //最長迴文串長度

        for(int i = 0; i< len;i++){
            int j = 1;
            while(i - j >=0 && j + i < len){
                if(res[i - j] ==  res[i + j]){
                    j++;
                }else{
                    break;
                }
            }
            if(2 * j - 1 > max){
                C = i;
                max = 2 * j - 1;
            }   
        }
        return s.substring((C - max/2)/2 , (C+ max/2)/2);
    }

演算法之字串——最長迴文子串

暴力法的缺點在於,最壞情況下時間複雜度趨近於 O(n^2)!
而馬拉車演算法的關鍵在於透過定義一個最大右邊界,不斷地判斷當前位置地情況,然後尋找當前位置地對稱點,利用之前已經求出的資訊,加速整個判斷過程!注意:此過程也需要預先轉換成特殊字串進行處理!!!

:one:首先我們定義幾個概念::eyes: :eyes: :eyes:
  1. 最大回文右邊界 R :即當前字串中的迴文子串達到的最右位置。例如 “#a#b#c#b#a#c#”,當到達第一個字元 ‘c’ 時,最大回文右邊界為下標10!
  2. 最大回文半徑 max_Radius 和 存放對應位置的最大回文半徑arr[ ] 陣列: 迴文半徑就是當前迴文中心到達右邊界或者左邊界的距離。如下圖所示

演算法之字串——最長迴文子串

  1. 迴文中心 C :即當前最大右邊界R的情況下,迴文串的中心,與最大會問右邊界互為對應關係。
:two:接下來就是加速過程!
  1. 當前位置 i 在最大回文右邊界 R 的外邊——以當前位置為中心往外擴,並更新 RC
    當遍歷到第一個字元’#’時,發現不能往外擴,即最大回文右邊界就是下標 0 ,然後碰到 字元 ‘a’的時候,發現 i 大於 R ,故需要往外擴,然後發現能擴出去,即此時 R 到了下標 2! C 也更新為了 i !

演算法之字串——最長迴文子串

  1. 當前位置 i 在最大回文右邊界 R 的裡邊——判斷 i 的對稱點 i’ 的情況!
    此時需要分三種情況:

    • 以 i’ 為中心的最大回文子串完全在最大左邊界即將最大右邊界RC為中心對稱過去)裡邊!如下圖,此時 i 位置的最大回文半徑 與 i’ 一樣,都為 2!此時時間複雜度為O(1)

    • 以 i’ 為中心的最大回文子串超出最大左邊界!如下圖所示,此時 i’ 的迴文串已經超出 L 到達了 左邊的 K 字元,那麼此時 i 的最大回文半徑就是 R - i ,為什麼此時不用擴呢?其實可以證明,如果此時可以擴,那麼只有一種情況, 最右邊的不是 E 而是 K ,但是如果是 K 的話,此時違反了 R 的最大回文右邊界 的定義!即 R 一定至少大於等於最右邊 E 的位置!所以此時 L 的左邊一個字元和 R 右邊的字元一定不等! 此時時間複雜度為O(1)

    • 以 i’ 為中心的最大回文子串正好在最大左邊界上!即壓線!如下圖所示。此時以i位置為中心,以arr[i’]為半徑,需要再往外擴!因為這時不確定 R 右邊的字元是否滿足條件!

綜上,情況就是分為兩大類,i 在迴文右邊界裡邊和外邊!需要擴的兩種情況,即 i 在 R 外邊和壓線,都是在不斷地往右推動這個 R,所以整個過程總起來看就是在不斷地往右推動著 R 前進,因為 R最多能到最右邊,故整體的時間複雜度為 O(n)級別的!!!!!:boom: :boom: :boom:

下面直接上程式碼

public void manacher(String s) {
        if (s == null || s.length() <= 1) {
            return;
        }
        char[] sp = getSPString(s);
        int len = sp.length;
        int[] arr = new int[sp.length]; //最長迴文半徑陣列
        int R = -1; //最大回文右邊界
        int C = -1; //最大回文中心
        int max = -1; // 最長迴文半徑
        for (int i = 0; i < len; i++) {
            arr[i] = R > i ? Math.min(arr[2 * C - i] , R - i) : 1;
            while(i + arr[i] < len && i - arr[i] >= 0){
                if(sp[i + arr[i]] == sp[i - arr[i]]){
                    arr[i] ++;
                } else{
                    break;
                }
            }
            if(i + arr[i] > R){
                R = i +arr[i];
                C = i;
            }
            if(max < arr[i]){
                max  = arr[i];
                center = i;
            }
        }
    }

演算法之字串——最長迴文子串

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章