题单 · 习题解析

洛谷 P8218 【深进1.例1】求区间和

模板题也有三处丢分:暴力值 50 分、s[l-1] 只在 l = 1 时咬人、int 恰好够(余量 2 倍)

原题:洛谷 P8218出自 第 6 章 前缀和与差分 的题单题面本地存档:2026-08-26
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P8218,日期见页头。两边不一致时信原站。

题目描述

给定由 n 个正整数组成的序列 a₁, a₂, ⋯, aₙm 个区间 [lᵢ, rᵢ], 分别求这 m 个区间的区间和。

输入格式

第一行包含一个正整数 n,表示序列的长度。

第二行包含 n 个正整数 a₁, a₂, ⋯, aₙ

第三行包含一个正整数 m,表示区间的数量。

接下来 m 行,每行包含两个正整数 lᵢ, rᵢ,满足 1 ≤ lᵢ ≤ rᵢ ≤ n

输出格式:共 m 行,其中第 i 行包含一个正整数,表示第 i 组答案的询问。

数据范围

  • 对于 50% 的数据:n, m ≤ 1000
  • 对于 100% 的数据:1 ≤ n, m ≤ 10⁵1 ≤ aᵢ ≤ 10⁴

输入输出样例

输入

4
4 3 2 1
2
1 4
2 3

输出

10
5

样例解释:第 1 到第 4 个数加起来和为 10;第 2 个数到第 3 个数加起来和为 5上面那段输出是仓库里的 p8218.cpp 真跑出来的。

1它就是第 6 章的模板题 —— 所以这一页讲别的

第 6 章前半场从头到尾就是这道题: s[i] = a[1] + … + a[i][l, r] 的和是 s[r] - s[l-1]算法部分到此结束。

所以这一页把力气花在三件「模板题也会丢分」的事上:

一、暴力那一版值多少分            <- ★ 题面自己写着 50%
二、s[l-1] 那个下标什么时候咬人    <- 只在 l = 1 时露头
三、int 到底够不够                <- 写代码之前就该算完的一笔账

2第 ① 版:每次从 l 加到 r —— ★ 它是实打实的 50 分

