前置知识
长度为的合法括号序列的数量为第
个卡特兰数
。
题目大意
已知一个合法括号序列 长度为
。现在给出
条限制(限制依次叠加),第
条限制
表示
构成配对的括号,保证任意两条限制不矛盾。要分别求出没有限制时和给出每一条限制后所有符合条件的
的数量。
F1(
)
不妨设 。
F1 的数据范围允许 ,因此可以每次添加限制后重新计算。
设 。显然
符合条件当且仅当
皆为合法括号序列,所以将下标按照
分组后,同一组下标按顺序连起来要构成合法括号序列,且同一组内无限制。因此,答案为
。
计算到第 条限制时,用字符串
存储每一条限制,即
。从头遍历
并维护一个栈,遇到
时入栈;遇到
时,不断弹出栈顶元素直至遇到
,将答案乘以弹出元素数量所对应的卡特兰数,然后将
也弹出。这样计算每一条限制的时间复杂度为
,总时间复杂度为
。
F2(
)
要解决 F2,添加限制时只能在原基础上修改,不可重新计算。
先考虑添加第 组限制后
的变化:
,很容易想到用线段树维护。然而,普通线段树无法在
内维护
。这时,就需要吉司机线段树登场了。
吉司机线段树是懒标记线段树的一种变体。我们用线段树同时维护区间最小值 ,次小值
,以及最小值数量
。当用
更新时,若
,则什么都不做;若
,则只有最小值改变,用
更新
;否则无法确认更新元素的数量,需递归处理子节点。另外,这里虽然是区间修改但是不用懒标记,因为
本身就有懒标记的作用。
吉司机线段树单次操作均摊时间复杂度为 ,证明见吉老师的 PPT:Segment tree Beats!.pdfhttps://pan.baidu.com/s/1o7xSSQ2https://pan.baidu.com/s/1o7xSSQ2
https://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. 这并非最佳做法,用线段树并查集可以做到,
但那个太难了。