最長回文子串

最長回文子串——Manacher 算法

1. 問題定義

最長回文字符串問題:給定一個字符串,求它的最長回文子串長度。
如果一個字符串正著讀和反著讀是一樣的,那它就是回文串。

舉個?? :

  • s="ababa", 最長回文長度為 5;即ababa
  • s="abccb", 最長回文長度為 4,即 bccb

2.暴力解法:

對于最長回文子串問題,最簡單粗暴辦法是:找到所有字符串的子串,遍歷每一個子串以驗證它們是否為回文串。一個子串由子串的起點和終點確定,因此對于一個長度為n的字符串,共有n^2個子串。這些子串的平均長度大約為n/2,因此這種解法的時間復雜度是O(n^3)

解法:

string findLongestPalindrome(string &s){

   if (s.empty()) {
       return "";
    }

    if (s.size() == 1) {
        return s;
     }
    // 字符串 長度
    unsigned long length = s.size();
    // 最長 回文 字符串 長度
    int maxLength = 0;
    // 最長 回文 字符串 起始地址
    int start = 0;

    for(int i = 0; i < length; i++){
        for (int j = i + 1; j < length; j++) {
            int tmp1, tmp2;
            // 判斷 是不是 回文
            for (tmp1 = i, tmp2 = j; tmp1 < tmp2; tmp1++, tmp2--) {
                if(s.at(tmp1) != s.at(tmp2)){
                    break;
                }
            }
            // 如果 遍歷 到中間 說明這是一個 回文 字符串
            if (tmp1 >= tmp2 && (j-i) >= maxLength) {
                maxLength = j - i + 1;
                start = i;
            }
        }
    }
    if (maxLength > 0) {
        return s.substr(start, maxLength);
    }
    return "";
}

3. 動態規劃

回文字符串的子串也是回文,所以對于母串s,我們用p[i][j] = 1(表示以i開始以j結束的子串)是回文字符串,那么p[i+1][j-1]也是回文字符串

  • 這樣當s[i] = s[j]時,如果p[i+1][j-1]回文子串,則p[i][j]也是回文子串。

  • 如果p[i+1][j-1]不是回文子串或者s[i] != s[j],那么p[i][j]就不是回文子串。

  • 特別地,對于這樣的字符串——只包含單個字符、或者兩個字符重復,其均為回文串

    p[i][i] = 1;
    c[i][i+1] = 1, if(s[i] == s[i+1])
    
  • 這樣需要額外的空間o(N^2),算法復雜度也是o(n^2).
    解法:

      static int const arrayLength = 100;
      string findLongestPalindrome(string &s){
          if (s.empty()) {
                return "";
          }
    
          if (s.size() == 1) {
               return s;
          }
          // 字符串 長度
          unsigned long length = s.size();
          // 最長 回文 字符串 長度
          int maxLength = 0;
          // 最長 回文 字符串 起始地址
          int start = 0;
          // 存儲 所有 子字符串
          bool p[arrayLength][arrayLength] = {false};
    
          for(int i = 0; i < length; i++){
              // 單個 字符 為 回文串
              p[i][i] = true;
              // 判斷 兩個字 重復 情況
              if ((i < length - 1) && s.at(i) == s.at(i+1)) {
                  p[i][i + 1] = true;
                  start = i;
                  maxLength = 2;
              }
          }
    
          // 子串 長度(因為已經計算了單個或兩個字重復的回文字符串,所以子串長度最低從3開始)
          // 計算 3 - length 所有子串中 所有最長子串
          for(int len = 3; len <= length; len++){
              // 子串 起始 地址
              // 在 字符串 中 找到 所有 長度為 len的子串并判斷
              for (int i = 0; i <= length - len; i++) {
                  int j = i + len - 1;
                  if (p[i+1][j-1] && s.at(i) == s.at(j)) {
                     p[i][j] = true;
                      maxLength = len;
                      start = i;
                  }
              }
          }
          if (maxLength >= 2) {
              return s.substr(start, maxLength);
          }
          return "";
         }
    

