1. 程式人生 > >P1809【USACO2.3.1】Longest Prefix最長字首 IOI'96

P1809【USACO2.3.1】Longest Prefix最長字首 IOI'96

P1809【USACO2.3.1】Longest Prefix最長字首 IOI’96

時間限制 : 15000 MS 空間限制 : 65536 KB

問題描述
在生物學中,一些生物的結構是用包含其要素的大寫字母序列來表示的。生物學家對於把長的序列分解成較短的序列(即元素)很感興趣。
如果一個集合 P 中的元素可以通過串聯(元素可以重複使用,相當於 Pascal 中的 “+” 運算子)組成一個序列 S ,那麼我們認為序列 S 可以分解為 P 中的元素。元素不一定要全部出現(如下例中BBC就沒有出現)。舉個例子,序列 ABABACABAAB 可以分解為下面集合中的元素:
{A, AB, BA, CA, BBC}
序列 S 的前面 K 個字元稱作 S 中長度為 K 的字首。設計一個程式,輸入一個元素集合以及一個大寫字母序列 S ,設S’是序列S的最長字首,使其可以分解為給出的集合P中的元素,求S’的長度K。

輸入格式
輸入資料的開頭包括 1..200 個元素(長度為 1..10 )組成的集合,用連續的以空格分開的字串表示。字母全部是大寫,資料可能不止一行。元素集合結束的標誌是一個只包含一個 “.” 的行。集合中的元素沒有重複。接著是大寫字母序列 S ,長度為 1..200,000 ,用一行或者多行的字串來表示,每行不超過 76 個字元。換行符並不是序列 S 的一部分。

輸出格式
只有一行,輸出一個整數,表示 S 符合條件的字首的最大長度。

樣例輸入
A AB BA CA BBC
.
ABABACABAABC

樣例輸出
11

關於這道題目的思路

本來讀完題目準備瞎暴力過一個的,因為畢竟雜湊,感覺雜湊都是瞎逼暴力就過了

然而,細心的我注意到了這大的嚇人的資料範圍,然後果斷捨棄了揹包
(假的…真實原因是我不知道怎麼把字串都連起來,然後被迫寫了個簡單的演算法)

但是據說這道題目暴力揹包也能過,就是時間沒保證

說正事

對於這種動不動就幾百萬上下的資料的題目,我們當然不能亂去暴力
尤其是這種輸入都要給你把一串分成幾串的題目
它連輸入都給你亂分了,你當然要懟回去,把它分得更細

於是,做法就出來了
分治

好吧講一下真實的思路

分治這個是一定想得到的,因為這麼大的資料一次性處理時間複雜度難以估計
尤其是上面那些分段的元素個數還很嚇人的情況下

但是怎麼分呢??

這就要注意到一個小細節
每一個元素最大的長度是10啊
這就是說,我們判斷某一段能不能構成元素,最多最多就只需要判10個字母就行了

然後考慮到暴力地一個一個去算的最大運算次數超過了(55n)次
這就很不科學,因為每一個元素我們最多需要呼叫10次(它作為元素開頭一直到它作為元素結尾),而且你還要判定
那麼我們能不能把呼叫次數減少一次,從而使每一個數據的最大運算次數由55次變到10次呢??

答案顯然是可以的,不然我上面說的豈不是廢話

滾動陣列的想法
由於有些DP的狀態DP(n)只與DP(n-1)有關
所以滾動陣列選擇只記錄DP(n-1)的資料

那麼我們算的資料更小,只與前10個字母有關
於是我們希望得到下面這樣的狀態

以長度為5為例(真實長度為10)

當我們第一個加入元素A時,設A的雜湊值為a

Title 1 2 3 4 5
當前字元 a

加入第二個雜湊值為b的元素B,AB合併的值簡單設為ab

Title 1 2 3 4 5
當前字元 b ab

然後加入第三個雜湊值為c的元素C