p8218Brute.cpp第 ① 版:每次询问重新加一遍
样例当然对。它挂的是规模,不是正确性。
// P8218 的第 ① 版:每次询问从 l 老老实实加到 r
//
// ★ 它**不是没用的** —— 题面明写着「对于 50% 的数据:n, m ≤ 1000」,
// 这一版在那一半数据上稳稳当当,**考场上就是实打实的 50 分**。
// ⇒ 第 45 章那条考场策略:**先把暴力写对,再想办法做快**;
// 不会正解的时候,暴力是分数,不是废纸。
//
// ⚠ 它在满数据上是 O(nm):n = m = 1e5、每次都问整段 ⇒ 100 亿次加法。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
int m;
cin >> m;
while (m--) {
int l, r;
cin >> l >> r;
long long sum = 0;
for (int i = l; i <= r; i++) sum += a[i]; // ← 同一段被反复加
cout << sum << '\n';
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 题面把分数明码标价了:50% 的数据 n, m ≤ 1000

n = m = 1000 时暴力最多做 100 万次加法 —— 稳过

第 45 章那条考场策略在这儿是白纸黑字的: 不会正解的时候,暴力不是废纸,是分数。 而且这道题的暴力十行以内,写它花不了三分钟。

★ 更实用的一条:先写暴力,再写正解,然后拿暴力当标准答案对拍 —— 这道题的暴力还兼职当了本页对拍器里那一栏「一定对的」。

3第 ② 版:前缀和 —— 那个哨兵是为 l = 1 准备的

p8218.cpp第 ② 版:前缀和(能 AC)
// P8218【深进1.例1】求区间和 —— 能 AC 的那一版:一维前缀和
//
// s[i] = a[1] + a[2] + … + a[i],于是 [l, r] 的和就是 s[r] - s[l-1]。
//
// ⚠ 两处下标:
// ① **前缀和数组从 1 开始存**(s[0] = 0 当哨兵)。这样 l = 1 时 s[l-1] = s[0] = 0,
// 不用写任何特判。要是把 a 存在 0..n-1 上,`s[l-1]` 在 l = 1 时就是 s[-1] —— 越界。
// ② 询问给的 l、r 都是 1 基的,别自己再减一次。
//
// ★ 上界算一遍(第 45 章那一课):n ≤ 1e5、a_i ≤ 1e4 ⇒ 总和最大 1e9,
// 而 int 的上限是 2 147 483 647 —— **恰好装得下,余量只有 2.1 倍**。
// ⇒ 这题 int 能用,但值得知道自己站在哪儿:a_i 的上界再大一位,int 就废了。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<long long> s(n + 1, 0); // s[0] = 0 —— 这个哨兵省掉了 l = 1 的特判
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
s[i] = s[i - 1] + x;
}
int m;
cin >> m;
while (m--) {
int l, r;
cin >> l >> r;
cout << s[r] - s[l - 1] << '\n'; // ⚠ 用 '\n' 不用 endl(endl 每次都刷缓冲区)
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 唯一能写错的地方:`s[l-1]`
vector<long long> s(n + 1, 0);      // ★ s[0] = 0,这个哨兵省掉了所有特判
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
cout << s[r] - s[l - 1] << '\n';

l = 1s[l-1] 就是 s[0] —— 只要数组是 1 基存的、s[0] 是 0,它自然成立。 反过来,把 a 存在 0 … n-1 上再照抄这个式子,l = 1 就变成 s[-1]:越界。

1 基存储 + 一个 0 号哨兵,这道题就没有边界特判了。 ★ 这是本书反复出现的同一件事: 第 5 章 P1618 换成交叉相乘、第 5 章 P1563 把四个 if 缩成一句 —— 能改成「那类错误不存在」的写法,就别留着「小心别写错」。

4★ 对拍:小数据随机,和小数据 + 专挑边界,不是一回事

p8218Gen.cpp生成器:两个档位
参数是「种子 档位」。档位 1 里一多半的询问贴着 l = 1。
// 数据生成器(P8218 对拍用):`./p8218Gen <seed> [level]`
//
// level 0(默认)随机 n <= 12、m <= 8,a_i <= 20,区间随机
// level 1 专挑边界:一半的询问是 l = 1(会用到 s[0] 那个哨兵)、
// l = r(单个数)、以及 l = 1 且 r = n(整段)
//
// ★ 为什么要有 level 1:这道题唯一能写错的地方就是 `s[l-1]` 那个下标,
// 而它**只在 l = 1 时才露头**。顺手随机的话,l = 1 的概率是 1/n ——
// n = 12 时三百轮当然撞得上,可换成 n = 1e5 的规模就永远撞不上了。
// ⇒ 「小数据随机」和「小数据 + 专挑边界」不是一回事,正文第 ④ 步有那张表。
#include <bits/stdc++.h>
using namespace std;
static mt19937 rng;
static int ri(int lo, int hi) { return lo + (int)(rng() % (unsigned)(hi - lo + 1)); }
int main(int argc, char** argv) {
unsigned seed = (argc > 1) ? (unsigned)atoi(argv[1]) : 1;
int level = (argc > 2) ? atoi(argv[2]) : 0;
rng.seed(seed);
int n = ri(1, 12), m = ri(1, 8);
printf("%d\n", n);
for (int i = 1; i <= n; i++) printf("%d%c", ri(1, 20), i == n ? '\n' : ' ');
printf("%d\n", m);
for (int i = 0; i < m; i++) {
int l, r;
if (level == 1 && ri(0, 2) == 0) { l = 1; r = n; } // 整段
else if (level == 1 && ri(0, 1) == 0) { l = 1; r = ri(1, n); }// 贴着左端
else if (level == 1) { l = r = ri(1, n); } // 单个数
else { l = ri(1, n); r = ri(l, n); }
printf("%d %d\n", l, r);
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

check:viz 每档 300 轮,暴力当标准答案 —— 两档都是 300 / 300 逐字节相同

★ 那为什么还要档位 1

因为「随机撞得上」这件事是有前提的:s[l-1] 那个 bug 只在 l = 1 时露头, 而顺手随机的 l 落在 1 上的概率是 1/n

  • 生成器造 n ≤ 12 ⇒ 每次约 8% ,300 轮里几十次,撞得上
  • 可要是照着题面规模造 n = 10⁵ ⇒ 十万分之一,300 轮基本撞不上

⇒ 同一个 bug、同一个生成器写法,抓得到还是抓不到,取决于你把 n 调到多大。 这正是第 5 章 P1618 那一页的同一课: 对拍的强度不看轮数,看生成器造得出什么。 ★ 顺手的办法:边界档单开一个 level,别指望概率。

5★ 换尺子 + 那笔一定要提前算的账

p8218Count.cpp两笔账:加法次数 / int 够不够
// 两笔账:① 两种写法各做了多少次加法;② int 到底够不够用
//
// 用法:./p8218Count <n> <m> 人话版
// ./p8218Count <n> <m> csv 只打 `键,值`,给 check:viz 用
//
// ① 尺子:**最坏形状**(每个询问都是 1..n)下
// 暴力 = n × m 次加法;前缀和 = n 次(预处理)+ m 次(每问一次减法)。
// ② 上界:题面给的是 n ≤ 1e5、a_i ≤ 1e4 ⇒ 总和最大 1e9。
// ★ 这一笔一定要在写代码之前算,而不是等 WA 了再算 —— 第 45 章那一课。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
long long n = (argc > 1) ? atoll(argv[1]) : 100000;
long long m = (argc > 2) ? atoll(argv[2]) : 100000;
bool csv = (argc > 3 && string(argv[3]) == "csv");
long long brute = n * m; // 最坏形状:每次都加满一整段
long long fast = n + m;
long long maxSum = 100000LL * 10000; // 题面上界:n × max(a_i)
long long intMax = 2147483647LL;
if (csv) {
printf("n,%lld\nm,%lld\nbrute,%lld\nfast,%lld\nratio,%lld\n"
"maxSum,%lld\nintMax,%lld\nfitsInt,%d\nheadroom,%lld\n",
n, m, brute, fast, brute / fast, maxSum, intMax,
maxSum <= intMax ? 1 : 0, intMax / maxSum);
} else {
printf("n = %lld, m = %lld(最坏形状:每个询问都是 1..n)\n", n, m);
printf("① 暴力 %lld 次加法\n", brute);
printf("② 前缀和 %lld 次(预处理 n 次 + 每问一次减法)\n", fast);
printf("⇒ 差 %lld 倍\n\n", brute / fast);
printf("题面上界:n <= 100000、a_i <= 10000 ⇒ 区间和最大 %lld\n", maxSum);
printf("int 的上限 %lld\n", intMax);
printf("⇒ %s,余量 %lld 倍\n", maxSum <= intMax ? "装得下" : "装不下", intMax / maxSum);
}
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
n = m = 10⁵(最坏形状:每个询问都是 1..n
① 暴力的加法次数 10 000 000 000
② 前缀和 200 000
比值 50 000 倍

秒表(B 机:原生 Ubuntu 7.0.0-30 / i5-10210U 8 线程 / 18 GB,2026-08-26,独占):

数据 ① 暴力 ② 前缀和
最坏形状(每问都是整段) 6.20 秒 0.03 秒
随机区间(平均长度约 n/3 1.55 秒 0.04 秒

⚠ 同样是 n = m = 10⁵「最坏」和「随机」差 4 倍 —— 要回答「它到底能不能过」,就得把旋钮拧到头(第 5 章 P1563 那条)。

★ 而这笔账要在写代码之前算:int 够不够
n ≤ 10⁵,a_i ≤ 10⁴  ⇒  区间和最大 10⁹
int 的上限                    2 147 483 647
⇒ 装得下,余量 2 倍

这道题 int 能用 —— 但你得知道自己站在哪儿:a_i 的上界再大一位(10⁵), int 就直接废了。⇒ 第 45 章那一课: 把数据范围乘一遍,是读题的一部分,不是 WA 之后的补救。 ★ 本页正解仍然用了 long long —— 不是因为需要,是因为这一笔的余量不值得省

6两个版本并排

版本 做法 样例 n = m = 10⁵ 最坏 分数
p8218Brute 每次现加 6.20 秒 50 分n, m ≤ 1000 那一半)
p8218 前缀和 0.03 秒 100 分
和算法无关、但值得一提的一条

m 有 10 万行输出,'\n' 别用 endlendl 每次都刷缓冲区)。 本机实测这道题上差 0.05 秒 vs 0.03 秒 —— 这一题无所谓, 但行数再多一个量级,它就是能把你卡掉的那 0.2 秒。

这一页记住三句话
  1. 题面把暴力的分数写在数据范围里了(50% 的数据 n, m ≤ 1000)。 不会正解也先写暴力,它还能当对拍的标准答案。
  2. 1 基存储 + s[0] = 0 哨兵l = 1 的特判就不存在了。
  3. ★★ 对拍的强度不看轮数,看生成器造得出什么。 同一个 bug, n ≤ 12 时随机就能撞上,n = 10⁵ 时得单开一个边界档 —— 别指望概率。