括号序列杂题选做

括号序列杂题选做

常用技巧:

  • 前缀和:将左括号视为 \(+1\) ,右括号视为 \(-1\) ,则合法括号序列的任意前缀和非负,转化为折线图就是恒在 \(x\) 轴上方,可以比较方便表示 flip、reverse 等操作。
  • 括号树:将合法括号序列映射为树形结构。
    • 正常括号树:按外层配对括号拆开:(...)(...)...(...)
    • 三度化后的括号树:拆第一对或最后一对配对括号,剩下视作整体:(...)......(...)
  • 匹配括号连弧线:则弧线只有包含关系和不交关系,并且每个环中顺时针边都比逆时针边多两条。

CF1685C Bring Balance

给出一个长 \(2n\) 的序列,由 \(n\) 个左括号和 \(n\) 个右括号组成。一次操作可以翻转一个区间,求使其变成合法括号序列的最小操作次数并给出方案。

\(n \leq 10^5\)

先画出折线图,则对 \([l + 1, r]\) 进行一次操作即为将 \([l, r]\) 之间的折线关于 \((\frac{l + r}{2}, \frac{s_l + s_r}{2})\) 中心对称。

有结论:任意序列最多两次就可以使其变为合法括号序列。证明考虑找到最大前缀和 \(s_x\) ,翻转 \([1, x]\)\([x + 1, 2n]\) 即可。

先判掉不用操作的情况,对于操作一次的情况,记 \(l, r\) 为最左和最右前缀和小于 \(0\) 的点,则操作区间必须包含 \([l, r]\) 。不难发现 \(l, r\) 所在位置越高越好,于是只要判断对 \([1, l]\)\([r, 2n]\) 之间的最高点操作是否合法即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7;

int s[N];
char str[N];

int n;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%s", &n, str + 1);
        int l = -1, r = -1;

        for (int i = 1; i <= n * 2; ++i) {
            s[i] = s[i - 1] + (str[i] == '(' ? 1 : -1);

            if (s[i] < 0) {
                if (l == -1)
                    l = i;

                r = i;
            }
        }

        if (l == -1 && r == -1) {
            puts("0");
            continue;
        }

        int mxl = max_element(s, s + l + 1) - s, mxr = max_element(s + r, s + n * 2 + 1) - s,
            mx = max_element(s + l, s + r + 1) - s;

        if (s[mxl] + s[mxr] - s[mx] >= 0)
            printf("1\n%d %d\n", mxl + 1, mxr);
        else {
            mx = max_element(s + 1, s + n * 2 + 1) - s;
            printf("2\n1 %d\n%d %d\n", mx, mx + 1, n * 2);
        }
    }

    return 0;
}

P6689 序列

