再探字符串匹配——Z算法/Z-BOX算法(附CF 149E-Martian Strings )

前言

关于字符串匹配,最常见使用最广泛的即 K M P KMP KMP算法,该算法通过对模式串的子串的前后缀进行处理而减少回溯次数,提高匹配效率,相关内容参考浅析字符串匹配算法——KMP算法。此文主要讨论字符串的另一种匹配算法——    Z    \;Z\; Z算法/    Z − B O X    \;Z-BOX\; ZBOX算法。

原理

何为    Z    \;Z\; Z算法,    Z    \;Z\; Z算法的主要设计思想是通过一个 z z z数组来记录一个字符串的后缀与该字符串的最大前缀匹配长度。换句话说,    z [ i ]    \;z\left [ i \right]\; z[i]表示对于字符串下标为    i    \;i\; i的字符开始,与字符串的最大前缀匹配长度。例如,对于字符串    a a a b a a b a    \;aaabaaba\; aaabaaba,对应的    z    \;z\; z函数为:

那么如何求得每一个字符对应的    z [ i ]    \;z\left [ i \right]\; z[i]值呢。在进行计算过程中,我们需要两个关键的信息,即对于   s [ i ]    \ s\left [ i \right]\;  s[i]而言,在计算    z [ i ]    \;z\left [ i \right]\; z[i]时,    z [ i ]    \;z\left [ i \right]\; z[i]的区间范围是多少,用    l    \;l\; l    r    \;r\; r来记录,其定义为当前元素所被包含范围内    r    \;r\; r最大的区间,也就是说对于每一个    z [ i ]    \;z\left [ i \right]\; z[i],同时称这个区间为一个盒子,也就是一个范围为 [ l , r ]    \left [ l,r \right]\; [l,r] b o x box box所盖住的元素。我们可以根据前    i − 1    \;i-1\; i1    z    \;z\; z函数以及不断维护的    l    \;l\; l    r    \;r\; r来求取当前    z [ i ]    \;z\left [ i \right]\; z[i]的值。

这么说还是很抽象难以理解,拿上述例子来说,对于    s [ 0 ]    \;s\left [ 0 \right]\; s[0]很显然其值为    s . l e n g t h    \;s.length\; s.length,此时将    l    \;l\; l    r    \;r\; r初始化为0,对于    s [ 1 ]    \;s\left [ 1 \right]\; s[1]计算得到    s [ 12 ]    = =    s [ 01 ]    \;s\left [ 12 \right]\;==\;s\left [ 01 \right]\; s[12]==s[01],所以我们将    z [ 1 ]    \;z\left [ 1 \right]\; z[1]设置为2,同时更新    l = 1 ,    r = 2    \;l=1,\;r=2\; l=1,r=2;紧接着对于    s [ 2 ]    \;s\left [ 2 \right]\; s[2],可以得到    s [ 2 ] = =    s [ 0 ]    \;s\left [ 2 \right]==\;s\left [ 0 \right]\; s[2]==s[0],将    z [ 2 ]    \;z\left [ 2 \right]\; z[2]设置为1,不更新    l    \;l\; l    r    \;r\; r,这是因为根据定义    l    \;l\; l    r    \;r\; r是盖住当前元素的 b o x box box右边界最大的区间范围,在这次计算过程中    r    \;r\; r仍为2,所以区间范围不变。同样方法可以求出所有元素的    z [ i ]    \;z\left [ i \right]\; z[i](理解    l    \;l\; l    r    \;r\; r的含义对算法实现十分关键)。

那么设置的    l    \;l\; l    r    \;r\; r对求    z [ i ]    \;z\left [ i \right]\; z[i]有什么作用呢,对于维护的    l    \;l\; l    r    \;r\; r我们首先能得到的信息是: s 0 s 1 . . . s r − l = = s l s l + 1 . . . s r s_0s_1...s_{r-l}==s_ls_{l+1}...s_r s0s1...srl==slsl+1...sr

