[CSP-S 2021] 括号序列

GitHub跳转原题关系图返回列表

用区间 DP 分别统计单个外层括号块和由多个块拼接成的合法超级括号序列。

OJ: luogu

题目 ID: P7914

难度:提高+/省选-

标签:动态规划区间 DP字符串

创建: 2026-07-06 08:46

更新: 2026-10-07 10:45

形式化题目

给定长度为 nn 的字符串 SS,每个位置可能是 '('、')'、'*' 或 '?'。要求将所有 '?' 替换为 '('、')' 或 '*',使得最终字符串是符合定义的"超级括号序列"。

定义(给定常数 kk):

  • () 和 (S) 是合法序列,其中 SS 是长度 1∼k1 \sim k 的纯 '*' 串
  • 若 AA、BB 均合法,则 ABAB、ASBASB 均合法
  • 若 AA 合法,则 (A)(A)、(SA)(SA)、(AS)(AS) 均合法

求合法替换方案数,对 109+710^9 + 7 取模。

暴力解法

思路

小数据下枚举所有 '?' 的替换(每个 '?' 有 3 种选择),再递归判断最终字符串是否合法。

判断合法性时记忆化:valid[l][r]valid[l][r] 表示区间 [l,r][l,r] 是否为合法序列,atom[l][r]atom[l][r] 表示是否为单个外层括号块。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-06 08:46
 * update_at: 2026-10-01 20:57
 */
// brute.cpp:小数据暴力解,枚举所有 ? 的替换,再用递归判断定义是否合法。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const ll MOD = 1000000007LL;

int n, K;
string pattern_s, cur;
vector<int> question_pos; // 所有 '?' 的位置

// memo_valid[l][r]:区间 [l,r] 是否为合法超级括号序列,-1 未计算,0/1 表示结果
// memo_atom[l][r]:区间 [l,r] 是否为单个外层括号块,-1 未计算,0/1 表示结果
int memo_valid[20][20];
int memo_atom[20][20];
ll answer;

// 区间 [l,r] 是否能全部填成星号(长度不超过 K 且每个位置都是 '*')
bool is_star_string(int l, int r) {
    if (l > r) return false;
    if (r - l + 1 > K) return false;
    for (int i = l; i <= r; i++) {
        if (cur[i] != '*') return false;
    }
    return true;
}

bool is_valid(int l, int r);

// 区间 [l,r] 是否为单个外层括号块
bool is_atom(int l, int r) {
    if (l >= r) return false;
    if (memo_atom[l][r] != -1) return memo_atom[l][r];
    bool ok = false;
    if (cur[l] == '(' && cur[r] == ')') {
        if (l + 1 == r) ok = true;                                 // ()
        if (is_star_string(l + 1, r - 1)) ok = true;              // (S)
        if (l + 1 <= r - 1 && is_valid(l + 1, r - 1)) ok = true;  // (A)
        for (int p = l + 1; p <= r - 2; p++) {
            if (is_star_string(l + 1, p) && is_valid(p + 1, r - 1)) ok = true; // (SA)
            if (is_valid(l + 1, p) && is_star_string(p + 1, r - 1)) ok = true; // (AS)
        }
    }
    memo_atom[l][r] = ok;
    return ok;
}

// 区间 [l,r] 是否为合法超级括号序列
bool is_valid(int l, int r) {
    if (l > r) return false;
    if (memo_valid[l][r] != -1) return memo_valid[l][r];
    bool ok = is_atom(l, r);
    for (int start = l + 1; start <= r && !ok; start++) {
        if (!is_atom(start, r)) continue;
        if (is_valid(l, start - 1)) ok = true;                    // AB
        for (int star_len = 1; star_len <= K && start - star_len - 1 >= l; star_len++) {
            int star_l = start - star_len;
            int left_r = star_l - 1;
            if (is_star_string(star_l, start - 1) && is_valid(l, left_r)) {
                ok = true;                                         // ASB
            }
        }
    }
    memo_valid[l][r] = ok;
    return ok;
}

// 枚举第 idx 个 '?' 的替换('(' / ')' / '*')
void dfs_replace(int idx) {
    if (idx == (int)question_pos.size()) {
        memset(memo_valid, -1, sizeof(memo_valid));
        memset(memo_atom, -1, sizeof(memo_atom));
        if (is_valid(1, n)) {
            answer++;
            if (answer >= MOD) answer -= MOD;
        }
        return;
    }

    int pos = question_pos[idx];
    cur[pos] = '(';
    dfs_replace(idx + 1);
    cur[pos] = ')';
    dfs_replace(idx + 1);
    cur[pos] = '*';
    dfs_replace(idx + 1);
    cur[pos] = '?';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> K >> pattern_s;
    cur = " " + pattern_s;
    for (int i = 1; i <= n; i++) {
        if (cur[i] == '?') {
            question_pos.push_back(i);
        }
    }

    dfs_replace(0);
    cout << (answer % MOD) << '\n';
    return 0;
}

复杂度

  • 时间:O(3q⋅n3)O(3^{q} \cdot n^3),qq 为 '?' 个数,判断合法性用记忆化递归
  • 空间:O(n2)O(n^2),记忆化数组

瓶颈

'?' 个数稍大就会指数爆炸,只能处理 n⩽15n \leqslant 15 左右的小数据。

正解

思路

为了避免拼接规则产生重复计数,区分两类状态:

  • atom[l][r]atom[l][r]:区间 [l,r][l,r] 是一个单个外层括号块
  • f[l][r]f[l][r]:区间 [l,r][l,r] 是一个完整合法超级括号序列,可能由多个 atomatom 拼接而成

