0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P8306,日期见页头。两边不一致时信原站。
题目描述
给定 n 个模式串 s₁, s₂, …, sₙ 和 q 次询问,每次询问给定一个文本串 tᵢ,
请回答 s₁ ~ sₙ 中有多少个字符串 sⱼ 满足 tᵢ 是 sⱼ 的前缀。
一个字符串 t 是 s 的前缀当且仅当从 s 的末尾删去若干个(可以为 0 个)连续的字符后与 t 相同。
输入的字符串大小写敏感。例如,字符串 Fusu 和字符串 fusu 不同。
输入格式
本题单测试点内有多组测试数据。
输入的第一行是一个整数,表示数据组数 T。
对于每组数据,格式如下:
第一行是两个整数,分别表示模式串的个数 n 和询问的个数 q。
接下来 n 行,每行一个字符串,表示一个模式串。
接下来 q 行,每行一个字符串,表示一次询问。
输出格式
按照输入的顺序依次输出各测试数据的答案。 对于每次询问,输出一行一个整数表示答案。
数据规模与约定
对于全部的测试点,保证 1 ≤ T, n, q ≤ 10⁵,且输入字符串的总长度不超过 3 × 10⁶。
输入的字符串只含大小写字母和数字,且不含空串。
说明:std 的 IO 使用的是关闭同步后的 cin/cout,本题不卡常。
时限 1 秒,内存 1048576 KB(= 1024 MiB)。
输入输出样例
输入
3 3 3 fusufusu fusu anguei fusu anguei kkksc 5 2 fusu Fusu AFakeFusu afakefusu fusuisnotfake Fusu fusu 1 1 998244353 9
输出
2 1 0 1 2 1
第一组:fusu 是 fusufusu 和 fusu 的前缀 ⇒ 2;anguei 只有它自己 ⇒ 1;kkksc 一个都没有 ⇒ 0。
★ 第二组就是题面那句「大小写敏感」的现场:问 Fusu 只有 Fusu 一个(1),
问 fusu 有 fusu 和 fusuisnotfake 两个(2)—— 而 AFakeFusu / afakefusu 一个都不算。
1★★★ 这道模板题的骨架,本章正文已经写完了 —— 会咬人的三处全在题面的数字里
第 50 章第 5 步那份 fast.cpp,就是这道题的原型(连「多组数据」都一样)。
所以这一页不重讲 Trie 是什么,它只做一件事:把真题题面上那几行数字,一行一行地乘一遍。
乘完之后有三处和正文不一样,而三处都足以让你 0 分:
| 正文那份 | 这道真题 | 后果 | |
|---|---|---|---|
| 字符集 | 26 个小写 | ⚠⚠ 62 个(大小写字母 + 数字),而且大小写敏感 | c - 'a' 会算出 −49(负下标写数组 = UB) |
| 内存 | 26 × 10⁶ × 4 = 99.2 MiB | ⚠ 62 × 3×10⁶ × 4 = 709.5 MiB | 题面给 1024 MiB —— 刚好够,不是慷慨 |
| 组数 | 几组 | ⚠ T 到 10⁵ |
memset 清空 ⇒ 74.4 TB 的抹写 |
★ 而这道题只问一个数(「以 t 为前缀的有几个」)⇒ 本章那两个计数,这里只用得上 cntPass。
cntEnd 一次都用不到 —— ⚠ 拿错了另一个,代码一样短、一样快,只是在回答另一道题。
// P8306【模板】字典树 —— 正解:Trie,O(总长)//// ★ 骨架和本章第 5 步那份 fast.cpp 一模一样,可这道**真题**上有三处要重新算一遍://// ① ⚠⚠ **字符集是 62 不是 26** —— 题面写着「只含大小写字母和数字」,// 而且专门补了一句「大小写敏感,Fusu 和 fusu 不同」。// ⇒ 拿 `c - 'a'` 配 26 列,'0' 会算出 -49(负下标写数组 = UB),'A' 算出 -32。// ② ⚠⚠ **内存要按 62 列重算**:3×10⁶ × 62 × 4 字节 = **709.5 MiB**。// 而这道题的内存限制是 **1048576 KB = 1024 MiB** —— 那不是慷慨,是刚好够。// (本章正文那张表算的是 26 列 × 10⁶ = 104 MB,这里是它的 7.15 倍。)// ③ ⚠ **多测**:T 可以到 10⁵,而 memset 整个 ch 是 709.5 MiB 的活 ——// T 遍就是 71 TB。⇒ 只清用过的那 tot + 1 个节点(clearUsed)。//// ★ 而这道题只问一个数:「有多少个 sⱼ 以 t 为前缀」= cntPass。// cntEnd 在这道题上一次都用不到 —— 本章那两个计数,这里只用了一个。
#include <bits/stdc++.h>using namespace std;
/* 节点数上限 = 所有字符串的总长 + 1(每个字符最多新开一个节点,再加一个根) */const int MAXN = 3000005;const int SIGMA = 62;
int ch[MAXN][SIGMA]; // ⚠ 709.5 MiB —— 这道题给了 1024 MiB,刚好够int cntPass[MAXN]; // 有多少个字符串路过这个节点int tot; // 已经用掉的最大节点编号(0 号是根)
/** ★ 62 个字符 → 0..61:数字 0~9、大写 10~35、小写 36~61 */inline int idx(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'Z') return c - 'A' + 10; return c - 'a' + 36;}
void insertWord(const string& s) { int u = 0; for (char c : s) { int k = idx(c); if (!ch[u][k]) ch[u][k] = ++tot; u = ch[u][k]; cntPass[u]++; }}
/** 以 s 为前缀的字符串有几个 */int ask(const string& s) { int u = 0; for (char c : s) { int k = idx(c); if (!ch[u][k]) return 0; // ⚠ 走不下去 ⇒ 一个都没有,立刻返回 u = ch[u][k]; } return cntPass[u];}
/** ★ 只清用过的那 tot + 1 个节点 —— 不是 memset 整个数组 */void clearUsed() { for (int i = 0; i <= tot; i++) { memset(ch[i], 0, sizeof(ch[i])); cntPass[i] = 0; } tot = 0;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int T; if (!(cin >> T)) return 0; while (T--) { int n, q; cin >> n >> q; for (int i = 0; i < n; i++) { string s; cin >> s; insertWord(s); } for (int i = 0; i < q; i++) { string p; cin >> p; cout << ask(p) << '\n'; } clearUsed(); } return 0;}点「运行 ▶」看结果
2第一版:一棵树都不建,挨个比一遍
所有人真实的第一反应,而且它没有任何一个 Trie 特有的坑:不用管字符集多大、不用算内存,
多测之间各存各的 vector 也天然不用清空。⇒ 正因为这样,它才配当对拍的标准答案。
一次询问要看 n 个串一眼 ⇒ 顶格 n = q = 10⁵ 就是 10¹⁰ 次「看一眼」,
而 Trie 是「每个字符走一步」,插入 + 查询各一遍 ⇒ 3 × 10⁶ 步。
要做多少次基本动作(顶格 n = q = 10⁵,串长 15) |
|
|---|---|
| ✗ 挨个比 | n × q = 10 000 000 000 |
| ★ Trie | (n + q) × 15 = 3 000 000 |
| 倍数 | ★ 3333 倍 |
本机实测(把规模缩到 n = q = 10⁴,也就是 1% 的工作量):挨个比 0.21 秒、Trie 0.02 秒
⇒ 按 O(n²) 外推回顶格是 21 秒,而时限 1 秒。
⚠ 而这道题一个部分分档都没有(题面只有「对于全部的测试点」那一行) ⇒ 第一版一分都拿不到。
3★★★ 第一个关卡:字符集是 62,而且大小写敏感
本章正文那份代码里写的是 int k = c - 'a';,配 ch[MAXN][26]。搬到这道题上:
'a' - 'a' = 0 ✓
'A' - 'a' = -32 ✗ 负下标写数组 —— UB
'0' - 'a' = -49 ✗ 同上⇒ 这不是 WA,是未定义行为:它可能给出一个荒唐的答案,也可能一声不吭地跑对
(第 47 章 P1598 上撞过一模一样的事:cnt[-19] 写出去了,答案却完全正确)。
编译器不会说一个字,-Wall 也不会。
数字 '0'..'9' → 0 .. 9
大写 'A'..'Z' → 10 .. 35
小写 'a'..'z' → 36 .. 61⚠ 千万别顺手 tolower 一下:题面专门加粗写着「大小写敏感,Fusu 和 fusu 不同」——
而官方样例的第二组数据(fusu / Fusu / AFakeFusu / afakefusu / fusuisnotfake)
就是照着这一条挑的,一测就死。
4★★ 第二个关卡:这棵树占多少内存 —— 而 1024 MiB 不是慷慨,是刚好够
// P8306 的三笔账:内存、清空、次数 —— 全部用**机器无关**的量来数//// 用法:./p8306Count csv (给 check-viz.mjs 读的 key,value)// ./p8306Count table (给人看的表)//// ★ 为什么这一份不真去建那棵 62 叉树:**用不着**。// Trie 的节点数 = 所有字符串的**不同前缀个数**,而把字符串排好序之后// 它就等于 `Σ (len_i − lcp(s_i, s_{i−1}))` —— 一句话就数完了,// 不用为了数一个数去申请 709.5 MiB。// ⇒ 而这也正好是本章第 7 步那句话的另一种说法:**空间是「不同前缀的个数」**。
#include <bits/stdc++.h>using namespace std;
static mt19937 rng(20260910u);static int ri(int lo, int hi) { return lo + (int)(rng() % (unsigned)(hi - lo + 1)); }
static const char* AL62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/** 节点数 = Σ (len_i − lcp(排序后相邻两个)) */static long long trieNodes(vector<string> v) { sort(v.begin(), v.end()); long long nodes = 0; string prev; for (const string& s : v) { size_t l = 0; while (l < s.size() && l < prev.size() && s[l] == prev[l]) l++; nodes += (long long)s.size() - (long long)l; prev = s; } return nodes;}
/** 造总长为 total 的一批串:shape = long / short / low3 / share */static vector<string> makeShape(const string& shape, long long total) { vector<string> v; long long used = 0; while (used < total) { string s; if (shape == "long") { // 长串、62 个字符随便抽 int len = (int)min<long long>(15, total - used); for (int i = 0; i < len; i++) s += AL62[ri(0, 61)]; } else if (shape == "short") { // 短串、62 个字符 int len = (int)min<long long>(4, total - used); for (int i = 0; i < len; i++) s += AL62[ri(0, 61)]; } else if (shape == "low3") { // 只有 3 个字母 int len = (int)min<long long>(15, total - used); for (int i = 0; i < len; i++) s += (char)('a' + ri(0, 2)); } else { // share:都以同一段开头 int len = (int)min<long long>(15, total - used); s = string("prefixprefix").substr(0, min<size_t>(12, (size_t)len)); while ((int)s.size() < len) s += AL62[ri(0, 61)]; } used += (long long)s.size(); v.push_back(s); } return v;}
/** ★ 从 stdin 读一份**真的输入**,报「名单一共多少字符 / Trie 多少节点 / 62 列要多少 MiB」 */static int fromInput() { int T; if (!(scanf("%d", &T) == 1)) return 0; long long chars = 0, nodes = 0, words = 0, maxNodes = 0; static char buf[100005]; for (int t = 0; t < T; t++) { int n, q; if (scanf("%d %d", &n, &q) != 2) break; vector<string> v; for (int i = 0; i < n; i++) { if (scanf("%100004s", buf) != 1) break; v.push_back(buf); } for (int i = 0; i < q; i++) { if (scanf("%100004s", buf) != 1) break; } for (const string& s : v) chars += (long long)s.size(); words += (long long)v.size(); long long here = trieNodes(v); nodes += here; maxNodes = max(maxNodes, here); // ⚠ 多组之间树是清空的 ⇒ **峰值**是各组的最大值,不是总和 } printf("words,%lld\nchars,%lld\nnodes,%lld\nmaxnodes,%lld\nmib,%.1f\n", words, chars, nodes, maxNodes, (double)maxNodes * 62 * 4 / 1048576.0); return 0;}
int main(int argc, char** argv) { string mode = argc > 1 ? argv[1] : "table"; if (mode == "input") return fromInput(); const long long TOTAL = 3000000; // 题面顶格:输入字符串总长 3×10⁶ const long long MAXN = 3000005, SIGMA = 62;
vector<pair<string, string>> out; auto put = [&](const string& k, const string& v) { out.push_back({k, v}); }; auto num = [](long long x) { return to_string(x); };
/* ① 内存账:四种形状,节点数差多少 */ for (const string& sh : {string("long"), string("short"), string("low3"), string("share")}) { vector<string> v = makeShape(sh, TOTAL); long long nodes = trieNodes(v); put(sh + "_words", num((long long)v.size())); put(sh + "_nodes", num(nodes)); char buf[64]; snprintf(buf, sizeof(buf), "%.1f", (double)nodes * SIGMA * 4 / 1048576.0); put(sh + "_mib", buf); } { char buf[64]; snprintf(buf, sizeof(buf), "%.1f", (double)MAXN * SIGMA * 4 / 1048576.0); put("array_mib", buf); // 静态数组本身开多大 put("limit_mib", num(1048576 / 1024)); // 题面给的 1048576 KB snprintf(buf, sizeof(buf), "%.1f", (double)1000000 * 26 * 4 / 1048576.0); put("chapter_mib", buf); // 本章正文那张表:26 列 × 10⁶ snprintf(buf, sizeof(buf), "%.2f", ((double)MAXN * SIGMA) / (1000000.0 * 26)); put("vs_chapter", buf); // 这道真题是正文那笔账的几倍 }
/* ② 清空账:顶格 T = 10⁵ 组,每组平均只有 30 个字符 */ { long long T = 100000; long long perGroup = TOTAL / T; // 每组 30 个字符 ⇒ 至多 30 个节点 long long goodCells = T * (perGroup + 1) * (SIGMA + 1); // 只清用过的:(tot+1) × (62 列 + 1 个计数) long long badCells = T * MAXN * (SIGMA + 1); // memset 整个数组 put("clear_T", num(T)); put("clear_good", num(goodCells)); put("clear_bad", num(badCells)); put("clear_ratio", num(badCells / goodCells)); char buf[64]; snprintf(buf, sizeof(buf), "%.1f", (double)T * MAXN * SIGMA * 4 / 1e12); put("clear_tb", buf); // memset 一共抹写多少 TB }
/* ③ 次数账:顶格一组 n = q = 10⁵、串长 15 —— 暴力比几个字符,Trie 走几步 */ { long long n = 100000, q = 100000, len = 15; put("brute_steps", num(n * q)); // 每次询问至少要看 n 个串一眼 put("trie_steps", num((n + q) * len)); // 每个字符走一步,插入 + 查询各一遍 put("steps_ratio", num((n * q) / ((n + q) * len))); }
/* ④ 输出账:题面一个字都没提,可它是真的大 */ { /* 让答案尽量多位:n = 10⁵ 个 "a",q = 10⁵ 次问 "a" ⇒ 每行 "100000\n" = 7 字节。 一组用掉 2×10⁵ 个字符,总长 3×10⁶ ⇒ 最多 15 组。 */ long long groups = TOTAL / (100000 + 100000); long long lines = groups * 100000; put("out_groups", num(groups)); put("out_lines", num(lines)); put("out_bytes", num(lines * 7)); char buf[64]; snprintf(buf, sizeof(buf), "%.1f", (double)lines * 7 / 1048576.0); put("out_mib", buf); }
if (mode == "csv") { for (auto& kv : out) printf("%s,%s\n", kv.first.c_str(), kv.second.c_str()); } else { for (auto& kv : out) printf(" %-16s %s\n", kv.first.c_str(), kv.second.c_str()); } return 0;}点「运行 ▶」看结果
节点数上限 = 所有字符串的总长 + 1(每个字符最多新开一个节点)⇒ MAXN = 3×10⁶ + 5。
ch[3000005][62] × 4 字节 = 744 001 240 字节 = 709.5 MiB
cntPass[3000005] × 4 字节 = 11.4 MiB
---------------------------------------------------------
合计 720.9 MiB (限制 1024 MiB)⇒ 余量 1.42 倍。 ★ 而这正是这道题为什么给 1024 MiB ——
换成常见的 256 MiB,这个写法当场 MLE(差 2.8 倍),只能改用 map 存儿子或者换儿子-兄弟表示法
(本章第 7 步那三条出路)。
四种形状,输入字符串总长都是 3 × 10⁶(名单和询问各占多少不同):
| 形状 | 名单里的字符数 | Trie 峰值节点数 | 算出来的 62 列 | 实测峰值内存 | 实测耗时 |
|---|---|---|---|---|---|
fat(3×10⁶ 个字符全在名单里) |
3 000 000 | 2 785 204 | 658.7 MiB | ⚠ 673.6 MiB | 0.59 秒 |
long(名单和询问对半) |
1 500 000 | 1 285 180 | 304.0 MiB | 313.0 MiB | 0.25 秒 |
share(都以同一段长前缀开头) |
1 500 000 | 85 650 | 20.3 MiB | 24.6 MiB | 0.02 秒 |
many(10⁵ 个小组,每组 30 个字符) |
1 500 000 | 15 | 0.0 MiB | 4.1 MiB | 0.03 秒 |
★ 算出来的 658.7 MiB 和实测的 673.6 MiB 差着 15 MiB,而那 15 MiB 是能对上账的:
cntPass 被碰到的那 2 785 204 格是 10.6 MiB,进程起步本身 4.1 MiB(many 那一行)
⇒ 658.7 + 10.6 + 4.1 = 673.4。
⚠⚠ 而最后一列最值钱:三行数据的总长一模一样,耗时差 29 倍、内存差 164 倍,
而两列几乎成正比(约 0.85 毫秒每 MiB)
⇒ ★★★ 这道题的正解,时间几乎全花在「第一次碰到那 673 MiB」上 ——
fat 那 0.59 秒里,真正在走 Trie 的部分连零头都不到。
⇒ 顶格余量只剩 1.7 倍,而吃掉它的是内存,不是算法。
正文量的是「26 列 × 总长 10⁶」= 99.2 MiB(正文写的 104 是 MB,这里是 MiB,同一个数)。 这道真题是 62 列 × 3×10⁶ ⇒ 7.15 倍。
⇒ ★★ 「正文用过这道题」不等于「解析页没得写」 —— 要把题面上每一行数字对一遍 (第 46 章 P1469 那条:正文把题面改小了 32 倍, 于是正文里写着「✓ 能过」的版本在真题上是个 MLE)。
5★★★ 第三个关卡:多测清空 —— 而 memset 那一版的答案一个字节都不差
sizeof(ch) = 3000005 × 62 × 4 = 744 001 240 字节 = 709.5 MiB
T 最大 10⁵ ⇒ 74.4 TB 的抹写清空动作碰了多少格(顶格 T = 10⁵) |
峰值内存 | 400 字节的输入上实测 | |
|---|---|---|---|
★ 正解(只清用过的 tot+1 个) |
195 300 000 | 4.1 MiB | 0.00 秒 |
✗ memset 整个数组 |
18 900 031 500 000 | ⚠ 725 MiB | 0.51 秒 |
| 倍数 | ★ 96 774 倍 | 177 倍 | — |
★★ 最右边那一列才是最刺眼的:输入只有 400 字节,它照样花掉半秒、占掉 725 MiB。
再往上加 30 组,耗时从 0.51 涨到 0.84 秒 ⇒ 每组约 11 毫秒
⇒ 顶格 T = 10⁵ 大约要 1100 秒(时限 1 秒)。
★ 而正解那个全局数组不碰就不占物理内存 —— many 那一档只用 4.1 MiB。
这是操作系统给的便宜,很多人不知道。
它和正解逐字节相同。300 轮也好、三万轮也好,对拍那一列永远是精确的 0。
⇒ 唯一的出路是换一把尺子:数「清空动作碰了多少格」(上面那张表的第一列)。 ★ 而那把尺子是机器无关的 —— 换台机器、换个编译器,96 774 这个数一个不变。
⚠ 顺带说清楚一件事:这一页的 300 轮对拍根本没让它跑 ——
memset 一次 709.5 MiB,300 轮 × 每轮 6 组就是 1.3 TB。
⇒ 它只在几组数据上验了「逐字节相同」,剩下的靠上面那句乘法。
它算的是:前 i 组的所有字符串堆在一起之后的答案。
⇒ 于是一切都是白送的推论:第一组永远对(还没有别的组);
T = 1 那一档是它结构性的精确的 0;
而官方样例第二组问 fusu 时它打出 4(把第一组的 fusufusu / fusu 也算了进去),
正解是 2 ⇒ 样例一测就死。
6★ 第四个坑:走不下去的时候必须立刻停
本章第 4 步那个 Callout 说过这件事:ch[u][k] 是 0,而 0 号节点就是根 ——
少了那句 return 0,u 就掉回了根,后面的字符又从根往下走。
⇒ ★ 它答出来的是 t 的某个后缀的答案。
把五个错法在官方样例上各跑一遍:
| Nocase① | End② | Miss③ | Noclear④ | 试金石(恒输出 0) | |
|---|---|---|---|---|---|
| 官方那三组样例 | 死 | 死 | ★ 放过 | 死 | 死 |
| 对拍的「顺手档」(300 轮) | 0 | 0 | ★ 296 | 0 | 1 |
⇒ ★★★ 两个关卡完全互补:样例挡住的三个,顺手写的对拍一个都抓不到; 而顺手写的对拍唯一抓得住的那个,正好是样例唯一放过的。
★ 样例为什么放过它:那三组数据里的每个询问,要么整条路都走得通(fusu、Fusu、9),
要么第一个字符就没路(kkksc)—— 而这个 bug 要的是「走到中间才断,
而断掉之后剩下那一截又恰好在树上」。样例里一次都没出现。
⇒ 这是第 49 章 P2957那条「两个关卡各被一样东西挡住」的更干净版本: 那次是一对一,这次是三对一,而且完全互补。
7★ 对拍:五个错法 + 一份「什么都不做」
| 档位 | ①Nocase | ②End | ③Miss | ④Noclear | ⑤Memset | 试金石 |
|---|---|---|---|---|---|---|
0 顺手写法(T = 1、全小写、询问也是随机串) |
0 | 0 | 296 | 0 | 0 | ⚠ 1 |
1 + 多组数据(T = 3~6) |
0 | 2 | 300 | ⚠ 7 | 0 | 2 |
| 2 + 只差大小写的复制品 + 数字 | 300 | 2 | 300 | 1 | 0 | 300 |
| 3 + 从短前缀派生 + 一半询问命中 | 0 | 300 | 283 | 223 | 0 | 300 |
| 4 最终档 = 1 + 2 + 3 | 300 | 300 | 241 | 221 | 0 | 300 |
★★★ 第一行是这张表的全部价值:顺手写的那一档,四个真错法里三个是精确的 0, 而「什么都不做」300 轮只被抓 1 次。
⇒ 三个 0 各有一行不用跑程序的证明:
- ①Nocase:全小写 ⇒
tolower是恒等变换; - ②End:随机长串几乎不共享前缀 ⇒
cntPass和cntEnd处处相等; - ④Noclear:
T = 1⇒ 根本没有第二组(结构性的 0,加多少轮都没用)。
⚠ 而试金石那个 1 更狠:顺手写的询问串本来就不在名单里,正解的答案本来就是 0 —— ⇒ 「一致有两种:都算对了,和都没算」在这一页是「都答 0」。
档 1 已经有 3~6 组了,可 ④Noclear 只被抓 7 次。 道理和上面那个试金石是同一个:上一组漏进来的字符串,只有在「被这一组问到」的时候才会露馅, 而顺手写的询问串谁也问不中。
⇒ 档 3 把「一半询问取自名单里某个串的前缀」加上去,同一列立刻变成 223。 ★★ 这是「抓不到时别加轮数,去想那条线在哪儿」的又一次: 那条线不是「有几组数据」,是「问得中问不中」。
8⚠ 最后一笔账:题面说「本题不卡常」—— 那句话有没有主语
// P8306 的 I/O 账:题面说「本题不卡常」—— 那句话有没有主语?//// 用法:./p8306Io read <sync|nosync|scanf|fread> <输入文件>// ./p8306Io write <endl|nl|endlfast|nosync|printf|buf> <输入文件> [答案文件] [计时文件]// (write 的六种:默认同步 + endl / 默认同步 + '\n' / **关同步 + endl** /// 关同步 + '\n' / printf / 自写缓冲)// ★★ 那个「关同步 + endl」不是凑数的 —— [第 48 章 P3375](/sol/p3375/) 量到// 「endl → '\n' 值 8.8 倍」,而这道题上只值 1.1 倍。差别就藏在**有没有关同步**里。// ⚠ 外加两个**对照**:none / nonefast —— 一模一样地做完这道题,只是**一个字都不打**。// ★ 没有它们,这张表量的是「读 + 算 + 写」,而这道题的读入有 3×10⁶ 个 token,// 默认 cin 在那一侧的开销比整个输出还大,会把 endl 那一项整个淹掉// ([第 47 章 P1308](/sol/p1308/) 那条:**量什么都要先问一句「这个数里有没有别人的份」**)。// 两条都打一行 `mode,毫秒,读到的串数,读到的字节数,写出的字节数`:// 给了「计时文件」就写进那个文件,否则写 stderr。// ⚠ 答案本身走 stdout(默认就是屏幕)—— 给了「答案文件」就 freopen 到那儿去,// 这样 stdout 上那 10 MiB 才不会跟着计时一起被读走。//// ⚠ 一律用 freopen 从**文件**读 —— 评测机就是这么干的。// (第 47 章 [P1308] 那一跤:从 node 的管道读会多出一截**加性**开销,// 四种读法各自多出来的都不到 1 毫秒,可占比从 8% 到 50% 上下,// **只有最快的那一版看得见它**。)
#include <bits/stdc++.h>#include <sys/time.h>using namespace std;
static double nowMs() { struct timeval tv; gettimeofday(&tv, nullptr); return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;}
const int MAXN = 3000005, SIGMA = 62;static int ch[MAXN][SIGMA];static int cntPass[MAXN];static int tot;
inline int idx(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'Z') return c - 'A' + 10; return c - 'a' + 36;}
/* ---- 自写缓冲:读 ---- */static char inbuf[1 << 22];static size_t inLen = 0, inPos = 0;static inline int gc() { if (inPos == inLen) { inLen = fread(inbuf, 1, sizeof(inbuf), stdin); inPos = 0; if (!inLen) return -1; } return inbuf[inPos++];}static bool readTokenFast(string& s) { int c = gc(); while (c == ' ' || c == '\n' || c == '\r') c = gc(); if (c < 0) return false; s.clear(); while (c > ' ') { s += (char)c; c = gc(); } return true;}
/* ---- 自写缓冲:写 ---- */static char obuf[1 << 22];static size_t oPos = 0;static inline void flushOut() { fwrite(obuf, 1, oPos, stdout); oPos = 0; }static inline void writeIntFast(int x) { if (oPos + 16 > sizeof(obuf)) flushOut(); if (!x) obuf[oPos++] = '0'; else { char t[12]; int k = 0; while (x) { t[k++] = (char)('0' + x % 10); x /= 10; } while (k) obuf[oPos++] = t[--k]; } obuf[oPos++] = '\n';}
int main(int argc, char** argv) { string what = argc > 1 ? argv[1] : "read"; string mode = argc > 2 ? argv[2] : "nosync"; if (argc > 3 && !freopen(argv[3], "r", stdin)) return 1; if (argc > 4 && !freopen(argv[4], "w", stdout)) return 1; const char* statPath = argc > 5 ? argv[5] : nullptr;
long long strs = 0, bytes = 0, outBytes = 0; auto report = [&](const char* m, double ms) { FILE* f = statPath ? fopen(statPath, "w") : stderr; if (!f) return; fprintf(f, "%s,%.1f,%lld,%lld,%lld\n", m, ms, strs, bytes, outBytes); if (statPath) fclose(f); }; double t0 = nowMs();
if (what == "read") { /* 只读,什么都不算 —— 量的是纯读入 */ if (mode == "sync" || mode == "nosync") { if (mode == "nosync") { ios::sync_with_stdio(false); cin.tie(nullptr); } string s; while (cin >> s) { strs++; bytes += (long long)s.size(); } } else if (mode == "scanf") { static char buf[10005]; while (scanf("%10004s", buf) == 1) { strs++; bytes += (long long)strlen(buf); } } else { string s; while (readTokenFast(s)) { strs++; bytes += (long long)s.size(); } } report(mode.c_str(), nowMs() - t0); return 0; }
/* what == write:真做一遍这道题,只换「怎么把答案打出去」 */ if (mode != "endl" && mode != "nl" && mode != "none") { ios::sync_with_stdio(false); cin.tie(nullptr); }
int T; if (!(cin >> T)) return 0; while (T--) { int n, q; cin >> n >> q; for (int i = 0; i < n; i++) { string s; cin >> s; strs++; bytes += (long long)s.size(); int u = 0; for (char c : s) { int k = idx(c); if (!ch[u][k]) ch[u][k] = ++tot; u = ch[u][k]; cntPass[u]++; } } for (int i = 0; i < q; i++) { string p; cin >> p; strs++; bytes += (long long)p.size(); int u = 0, ans = 0; bool ok = true; for (char c : p) { int k = idx(c); if (!ch[u][k]) { ok = false; break; } u = ch[u][k]; } if (ok) ans = cntPass[u]; { int x = ans; int d = 1; while (x >= 10) { x /= 10; d++; } outBytes += d + 1; } if (mode == "none" || mode == "nonefast") { /* 对照:什么都不打 */ } else if (mode == "endl" || mode == "endlfast") cout << ans << endl; else if (mode == "printf") printf("%d\n", ans); else if (mode == "buf") writeIntFast(ans); else cout << ans << '\n'; } for (int i = 0; i <= tot; i++) { memset(ch[i], 0, sizeof(ch[i])); cntPass[i] = 0; } tot = 0; } if (mode == "buf") flushOut(); cout.flush(); fflush(stdout); report(mode.c_str(), nowMs() - t0); return 0;}点「运行 ▶」看结果
读 3 000 011 个字符(fat 那一档,10⁵ 个串) |
毫秒(3 次取中位数) |
|---|---|
默认 cin |
31.5 |
关同步 cin |
3.5 |
scanf |
5.6 |
| 自写缓冲 | 6.8 |
⇒ 倍数是 9.0 倍,可最慢的那一种也只吃掉时限的 3.2%。 ★ 倍数跨题几乎不变,变的是绝对时间,而分数线在绝对时间上 (第 38 章 P1972 那条)—— 这道题的读入是真的不卡常。
先算输出能有多大 —— 题面一个字都没提:
让每行答案都是 6 位数(n = 10⁵ 个 a,q = 10⁵ 次问 a),一组用掉 2×10⁵ 个字符
⇒ 总长 3×10⁶ 最多摆 15 组 ⇒ 1 500 000 行 × 7 字节 = 10 500 000 字节(10.0 MiB)。
本机实测(3 次取中位数,out 那一档):
| 打法 | 端到端 | 减去对照 = 净写 |
|---|---|---|
| 对照:默认同步、一个字都不打 | 164 | — |
| 对照:关同步、一个字都不打 | 50 | — |
默认同步 + endl |
389 | 225 |
默认同步 + '\n' |
386 | 222 |
关同步 + endl |
265 | 215 |
关同步 + '\n' |
90 | 40 |
printf |
95 | 45 |
| 自写缓冲 | 64 | 14 |
⇒ ★★★ 同一句「把 endl 换成 '\n'」:关了同步值 5.4 倍,没关同步只值 1.01 倍。
而第 48 章 P3375 上量到的是 8.8 倍 ——
⇒ ★★ 那句话的主语是「你有没有关同步」,不是「这道题输出多大」。
★ 机理说得清:endl 比 '\n' 多做的事只有一件 —— 每行冲一次缓冲,
而「冲一次」贵不贵,取决于那个缓冲里攒着多少东西。
关了同步,cout 自己攒着一大块,每行冲一次就等于把它拆成 150 万次小写;
同步开着的时候 cout 根本没在攒(每个字符直接交给 stdio),所以没什么可冲的
—— 这一点从表里也读得出来:同步开着时光是 '\n' 那一版就已经比关同步慢 5.6 倍。
⚠⚠ 而这张表必须有那两行对照:不减掉它们,「默认同步 + endl」那 389 毫秒里 有 164 毫秒是读入的份 —— 那是第 47 章 P1308 那条 「量什么都要先问一句:这个数里有没有别人的份」的第四次现场 (前三次是 P3366 的 19 毫秒读入、P1469 的进程起步、P1308 的管道)。
9★ 哪一版就已经能过了
| 版本 | 能过吗 | 为什么 |
|---|---|---|
| ✗ 挨个比 | 一分不给 | 顶格 10¹⁰ 次「看一眼」,外推 21 秒;⚠ 这道题一个部分分档都没有 |
✗ memset 清空 |
一分不给 | 答案全对,T = 10⁵ 要 74.4 TB 的抹写(约 1100 秒) |
✗ 26 列 / tolower |
0 分 | 前者是负下标(UB),后者样例二一测就死 |
| ★ 正解 | ✓ | 顶格 fat 0.59 秒 / 1 秒、673.6 MiB / 1024 MiB |
⚠ 两个余量都不大(1.7 倍 / 1.5 倍),而吃掉它们的是同一件事:那 673 MiB。
⇒ 想要更宽的余量,办法在本章第 7 步那张表里(map 存儿子 / 儿子-兄弟表示法),
代价是查一次多一个 log 或一条兄弟链 —— 这道题用不着,但要知道它在那儿。
这道模板题真正在考的,是「把题面上那几行数字乘一遍」的习惯 ——
Trie 本身第 50 章已经写完了,而这一页的三个关卡(62 列 / 709.5 MiB / T = 10⁵)
全部躺在「数据规模与约定」那三行里,一行都不在算法里。