自己看出来了是道可以用KMP做的题...
然而因为我理解不够深入,所以没打出正解
俗话说“好命不如好名”,小h准备给他的宠物狗起个新的名字,于是他把一些英文的名字全抄下来了,写成一行长长的字符串,小h觉得一个名字如果是好名字,那么这个名字在这个串中既是前缀,又是后缀,即是这个名字从前面开始可以匹配,从后面开始也可以匹配,例如abc在 abcddabc中既是前缀,也是后缀,而ab就不是,可是长达4*10^5的字符让小h几乎昏过去了,为了给自己的小狗起个好名字,小h向你求救,并且他要求要将所有的好名字的长度都输出来。
Input
一行,要处理的字符串(都是小写字母)。
Output
一行若干个数字,从小到大输出,表示好名字的长度。
Sample Input
abcddabc
Sample Output
3 8
法一:哈希
递推求每个前缀 / 后缀的 Hash 值,然后枚举即可
(我对哈希不熟,所以即使考试时想到了,也没写QAQ...)
法二:KMP(一篇神仙博客,写的超好:https://www.cnblogs.com/ZuoAndFutureGirl/p/9028287.html)
实际上只用到了KMP中求数组next[ ]的部分,
我们从短的前缀后缀递推求得更长的前缀后缀,即得出next[ i ],
那么我们也可以递推回去,从最长的前缀后缀往前跳,直到next为0
(自己已经尽力解释了)
还有个细节:答案最后要加上“整个字符串的长度”
从上面博客中截了一部分与next数组有关的部分,很详细,有助于理解,推荐初学者\不懂的人认真耐心地看:
3.3.2 基于《最大长度表》匹配
因为模式串中首尾可能会有重复的字符,故可得出下述结论:
失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值
下面,咱们就结合之前的《最大长度表》和上述结论,进行字符串的匹配。如果给定文本串“BBC ABCDAB ABCDABCDABDE”,和模式串“ABCDABD”,现在要拿模式串去跟文本串匹配,如下图所示:
通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。
3.3.3 根据《最大长度表》求next 数组
由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:
而且,根据这个表可以得出下述结论
上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。
给定字符串“ABCDABD”,可求得它的next 数组如下:
把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。
换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:
根据最大长度表求出了next 数组后,从而有
失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值
而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:
所以,你可以把《最大长度表》看做是next 数组的雏形,甚至就把它当做next 数组也是可以的,区别不过是怎么用的问题。
3.3.4 通过代码递推计算next 数组
接下来,咱们来写代码求下next 数组。
基于之前的理解,可知计算next 数组的方法可以采用递推:
举个例子,如下图,根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2,代表字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串需要向右移动j - next [j] = 6 - 2 =4位。
向右移动4位后,模式串中的字符C继续跟文本串匹配。
对于P的前j+1个序列字符:
一般的文章或教材可能就此一笔带过,但大部分的初学者可能还是不能很好的理解上述求解next 数组的原理,故接下来,我再来着重说明下。
如下图所示,假定给定模式串ABCDABCE,且已知next [j] = k(相当于“p0 pk-1” = “pj-k pj-1” = AB,可以看出k为2),现要求next [j + 1]等于多少?因为pk = pj = C,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3)。代表字符E前的模式串中,有长度k+1 的相同前缀后缀。
但如果pk != pj 呢?说明“p0 pk-1 pk” ≠ “pj-k pj-1 pj”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。
结合上图来讲,若能在前缀“ p0 pk-1 pk ” 中不断的递归前缀索引k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk'-1 pk' = pj-k' pj-1 pj,则最大相同的前缀后缀长度为k' + 1,从而next [j + 1] = k’ + 1 = next [k' ] + 1。否则前缀中没有D,则代表没有相同的前缀后缀,next [j + 1] = 0。
那为何递归前缀索引k = next[k],就能找到长度更短的相同前缀后缀呢?这又归根到next数组的含义。我们拿前缀 p0 pk-1 pk 去跟后缀pj-k pj-1 pj匹配,如果pk 跟pj 失配,下一步就是用p[next[k]] 去跟pj 继续匹配,如果p[ next[k] ]跟pj还是不匹配,则需要寻找长度更短的相同前缀后缀,即下一步用p[ next[ next[k] ] ]去跟pj匹配。此过程相当于模式串的自我匹配,所以不断的递归k = next[k],直到要么找到长度更短的相同前缀后缀,要么没有长度更短的相同前缀后缀。如下图所示:
所以,因最终在前缀ABC中没有找到D,故E的next 值为0:
模式串的后缀:ABDE
模式串的前缀:ABC
前缀右移两位: ABC
读到此,有的读者可能又有疑问了,那能否举一个能在前缀中找到字符D的例子呢?OK,咱们便来看一个能在前缀中找到字符D的例子,如下图所示:
给定模式串DABCDABDE,我们很顺利的求得字符D之前的“DABCDAB”的各个子串的最长相同前缀后缀的长度分别为0 0 0 0 1 2 3,但当遍历到字符D,要求包括D在内的“DABCDABD”最长相同前缀后缀时,我们发现pj处的字符D跟pk处的字符C不一样,换言之,前缀DABC的最后一个字符C 跟后缀DABD的最后一个字符D不相同,所以不存在长度为4的相同前缀后缀。
怎么办呢?既然没有长度为4的相同前缀后缀,咱们可以寻找长度短点的相同前缀后缀,最终,因在p0处发现也有个字符D,p0 = pj,所以p[j]对应的长度值为1,相当于E对应的next 值为1(即字符E之前的字符串“DABCDABD”中有长度为1的相同前缀和后缀)。
综上,可以通过递推求得next 数组,代码如下所示:
void GetNext(char* p,int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p[j] == p[k])
{
++k;
++j;
next[j] = k;
}
else
{
k = next[k];
}
}
}
用代码重新计算下“ABCDABD”的next 数组,以验证之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组是否正确,计算结果如下表格所示:
从上述表格可以看出,无论是之前通过“最长相同前缀后缀长度值右移一位,然后初值赋为-1”得到的next 数组,还是之后通过代码递推计算求得的next 数组,结果是完全一致的。
void get_next()
{
next[1]=0;
for(int i=2,j=0;i<=len;i++)
{
while(j>0&&a[i]!=a[j+1])
j=next[j];
if(a[i]==a[j+1])
j++;
next[i]=j;
}
}
暴力枚举了模式串,再套用KMP的板子,大概是O(n^2),时间肯定超了
#include
#include
#include
#include
#include
#include
using namespace std;
const int MAXN=4e5;
char a[MAXN+5],b[MAXN+5],tmp[MAXN+5];
int next[MAXN+5],f[MAXN+5];
int len;
vector ans;
void get_next()
{
next[1]=0;
for(int i=2,j=0;i<=len;i++)
{
while(j>0&&a[i]!=a[j+1])
j=next[j];
if(a[i]==a[j+1])
j++;
next[i]=j;
}
}
void get_f(char a[],int n)
{
for(int i=len-n+1,j=0;i<=len;i++)
{
while(j>0&&(j==n||b[i]!=a[j+1]))
j=next[j];
if(b[i]==a[j+1])
j++;
f[i]=j;
}
if(f[len]==n)
ans.push_back(n);
}
int main()
{
scanf("%s",a+1);
len=strlen(a+1);
memcpy(b,a,sizeof(a));
get_next();
for(int i=1;i<=len;i++)
{
tmp[i]=a[i];
get_f(tmp,i);
}
for(int i=0;i
从短的前缀后缀递推求得更长的前缀后缀,得出next[ i ],那么我们也可以递推回去,从最长的前缀后缀往前跳,直到为0
(自己研究了半天终于懂了,真的很感谢上面所载的那篇博客)
#include
#include
#include
#include
#include
using namespace std;
const int MAXN=4e5;
char a[MAXN+5],b[MAXN+5],tmp[MAXN+5];
int next[MAXN+5],ans[MAXN+5];
int len,cnt;
void get_next()
{
next[1]=0;
for(int i=2,j=0;i<=len;i++)
{
while(j>0&&a[i]!=a[j+1])
j=next[j];
if(a[i]==a[j+1])
j++;
next[i]=j;
}
}
int main()
{
scanf("%s",a+1);
len=strlen(a+1);
memcpy(b,a,sizeof(a));
get_next();
int tmp=next[len];
ans[++cnt]=len;
ans[++cnt]=next[len];
while(next[tmp]!=0)
{
tmp=next[tmp];
ans[++cnt]=tmp;
}
sort(ans+1,ans+cnt+1);
for(int i=1;i<=cnt;i++)
if(i==1)
printf("%d",ans[i]);
else
printf(" %d",ans[i]);
return 0;
}
通过这次考试,得出了教训:
学什么都不能一知半解,要尽可能地去弄懂吃透才能灵活变化、运用,来应对变化万千的题
也才能做到“一题多解、多题一解”