Chain Contestant 题解

前言

题目链接:洛谷AtCoder

最慢的点才跑 2 ms 的题解确定不看一看?

题意简述

给定长度为 n 的字符串 s,其中 siΩ,求有多少子序列 T 满足任意 xΩ,其在 T 出现的位置为连续一段,当然,对 998244353 取模。

n105|Ω|14,时间 1 秒,空间 5 MB。

(嗯……其实原题 n103|Ω|10,时间 2 秒,空间 1 GB。)

题目分析

一眼状压 DP。因为我们需要知道之前选过的字符集合,以及当前上一位选出来的字符是什么。所以我们考虑记 fi,S 表示考虑前 i 位,必选 i,选出的字符集合为 S 的方案数。注意区分集合 S 和字符串 s。有如下两种转移:

  1. 当前字符是子序列第一位:
    直接 fi,{si}=1

  2. 当前字符不是子序列第一位:
    那么肯定是从 j[1,i1] 拼接来的。枚举 j 处选出的字符集合 S,根据上一次选出的最后一位 sj,结合题目要求有如下转移:

    fi,S{si}fi,S{si}+{fj,S if si=sjfj,S if sisjsiS0 otherwise 

    解释一下吧,第一种情况是直接接在 j 之后,第二种情况是新开一段。

答案就是 i=1nSΩfi,S

这样,时间复杂度是 Θ(n22|Ω|)菜爆了,无论时空都不优秀。优化思路是先把 Θ(n) 地枚举 j 优化掉。自然想到可以滚一滚,删除 i 这一维,但是这样我们不好判断上一次选出来的字符是什么,所以再加上一维 k 表示上一次选出来的是哪一种字符。

类似 01 背包,我们每次需要让 SΩ,即代码实现上的从大到小枚举。类似得到转移方程。对于每一个包含 siS,计算 fS,si

  1. fS,sifS,si+fS,si
    表示选出 si,有 fS,si 种方案,又由于 f 被滚了,是类似前缀和的操作,所以累加上去。
  2. fS,sifS,si+fS{si},k
    表示新开一段,其中 kS{si}

注意到,我们还有把当前字符选出来成为第一位这一操作,由于滚动数组,我们不能再上述操作前修改,而是再上述操作后使 f{si},sif{si},si+1

统计答案显然是 SΩiSfS,i

时间复杂度优化到 Θ(n|Ω|2|Ω|),还是不够。

发现瓶颈在枚举 k 的过程中。我们发现对于 kS{si} 的情况,由于 fS{si},k=0,并不会对答案产生影响。所以等价于求 kΩfS{si},k。所以,我们同时维护 gS 表示 kΩfS,k 就能快速转移了。

答案便是 SΩgS

时间复杂度 Θ(n2|Ω|),就差一点点了,似乎差一个 12 的常数,上哪找去呢?

发现,枚举对于每一个包含 siSΘ(n) 的,但是我们完全可以只枚举 TΩ{si},而 S=T{si},这样就有了一个 12 的常数,能够通过。

当然,聪明的读者肯定会发现,空间也可以有一个 12 的常数,因为只有对于 kSfS,k 才是有效的,程序二进制上体现为这一位一定为 1,我们可以扣掉 Sk 位的 1,然后把之后的比特位向前挪动一格,这样就能节约空间。

代码

快到飞起的提交记录。为了观感,以下是没有卡空间的代码。

#include <cstdio>
using namespace std;
using mint = int;
using sint = long long;
constexpr const mint mod = 998244353;
constexpr inline mint add(const mint a, const mint b) {
return a + b >= mod ? a + b - mod : a + b;
}
template <typename... Types>
inline mint& toadd(mint &a, Types... b) {
return a = add(a, b...);
}
const int N = 1010, M = 10;
int n, val[N];
char str[N];
mint dp[1 << M][M], g[1 << M];
signed main() {
scanf("%d%s", &n, str + 1);
for (int i = 1; i <= n; ++i) val[i] = str[i] - 'A';
dp[1 << val[1]][val[1]] = 1;
g[1 << val[1]] = 1;
for (int i = 2; i <= n; ++i) {
int ST = ((1 << M) - 1) ^ 1 << val[i];
for (int S = ST; ; S = (S - 1) & ST) {
int st = S | 1 << val[i];
toadd(g[st], dp[st][val[i]]);
toadd(g[st], g[st ^ 1 << val[i]]);
toadd(dp[st][val[i]], dp[st][val[i]]);
toadd(dp[st][val[i]], g[st ^ 1 << val[i]]);
if (!S) break;
}
toadd(dp[1 << val[i]][val[i]], 1);
toadd(g[1 << val[i]], 1);
}
mint ans = 0;
for (int st = 0; st < 1 << M; ++st)
toadd(ans, g[st]);
printf("%d", ans);
return 0;
}

后记

一道大水题,但是完全可以优化,别的题解都太劣了。以及本来想申请撤下一篇题解的,但是发现他已经被禁用专栏了,乐。

希望我的题解能给大家带来启示和帮助。

posted @   XuYueming  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示