NOIP 算法笔记全部章节
本章目录

第 9 章 部分分策略

本章定位:NOIP 的 T3、T4 是给前几名准备的,其他人比的是谁的部分分拿得多。CSP-S 2026 上,T3、T4 各有一档只要 BFS 和子集枚举就能拿的分(24 分和 15 分,已在官方数据上验证),这次都是 0。本章讲怎么把这种分稳稳拿到手,全部是【必会】。

9.1 部分分从哪来 【必会】

NOIP 每道题有 10~25 个测试点,每个点单独给分。题面最后的数据范围表把测试点分成几档,每一档要么规模小,要么带一条“特殊性质”。下面是本系统 E03(哨塔)的表,结构和真题一样:

测试点 n ≤ 特殊性质 这一档在考什么
1 ~ 6 18 无 枚举所有选法(2^18 种)
7 ~ 10 2×10^5 链 树退化成序列,线性 DP
11 ~ 12 2×10^5 菊花 只能选一个点,取最大值
13 ~ 14 1000 无 正解,但常数大也能过
15 ~ 20 2×10^5 无 正解

前三档合计 60 分,三段代码加起来不到 60 行,不需要想出正解。出题人把这 60 分明码标价放在表里,剩下的事情是把它们一档一档写出来。

9.2 读表三步 【必会】

读完题面先别想正解,先对着表做三件事,结果写在草稿纸上:

  1. 按规模定算法。 对照第 0 章 0.7 的复杂度表:n ≤ 10 是全排列,n ≤ 20 是子集枚举,n ≤ 500 是三重循环,n ≤ 5000 是两重循环。
  2. 把每条特殊性质翻译成“问题变成了什么”。 见 9.3。
  3. 给每一档标上“分值 / 要花的时间”。 先写最划算的。

草稿纸上应该出现这样的东西:

T3:1-6 子集枚举 30分 20分钟  ← 必写
    7-10 链=序列DP 20分 15分钟 ← 必写
    11-12 菊花 取max 10分 5分钟 ← 必写
    其余 40分 树形DP 没把握     ← 最后再说

9.3 常见特殊性质怎么用 【必会】

表里写的 问题变成了什么 做法
n ≤ 10 可以试遍所有顺序 next_permutation(第 2 章)
n ≤ 20 可以试遍所有选法 二进制枚举子集(第 2 章)
树是一条链 就是一个序列 树形 DP 变线性 DP;路径变区间;树上差分变一维差分;LCA 变成比较下标
树是菊花(所有点连着同一个中心) 只有“中心”和“叶子”两种点 多数情况能直接推出公式,或者排序后贪心
所有 a_i 相等,或只有 0 和 1 只和个数有关 数一数个数,套组合数或公式
k = 1、m = 0、q = 1 退化成最简单的情形 往往一两行;也是检验你有没有读懂题的机会
保证有解 / 保证连通 / 保证无环 不用处理特殊情况 省掉判断。反过来,没有这句话时一定要处理
数据随机生成 树的深度很小,极端情况不会出现 朴素做法往往不慢
输入有序 / 单调 不用排序,可以二分或双指针 第 1 章

链的两种说法要分清:“第 i 条边连接 i 和 i+1”时,点的编号就是它在序列里的位置,直接当数组用;只说“每个点的度数不超过 2”时,要先从一个端点(度数为 1 的点)出发走一遍,得到序列。

9.4 一份程序拿多档分 【必会】

每一档写成一个独立的函数,main 里按性质分流。性质要从输入里判断出来,不要假设。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N = 200005;
int n, deg[N];

bool is_chain() {  // 链:第 i 条边连接 i 和 i+1。读边时顺手记下来更简单,这里用度数判断
    for (int i = 1; i <= n; i++)
        if (deg[i] > 2) return false;
    return true;
}

bool is_star() {   // 菊花:有一个点连着其他所有点
    for (int i = 1; i <= n; i++)
        if (deg[i] == n - 1) return true;
    return false;
}

