时间限制 : 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
#include
#include
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;i131+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;
}