Codeforces 2063 F1 + F2 题解(吉司机线段树)

前置知识

长度为2n的合法括号序列的数量为第n个卡特兰数 H(n) = \frac{(2n)!}{n!(n+1)!}

题目大意

已知一个合法括号序列 S 长度为 2n。现在给出 n 条限制(限制依次叠加),第 i 条限制 l_i, r_i 表示 S_{l_i}, S_{r_i} 构成配对的括号,保证任意两条限制不矛盾。要分别求出没有限制时和给出每一条限制后所有符合条件的 S 的数量。

F12\leq n\leq5000

不妨设 S_0 = \text{`('}, S_{2n+1}= \text{`)'}

F1 的数据范围允许 O(n^2),因此可以每次添加限制后重新计算。

设 d_i = \begin{cases}\max \{l_j : l_j < i < r_j\} & \forall k (i \neq l_k \land i \neq r_k) \\ \infty & \mathrm{otherwise} \end{cases}。显然 S 符合条件当且仅当 S_{(l_i, r_i)}, S_{[0, l_i) \cup (r_i, 2n + 1]} 皆为合法括号序列,所以将下标按照 d_i 分组后,同一组下标按顺序连起来要构成合法括号序列,且同一组内无限制。因此,答案为 \prod_{j = 0}^{2n} H\left( \left|\{ i : d_i = j\}\right| \right )