一个 atomatom 必须形如外层一对括号:

text
()
(S)
(A)
(SA)
(AS)

其中 SS 是长度 1∼k1 \sim k 的星号串,AA 是一个合法序列(即 ff 状态)。

而一个完整序列 f[l][r]f[l][r] 可以是:

  • 单个 atom[l][r]atom[l][r]
  • 前面一段完整序列,加上后面的一个 atomatom
  • 前面一段完整序列,中间隔一个长度不超过 kk 的星号串,再加后面的一个 atomatom

这种"按最后一个 atom 来拆"的方式是唯一的,因此不会把 ABCA B C 这样的串用不同括号方式重复统计。

代码中 sep_sum[l][start]sep\_sum[l][start] 表示:从 ll 开始的一段合法序列,后面接空串或一段合法星号串后,使下一个 atomatom 从 startstart 开始的方案数。这样转移 f[l][r]f[l][r] 时只需要枚举最后一个 atomatom 的起点 startstart。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-06 08:46
 * update_at: 2026-10-01 20:57
 */
// main.cpp:区间 DP,atom 表示单个外层括号块,f 表示若干 atom 拼成的合法序列。
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const int MAXN = 505;
const ll MOD = 1000000007LL;

int n, K;
string s;

// bad_star_prefix[i]:前缀 [1,i] 中不能当 '*' 的位置数(即不是 '*' 也不是 '?' 的位置)
int bad_star_prefix[MAXN];

// atom_dp[l][r]:区间 [l,r] 构成单个外层括号块的方案数
int atom_dp[MAXN][MAXN];

// f[l][r]:区间 [l,r] 构成合法超级括号序列的方案数(可由多个 atom 拼接)
int f[MAXN][MAXN];

// sep_sum[l][r]:从 l 开始的一段合法序列,后面接空串或星串,使下一个 atom 从 r 开始的方案数
int sep_sum[MAXN][MAXN];

// 位置 pos 能否放左括号 '('
bool can_left(int pos) {
    return s[pos] == '(' || s[pos] == '?';
}

// 位置 pos 能否放右括号 ')'
bool can_right(int pos) {
    return s[pos] == ')' || s[pos] == '?';
}

// 位置 pos 能否放星号 '*'
bool can_star_char(int pos) {
    return s[pos] == '*' || s[pos] == '?';
}

// 区间 [l,r] 是否能全部填成星号(长度不超过 K 且每个位置都能放 '*')
bool is_star_string(int l, int r) {
    if (l > r) {
        return false;
    }
    if (r - l + 1 > K) {
        return false;
    }
    return bad_star_prefix[r] - bad_star_prefix[l - 1] == 0;
}

// 模意义下加法:x = (x + y) % MOD
void add_mod(int &x, ll y) {
    x = (x + y) % MOD;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> K;
    cin >> s;
    s = " " + s;

    for (int i = 1; i <= n; i++) {
        bad_star_prefix[i] = bad_star_prefix[i - 1] + (can_star_char(i) ? 0 : 1);
    }

    for (int len = 1; len <= n; len++) {
        for (int l = 1; l + len - 1 <= n; l++) {
            int r = l + len - 1;

            // 计算 sep_sum[l][r]:f[l][..] 后接空串或星串,使下一个 atom 从 r 开始
            if (l < r) {
                sep_sum[l][r] = f[l][r - 1]; // 两个合法块直接相接
                int max_len = min(K, r - l - 1);
                for (int star_len = 1; star_len <= max_len; star_len++) {
                    int star_l = r - star_len;
                    int left_end = star_l - 1;
                    if (left_end >= l && is_star_string(star_l, r - 1)) {
                        add_mod(sep_sum[l][r], f[l][left_end]);
                    }
                }
            }

            // 计算 atom_dp[l][r]:区间 [l,r] 是单个外层括号块
            if (len >= 2 && can_left(l) && can_right(r)) {
                if (l + 1 == r) {
                    add_mod(atom_dp[l][r], 1); // ()
                }
                if (is_star_string(l + 1, r - 1)) {
                    add_mod(atom_dp[l][r], 1); // (S)
                }
                if (l + 1 <= r - 1) {
                    add_mod(atom_dp[l][r], f[l + 1][r - 1]); // (A)
                }

                // (SA)
                for (int p = l + 1; p <= r - 2 && p - (l + 1) + 1 <= K; p++) {
                    if (is_star_string(l + 1, p)) {
                        add_mod(atom_dp[l][r], f[p + 1][r - 1]);
                    }
                }

                // (AS)
                for (int p = l + 1; p <= r - 2; p++) {
                    if (is_star_string(p + 1, r - 1)) {
                        add_mod(atom_dp[l][r], f[l + 1][p]);
                    }
                }
            }

            // 计算 f[l][r]:单个 atom 或多个 atom 拼接
            f[l][r] = atom_dp[l][r];
            for (int start = l + 1; start <= r; start++) {
                add_mod(f[l][r], (ll)sep_sum[l][start] * atom_dp[start][r]);
            }
        }
    }

    cout << f[1][n] << '\n';
    return 0;
}

复杂度

  • 时间:O(n3)O(n^3),区间数 O(n2)O(n^2),计算 atomatom 和分隔星号串需要枚举区间内部位置
  • 空间:O(n2)O(n^2)

总结

本题最重要的是防止重复计数。把合法串拆成"若干个单独括号块 atomatom 的序列",并固定按最后一个 atomatom 来转移,就能把语法规则变成稳定的区间 DP。