這種解法,當最大的回文子串有多個時,取最后一個,如果要取第一個,則在 start = i后面加上break;即可。

C語言解法:

static int const arrayLength = 100;
/**
 找到 最長 回文 子串 (動態 規劃 方法)
 
 @param string 字符串
 @param stringLength 字符串 長度
 */
void findLongestPalindromeTwo(char *string, int stringLength) {
    if (string == NULL || stringLength == 0) {
        return;
    }
    if (stringLength == 1) {
        printf("%s\n", string);
        return;
    }

    // 回文串 長度
    int maxLength = 0;
    // 起始 位置
    int startPosition = 0;
    // 輔助 數組(存儲 所有 字符串)
    int helperArray [arrayLength][arrayLength] = {false};
    
    // 循環 遍歷
    for (int tmpIndex = 0; tmpIndex < stringLength; tmpIndex++) {
        // 單個 字符 為 回文串
        helperArray[tmpIndex][tmpIndex] = true;
        if (string[tmpIndex] == string[tmpIndex + 1]) {
            // 相同 字符 比如 aa 也是 回文串
            helperArray[tmpIndex][tmpIndex + 1] = true;
            startPosition = tmpIndex;
            maxLength = 2;
        }
    }
    
    // 循環 遍歷
    // 子串 長度(因為已經計算了單個或兩個字重復的回文字符串,所以子串長度最低從3開始)
    // 計算 3 - length 所有子串中 所有最長子串
    for (int len = 3; len <= stringLength; len ++) {
        for (int i = 0; i <= stringLength - len; i ++) {
            int j = i + len - 1;
            if (helperArray[i + 1][j - 1] == true && string[i] == string[j]) {
                helperArray[i][j] = true;
                maxLength = len;
                startPosition = i;
            }
        }
    }
    
    if (maxLength > 1) {
        for (int tmpIndex = 0; tmpIndex < maxLength; tmpIndex++) {
            printf("%c", string[startPosition]);
            startPosition++;
        }
        printf("\n");
    }
}

4.中心擴展

  • 很明顯所有的回文字符串都是對稱的;

  • 長度為奇數回文字符串以最中間字符位置為對稱軸左右對稱。

  • 長度為偶數的回文串以中間兩個字符的空隙為對稱軸對稱。

  • 因此,整個字符串中所有字符,以及字符間的空隙都有可能是某個回文子串的對稱軸位置。可以遍歷這些位置,在每個位置上同時向左向右擴展,直到左右兩邊字符不同或者到達邊界。

  • 對于一個長度為n的字符串,這樣的位置一共有n+n-1=2n-1個,在每個位置上平均要進行大約n/4次比較,此算法的時間復雜度為o(n^2).

解法:

    string findLongestPalindrome(string &s) {
    
        if (s.empty()) {
            return "";
        }
    
        if (s.size() == 1) {
            return s;
        }
    
        unsigned long length = s.size();
    
        int maxlength = 0;
    
        int start = 0;
    
        string tmpStr = s;
        for(int i = 0,k = 0; i <= length; i++){
            s.insert(k, "#");
            k = k + 2;
        }

        for (int i = 0 ; i < length; i++) {
            // 間隔 兩個 字符
            int j = i - 1, k = i + 1;
            while (j >= 0 && k < length && s.at(j) == s.at(k)) {
                if ((k - j + 1) > maxlength) {
                    maxlength = k - j +1;
                    start = j;
                }
                j--;
                k++;
            }
        }
    
        if(maxlength > 0){
            int tmpMaxLength = (maxLength - 1)/2;
            int tmpStartPostion = start/2;
            return tmpStr.substr(tmpStartPostion,tmpMaxLength);
        }
        return "";
    }

C語言解法:

/**
 找到 最長 回文 子串 (中心 對稱 方法)

 @param string 字符串
 @param stringLength 字符串 長度
 */
