算法-字符串之最長回文子串

上一篇KMP算法之后好幾天都沒有更新,今天介紹最長回文子串。

首先介紹一下什么叫回文串,就是正著讀和倒著讀的字符順序都是一樣的,eg:level,noon。而回文子串,顧名思義,就是主串中滿足回文性質的子串。

求解的常規思想,就是先求出主串的所有子串,在判斷是否是回文串,然后選出最長的,這一種方法的時候復雜度較高,是O(n^3),所以一般不采用這種方法,下面介紹兩種方法求解。

1. 中心擴展法

中心擴展法可以說是常規算法的改進。首先我們知道,回文串是中心對稱的,相比從頭到尾遍歷字符串的方法,從中間開始向兩邊擴展,時間會減少一半。
算法思想:把主串中的每一個字符當做回文串的中心,向兩邊擴展,求出最長的回文子串。其中要注意奇數位的回文子串和偶數位的回文子串的區別。eg:aba的中心是b,而abba的中心應該是bb。使用中心擴展法的時間復雜度是O(n^2),空間復雜度是O(1)。

代碼
核心算法是l2r的部分,以傳入的mid為回文串的中心計算最長的回文子串,其中需要注意的地方有兩點:

  1. l2r中的第一個while循環,之前提到過要注意奇數位的回文串和偶數位的回文串,在代碼中,判斷中心點的字符和右邊的字符是否相等,就可以跳過偶數位的中心點。

  2. 注意l2r中返回語句,從第二個while循環中跳出的時候,已經多進行了一步left--,right++的操作了。

int longest;//子串長
int start;//最長回文子串在主串中的起始位置

/*計算以mid為中心的最長回文子串*/
int l2r(char *string, int mid) {
    int len = strlen(string);
    int left = mid - 1, right = mid + 1;
    //跳過相同的部分
    while (string[right] == string[mid])
        right++;
    while (left > 0 && right < len){
        if (string[left] == string[right]) {
            //從中間開始算,各向外移一位
            start = left;
            left--;
            right++;
     }
        else {
            break;
        }
    }
    //重置回文串的起始位置
    if (left < 0)
        start = 0;
    //printf("\tstart:%d left:%d right:%d len:%d\n", start, left, right, right - 1 - (left + 1) + 1);
    //跳出while循環的時候,要么是index不滿足條件,要么是當前right和left的位置字符不等,都要在將right,left還原成刪一個狀態才能計算回文串長
    return right - 1 - (left + 1) + 1;
}

/*計算最長回文子串的長度*/
int longestPald(char *string) {
    int len = strlen(string);
    if (string == NULL || len == 0)
        assert("ERROR");
    if (len == 1){
        longest = 1;
        start = 0;
        return 1;
    }

    for (int i = 0; i < len; i++){//遍歷整個主串,以它的每一位為回文串的中心,計算最長的回文串
        int tempLen = l2r(string, i);
        if (tempLen > longest) {
            longest = tempLen;
        }
    }
    return 0;
}

int main(void)
{
    char string[] = "abcdeedcbdac";
    longestPald(string);
        printf("\t%s的最長回文子串:\n", string);       
        printf("\t起始位置:%d  串長:%d\n", start, longest);
    system("pause");
    return 0;
}

結果:

abcdeedcbdac的最長回文子串:bcdeedcb

2. 動態規劃法???

之前看到網上有很多用動態規劃法求解最長回文子串的,但是我看了之后覺得有問題。動態規劃法中是用二維矩陣保存回文串長,c[i][j]表示主串中s[i...j]是回文串,當前位置的c[i][j]需要依賴于c[i+1][j-1],但是有的地方c[i+1][j-1]是不知道的,反而覺得用遞歸來計算矩陣c會更好。不知道是我理解錯誤還是這個方法確實不對。如果有用動態規劃法求解出最長回文子串的,還請賜教~

3. Manacher算法

這是幾個方法中最為高效的方法,時間復雜度為O(n).Manacher算法也是利用回文串的對稱性,標記回文串的中間位,向兩邊遍歷。同樣是標記中間位,向兩邊遍歷,那它和中心擴展法有什么區別呢?

區別:中心擴展法的思想是以主串的每一個字符為中心,計算最長的回文子串,外層循環執行n次,內存循環至多2/n次;而Manacher的中心字符并不是這樣的,Manacher利用之前計算過的回文子串,巧妙的計算出新的中心點。但同時它也做出了一些折中的處理,比如說,要確定唯一的中心點,所以要擴展主串。

算法思想:Manacher采用從中間向兩邊遍歷得到最長回文子串的思想,將原來的主串進行擴展,這個算法嚴格要求對稱,只允許有一個中心點。eg:abc-- > #a#b#c#,這樣不管回文串是奇數位還是偶數位都都會變成奇數位的,滿足只有一個中心字符的要求。Manacher利用之前計算的回文子串,避免了一些重復的回文子串的計算。