考虑如下几种情况:
1.
此时我们要计算的    z [ i ]    \;z\left [ i \right]\; z[i]的取值已经超出了    b o x    \;box\; box的范围,所以这时候已有的    b o x    \;box\; box已经不能为我们提供有用信息,此时只能通过枚举逐一比对,如果存在与前缀相同的后缀,更新    l = i , r = l e n g t h \;l=i,r=length l=i,r=length
2.
对于    i    \;i\; i位于 [ l , r ] \left [ l,r \right] [l,r]中此时    l    \;l\; l    r    \;r\; r就起到了关键作用。    i    \;i\; i位于    b o x    \;box\; box那么一定就有    s i − l s i − l + 1 . . . s r − l = = s i s i + 1 . . . s r    \;s_{i-l}s_{i-l+1}...s_{r-l}==s_{i}s_{i+1}...s_{r}\; silsil+1...srl==sisi+1...sr,那么根据    z [ i − l ]    \;z\left [ i-l \right]\; z[il]的大小又可以分为如下两种情况:

如果    z [ i − l ] < r − i + 1 \;z\left [ i-l \right]<r-i+1 z[il]<ri+1时,那么有    z [ i ]    = z [ i − l ]    \;z\left [ i \right]\;=z\left [ i-l \right]\; z[i]=z[il]。其中    r − i + 1    \;r-i+1\; ri+1表示    s i s i + 1 . . . s r    \;s_is_{i+1}...s_r\; sisi+1...sr的长度。也就是说,如果    z [ i − l ]    \;z\left [ i-l \right]\; z[il]的长度在当前的 b o x box box内,根据这种相等关系可以直接得出    z [ i ]    \;z\left [ i \right]\; z[i]的值,同时这种情况下不需要更新    l    \;l\; l    r    \;r\; r,因为新的    r    \;r\; r还是在原    b o x    \;box\; box中。

另外一种情况就是    z [ i − l ] ≥ r − i + 1    \;z\left [ i-l \right]≥r-i+1\; z[il]ri+1时,这时候根据已有的区间信息我们仅仅只能得到    s i − l s i − l + 1 . . . s r − l = = s i s i + 1 . . . s r    \;s_{i-l}s_{i-l+1}...s_{r-l}==s_is_{i+1}...s{r}\; silsil+1...srl==sisi+1...sr,而对于    r    \;r\; r之后的元素,由于已经在    b o x    \;box\; box之外,所以我们没法判断其是否与对应的前缀相等,所以这时候仍然需要去枚举一一比对。在这种情况下,如果    r    \;r\; r之后有相同元素,那么    l    \;l\; l    r    \;r\; r将更新。特别地,考虑    z [ i − l ] = = r − i + 1    \;z\left [ i-l \right]==r-i+1\; z[il]==ri+1的情况为什么不是和上一种情况一样,此时    z [ i − l ]    \;z\left [ i-l \right]\; z[il]的长度并没有超出    b o x    \;box\; box,这是因为及时没有超出    b o x    \;box\; box,但它已经到了临界范围,对于下一个元素是否相同是未知的,所以需要进行枚举比对。

有了上述设计思想后,实现代码如下:

void get_next(char* ch, int f)
{
	int l = 0, r = 0;
	z[0] = len;
	for (int i = 1; i < len; i++)
	{
		if (i > r)			//对应于第一种情况,此时box不能提供帮助,所以枚举得到z[i]
		{
			int j = 0;
			while (ch[j] == ch[i + j])
			{
				j++;
			}
			if (j)		//如果存在与前缀相同的后缀,则需要更新box的范围
			{
				l = i;
				r = i + j - 1;
			}
			z[i] = j;
		}
		else
		{
			if (z[i - l] < r - i + 1)			//对于第二种情况的第一种情况
			{
				z[i] = z[i - l];
			}
			else
			{
				int j = 1;
				while (ch[r + j] == ch[r - i + j])			//枚举box范围外有多少相同元素
				{
					j++;
				}
				if (j > 1)			//如果box范围外还存在相同元素,更新新的l和r
				{
					l = i;
					r += j - 1;
				}
				z[i] = r - i + 1;
			}
		}
	}
}

那么该如何应用    Z    \;Z\; Z算法呢,只需要将模式串放在文本串之前,然后在计算    z [ i ] \;z\left [ i \right] z[i]的过程中,一旦    i > s . l e n g t h ( ) & & z [ i ] ≥ s . l e n g t h    \;i>s.length()\&\&z\left [ i \right]≥s.length\; i>s.length()&&z[i]s.length,那么就可以得到文本串中存在模式串的子串。

