P1809【USACO2.3.1】Longest Prefix最长前缀 IOI'96

来源:互联网 发布:初中数学网络研讨 编辑:程序博客网 时间:2024/05/21 14:05

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