【字符串-KMP-哈希】中山纪念中学暑期游Day13——seek

前言

自己看出来了是道可以用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”,现在要拿模式串去跟文本串匹配,如下图所示:

        

  • 1. 因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直到模式串中的字符A跟文本串的第5个字符A匹配成功:

  • 2. 继续往后匹配,当模式串最后一个字符D跟文本串匹配时失配,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此时已经匹配的字符数为6个(ABCDAB),然后根据《最大长度表》可得失配字符D的上一位字符B对应的长度值为2,所以根据之前的结论,可知需要向右移动6 - 2 = 4 位。

  • 3. 模式串向右移动4位后,发现C处再度失配,因为此时已经匹配了2个字符(AB),且上一位字符B对应的最大长度值为0,所以向右移动:2 - 0 =2 位。

           

  • 4. A与空格失配,向右移动1 位。

 

  • 5. 继续比较,发现D与C 失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的最大长度2,即向右移动6 - 2 = 4 位。

           

  • 6. 经历第5步后,发现匹配成功,过程结束。

          

    通过上述匹配过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的前缀和后缀公共部分的最大长度后,便可基于此匹配。而这个最大长度便正是next 数组要表达的含义。

3.3.3 根据《最大长度表》求next 数组

    由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:

 

 

    而且,根据这个表可以得出下述结论

  • 失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

    上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。

    给定字符串“ABCDABD”,可求得它的next 数组如下:

 

    把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。

    换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:

    根据最大长度表求出了next 数组后,从而有

 

失配时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值

    而后,你会发现,无论是基于《最大长度表》的匹配,还是基于next 数组的匹配,两者得出来的向右移动的位数是一样的。为什么呢?因为:

  • 根据《最大长度表》,失配时,模式串向右移动的位数 = 已经匹配的字符数 - 失配字符的上一位字符的最大长度值
  • 而根据《next 数组》,失配时,模式串向右移动的位数 = 失配字符的位置 - 失配字符对应的next 值
    • 其中,从0开始计数时,失配字符的位置 = 已经匹配的字符数(失配字符不计数),而失配字符对应的next 值 = 失配字符的上一位字符的最大长度值,两相比较,结果必然完全一致。

    所以,你可以把《最大长度表》看做是next 数组的雏形,甚至就把它当做next 数组也是可以的,区别不过是怎么用的问题。

3.3.4 通过代码递推计算next 数组

    接下来,咱们来写代码求下next 数组。

    基于之前的理解,可知计算next 数组的方法可以采用递推:

  • 1. 如果对于值k,已有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,相当于next[j] = k。
    • 此意味着什么呢?究其本质,next[j] = k 代表p[j] 之前的模式串子串中,有长度为k 的相同前缀和后缀。有了这个next 数组,在KMP匹配中,当模式串中j 处的字符失配时,下一步用next[j]处的字符继续跟文本串匹配,相当于模式串向右移动j - next[j] 位。
    • 举个例子,如下图,根据模式串“ABCDABD”的next 数组可知失配位置的字符D对应的next 值为2,代表字符D前有长度为2的相同前缀和后缀(这个相同的前缀后缀即为“AB”),失配后,模式串需要向右移动j - next [j] = 6 - 2 =4位。

      向右移动4位后,模式串中的字符C继续跟文本串匹配。

  • 2. 下面的问题是:已知next [0, ..., j],如何求出next [j + 1]呢?

    对于P的前j+1个序列字符:

  • 若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;
  • 若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] =  next[k] + 1,否则继续递归前缀索引k = next[k],而后重复此过程。 相当于在字符p[j+1]之前不存在长度为k+1的前缀"p0 p1, …, pk-1 pk"跟后缀“pj-k pj-k+1, …, pj-1 pj"相等,那么是否可能存在另一个值t+1 < k+1,使得长度更小的前缀 “p0 p1, …, pt-1 pt” 等于长度更小的后缀 “pj-t pj-t+1, …, pj-1 pj” 呢?如果存在,那么这个t+1 便是next[ j+1]的值,此相当于利用已经求得的next 数组(next [0, ..., k, ..., j])进行P串前缀跟P串后缀的匹配。

   一般的文章或教材可能就此一笔带过,但大部分的初学者可能还是不能很好的理解上述求解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 数组,结果是完全一致的。


自己习惯这样写来求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<cstdio>
#include<vector>
#include<cmath>
#include<cstring>
#include<iostream>
#include<algorithm>
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<int> 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<ans.size();i++)
		if(i==0)
			printf("%d",ans[i]);
		else
			printf(" %d",ans[i]);
	
	return 0;
}
/*
感觉是DP...
dp[i]:前i个字符中包含的满足题意的最大子串数 (?)
next[i]=max{j},j<i,a[i-j+1~i]=a[1~j]
f[i]=max{j},j<=i,b[i-j+1~i]=a[1~j]
求a[1~n]是否是b[1~m]的子串,
且求a[]在b[]中各次出现的位置 
感觉打了会超时 
*/

AC代码

从短的前缀后缀递推求得更长的前缀后缀,得出next[ i ],那么我们也可以递推回去,从最长的前缀后缀往前跳,直到为0

(自己研究了半天终于懂了,真的很感谢上面所载的那篇博客)

#include<cstdio>
#include<cmath>
#include<cstring>
#include<iostream>
#include<algorithm>
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;
}

番外

通过这次考试,得出了教训:

学什么都不能一知半解,要尽可能地去弄懂吃透才能灵活变化、运用,来应对变化万千的题

也才能做到“一题多解、多题一解”

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值