【学习笔记】KMP中的border及其应用

前言

KMP是一个普及度很高的字符串算法。在NOI2014与NOIP2020中均有考察。同时,上述题目均在一定程度上考察了字符串的周期这个点。本文将探讨字符串的周期。
因笔者能力有限,本文讲述的并不会很深,也不会非常简洁,而是致力于达到可以理解并应用的层面。如果您需要更深层次的探讨或是简洁的概述,可在“鸣谢”栏中继续寻找资料。同时,由于本文将大量运用KMP算法,在阅读本文前,您需要先学习KMP算法。
本文正在施工中,敬请谅解(不保证不咕)

定义

以下字符串的长度将用(|S|)表示,子串将使用(S_{l..r})的形式表示。字符串下标从1开始
我们定义一个字符串S的border为(S_{1..x}=S_{|S|-x+1,|S|} x eq |S|),则(S_{1..x})(S)的border

概述

Q:这玩意看似与KMP无关啊
A:先回忆KMP中,我们用的(next)数组(亦称(fail)数组,下文统一用(fail)数组表示)的定义,还记得吗?

对于字符串(S)的前(i)个字符构成的子串,既是它的后缀又是它的前缀的字符串中(它本身除外),最长的长度记作(next[i])
——【NOI2014】动物园

所以,它就是最大border
现在我们来解决一个问题:求(S)的每个子串的最大border。(题目来源:Luogu P3435 [POI2006]OKR-Periods of Words)那其实就是(fail)数组的和了。
全文完
别急,border有不少性质,下文中我们将讲解。

题目讲解

Luogu P5829 【模板】失配树

题目链接

题目大意:求字符串(S)([1..p] [1..q])的最长公共border

题目思路:
首先,(fail_{fail_i})一定是([1,fail_i])的border。又因(fail_i)([1..i])的border,那么([1..fail_{fail_i}])([1..i])的border。读者可自行理解。
那么,fail数组实际构成了一棵树,任意节点的父亲节点均是其border,那么我们跑LCA即可。
事实上,这颗树我们没有必要建出来,否则会引起空间浪费。
代码:

#include<bits/stdc++.h>
using namespace std;
char st[1090000];int f[1001000],dep[1001000],fa[21][1001000],m,u,v;
long long fain()
{
	long long s=0;char ch;
	while (1)
	{
		ch=getchar();
		if (ch>='0'&&ch<='9') break;
	}
	s=ch-'0';
	while (1)
	{
		ch=getchar();
		if (ch<'0'||ch>'9') return s; else s=s*10+ch-'0';
	}
	return s;
}
int main()
{
	int len=0;
	while (1)
	{
		st[++len]=getchar();
		if (st[len]<'a'||st[len]>'z') break;
	}
	f[1]=0;
	for (int i=2,j=0;i<len;i++)
	{
		while (st[i]!=st[j+1]&&j!=0) j=f[j];
		if (st[i]==st[j+1]) j++,f[i]=j,dep[i]=dep[j]+1;else f[i]=0,dep[i]=0;
		fa[0][i]=f[i];
		for (int k=1;k<=20;k++)
		  fa[k][i]=fa[k-1][fa[k-1][i]];
	}
	m=fain();
	for (int i=1;i<=m;i++)
	{
		u=fain();v=fain();
		if (dep[u]>dep[v]) swap(u,v);
		if (u==v) {cout<<f[u]<<endl;continue;}
		for (int j=20;j>=0;j--)
		  if (dep[fa[j][v]]>=dep[u]) v=fa[j][v];
		if (u==v) {cout<<f[u]<<endl;continue;}
		for (int j=20;j>=0;j--)
		  if (fa[j][u]!=fa[j][v]) u=fa[j][u],v=fa[j][v];
		if (fa[0][u]==fa[0][v]) 
	      cout<<fa[0][u]<<endl; 
	}
	return 0;
}

Luogu P2375 【NOI2014】动物园

题目链接

题目大意:
现在求一个(num)数组,定义为对于字符串(S)的前(i)个字符构成的子串,既是它的后缀同时又是它的前缀,并且该后缀与该前缀不重叠,将这种字符串的数量记作(num[i])
输出(num_i+1)的乘积。
多组((le 5))数据,字符串长度(le 1 imes 10 ^6)
题目思路:
暴力KMP,50分。
不难发现,对于节点(x),若(y)是合法的,那么y的border也是合法的。
考虑KMP的过程中,将当前位置与(fail_i)连边,会形成一棵树。那么我们可以在这棵树上通过树上倍增找到最深的合法的节点,则该节点的父亲均合法。
复杂度(T imes NlogN),理论上应该能过。但很遗憾,T了。
考虑快读+数组维度优化,Luogu (100) UOJ (90) LOJ (100)
代码:

#include<bits/stdc++.h>
#define mod 1000000007
using namespace std;
int t,f[1000100],fa[22][1000100],cnt[1000100],mi[21];char st[1000100];
int main()
{
	t=int(getchar()-'0');getchar();
	mi[0]=1;
	for (int i=1;i<=20;i++)
		mi[i]=mi[i-1]*2;
	for (int tt=1;tt<=t;tt++)
	{
		int len=1;
		st[1]=getchar();
		if (st[1]<'a'||st[1]>'z') {cout<<"1
";continue;}
		f[1]=0;cnt[1]=0;
		for (int i=2,j=0;;i++)
		{
			len++;
			st[i]=getchar();
			if (st[i]<'a'||st[i]>'z') {len--;break;}
			while (st[i]!=st[j+1]&&j)
				j=f[j];
			if (st[i]==st[j+1]) f[i]=j+1,j++,cnt[i]=cnt[j]+1;else f[i]=0,cnt[i]=0;			
			bool flag=true;
			if (f[i]) fa[0][i]=f[i];else fa[0][i]=0,flag=false;
			for (int k=1;mi[k]<=i;k++)
				fa[k][i]=fa[k-1][fa[k-1][i]];
		}
		long long ans=1;
		for (int i=2,sm=0;i<=len;i++)
		{
			int s=1,k=i;
			if (i>mi[sm]) sm++;
			for (int j=sm;j>=0;j--)
				if ((f[fa[j][k]]<<1)>i) k=fa[j][k];
			if ((f[k]<<1)>i)
			{
				if ((f[fa[0][k]]<<1)<=i) k=fa[0][k]; else continue;
			}
			s+=cnt[k];
			ans*=s;
			ans%=mod;
		}
		printf("%lld
",ans);
	}
	return 0;
}

正解是用dfs,有兴趣的可自行寻找资料。

鸣谢

1.本文大量参考@command-block的相关博客

原文地址:https://www.cnblogs.com/fmj123/p/14146588.html