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 分
// 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;}点「运行 ▶」看结果
n = m = 1000 时暴力最多做 100 万次加法 —— 稳过。
⇒ 第 45 章那条考场策略在这儿是白纸黑字的: 不会正解的时候,暴力不是废纸,是分数。 而且这道题的暴力十行以内,写它花不了三分钟。
★ 更实用的一条:先写暴力,再写正解,然后拿暴力当标准答案对拍 —— 这道题的暴力还兼职当了本页对拍器里那一栏「一定对的」。
3第 ② 版:前缀和 —— 那个哨兵是为 l = 1 准备的
// 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;}点「运行 ▶」看结果
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 = 1 时 s[l-1] 就是 s[0] —— 只要数组是 1 基存的、s[0] 是 0,它自然成立。
反过来,把 a 存在 0 … n-1 上再照抄这个式子,l = 1 就变成 s[-1]:越界。
⇒ 1 基存储 + 一个 0 号哨兵,这道题就没有边界特判了。
★ 这是本书反复出现的同一件事:
第 5 章 P1618 换成交叉相乘、第 5 章 P1563 把四个 if 缩成一句 ——
能改成「那类错误不存在」的写法,就别留着「小心别写错」。
4★ 对拍:小数据随机,和小数据 + 专挑边界,不是一回事
// 数据生成器(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 逐字节相同。
因为「随机撞得上」这件事是有前提的:s[l-1] 那个 bug 只在 l = 1 时露头,
而顺手随机的 l 落在 1 上的概率是 1/n。
- 生成器造
n ≤ 12⇒ 每次约 8% ,300 轮里几十次,撞得上。 - 可要是照着题面规模造
n = 10⁵⇒ 十万分之一,300 轮基本撞不上。
⇒ 同一个 bug、同一个生成器写法,抓得到还是抓不到,取决于你把 n 调到多大。 这正是第 5 章 P1618 那一页的同一课: 对拍的强度不看轮数,看生成器造得出什么。 ★ 顺手的办法:边界档单开一个 level,别指望概率。
5★ 换尺子 + 那笔一定要提前算的账
// 两笔账:① 两种写法各做了多少次加法;② 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 那条)。
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' 别用 endl(endl 每次都刷缓冲区)。
本机实测这道题上差 0.05 秒 vs 0.03 秒 —— 这一题无所谓,
但行数再多一个量级,它就是能把你卡掉的那 0.2 秒。
- ★ 题面把暴力的分数写在数据范围里了(50% 的数据
n, m ≤ 1000)。 不会正解也先写暴力,它还能当对拍的标准答案。 - ★ 1 基存储 +
s[0] = 0哨兵,l = 1的特判就不存在了。 - ★★ 对拍的强度不看轮数,看生成器造得出什么。 同一个 bug,
n ≤ 12时随机就能撞上,n = 10⁵时得单开一个边界档 —— 别指望概率。