2025.11.19 听课总结
去听了上海曹杨二中(CYEZ)神佬们的杂题选讲,%%%。
T2:QOJ401 - 因子统计 (Ex)
别问我为什么从 T2 开始(
题目链接:https://qoj.ac/problem/401。
题意简述
有 \(q\) 次离线询问,求 \(C_n^m\) 的因子数,对 \(10^9 + 7\) 取模。
数据范围:\(1 \leq q \leq 5 \times 10^5\),\(1 \leq n \leq 10^6\);\(2s\)、\(1024MB\)。
题解
数学题。
令 \(N\) 表示所有 \(n\) 的最大值,记 \(v_p(x)\) 表示对 \(x\) 质因数分解后 \(p\) 的幂次。
根据组合数的知识,有 \(C_n^m = \frac{n!}{m!(n - m)!}\),因此 \(v_p(C_n^m) = v_p(n!) - v_p(m!) - v_p((n - m)!)\)。
下面考虑 \(v_p(x!)\) 怎么求。显然,\(x!\) 就是 \(1 \sim x\) 乘起来,而 \(1 \sim x\) 中 \(p\) 的倍数有 \(\lfloor \frac{x}{p} \rfloor\) 个,\(p^2\) 的倍数有 \(\lfloor \frac{x}{p^2} \rfloor\) 个,等等。
注意到我们计算 \(p\) 的倍数时,正好把 \(p^2\) 的倍数全算了一遍,但 \(p^2\) 比 \(p\) 多一个 \(p\)!因此,我们可以得到式子:
这样对于一次询问的时间复杂度是 \(\mathcal{O}(\log_p(n))\)。因此对于所有质数 \(p \leq \sqrt{N}\),我们可以利用这种方法统计贡献,总时间复杂度为 \(\mathcal{O}(q\frac{\sqrt{N}}{\log N})\)。
下面考虑大于 \(\sqrt{N}\) 的质数 \(p\)。此时由于 \(p\) 很大,所以在刚才的式子中 \(k\) 只能取到 \(1\)!换句话说,就是 \(v_p(x!) = \lfloor \frac{x}{p} \rfloor\)。同时,注意到由于 \(p\) 很大,所以 \(v_p(C_n^m) = v_p(n!) - v_p(m!) - v_p((n - m)!) = \frac{n}{p} - \frac{m}{p} - \frac{n - m}{p} \in \{0, 1\}\)。
因此,我们可以通过预处理有哪些 \(p\) 使得 \(v_p(C_n^m) = 1\),贡献即 \(2^{cnt_p}\)。
预处理的时间复杂度为 \(\mathcal{O}(N + \sum\limits_{p > \sqrt{N}} \frac{N}{p}) = \mathcal{O}(N)\),因此这部分可以快速处理。
综上,总时间复杂度为 \(\mathcal{O}(q\frac{\sqrt{N}}{\log N})\),可以通过本题。
P.S. 据说用合理的数据结构处理 \(N^{\frac{1}{3}} \sim \sqrt{N}\) 之间的质数可以更快,但我不会(
参考代码(C++):
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x = 0, f = 1;
char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)){
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
inline void write(int x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) write(x / 10);
putchar(x % 10 + '0');
return;
}
const int Mod = 1e9 + 7;
int q, n[500005], m[500005], N, vpfac[1000005], ans[500005], pow2[1000005], f[1000005];
bool isPrime[1000005];
vector<int> Prime;
signed main(){
q = read();
for(int i = 1; i <= q; i++) n[i] = read(), m[i] = read(), N = max(N, n[i]), ans[i] = 1;
memset(isPrime, true, sizeof(isPrime));
isPrime[1] = false;
for(int i = 2; i <= N; i++){
if(isPrime[i]) Prime.push_back(i);
for(auto p : Prime){
if(i * p > N) break;
isPrime[i * p] = false;
if(i % p == 0) break;
}
}
pow2[0] = 1;
for(int i = 1; i <= N; i++) pow2[i] = pow2[i - 1] * 2ll % Mod;
for(auto p : Prime){
if(p > sqrt(N)) break;
memset(vpfac, 0, sizeof(vpfac));
for(int i = 1; i <= N; i++){
int base = p;
while(base <= i){
vpfac[i] += i / base;
base *= p;
}
}
for(int i = 1; i <= q; i++){
int vp = ((vpfac[n[i]] - vpfac[m[i]] - vpfac[n[i] - m[i]]) % Mod + Mod) % Mod;
ans[i] = ans[i] * (vp + 1) % Mod;
}
}
for(auto p : Prime) if(p > sqrt(N)){
for(int i = p; i <= N; i += p){
f[i]++;
}
}
for(int i = 1; i <= N; i++) f[i] += f[i - 1];
for(int i = 1; i <= q; i++) write(ans[i] * pow2[f[n[i]] - f[m[i]] - f[n[i] - m[i]]] % Mod), putchar(" \n"[i == q]);
return 0;
}
T3:4th Ucup S3 E - Maximum Segment Sum
题目链接:https://qoj.ac/problem/14419。
题意简述
给定序列长度 \(n\),序列的每一项可以是 \(1\) 或 \(-1\)。
对于每一个 \(k\),求出有多少序列满足最大子段和为 \(k\)。
答案对 \(998244353\) 取模。
数据范围:\(1 \leq n \leq 5 \times 10^5\);\(1.5s\)、\(1024MB\)。
题解
首先考虑对于给定序列,如何求其最大子段和。
显然,我们可以维护一个变量 \(cur\),每次执行 \(cur \leftarrow \max(0, cur + a_i)\),并记录整个过程中最大的 \(cur\) 即可。
其次,我们可以通过容斥原理,把恰好为 \(k\) 转化为不超过 \(k\)。
我们用一个平面直角坐标系去刻画 \(cur\) 的变化,可以得到如下所示的图:

