P9753 [CSP-S 2023] 消消乐

题目传送门

博客传送门

曾经某位大佬说过,计数类问题一般考虑排列组合或dp。它看起来不像是排列组合的题目,所以考虑dp。

0.消消乐序列

我们发现这个可消串类似一个括号匹配。于是类似地,我们定义消消乐序列如下:

1.空串是一个合法的消消乐序列。

2.对于任意消消乐序列 A,SAS 是一个合法的消消乐序列,其中 S 为任意一个小写字符。

3.对于任意消消乐序列 A,B,AB是一个合法的消消乐序列。

4.任意一个消消乐序列均可以通过以上方式构造出来。

原问题等价于求原串有多少非空连续子串是消消乐序列。

1.dp转移方程式

\(dp_i\) 表示以 \(i\) 结尾的消消乐序列的个数,再设 \(lst_i\) 表示最大的使得 \([lst_i,i]\) 为消消乐序列的数。

根据性质 3,所有以 \(lst_i-1\) 为结尾的消消乐序列都可以拼接上这个消消乐序列,变成以 \(i\) 为结尾的消消乐序列。而这个序列本身也是个消消乐序列。所以状态转移方程为 \(dp_i=dp_{lst_i-1}+1\)

为什么我们要求 \(lst_i\) 最大呢?

P9753_1_1

考虑如图所示的情况。此时 \([j,i]\)\([lst_i,i]\) 均为消消乐序列。

然后我们就发现,如果按照刚才的递推式是忽略了 \([lst_i,i]\) 这个序列的。如果改用其他转移方程,也不好写转移方程式或者时间复杂度不优。

2.暴力跳

那如何求 \(lst_i\) 呢?或者说,如果我们求完了 \(i\) 的各种信息,怎么求 \(lst_{i+1}\) 呢?

第一种情况类似于 abcddcba\(s_{i+1}=s_{lst_i-1}\),也就是 \(s_{lst_i-1}\)\(s_{i+1}\) 可以把 \([lst_i,i]\) 这个序列包起来,形成一个新的合法序列。那我们直接跳到 \(lst_i-1\),并令 \(lst_{i+1}=lst_i-1\) 就好了。

第二种情况类似于 dabbaceffecd,当前 \(i+1\) 在第二个 \(d\) 处。那我们跳了一段合法序列 \([lst_i,i]\) 后,发现 \(s_{lst_i-1}\)\(s_{i+1}\) 并不能把该序列包起来。

这说明什么,说明 \(i+1\)\(lst_{i+1}\) 之间隔了很多个消消乐序列,那我们就接着暴力往后跳,跳过 \([lst_{lst_i},lst_i]\) 再次检查,再不行就跳过 \([lst_{lst_{lst_i}},lst_{lst_i}]\)……

总之,我们一个消消乐序列一个消消乐序列地往后跳,直到找到一个位置可以和它包住前面的消消乐序列。或者跳到序列以外,说明并没有以 \(i+1\) 结尾的消消乐序列。

3.时间复杂度

本题题解区还是有很多大佬证明过这个东西的,本蒟蒻也浅浅证明一下这个东西是 \(O(n|S|)\) 的叭。

我们的核心思路就是证明,对于点 \(i\) 和某种字符 \(S\),只会有 1 个点 \(j\) 跳到点 \(i\)

反证法,假设 \(k\)\(j\) 两个位置都能跳到 \(i\),并且 \(s_j=s_k,k>j\)。这说明 \([i+1,j-1]\)\([i+1,k-1]\) 都是消消乐序列。那根据性质 3 的变形,他们相减后的区间 \([j,k-1]\) 也是消消乐序列。

这样的话肯定会存在一个最小的 \(t \in (j,k-1]\)\([j,t]\) 是消消乐序列(最起码 \(t=k-1\) 满足条件)。如果 \(t=k-1\),那 \(k\) 第一次跳的时候就直接满足条件退出了。

否则的话 \([t+1,k-1]\) 是个消消乐序列,\(i < j < t \le lst_k\)(当 \([t+1,k-1]\) 是极短消消乐序列时取等),\(k\) 也不可能跳到 \(i\)

综上,原结论成立。所以每个位置最多被跳 26 次,时间复杂度 \(O(n|S|)\)

代码极其简短,如下所示:

P9753
#include<bits/stdc++.h>
#define int long long
using namespace std;

inline int read(){
	int x=0,f=1;char c=getchar();
	while(c<48){
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>47) x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return x*f;
}

const int N=2e6+6;
int n,dp[N],lst[N];
char s[N];

signed main(){
	n=read();
	scanf("%s",s+1);
	int ans=0;
	for(int i=2;i<=n;i++){
		for(int j=i-1;j>0;j=lst[j]-1){//暴力跳一个消消乐序列 
			if(s[j]==s[i]){
				lst[i]=j;
				break;
			}
		}
		if(lst[i]){//存在以 i 为结尾的消消乐序列 
			dp[i]=dp[lst[i]-1]+1;
			ans+=dp[i];
		}
	}
	printf("%lld",ans);
	return 0;
}

参考资料:

1.洛谷题解1

2.洛谷题解2

posted @ 2025-10-29 09:13  qwqSW  阅读(17)  评论(0)    收藏  举报