ll solve_brute() { return 0; }  // n 小:枚举
ll solve_chain() { return 0; }  // 链:线性 DP
ll solve_star() { return 0; }   // 菊花:公式

int main() {
    freopen("tower.in", "r", stdin);
    freopen("tower.out", "w", stdout);
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        deg[u]++, deg[v]++;
    }
    ll ans;
    if (n <= 18) ans = solve_brute();
    else if (is_chain()) ans = solve_chain();
    else if (is_star()) ans = solve_star();
    else ans = 0;  // 不会的档:输出一个格式合法的答案,不要让程序崩溃或超时
    cout << ans << '\n';
    return 0;
}

易错点

  • 分流的顺序:先判规模小的(暴力最可靠),再判特殊性质。
  • 每个函数用自己的变量,不要互相借用全局状态;多组数据时每个函数里用到的数组都要清空。
  • 不会做的那一档也要输出一个格式正确的结果并正常结束,不要留下死循环或越界。
  • 近两年的题常在输入第一行给出“测试点编号”。可以用它辅助判断,但直接检查性质更可靠。

练一练:本系统 E03(目标 60 分)、E06(60 分)、E07(70 分)、E10(70 分)、E15(60 分)。这几题的 solutions/<题>/partial.cpp 就是按本节的写法写的,做完再对照。

9.5 先写暴力,再想正解 【必会】

顺序很重要:

  1. 读懂题,先花 15~20 分钟把最小规模的暴力写出来,过样例,手造两组小数据。
  2. 把这份暴力另存一份(比如 brute.cpp)。从这一刻起这道题已经有分了。
  3. 然后才去想特殊性质和正解。想出来了,暴力正好拿来对拍;想不出来,分已经在手里。

反过来的顺序——先想正解,想了一个小时没想出来,再匆忙写暴力——是 0 分的主要来源。

暴力的写法见第 2 章 2.8,核心是五个字:最笨但一定对。

9.6 实在没有思路的时候 【常考】

  • 打表找规律。 用暴力把 n = 1, 2, 3, …, 10 的答案全部打印出来,看有没有规律(等差、乘 2、组合数、斐波那契)。数学题和计数题经常能这样看出公式。NOIP2017 的“小凯的疑惑”答案就是 a·b − a − b,考场上很多人是打表看出来的。
  • 写一个看起来合理的贪心。 排个序、每次取最大的。不保证对,但常能过掉一部分点。前提是暴力已经写好并保存。
  • 不要指望“全部输出 0”或“全部输出 -1”。 现在的测试点大多一个文件里有多组数据,全猜对才给分,基本拿不到。别在这上面花时间。

9.7 四个半小时怎么分 【必会】

时间 做什么 目标
0:00 ~ 0:20 四道题全部读完,对每题做 9.2 的“读表三步” 草稿纸上有每题的拿分计划
0:20 ~ 1:10 T1:写、过全部样例、对拍 100 分,并且确信是 100 分
1:10 ~ 2:30 T2:先暴力(20 分钟),再特殊性质,再正解 至少 40 分,争取 100 分
2:30 ~ 3:10 T3:暴力 + 能写的特殊性质 20~40 分
3:10 ~ 3:50 T4:暴力 + 能写的特殊性质 15~30 分
3:50 ~ 4:15 回到最有希望的那道题继续往上够 多拿一档
4:15 ~ 4:30 不写新代码,逐题过第 0 章的检查清单 不丢已经到手的分

两条纪律:

  • 一道题连续 40 分钟没有新进展,就换题。 换之前把已经能得分的版本保存好。
  • T1 没有确认满分之前,不碰后面的题。 T1 的 100 分比后面任何一档部分分都大。

本章必会清单

  • 拿到任何一道题,5 分钟内说出数据范围表每一档打算怎么做
  • 能把“链”“菊花”“n ≤ 20”“全相等”翻译成具体做法
  • 能写出 9.4 那样按性质分流的程序
  • 做题顺序固定为:暴力 → 保存 → 特殊性质 → 正解 → 对拍
  • 背下 9.7 的时间表,模拟考时照着执行