void findLongestPalindrome(char *string, int stringLength) {
    if (string == NULL || stringLength == 0) {
        return;
    }
    if (stringLength == 1) {
        printf("%s\n", string);
        return;
    }
    
    // 插入 特殊字符 后字符串 長度
    int tmpStringLength = stringLength * 2 + 1;
    // 開辟 新的字符串
    char *tmpString = malloc(sizeof(char) * tmpStringLength);
    // 新字符串 復制 舊字符串 并在空隙插入 '#'
    tmpString[0] = '#';
    for (int tmpIndex = 0; tmpIndex < stringLength; tmpIndex ++) {
        tmpString[tmpIndex * 2 + 1] = string[tmpIndex];
        tmpString[tmpIndex * 2 + 2] = '#';
    }
    
    // 回文串 長度
    int maxLength = 0;
    // 起始 位置
    int startPosition = 0;
    
    // 遍歷 字符串
    for(int i = 0; i < tmpStringLength; i++) {
        int j = i - 1;
        int k = i + 1;
        while (j >= 0 && k < tmpStringLength && tmpString[j] == tmpString[k]) {
            if (k - j + 1 > maxLength ) {
                maxLength = k - j + 1;
                startPosition = j;
            }
            j --;
            k ++;
        }
    }
    
    if (maxLength > 1) {
        int tmpMaxLength = (maxLength - 1)/2;
        int tmpStartPostion = startPosition/2;
        
        for (int tmpIndex = 0; tmpIndex < tmpMaxLength; tmpIndex++) {
            printf("%c", string[tmpStartPostion]);
            tmpStartPostion++;
        }
        printf("\n");
    }
}

5. Manacher 算法

中心擴展的算法是存在缺陷的:

  • 由于回文字符串的奇偶性造成了不同性質的對稱軸位置,因此要分兩種情況進行處理。

  • 很多子串被重復多次訪問,造成較差的時間效率。
    舉個?? :

    s : a b a b a
    i : 0 1 2 3 4
    

i == 1i == 2時,左邊的子串aba分別被遍歷了一次。

A. 解決長度奇偶性帶來的對稱軸位置問題
Manacher算法首先對字符串做一個預處理,在所有的空隙位置(包括首尾)插入同樣的符號,要求這個符號是不會出現在原串中出現的,這樣會使得所有的串都是奇數長度的。已插入#號為例。

 aba  ———>  #a#b#a#
 abba ———>  #a#b#b#a#

插入的是同樣的符號,且符號不存在于原串,因此子串的回文性不受影響,原來是回文的串,插完之后還是回文的,原來不是回文的,依然不是回文的。

B. 解決重復訪問的問題
我們把一個回文中最左或最右位置的字符與其對稱軸的距離稱為回文半徑。Manacher定義了一個回文半徑數組RL,用RL[i]表示以第i個字符為對稱軸的回文串的回文半徑。我們一般對字符串從左往右處理,因此這里定義RL[i]為第i個字符為對稱軸的回文串的最右一個字符與字符i距離。對于上面插入分隔符之后的兩個串,可以得到RL數組。

   s:    # a # b # a #
 RL :    1 2 1 4 1 2 1
RL-1:    0 1 0 3 0 1 0
  i :    0 1 2 3 4 5 6

   s:     # a # b # b # a #
 RL :     1 2 1 2 5 2 1 2 1
RL-1:     0 1 0 1 4 1 0 1 0
  i :     0 1 2 3 4 5 6 7 8

上面我們還求了一下RL[i]-1。通過觀察可以發現,RL[i]-1的值,正是在原來那個沒有插入過分割符的串中,以位置i為對稱軸的最長回文串的長度。那么只要我們求出RL數組,就能得到最長回文子串的長度。

那么問題就變成了,怎樣高效地求RL數組,基本思路是利用回文串的對稱性,擴展回文串。
我們再引入一個輔助變量MaxRight,表示當前訪問到的所有回文子串,所能觸及的最右一個字符的位置。另外還要記錄下MaxRight對應的回文串的對稱軸所在位置,記為pos,它們的位置關系如下。

image.png

我們從左往右地訪問字符串來求RL,假設當前訪問到的位置是i,即要求RL[i],在對應上圖,i必然在pos右邊,但是我們更關注的是,i是在MaxRight的左邊還是右邊,我們分情況分析。

  • ** 當i在MaxRight的左邊**
    如下圖所示:
