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;
}