[CF932G]Palindrome Partition

38 篇文章 0 订阅
2 篇文章 0 订阅

题目

传送门 to luogu

思路

可以看看 y y b \tt yyb yyb 大佬的博客。如果你直接觉得 “ 没问题!简单!” 那就不用看这篇博客了 😉

第一步,令 S ′ = S 1 S n S 2 S n − 1 S 3 S n − 2 … S n 2 S n 2 + 1 S'=S_1S_nS_2S_{n-1}S_3S_{n-2}\dots S_{\frac{n}{2}}S_{\frac{n}{2}+1} S=S1SnS2Sn1S3Sn2S2nS2n+1 ,那么问题转化为,将 S ′ S' S 划分为若干个偶数长度的回文串。这个构造请自己手推。

f ( x ) f(x) f(x) 表示长度为 x x x 的前缀的划分方案数。建立回文自动机之后,不难想到 O ( n 2 ) \mathcal O(n^2) O(n2) 的暴力跳 f a i l fail fail 的转移。为了让 “ 偶数长度 ” 这个奇怪的限制滚蛋,直接令 f ( 2 x + 1 ) = 0 f(2x+1)=0 f(2x+1)=0 就可以无差别转移了。

第二步,由于转移只是单纯的 ∑ f \sum f f ,可以考虑用 g g g 存储一段 d i f ( x ) = l e n ( x ) − l e n ( f a i l ) dif(x)=len(x)-len(fail) dif(x)=len(x)len(fail) 相等的点。

然而,这一步有问题了!那就是, g g g 到底代表哪些 f f f 的和?因为字符串的右端点是在改变的!

我的理解是,事实上 g ( x ) = ∑ k f [ ω − 1 + k    d i f ( x ) ] g(x)=\sum_{k}f[\omega-1+k\;dif(x)] g(x)=kf[ω1+kdif(x)] ,其中 ω \omega ω 是该字符串 最后一次出现 的左端点位置。而 k k k 的范围是什么呢?当然是 “ d i f dif dif 等值链 ” 的长度。

此时重新考虑这个问题。加入一个字符,会影响哪些 g g g 的值呢?也就是 ω \omega ω 什么时候会变?显然是后缀。那么我们只要把新点 f a i l fail fail 链上的 g g g 更新了即可!

好,我们继续。怎么转移 g g g 呢?考虑 f a i l ( x ) fail(x) fail(x) g g g 是哪些 f f f众所周知,对于一个回文自动机,新产生的后缀串 S t r ( f a i l ( x ) ) Str(fail(x)) Str(fail(x)) ,如果长度不小于 l e n ( x ) 2 \frac{len(x)}{2} 2len(x)其最后一次出现恰好是将其用 S t r ( x ) Str(x) Str(x) 翻转后的结果

这个东西挺好证。首先,用 S t r ( x ) Str(x) Str(x) 将其翻转后结果必然是一次出现。其次,如果它有更靠后的出现,那么这两个串取并也是一个回文串,与 f a i l ( x ) fail(x) fail(x) 是最长的回文后缀冲突。

要不咱还是画个图?虽然是用代码块的像素风。

Str(x):                   ------xxxxxxxxxx
Str(fail):                ---------xxxxxxx
REVERSE, Str(fail):       ------xxxxxxx---
if Str(fail):             -------xxxxxxx--
then Str(?):              -------xxxxxxxxx

不难验证,上图中的 S t r ( ? ) Str(?) Str(?) 是一个回文串,它是由别的 S t r ( f a i l ) Str(fail) Str(fail)(我们假设的一个,在 if 语句中的)和后缀 S t r ( f a i l ) Str(fail) Str(fail) 拼接而成的。如果它真的存在,它肯定会成为 f a i l fail fail ,因为它更长。

既然这个结论成立,那么我们的更新就可以变的骚一点。先记 g ( x ) g(x) g(x) 恰好管不着的那个点为 t o p ( x ) top(x) top(x)(确实很像链剖分)。如果 t o p ( x ) = f a i l ( x ) top(x)=fail(x) top(x)=fail(x) ,直接更新即可(因为此时 g g g 只能管自己的值)。否则, d i f ( x ) < l e n ( x ) 2 dif(x)<\frac{len(x)}{2} dif(x)<2len(x) ,毕竟 d i f [ f a i l ( x ) ] = d i f ( x ) dif[fail(x)]=dif(x) dif[fail(x)]=dif(x) 嘛,跳两下父节点的话,长度要变小 2    d i f ( x ) 2\;dif(x) 2dif(x) 的。

