【KMP】洛谷P2375 [NOI2014]動物園 題解
一開始的方向應該對了,但是沒有想到合理的優化還是沒寫出來……
題目描述
近日,園長發現動物園中好吃懶做的動物越來越多了。例如企鵝,只會賣萌向遊客要吃的。為了整治動物園的不良風氣,讓動物們憑自己的真才實學向遊客要吃的,園長決定開設算法班,讓動物們學習算法。
某天,園長給動物們講解KMP算法。
園長:“對於一個字符串\(S\),它的長度為\(L\)。我們可以在\(O(L)\)的時間內,求出一個名為\(next\)的數組。有誰預習了\(next\)數組的含義嗎?”
熊貓:“對於字符串\(S\)的前\(i\)個字符構成的子串,既是它的後綴又是它的前綴的字符串中(它本身除外),最長的長度記作\(next[i]\)。”
園長:“非常好!那你能舉個例子嗎?”
熊貓:“例\(S\)為abcababc,則\(next[5]=2\)。因為\(S\)的前\(5\)個字符為abcab,ab既是它的後綴又是它的前綴,並且找不到一個更長的字符串滿足這個性質。同理,還可得出\(next[1]=next[2]=next[3]=0,next[4]=next[6]=1,next[7]=2,next[8]=3\)。”
園長表揚了認真預習的熊貓同學。隨後,他詳細講解了如何在\(O(L)\)的時間內求出\(next\)數組。
下課前,園長提出了一個問題:“KMP算法只能求出\(next\)數組。我現在希望求出一個更強大num數組一一對於字符串\(S\)的前\(i\)個字符構成的子串,既是它的後綴同時又是它的前綴,並且該後綴與該前綴不重疊,將這種字符串的數量記作\(num[i]\)。例如\(S\)為aaaaa,則\(num[4]=2\)。這是因為\(S\)的前\(4\)個字符為aaaa,其中a和aa都滿足性質‘既是後綴又是前綴’,同時保證這個後綴與這個前綴不重疊。而aaa雖然滿足性質‘既是後綴又是前綴’,但遺憾的是這個後綴與這個前綴重疊了,所以不能計算在內。同理,\(num[1]=0,num[2]=num[3]=1,num[5]=2\)。”
最後,園長給出了獎勵條件,第一個做對的同學獎勵巧克力一盒。聽了這句話,睡了一節課的企鵝立刻就醒過來了!但企鵝並不會做這道題,於是向參觀動物園的你尋求幫助。你能否幫助企鵝寫一個程序求出\(num\)數組呢?
特別地,為了避免大量的輸出,你不需要輸出\(num[i]\)分別是多少,你只需要輸出\(\prod_{i=1}^L (num[i]+1)\),對\(1,000,000,007\)取模的結果即可。
其中\(\prod_{i=1}^n (num[i]+1)=(num[1]+1)\times (num[2]+1)\times \dots \times (num[n]+1)\)。
輸入輸出格式
輸入格式:
第\(1\)行僅包含一個正整數\(n\) ,表示測試數據的組數。
隨後\(n\)行,每行描述一組測試數據。每組測試數據僅含有一個字符串\(S\),\(S\)的定義詳見題目描述。數據保證\(S\)中僅含小寫字母。輸入文件中不會包含多余的空行,行末不會存在多余的空格。
輸出格式:
包含\(n\)行,每行描述一組測試數據的答案,答案的順序應與輸入數據的順序保持一致。對於每組測試數據,僅需要輸出一個整數,表示這組測試數據的答案對\(1,000,000,007\)取模的結果。輸出文件中不應包含多余的空行。
輸入輸出樣例
輸入樣例#1:3 aaaaa ab abcababc輸出樣例#1:36 1 32說明
測試點編號 約定 1 \(N≤5,L≤50\) 2 \(N≤5,L≤200\) 3 \(N≤5,L≤200\) 4 \(N≤5,L≤10,000\) 5 \(N≤5,L≤10,000\) 6 \(N≤5,L≤100,000\) 7 \(N≤5,L≤200,000\) 8 \(N≤5,L≤500,000\) 9 \(N≤5,L≤1,000,000\) 10 \(N≤5,L≤1,000,000\)
題解:
既然題面中反復提到KMP,那這道題就應該與KMP緊密相關。
我們知道,當模式串匹配自己失配時,會立即跳到下一個nxt[]去,在nxt[]為0之前,跳了多少個nxt就說明有多少個與後綴相同的前綴,也是nxt的其中一個定義。這樣我們就有了\(O(n^2)\)暴力算法,求完\(nxt[i]\)後,遞歸nxt,看有多少次值在\(\lfloor \frac i2\rfloor\)以內。
考慮優化這個遞歸過程。因為現在的\(nxt[i]\)可以從前面的\(nxt[j]+1\)轉移過來,因此現在的\(num[i]\)也可以從前面的\(num[j]+1\)轉移過來。於是\(nxt[i]\)只從\(\le \lfloor \frac i2\rfloor\)轉移。於是有了下面這段代碼:
for(int i=2,j=0;i<=n;++i) { while(j&&(s[j+1]!=s[i]||j+1>(i>>1)))//保證了只從i>>1轉移過來,j+1是考慮匹配上了會增加1 j=nxt[j]; if(s[j+1]==s[i]) ++j; nxt[i]=j; num[i]=num[j]+1; }
交上去……0分?手測了一下發現會有這種情況:
aaaaaaa ↑
\(num[7]\)按照上面的代碼應該從\(num[3]\)轉移得到\(num[7]=2\),但是觀察發現\(num[7]\)應該=3。為什麽呢?\(num[3]\)嚴格遵守了前後綴不重疊,但是到了\(num[7]\)就沒有了\(num[3]\)的約束,也就是\(num[3]\)不能從\(num[2]\)轉移,但是\(num[7]\)可以,這樣中間\(num[2]\)就會丟失。
所以,為了不丟失\(num[2]\)我們試著讓\(num[i]\)表示可重疊的相等前後綴的個數,只在統計答案時從前面轉移就好了。
其實就是在做第二遍模式串匹配,此時和第一遍一樣,只是要控制\(j\le \lfloor \frac i2\rfloor\),然後更新存儲答案的\(num1[i]=num[j]+1\)。
Code:
#include<cstdio> #include<cstring> char s[1000005]; long long num[1000001],num1[1000001]; int nxt[1000001]; void work() { scanf("%s",s+1); int n=strlen(s+1); num[0]=-1; for(int i=2,j=0;i<=n;++i) { while(j&&s[j+1]!=s[i]) j=nxt[j]; if(s[j+1]==s[i]) ++j; nxt[i]=j; num[i]=num[j]+1; } for(int i=2,j=0;i<=n;++i) { while(j&&(s[j+1]!=s[i]||j+1>(i>>1)))//和上面的比只加了一個條件 j=nxt[j]; if(s[j+1]==s[i]) ++j; num1[i]=num[j]+1;//從前面的nxt轉移過來 } long long ans=1; for(int i=1;i<=n;++i) { //printf("%d ",num1[i]);調試用 ans*=num1[i]+1; ans%=1000000007; } printf("%lld\n",ans); return; } int main() { int n; scanf("%d",&n); for(int i=1;i<=n;++i) work(); return 0; }
【KMP】洛谷P2375 [NOI2014]動物園 題解