显然,不超过 \(k\) 就是不能跨过 \(y = k\),即不能碰到 \(y = k + 1\)。
但跟 \(0\) 取 \(\max\) 这个操作很烦,故考虑把它变成碰到 \(y = -0.5\) 就反射。显然,如果我们这样反射但不容斥,可以解决跟 \(0\) 取 \(\max\) 的问题。
但是,翻转后也得保证不超过 \(k\),因此我们需要同时满足不碰到 \(y = k + 1\) 和 \(y = -k - 2\)。
此时问题已经变成很标准的双线反射容斥了。但注意,你的终点实际上是很多点,不能直接计算。
容易发现终点们组成了一条垂线段,垂线段关于某条水平直线翻折仍然是垂线段!因此,利用前缀和统计组合数的和即可。
P.S. 学到了一种神奇的语法,可以直接处理偏移量,详见代码。
参考代码(C++):
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x = 0, f = 1;
char ch = getchar();
while(!isdigit(ch)){
if(ch == '-') f = -1;
ch = getchar();
}
while(isdigit(ch)){
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x * f;
}
inline void write(int x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) write(x / 10);
putchar(x % 10 + '0');
return;
}
const int Mod = 998244353;
int n, fac[4100000], inv[4100000];
int Pre[4100000], *pre = Pre + 2000000;
int Ans[4100000], *ans = Ans + 2000000;
inline int fp(int a, int b){
int res = 1;
while(b){
if(b & 1) res = res * a % Mod;
a = a * a % Mod;
b >>= 1;
}
return res;
}
inline int sum(int l, int r){return (pre[r] - pre[l - 1] + Mod) % Mod;}
signed main(){
//Input
n = read();
//Init
const int N = 4000000;
fac[0] = 1; for(int i = 1; i <= N; i++) fac[i] = fac[i - 1] * i % Mod;
inv[N] = fp(fac[N], Mod - 2); for(int i = N; i >= 1; i--) inv[i - 1] = inv[i] * i % Mod;
for(int i = -n; i <= 3 * n; i++){
pre[i] = (pre[i - 1] +
(
abs(n - i) & 1 ? 0 : fac[n] * inv[(n + i) >> 1] % Mod * inv[(n - i) >> 1] % Mod
)
) % Mod;
}
//Solve
for(int k = 0; k <= n; k++){
for(int x = ((k + 1) - (-k - 2)), f = -1; x - k - 1 <= n; x += ((k + 1) - (-k - 2))) ans[k] = (ans[k] + f * sum(x - k - 1, x + k)) % Mod, f = -f;
for(int x = 0, f = 1; x + k >= -n; x -= ((k + 1) - (-k - 2))) ans[k] = (ans[k] + f * sum(x - k - 1, x + k)) % Mod, f = -f;
}
for(int k = n; k >= 1; k--) ans[k] = (ans[k] - ans[k - 1] + Mod) % Mod;
//Output
for(int k = 0; k <= n; k++) write((ans[k] % Mod + Mod) % Mod), putchar(" \n"[k == n]);
return 0;
}

浙公网安备 33010602011771号