image.png

我們知道,圖中兩個紅色塊之間(包括紅色塊)的串是回文的;并且以i對稱軸的回文串,是與紅色塊間的回文串有所重疊的。我們找到i關于pos的對稱位置j,這個j對應RL[i]我們已經算過的。根據回文串的對稱性,以i為對稱軸的回文串和以j為對稱軸的回文串,有一部分是相同的。這里又有兩種細分情況。

a. 以j為對稱軸的回文串比較短,短到如下圖所示:

image.png

這時我們知道RL[i]至少不會小于RL[j],并且已經知道了部分的以i為中心的回文串,于是我們可以令RL[i]=RL[j].但是以i對稱軸的回文串可能實際上更長,因此我們試著以i為對稱軸,繼續向左右兩邊擴展,知道左右兩邊字符不同或者到達邊界。
b.以j為對稱軸的回文串很長,如下圖所示:

image.png

這時,我們只能確定,兩條藍線之間的部分(及不超過MaxRight的部分)是回文的,于是從這個長度開始,嘗試以i為中心向左右兩邊擴展,知道左右兩邊字符不同或者到達邊界。

綜上,我們只能獲取RL[2*pos - i]MaxRight-1這兩者中最小的值,來保證該范圍內的字符串是回文字符串,RL[i] = min(RL[2*pos - i], MaxRight-1),之后都要嘗試更新MaxRightpos,因為有可能得到更大MaxRight.

具體操作如下:

step 1: 令RL[i]=min(RL[2*pos-i], MaxRight-i)
step 2: 以i為中心擴展回文串,直到左右兩邊字符不同,或者到達邊界。
step 3: 更新MaxRight和pos
  • iMaxRight的右邊
    遇到這種情況,說明以i為對稱軸的回文串還沒有任何一部分被訪問過,于是只能從i的左右兩邊開始嘗試擴展了,當左右兩邊字符不同或者到達字符串邊界時停止更新。然后更新MaxRight和pos。
    解法:

    string findLongestPalindrome3(string &s) {
    
          if (s.empty()) {
              return "";
          }
    
          if (s.size() == 1) {
              return s;
          }
    
          unsigned long length = s.size();
    
          int MaxRight = 0;
          int Maxlen = 0;
          int pos = 0;
          string tmpStr = s;
          for(int i = 0,k = 0; i <= length; i++){
              s.insert(k, "#");
              k = k + 2;
          }
          length = s.size();
    
          int *RL = new int[length]();
          memset(RL, 0x00, sizeof(length));
          for (int i = 0; i < length; i++) {
              if (i < MaxRight) {
                  RL[i] = min(RL[2*pos - i], MaxRight-1);
              }else {
                  RL[i] = 1;
              }
              while (i - RL[i] >= 0 && i+RL[i] < length && s[i - RL[i]] == s[i + RL[i]]) {
                  RL[i] += 1;
              }
              if (RL[i] + i - 1 > MaxRight) {
                  MaxRight = RL[i] + i - 1;
                  pos = i;
              }
              Maxlen = max(Maxlen, RL[i]);
          }
          if (Maxlen > 0) {
              return tmpStr.substr((pos+1)/2 - Maxlen/2, Maxlen - 1);
          }
          free(RL);
          return "";
      }
    

C語言解法:

/**
 找到 最長 回文 子串 (拉馬車 方法)
 
 @param string 字符串
 @param stringLength 字符串 長度
 */
