2025.3.26 数学专题
CF1264D2 Beautiful Bracket Sequence (hard version)
定义一个括号序列的“深度”为删去一些括号后匹配括号层数的最大值,现给定一个每一位为 \((\ )\ ?\) 的括号序列,\(?\) 既可以看作是 \((\) 也可以看作是 \()\),求所有可能的括号序列的深度之和 \(\bmod998244353\)。
考虑删去一些括号之后最后可以直接统计深度的一定是形如 \(((\cdots()))\cdots)\) 的序列且左右括号数量相等,我们不妨直接统计序列中左边有用的左括号有多少个。那么枚举左右括号的分界点 \(t\),如果左边有 \(l\) 个 \((\),右边有 \(r\) 个 \()\),左边和右边分别有 \(x\) 和 \(y\) 个 \(?\)。如果我们在左边从 \(x\) 拿出来 \(i\) 个 \(?\) 成为 \((\),一共有 \(l+i\) 个左括号,贡献即为 \(l+i\);在右边从 \(y\) 中拿出一部分和 \(r\) 一起凑出 \(l+i\) 个右括号,选择的 \(?\) 个数就是 \(l+i-r\),其实总贡献就可以表示为
每个位置的 \(l,r,x,y\) 是好处理的,所以可以每个位置单独计算;现在考虑中间那块组合数怎么快速计算。先拆贡献,有:
对组合数进行基本的恒等变换,吸收公式 \(+\) 对称公式 \(+\) 范德蒙德卷积即可得到
预处理可以做到 \(O(n)\) 的时间复杂度。注意组合数可能有负指标,要稍微判一下。
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e6 + 10, mo = 998244353;
int n, prel[maxn], prer[maxn], preq[maxn];
char c[maxn];
ll fac[maxn], ifac[maxn], ans;
ll qpow(ll x, ll y) {
ll res = 1;
while(y) {
if(y & 1) (res *= x) %= mo;
y >>= 1, (x *= x) %= mo;
} return res;
}
void pre_calc() {
fac[0] = ifac[0] = 1; for(int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mo;
ifac[n] = qpow(fac[n], mo - 2); for(int i = n - 1; i >= 1; i--) ifac[i] = ifac[i + 1] * (i + 1) % mo;
for(int i = 1; i <= n; i++) prel[i] = prel[i - 1] + (c[i] == '('), prer[i] = prer[i - 1] + (c[i] == ')'), preq[i] = preq[i - 1] + (c[i] == '?');
return;
}
ll C(ll x, ll y) {return (x < y || x < 0 || y < 0) ? 0ll : (fac[x] * ifac[y] % mo * ifac[x - y] % mo);}
int main() {
// ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
scanf("%s", c + 1); n = strlen(c + 1); pre_calc();
for(int i = 1; i <= n; i++) {
ll x = preq[i], y = preq[n] - preq[i], l = prel[i], r = prer[n] - prer[i];
ans = (ans + l * C(x + y, y + r - l) % mo + x * C(x + y - 1, y - l + r - 1) % mo) % mo;
}
printf("%lld", ans);
return 0;
}
其实 easy version 的 \(O(n^2)\) DP 挺有意思的,也简单说一下。
考虑区间 DP 如何不重不漏地计算总贡献。设 \(f_{i,j}\) 表示区间 \([i,j]\) 的总贡献,答案即为 \(f_{1,n}\)。
考虑小区间如何转移到大区间。对于一个确定的括号序列,如果 \([i,j]\) 最左边是 \()\),那么新增加这一位不会有任何新匹配的括号,就不可能产生新的贡献,即有 \(f_{i,j}=f_{i+1,j}\);如果最右边是 \((\),同样不会产生任何贡献,即有 \(f_{i,j}=f_{i,j-1}\);如果最左边和最右边同时是 \((\) 和 \()\),那么答案贡献就一定增加 \(1\),即 \(f_{i,j}=f_{i+1,j-1}+1\)。总转移:
考虑如何拓展到有 \(?\) 的非确定括号序列上,那不是直接同时转移上面的所有式子就好了?
*其实不然.考虑两边都是问号时,如果每个式子都转移,相当于统计了 \()\cdots)/(\) 和 \()/(\cdots(\) 和 \((\cdots)\),\()\cdots(\) 这种情况就被重复统计了。类似的,就算只有一个问号的 \()\cdots)/(\) 和 \()/(\cdots(\) 或一个问号也没有的 \()\cdots(\),由于方案数是符合条件直接累加的,所以也重复转移了,要减去重复贡献 \(f_{i+1,j-1}\) 。
如果记 \([i+1,j-1]\) 的 \(?\) 数量为 \(cntq\),由于 \(?\) 决定了确定括号序列的数量是 \(2^{cntq}\) ,一对合法括号获得的贡献就是 \(2^{cntq}\)。所以有转移
从小到大枚举区间长度和左右端点即可,复杂度 \(O(n^2)\)。
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e3 + 10, mo = 998244353;
int n, cntq[maxn];
char c[maxn];
ll f[maxn][maxn];
ll qpow(ll x, ll y) {
ll res = 1;
while(y) {
if(y & 1) (res *= x) %= mo;
(x *= x) %= mo, y >>= 1;
} return res;
}
int main() {
// ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
scanf("%s", c + 1); n = strlen(c + 1);
for(int i = 1; i <= n; i++) cntq[i] = cntq[i - 1] + (c[i] == '?');
for(int len = 2; len <= n; len++) {
for(int i = 1; i <= n - len + 1; i++) {
int j = i + len - 1;
if(c[i] != '(') f[i][j] += f[i + 1][j];
if(c[j] != ')') f[i][j] += f[i][j - 1];
if(c[i] != ')' && c[j] != '(') f[i][j] += f[i + 1][j - 1] + qpow(2, cntq[j - 1] - cntq[i]);
if(c[i] != '(' && c[j] != ')') f[i][j] -= f[i + 1][j - 1];
((f[i][j] %= mo) += mo) %= mo;
}
}
printf("%lld", f[1][n]);
return 0;
}
P5572 简单的数论题
给定 \(n,m\),求
\(T\le3e4\) 次询问,\(m\le n\le5e4\)。
肯定是要推式子的
先拆开 \(\text {lcm}(i,j)\)
枚举 \(d=gcd(i,j)\),枚举 \(d\) 的倍数,消去分母的 \(d^2\)
积性函数性质直接拆开,用 \(\mu\) 来刻画互质
交换求和次序,再枚举 \(t\) 的倍数化简限制
这样搞还是没办法整除分块,先令 \(G(n,k)=\sum_{i=1}^n\varphi(ik)\),注意到所有的 \(G(n,k)\) 是可以 \(O(n\log n)\) 预处理的。
再令 \(T=dk\) 再简化一下
其实还是没法整除分块,再设 \(R(n,m,t)=\sum_{t\vert T}\mu(t)G(\lfloor{n\over T}\rfloor,t)G(\lfloor{m\over T}\rfloor,t)\)。
考虑枚举三维的 \(n,m,t\) 预处理 \(\le \sqrt L\) 的 \(R\),算 \(t\) 时枚举是调和级数,所以这一部分时间复杂度 \(O(L\sqrt L\log\sqrt L)\)。
于是是剩下的 \(n\ge\sqrt L\) 和 \(m\ge\sqrt L\) 就可以整除分块

浙公网安备 33010602011771号