Title 1 2 3 4 5
當前字元 c bc abc

這樣一來,我們每加入一個元素,經過至多10次運算,就可以將以次元素為尾的所有可能的字串的雜湊值都算出來,然後再10次判定即可

同時,為了消除顧慮,我們來看看上述列表飽和時加入新元素的情況

已加入abcde

Title 1 2 3 4 5
當前字元 e de cde bcde abcde

然後加入f
得到

Title 1 2 3 4 5
當前字元 f ef def cdef bcdef

沒錯,a元素被擠出去了,這樣我們就能夠得到f為尾的長度為1-5的子字串長度了

那麼如何判定呢?
我們選擇通過記錄當前字母是否可行,並且讓當前字母跟隨上述列表一起滾動的方式進行判定

Title 1 2 3 4 5
當前字元 a
可行性 1 0 0 0 0

當前列表表示到a為止整個字首都可行

經過數輪滾動及判定之後得到
其中,a和bcde為可行元素

Title 1 2 3 4 5
當前字元 e de cde bcde abcde
可行性 1 0 0 0 1

當前列表表示的並非abcde和e可行
而是到a為止的字首和到e為止的字首可行
判定過程如下

我們得到a可行,並在加入元素e之後,判定到bcde是可行的
於是我們想要知道bcde之前的字串是否是合法字首
於是我們呼叫bcde之前的那一個記錄值,記錄的即是bcde之前的字首是否可行
即可判定

具體原因如下:
    a可行,於是經過一輪新增元素之後,a原本的為止要右移一位
    於是記錄a可行變成了記錄ab可行
    但是實際上我們記錄的是到a為止(即當前字串第一個字母為止)的字首是否可行
    所以同理,經過4次新增元素之後,我們就有了abcde記錄值為1
    但實際上它的意思是a為止的字首可行

然後,由於判定為1
我們就可以記錄到e為止的字首可行,於是記錄e的值為1

過程簡單描述結束

有一點細節你可能需要注意

1、滾動要從後往前滾動
2、邊界條件很刁鑽,需要謹慎地思考
3、需要一個數字來記錄當前元素的為止,以便更新答案
4、運用雜湊表能夠做到運算後快速判斷
5、可提前結束程式,條件是當前列表中無1,表示從上一個可行字首到目前為止10位,沒出現一個可行元素,那麼即使之後的元素可行,由於對之前元素為止字首可行性的判定一定為0,不可能再成立了

就是這些,感謝觀看

附上原始碼

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

int ans=0,zero=0;
unsigned int add[11]={1};
bool ze,flag,haha[598765],Jud[12]={0,1};
char use[11],mem[98];

void HASH()//用於記錄元素的雜湊值
{
    unsigned int Hash=1,n=strlen(use);
    for(int i=0;i<n;i++)Hash=Hash*131+use[i];
    haha[Hash & 0x7FFFF]=1;
}

bool mult(char c)
{
    ze=flag=0;
    zero++;//記錄當前討論字元的位置(字首長度)
    for(int i=9;i>=0;i--)if(add[i])add[i+1]=add[i]*131+c;//更新雜湊值
    for(int i=10;i;i--)
    {
        flag|=Jud[i];
        if(haha[add[i] & 0x7FFFF]&&Jud[i])
        {
            flag=1;
            ze=1;
            ans=max(ans,zero);
        }
        Jud[i+1]=Jud[i];
    }
    Jud[1]=ze;
    if(!ze&&!flag)return 0;
    return 1;
}

int main()
{
    while(use[0]!='.')
    {
        memset(use,0,sizeof(use));
        scanf("%s",&use);
        HASH();
    }
    while(scanf("%s",&mem)!=EOF)
    {
        for(int i=0;i<strlen(mem);i++)
            if(!mult(mem[i])){printf("%d",ans);return 0;}
        memset(mem,0,sizeof(mem));
    }
    printf("%d",ans);
    return 0;
}