mancher入門-- mancher模板+解釋


一:背景

  給定一個字符串,求出其最長迴文子串。例如:
  (1)s="abcd",最長迴文長度爲 1;
  (2)s="ababa",最長迴文長度爲 5;
  (3)s="abccb",最長迴文長度爲 4,即 bccb。
  以上問題的傳統思路大概是,遍歷每一個字符,以該字符爲中點向兩邊查找。其時間複雜度爲$O(n^2)$,很不高效。而在1975年,一個叫Manacher的人發明了一個算法,Manacher算法,也稱馬拉車算法,該算法可以把時間複雜度提升到$O(n)$。下面來看看馬拉車算法是如何工作的。

二:算法過程分析

  由於迴文分爲偶迴文(比如 bccb)和奇迴文(比如 bcacb),而在處理奇偶問題上會比較繁瑣,所以這裏我們使用一個技巧,在字符間插入一個字符(前提這個字符未出現在串裏)。舉個例子:s="abbahopxpo",轉換爲s_new="$#a#b#b#a#h#o#p#x#p#o#"(這裏的字符 $ 只是爲了防止越界,下面代碼會有說明),如此,s 裏起初有一個偶迴文abba和一個奇迴文opxpo,被轉換爲#a#b#b#a##o#p#x#p#o#,長度都轉換成了奇數。
  定義一個輔助數組int p[]p[i]表示以s_new[i]爲中心的最長迴文的半徑,例如:

i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
s_new[i] $ # a # b # b # a # h # o # p # x # p # o #
p[i]   1 2 1 4 5 2 1 2 1 2 1 2 1 2 1 6 1 2 1 2 1

可以看出,p[i]-1正好是原字符串中最長迴文串的長度。
  Manacher算法之所以快,就快在對 p 數組的求法上有個捷徑。在我們解決了奇偶迴文的繁瑣時,剩下的難點就是求 p 數組,按照普通思維,我們是這樣求解的:求解p[i],先初始化p[i]=1,再以s_new[i]爲中心判斷兩邊是否相等,相等就p[i]++。這就是普通的思維,但是我們想想,能否讓p[i]的初始化不是 1,讓它更大點,看下圖:

  設置兩個變量,mx 和 id 。
  mx 代表以s_new[id]爲中心的最長迴文最右邊界,也就是mx=id+p[id]
  假設我們現在求p[i],也就是以s_new[i]爲中心的最長迴文半徑,如果i<mx,如上圖,那麼:

 if (i < mx)  
            p[i] = min(p[2 * id - i], mx - i);

2 * id -i其實就是等於 j ,p[j]表示以s_new[j]爲中心的最長迴文半徑,見上圖,因爲 i 和 j 關於 id 對稱,我們利用p[j]來加快查找。

三:代碼

/**
 * 
 * author 劉毅(Limer)
 * date   2017-02-25
 * mode   C++ 
 */
#include<iostream>  
#include<string.h>
#include<algorithm>  
using namespace std;

char s[1000];
char s_new[2000];
int p[2000];

int Init()
{
    int len = strlen(s);
    s_new[0] = '$';
    s_new[1] = '#';
    int j = 2;

    for (int i = 0; i < len; i++)
    {
        s_new[j++] = s[i];
        s_new[j++] = '#';
    }

    s_new[j] = '\0';  //別忘了哦  

    return j;  //返回s_new的長度  
}

int Manacher()
{
    int len = Init();  //取得新字符串長度並完成向s_new的轉換  
    int maxLen = -1;   //最長迴文長度  

    int id;
    int mx = 0;

    for (int i = 1; i < len; i++)
    {
        if (i < mx)
            p[i] = min(p[2 * id - i], mx - i);  //需搞清楚上面那張圖含義, mx和2*id-i的含義
        else
            p[i] = 1;

        while (s_new[i - p[i]] == s_new[i + p[i]])  //不需邊界判斷,因爲左有'$',右有'\0'  
            p[i]++;


        if (mx < i + p[i])  //我們每走一步i,都要和mx比較,我們希望mx儘可能的遠,這樣才能更有機會執行if (i < mx)這句代碼,從而提高效率  
        {
            id = i;
            mx = i + p[i];
        }

        maxLen = max(maxLen, p[i] - 1);
    }

    return maxLen;
}

int main()
{
    while (printf("請輸入字符串:\n"))
    {
        scanf("%s", s);
        printf("最長迴文長度爲 %d\n\n", Manacher());
    }

    return 0;
}

四:算法複雜度分析

  文章開頭已經提及,Manacher算法爲線性算法,即使最差情況下其時間複雜度亦爲$O(n)$,在進行證明之前,我們還需要更加深入地理解上述算法過程。
  定義 mx 爲以s_new[id]爲中心的最長迴文最右邊界,也就是mx=id+p[id]。j 與 i 關於 id 對稱,根據迴文的性質,p[i]的值基於以下三種情況得出:
  (1)j 的迴文串有一部分在 id 的之外,如下圖:

