USACO Silver 2021 Dec

Convoluted Intervals

Problem 1160

讲解与题解分析

题目描述

给定 $N$ 个区间 $[a_i, b_i]$,其中 $0 \le a_i \le b_i \le M$。

对于 $0$ 到 $2M$ 之间的每一个整数 $k$,计算有多少对索引 $(i, j)$ 满足:

$$ a_i + a_j \le k \le b_i + b_j $$

数据范围:

  • $1 \le N \le 2 \cdot 10^5$
  • $1 \le M \le 5000$

理解题意:区间求和

条件 $a_i + a_j \le k \le b_i + b_j$ 实际上是在描述两个区间的

区间 i [1, 3] 区间 j [2, 4] 和区间 [3, 7] 1+2=3 3+4=7

如果 $k$ 落在 和区间 内,则这对 $(i, j)$ 贡献 +1。

朴素思路 (Time Limit Exceeded)

直接枚举所有可能的区间对 $(i, j)$:


for (int i = 0; i < N; i++) {
    for (int j = 0; j < N; j++) {
        int start = a[i] + a[j];
        int end = b[i] + b[j];
        // 在差分数组 [start, end] 区间 +1
    }
}
                    

复杂度分析:

  • $N$ 高达 $2 \cdot 10^5$
  • $N^2 \approx 4 \cdot 10^{10}$ 次运算
  • 这显然会 超时 (Time Limit: 1s 通常只能处理 $\sim 10^8$)

关键观察

$N$ 很大,但是 $M$ 很小

$M \le 5000$

  • 区间的端点值 $a_i, b_i$ 都在 $[0, M]$ 之间。
  • 两数之和 $a_i + a_j$ 的范围是 $[0, 2M]$,即 $[0, 10000]$。
  • 我们不需要关心具体的 $i, j$ 是谁,只需要知道:
    • 有多少个 $a$ 的值为 $x$?
    • 有多少个 $b$ 的值为 $y$?

优化策略:桶 (Buckets) & 卷积

  1. 统计频率:
    使用数组 `cnta[v]` 记录起始点为 $v$ 的区间数量。
    使用数组 `cntb[v]` 记录结束点为 $v$ 的区间数量。
  2. 枚举和 (卷积):
    枚举所有可能的 $a$ 值 $i$ 和 $j$ (范围 $0 \sim M$)。
    新的起始点 $S = i + j$ 的出现次数为 `cnta[i] * cnta[j]`。
  3. 差分数组:
    利用差分数组快速进行区间更新。

算法核心流程

1. 统计端点 a=1 x3 a=2 x2 2. 组合 (卷积) Start Sum = 1 + 2 = 3 共 3 * 2 = 6 对 3. 差分更新 diff[3] += 6 S=3 diff[E+1] -= 6 E (b的和) 有效区间贡献 +6

代码逻辑详解


// 统计频率
for (int i = 0; i < n; i++) {
    scanf("%d", &a[i]);
    scanf("%d", &b[i]);
    cnta[a[i]]++; // 统计起始点出现次数
    cntb[b[i]]++; // 统计结束点出现次数
}

// 核心:O(M^2) 枚举所有可能的和
for (int i = 0; i <= 2*m; i++) // i 代表两数之和
    for (int j = 0; j <= i; j++) { // j 代表第一个数
        // j 和 i-j 是一对数。cnta[j]*cnta[i-j] 是这对数组合出现的次数
        // 如果 j 或 i-j 超过 M,数组值为0,不影响结果
        
        // 在和为 i 的位置开始,区间数增加
        diff[i] += cnta[j] * cnta[i-j]; 
        
        // 在和为 i 的位置结束(的下一位),区间数减少
        // 注意:这里处理的是 End Sum
        diff[i+1] -= cntb[j] * cntb[i-j]; 
    }

// 前缀和还原答案
ll ans = 0;
for (int i = 0; i <= 2*m; i++) {
    ans += diff[i];
    printf("%lld\n", ans);
}
                    

复杂度分析

  • 空间复杂度:
    • $O(N + M)$。我们需要存储 $a, b$ 数组以及频率数组 `cnt` (大小约 $10000$)。
  • 时间复杂度:
    • 读取输入并统计频率:$O(N)$。
    • 枚举和计算差分:外层循环 $2M$,内层平均 $M$,总计约 $O(M^2)$。
    • 前缀和输出:$O(M)$。
    • 总复杂度:$O(N + M^2)$
  • 计算量:
    • $N = 2 \cdot 10^5$, $M = 5000$。
    • $M^2 \approx 2.5 \cdot 10^7$,远小于 $10^8$ (1秒限制),可以通过。

完整参考代码 (AC)


#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;

// 数据规模定义
int n, m;
int a[200005], b[200005];
ll cnta[10005], cntb[10005]; // 频率数组,大小 > 2*M
ll diff[10005];              // 差分数组

int main() {
    // 1. 输入与频率统计
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
        scanf("%d", &b[i]);
        cnta[a[i]]++;
        cntb[b[i]]++;
    }

    // 2. 计算卷积与构建差分数组
    // 枚举所有可能的和 i (从 0 到 2M)
    for (int i = 0; i <= 2*m; i++) {
        for (int j = 0; j <= i; j++) {
            // 边界检查由数组大小和数据特性自然保证(越界值为0)
            // 但严谨来说,只有 j<=M 且 i-j<=M 时才有意义
            if (j <= m && (i-j) <= m) {
               // Start Sum 贡献 +
               diff[i] += cnta[j] * cnta[i-j];
               // End Sum 贡献 - (在 i+1 处)
               diff[i+1] -= cntb[j] * cntb[i-j];
            }
        }
    }

    // 3. 前缀和计算最终答案并输出
    ll ans = 0;
    for (int i = 0; i <= 2*m; i++) {
        ans += diff[i];
        printf("%lld\n", ans);
    }

    return 0;
}
                    

总结

这道题是经典的"值域小,数据量大"的类型。

  • 遇到 $O(N^2)$ 不可行时,检查数据范围约束。
  • 利用 $M$ 较小的特性,将通过索引对 $(i,j)$ 的枚举转化为通过的枚举。
  • 差分数组 (Difference Array) 是处理区间覆盖问题的利器。
  • 卷积 (Convolution) 思想:计算两个数组所有元素两两之和的分布。