【日常训练】简单的字符串

Problem

求长度为 \(n\) 的、字符集大小 \(5000\) 的串有多少个偶数长的子串前一半和后一半循环同构。
\(n \leq 5000\)

Solution

分析

问题即为求有多少个子区间可以表示成 \(uvvu\)\(|u|\neq 0\)\(|v|\) 可以为 \(0\))的形式。

我们发现,如果以两个 \(v\) 作为中心,从中心分别向两边扩展,从这个过程可以得到两个字符串 \(a=v^Ru^R,b=vu\)\(u^R\) 表示 \(u\) 的反转),那么 \(s=a_1b_1a_2b_2\dots a_nb_n\) 这个串相当于是 \(v\)\(v^R\) 交错拼起来,\(u\)\(u^R\) 交错拼起来,然后再把这两个得到的字符串连起来。那么 \(s\) 这个串相当于两个偶回文串拼接而成。

考虑枚举中心,然后得到向两边扩展的字符串 \(a,b\),两个字符串都只保留前缀 \(1\dots \min\{|a|,|b|\}\),然后根据上面的方式得到字符串 \(s\)。那么我们就是需要统计,\(s\) 有多少个前缀,能够表示成两个偶回文串拼接而成的结果(注意特判只有 \(|v|=0\))。

Lemma 1: 对于一个双回文串 \(s\)(能够表示成两个非空回文串拼接的结果),若 \(s=x_1x_2=y_1y_2=z_1z_2(|x_1|<|y_1|<|z_1|)\)\(x_2,y_1,y_2,z_1\) 是回文串,则 \(x_1,z_2\) 也是回文串。

证明: 下面只证 \(x_1\) 是回文串,\(z_2\) 同理。

如上图,设 \(z_1=y_1v\),则 \(v\)\(y_2\) 的前缀,\(v^R\)\(x_2,y_2\) 的后缀,\(v\)\(x_2\) 的前缀,于是 \(x_1v\)\(z_1\) 的前缀。

\(y_1\)\(z_1\) 的 border,所以 \(|v|\)\(z_1\) 的 period,于是 \(|v|\) 也是 \(x_1v\) 的 period。

所以 \(x_1\)\(v^{\infty}\) 的后缀。

\(v^R\)\(x_1,z_1\) 的前缀,而 \(|v|\)\(x_1\) 的 period,所以 \(x_1\)\(\left(v^R\right)^{\infty}\) 的前缀。

\(x_1\) 是回文串。\(\square\)

Lemma 2: 对于一个双回文串 \(s\),存在一种回文划分 \(s=ab\)\(a,b\) 均为回文串且非空),使得 \(a\)\(s\) 的最长回文前缀,或 \(b\)\(s\) 的最长回文后缀。

可以根据 Lemma 1 加上一些分类得到,这里不详细证明。

Lemma 3: 对于一个双偶回文串 \(s\)(能表示成两个偶回文串拼接的结果),存在一种回文划分 \(s=ab\)\(a,b\) 均为偶回文串且非空),使得 \(a\)\(s\) 的最长回文前缀,或 \(b\)\(s\) 的最长回文后缀。

将 Lemma 1 和 Lemma 2 的「回文串」改成「偶回文串」结论仍然成立。

Manacher 做法

因此我们只需要求出所有前缀的最长偶回文前缀和最长偶回文后缀,并分别判断剩下的部分是否回文。

求所有前缀的最长偶回文后缀,可以从左往右扫,维护一个当前的回文串能延伸右边界 \(rit\)。每次枚举到新的回文中心 \(i\),只需要更新左端点在区间 \((rit,i+r_i)\) 内的信息即可(\(r_i\) 表示 \(i\) 的回文半径)。正确性显然。(注意因为我们只考虑偶回文串,所以我们只枚举以特殊字符 #(Manacher 时插入的特殊字符)为回文中心的贡献)

判断剩下的部分是否回文就直接用中心的回文半径判即可。最长偶回文前缀就枚举的时候顺便维护一下即可。

#include <bits/stdc++.h>

template <class T>
inline void read(T &x)
{
	static char ch; 
	while (!isdigit(ch = getchar())); 
	x = ch - '0'; 
	while (isdigit(ch = getchar()))
		x = x * 10 + ch - '0'; 
}

template <class T>
inline void relax(T &x, const T &y)
{
	if (x < y)
		x = y; 
}

template <class T>
inline void tense(T &x, const T &y)
{
	if (x > y)
		x = y; 
}

const int MaxN = 1e4 + 5; 

int n, m, ans; 
int a[MaxN], s[MaxN]; 

inline void solve(int *s, int n)
{
	static int t[MaxN], r[MaxN], m; 
	static int max_suf[MaxN]; 
	m = 0;

	for (int i = 1; i <= n; ++i)
	{
		t[(i << 1) - 1] = 0; 
		t[i << 1] = s[i]; 
	}

	m = n << 1 | 1, t[m] = 0; 
	t[0] = -1, t[m + 1] = -2; 

	int rit = 0, p = 0; 
	for (int i = 1; i <= m; ++i)
	{
		if (rit > i)
		{
			int j = (p << 1) - i; 
			r[i] = std::min(r[j], rit - i); 
		}
		else
			r[i] = 1; 
		while (t[i - r[i]] == t[i + r[i]])
			++r[i]; 
		if (i + r[i] > rit)
		{
			rit = i + r[i]; 
			p = i; 
		}
		max_suf[i] = 0; 
	}

	rit = 1; 
	for (int i = 1; i <= m; i += 2)
	{
		for (int j = i + r[i] - 1; j >= rit && j > i; --j)
			relax(max_suf[j], (j - i) << 1 | 1); 
		relax(rit, i + r[i]); 
	}

	int cur_pre = 0; 
	for (int i = 2; i <= n; i += 2)
	{
		bool flg = false; 

		if (r[i + 1] > i)
		{
			flg = true; 
			cur_pre = i + 1;
		} 
		if (max_suf[i << 1])
		{
			int cur = i - (max_suf[i << 1] >> 1) - 1; 
			if (r[cur + 1] > cur)
				flg = true; 
		}
		if (cur_pre && r[cur_pre + i] > i - cur_pre)
			flg = true; 
		ans += flg; 
	}
}

int main()
{
	freopen("naive.in", "r", stdin); 
	freopen("naive.out", "w", stdout); 

	read(n);
	for (int i = 1; i <= n; ++i)
		read(a[i]); 
	for (int i = 1; i < n; ++i)
	{
		m = 0; 
		int l = i, r = i + 1; 
		while (l >= 1 && r <= n)
		{
			s[++m] = a[l]; 
			s[++m] = a[r]; 
			--l, ++r; 
		}

		solve(s, m); 
	}
	std::cout << ans << std::endl; 

	return 0; 
}

回文树做法

用 Hash 判回文。求每个前缀的最长偶回文前缀只要枚举的时候维护一下即可。

然后求最长偶回文后缀只要在回文树上找到对应点的 fail 链上深度最大的偶回文串即可,这个也可以在构造回文树的时候顺带维护一下。

(代码懒得写)

参考文献

  1. WC2017 字符串算法选讲 —— 金策

  2. 《简单的字符串》题解 —— 钟子谦

posted @ 2019-11-26 20:26  changle_cyx  阅读(372)  评论(0编辑  收藏  举报