上圖中,黑線爲 id 的迴文,i 與 j 關於 id 對稱,紅線爲 j 的迴文。那麼根據代碼此時p[i]=mx-i,即紫線。那麼p[i]還可以更大麼?答案是不可能!見下圖:

假設右邊新增的紫色部分是p[i]可以增加的部分,那麼根據迴文的性質,a 等於 d ,也就是說 id 的迴文不僅僅是黑線,而是黑線+兩條紫線,矛盾,所以假設不成立,故p[i]=mx-i,不可以再增加一分。
  (2)j 迴文串全部在 id 的內部,如下圖:

此時p[i]=p[j],那麼p[i]還可以更大麼?答案亦是不可能!見下圖:

假設右邊新增的紅色部分是p[i]可以增加的部分,那麼根據迴文的性質,a 等於 b ,,也就是說 j 的迴文應該再加上 a 和 b ,矛盾,所以假設不成立,故p[i]=p[j],也不可以再增加一分。
  (3)j 迴文串左端正好與 id 的迴文串左端重合,見下圖:

此時p[i]=p[j]p[i]=mx-i,並且p[i]還可以繼續增加,所以需要

while (s_new[i - p[i]] == s_new[i + p[i]]) 
    p[i]++;

  根據(1)(2)(3),很容易推出Manacher算法的最壞情況,即爲字符串內全是相同字符的時候。在這裏我們重點研究Manacher()中的for語句,推算髮現for語句內平均訪問每個字符5次,即時間複雜度爲:$T_{worst}(n)=O(n)$。
  同理,我們也很容易知道最佳情況下的時間複雜度(最佳情況即字符串內字符各不相同)。推算得平均訪問每個字符4次,即時間複雜度爲:$T_{best}(n)=O(n)$。
  綜上,Manacher算法的時間複雜度爲$O(n)$



下面是我調試的過程:


#include<iostream>
#include<string.h>
#include<iomanip>
#include<string>
using namespace std;
char s[1000],s_new[1000];
int p[2000];
int i,j,k;

int init()
{
	int len=strlen(s);
	s_new[0]='$';
	s_new[1]='#';
	int j=2;
	
	for(i=0;i<len;i++)
	{
		s_new[j++]=s[i];
		s_new[j++]='#';
	}
	s_new[j]='\0';
	cout<<"構成的字符串爲:"<<endl;
	cout<<setw(8)<<"序列號p[i]:";
	for(i=0;i<j;i++)
		cout<<setw(4)<<i<<" ";
	cout<<endl;
	cout<<setw(8)<<"字符串:    ";
	for(i=0;i<j;i++)
		cout<<setw(4)<<s_new[i]<<" ";
	cout<<endl<<endl;
	return j;
}
int mancher()
{
	int len=init();  //取得新字符串長度並完成向s_new的轉換
	int maxlen=-1;  //最長迴文長度
	int id;
	int mx=0;
	
	for(i=1;i<len;i++)
	{
		if(i<mx)
		{
			p[i]=min(p[2*id-i],mx-i);
			cout<<"if(i<mx)p[i]="<<p[i]<<"     mx="<<mx<<"   i="<<i<<endl;
		}
        else
		{
			p[i]=1;
			cout<<"if(i>mx)="<<p[i]<<"     mx="<<mx<<"   i="<<i<<endl;
		}

		cout<<"判斷中和兩邊:"<<p[i];
		while(s_new[i-p[i]]==s_new[i+p[i]])//無需邊界判斷
		{
			p[i]++;
			cout<<" "<<p[i];
		}
		cout<<endl;

		if(mx<i+p[i]) //我們每走一步i 都要和mx比較 我們希望mx儘可能原,這樣猜能更有機會執行 if(i<mx)這個語句 從而提升他效率
		{
			id=i;  //將當前的中心設置爲 i
			mx=i+p[i]; //mx則爲當前爲中心迴文串的右邊界
			cout<<"    mx<p[i]+i     當前更新 mx="<<mx<<"    中心 id="<<id<<endl;
		}
		maxlen=max(maxlen,p[i]-1);
		cout<<"當前的迴文cd:"<<p[i]<<"      真正的cd:"<<p[i]-1<<"      更新過的cd:"<<maxlen<<endl;
		cout<<endl;
	}
	return maxlen;
}


int main()
{
	while(cout<<"請輸入字符串:"<<endl)
	{
		cin>>s;
		int cd=mancher();
		cout<<cd<<endl<<endl;
	}
	return 0;
}




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