钦定括号序列 \(S\) 的长度为 \(n\)\(S\) 初始时全为 ( 。初始设定了一个参数 \(k\),并按照如下流程随机,直到 \(k = 0\)

  • \([1, n]\) 的范围内均匀随机一个整数,把 \(S\) 这一位上的括号取反。
  • 若本次操作将一个 ( 变为 ) ,则令 \(k\)\(1\)

求经过上述操作后 \(S\) 中最长合法括号子序列(不要求连续)长度的期望。

\(n, k \leq 5000\)

因为是均匀随机,因此每个位置等价,所有右括号数量相等的方案概率相等。

\(f_{i, j}\) 表示当前参数为 \(i\)\(S\) 中有 \(j\) 个右括号的概率,则:

\[f_{i, j} = \frac{n - j + 1}{n} \sum_{k = j - 1}^n f_{i + 1, k} \times \frac{k!}{(j - 1)!} \times \frac{1}{n^{k - j + 1}} \]

容易前缀和优化在 \(O(nk)\) 的时间复杂度内求出 \(f_{0, i}\) 表示最终有 \(i\) 个右括号的概率,下面考虑计算有 \(i\) 个右括号的序列的期望最长合法括号子序列长度。

对于一个括号序列的最长合法子序列长度,令 (\(1\))\(-1\)\(s_n\) 表示前缀和,则答案为 \(n - s_n + 2 \min s_i\)

证明:取到最小 \(s_i\) 的位置 \(i\) 前面有 \(-s_i\)) 失配,后面有 \(s_n - s_i\)( 失配,用总长减去失配的数量即可。

固定了右括号的数量,于是只要求 \(\min s_i\) 的期望即可。考虑枚举 \(\min s_i\) ,那么问题转化为不越过 \(y = \min s_i\) 的格路计数问题,直接组合数算即可做到 \(O(n^2)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;

int f[N][N], fac[N], inv[N], invfac[N], pw[N], ipw[N], s[N], p[N];

int n, k;

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

inline int invC(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * invfac[n] * fac[m] % Mod * fac[n - m] % Mod;
}

inline int calc(int n, int d, int m) {
    return dec(C(n, n - d - m), C(n, n - d - m + 1));
}

signed main() {
    scanf("%d%d", &n, &k);
    prework(n), pw[0] = ipw[0] = 1;

    for (int i = 1; i <= n; ++i)
        pw[i] = 1ll * pw[i - 1] * n % Mod, ipw[i] = 1ll * ipw[i - 1] * inv[n] % Mod;

    f[k][0] = 1;

    for (int i = k - 1; ~i; --i) {
        for (int j = n; ~j; --j)
            s[j] = add(s[j + 1], 1ll * f[i + 1][j] * fac[j] % Mod * ipw[j] % Mod);

        for (int j = 0; j <= n; ++j)
            f[i][j] = 1ll * (n - j + 1) * inv[n] % Mod * invfac[j - 1] % Mod * pw[j - 1] % Mod * s[j - 1] % Mod;
    }

    for (int i = 1; i <= n; ++i)
        p[i] = 1ll * f[0][i] * invC(n, i) % Mod;

    int ans = 0;

    for (int i = 1; i <= n; ++i)
        for (int j = 2; j <= n && j <= 2 * min(i, n - i); j += 2)
            ans = add(ans, 1ll * j * p[i] % Mod * calc(n, i, (j - 2 * i) / 2) % Mod);

    printf("%d", ans);
    return 0;
}

CF1830C Hyperregular Bracket Strings

给定 \(m\) 个区间 \([l_i, r_i] \subseteq [1, n]\) ,限制为 \([l_i, r_i]\) 为一个合法括号序列,求所有长度为 \(n\) 的合法括号序列的数量。

\(n, m \leq 3 \times 10^5\)

首先考虑只有包含关系的情况。对于 \(l_1 \leq l_2 \leq r_2 \leq r_1\) ,则对于 \([l_2, r_2]\) ,其为一个合法括号序列,那么显然 \([l_1, l_2)\)\((r_2, r_1]\) 拼接起来也为一个合法的括号序列,不难建立树形结构后直接算卡特兰数。

接下来考虑相交的情况。对于 \(l_1 \leq l_2 \leq r_1 \leq r_2\) ,可以发现其等价于 \([l_1, l_2)\)\([l_2, r_1]\)\((r_1, r_2]\) 均为合法括号序列。

由此可以发现,可以对 \(m\) 个限制做线段覆盖,那么覆盖情况相同的位置拿出来需要组成一个合法括号序列,不难用异或哈希判定处理。

#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int Mod = 998244353;
const int N = 3e5 + 7;

ull c[N];
int fac[N], inv[N], invfac[N];

mt19937_64 myrand(time(0));
int n, m;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

inline int Cat(int n) {
    return dec(C(n * 2, n), C(n * 2, n - 1));
}

signed main() {
    prework();
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%d", &n, &m);
        memset(c + 1, 0, sizeof(ull) * n);

        for (int i = 1; i <= m; ++i) {
            int l, r;
            scanf("%d%d", &l, &r);
            ull val = myrand();
            c[l] ^= val, c[r + 1] ^= val;
        }

        map<ull, int> mp;

        for (int i = 1; i <= n; ++i)
            ++mp[c[i] ^= c[i - 1]];

        int ans = 1;

        for (auto it : mp) {
            if (it.second & 1) {
                ans = 0;
                break;
            }
            
            ans = 1ll * ans * Cat(it.second / 2) % Mod;
        }

        printf("%d\n", ans);
    }

    return 0;
}

CF2063F2 Counting Is Not Fun (Hard Version)

有一个长度为 \(2n\) 的括号序列,依次给出 \(n\) 对匹配括号的位置,每次求出满足条件的合法括号序列数量。

\(n \leq 3 \times 10^5\)

由于给出的都是相匹配的括号对,因此给出的区间只有包含关系,考虑建立树形结构,那么给出一个限制就相当于在树上加点。

考虑时光倒流转化为删点,那么只要用并查集维护每个点与父亲的关系,删 \(u\) 就相当于把 \(u\)\(fa_u\) 合并即可。

注意算卡特兰数的时候不能纳入端点(因为需要满足端点严格匹配),时间复杂度 \(O(n \alpha(n))\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 6e5 + 7;

struct DSU {
    int fa[N];
    
    inline void prework(int n) {
        iota(fa + 1, fa + n + 1, 1);
    }
    
    inline int find(int x) {
        while (x != fa[x])
            fa[x] = fa[fa[x]], x = fa[x];
    
        return x;
    }
    
    inline void merge(int x, int y) {
        fa[find(y)] = find(x);
    }
} dsu;

int fac[N], inv[N], invfac[N];
int id[N], fa[N], sta[N], val[N], ans[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

inline int invC(int n, int m) {
    return m > n || m < 0 ? 0 : 1ll * invfac[n] * fac[m] % Mod * fac[n - m] % Mod;
}

inline int Cat(int n) {
    return 1ll * C(n * 2, n) * inv[n + 1] % Mod;
}

inline int invCat(int n) {
    return 1ll * invC(n * 2, n) * (n + 1) % Mod;
}

signed main() {
    prework();
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);

        for (int i = 1; i <= n; ++i) {
            int l, r;
            scanf("%d%d", &l, &r);
            id[l] = id[r] = i;
        }

        for (int i = 1, top = 0; i <= n * 2; ++i) {
            if (sta[top] != id[i])
                sta[++top] = id[i];
            else
                fa[id[i]] = sta[--top];
        }

        memset(val, 0, sizeof(int) * (n + 1));
        ans[n + 1] = 1, dsu.prework(n);

        for (int i = n; i; --i) {
            int u = dsu.find(fa[i]);
            ans[i] = 1ll * ans[i + 1] * invCat(val[i]) % Mod * invCat(val[u]) % Mod * 
                Cat(val[u] + val[i] + 1) % Mod;
            dsu.merge(u, i), val[u] += val[i] + 1;
        }

        for (int i = 1; i <= n + 1; ++i)
            printf("%d ", ans[i]);

        puts("");
    }

    return 0;
}

事实上这个问题可以在线处理,考虑离线转在线后会遇到的几个问题:

  • 找到这个区间的父亲:实际就是左端点 \(< l\) 的区间中满足右端点 \(>r\) 的左端点最大(右端点最小)区间。
  • 计算该点的贡献:注意到一个点没有被 \([l, r]\) 儿子覆盖等价于其覆盖次数等于 \(u\) 的祖先数量。

第一个可以线段树二分,第二个就是查询区间最小值数量。为了方便可以提前插入一个 \([0, 2n + 1]\) ,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 6e5 + 7;

int fac[N], inv[N], invfac[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline int C(int n, int m) {
    return m > n ? 0 : 1ll * fac[n] * invfac[m] % Mod * invfac[n - m] % Mod;
}

inline int invC(int n, int m) {
    return m > n ? 0 : 1ll * invfac[n] * fac[m] % Mod * fac[n - m] % Mod;
}

inline int Cat(int n) {
    return 1ll * C(n * 2, n) * inv[n + 1] % Mod;
}

inline int invCat(int n) {
    return 1ll * invC(n * 2, n) * (n + 1) % Mod;
}

namespace SMT {
int mx[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

void build(int x, int l, int r) {
    mx[x] = 0;

    if (l == r)
        return;

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
}

void update(int x, int nl, int nr, int p, int k) {
    if (nl == nr) {
        mx[x] = k;
        return;
    }

    int mid = (nl + nr) >> 1;

    if (p <= mid)
        update(ls(x), nl, mid, p, k);
    else
        update(rs(x), mid + 1, nr, p, k);

    mx[x] = max(mx[ls(x)], mx[rs(x)]);
}

pair<int, int> query(int x, int nl, int nr, int p, int k) {
    if (nl > p || mx[x] <= k)
        return make_pair(-1, -1);

    if (nl == nr)
        return make_pair(nl, mx[x]);

    int mid = (nl + nr) >> 1;
    auto res = query(rs(x), mid + 1, nr, p, k);
    return res == make_pair(-1, -1) ? query(ls(x), nl, mid, p, k) : res;
}
} // namespace SMT

namespace SGT {
struct Node {
    int mn, cnt;

    inline Node operator + (const Node &rhs) const {
        Node res = (Node) {min(mn, rhs.mn), 0};

        if (mn == res.mn)
            res.cnt += cnt;

        if (rhs.mn == res.mn)
            res.cnt += rhs.cnt;

        return res;
    }
} nd[N << 2];

int tag[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

void build(int x, int l, int r) {
    nd[x] = (Node) {0, r - l + 1}, tag[x] = 0;

    if (l == r)
        return;

    int mid = (l + r) >> 1;
    build(ls(x), l, mid), build(rs(x), mid + 1, r);
}

inline void spread(int x, int k) {
    nd[x].mn += k, tag[x] += k;
}

inline void pushdown(int x) {
    if (tag[x])
        spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}

void update(int x, int nl, int nr, int l, int r, int k) {
    if (l <= nl && nr <= r) {
        spread(x, k);
        return;
    }

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (l <= mid)
        update(ls(x), nl, mid, l, r, k);
    
    if (r > mid)
        update(rs(x), mid + 1, nr, l, r, k);

    nd[x] = nd[ls(x)] + nd[rs(x)];
}

Node query(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r)
        return nd[x];

    pushdown(x);
    int mid = (nl + nr) >> 1;

    if (r <= mid)
        return query(ls(x), nl, mid, l, r);
    else if (l > mid)
        return query(rs(x), mid + 1, nr, l, r);
    else
        return query(ls(x), nl, mid, l, r) + query(rs(x), mid + 1, nr, l, r);
}
} // namespace SGT

signed main() {
    prework();
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);
        SMT::build(1, 0, n * 2 + 1), SMT::update(1, 0, n * 2 + 1, 0, n * 2 + 1);
        SGT::build(1, 0, n * 2 + 1), SGT::update(1, 0, n * 2 + 1, 0, n * 2 + 1, 1);
        int ans = Cat(n);
        printf("%d ", ans);

        for (int i = 1; i <= n; ++i) {
            int l, r;
            scanf("%d%d", &l, &r);
            auto fa = SMT::query(1, 0, n * 2 + 1, l - 1, r);
            ans = 1ll * ans * invCat(SGT::query(1, 0, n * 2 + 1, fa.first, fa.second).cnt / 2 - 1) % Mod;
            ans = 1ll * ans * Cat(SGT::query(1, 0, n * 2 + 1, l, r).cnt / 2 - 1) % Mod;
            SMT::update(1, 0, n * 2 + 1, l, r), SGT::update(1, 0, n * 2 + 1, l, r, 1);
            ans = 1ll * ans * Cat(SGT::query(1, 0, n * 2 + 1, fa.first, fa.second).cnt / 2 - 1) % Mod;

            printf("%d ", ans);
        }

        puts("");
    }

    return 0;
}

HDU6647 Bracket Sequences on Tree

求遍历一棵无根树产生的本质不同括号序列方案数。

\(n \leq 10^5\)

首先可以发现两棵不同构的有根树无法产生相同的括号序列,因此只要求出每个点为根的答案之后,去掉同构的树的答案即可。

先考虑确定根的情况,则每个儿子的遍历顺序决定了括号序列,答案即为:

\[\prod_{i = 1}^n |son(i)|! \]

但是这样会算重,需要除掉每种本质不同子树出现次数阶乘的乘积。

然后不难换根求出每个点为根的答案。

#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int Mod = 998244353;
const int N = 1e5 + 7;

struct Graph {
    vector<int> e[N];

    inline void clear(int n) {
        for (int i = 1; i <= n; ++i)
            e[i].clear();
    }
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

map<ull, int> mp[N];

ull hs[N];
int fac[N], inv[N], invfac[N];
int f[N], g[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

inline void prework() {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i < N; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline ull calc(ull x) {
    x = x * x * x;
    x ^= x >> 33;
    x *= 2398562385683465ull;
    x ^= x >> 17;
    x = x * (x - 1);
    return x;
}

void dfs1(int u, int fa) {
    hs[u] = 2375462552572571ull, f[u] = 1, mp[u].clear();

    for (int v : G.e[u])
        if (v != fa)
            dfs1(v, u), hs[u] += calc(hs[v]), f[u] = 1ll * f[u] * f[v] % Mod, ++mp[u][hs[v]];

    f[u] = 1ll * f[u] * fac[G.e[u].size() - (u != 1)] % Mod;

    for (auto it : mp[u])
        f[u] = 1ll * f[u] * invfac[it.second] % Mod;
}

void dfs2(int u, int fa) {
    g[u] = (fa ? 1ll * f[u] * G.e[u].size() % Mod * inv[++mp[u][hs[fa] - calc(hs[u])]] % Mod * 
        g[fa] % Mod * mi(f[u], Mod - 2) % Mod * inv[G.e[fa].size()] % Mod * mp[fa][hs[u]] % Mod : f[u]);
    hs[u] = hs[u] + (fa ? calc(hs[fa] - calc(hs[u])) : 0);

    for (int v : G.e[u])
        if (v != fa)
            dfs2(v, u);
}

signed main() {
    prework();
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d", &n);
        G.clear(n);

        for (int i = 1; i < n; ++i) {
            int u, v;
            scanf("%d%d", &u, &v);
            G.insert(u, v), G.insert(v, u);
        }

        dfs1(1, 0), dfs2(1, 0);
        set<ull> st;
        int ans = 0;

        for (int i = 1; i <= n; ++i)
            if (st.find(hs[i]) == st.end())
                ans = add(ans, g[i]), st.emplace(hs[i]);

        printf("%d\n", ans);
    }

    return 0;
}

[ARC194D] Reverse Brackets

给出一个长度为 \(n\) 的合法括号序列 \(S\) ,定义一次操作为:选择一个子串 \(S[l, r]\) ,满足其为合法括号序列,对于 \(i \in [l, r]\) ,若替换前 \(S[l + r - i]\)( ,则将 \(S_i\) 替换为 ) ,否则替换为 )

求任意次操作后本质不同括号串数量。

\(n \leq 5000\)

先在外面套一对括号,将括号序列建成树状结构。

不难发现操作的本质就是重排儿子的顺序,答案即为:

\[\prod_{i = 1}^n |son(i)|! \]

但是这样会算重,需要除掉每种本质不同子树出现次数阶乘的乘积。

#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int Mod = 998244353;
const int N = 5e3 + 7;

struct Graph {
    vector<int> e[N];
    
    inline void insert(int u, int v) {
        e[u].emplace_back(v);
    }
} G;

ull hs[N];
int fac[N], inv[N], invfac[N], f[N], sta[N];
char str[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

inline int mi(int a, int b) {
    int res = 1;
    
    for (; b; b >>= 1, a = 1ll * a * a % Mod)
        if (b & 1)
            res = 1ll * res * a % Mod;
    
    return res;
}

inline void prework(int n) {
    fac[0] = fac[1] = 1;
    inv[0] = inv[1] = 1;
    invfac[0] = invfac[1] = 1;
    
    for (int i = 2; i <= n; ++i) {
        fac[i] = 1ll * fac[i - 1] * i % Mod;
        inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
        invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
    }
}

inline ull calc(ull x) {
    x = x * x * x;
    x ^= x >> 33;
    x *= 2398562385683465ull;
    x ^= x >> 17;
    x = x * (x - 1);
    return x;
}

void dfs(int u, int fa) {
    hs[u] = 2375462552572571ull, f[u] = 1;
    map<ull, int> mp;

    for (int v : G.e[u])
        dfs(v, u), hs[u] += calc(hs[v]), f[u] = 1ll * f[u] * f[v] % Mod, ++mp[hs[v]];

    f[u] = 1ll * f[u] * fac[G.e[u].size()] % Mod;

    for (auto it : mp)
        f[u] = 1ll * f[u] * invfac[it.second] % Mod;
}

signed main() {
    scanf("%d%s", &n, str + 2);
    str[1] = '(', str[n + 2] = ')', n += 2;
    int tot = 0;

    for (int i = 1, top = 0; i <= n; ++i) {
        if (str[i] == '(')
            sta[++top] = ++tot;
        else
            G.insert(sta[top - 1], sta[top]), --top;
    }

    prework(tot), dfs(1, 0);
    printf("%d", f[1]);
    return 0;
}

CF1340F Nastya and CBS

给定一个长度为 \(n\) 、由多种括号组成的括号序列,\(q\) 次操作,操作有两种:

  • 单点修改。
  • 判定一个区间是否合法。

\(n, q \leq 10^5\)

考虑开一棵线段树,对每个区间维护形如若干左括号(可以为空)拼上若干右括号(可以为空)的结构,若不存在这种结构则非法。

合并的时候可以用哈希判定合法性,这样只需要维护前面左括号和后面右括号的哈希值即可。但是还需要取出一段前/后缀的哈希值,这是无法直接维护的,用类似楼房重建的方法即可。

时间复杂度 \(O((n + q) \log^2 n)\)

#include <bits/stdc++.h>
using namespace std;
const int base = 233333, Mod = 1e9 + 7;
const int N = 1e5 + 7;

int a[N], pw[N], ipw[N];

int n, m, q;

namespace SMT {
struct Node {
    int x, k;

    inline Node(int _x = 0, int _k = 0) : x(_x), k(_k) {}

    inline bool operator == (const Node &rhs) const {
        return x == rhs.x && k == rhs.k;
    }

    inline bool operator != (const Node &rhs) const {
        return x != rhs.x || k != rhs.k;
    }

    inline friend Node operator + (const Node &a, const Node &b) {
        return Node((a.x + 1ll * b.x * pw[a.k]) % Mod, a.k + b.k);
    }

    inline friend Node operator - (const Node &a, const Node &b) {
        return Node(1ll * (a.x - b.x + Mod) * ipw[b.k] % Mod, a.k - b.k);
    }
} ndl[N << 2], ndr[N << 2], hs[N];

int seq[N], top;
bool legal[N << 2];

inline int ls(int x) {
    return x << 1;
}

inline int rs(int x) {
    return x << 1 | 1;
}

Node queryl(int x, int k) {
    if (!k)
        return Node(0, 0);
    else if (k == ndl[x].k)
        return ndl[x];
    else
        return k <= ndl[ls(x)].k ? queryl(ls(x), k) : 
            ndl[ls(x)] + (queryl(rs(x), k - ndl[ls(x)].k + ndr[ls(x)].k) - ndr[ls(x)]);
}

Node queryr(int x, int k) {
    if (!k)
        return Node(0, 0);
    else if (k == ndr[x].k)
        return ndr[x];
    else
        return k <= ndr[rs(x)].k ? queryr(rs(x), k) : 
            ndr[rs(x)] + (queryr(ls(x), k - ndr[rs(x)].k + ndl[rs(x)].k) - ndl[rs(x)]);
}

inline void pushup(int x) {
    legal[x] = legal[ls(x)] && legal[rs(x)];

    if (!legal[x])
        return;

    ndl[x] = ndl[ls(x)], ndr[x] = ndr[rs(x)];

    if (ndr[ls(x)].k <= ndl[rs(x)].k) {
        if (ndr[ls(x)] == queryl(rs(x), ndr[ls(x)].k))
            ndl[x] = ndl[x] + (ndl[rs(x)] - ndr[ls(x)]);
        else
            legal[x] = false;
    } else {
        if (ndl[rs(x)] == queryr(ls(x), ndl[rs(x)].k))
            ndr[x] = ndr[x] + (ndr[ls(x)] - ndl[rs(x)]);
        else
            legal[x] = false;
    }
}

void update(int x, int nl, int nr, int p, int k) {
    if (nl == nr) {
        legal[x] = true;

        if (k > 0)
            ndl[x] = Node(0, 0), ndr[x] = Node(k, 1);
        else
            ndl[x] = Node(-k, 1), ndr[x] = Node(0, 0);

        return;
    }

    int mid = (nl + nr) >> 1;

    if (p <= mid)
        update(ls(x), nl, mid, p, k);
    else
        update(rs(x), mid + 1, nr, p, k);

    pushup(x);
}

void getseq(int x, int nl, int nr, int l, int r) {
    if (l <= nl && nr <= r) {
        seq[++top] = x;
        return;
    }

    int mid = (nl + nr) >> 1;

    if (l <= mid)
        getseq(ls(x), nl, mid, l, r);

    if (r > mid)
        getseq(rs(x), mid + 1, nr, l, r);
}

Node extract(int x, int k) {
    if (!k)
        return Node(0, 0);
    else if (k == hs[x].k)
        return hs[x];
    else
        return k <= ndr[seq[x]].k ? queryr(seq[x], k) : 
            ndr[seq[x]] + (extract(x - 1, k - ndr[seq[x]].k + ndl[seq[x]].k) - ndl[seq[x]]);
}

inline bool query(int l, int r) {
    top = 0, getseq(1, 1, n, l, r);

    for (int i = 1; i <= top; ++i) {
        int x = seq[i];

        if (!legal[x] || hs[i - 1].k < ndl[x].k || ndl[x] != extract(i - 1, ndl[x].k))
            return false;

        hs[i] = ndr[x] + (hs[i - 1] - ndl[x]);
    }

    return !hs[top].k;
}
} // namespace SMT

signed main() {
    scanf("%d%d", &n, &m);
    pw[0] = ipw[0] = 1, pw[1] = 233333, ipw[1] = 405712011;

    for (int i = 2; i <= n; ++i)
        pw[i] = 1ll * pw[i - 1] * pw[1] % Mod, ipw[i] = 1ll * ipw[i - 1] * ipw[1] % Mod;

    for (int i = 1; i <= n; ++i)
        scanf("%d", a + i), SMT::update(1, 1, n, i, a[i]);

    scanf("%d", &q);

    while (q--) {
        int op, x, y;
        scanf("%d%d%d", &op, &x, &y);

        if (op == 1)
            SMT::update(1, 1, n, x, y);
        else
            puts(SMT::query(x, y) ? "Yes" : "No");
    }

    return 0;
}

P8293 [省选联考 2022] 序列变换

给定一个长为 \(2n\) 的合法括号串和常数 \(x, y\) ,第 \(i\) 个左括号权值为 \(v_i\) 。记 A, B 为合法括号子串,p, q 为任意括号子串,则可以进行两种操作:

  • 将整个串拆分为 p(A)(B)q ,然后交换中间两个括号将其变为 p(A()B)q ,代价为 \(x\) 乘上 (A) 中第一个左括号的权值与上 \(y\) 乘上 (B) 中第一个左括号的权值之和。
  • 将整个串拆分为 pABq ,然后将其变为 pBAq ,代价为 \(0\)

注意交换的时候所有左括号的权值是跟着这个括号一起交换的。

求最小代价,满足最终串不存在形如 (A)(B) 的子串,即形如 ((((...)))) 的括号序列。

\(n \leq 4 \times 10^5\)\(0 \leq x, y \leq 1\)

考虑建出括号树,则操作一等价于将某个点的两个儿子 \(a, b\) 做如下变换:将 \(b\) 的所有儿子挂到 \(a\) 上,然后再将 \(b\) 挂到 \(a\) 上,代价为 \(x v_x + y v_b\) 。操作二相当于交换两个儿子的顺序,即操作一的 \(a, b\) 可以任意选取。

观察树的形态,不难发现总操作次数是一定的。而操作的顺序贪心从浅到深进行所有的操作是最优的,因为操作一会不断将子树挂到底下,这样后面的操作决策更多,严格不劣。

分类讨论 \(x, y\) 的权值:

  • \(x = y = 0\) :无需处理,此时答案为 \(0\)

  • \(x = 0, y = 1\) :代价为当前层的权值和减去当前层留下来的权值,显然保留最大值(不下放)是最优的。

  • \(x = y = 1\) :记 \(c\) 表示当前层的点数,显然保留最大值是最优的,最小代价为权值和加上最小权值 \(\times (c - 2)\) ,构造就是将剩下 \(c - 2\) 个点先接到最小值下面,然后再将最小值接到最大值下面。

  • \(x = 1, y = 0\) :当被留下的节点已经定下来时,策略和上一种情况是一致的,最小代价为最小权值 \(\times (c - 2)\) 加上当前层保留的权值。发现这不好直接贪心,因为可以将一个最大值不断下放使得它对答案不会有贡献。

    由于所有未下放至最后一层的节点均会以某一层被留下的形式贡献答案,所以答案为总权值和减去被下放至最后一层的权值和,再加上每一层的权值最小值 \(\times (c - 2)\)

    这意味着贪心方向为:既要下放最小值,也要下放最大值。考虑对 \(c\) 分类讨论:

    • \(c = 1\) :无法下放,忽略。
    • \(c > 2\) :此时可以同时下放最小值和最大值。
    • \(c = 2\) :注意到过程中若 \(c > 2\) ,则只有到最后两层才会依次变为 \(2, 1\) 。而前面 \(c = 2\) 本身不会产生贡献(\(c - 2 = 0\)),因此这一段仅会下放最小值或最大值。因为下放最小值能让 \(c \geq 3\) 时的最小值尽量小,而下放最大值能让最后一层尽量大。枚举下放的是最大值还是最小值即可。

时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e6 + 7;

vector<int> vec[N];

int sta[N];
char str[N];

int n, a, b;

signed main() {
    scanf("%d%d%d%s", &n, &a, &b, str + 1);

    if (!a && !b)
        return puts("0"), 0;

    for (int i = 1, top = 0; i <= n * 2; ++i) {
        if (str[i] == '(')
            scanf("%d", sta + (++top));
        else
            vec[top].emplace_back(sta[top]), --top;
    }

    if (!a && b) {
        multiset<int> st;
        ll ans = 0, sum = 0;

        for (int i = 1; i < n; ++i) {
            for (int it : vec[i])
                st.emplace(it), sum += it;

            ans += (sum -= *st.rbegin()), st.erase(prev(st.end()));
        }

        printf("%lld", ans);
    } else if (a && b) {
        multiset<int> st;
        ll ans = 0, sum = 0;

        for (int i = 1; i < n; ++i) {
            for (int it : vec[i])
                st.emplace(it), sum += it;

            ans += sum + *st.begin() * (st.size() - 2), sum -= *st.rbegin(), st.erase(prev(st.end()));
        }

        printf("%lld", ans);
    } else {
        vector<int> siz(n + 1);
        siz[0] = 1;

        for (int i = 1; i <= n; ++i)
            siz[i] = siz[i - 1] + vec[i].size() - 1;

        int L = 1;

        while (L <= n && siz[L] == 1)
            ++L;

        int R = L;

        while (R <= n && siz[R] == 2)
            ++R;

        vector<int> mx(n + 1), mn(n + 1, inf);
        ll sum = 0;

        for (int i = L; i < n; ++i) {
            if (i != R)
                mn[i] = mn[i - 1], mx[i] = mx[i - 1];

            for (int it : vec[i])
                mn[i] = min(mn[i], it), mx[i] = max(mx[i], it), sum += it;
        }

        ll ans1 = sum - mx[n - 1], ans2 = sum - max(mx[R - 1], mx[n - 1]);

        for (int i = R; i < n; ++i)
            ans1 += 1ll * (siz[i] - 2) * min(mn[R - 1], mn[i]), ans2 += 1ll * (siz[i] - 2) * mn[i];

        printf("%lld", min(ans1, ans2));
    }

    return 0;
}

CF1896F Bracket Xoring

给定一个长度为 \(2n\) 的 01 串,试用 \(\leq 10\) 次操作将整个串变为全 \(0\) ,或报告无解。

一次操作可以给出一个长度为 \(2n\) 的合法的括号序列 \(S\) ,对于 \(n\) 对匹配的括号,将这段区间异或 \(1\)

\(n \leq 2 \times 10^5\)

首先考虑一些特殊情况:

  • 由于每次都会反转头尾两个位置,因此头尾不同则无解。
  • 由于每次都会反转偶数个位置,因此 \(1\) 的数量为奇数则无解。

考虑一次操作的影响,记 \(a_i\) 表示 \([1, i]\) 中左括号的数量,\(b_i\) 表示 \([1, i]\) 中右括号的数量,则 \(i\) 会被反转 \(a_i - b_{i - 1}\) 次。记 \(f(x) = [2 \mid x]\) ,则操作后 \(x_i\) 会变为 \(x_i \oplus f(a_i - b_{i - 1})\) ,即 \(x_i \oplus f(a_i + b_{i - 1})\) ,即 \(x_i \oplus f(i + [S_i = \text{')'}])\)

下面考虑头尾为 \(0\) 的情况,头尾为 \(1\) 的情况可以用一次操作处理,此时必须操作偶数次。

先考虑操作两次的情况,记两次的操作括号序列分别为 \(a, b\) ,则:

\[x_i \oplus f(i + [a_i = \text{')'}]) \oplus f(i + [b_i = \text{')'}]) = 0 \]

化简得到:

\[x_i = [a_i = \text{')'}] \oplus [b_i = \text{')'}] \]

因此可以考虑如下构造:

  • 对于 \(x_i = 0\) 的位置,出现的前一半令 \(a_i = b_i = \text{'('}\) ,出现的后一半令 \(a_i = b_i = \text{')'}\)
  • 对于 \(x_i = 1\) 的位置,第奇数个出现则令 \(a_i = \text{'('}, b_i = \text{')'}\) ,第偶数个出现则令 \(a_i = \text{')'}, b_i = \text{'('}\)

由于头尾均为 \(0\) ,因此显然匹配合法。

#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7;

char str[N];

int n;

signed main() {
    int T;
    scanf("%d", &T);

    while (T--) {
        scanf("%d%s", &n, str + 1);

        if (str[1] != str[n * 2] || (count(str + 1, str + n * 2 + 1, '1') & 1)) {
            puts("-1");
            continue;
        }

        if (str[1] == '1') {
            puts("3");

            for (int i = 1; i <= n * 2; ++i)
                putchar(i & 1 ? '(' : ')'), str[i] ^= 1;

            puts("");
        } else
            puts("2");

        int lim = count(str + 1, str + n * 2 + 1, '0') / 2;

        for (int i = 1, c0 = 0, c1 = 0; i <= n * 2; ++i)
            putchar(str[i] == '0' ? ((++c0) <= lim ? '(' : ')') : ((++c1) & 1 ? '(' : ')'));

        puts("");

        for (int i = 1, c0 = 0, c1 = 0; i <= n * 2; ++i)
            putchar(str[i] == '0' ? ((++c0) <= lim ? '(' : ')') : ((++c1) & 1 ? ')' : '('));

        puts("");
    }

    return 0;
}

CF1503F Balance the Cards

将正整数视为左括号,负整数视为右括号,绝对值相等的左右括号可以匹配。

\(2n\) 张牌,其中 \(1 \sim n\)\(-n \sim -1\) 恰好在所有卡的正面各出现恰好一次,恰好在所有卡的反面各出现恰好一次。

可以任意调整牌的顺序,但不能翻转牌。构造一组方案满足正反面均排成合法括号序列,或报告无解。

\(n \leq 2 \times 10^5\)

考虑将 \(2n\) 张牌视为 \(2n\) 个点,将正反两面匹配的括号连起来,可以得到一张边数为 \(2n\) 的弧形图。该图由若干个环组成,满足:

  • 每个点都连一条正面边和一条反面边。
  • 一条路径上正面边和反面边交替出现。
  • 正面(或反面)的弧只有包含关系和不交关系。

考虑合法括号序列的弧形图:每个环中顺时针边都比逆时针边多两条。

因此对于相邻的两条异向边(顺逆方向不同),可以将它们合并,则最后若每个连通块都剩下两条顺时针边(或逆时针,只要翻转即可)则合法。

具体实现就是若存在 \((a, b) \to (c, -b) \to (-c, d)\) ,则可以将其合并为 \((a, d)\) ,如果最后每个连通块只剩下两个点 \((a, b)\)\((-a, -b)\) 则合法。

时间复杂度线性。

#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7, V = 2e5;

struct Node {
    int a, b, u, v, w, id;

    inline Node(int _a = 0, int _b = 0, int _u = 0, int _v = 0, int _w = 0) : a(_a), b(_b), u(_u), v(_v), w(_w) {}
} nd[N << 1];

int pa[N], pb[N];
bool vis[N << 1];

int n, tot;

void print(int x) {
    if (!nd[x].u)
        printf("%d %d\n", nd[x].a - V, nd[x].b - V);
    else
        print(nd[x].u), print(nd[x].v), print(nd[x].w);
}

signed main() {
    scanf("%d", &n);
    queue<int> q;

    for (int i = 1; i <= n * 2; ++i) {
        int x, y;
        scanf("%d%d", &x, &y), pa[x += V] = pb[y += V] = i;
        nd[++tot] = Node(x, y), nd[tot].id = tot;

        if ((x < V && V < y) || (y < V && V < x))
            q.emplace(i);
    }

    while (!q.empty()) {
        int x = q.front();
        q.pop();

        if (vis[x])
            continue;

        int u = pa[V * 2 - nd[x].a], v = pb[V * 2 - nd[x].b];

        if (u == v)
            return puts("NO"), 0;

        if (nd[x].a > V)
            nd[++tot] = Node(nd[v].a, nd[u].b, v, x, u), nd[tot].id = tot;
        else
            nd[++tot] = Node(nd[v].a, nd[u].b, u, x, v), nd[tot].id = tot;

        vis[v] = vis[u] = vis[x] = true, pa[nd[tot].a] = pb[nd[tot].b] = tot;

        if ((nd[tot].a < V && V < nd[tot].b) || (nd[tot].b < V && V < nd[tot].a))
            q.emplace(tot);
    }

    vector<Node> ans;

    for (int i = 1; i <= tot; ++i)
        if (!vis[i])
            ans.emplace_back(nd[i]);

    sort(ans.begin(), ans.end(), [](const Node &a, const Node &b) {
        return abs(a.a - V) == abs(b.a - V) ? abs(a.b - V) < abs(b.b - V) : abs(a.a - V) < abs(b.a - V);
    });

    for (int i = 0; i < ans.size(); i += 2) {
        if (ans[i].a != V * 2 - ans[i + 1].a || ans[i].b != V * 2 - ans[i + 1].b ||
            (ans[i].a < V && V < ans[i].b) || (ans[i].b < V && V < ans[i].a))
            return puts("NO"), 0;

        if (ans[i].a < V)
            swap(ans[i], ans[i + 1]);
    }

    puts("YES");

    for (auto it : ans)
        print(it.id);

    return 0;
}

CF1610G AmShZ Wins a Bet

初始有一个括号串 \(S\),定义一次操作为:将 \(S\) 分为三个子串 \(A, B, C\) ,然后令 \(S\)\(A(B)C\) 。给出操作后的括号串,求字符序最小的可能的初始串。

\(|S| \le 3 \times 10^5\)

首先可以钦定删除的括号一定是连续的,否则考虑删除 (?...) 两端的括号,若 ?( ,则删除 ?...) 不劣,否则留下一个 ) 不如不删。因此问题可以转化为保留若干连续段,使得剩下串的字典序最小。

\(f_i\) 表示考虑后 \(i\) 个字符留下来字典序最小的串,可以在 \(f_{i + 1}\) 的基础上添加,也可以找到与当前 ( 匹配的 ) 然后删除一整段。记匹配位置为 \(nxt_i\) ,则 \(f_i = \min(S_i + f_{i + 1}, f_{nxt_i + 1})\)

由于 \(f_i\) 是一个括号串,考虑如何维护。由于需要比较字典序,一个想法是暴力维护哈希,二分出 LCP 之后比较。

考虑维护一个动态增加叶子的 Trie 树,用树上倍增的方法跳 LCP 即可,时间复杂度 \(O(n \log n)\)

#include <bits/stdc++.h>
using namespace std;
const int base = 233, Mod = 1e9 + 7;
const int N = 3e5 + 7, LOGN = 19, S = 2;

int pw[N], f[N], sta[N], nxt[N];
char str[N];

int n;

namespace Trie {
int fa[N][LOGN], hs[N][LOGN], ch[N][S];

int tot;

inline int extend(int las, int c) {
    if (ch[las][c])
        return ch[las][c];

    int x = ++tot;
    ch[las][c] = x, fa[x][0] = las, hs[x][0] = c;

    for (int i = 1; i < LOGN; ++i) {
        fa[x][i] = fa[fa[x][i - 1]][i - 1];
        hs[x][i] = (1ll * hs[fa[x][i - 1]][i - 1] * pw[1 << (i - 1)] + hs[x][i - 1]) % Mod;
    }

    return x;
}

inline bool cmp(int x, int y) {
    for (int i = LOGN - 1; ~i; --i)
        if (fa[x][i] && fa[y][i] && hs[x][i] == hs[y][i])
            x = fa[x][i], y = fa[y][i];

    if (x == 1)
        return true;
    else if (y == 1)
        return false;
    else
        return hs[x][0] < hs[y][0];
}
} // namespace Trie

signed main() {
    scanf("%s", str + 1), n = strlen(str + 1);
    pw[0] = 1;

    for (int i = 1; i <= n; ++i)
        pw[i] = 1ll * pw[i - 1] * base % Mod;

    f[n + 1] = ++Trie::tot;

    for (int i = n, top = 0; i; --i) {
        if (str[i] == '(') {
            if (top)
                nxt[i] = sta[top--];

            int v = Trie::extend(f[i + 1], 0);
            f[i] = (nxt[i] && Trie::cmp(f[nxt[i] + 1], v) ? f[nxt[i] + 1] : v);
        } else
            sta[++top] = i, f[i] = Trie::extend(f[i + 1], 1);
    }

    for (int x = f[1]; x != 1; x = Trie::fa[x][0])
        putchar(Trie::hs[x][0] ? ')' : '(');

    return 0;
}

LOJ2839. 「JOISC 2018 Day 3」安全门

定义对括号串的区间反转为选择一个连续的区间 \([l, r]\) 将内部的 ( 改成 )) 改成 (

给定一个长度为 \(n\) 的只包含 ( )x 的字符串,求有多少种将每一个 x 修改为 () 的方案(每个 x 之间独立),使得其可以由至多一次区间反转变为一个合法的括号串。

\(n \leq 300\)

令左括号为 \(1\) ,右括号为 \(-1\) ,假设翻转区间为 \((l, r]\) ,则 \(S_n - 2(S_r - S_l) = 0\) ,即 \(S_r = S_l + \frac{1}{2} S_n\) ,且前缀和变为:

\[\begin{cases} S_i & i \leq l \\ 2 S_l - S_i & l < i \leq r \\ S_i - S_n & r < i \leq n \end{cases} \]

括号序列合法当且仅当 \(\forall i, S_i \geq 0, S_i - S_n \geq 0\) ,考虑按这两个条件分类讨论。

若两个条件均合法,问题转化为合法括号序列计数,不难 \(O(n^2)\) DP 处理。

若恰有一侧不合法,不妨钦定存在 \(S_i < 0\) 。找到第一个 \(S_i = -1\) 的位置 \(p\) ,则翻转的区间必须满足 \(l < p\)

注意到选择 \(S_l\) 越大的位置翻转越优,因此考虑选择 \(S_{0 \sim p - 1}\) 中的最大值作为 \(S_l\) 。由于 \(S_i - S_n \geq 0\) ,因此只要找一个最小的 \(r\) 满足 \(S_r = S_l + \frac{1}{2} S_n\) 即可。

\(S_l + \frac{1}{2} S_n \geq 0\) ,直接在 \((l, p]\) 中就能找到合法的 \(r\) 。否则需要 DP,满足 \(\max_{i = p}^r S_i \leq 2 S_l + S_n\) 。可以从后往前维护 \(T_i = S_i - S_n\) ,则 \(r\) 满足 \(T_r = S_l - \frac{1}{2} S_n\) ,限制为 \(\forall i \in [p, r], T_i \leq 2S_l - S_n\)

枚举 \(2 S_l - S_n\) 然后 DP,前缀 DP 设 \(g_{i, j}\) 表示 \(S_i = -1\)\(\max_{k = 1}^i S_k = j\) 的方案数,后缀 DP 设 \(f_{i, j, 0/1}\) 表示 \(T_i = j\) 、是否需要在 \([p, n]\) 确定 \(r\) 的方案数。答案即为:

\[\sum_{i = 1}^n \sum_{j = 0}^i g_{i, j} \times f_{i, -S_n - 1, [j + \frac{1}{2} S_n < 0]} \]

若两侧均不合法,找到第一个 \(S_i = -1\) 的位置 \(p\) 与最后一个 \(S_i - S_n = -1\) 的位置 \(q\) ,则 \(l < p\)\(r \geq q\) ,显然 \(l\) 选取 \(S_{0 \sim p - 1}\) 中的最大值最优,\(r\) 则要求 \(S_l + \frac{1}{2} S_n \leq \max_{i = q}^n S_i\) 才有 \(r\) 可取。

\(S_l + \frac{1}{2} S_n > \max_{i = q}^n S_i\) ,则将序列倒过来之后一定有 \(S_l + \frac{1}{2} S_n \leq \max_{i = q}^n S_i\) ,注意需要减去被算两次的 \(S_l + \frac{1}{2} S_n = \max_{i = q}^n S_i\) 的情况。

同样维护 \(T_i = S_i - S_n\) ,则 \(r\) 满足 \(T_r = S_l - \frac{1}{2} S_n\) 。考虑前后分别 DP,前缀 DP 沿用 \(g_{i, j}\) 定义不变,后缀 DP 设 \(f_{i, j, 0/1/2}\) 表示 \(T_i = j\)\(k = 0\) 表示没确定 \(r\)\(k = 1\) 表示确定了 \(r\)\(k = 2\) 表示确定了 \(q\)

\(k \neq 0\) ,则要求 \(T_i \leq 2S_l - S_n\) ,具体和上一步是类似的,注意 \(k = 2\)\(j\) 可以 \(<0\) ,答案即为:

\[\sum_{i = 1}^n \sum_{j = 0}^i g_{i, j} \times f_{i, -S_n - 1, 2} \]

总时间复杂度 \(O(n^3)\)

#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 3e2 + 7;

char str[N];

int n;

inline int add(int x, int y) {
    x += y;
    
    if (x >= Mod)
        x -= Mod;
    
    return x;
}

inline int dec(int x, int y) {
    x -= y;
    
    if (x < 0)
        x += Mod;
    
    return x;
}

namespace Solver1 { // prefix and suffix are valid
int f[N][N];

inline int solve() {
    f[0][0] = 1;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j <= i; ++j) {
            if (str[i + 1] != ')')
                f[i + 1][j + 1] = add(f[i + 1][j + 1], f[i][j]);

            if (j && str[i + 1] != '(')
                f[i + 1][j - 1] = add(f[i + 1][j - 1], f[i][j]);
        }

    return f[n][0];
}
} // namespace Solver1

namespace Solver0 { // prework
int f[N][N][N], g[N][N];
// f[i][j][k] : S[i] = j, max_pre_sum = k
// g[i][j] : S[i] = -1, max_pre_sum = j

inline void solve() {
    memset(f, 0, sizeof(f)), memset(g, 0, sizeof(g));
    f[0][0][0] = 1;

    for (int i = 0; i < n; ++i)
        for (int j = 0; j <= i; ++j)
            for (int k = j; k <= i; ++k) {
                if (str[i + 1] != ')')
                    f[i + 1][j + 1][max(k, j + 1)] = add(f[i + 1][j + 1][max(k, j + 1)], f[i][j][k]);

                if (str[i + 1] != '(') {
                    if (j)
                        f[i + 1][j - 1][k] = add(f[i + 1][j - 1][k], f[i][j][k]);
                    else
                        g[i + 1][k] = add(g[i + 1][k], f[i][j][k]);
                }
            }
}
} // namespace Solver0

namespace Solver2 { // prefix is invalid, suffix id valid
int f[N][N][2]; // f[i][j][0/1] : T[i] = S[i] - S[n] = j, [r is set]

inline int solve() {
    int ans = 0;

    for (int s = 0; s <= n; ++s) { // s = S[l] - S[n] / 2
        memset(f, 0, sizeof(f)), f[n][0][0] = 1;

        for (int i = n; i; --i)
            for (int j = 0; j <= n - i; ++j) 
                for (int k = 0; k <= 1; ++k) {
                    if (k && j > s * 2) // T[i] should <= 2 S[l] - S[n]
                        f[i][j][k] = 0;
                    else if (k && j == s) // find r
                        f[i][j][k] = f[i][j][0];

                    if (str[i] != '(')
                        f[i - 1][j + 1][k] = add(f[i - 1][j + 1][k], f[i][j][k]);

                    if (str[i] != ')' && j > 0)
                        f[i - 1][j - 1][k] = add(f[i - 1][j - 1][k], f[i][j][k]);
                }

        for (int i = 1; i <= n; ++i) // first S[i] = -1
            for (int j = 0; j <= i; ++j) { // S[l]
                int k = (j - s) * 2; // S[n]

                if (-n <= k && k < 0)
                    ans = add(ans, 1ll * Solver0::g[i][j] * f[i][-k - 1][j + k / 2 < 0] % Mod);
            }
    }

    return ans;
}
} // namespace Solver2

namespace Solver3 { // prefix and suffix are invalid
int f[N][N << 1][3]; // f[i][j][0/1/2] : T[i] = j, [r is set] + [q is set]

inline int solve(bool flag) {
    int ans = 0;

    for (int s = 0; s <= n; ++s) { // s = S[l] - S[n] / 2
        memset(f, 0, sizeof(f)), f[n][n][0] = 1;

        for (int i = n; i; --i)
            for (int j = i - n; j <= n - i; ++j)
                for (int k = 0; k <= 2; ++k) {
                    if (k && j > s * 2) // T[i] should <= 2 S[l] - S[n]
                        f[i][j + n][k] = 0;
                    else if (k == 1 && j == s) // find r
                        f[i][j + n][k] = f[i][j + n][0];

                    if (flag && k != 2 && j > s)
                        f[i][j + n][k] = 0;

                    if (str[i] != '(')
                        f[i - 1][j + 1 + n][k] = add(f[i - 1][j + 1 + n][k], f[i][j + n][k]);

                    if (str[i] != ')') {
                        if (j)
                            f[i - 1][j - 1 + n][k] = add(f[i - 1][j - 1 + n][k], f[i][j + n][k]);
                        else if (k)
                            f[i - 1][j - 1 + n][2] = add(f[i - 1][j - 1 + n][2], f[i][j + n][k]);
                    }
                }

        for (int i = 1; i <= n; ++i) // first S[i] = -1
            for (int j = 0; j <= n; ++j) { // S[l]
                int k = (j - s) * 2; // S[n]

                if (-n <= k && k <= n)
                    ans = add(ans, 1ll * Solver0::g[i][j] * f[i][-k - 1 + n][2] % Mod);
            }
    }

    return ans;
}
} // namespace Solver3

signed main() {
    scanf("%d%s", &n, str + 1);

    if (n & 1)
        return puts("0"), 0;

    int ans = Solver1::solve();
    Solver0::solve(), ans = add(ans, add(Solver2::solve(), Solver3::solve(false)));
    reverse(str + 1, str + n + 1);

    for (int i = 1; i <= n; ++i)
        if (str[i] != 'x')
            str[i] ^= 1;

    Solver0::solve(), ans = add(ans, add(Solver2::solve(), Solver3::solve(false)));
    printf("%d", dec(ans, Solver3::solve(true)));
    return 0;
}
posted @ 2025-06-13 10:48  wshcl  阅读(32)  评论(0)    收藏  举报