[题解] csp-s2021 T2 括号序列
本文于 2023.03.26 大修(几乎重写)
本文首先发布于个人博客,博客园不定期更新。推荐去我的博客阅读本文,体验更佳。
因为 csp-s 考的太惨了,所以一直想着回头把前三题都AC了,结果一拖拖到现在才做完(
还有就是动态规划好难(wtcl)
题意
本题题意较复杂,建议直接去看原题(题目链接)。
总的来说,合法的括号序列一共分两类:
-
包含型:,,,,
-
并列型: ,
显然合法的括号序列两端必然分别是左右括号,而且合法的括号序列最小长度为2。
初步分析
数据范围可知这题大概需要一个 的方法
这道题一看就像是区间DP。设状态 表示 这段区间内的数量,由于上文已经证明看出“合法的括号序列两端必然分别是左右括号”,所以需要先确保 对应位置可以为 (
,即 a[i] == '(' || a[i] == '?'
,对 也同理,不满足的话 一律为 0。后文的讨论均基于 满足要求的情况。
按照刚刚上面的分类进行讨论,记 表示 能否完全由不超过 k 个连续的 *
或者 ?
组成,或者说 表示 能否构成题目中的 S。则可列出方程:
(放在逗号右面表示只有当满足这个条件时才计算)
只要枚举一下 S 就行, 为 A 的右端点(包含):
与 差不多,留作习题答案略。那么包含型的情况已经讨论完了。
下面是并列型的情况。在计算 的贡献的时候,如果像我们刚刚那样设计状态就会列出错误的方程:
这个式子即枚举 的右端点 和 的左端点 (均包含)来间接枚举 ,从而转移 ,这里 S 能取空所以可以连 AB 的情况一起处理。
但是这样的转移方程为什么是错的呢,我们看下面这样的括号序列:
()()??()
有以下几种情况:
cnt | A | S | B | ASB |
---|---|---|---|---|
1 | () | null | ()()() | ()()()() |
2 | () | null | ()**() | ()()**() |
3 | ()() | null | ()() | ()()()() |
4 | ()() | null | **() | ()()**() |
5 | ()() | ** | () | ()()**() |
6 | ()()() | null | () | ()()()() |
显然算重了(由于在计算子区间 和 的并列型情况时还会有重复,所以按这种错误方法算出的情况数不止上述 6 种)。
去重
如果每次在左边的 只枚举包含型,不枚举并列型的结构,而 包含型与并列型都枚举,这样每种拆法就只会数一次了。后文将这种拆法表示为 ( 表示可有可无)。由于 没法再拆成其他的并列型,所以显然不会重复,下面简要证明不会遗漏:
一种并列型的 总能表示为 ,其中 为包含型, 任意,情况 ,符合 ,故能够被计数。
重新设计一下状态,用 表示该区间内包含型序列的数量, 表示并列型序列的数量。
重新看一下原来的转移方程,你会发现 , 的转移方程不需要改, 的转移方程只是把后面的 改成 即可。
仿照原来的思路, 的转移方程如下:
代码如下(为了清晰直观,删去取模的过程):
for(int p=i+1; p <= j-2; p++){
for(int q=p+1; q <= j-1; q++){
if(s[p+1][q-1] || q == p+1) g[i][j] += (ULL)f[i][p] * (f[q][j] + g[q][j]);
else break;
}
}
按这个思路写对了应该就能拿到 65 pts 了,现在离 AC 还差一个优化。
优化
复杂度瓶颈在 的 上,所以观察 的式子,此时先把 和 当做定值。
后面的 的值与 无关,因此我们可以把 从最内层的求和里提出来。
只关注内层的循环,这时如何优化就明显一些了——前缀和。
我们设一个新数组 :
若忽略 的条件,最内层循环等于 。
再考虑 ,由于最内层循环在递增的是 而 并不变化——作为条件的区间 是左端点不动向右延伸的——我们可以预处理出每一个位置往后延伸的 *
或者 ?
的最长的长度,记为 ,则从 点最远能延伸到的下标为 。所以我们就可以在每次循环 的时候预处理 数组的值,用 代替原来最内层的求和了。
因为 递增,所以不用担心 、 还没有求(这两个区间肯定比正在求的小)。
(其实建议预处理时直接处理出向后延伸到的最大下标,应该会方便一点,但我懒得改了)
最终代码(代码丑,请见谅):
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 550;
const int MOD = 1000000007;
typedef unsigned long long ULL;
int inline add(int a, int b){
return (a%MOD + b%MOD) % MOD;
}
int n, k;
char a[maxn];
int f[maxn][maxn]; // 包含型,后用 F 代替
int g[maxn][maxn]; // 并列型,后用 G 代替
bool s[maxn][maxn]; // a[i:j] 能否构成连续个数小于 k 的 *
int h[maxn], b[maxn];
int main(){
scanf("%d%d", &n, &k);
scanf("%s", a+1);
for(int i=1; i <= n; i++){
int j;
for(j=i; j <= n && j-i+1 <= k; j++){
if(a[j] == '*' || a[j] == '?') s[i][j] = true;
else break;
}
b[i] = j-i;
}
for(int l=2; l <= n; l++){
for(int i=1; i+l-1 <= n; i++){
int j = i+l-1;
// 合法的括号序列两端必然分别是左右括号
if(a[i] != '(' && a[i] != '?' || a[j] != ')' && a[j] != '?') continue;
if(i+1 == j){ // 单独一对括号 ()
f[i][j] = 1;
continue;
}
if(s[i+1][j-1]) f[i][j] += 1; // (S)
f[i][j] = add(f[i][j], add(f[i+1][j-1], g[i+1][j-1])); // (F) 或 (G), 对应 (A)
for(int p=i+1; p < j-1; p++){ // (SF) 或 (SG), 对应 (SA)
if(s[i+1][p]) f[i][j] = add(f[i][j], add(f[p+1][j-1], g[p+1][j-1]));
else break;
}
for(int p=j-1; p > i+1; p--){ // (FS) 或 (GS), 对应 (AS)
if(s[p][j-1]) f[i][j] = add(f[i][j], add(f[i+1][p-1], g[i+1][p-1]));
else break;
}
// AB 或者 ASB
h[i+1] = 0; // 前缀和优化
for(int p=i+2; p <= j; p++){
h[p] = add(h[p-1], add(f[p][j], g[p][j]));
}
for(int p=i+1; p <= j-2; p++){
// 一定要注意加一个MOD, 我之前没写查了半天
g[i][j] = add(g[i][j], (ULL)f[i][p] * ((ULL)MOD + h[min(j, p+b[p+1]+1)] - h[p]) % MOD);
}
}
}
printf("%d", add(f[1][n], g[1][n]));
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)