例题:CF 149E-Martian Strings



题意大概描述的就是给一个文本串    s    \;s\; s和多个模式串    p    \;p\; p,问是否能在    s    \;s\; s中找到两个不重复的连续子串使其组合成为    p    \;p\; p。(当然这道题也可以利用    k m p    \;kmp\; kmp算法或者    A C    \;AC\; AC自动机完成,这里重点为突出    Z    \;Z\; Z算法的使用。)

算法思想:由于考虑到现在所要匹配的字符串分为了两个部分,所以我们可以分别正向和反向匹配一次,正向匹配记录下每个长度的前缀第一次出现的位置,反向匹配记录下每个长度的后缀第一次出现的位置,然后枚举前缀和后缀,如果二者位置不重合,则可认为存在这样的两个字串构成模式串。

Solution:

#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 1005;
int m, len, len_s, len_t, ans;
int forward_right[maxn], reverse_left[maxn], z[maxn];		//forward_right记录正向匹配过程中,每一个长度前缀出现的末位置,reverse_left记录记录反向匹配过程中,每一个长度后缀出现的始位置
char s1[maxn], t1[maxn], s2[maxn], t2[maxn];
bool flag;
inline void z_func(char* ch, int f)			//f作为标志域,判断是正向匹配还是反向匹配
{
	flag = false;
	memset(z, 0, sizeof(z));
	int l = 0, r = 0;
	z[0] = len;
	for (int i = 1; i < len; i++)
	{
		//对应三种情况
		if (i > r)
		{
			int j = 0;
			while (ch[j] == ch[i + j])
			{
				j++;
			}
			if (j)
			{
				l = i;
				r = i + j - 1;
			}
			z[i] = j;
		}
		else
		{
			if (z[i - l] < r - i + 1)
			{
				z[i] = z[i - l];
			}
			else
			{
				int j = 1;
				while (ch[r + j] == ch[r - i + j])
				{
					j++;
				}
				if (j > 1)
				{
					l = i;
					r += j - 1;
				}
				z[i] = r - i + 1;
			}
		}
		if (z[i] >= len_t)		//当z[i]值大于模式串的长度时,意味着文本串中有模式串的子串,这时候可以肯定结果,不需要再匹配了
		{
			ans++;
			flag = true;
			return;
		}
		if (i >= len_t && z[i])			//当开始匹配文本串位置的字符时,如果当前位置有与前缀相同部分,则记录下前缀出现的位置
		{
			if (f)
			{
				if (!forward_right[z[i]])			//这里是为了保证记录的位置尽可能靠前
				{
					for (int j = i, t = 1; t <= z[i]; j++, t++)
					{
						if (forward_right[t])
						{
							continue;
						}
						forward_right[t] = j;
					}
				}
			}
			else
			{
				if (!reverse_left[z[i]])			//这里是为了保证记录的位置尽可能靠后
				{
					for (int j = i, t = 1; t <= z[i]; j++, t++)
					{
						if (reverse_left[t])
						{
							continue;
						}
						reverse_left[t] = len - j + len_t - 1;
					}
				}
			}
		}
	}
}
int main()
{
	scanf("%s%d", s1, &m);
	while (m--)
	{
		memset(forward_right, 0, sizeof(forward_right));
		memset(reverse_left, 0, sizeof(reverse_left));
		scanf("%s", t1);
		len_s = (int)strlen(s1);
		len_t = (int)strlen(t1);
		if (len_t < 2 || len_t>len_s)
		{
			continue;
		}
		strcpy(s2, s1);
		strcpy(t2, t1);
		len = len_s + len_t;
		strcat(t1, s2);			//t1表示正向t+s
		z_func(t1, 1);
		if (flag)
		{
			continue;
		}
		reverse(s2, s2 + len_s);
		reverse(t2, t2 + len_t);
		strcat(t2, s2);			//t2表示反向t+s
		z_func(t2, 0);
		if (forward_right[len_t])
		{
			ans++;
			continue;
		}
		for (int i = 1; i < len_t; i++)
		{
			if (forward_right[i] && reverse_left[len_t - i])
			{
				if (reverse_left[len_t - i] > forward_right[i])
				{
					ans++;
					break;
				}
			}
		}
	}
	printf("%d\n", ans);
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值