P9753 CSP-S 2023 消消乐 - 洛谷 (luogu.com.cn)
1.26 n
求区间数量,肯定先枚举右端点,就设 是当区间右端点为
的区间数量。
考虑到只要 和
满足要求,
也一定满足要求。
所以 转移:
cpp
for (int r = 1; r <= n; r ++) {
int l = 满足 [l + 1, r] 回文最大的 l
f[r] = f[l] + 1;
}
可以肯定其他满足段合法但更小的 没有最大
的贡献多,并且这样不重不漏。
我们求最大的 ,由于合法段就是回文段,考虑使用类似 kmp 的失配指针的方式。
因为当我们解决新的下标匹配时,原有的已经成段的匹配就没必要一个个查了,可以直接跳过。
设 为离
最近满足
回文的下标。
对于
,我们假设
已经求好。
如果
,那就最好,直接让
。
但如果出现
这种情况,
。
这时我们需要再往前跳一个合法段,寻找可能满足
相等的点。
因为总共有 个字母,而任意
。
并且 ,不然它俩就是能并到一起最近的。
所以时间复杂度为 ,即最坏遍历所有的字母才找到相等的。
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e6 + 10;
stack<int> sta;
LL f[N], las[N];
char s[N];
int main () {
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
cin >> (s + 1);
LL ans = 0;
memset(f, 0, sizeof(f));
memset(las, 0, sizeof(las));
for (int i = 1; i <= n; i ++) {
for (int j = i - 1; j >= 1; j = las[j] - 1) {
if (s[i] == s[j]) {
las[i] = j;
break;
}
}
if (las[i] != 0) f[i] = f[las[i] - 1] + 1;
}
for (int i = 1; i <= n; i ++) {
ans += f[i];
}
cout << ans << "\n";
return 0;
}
2.map
对于匹配栈,如果 和
进行完消除操作的栈状态相同,那么
就是可消除的段。
对于一个栈状态出现次数为 ,那么从中任选两个状态组合都可以是消除段。
代码我不写了,懒,这是别人的。
cpp
//B
//哈希做法
//The Way to The Terminal Station......
#include<cstdio>
#include<iostream>
#include<map>
#include<stack>
#include<set>
#define int unsigned long long // 使用 unsigned long long 避免自然溢出,但需注意后续 main 返回类型
const int P = 29; // 哈希基数(大于字母种类即可,通常取 29 或 31)
using namespace std;
const int maxn = 2e6 + 10;
int has[maxn]; // has[i]:前缀 1..i 经过消除后剩余栈的哈希值(即当前状态的哈希)
int ans; // 最终答案(long long 类型,因 int 被替换,实际为 unsigned long long)
map<int, int> mp; // 统计每个哈希值出现的次数
stack<int> q, pos; // q 存栈中字符(整数),pos 存对应字符在原串中的下标
set<int> S; // 记录所有出现过的不同哈希值(用于最后遍历)
inline int read() // 快读(返回 unsigned long long)
{
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 - '0'; ch = getchar(); }
return x * f;
}
signed main() // signed 强制 main 返回 signed int,防止被宏替换
{
int n = read();
string s;
cin >> s;
s = '%' + s; // 使下标从 1 开始
S.insert(0); // 空状态的哈希值为 0,也要加入集合
mp[0]++; // 前缀长度 0 出现一次(空栈)
for(int i = 1; i <= n; i++)
{
// 若栈非空且栈顶字符与当前字符相同,则它们可以配对消除
if(!q.empty() && q.top() == s[i] - 'a')
{
// 消除后,当前状态退回到栈顶前一个字符所在位置的状态
// pos.top() 是栈顶字符在原串中的下标,pos.top()-1 即该字符之前的那个位置
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]]++; // 计数加一
}
}
// 对于每个不同的哈希值,设有 cnt 个位置出现,则任选两个位置 i<j,
// 前缀 i 和前缀 j 的状态相同,说明 (i+1..j) 是可消除子串。
// 因此贡献为 C(cnt, 2) = cnt * (cnt - 1) / 2。
for(int v : S)
ans += mp[v] * (mp[v] - 1) / 2;
cout << ans << '\n';
return 0;
}
3.线性
我们发现栈状态其实可以简化为栈顶元素的下标。
对于同样的消除操作后栈状态,我们只想找到加入字符同样为 的。
也就有了另一个类似自动机的方法:
cpp
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e6 + 5; // 最大字符串长度
int n; // 字符串长度
int dp[N]; // dp[i]: 以 i 结尾的可消除连续子串个数
int a[N][26]; // a[state][c]: 在状态 state 下,字符 c 最后一次出现的位置
int to[N]; // to[i]: 前缀 1..i 消除后栈顶元素在原串中的下标(0 表示栈空)
char s[N]; // 原字符串,下标从 1 开始
ll ans; // 答案,注意 long long
int main() {
scanf("%d%s", &n, s + 1); // 读入 n 和字符串(从 s[1] 开始)
for (int i = 1; i <= n; i++) {
to[i] = i; // 先假设当前字符压入栈,新状态就是 i
int c = s[i] - 'a'; // 当前字符编号
int state = to[i - 1]; // 前一个前缀消除后的状态(栈顶位置)
int x = a[state][c]; // 在 state 状态下,字符 c 最近一次出现的位置
if (x) { // 若存在这样的位置 x
// 说明 x 与 i 可以配对消除,中间部分 (x+1..i-1) 已可消除
// 消除后新状态退回到 x-1 前缀消除后的状态
to[i] = to[x - 1];
// 以 i 结尾的可消除子串数 = 以 x-1 结尾的可消除子串数 + 1
dp[i] = dp[x - 1] + 1;
} else {
// 无法配对,当前字符只能压入栈,状态为 i,dp[i] 保持 0
// to[i] 已经初始化为 i,dp[i] 默认为 0
}
// 更新自动机:在当前新状态 to[i] 下,字符 c 最近出现的位置更新为 i
a[to[i]][c] = i;
ans += dp[i]; // 累加所有以 i 结尾的可消除子串数
}
printf("%lld\n", ans);
return 0;
}