Manacher算法能夠在O(N)的時間復雜度內得到一個字符串以任意位置為中心的回文子串。其算法的基本原理就是利用已知回文串的左半部分來推導右半部分。
轉:http://blog.sina.com.cn/s/blog_70811e1a01014esn.html
首先,在字符串s中,用rad[i]表示第i個字符的回文半徑,即rad[i]盡可能大,且滿足:
s[i-rad[i],i-1]=s[i+1,i+rad[i]]
很明顯,求出了所有的rad,就求出了所有的長度為奇數的回文子串.
至於偶數的怎么求,最后再講.
假設現在求出了rad[1..i-1],現在要求后面的rad值,並且通過前面的操作,得知了當前字符i的rad值至少為j.現在通過試圖擴大j來掃描,求出了rad[i].再假設現在有個指針k,從1循環到rad[i],試圖通過某些手段來求出[i+1,i+rad[i]]的rad值.
根據定義,黑色的部分是一個回文子串,兩段紅色的區間全等.
因為之前已經求出了rad[i-k],所以直接用它.有3種情況:
①rad[i]-k<rad[i-k]
如圖,rad[i-k]的范圍為青色.因為黑色的部分是回文的,且青色的部分超過了黑色的部分,所以rad[i+k]肯定至少為rad[i]-k,即橙色的部分.那橙色以外的部分就不是了嗎?這是肯定的.因為如果橙色以外的部分也是回文的,那么根據青色和紅色部分的關系,可以證明黑色部分再往外延伸一點也是一個回文子串,這肯定不可能,因此rad[i+k]=rad[i]-k.為了方便下文,這里的rad[i+k]=rad[i]-k=min(rad[i]-k,rad[i-k]).
②rad[i]-k>rad[i-k]
如圖,rad[i-k]的范圍為青色.因為黑色的部分是回文的,且青色的部分在黑色的部分里面,根據定義,很容易得出:rad[i+k]=rad[i-k].為了方便下文,這里的rad[i+k]=rad[i-k]=min(rad[i]-k,rad[i-k]).
根據上面兩種情況,可以得出結論:當rad[i]-k!=rad[i-k]的時候,rad[i+k]=min(rad[i]-k,rad[i-k]).
注意:當rad[i]-k==rad[i-k]的時候,就不同了,這是第三種情況:
如圖,通過和第一種情況對比之后會發現,因為青色的部分沒有超出黑色的部分,所以即使橙色的部分全等,也無法像第一種情況一樣引出矛盾,因此橙色的部分是有可能全等的,但是,根據已知的信息,我們不知道橙色的部分是多長,因此就把i指針移到i+k的位置,j=rad[i-k](因為它的rad值至少為rad[i-k]),等下次循環的時候再做了.
整個算法就這樣.
至於時間復雜度為什么是O(n),我已經證明了,但很難說清楚.所以自己體會吧.
上文還留有一個問題,就是這樣只能算出奇數長度的回文子串,偶數的就不行.怎么辦呢?有一種直接但比較笨的方法,就是做兩遍(因為兩個程序是差不多的,只是rad值的意義和一些下標變了而已).但是寫兩個差不多的程序是很痛苦的,而且容易錯.所以一種比較好的方法就是在原來的串中每兩個字符之間加入一個特殊字符,再做.如:aabbaca,把它變成(#a#a#b#b#a#c#a#),左右的括號是為了使得算法不至於越界。這樣的話,無論原來的回文子串長度是偶數還是奇數,現在都變成奇數了.
HDU-3068 最長回文
分析:直接套上算法即可,注意插入一些字符來使得算法能夠適應長度為奇數和偶數的情況。

#include <cstdlib> #include <cstring> #include <cstdio> #include <iostream> #include <algorithm> using namespace std; const int N = 110005; char str[N], cpy[N<<1]; int seq[N<<1]; void manacher(char s[], int length, int rad[]) { for (int i=1,j=0,k; i < length; i+=k) { while (s[i-j-1] == s[i+j+1]) ++j; rad[i] = j; for (k = 1; k <= rad[i] && rad[i-k] != rad[i]-k; ++k) { // 利用類似鏡像的方法縮短了時間 rad[i+k] = min(rad[i-k], rad[i]-k); } j = max(j-k, 0); } } int main() { while (scanf("%s", str) != EOF) { int len = strlen(str); cpy[0] = '(', cpy[1] = '#'; for (int i=0, j=2; i < len; ++i, j+=2) { cpy[j] = str[i]; cpy[j+1] = '#'; } len = len*2+3; cpy[len-1] = ')'; manacher(cpy, len, seq); int Max = 1; for (int i = 0; i < len; ++i) { Max = max(Max, seq[i]); } printf("%d\n", Max); } return 0; }
HDU-4513 吉哥系列故事——完美隊形II
題意:給定一個數列,長度最長達到100000,要求找出一個最長的左邊單調遞增,右邊單調遞減的回文子串。
分析:剛開始的錯誤想法想法是所有的合法的解必定是一個回文串,因此把以任意一點為中心的所有回文串長度求出來,然后按照長度由長到短排一個序,暴力先判定最長的單調回文串,然后依據當前的最優值進行剪枝,不過還是TLE。網上看了下別人的想法,都說是一個Manacher的應用,而且貌似別人的模板和我的不太一樣,現在來說說我的理解。
其實一開始的時候我就有想過直接定義一個單調回文來做,但是仔細想想,如果某個單調回文已經求了出來,那么其左翼對應右翼肯定不會是一個單調回文,因為左翼一定是一個單調的序列,不會出現以一個為中心向兩邊下降的情況,當然除非出現相同的值。之所以出現這種想法是因為我以為只有左翼里面包含回文子串才會使得時間復雜度降低,而時間上該題的模型中,求出了一個單調回文區間,那么利用左翼的對應面不可能產生單調回文同樣能夠加速匹配。總而言之,該算法就是通過求回文來使得右翼的值復制左翼的值達到降低時間復雜度的目的,因此該題只要更改擴展原則即可,由單一的相等改為單調遞增或遞減。

#include <cstdlib> #include <cstring> #include <cstdio> #include <algorithm> #include <iostream> using namespace std; const int N = 100005; const int inf = 0x3f3f3f3f; int n; int seq[N<<1]; int rad[N<<1]; inline bool check(int seq[], int a, int b) { if (seq[a] != seq[b]) return false; if (!seq[a] && !seq[b] || a == b) return true; int ar = a+2, bl = b-2; if (seq[a] <= seq[ar]) return true; return false; } void manacher(int seq[], int rad[], int length) { for (int i=1,j=0,k; i<length; i+=k,j-=k) { while (check(seq, i-j-1, i+j+1)) ++j; rad[i] = j; for (k=1; k<=j && rad[i-k]!=rad[i]-k; ++k) { rad[i+k] = min(rad[i-k], rad[i]-k); } } } inline void getint(int &t) { char ch; while ((ch = getchar()), ch < '0' || ch > '9') ; t = ch - '0'; while ((ch = getchar()), ch >= '0' && ch <= '9') { t = t * 10 + ch - '0'; } } int main() { int T; scanf("%d", &T); while (T--) { memset(seq, 0, sizeof (seq)); scanf("%d", &n); seq[0] = inf-1; for (int i=2,j=0; j < n; i+=2,++j) { getint(seq[i]); } n = n*2+3; seq[n-1] = inf-2; manacher(seq, rad, n); int ret = 1; for (int i = 0; i < n; ++i) { ret = max(ret, rad[i]); } printf("%d\n", ret); } return 0; }
zstu-3769 數回文子串
題意:給定一個字符串序列,統計其中一共有多少個回文串,串的長度大於1。
分析:Manacher算法分析出以每一個位置為中心的長度,相加即可。

#include <cstdlib> #include <cstring> #include <cstdio> #include <iostream> #include <algorithm> using namespace std; const int N = 100005; char seq[N]; char cpy[N<<1]; int rad[N<<1]; void manacher(char str[], int rad[], int len) { for (int i=1,j=0,k; i < len; i+=k,j-=k) { while (str[i-j-1] == str[i+j+1]) ++j; rad[i] = j; for (k=1; k<=j && rad[i-k]!=rad[i]-k; ++k) { rad[i+k] = min(rad[i-k], rad[i]-k); } } } int main() { while (scanf("%s", seq) != EOF) { int len = strlen(seq); cpy[0] = '(', cpy[1] = '#'; for (int i=2,j=0; j < len; i+=2,++j) { cpy[i] = seq[j]; cpy[i+1] = '#'; } len = len*2+3; cpy[len-1] = ')'; manacher(cpy, rad, len); int ret = 0; for (int i = 0; i < len; ++i) { ret += rad[i] / 2; } printf("%d\n", ret); } return 0; }
HDU-3948 The Number of Palindromes
題意:給定一個長度為N的字符串,統計其中一共有多少個不同的回文子串。
分析:本來自己想的方法是manacher處理的同時暴力保留回文串,然后set去重,有算法本身可以知道若一個串是回文串,那么根據左右對稱,在第二次循環鏡像更新時便可知道右邊的回文串在前面就被統計過了,所以不同的回文串只要在第一次擴充之后進行判定即可,因此也可知道不同回文串的個數是O(N)級別的。可惜我的方法還是超時了,遇到極端的aa...aa就會重復構造一個子串。
看到博客好像貌似可以使用后綴數組寫,不會后綴數組只有另辟他徑了。由於回文串的個數是O(N)級別的,因此可以直接枚舉每一個中心點,從長度最長的回文串進行枚舉,使用字符串hash(多項式插值取模)來判定是否已經被統計過,如果這個長串已經統計過就可以直接跳過了(因為長串中的短串在之前也一定統計過了)。
關於多項式插值取模:在給定一個字符串左右區間的情況下O(1)計算出其hash值。設一個串為1234123,那么定義一個數組sum[i],其中:
sum[1] = 1, sum[2] = 1*T+2, sum[3] = 1*T^2+2*T+3, sum[4] = 1*T^3+2*T^2+3*T+4 ......
通過這樣的定義,[L,R]的值就為sum[R]-sum[L-1]*T^(R-L+1)。

#include <cstdlib> #include <cstring> #include <cstdio> #include <iostream> #include <algorithm> #include <string> #include <cctype> #include <set> using namespace std; typedef unsigned long long uint; const int N = 100005; char seq[N]; char cpy[N<<1]; int rad[N<<1]; const int muts = 37; uint mutpower[N]; uint sum[N]; struct hash_map { const static int mod = N*3+2; int idx, head[mod]; struct hash_tables { uint key; int next; }ele[N*2]; void init() { idx = 0; memset(head, 0xff, sizeof (head)); } void clear() { // clear的效率要高於init的效率,后期的用以替換init for (int i = 0; i < idx; ++i) head[ele[i].key%mod] = -1; idx = 0; } bool find(uint x) { int hashcode = x%mod; for (int i = head[hashcode]; i!=-1; i=ele[i].next) { if (ele[i].key == x) return true; } return false; } void insert(uint x) { int tmp = x % mod; ele[idx].key = x; ele[idx].next = head[tmp]; head[tmp] = idx++; } }; // 將hash表的實現封裝成一個類 hash_map hash; void manacher(char str[], int rad[], int len) { for(int i=1,j=0,k; i < len; i+=k, j=max(j-k,0)) { while (str[i-j-1] == str[i+j+1]) ++j; rad[i] = j; for (k=1; k<=j && rad[i-k] != rad[i]-k; ++k) { rad[i+k] = min(rad[i-k], rad[i]-k); } } } // str串以i為中心,回文半徑為j的回文串剔除插入字符后的hashcode uint gethashcode(char str[], int i, int j) { int L, R; // 在原來字符串中該回文串左右界 uint ret; if (isalpha(str[i])) { // 如果中心字符為字母 L = i/2-1 - j/2; R = i/2-1 + j/2; } else { L = i/2 - j/2; R = i/2-1 + j/2; } ret = sum[R]; if (L) ret -= sum[L-1]*mutpower[R-L+1]; return ret; } void gao(char str[], int len) { // 枚舉每一點作為回文串的中心 int ret = 0; for (int i = 2; i < len-2; ++i) { int ee = (bool)(!isalpha(str[i])); for (int j = rad[i]; j >= ee; j-=2) { uint hashcode = gethashcode(str, i, j); if (!hash.find(hashcode)) { ++ret; hash.insert(hashcode); } else { // 如果一個長串已經在hash表里面,那么短的回文串也一定在里面 break; } } } printf("%d\n", ret); } int main() { mutpower[0] = 1; for (int i = 1; i < N<<1; ++i) { mutpower[i] = mutpower[i-1] * muts; } hash.init(); int T, ca = 0; scanf("%d", &T); while (T--) { scanf("%s", seq); hash.clear(); int len = strlen(seq); sum[0] = seq[0]-'a'+1; // 避免0元素的出現,其將導致00和0無區別 for (int i = 1; i < len; ++i) { // 做出一個前綴的多項式值模式 sum[i] = sum[i-1]*muts+seq[i]-'a'+1; } cpy[0] = '(', cpy[1] = '#'; for (int i=2,j=0; j < len; i+=2,++j) { cpy[i] = seq[j]; cpy[i+1] = '#'; } len = len*2+3; cpy[len-1] = ')'; printf("Case #%d: ", ++ca); manacher(cpy, rad, len); gao(cpy, len); } return 0; }
ZOJ-3661 Palindromic Substring
題意:給定一個字符串,現在取出串中回文串的一半,奇數回文的取左邊部分加上中心元素,偶數回文取左邊的一半,現在給一個回文串一個權值,要求統計所有回文串權值中倒數第K小的值為多少。
分析:主要想法是通過Manacher算法處理出以每個字符為中心所產生的回文半徑的長度,然后將每個回文串的按照長度從長到短倒着插入到字段樹中,插入的過程中訪問hash表是否已經插入過回文串,hash表直接保存着上次回文串插入的位置,這樣就可以O(1)的時間找到要插入的位置,然后將多出來的長度插入即可。由於上題中已經得知回文串的個數最多是O(N)的,因此這里插入字段樹的次數也會控制在O(N)以內。最后通過遍歷一次字典樹得到最終的結果,從某節點出發,其子樹上的數量將要累加到父親節點上,因為這個子節點都包括這個父親節點所表示的回文串。另外奇數串和偶數串需要分開處理。

#include <cstdlib> #include <cstring> #include <cstdio> #include <vector> #include <iostream> #include <algorithm> using namespace std; typedef unsigned long long LL; const int MOD = 777777777; const int N = 100005; const int P = 37; char seq[N]; char cpy[N<<1]; int rad[N<<1]; int val[30]; LL POW[N]; LL sum[N]; vector<pair<LL, int> >vt; int n, m; void manacher(char str[], int rad[], int len) { for (int i=1,j=0,k; i < len; i+=k,j-=k) { while (str[i-j-1] == str[i+j+1]) ++j; rad[i] = j; for (k=1; k<=j && rad[i-k]!=rad[i]-k; ++k) { rad[i+k] = min(rad[i-k], rad[i]-k); } } } LL getkey(int l, int r) { if (!l) return sum[r]; else return sum[r]-sum[l-1]*POW[r-l+1]; } struct Hash_map { static const int mod = N*3+2; int idx, head[mod]; struct hash_tables { LL key; // 字符串hash之后的值 int pos; // pos表示在字段樹中的位置 int nxt; }ele[N]; // 最多N個不同回文串 void init() { idx = 0; memset(head, 0xff, sizeof (head)); } void clear() { for (int i = 0; i < idx; ++i) { head[ele[i].key%mod] = -1; } idx = 0; } int find(LL _key) { int id = _key % mod; for (int i = head[id]; ~i; i=ele[i].nxt) { if (ele[i].key == _key) { // 如果該元素在hash表中,說明已經插入到了字典樹當中 return ele[i].pos; } } return -1; // 如果沒有搜索到的話,返回-1 } void insert(LL _key, int _pos) { int id = _key % mod; ele[idx].key = _key, ele[idx].pos = _pos; ele[idx].nxt = head[id], head[id] = idx++; } }; Hash_map hash; struct Trie { int root, idx; struct Node { int ch[26]; int end; }ele[N]; // 這個空間會不會小了呢 int malloc() { ele[idx].end = 0; memset(ele[idx].ch, 0xff, sizeof (ele[idx].ch)); return idx++; } void init() { idx = 0; root = malloc(); } void insert(int p, int l, int r, int axis) { for (int i = r; i >= l; --i) { ele[p].ch[seq[i]-'a'] = malloc(); p = ele[p].ch[seq[i]-'a']; hash.insert(getkey(i, axis), p); } ++ele[p].end; } int cal(int p, LL fac, LL value) { int tot = ele[p].end; for (int i = 0; i < 26; ++i) { if (ele[p].ch[i] != -1) { tot += cal(ele[p].ch[i], fac*26%MOD, (value+val[i]*fac)%MOD); } } if (p != root) // 根節點上沒有任何信息不應該被統計 vt.push_back(make_pair(value, tot)); return tot; } }; Trie Todd, Teven; void gao() { for (int i = 0; i < n; ++i) { // 處理奇數個元素構成的回文串 int pos = Todd.root, cpyi = i*2+2; int left = i-rad[cpyi]/2, right = i; for (int j = left; j <= right; ++j) { int tmp = hash.find(getkey(j, i)); if (tmp != -1) { pos = tmp; right = j-1; break; } } Todd.insert(pos, left, right, i); } hash.clear(); for (int i = 0; i < n-1; ++i) { // 處理偶數個元素構成的回文串 int pos = Teven.root, cpyi = i*2+3; int left = i-rad[cpyi]/2+1, right = i; for (int j = left; j <= right; ++j) { int tmp = hash.find(getkey(j, i)); if (tmp != -1) { pos = tmp; right = j-1; break; } } Teven.insert(pos, left, right, i); } for (int i = 0; i < m; ++i) { vt.clear(); LL K; scanf("%llu", &K); // K可能很大 for (int j = 0; j < 26; ++j) { scanf("%d", &val[j]); } Todd.cal(Todd.root, 1, 0), Teven.cal(Teven.root, 1, 0); sort(vt.begin(), vt.end()); for (int h=0; h < (int)vt.size(); ++h) { if (K > vt[h].second) { K -= vt[h].second; } else { K = 0; printf("%llu\n", vt[h].first); break; } } if (K) { // 如果總共不足K個回文串 printf("0\n"); } } puts(""); } int main() { POW[0] = 1; for (int i = 1; i < N; ++i) { POW[i] = POW[i-1]*P; } hash.init(); int T; scanf("%d", &T); while (T--) { hash.clear(); Todd.init(), Teven.init(); scanf("%d %d", &n, &m); scanf("%s", seq); sum[0] = seq[0]-'a'+1; for (int i = 1; i < n; ++i) { sum[i] = sum[i-1]*P+seq[i]-'a'+1; } // 做成一個多項式的形式 cpy[0] = '(', cpy[1] = '#'; for (int i=2,j=0; j < n; i+=2,++j) { cpy[i] = seq[j]; cpy[i+1] = '#'; } int len = n*2+3; cpy[len-1] = ')'; manacher(cpy, rad, len); // 求出每個點為中心的回文半徑 gao(); } return 0; }