void findLongestPalindromeThree(char *string, int stringLength) {
    if (string == NULL || stringLength == 0) {
        return;
    }
    if (stringLength == 1) {
        printf("%s\n", string);
        return;
    }

    // 插入 特殊字符 后字符串 長度
    int tmpStringLength = stringLength * 2 + 1;
    // 開辟 新的字符串
    char *tmpString = malloc(sizeof(char) * tmpStringLength);
    // 新字符串 復制 舊字符串 并在空隙插入 '#'
    tmpString[0] = '#';
    for (int tmpIndex = 0; tmpIndex < stringLength; tmpIndex ++) {
        tmpString[tmpIndex * 2 + 1] = string[tmpIndex];
        tmpString[tmpIndex * 2 + 2] = '#';
    }
    
    
    // 記錄 最長 半徑 范圍
    int maxRight = 0;
    // 記錄 最長 回文 字符串 長度
    int maxLength = 0;
    // 當前 最長 半徑 的 對稱軸
    int currentPosition = 0;
    // 記錄 每個 位置 最長回文 長度
    int *palindromeArray = malloc(sizeof(int) * tmpStringLength);
    memset(palindromeArray, 0x00, sizeof(tmpStringLength));
    
    // 遍歷 字符串
    for (int tmpIndex = 0; tmpIndex < tmpStringLength; tmpIndex++) {
        // 當前 字符 在最大 半徑 范圍 左邊
        if (tmpIndex < maxRight) {
            if (palindromeArray[2*currentPosition - tmpIndex] > maxRight - tmpIndex) {
                palindromeArray[tmpIndex] = maxRight - tmpIndex;
            }
            else {
                palindromeArray[tmpIndex] = palindromeArray[2*currentPosition - tmpIndex];
            }
        }
        // 當前 字符 在 最大半徑 范圍 右邊(沒有被遍歷過)
        else {
            palindromeArray[tmpIndex] = 1;
        }
        
        // 在先前 計算的 回文長度 基礎 上 擴展遍歷
        while (tmpIndex - palindromeArray[tmpIndex] >= 0 &&
               tmpIndex + palindromeArray[tmpIndex] < tmpStringLength &&
               tmpString[tmpIndex - palindromeArray[tmpIndex]] == tmpString[tmpIndex + palindromeArray[tmpIndex]]) {
            palindromeArray[tmpIndex] += 1;
        }
        
        if (palindromeArray[tmpIndex] + tmpIndex - 1 > maxRight) {
            maxRight = palindromeArray[tmpIndex] + tmpIndex - 1;
            currentPosition = tmpIndex;
        }
        
        // 更新 長度
        if (maxLength < palindromeArray[tmpIndex]) {
            maxLength = palindromeArray[tmpIndex];
        }
    }
    
    if (maxLength) {
        int tmpMaxLength = maxLength - 1;
        int tmpStartPostion = (currentPosition + 1)/2 - maxLength/2;
        
        for (int tmpIndex = 0; tmpIndex < tmpMaxLength; tmpIndex++) {
            printf("%c", string[tmpStartPostion]);
            tmpStartPostion++;
        }
        printf("\n");
    }
    
}

C.復雜度分析:

  • 空間復雜度:插入分隔符行程新串,占用了線性的空間大小;RL數組也占用線性 大小的空間,因此空間復雜度是線性的。

  • 時間復雜度:盡管代碼里面有兩層循環,由于內層循環只是對尚未匹配的部分進行,因此對于每一個字符而言只會進行一次,因此時間復雜度是o(n).

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內容

  • 問題定義 最長回文子串問題:給定一個字符串,求它的最長回文子串長度。 解法1:暴力解法 找到字符串的所有子串,判斷...
    HITMiner閱讀 693評論 0 2
  • 這次要記錄的是一個經典的字符串的題目,也是一個經典的馬拉車算法的實踐。相信在很多地方都會考到或者問到這道題目,這道...
    檸檬烏冬面閱讀 2,924評論 0 9
  • 最長回文串問題是一個經典的算法題。 0. 問題定義 最長回文子串問題:給定一個字符串,求它的最長回文子串長度。如果...
    曾會玩閱讀 4,057評論 2 25
  • 問題:給定一個字符串,求它的最長回文子串長度。提示:如果一個字符串正著讀和反著讀是一樣的,那它就是回文串。下面是一...
    KevinHwong閱讀 520評論 0 0
  • 上一篇KMP算法之后好幾天都沒有更新,今天介紹最長回文子串。 首先介紹一下什么叫回文串,就是正著讀和倒著讀的字符順...
    zero_sr閱讀 2,321評論 2 8