计算到第 k 条限制时,用字符串 T 存储每一条限制,即 T_i = \begin{cases} \text{`('} & \exists j \leq k, l_j = i \\ \text{`)'} & \exists j \leq k, r_j = i \\ \text{` '} & \mathrm{otherwise} \end{cases}    。从头遍历 T 并维护一个栈,遇到 \text{`('}, \text{` '}     时入栈;遇到 \text{`)'}     时,不断弹出栈顶元素直至遇到 \text{`('}     ,将答案乘以弹出元素数量所对应的卡特兰数,然后将 \text{`('}     也弹出。这样计算每一条限制的时间复杂度为 \Theta(n) ,总时间复杂度为 \Theta(n^2)

F22 \leq n \leq 3 \times 10^5

要解决 F2,添加限制时只能在原基础上修改,不可重新计算。

先考虑添加第 j 组限制后 d 的变化:\forall i \in \left(l_j, r_j\right) (d_i \gets \max (d_i, l_j)),很容易想到用线段树维护。然而,普通线段树无法在 O(\log n) 内维护 c_j = \left| \{ i : d_i = j\} \right|。这时,就需要吉司机线段树登场了。

吉司机线段树是懒标记线段树的一种变体。我们用线段树同时维护区间最小值 \mathtt{mn},次小值 \mathtt{smn},以及最小值数量 \mathtt{cnt}。当用 v 更新时,若 v \leq \mathtt{mn},则什么都不做;若 \mathtt{mn} < v < \mathtt{smn},则只有最小值改变,用 \texttt{cnt} 更新 c_\mathtt{mn}, c_v;否则无法确认更新元素的数量,需递归处理子节点。另外,这里虽然是区间修改但是不用懒标记,因为 \mathtt{mn} 本身就有懒标记的作用。

吉司机线段树单次操作均摊时间复杂度为 O(\log n),证明见吉老师的 PPT:Segment tree Beats!.pdfhttps://pan.baidu.com/s/1o7xSSQ2https://pan.baidu.com/s/1o7xSSQ2https://pan.baidu.com/s/1o7xSSQ2

代码

#include <bits/stdc++.h>
using namespace std;

struct Node
{
    size_t mn, smn, cnt;
};

int main()
{
    cin.tie(nullptr)->sync_with_stdio(false);
    int t;
    cin >> t;
    while (t--)
    {
        constexpr size_t mod = 998244353, inf = 0x3f3f3f3f;
        size_t n;
        cin >> n, ++n;
        const size_t n2 = n << 1, log = 64 - __builtin_clzll(n2), size = 1ULL << log;
        vector<Node> d(size << 1, {inf, inf << 1, 1});
        for (size_t i{1}; i < n2; ++i) d[i + size].mn = 0;
        vector<size_t> cnt(n2);
        cnt.front() = n2 - 2;
        auto update = [&](const size_t& k)
        {
            auto [lmn, lsmn, lcnt] = d[k << 1];
            auto [rmn, rsmn, rcnt] = d[k << 1 | 1];
            if (lmn < rmn) d[k] = {lmn, min(lsmn, rmn), lcnt};
            else if (lmn > rmn) d[k] = {rmn, min(lmn, rsmn), rcnt};
            else d[k] = {lmn, min(lsmn, rsmn), lcnt + rcnt};
        };
        for (size_t i{size - 1}; i; --i) update(i);
        auto naive_apply = [&](const size_t& k, const size_t& v)
        {
            if (auto& mn = d[k].mn; v > mn) mn = v;
        };
        auto push = [&](const size_t& k)
        {
            const size_t& mn = d[k].mn;
            naive_apply(k << 1, mn), naive_apply(k << 1 | 1, mn);
        };
        function<void(const size_t&, const size_t&)> all_apply = [&](const size_t& k, const size_t& v)
        {
            if (auto& [mn, smn, num] = d[k]; v <= mn) return;
            else if (v < smn)
            {
                cnt[mn] -= num, cnt[mn = v] += num;
                return;
            }
            push(k);
            all_apply(k << 1, v), all_apply(k << 1 | 1, v);
            update(k);
        };
        auto set = [&](size_t p)
        {
            p |= size;
            for (size_t i{log}; i; --i) push(p >> i);
            --cnt[d[p].mn], d[p].mn = inf;
            for (size_t i{1}; i <= log; ++i) update(p >> i);
        };
        auto apply = [&](size_t l, size_t r, const size_t& v)
        {
            l += size, r += size;
            for (size_t i = log; i; --i)
            {
                if (l >> i << i != l) push(l >> i);
                if (r >> i << i != r) push(r - 1 >> i);
            }
            for (size_t l2 = l, r2 = r; l2 < r2; l2 >>= 1, r2 >>= 1)
            {
                if (l2 & 1) all_apply(l2++, v);
                if (r2 & 1) all_apply(--r2, v);
            }
            for (size_t i = 1; i <= log; ++i)
            {
                if (l >> i << i != l) update(l >> i);
                if (r >> i << i != r) update(r - 1 >> i);
            }
        };
        auto get = [&](size_t p)
        {
            p |= size;
            for (size_t i = log; i; --i) push(p >> i);
            return d[p].mn;
        };
        vector<size_t> fac(n2, 1LL), ifac(n2, 1LL);
        for (size_t i = 1; i < n2; ++i) fac[i] = fac[i - 1] * i % mod;
        for (size_t m = mod - 2, &now = ifac.back(), x = fac.back(); m; m >>= 1, (x *= x) %= mod)
            if (m & 1) (now *= x) %= mod;
        for (size_t i{n2 - 2}; i; --i) ifac[i] = ifac[i + 1] * (i + 1) % mod;
        auto cat = [&](const size_t& x)
        {
            return fac[x << 1] * ifac[x] % mod * ifac[x + 1] % mod;
        };
        auto icat = [&](const size_t& x)
        {
            return ifac[x << 1] * fac[x] % mod * fac[x + 1] % mod;
        };
        size_t ans = cat(n - 1);
        cout << ans << ' ';
        for (size_t i{1}; i < n; ++i)
        {
            size_t l, r;
            cin >> l >> r;
            const size_t ori = get(l);
            (ans *= icat(cnt[ori] >> 1)) %= mod;
            set(l), set(r), apply(l + 1, r, l);
            cout << ((((ans *= cat(cnt[l] >> 1)) %= mod) *= cat(cnt[ori] >> 1)) %= mod) << ' ';
        }
        cout << '\n';
    }
}

P.S. 这并非最佳做法,用线段树并查集可以做到O(n\alpha(n))但那个太难了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值