1 条题解

  • 0
    @ 2025-10-23 22:42:45

    题目理解

    我们有 nn 个位置,初始为 00
    执行 kk 次操作,每次操作 (a,,d)(a, \ell, d) 表示在位置 a,a+d,a+2d,,a+(1)da, a+d, a+2d, \dots, a+(\ell-1)d 各加 11
    最后输出每个位置的总数。


    问题转化

    设最终数组为 c1,c2,,cnc_1, c_2, \ldots, c_n,其中: [ c_i = \sum_{j=1}^k [\text{操作 }j\text{ 覆盖了位置 }i] ]

    对于每个操作 (a,,d)(a, \ell, d),它覆盖的位置集合是: [ {a, a+d, a+2d, \ldots, a+(\ell-1)d} ]


    直接暴力做法

    最直接的方法是:

    for (int i = 0; i < l; i++)
        a[x + i * d]++;
    

    这样一次操作复杂度是 O()O(\ell),最坏情况 \ell 可能接近 nnkk 次操作可能 O(nk)O(nk),会超时。


    优化思路:阈值分治

    关键观察:如果 dd 很大,那么 \ell 会很小(因为 a+(1)dna + (\ell-1)d \le n),所以 nd\ell \le \frac{n}{d}
    因此对于 dd 很大的情况,直接循环加是可以的,因为 \ell 很小。

    问题在于 dd 很小的情况,例如 d=1d=1\ell 可能很大,直接循环会超时。


    差分数组技巧

    对于 dd 较小的情况,我们可以用"按模分类的差分数组"来优化。

    s[d][j]s[d][j] 表示在模 dd 的剩余类中,起始位置 jj 的差分标记。
    对于一次操作 (a,,d)(a, \ell, d),如果 dBd \le B,我们就在 s[d][a]s[d][a]11,在 s[d][a+d]s[d][a + \ell \cdot d]11

    之后,对于每个 dd,我们按模 dd 的剩余类分别做前缀和:

    for (int r = 0; r < d; r++) {
        for (int j = r; j <= n; j += d) {
            if (j >= d) s[d][j] += s[d][j - d];
            a[j] += s[d][j];
        }
    }
    

    这样对于每个 dd,复杂度是 O(n)O(n),总复杂度 O(Bn)O(B \cdot n)


    阈值选择与复杂度分析

    我们取阈值 BB,当 dBd \le B 时用差分法,当 d>Bd > B 时直接循环。

    直接循环的复杂度分析
    对于 d>Bd > Bnd<nB\ell \le \frac{n}{d} < \frac{n}{B},所以总操作次数 knBk \cdot \frac{n}{B}

    因此总复杂度为: [ O\left(B \cdot n + k \cdot \frac{n}{B}\right) ] 取 BkB \approx \sqrt{k}n\sqrt{n} 可以平衡。这里代码取 B=300B = 300,因为 n,k105n,k \le 10^5,这样 Bn3×107B \cdot n \approx 3\times 10^7,可以接受。


    代码实现详解

    #include <bits/stdc++.h>
    using namespace std;
    
    const int N = 1e5 + 1e3 + 7, B = 300;  // N 略大于 n 最大值,B 是阈值
    
    int n, k;
    int a[N];  // 最终答案数组
    int s[B + 1][N];  // s[d][j] 用于 d <= B 的差分标记
    
    signed main() {
        ios::sync_with_stdio(false);
        cin.tie(0);
        cin >> n >> k;
    
        while (k--) {
            int x, l, d;
            cin >> x >> l >> d;
    
            if (d <= B) {
                // 使用差分标记:在起始位置加1,在结束位置的下一个位置减1
                s[d][x]++;
                if (x + l * d <= n)
                    s[d][x + l * d]--;
            } else {
                // 直接循环加:因为d大时l小
                for (int i = 0; i < l; i++)
                    a[x + i * d]++;
            }
        }
    
        // 对每个 d <= B,按剩余类做前缀和
        for (int d = 1; d <= B; d++) {
            for (int r = 0; r < d; r++) {  // 枚举每个剩余类
                for (int j = r; j <= n; j += d) {  // 在同一个剩余类中做前缀和
                    if (j >= d) 
                        s[d][j] += s[d][j - d];
                    a[j] += s[d][j];
                }
            }
        }
    
        // 输出结果
        for (int i = 1; i <= n; i++)
            cout << a[i] << " \n"[i == n];
    }
    

    算法正确性分析

    dd 情况
    对于每个 dBd \le B,我们使用差分数组 s[d]s[d]。操作 (a,,d)(a, \ell, d)s[d][a]s[d][a]+1+1,在 s[d][a+d]s[d][a+\ell d]1-1
    当按剩余类做前缀和时,对于每个剩余类 rrs[d][r],s[d][r+d],s[d][r+2d],s[d][r], s[d][r+d], s[d][r+2d], \ldots 会正确累加,使得从位置 aaa+(1)da+(\ell-1)d 的每个位置都 +1+1

    dd 情况
    由于 nd<nB\ell \le \frac{n}{d} < \frac{n}{B},直接循环的代价是可接受的。


    算法标签

    • 阈值分治:根据 dd 的大小采用不同策略
    • 差分数组:对小的 dd 使用模意义下的差分
    • 直接枚举:对大的 dd 直接循环
    • 数论:利用模运算性质优化

    时间复杂度

    • dd 部分:O(Bn)O(B \cdot n)
    • dd 部分:O(knB)O(k \cdot \frac{n}{B})
    • 总复杂度:O(Bn+knB)O(B \cdot n + k \cdot \frac{n}{B}),取 B=kB = \sqrt{k} 时为 O(nk)O(n\sqrt{k})

    对于 n,k105n,k \le 10^5,这是可以接受的。


    总结

    本题通过阈值分治巧妙解决了等差数列区间加问题:

    对稀疏访问(大 dd)采用直接枚举

    对密集访问(小 dd)采用差分技巧

    这种思想在解决类似"根据参数特征选择不同算法"的问题时非常有用,是算法竞赛中的经典优化模式。

    • 1

    信息

    ID
    3956
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者