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\) 最大呢?

考虑如图所示的情况。此时 \([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

浙公网安备 33010602011771号