1. 程式人生 > >【KMP】洛谷P2375 [NOI2014]動物園 題解

【KMP】洛谷P2375 [NOI2014]動物園 題解

clu 控制 () borde 會有 strlen 觀察 約束 brush

一開始的方向應該對了,但是沒有想到合理的優化還是沒寫出來……

題目描述

近日,園長發現動物園中好吃懶做的動物越來越多了。例如企鵝,只會賣萌向遊客要吃的。為了整治動物園的不良風氣,讓動物們憑自己的真才實學向遊客要吃的,園長決定開設算法班,讓動物們學習算法。

某天,園長給動物們講解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]動物園 題解