于是, l e n [ f a i l ( x ) ] > l e n ( x ) 2 len[fail(x)]>\frac{len(x)}{2} len[fail(x)]>2len(x) ,恰好满足我们上面的结论触发条件。所以 f a i l ( x ) fail(x) fail(x) x x x 都满足 ω = n − l e n ( x ) + 1 \omega=n-len(x)+1 ω=nlen(x)+1 。那么 g [ f a i l ( x ) ] g[fail(x)] g[fail(x)] g ( x ) g(x) g(x) 的唯一差别就只在于定义式中 k k k 的范围。显然 x x x 对应的 k k k 的范围比 f a i l ( x ) fail(x) fail(x) 的大 1 1 1 。直接加上这个差异点 f [ n − l e n ( t o p ) − d i f ] f[n-len(top)-dif] f[nlen(top)dif] 即可。

用行中公式写一下: g ( x ) = g [ f a i l ( x ) ] + f [ n − l e n ( t o p ) − d i f ] g(x)=g[fail(x)]+f[n-len(top)-dif] g(x)=g[fail(x)]+f[nlen(top)dif]

如果你没有看懂上面这段话!请看 l l s w \tt llsw llsw 题解中的最后一张图片。
E x t r a    E x p l a n a t i o n Extra\;Explanation ExtraExplanation:为啥差异点是那个玩意儿?因为 g ( x ) g(x) g(x) 恰好管不着的位置是 n − l e n ( t o p ) n-len(top) nlen(top) 嘛,往回退一步就是管的到的。

所以,只要 f a i l ( x ) fail(x) fail(x) 信息正确,就可以求出 g ( x ) g(x) g(x)这么显然应该不用加粗才对。

可是这样仍然是 O ( n 2 ) \mathcal O(n^2) O(n2) 的,因为整条链的 g g g 都要被修改……

给出骚操作:每次更新,只更新 “ d i f dif dif 等值链 ” 的交界处 和当前点(目前的整个串的最长回文后缀)。这样就足以保证正确性。

怎么想到的?不知道。但是正确性容易证明。考虑没有被及时更新的点,其本质是 d i f ( x ) = d i f ( s o n x ) dif(x)=dif(son_x) dif(x)=dif(sonx)(虽然并没有 s o n son son 这个数组,意会就好),否则就会成为链的交界处。

假设我们正在更新 g ( y ) g(y) g(y) 。考虑一下, g [ f a i l ( y ) ] g[fail(y)] g[fail(y)] 是不是在我们想要的 n − d i f ( y ) n-dif(y) ndif(y) 处更新了呢?不妨用反证法,假设 当初 它没有被更新,因为它被子节点 x x x 跳了过去,满足 f a i l ( x ) = f a i l ( y ) fail(x)=fail(y) fail(x)=fail(y) d i f ( x ) = d i f [ f a i l ( x ) ] dif(x)=dif[fail(x)] dif(x)=dif[fail(x)]

首先你要意识到, d i f dif dif 就是周期,因为 f a i l fail fail 就是 b o r d e r \rm border border 。有了这一点,你就会意识到 d i f dif dif f a i l fail fail 链上是单减的(不然周期不会变)。由于我们要更新 y y y ,所以 d i f ( s o n y ) > d i f ( y ) dif(son_y)>dif(y) dif(sony)>dif(y)

并且,因为 d i f dif dif 是周期,所以 S t r ( x ) Str(x) Str(x) 一定是 S t r [ f a i l ( x ) ] Str[fail(x)] Str[fail(x)] 前面加上一个周期(一定是完整的周期)得到的结果。而我们知道, S t r [ f a i l ( x ) ] Str[fail(x)] Str[fail(x)] 当初的右端点为 n − d i f ( y ) n-dif(y) ndif(y) ,恰好是 S t r ( y ) Str(y) Str(y) 去掉从右边开始数的一个周期(尽管可能不是所谓的完整周期,但是确实为一个周期的长度),所以 S t r ( x ) = S t r ( y ) \color{black}Str(x)=Str(y) Str(x)=Str(y)

还是 用代码块风格 画一张图。要想到这一点: f a i l fail fail 只跟这个节点本身有关,所以无论右端点怎么变, f a i l fail fail 不变,进而 d i f dif dif 也不变。因为 f a i l ( y ) fail(y) fail(y) d i f dif dif 不变,所以导致它被跳过的 x x x 也就固定了。

