「CSP-S 2021 括号序列」题解
考场挂分器,送分题变成送命题。
一、状态:
令 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示 [ i , j ] [i, j] [i,j] 这个区间内组成超级括号序列的方案数
令 f [ i ] [ j ] f[i][j] f[i][j] 表示在 d p [ i ] [ j ] dp[i][j] dp[i][j] 的要求上,添加限制:方案中,括号 i i i, j j j 匹配。
令 a n s [ i ] [ j ] ans[i][j] ans[i][j] 表示 [ i , j ] [i, j] [i,j] 这个区间能不能做到 [ l , r ] [l, r] [l,r] 全为 “*”。
令 c h e c k ( l , r ) = [ [ [ s [ l ] = ? ] ∨ [ s [ l ] = ∗ ] ] ∧ [ [ s [ r ] = ? ] ∨ [ s [ r ] = ∗ ] ] ] check (l, r) = \bigg[ \Big[ [s[l] = ?] \lor [s[l] = *] \Big] \wedge \Big[ [s[r] = ?] \lor [s[r] = *] \Big] \bigg] check(l,r)=[[[s[l]=?]∨[s[l]=∗]]∧[[s[r]=?]∨[s[r]=∗]]]
attention: 这里区间的含义和数学上是一样的,不会是空集。
二、初始化:
a n s ans ans 即判断这个区间内是不是全是 “?” 或 “*”。 d p , f dp, f dp,f 全部设为 0 0 0。
三、状态转移:
考虑所有情况,每种情况对应一种转移。
- ()
这种情况特判即可。
d p [ l ] [ r ] = f [ l ] [ r ] = c h e c k ( l , r ) ∗ [ l + 1 = = r ] dp[l][r] = f[l][r] = check (l, r) * [l + 1 == r] dp[l][r]=f[l][r]=check(l,r)∗[l+1==r]
- (S)
这是 s [ l ] , s [ r ] s[l], s[r] s[l],s[r] 已经匹配好了,则 [ l + 1 , r − 1 ] [l + 1, r - 1] [l+1,r−1] 全为 * 就行了。
d p [ l ] [ r ] = f [ l ] [ r ] = a n s [ l + 1 ] [ r − 1 ] dp[l][r] = f[l][r] = ans[l + 1][r - 1] dp[l][r]=f[l][r]=ans[l+1][r−1]
- (SA)
我们枚举 S 的最后一个字符的位置。
即统计 [ l + 1 , i ] [l + 1, i] [l+1,i] 全为 *, [ i + 1 , r − 1 ] [i + 1, r - 1] [i+1,r−1] 为超级括号序列的方案数。
d p [ l ] [ r ] = f [ l ] [ r ] = ∑ i = l + 1 i + 1 ≤ r − 1 a n s [ l + 1 ] [ i ] ∗ d p [ i + 1 ] [ r − 1 ] dp[l][r] = f[l][r] = \sum_{i = l + 1}^{i + 1 \leq r - 1} ans[l + 1][i] * dp[i + 1][r - 1] dp[l][r]=f[l][r]=i=l+1∑i+1≤r−1ans[l+1][i]∗dp[i+1][r−1]
- (AS)
同理 3。
d p [ l ] [ r ] = f [ l ] [ r ] = ∑ i = l + 1 i + 1 ≤ r − 1 d p [ l + 1 ] [ i ] ∗ a n s [ i + 1 ] [ r − 1 ] dp[l][r] = f[l][r] = \sum_{i = l + 1}^{i + 1 \leq r - 1} dp[l + 1][i] * ans[i + 1][r - 1] dp[l][r]=f[l][r]=i=l+1∑i+1≤r−1dp[l+1][i]∗ans[i+1][r−1]
- AB
我们枚举 A 的最后一个字符的位置。
同理 3,我们可以写出这样的转移
d p [ l ] [ r ] = ∑ i = l i ≤ r − 1 d p [ l ] [ i ] ∗ d p [ i + 1 ] [ r ] dp[l][r] = \sum_{i = l}^{i \leq r - 1} dp[l][i] * dp[i + 1][r] dp[l][r]=i=l∑i≤r−1dp[l][i]∗dp[i+1][r]
但是发现它会算重:
e . g . e.g. e.g.
6 10
()()()
在这种情况下,因为有 [1, 2] 和 [3, 6], [1,4] 和 [5, 6] 的两种分裂方法,所以这个序列被重复计算了。为了防止一种满足要求的答案因可以被多个地方裂开而算重,我们可以钦定 A 是一个第一个和最后一个元素匹配的超级括号序列。这样的话,从不同的点分裂,那对应的答案序列一定不一样。(比如两个序列分别从 i, j 分裂,序列 1 s[l] 对应的是 s[i], 而序列 2 s[l] 对应的是 s[j],所以说这两个序列一定不一样)。
所以方程应该写为
d p [ l ] [ r ] = ∑ i = l i ≤ r − 1 f [ l ] [ i ] ∗ d p [ i + 1 ] [ r ] dp[l][r] = \sum_{i = l}^{i \leq r - 1} f[l][i] * dp[i + 1][r] dp[l][r]=i=l∑i≤r−1f[l][i]∗dp[i+1][r]
- ASB
同理 5,枚举左端点和右端点匹配的超级括号 A 的最后一位,以及 S 的最后一位。
即统计 A : [ l , i ] , S : [ i + 1 , j − 1 ] , B [ j , r ] A:[l, i], S:[i + 1, j - 1], B[j, r] A:[l,i],S:[i+1,j−1],B[j,r] 的方案数。
d p [ l ] [ r ] = ∑ i = l i ≤ r ∑ j = i + 2 j ≤ r − 1 f [ l ] [ i ] ∗ a n s [ i + 1 ] [ j − 1 ] ∗ d p [ j ] [ r ] dp[l][r] = \sum_{i = l}^{i\leq r} \sum_{j = i + 2}^{j \leq r - 1}f[l][i] * ans[i + 1][j - 1] * dp[j][r] dp[l][r]=i=l∑i≤rj=i+2∑j≤r−1f[l][i]∗ans[i+1][j−1]∗dp[j][r]
发现是一个 O ( n 2 ) O (n ^ 2) O(n2) 的转移,总时间复杂度会变为 O ( n 4 ) O(n^4) O(n4),考虑优化。
a n s [ i + 1 ] [ j − 1 ] ans[i + 1][j - 1] ans[i+1][j−1] 是一个关于 j j j 的单调不递增函数,而只有当 a n s [ i + 1 ] [ j − 1 ] = 1 ans[i + 1][j - 1] = 1 ans[i+1][j−1]=1 时才可能有贡献,所以不妨考虑找到这个临界点,记作 p,则 ∀ k ∈ [ i + 2 , p ] , a n s [ i + 1 ] [ k − 1 ] = 1 \forall k \in [i + 2,p], ans[i + 1][k - 1] = 1 ∀k∈[i+2,p],ans[i+1][k−1]=1。
将转移式中的公因数提出来。
d p [ l ] [ r ] = ∑ i = l i ≤ r ( f [ l ] [ i ] ∗ ∑ j = i + 2 j ≤ p d p [ j ] [ r ] ) dp[l][r] = \sum_{i = l}^{i\leq r} (f[l][i] * \sum_{j = i + 2}^{j \leq p}dp[j][r]) dp[l][r]=i=l∑i≤r(f[l][i]∗j=i+2∑j≤pdp[j][r])
发现后面的求和里只与 j j j 有关,所以可以用一维后缀和进行优化。
令 f a l l [ i ] = ∑ j = i j ≤ r d p [ j ] [ r ] fall[i] = \sum_{j=i}^{j \leq r} dp[j][r] fall[i]=∑j=ij≤rdp[j][r]
则式子可以化为
d p [ l ] [ r ] = ∑ i = l i ≤ r ( f [ l ] [ i ] ∗ ( f a l l [ i + 2 ] − f a l l [ p + 1 ] ) ) dp[l][r] = \sum_{i = l}^{i\leq r} (f[l][i] * (fall[i + 2] - fall[p + 1])) dp[l][r]=i=l∑i≤r(f[l][i]∗(fall[i+2]−fall[p+1]))
小 Trick: 发现只考虑操作 1 ~ 3 时, d p [ l ] [ r ] = f [ l ] [ r ] dp[l][r] = f[l][r] dp[l][r]=f[l][r], 所以我们可以在计算了操作 1 ~ 3 对 d p [ l ] [ r ] dp[l][r] dp[l][r] 的贡献之后,将它直接赋给 f [ l ] [ r ] f[l][r] f[l][r]。
参考代码:
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define LL long long
#define ULL unsigned long long
#define PII pair <int, int>
#define MP(x,y) make_pair (x, y)
#define rep(i,j,k) for (int i = (j); i <= (k); i++)
#define per(i,j,k) for (int i = (j); i >= (k); i--)
#define fi first
#define se second
template <typename T> T Max (T x, T y) { return x > y ? x : y; }
template <typename T>
void read (T &x) {
x = 0; T f = 1;
char ch = getchar ();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar ();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + ch - '0';
ch = getchar ();
}
x *= f;
}
template <typename T>
void write (T x) {
if (x < 0) {
x = -x;
putchar ('-');
}
if (x < 10) {
putchar (x + '0');
return;
}
write (x / 10);
putchar (x % 10 + '0');
}
template <typename T>
void print (T x, char ch) {
write (x); putchar (ch);
}
const int Maxn = 500;
const LL Mod = 1e9 + 7;
int n, k;
char s[Maxn + 5];
LL fall[Maxn + 5];
LL dp[Maxn + 5][Maxn + 5], f[Maxn + 5][Maxn + 5];
LL ans[Maxn + 5][Maxn + 5];
bool check (int l, int r) {
if (s[l] == '(' && s[r] == ')') return 1;
if (s[l] == '(' && s[r] == '?') return 1;
if (s[l] == '?' && s[r] == ')') return 1;
if (s[l] == '?' && s[r] == '?') return 1;
return 0;
}
void add (LL &x, LL y) {
x = (x + y % Mod) % Mod;
}
int main () {
freopen ("C:\\Users\\Administrator\\Desktop\\vscode\\1.in", "r", stdin);
freopen ("C:\\Users\\Administrator\\Desktop\\vscode\\1.out", "w", stdout);
read (n); read (k);
scanf ("%s", s + 1);
for (int l = 1; l <= n; l++)
for (int r = l; r <= n; r++) {
if (s[r] != '*' && s[r] != '?') break;
if (r - l + 1 > k) break;
ans[l][r] = 1;
}
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
if (check (l, r)) {
if (l + 1 == r) add (dp[l][r], 1); // 1
else add (dp[l][r], dp[l + 1][r - 1] + ans[l + 1][r - 1]); // 2
for (int i = l + 1; i + 1 <= r - 1; i++) {
add (dp[l][r], ans[l + 1][i] * dp[i + 1][r - 1]); // 3
add (dp[l][r], dp[l + 1][i] * ans[i + 1][r - 1]);//4
}
}
f[l][r] = dp[l][r];
for (int i = l; i + 1 <= r; i++)
add (dp[l][r], f[l][i] * dp[i + 1][r]);//5
memset (fall, 0, sizeof fall);
for (int i = r; i >= l; i--) {
add (fall[i], fall[i + 1] + dp[i][r]);
}
int p = 0;
for (int i = l; i <= r; i++) {
// /* O (n ^ 3)
p = Max (p, i + 1);
while (ans[i + 1][(p + 1) - 1]) {
p++;
}
add (dp[l][r], f[l][i] * (fall[i + 2] - fall[p + 1]));//6
// */
/* O (n ^ 4)
for (int j = i + 2; j <= r - 1; j++) {
//[l, i][i + 1, j - 1][j, r]
add (dp[l][r], f[l][i] * ans[i + 1][j - 1] * dp[j][r]);
}
*/
}
}
}
write (dp[1][n]);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现