輔助變量:
既然要利用之前求得信息,就需要記錄。
p[]:數組p保存的是主串中以某個字符為中心的最長回文子串的半徑,eg:p[i]存儲的是以str[i]為中心的最長回文串的半徑,這個半徑值是在擴展之后的字符串中
mid:保存得到的回文串的中心點。
max:保存當前的回文串的影響范圍,也就是這個回文串的右邊界。
注:mid和max的值是由最長回文串計算得到的。

現在,我們來看一下str和p的關系,便于理解。s是在原來的字符串


s和p的關系

接下來計算p[],這時要用到max和mid。先解釋一下最難懂的地方。利用之前計算的回文子串的信息計算當前的p[i],現則最小的值。

p[i] = (max - i) > p[j] ? p[j] : (max-i);

解釋:(以下解釋摘自另一篇博客)
1.當 mx - i > P[j] 的時候,以S[j]為中心的回文子串包含在以S[id]為中心的回文子串中,由于 i 和 j 對稱,以S[i]為中心的回文子串必然包含在以S[id]為中心的回文子串中,所以必有 P[i] = P[j],見下圖。

當 mx - i > P[j] 的時候

2.當 P[j] > mx - i 的時候,以S[j]為中心的回文子串不完全包含于以S[id]為中心的回文子串中,但是基于對稱性可知,下圖中兩個綠框所包圍的部分是相同的,也就是說以S[i]為中心的回文子串,其向右至少會擴張到mx的位置,也就是說 P[i] >= mx - i。至于mx之后的部分是否對稱,就只能一個一個匹配了。
P[j] > mx - i 的時候

接下來解釋算法為線性的原因:(算法中其實有兩層循環)
image.png

代碼:
代碼中有幾個需要注意的地方:

  1. 在pre函數中,擴展主串時,擴展串的第一個位置是'$',這是為了諸侯方便處理越界的問題。而字符串越界會出現在哪里呢?就是manacher中的為一個一個while循環那里。
  2. 注意重置longest和start時候的值,在介紹str和p的關系的時候已經提到過p[i]-1的意義,在設置longest和start時要考慮到這個關系。(longest是最長回文子串的長度,start是其在原串中的下標)。
  3. 理解p[i] = (max - i) > p[j] ? p[j] : (max-i);很重要,結合代碼中的注釋和上面的圖多理解。
int pre(char *string, char *strCopy) {
    strCopy[0] = '$';
    int j = 1;
    for (int i = 0; i < strlen(string); i++) {
        strCopy[j++] = '#';
         strCopy[j++] = string[i];
}
    strCopy[j] = '#';
    return j + 1;//表示strCopy的長度
}


void manacher(char *str,int n) {
    int p[MAXLEN];//數組p中保存字符串str中以某一點為中心點的最長回文子串的半徑
    p[0] = 0;//p[0]對應str[0]-->$
    //max存儲之前計算的回文子串的右邊界,mid保存當前的回文子串的中心,這兩個值都不一定是最長回文子串求得
    int max = 0, mid = 1;

    for (int i = 1; i < n; i++) {
        if (max > i)
        {
            int j = 2 * mid - i;//j是字符串中i關于mid的對稱點
            p[i] = (max - i) > p[j] ? p[j] : (max - i);//!!!
        }
        else {//否則max<i,說明i不包含在當前計算的回文串中,
             //那么就不能用便捷方法來計算p[i],只能一個一個計算
            p[i] = 1;//初始值為1
        }

        //基于當前以i為中心的回文串的半徑,計算下一個位置的字符是否滿足回文。這里會出現越界的問題!!!
        while (str[i - p[i]] == str[i + p[i]]){
            p[i]++;
         }

        if ((i + p[i]) > max){
            max = i + p[i];//當前計算得到的回文串已經大于之前計算的邊界了,重置邊界
            mid = i;
        }

        if (longest < p[i] - 1){//p[i]-1就是原串中以i為中心的回文串的長度
            longest = p[i] - 1;
            //在遇到最長回文子串包含第0個字符的時候,start計算得-1,所以這里要處理一下
            if ((i - p[i] - 1) < 0)
                start = 0;
            else
                start = i - p[i] - 1;
        }
     }
}

int main(void)
{
    char string[] = "acab";//"abcdeedcbdac";
    char strCopy[MAXLEN];
    int len = pre(string, strCopy);

    printf("原串:%s -->", string);
    //輸出strCopy
    for (int i = 0; i < len; i++){
        printf("%c", strCopy[i]);
    }
    printf("\n");
    manacher(strCopy,len);
    printf("\t%s的最長回文子串:\n", string);
    printf("\t起始位置:%d  串長:%d\n", start, longest);
    system("pause");
    return 0;
}

結果:

manacher算法的結果

總結

好了,這次就到這里了。不足之處,歡迎指正。

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

推薦閱讀更多精彩內容