.............. : son(y) (whatever string)
   abababababa : y (dif vary, so it's processed)
     ababababa : fail(y)
       abababa : fail(fail(y)) (just to show dif = 2)
   ababababa   : expected fail(y)
 abababababa   : x, who caused skip on fail(y)
 ababababababa : x∪y, who should be fail(son(y))

显然 S t r ( x ) Str(x) Str(x) S t r ( y ) Str(y) Str(y) 相交,所以二者的并也是回文串。不难发现这是从 n − d i f ( y ) − l e n ( y ) + 1 n-dif(y)-len(y)+1 ndif(y)len(y)+1 n n n 的。然鹅, d i f ( s o n y ) > d i f ( y ) ⇒ l e n ( s o n y ) > l e n ( y ) + d i f ( y ) dif(son_y)>dif(y)\Rightarrow len(son_y)>len(y)+dif(y) dif(sony)>dif(y)len(sony)>len(y)+dif(y) ,所以这个新回文串仍然包含在 s o n y son_y sony 以内。也就是说!在 S t r ( s o n y ) Str(son_y) Str(sony) 内!有一个比 f a i l fail fail 更长的回文后缀!矛盾!

当然,你会说,如果 y y y 是当前点而寻求更新呢?不一定满足 d i f ( s o n y ) > d i f ( y ) dif(son_y)>dif(y) dif(sony)>dif(y) 了吧(因为根本没有子节点)?然而这种情况 S t r ( x ) = S t r ( y ) Str(x)=Str(y) Str(x)=Str(y) 是仍然成立的。怎么可能新加入的点代表一个已经存在的字符串呢?

总结:虽然我们的更新不完全,但它足以应付我们需要用到的 g g g 了。

时间复杂度呢?由于 d i f dif dif b o r d e r \rm border border ,所以一定要 l e n ≤ d i f len\le dif lendif 时才能切换一个 d i f dif dif 。这个分析就跟辗转相除一样了:如果 d i f dif dif 不超过 l e n 2 \frac{len}{2} 2len ,那么余数自然不能超过 d i f dif dif ;而 d i f dif dif 超过一半长度时,一步到位。最多跳 O ( log ⁡ n ) \mathcal O(\log n) O(logn) 次。

代码

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}

const int Mod = 1e9+7;
const int MaxN = 1000005;
const int CharSiz = 26;

int f[MaxN], g[MaxN];
namespace PAM{
	int ch[MaxN][CharSiz];
	int fail[MaxN], cntNode = 1;
	char item[MaxN]; int len[MaxN];
	int n = 0, lst = 1, top[MaxN];
	void init(){
		++ cntNode; // include 0
		memset(ch,0,cntNode*CharSiz<<2);
		memset(fail,0,cntNode<<2);
		n = 0, cntNode = lst = 1;
		fail[0] = fail[1] = 1;
		len[1] = item[0] = -1;
	}
	void add(char c){
		item[++ n] = c; int x = lst;
		while(item[n-len[x]-1] != item[n])
			x = fail[x]; // find it
		int now = ch[x][c];
		if(now == 0){
			now = ++ cntNode; // new node
			int &p = fail[now] = fail[x];
			while(item[n-len[p]-1] != item[n])
				p = fail[p]; // find it
			p = ch[p][c]; // add char c
			ch[x][c] = now; // last step
			len[now] = len[x]+2;
			if((len[fail[now]]<<1) ==
			len[now]+len[fail[fail[now]]])
				top[now] = top[fail[now]];
			else top[now] = fail[now];
		}
		lst = now; // update lst
	}
	void solve(){
		int p = lst;
		for(; p&&p!=1; p=top[p]){
			int dif = len[p]-len[fail[p]];
			g[p] = f[n-len[top[p]]-dif];
			if(fail[p] != top[p])
				g[p] = (g[p]+g[fail[p]])%Mod;
			if(!(n&1)) f[n] = (f[n]+g[p])%Mod;
		}
	}
}

char xez[MaxN], tmp[MaxN];
int main(){
	scanf("%s",tmp);
	int zxy = strlen(tmp);
	for(int i=0; (i<<1)<zxy; ++i)
		xez[i<<1] = tmp[i];
	for(int i=0; (i<<1|1)<zxy; ++i)
		xez[i<<1|1] = tmp[zxy-1-i];
	PAM::init(), f[0] = 1;
	for(int i=0; i<zxy; ++i){
		PAM::add(xez[i]-'a');
		PAM::solve();
	}
	printf("%d\n",f[PAM::n]);
	return 0;
}

双倍经验

这道题做法没有任何区别。同样的构造方式。同样的优化方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值