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\)!因此,我们可以得到式子:

\[v_p(x!) = \sum\limits_{k \geq 1} \lfloor \frac{x}{p^k} \rfloor \]

这样对于一次询问的时间复杂度是 \(\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;
}
posted @ 2025-11-19 16:22  zhang_kevin  阅读(9)  评论(0)    收藏  举报