CSP-S 2026 复赛突击教材
本教材由押题报告扩展而来,覆盖 8 个最可能考的专题。每个专题包含:考点精讲模板代码(可复制)推荐练习 + 详细解答。
押题为经验性推断,最终以 noi.cn 发布的《NOI 大纲(2025 年修订版)》与 CCF 官方通知为准。
一、考纲与考情速览
1.1 必须知道的官方信息
| 项目 | 内容 |
|---|---|
| 时间 | 2026-10-31(周六)14:30–18:30,共 4 小时 |
| 题量 | 4 道编程题,每题 100 分(官方通知原文:"每次认证有四个题目") |
| 环境 | NOI Linux,g++ 编译,文件 IO(freopen) |
| 考纲 | 《NOI 大纲(2025 年修订版)》,提高级知识点 ≈ 5–7 级 |
1.2 大纲 2025 修订版对提高级的变化(押题核心依据)
| 变化 | 知识点 | 对你的意义 |
|---|---|---|
| 新增 | Manacher(NOI 级下放到 7 级) | 最明确的扩考信号 → 专题② |
| 新增 | 多维动态规划(6 级) | 2025 T4 已考三维 DP → 专题⑦ |
| 新增 | 扫描线(7 级) | 矩形面积并 / 区间覆盖 → 专题⑧ |
| 新增 | bitset、离散化 | 状压与值域技巧 → 专题④ |
| 删除 | 次小生成树 | 可战略放弃 |
二、七年命题规律(2019–2025,34 题)
| 题位 | 难度走势 | 高频考点 | 近三年代表 |
|---|---|---|---|
| T1 | 黄 → 绿(门槛上升) | 贪心 / 模拟 / 枚举 | 2024 决斗(双指针)· 2025 社团招新(反悔贪心) |
| T2 | 绿 → 蓝 | DP / 状压 / 数据结构 | 2025 道路修复(2k 枚举 + MST) |
| T3 | 蓝 → 紫 | 字符串 / 哈希 / 大模拟 | 2023 结构体(大模拟)· 2025 谐音替换(AC 自动机) |
| T4 | 紫 → 黑(黑题连续两年) | 图 / 树 / 高维计数 | 2023 种树 · 2024 擂台游戏 · 2025 员工招聘(三维 DP) |
三条周期性规律
- 大模拟值得练习:2020、2023 年曾出现相关题型,样本不足以推断固定周期,也不能据此保证 2026 年出现(专题⑥)。
- 计数 DP 压轴连续化:2024、2025 连续两年,官方简评点名表扬(专题⑦)。
- 树上问题从不长期缺席:七年 5 年 T4 是树/图(专题⑤)。
三、七日突击计划(点击打卡)
专题① 反悔贪心 押题 T1 · ★★★★★
核心思想
贪心做错了怎么办?——允许"撤销"。先按普通贪心做,当出现约束不满足(或存在更优选择)时,把之前一个"性价比最低"的决定换掉。实现上通常是 堆 + 反悔标记,复杂度 O(n log n)。
三个经典变式(务必都见过)
- 带截止时间的收益最大化(P2949):按时间排序,依次"占坑",坑满了就踢掉收益最小的。
- 带配额上限的分配(2025 社团招新):先让每人去最优部门,超编的部门按"损失最小"把人转出去。
- 不相邻选取(P1484 种树):选了位置 i 获益 a[i],同时禁止选 i−1、i+1;反悔技巧是把 i 换成
a[i−1]+a[i+1]−a[i]的新节点放回堆中,实现"撤销 i 改选两个邻居"。
练习 P2949 Work Scheduling(详细解答)
题意:n 个工作(n ≤ 105),第 i 个截止时间为 di、报酬 pi。每个单位时间只能做一个工作,过期不能做,求最大报酬。
提示:先自己想 20 分钟 —— 排序 + 堆,考虑"反悔"发生在什么时候
详细解答(含正确性说明与代码)
为什么正确:按 d 排序后处理到 i 时,所有已选工作的截止时间都 ≤ di,因此"已选数量 ≤ di"是可行方案存在的充要条件(调度按截止时间从早到晚排即可)。堆中始终保持"当前 d 下的最优前 k 个收益",被弹出的恰是最该放弃的。复杂度 O(n log n)。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; scanf("%d", &n);
vector<pair<long long, long long>> a(n); // (d, p)
for (int i = 0; i < n; i++) scanf("%lld %lld", &a[i].first, &a[i].second);
sort(a.begin(), a.end()); // 按截止时间排序
priority_queue<long long, vector<long long>, greater<long long>> pq;
long long ans = 0;
for (int i = 0; i < n; i++) {
long long d = a[i].first, p = a[i].second;
pq.push(p); ans += p; // 先贪心地"接下"
if ((long long)pq.size() > d) { // 超过 d 个时刻 → 必须反悔一个
ans -= pq.top(); pq.pop(); // 踢掉收益最小的
}
}
printf("%lld\n", ans);
return 0;
}
走查一遍(4 个工作:(d,p) = (1,50)、(2,10)、(2,40)、(3,30)):
| 步骤 | 当前工作 | 操作 | 堆(小根堆) | ans |
|---|---|---|---|---|
| 1 | d=1, p=50 | 入堆,1 个 ≤ 1 ✓ | {50} | 50 |
| 2 | d=2, p=10 | 入堆,2 个 ≤ 2 ✓ | {10, 50} | 60 |
| 3 | d=2, p=40 | 入堆后 3 个 > 2 → 弹出最小的 10(反悔) | {40, 50} | 90 |
| 4 | d=3, p=30 | 入堆,3 个 ≤ 3 ✓ | {30, 40, 50} | 120 |
常见错误:① 忘开 long long(p ≤ 109,n 个加起来爆 int);② 误把条件写成 size() ≥ d——d 个时刻恰好能做 d 个工作。
练习 P1484 种树(进阶变式 · 解答要点)
解题要点(反悔贪心 + 双向链表)
题意:一排 n 个坑,收益 a[i],选中的坑不相邻,最多选 k 个,求最大收益。
关键反悔设计:每次取全局最大 a[i] 并选中;随后把 i 与左右邻居从候选中删除,并插入一个新位置,其权值为 a[i-1] + a[i+1] − a[i](可为负)。若之后选了这个新位置,等价于"撤销选 i,改选 i 的两个邻居"——恰好把不相邻约束修正回来。用双向链表维护左右邻居,大根堆 + 懒删除选最大。选 k 次取过程中最大累积和(收益为负时提前停止)。
复杂度:O(n log n)。对照:2025 社团招新的"按损失最小转出超编成员"本质就是这类反悔。
走查一遍(a = [3, 5, 2, 4],至多 k = 2 棵):
| 轮次 | 堆顶(坑位:权值) | 动作 | 累计收益 |
|---|---|---|---|
| 1 | ②:5 | 选②;①、③ 出局,插入"悔棋子"权值 3+2−5 = 0 | 5 |
| 2 | ④:4 | 选④(与②不相邻,合法) | 9 |
| 停止 | 悔棋子:0 | 堆顶 ≤ 0,再选不赚,停止("至多 k 棵"的体现) | 9 |
专题② Manacher 与回文 押题 T2/T3 · ★★★★★
核心知识
- Manacher:O(n) 求出以每个位置为中心的最长回文半径。技巧:插入
#统一奇偶长度;利用已知回文的对称性p[i] = min(R−i, p[2C−i])跳过重复比较。复杂度 O(n)。 - 回文相关 DP 常客:最少切割次数(区间 DP 或以 i 结尾转移)、回文子序列个数、回文划分计数。
- 考点组合趋势:Manacher/哈希 求出所有回文信息后,再套一层计数或贪心,这是"模板 + 思维"的典型命题方式。
练习 P3805【模板】Manacher(详细解答)
题意:给定字符串(|s| ≤ 1.1×107),求最长回文子串长度。
提示:如何用 # 统一奇偶?为什么 p[i] 可以从对称位置继承?
详细解答(含代码与易错点)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
string s; cin >> s;
string t = "^#";
for (char c : s) { t += c; t += '#'; }
t += '$'; // 哨兵保证 while 不越界
int n = t.size(), C = 0, R = 0, ans = 0;
vector<int> p(n, 0);
for (int i = 1; i < n - 1; i++) {
p[i] = (i < R) ? min(R - i, p[2 * C - i]) : 0;
while (t[i + p[i] + 1] == t[i - p[i] - 1]) p[i]++; // 暴力扩展
if (i + p[i] > R) { C = i; R = i + p[i]; } // 更新最右回文
ans = max(ans, p[i]);
}
cout << ans << "\n";
return 0;
}
走查一遍(s = "abba",t = "^#a#b#b#a#$",p[i] 为臂长,原串回文长度 = p[i]):
| i | t[i] | p[i] | 来源 | 对应原串回文 |
|---|---|---|---|---|
| 2 | a | 1 | 扩展 | "a" |
| 4 | b | 1 | 扩展 | "b" |
| 5 | #(正中) | 4 | 扩展 | "abba"(最长 ✓) |
| 6 | b | 1 | 镜像照抄(i′=4) | "b" |
| 7 | # | 0 | 镜像照抄(i′=3) | 无 |
| 8 | a | 1 | 镜像照抄(i′=2) | "a" |
易错点:① 哨兵字符必须与 # 及所有字母不同(^ 和 $);② 千万别写 cin 不关同步(1e7 数据会 TLE);③ 若题目问"原串中的回文位置",中心 i 在原串的坐标是 (i−2)/2,长度即 p[i]。
组合训练:会模板后做"回文划分计数"——f[i] = Σ f[j](若 s[j..i] 是回文),用 Manacher/哈希 O(n²) 或 Eertree O(n) 实现,是 2026 最可能的出题方式。
专题③ AC 自动机与多模式匹配 押题 T3 · ★★★★★
核心知识
- AC 自动机 = Trie + KMP 的 fail 指针:fail[u] 指向 u 的最长真后缀对应的 Trie 节点。BFS 建 fail 时顺便做"路径压缩"(
ch[u][c]直接指向转移目标),文本串匹配无需跳 fail 链。 - 统计出现次数:文本串每个位置在自动机上走一步并
cnt[cur]++;随后按 BFS 序逆序做cnt[fail[v]] += cnt[v](把 fail 树上子树和求出来),每个模式串末节点的 cnt 即出现次数——这正是"fail 树"思想的入门。 - 2025 真题把匹配对象抽象成"A{B D{C"式的变量拼接串,考的是"把题面建模成模式串"的能力,而非裸模板。
练习 P5357【模板】AC 自动机(二次加强版)详细解答
题意:n 个模式串(总长 ≤ 2×105)、一篇文本(≤ 2×106),输出每个模式串在文本中的出现次数(可重叠)。注意:不能暴力跳 fail(会被 aaaa... 卡成 O(n²)),必须用 fail 树统计。
提示:为什么暴力跳 fail 会 TLE?cnt 怎么从子节点汇到 fail 父节点?
详细解答(含完整代码)
#include <bits/stdc++.h>
using namespace std;
const int N = 200005; // 模式串总长 + 1
int ch[N][26], fail[N], idx = 0; // Trie(已做转移压缩)
int pos[N]; long long cnt[N]; int q[N];
int insert_(const char *s, int id) {
int u = 0;
for (; *s; s++) {
int v = *s - 'a';
if (!ch[u][v]) ch[u][v] = ++idx;
u = ch[u][v];
}
return pos[id] = u;
}
void build() { // BFS 建 fail + 转移压缩
int h = 0, t = 0;
for (int c = 0; c < 26; c++) if (ch[0][c]) q[t++] = ch[0][c];
while (h < t) {
int u = q[h++];
for (int c = 0; c < 26; c++) {
int v = ch[u][c];
if (v) { fail[v] = ch[fail[u]][c]; q[t++] = v; }
else ch[u][c] = ch[fail[u]][c];
}
}
}
int main() {
int n; scanf("%d", &n);
static char buf[200006]; // 单个模式串长度 ≤ 2e5
for (int i = 0; i < n; i++) { scanf("%s", buf); insert_(buf, i); }
build();
static char text[2000006]; scanf("%s", text);
int u = 0;
for (char *p = text; *p; p++) { u = ch[u][*p - 'a']; cnt[u]++; } // 只打标记
// q[0..idx-1] 为 BFS 序(共 idx 个非根节点),倒序累加 = fail 树自底向上求子树和
for (int i = idx - 1; i >= 0; i--) cnt[fail[q[i]]] += cnt[q[i]];
for (int i = 0; i < n; i++) printf("%lld\n", cnt[pos[i]]);
return 0;
}
走查一遍(模式串 {he, she},文本 "ushers"):
| 阶段 | 动作 | 结果 |
|---|---|---|
| 扫描 u | root 无此边,停在 root | — |
| 扫描 s、h、e | 沿 Trie 走到 she★ | cnt[she] = 1(只打标记,不跳 fail) |
| 扫描 r、s | 沿压缩转移继续走 | 无新标记 |
| 逆 BFS 序累加 | cnt[she] 上缴给 fail 父节点 | cnt[he] += 1 → he 出现 1 次 ✓("she" 里藏着 "he") |
复杂度:O(Σ|模式| × 26 + |文本|)。易错点:① 累加必须沿 BFS 序逆序(fail[u] 的 BFS 序小于 u);② 二次加强卡常,读入用 scanf/快读;③ cnt 开 long long(大文本同一位置可被统计多次)。
进阶对照(P3796 最出现次数的模式串):同样的 cnt 统计,最后扫一遍 end 节点取 max,注意"重复答案按输入序输出"。套路完全一致。
专题④ 状压 + 图连通 押题 T2 · ★★★★
核心思想
- 识别信号:题面出现"某类关键对象个数 k ≤ 15~20",其余规模很大 → 对关键对象做
2^k状压枚举,其余部分用多项式算法(MST / 最短路 / 贪心)。 - 2025 真题结构:k ≤ 10 个乡镇,每个乡镇有开通费 + 到各城市的连边费;2^k 枚举开通集合 → 并入新图跑 MST。优化点:基础图的 MST 树边只有 n−1 条,先把边排序到循环外,循环内只处理与所选乡镇相关的边,复杂度 O(2^k·(n+k)·k·α)。
- bitset(大纲新增):把 0/1 集合压成机器字做与/或/移位,典型用法:可达性传递
reach[v] |= reach[u]、0/1 背包按位优化、bitset 求二维偏序。
练习 P1171 售货员的难题(TSP,详细解答)
题意:n ≤ 20 个城市,完全图距离矩阵 w,从城市 1 出发经过每个城市恰好一次回到 1,求最短回路。
提示:状态怎么设计?为什么是 O(2^n·n²) 而不是 O(n!)?
详细解答(含代码)
#include <bits/stdc++.h>
using namespace std;
int n, w[20][20], dp[1 << 20][20];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) scanf("%d", &w[i][j]);
memset(dp, 0x3f, sizeof dp);
dp[1][0] = 0; // 只访问过起点
for (int S = 1; S < (1 << n); S++)
for (int i = 0; i < n; i++) {
if (!(S >> i & 1) || dp[S][i] == 0x3f3f3f3f) continue;
for (int j = 0; j < n; j++)
if (!(S >> j & 1))
dp[S | 1 << j][j] = min(dp[S | 1 << j][j], dp[S][i] + w[i][j]);
}
int ans = INT_MAX;
for (int i = 0; i < n; i++) ans = min(ans, dp[(1 << n) - 1][i] + w[i][0]);
printf("%d\n", ans);
return 0;
}
走查一遍(n = 3,距离矩阵 w:0↔1 为 1,1↔2 为 3,0↔2 为 2):
| 状态 S | 停在 i | f[S][i] | 来源 |
|---|---|---|---|
| {0} | 0 | 0 | 起点 |
| {0,1} | 1 | 1 | 0→1 |
| {0,2} | 2 | 2 | 0→2 |
| {0,1,2} | 2 | 1+3 = 4 | 0→1→2 |
| {0,1,2} | 1 | 2+3 = 5 | 0→2→1 |
| 答案(加回起点) | min(4 + w[2][0], 5 + w[1][0]) = min(4+2, 5+1) = 6 | ||
易错点:① 枚举 S 时内层直接跳过不合法状态(上面写法已保证);② 内存 2^20×20×4B ≈ 84MB,若 MLE 用滚动数组或把 dp 压成 dp[S] 为 vector;③ 实际考点常变形为"2^k 枚举 + MST"(道路修复型),TSP 是打基础。
bitset 练手(P2709 小 B 的询问 · 要点):莫队 + 桶统计,把"某颜色出现次数是否为奇数/平方贡献"用 bitset 维护,体会"位运算压集合"的写法即可,不必追求满分。
专题⑤ 树形 / 换根 DP 押题 T3/T4 常客 · ★★★★
核心知识
- 换根 DP(re-rooting)三板斧:① 第一次 DFS 自底向上算"以 1 为根"的答案;② 第二次 DFS 自顶向下把根从父亲"搬"到儿子:
ans[child] = ans[parent] + (n − sz[child]) − sz[child](距离和型);③ 注意取最小/最小时输出最小编号等细节。 - 树上常见模型:树上调度(二分天数 + 最晚期限贪心,2023 种树)、树上路径 k≤3 矩阵 DP(2022 数据传输)、子树内统计 + 全局数据结构。
练习 P1395 会议(详细解答)
题意:n 个村庄、n−1 条长度为 1 的路构成树。选一个村庄开会,使其他村庄到它的距离总和最小。输出(编号最小的)最优村庄编号与距离和。
提示:以 1 为根算出每个点的子树大小与距离和后,根从 u 移到儿子 v,距离和怎么变?
详细解答(含代码)
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
vector<int> g[N];
long long sz[N], f[N];
int n;
void dfs1(int u, int fa, long long d) { // 算子树大小与 f[1]
sz[u] = 1; f[1] += d;
for (int v : g[u]) if (v != fa) { dfs1(v, u, d + 1); sz[u] += sz[v]; }
}
void dfs2(int u, int fa) { // 换根递推
for (int v : g[u]) if (v != fa) {
f[v] = f[u] + (long long)(n - sz[v]) - sz[v];
dfs2(v, u);
}
}
int main() {
scanf("%d", &n);
for (int i = 1, x, y; i < n; i++) { scanf("%d%d", &x, &y); g[x].push_back(y); g[y].push_back(x); }
dfs1(1, 0, 0); dfs2(1, 0);
long long best = LLONG_MAX; int id = 0;
for (int i = 1; i <= n; i++)
if (f[i] < best) { best = f[i]; id = i; } // 顺序遍历 → 天然取最小编号
printf("%d %lld\n", id, best);
return 0;
}
走查一遍(链 1—2—3,n = 3):第一遍 dfs 得 sz = [3,2,1](1 号起)、f[1] = 深度和 0+1+2 = 3;第二遍换根:f[2] = f[1] + (3−2) − 2 = 2,f[3] = f[2] + (3−1) − 1 = 3。最小距离和 2 在村庄 2 ✓(中间点开会更公平,符合直觉)。
易错点:① 链形树下距离和最大约 n²/2 ≈ 1.3×109(n ≤ 5×104),已逼近 int 上限,稳妥起见用 long long;② 递归深度——若 n 达 106 级别需改成 BFS/迭代栈(考场上 NOI Linux 栈与内存限制相同,一般安全,但要有意识);③ "同值取最小编号"这类输出细节是真实考点。
进阶对照(P3047 Nearby Cows · 要点):求每个点周围 k 步内的权和——维护"向上 i 层祖先的子树和"的前缀套 DP(f[u][j] 覆盖子树内、g[u][j] 覆盖子树外),体会"子树内 + 子树外"的拆分。
专题⑥ 大模拟 押题 T3 · ★★★(周期到期)
核心方法论(大模拟拼的不是算法,是工程)
- 读题三遍,先抄规则清单:把题面所有规则逐条编号抄在草稿纸上,漏一条=全盘皆输。
- 状态设计成 struct:位置、方向、计数等打包,转移写成独立函数,主循环只做调度。
- 方向/循环偏移用同余:环形结构
(pos + step % n + n) % n;P1563 的坑:玩具朝向 face(0 朝内/1 朝外)× 指令 dir(0 左/1 右)——face == dir 时下标减小,否则增大(画图验证!这是本题唯一难点)。 - 对拍:写一个暴力 O(n²) 版本 + 随机数据生成器,本地对拍 10 分钟胜过盯着代码找 1 小时。
练习 P1563 玩具谜题(详细解答)
题意:n 个玩具围成圈,每个有朝向(0 朝内圈、1 朝外圈)和职业名;m 条指令(0 左数 s 个、1 右数 s 个),从第 0 个玩具出发,求最终玩具。
详细解答(含方向推导与代码)
方向推导(务必自己画圈验证):朝内的玩具,"左边"是逆时针方向(下标减小);朝外的玩具,"左边"反而是顺时针(下标增大)。归纳:face 与 dir 相同 → 下标减 s;不同 → 下标加 s。
走查一遍(n = 4,face = [0,1,0,1],从 0 号出发,指令:(0 左, s=1)、(1 右, s=2)):
| 指令 | 当前 pos(face) | dir 与 face | 移动计算 | 新 pos |
|---|---|---|---|---|
| 0 左数 1 | 0(朝内 0) | 相同 → 减 | (0 − 1 + 4) % 4 | 3 |
| 1 右数 2 | 3(朝外 1) | 相同 → 减 | (3 − 2 + 4) % 4 | 1 |
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, m; scanf("%d%d", &n, &m);
vector<int> face(n); vector<string> name(n);
for (int i = 0; i < n; i++) {
char s[12]; scanf("%d %s", &face[i], s); name[i] = s;
}
int pos = 0;
while (m--) {
int dir; long long s; scanf("%d%lld", &dir, &s);
if (dir == face[pos]) pos = ((pos - s % n) % n + n) % n; // 同向:逆着编号走
else pos = (pos + s) % n;
}
printf("%s\n", name[pos].c_str());
return 0;
}
易错点:① s 可达 109,先模 n 再运算,且减法后要 +n 防负;② "同向相减"记错方向就全错——考场上用样例验证(样例一定能检验方向规则);③ 面向 2026:真正的考场大模拟(结构体/儒略日级别)会把本题材放大 10 倍,务必练"规则清单 + 分函数 + 对拍"三件套。
进阶对照(P1514 引水入城 · 要点):第一问 BFS 可达性;第二问把"每个第一行点能覆盖的最后一段区间"求出后做区间覆盖贪心——与 2024 超速检测的区间选点同族,模拟+贪心复合是流行出题法。
专题⑦ 多维计数 DP 与容斥 押题 T4 · ★★★★★
核心知识
- 状态设计心法:把影响后继决策的最小信息塞进状态——2025 真题的状态是 f[i][j][k](前 i 天、已淘汰 j 人、其中 k 人耐心值 ≤ j),因为"淘汰是否发生"取决于当前已淘汰人数。
- 贡献延迟技巧:转移时某个选择的影响无法立刻确定(如"选了哪个数"),就先记数量、在约束触发的那一刻再结算——考场上称"提前/延迟钦定"。
- 容斥:约束"每个 ≤ 上限"的计数 = 无约束方案 − 至少一个越界方案……,对 2^m 个越界集合做符号求和。
- 取模纪律:998244353 / 1e9+7;减法后 +mod;读清"方案数"还是"期望"。
练习 P1450【HAOI2008】硬币购物(详细解答)
题意:4 种面值 c1..4 的硬币无限枚;q ≤ 1000 次询问,每次给 4 个使用上限 di 与金额 s ≤ 105,问恰好凑出 s 且第 i 种硬币用量 ≤ di 的方案数。
提示:4 个上限如果直接进状态是多少?容斥的"越界集合"是什么?
详细解答(含推导与代码)
容斥推导:设 Ai = "第 i 种硬币用量 ≥ di+1" 的方案集合。答案 = |无任何 Ai| = ΣS⊆{1,2,3,4} (−1)|S| · g(s − Σi∈S(di+1)ci),其中 g(x) 为无上限时凑出 x 的方案数(完全背包预处理)。金额为负时该项为 0。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[100005]; int c[4], d[4];
int main() {
for (int i = 0; i < 4; i++) scanf("%d", &c[i]);
f[0] = 1;
for (int i = 0; i < 4; i++) // 完全背包:无上限方案数
for (int x = c[i]; x <= 100000; x++) f[x] += f[x - c[i]];
int q; scanf("%d", &q);
while (q--) {
for (int i = 0; i < 4; i++) scanf("%d", &d[i]);
ll s, ans = 0; scanf("%lld", &s);
for (int S = 0; S < 16; S++) { // 2^4 枚举越界集合
ll t = s; int bits = 0;
for (int i = 0; i < 4; i++)
if (S >> i & 1) { t -= (ll)(d[i] + 1) * c[i]; bits++; }
if (t >= 0) ans += (bits & 1 ? -1 : 1) * f[t];
}
printf("%lld\n", ans);
}
return 0;
}
走查一遍(c = [1,2,3,4],询问 d = [2,1,0,0],s = 4;预处理得 f[0]=1、f[1]=1、f[4]=5):
| 越界集合 S | 扣减金额 Σ(di+1)ci | f 值 | 符号 | 贡献 |
|---|---|---|---|---|
| ∅ | 0 | f[4] = 5 | + | +5 |
| {1}(硬币1 ≥ 3 枚) | 3×1 = 3 | f[1] = 1 | − | −1(排除 {1,1,1,1}) |
| {2}(硬币2 ≥ 2 枚) | 2×2 = 4 | f[0] = 1 | − | −1(排除 {2,2}) |
| {3}(硬币3 ≥ 1 枚) | 1×3 = 3 | f[1] = 1 | − | −1(排除 {1,3}) |
| {4}(硬币4 ≥ 1 枚) | 1×4 = 4 | f[0] = 1 | − | −1(排除 {4}) |
| 其余集合 | 金额为负,贡献 0 | 0 | ||
| 合计 | 5 − 4 = 1(唯一合法方案 {1,1,2} ✓) | |||
易错点:① (d+1)·c 可能溢出 int → long long;② 符号 (−1)^{|S|} 别写反;③ f[t] 本身就是 ll,无需再取模(本题不取模,若考场题要求取模则全程取模)。本题是"容斥 + 多维约束计数"的最小完整模型,直接对标 2025 T4 的思维框架。
进阶对照(P7914 括号序列 · 要点):区间 DP 防重技巧——f[l][r](两端匹配)与 g[l][r](整体合法但不匹配),并列拼接时"规定最后一段是完整匹配段"来去重;转移枚举中间 * 段长度 ≤ k。
专题⑧ 扫描线与区间覆盖 押题 T2/T3 · ★★★★
核心知识
- 面积并扫描线:把每个矩形拆成"左边界 +1、右边界 −1"两个事件,按 x 排序;线段树维护 y 轴上"被完全覆盖的长度",相邻事件间面积 = 覆盖长度 × Δx。
- 线段树节点不 pushdown:覆盖计数是"整段打标记 + 回溯时由子节点上拉",单点修改很少,是懒标记的变体——注意 cover 长度的更新公式:
若整段被覆盖 len = 区间长;否则 len = 左子 len + 右子 len。 - 离散化(大纲新增技巧):y 坐标先排序去重,线段树建在"坐标段"上而非数值点上。
- 姐妹模型 · 区间选点(2024 超速检测):给定区间求最少选点使每区间含一个点——按右端点排序贪心,能复用就不加新点。
练习 P5490【模板】扫描线(详细解答)
题意:n ≤ 105 个矩形(坐标 ≤ 109),求面积并。
提示:线段树每个节点要存什么?为什么不需要懒标记下传?
详细解答(含代码)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 100005;
struct Node { int cnt; ll len; } t[8 * N]; // 离散化后最多 2n-1 ≈ 2e5 段 → 需 8e5 节点
struct Ev { ll x, y1, y2; int v; }; // v=+1 进 -1 出
vector<Ev> ev; vector<ll> ys;
void pushup(int u, int l, int r) {
if (t[u].cnt) t[u].len = ys[r + 1] - ys[l]; // 整段被覆盖
else t[u].len = (l == r) ? 0 : t[u*2].len + t[u*2+1].len;
}
void update(int u, int l, int r, int ql, int qr, int v) {
if (qr < l || r < ql) return;
if (ql <= l && r <= qr) { t[u].cnt += v; pushup(u, l, r); return; }
int m = (l + r) / 2;
update(u*2, l, m, ql, qr, v); update(u*2+1, m+1, r, ql, qr, v);
pushup(u, l, r);
}
int main() {
int n; scanf("%d", &n);
for (int i = 0; i < n; i++) {
ll x1, y1, x2, y2; scanf("%lld%lld%lld%lld", &x1, &y1, &x2, &y2);
ev.push_back({x1, y1, y2, 1}); ev.push_back({x2, y1, y2, -1});
ys.push_back(y1); ys.push_back(y2);
}
sort(ev.begin(), ev.end(), [](auto&a, auto&b){ return a.x < b.x; });
sort(ys.begin(), ys.end()); ys.erase(unique(ys.begin(), ys.end()), ys.end());
int m = ys.size() - 1; // m 段
ll ans = 0, lastX = ev[0].x;
for (auto &e : ev) {
ans += t[1].len * (e.x - lastX); // 上一状态覆盖了 (lastX, e.x)
int l = lower_bound(ys.begin(), ys.end(), e.y1) - ys.begin();
int r = lower_bound(ys.begin(), ys.end(), e.y2) - ys.begin() - 1;
if (l <= r) update(1, 0, m - 1, l, r, e.v);
lastX = e.x;
}
printf("%lld\n", ans);
return 0;
}
走查一遍(矩形 A:x∈[0,3)、y∈[0,2);矩形 B:x∈[2,5)、y∈[1,3)):
| 事件 x | 事件内容 | 本段贡献(旧覆盖长 × Δx) | 处理后覆盖段(长度) |
|---|---|---|---|
| 0 | A 进入 y[0,2) | —(起点) | [0,2)(2) |
| 2 | B 进入 y[1,3) | 2 × (2−0) = 4 | [0,3)(3) |
| 3 | A 离开 | 3 × (3−2) = 3 | [1,3)(2) |
| 5 | B 离开 | 2 × (5−3) = 4 | 无(0) |
| 合计 | 4 + 3 + 4 = 11(验证:6 + 6 − 1 = 11 ✓) | ||
易错点:① 线段树叶子对应段 [ys[i], ys[i+1]),右边界查询要 −1;② 面积累加放在"处理事件前"用旧 len 乘 Δx;③ 坐标 1e9 必须 ll;④ 周长并(P1856)还需维护覆盖段数与左右端点,是本题的加练变式。
四、自测小测(15 题,即时判分)
五、考场策略与防爆零清单(点击打卡)
时间分配
| 时段 | 动作 |
|---|---|
| 14:30–14:50 | 通读 4 题,给每题标"暴力档/正解档",排做题顺序 |
| 14:50–16:00 | T1 + T2 拿满(目标 200) |
| 16:00–17:10 | T3:先写暴力档,再冲正解 |
| 17:10–18:10 | T4:特殊性质档逐一落袋(通常可拿 20–40) |
| 18:10–18:30 | 全面检查:文件名 / freopen / 多组数据清空 / 删调试输出 |