【题解】P9753 [CSP-S 2023] 消消乐(字符串哈希,DP)
【题解】P9753 [CSP-S 2023] 消消乐
不知道考场脑子是抽了还是有病,全程都不知道在放什么屁。
特别鸣谢:@dbxxx 给我讲解了解法一的满分做法,并让我对哈希有了更加深刻的认识;@Daidly 给我讲解了解法二。
博客园食用效果更佳
题目链接
P9753 [CSP-S 2023] 消消乐
题意概述
给定一个长度为
数据范围
对于所有测试数据有:
测试点 | 特殊性质 | |
---|---|---|
无 | ||
无 | ||
无 | ||
A | ||
B | ||
无 | ||
无 |
特殊性质 A:字符串中的每个字符独立等概率地从字符集中选择。
特殊性质 B:字符串仅由 a
和 b
构成。
思路分析
解法一
首先考虑一个字符串什么时候是“可消除的”,我们可以考虑类似于括号匹配的办法:
维护一个栈,按顺序遍历字符串,若当前字符恰好等于栈顶,则弹出栈顶;反之则将该字符入栈。若遍历结束后,栈为空,则说明该字符串是“可消除的“。
题目要求对于一个字符串所有的子串是否为“可消除的”,那么最暴力的想法就是暴力枚举该字符串的每个子串
时间复杂度
考虑优化。注意到对于起点
遍历
时间复杂度
考虑特殊性质 A,发现在随机序列下,符合条件的子串非常短,那么我们只需要选择区间长度较小的子串进行验证,就可以在题目要求的时间内过掉这两个点。
该特殊性质加上
受
发现
维护栈似乎是无法优化的,那么考虑我们如何做可以不用枚举子串起点。
发现在从
那么我们可以通过字符串哈希来维护每个时刻的栈序列,那么栈序列相同说明该情况下哈希值完全相同,可以用 map
或 unordered_map
来维护每种哈希值出现了多少次。假设一种哈希值出现了
对于每种哈希值对答案的贡献求和,即为最终答案。
时间复杂度:map
维护 unordered_map
维护
注意:若采用单模数哈希,模数如果为
998244353 或10^9+7 ,相当于是2\times 10^6 个数要落在大约是[0,10^9] 这个区间,产生哈希冲突的可能性较大。所以最好用双模数哈希/自然溢出。双模数哈希相当于随机范围是两个模数相乘,自然溢出相当于是[0,2^{64}] ,产生哈希冲突的可能性较小。由于自然溢出更好写,我采用的是自然溢出。
解法二
上一种解法,我们其实主要是站在「如何消」的角度展开思考的,下来我们来站在「区间合法性」的角度来思考这个问题。
说明:以下分析用
s_i 表示给定字符串的第i 个字符。
考虑 DP,我们用
考虑
要使得
现在考虑如何找
我们假设
- 当
s_{lst_i-1}=s_{i+1} 时,把s_{i+1} 和s_{lst_i-1} 接在[lst_i,i] 的两端,合成[lst_i-1,i+1] 一并消除,此时lst_{i+1}=lst_i-1 ; - 当
s_{lst_i-1}\ne s_{i+1} 时,那么一定是类似于faabccbf
这样(现在是第二个f
),中间有多个合法区间连在一起,那么我们就考虑以lst_i-1 结尾的最小合法区间[lst_{lst_i-1},lst_i-1] 。如果s_{lst_{lst_i-1}-1}=s_{i+1} ,那么就可以把s_{i+1} 和s_{lst_{lst_i-1}-1} 并在[lst_{lst_i-1},i] 两端一并消除,此时,lst_{i+1}=lst_{lst_{i-1}-1} ;如果s_{lst_{lst_i-1}-1}\ne s_{i+1} ,就再往前跳找lst_i 。
考虑复杂度分析。
定义「等价类」表示解法一(哈希做法)中同一个前缀栈哈希值。那么我们发现我们往前跳
假如我们当前在某个等价类的最后一个字符,那么这个等价类就可以构成一个字符串。然后要在当前为止后面再加一个字符 a
,那么实际上就是要找当前等价类字符串里的最后一个 a
,那么时间复杂度就是等价类字符串里两个 a
的下标差。
即对于单个字符 a
,查找 a
的下标减去倒数第二个下标加上倒数第二个下标减去倒数第三个下标,以此类推,线性相当于等价类大小。由于等价类大小总和为线性,那么单个字符的复杂度线性,所以总复杂度就是
代码实现
解法一
//B
//哈希做法
//The Way to The Terminal Station……
#include<cstdio>
#include<iostream>
#include<map>
#include<stack>
#include<set>
#define int unsigned long long
const int P=29;
using namespace std;
const int maxn=2e6+10;
int has[maxn],ans;
map<int,int>mp;
stack<int>q,pos;
set<int>S;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
signed main()
{
int n=read();
string s;
cin>>s;s='%'+s;
S.insert(0);
mp[0]++;
for(int i=1;i<=n;i++)
{
if(!q.empty()&&q.top()==s[i]-'a')
{
mp[has[pos.top()-1]]++;
has[i]=has[pos.top()-1];
q.pop();
pos.pop();
}
else
{
q.push(s[i]-'a');
pos.push(i);
has[i]=has[i-1]*P+s[i]-'a'+1;
S.insert(has[i]);
mp[has[i]]++;
}
}
for(int v:S)ans+=mp[v]*(mp[v]-1)/2;
cout<<ans<<'\n';
return 0;
}
解法二
//B
//The Way to The Terminal Station…
#include<cstdio>
#include<iostream>
#define int long long
using namespace std;
const int maxn=2e6+10;
int dp[maxn],lst[maxn];
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
signed main()
{
int n=read();
string s;
cin>>s;s='%'+s;
int ans=0;
for(int i=1;i<=n;i++)
{
for(int j=i-1;j>0;j=lst[j]-1)
{
if(s[i]==s[j])
{
lst[i]=j;break;
}
}
if(lst[i])dp[i]=dp[lst[i]-1]+1,ans+=dp[i];
}
cout<<ans<<'\n';
return 0;
}
如果觉得我的题解写得好,请给我的题解点个赞,谢谢!