【题解】P9753 [CSP-S 2023] 消消乐

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;
}
相关推荐
竞赛考级题库1 小时前
202606 青少年等级考试C/C++真题一级 建议答题时长:60min
java·c语言·c++
shehuiyuelaiyuehao1 小时前
算法40,模拟运算,替换所有的问号
数据结构·算法·leetcode
linx2951 小时前
阶段五讲义:STL 全貌——扔掉手写循环,学“动词“
c++
用户204937554951 小时前
从“能识别”到“稳定识别”:离线ASR在真实会议场景中的问题与工程优化实践
算法
handler012 小时前
【Linux】动态库、静态库使用与原理
linux·运维·服务器·c++·c·动态库·静态库
YSL0701242 小时前
OpenCode保姆级安装教程
数据结构
raindayinrain2 小时前
c++并发--同步
c++·同步
鹿角片ljp2 小时前
LeetCode 148:排序链表|归并排序、快慢指针找左中点与链表断开
算法
LabVIEW开发2 小时前
LabVIEW字符串特殊字符检测兼容
算法·labview·labview知识·labview功能·labview程序