CSP-J 复赛真题集训手册
主题配色
清爽浅色
深邃暗色
护眼暖色
学习进度0 / 28
CSP-J 第二轮认证 · 集训精讲

CSP-J 复赛真题集训手册

2019 – 2025 七届真题全解 · 按题号分组精练 · 配套洛谷同难度练习 150 题 · 内置离线模拟器

7届真题
28道原题精讲
150道洛谷配套练习
0已掌握
📘
第一部分

真题精讲

按题目编号(T1 → T4)分组,同一题号跨年度对比训练,快速建立题型手感。

📗
T1

T1 · 第一题 专项

本组共 7 题,来自 2019–2025 年 CSP-J 第二轮认证。

2019

数字游戏 number

普及- 文件:number.in / number.out 字符串模拟枚举
✓已掌握

【题目描述】

小 K 同学向小 P 同学发送了一个长度为 8 的 01 字符串来玩数字游戏,小 P 同学想要知道字符串中究竟有多少个 1。

注意:01 字符串为每一个字符是 0 或者 1 的字符串,如“101”(不含双引号)为一个长度为 3 的 01 字符串。

【输入格式】

输入文件名为 number.in。

输入文件只有一行,一个长度为 8 的 01 字符串 s。

【输出格式】

输出文件名为 number.out。

输出文件只有一行,包含一个整数,即 01 字符串中字符 1 的个数。

【输入输出样例 1】

输入
00010100
输出
2

【输入输出样例 2】

输入
11111111
输出
8

【数据规模与约定】

对于 20% 的数据,保证输入的字符全部为 0。

对于 100% 的数据,输入只可能包含字符 0 和字符 1,字符串长度固定为 8。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

签到题。给定一个长度固定为 8 的 01 串,统计其中字符 ‘1’ 的个数。考查最基本的“读入字符串 + 遍历 + 计数器”,是入门选手必须稳拿的第一分。

【算法思路】

直接读入字符串 s(长度恒为 8,也可以用 cin >> s),从下标 0 到 7 逐位判断:若 s[i] == '1' 就令计数器 ans 自增。最后输出 ans。

由于“长度固定为 8”这个极强的限制,甚至不需要考虑换行、空格等多余字符,cin 读到字符串即可。注意不要用 scanf("%d") 读,因为 00010100 会被当作整数解析。

【复杂度分析】

时间复杂度 O(8) = O(1),空间复杂度 O(1)。

【易错点提示】

1. 文件输入输出名必须是 number.in / number.out,大小写与拼写要和题目完全一致;

2. 若用 freopen,务必在开头调用,且 main 函数返回 int。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

int main() {
    freopen("number.in", "r", stdin);
    freopen("number.out", "w", stdout);

    string s;
    cin >> s;                 // 长度固定为 8
    int ans = 0;
    for (int i = 0; i < 8; ++i)
        if (s[i] == '1') ++ans;
    cout << ans << endl;

    return 0;
}
2020

优秀的拆分 power

普及- 文件:power.in / power.out 位运算进制贪心数学
✓已掌握

【题目描述】

一般来说,一个正整数可以拆分成若干个正整数的和。例如,1 = 1,10 = 1 + 2 + 3 + 4 等。

对于正整数 n 的一种特定拆分,我们称它为“优秀的”,当且仅当在这种拆分下,n 被分解为了若干个不同的 2 的正整数次幂。注意,一个数 x 能被表示成 2 的正整数次幂,当且仅当 x 能通过正整数个 2 相乘在一起得到。

例如,10 = 8 + 2 = 2^3 + 2^1 是一个优秀的拆分。但是,7 = 4 + 2 + 1 = 2^2 + 2^1 + 2^0 就不是一个优秀的拆分,因为 1 不是 2 的正整数次幂。

现在,给定正整数 n,你需要判断这个数的所有拆分中,是否存在优秀的拆分。若存在,请你给出具体的拆分方案。

【输入格式】

输入文件名为 power.in。输入文件只有一行,一个正整数 n,代表需要判断的数。

【输出格式】

输出文件名为 power.out。如果这个数的所有拆分中,存在优秀的拆分,那么你需要从大到小输出这个拆分中的每一个数,相邻两个数之间用一个空格隔开。可以证明,在规定了拆分数字的顺序后,该拆分方案是唯一的。若不存在优秀的拆分,输出 “-1”(不包含双引号)。

【输入输出样例 1】

输入
6
输出
4 2

样例说明:6 = 4 + 2 = 2^2 + 2^1 是一个优秀的拆分。注意 6 = 2 + 2 + 2 不是优秀拆分,因为拆分成的 3 个数不满足每个数互不相同。

【输入输出样例 2】

输入
7
输出
-1

【数据范围与提示】

对于 20% 的数据,n ≤ 10。

对于另外 20% 的数据,保证 n 为奇数。

对于另外 20% 的数据,保证 n 为 2 的正整数次幂。

对于 80% 的数据,n ≤ 1024。

对于 100% 的数据,1 ≤ n ≤ 1 × 10^7。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

数学 + 二进制拆分。要求的“优秀拆分”正是把一个偶数写成若干个互不相同的 2^k(k ≥ 1)之和,也就是二进制表示中所有为 1 的位(且最低位必须是 2^1 位)。

【关键结论】

1. 若 n 是奇数,则它一定不行。因为所有 2 的正整数次幂都是偶数,若干个偶数之和仍是偶数,无法拼出奇数,直接输出 -1。

2. 若 n 是偶数,一定可行。把 n 做二进制分解,去掉最低位(2^0 位)后剩下的部分就是答案;由于 n 是偶数,2^0 位本来就是 0,所以直接输出 n 的所有为 1 的二进制位即可。

“唯一性”也由此保证:二进制表示唯一。

【实现方式】

方法一(位运算):从高位 i = 24 向低位枚举,若 n 的第 i 位为 1(即 n 的二进制中 2^i 这一位是 1),就输出 2^i 并把这一位从 n 中减掉。因为题目保证 n ≤ 10^7 < 2^24,所以最高枚举到 24 足够。

方法二(打表/递推):预处理所有 2^k 的取值,从大到小逐个试减。

【复杂度分析】

枚举 24 个二进制位,时间复杂度 O(log n),空间复杂度 O(1)。

【易错点提示】

1. 注意 1 = 2^0 不算“2 的正整数次幂”,所以 1 和所有奇数都要输出 -1;

2. 输出要求从大到小,用高位到低位的循环天然满足;

3. 输出末尾可以有空格,评测会过滤行末空格。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

int main() {
    freopen("power.in", "r", stdin);
    freopen("power.out", "w", stdout);

    int n;
    scanf("%d", &n);

    if (n % 2 == 1) {          // 奇数不可能
        printf("-1\n");
        return 0;
    }
    for (int i = 24; i >= 1; --i) {      // 从大到小枚举 2^i
        if (n >= (1 << i) && (n >> i & 1)) {
            printf("%d ", 1 << i);
        }
    }
    printf("\n");
    return 0;
}
2021

分糖果 candy

普及- 文件:candy.in / candy.out 数学取模运算分类讨论
✓已掌握

【题目背景】

红太阳幼儿园的小朋友们开始分糖果啦!

【题目描述】

红太阳幼儿园有 n 个小朋友,你是其中之一。保证 n ≥ 2。

有一天你在幼儿园的后花园里发现无穷多颗糖果,你打算拿一些糖果回去分给幼儿园的小朋友们。由于你只是个平平无奇的幼儿园小朋友,所以你的体力有限,至多只能拿 R 块糖回去。但是拿的太少不够分的,所以你至少要拿 L 块糖回去。保证 n ≤ L ≤ R。

也就是说,如果你拿了 k 块糖,那么你需要保证 L ≤ k ≤ R。如果你拿了 k 块糖,你将把这 k 块糖放到篮子里,并要求大家按照如下方案分糖果:只要篮子里有不少于 n 块糖果,幼儿园的所有 n 个小朋友(包括你自己)都从篮子中拿走恰好一块糖,直到篮子里的糖数量少于 n 块。此时篮子里剩余的糖果均归你所有——这些糖果是作为你搬糖果的奖励。

作为幼儿园高质量小朋友,你希望让作为你搬糖果的奖励的糖果数量(而不是你最后获得的总糖果数量!)尽可能多;因此你需要写一个程序,依次输入 n, L, R,并输出出你最多能获得多少作为你搬糖果的奖励的糖果数量。

【输入格式】

从文件 candy.in 中读入数据。输入一行,包含三个正整数 n, L, R,分别表示小朋友的个数、糖果数量的下界和上界。

【输出格式】

输出到文件 candy.out 中。输出一行一个整数,表示你最多能获得的作为你搬糖果的奖励的糖果数量。

【样例 1】

输入
7 16 23
输出
6

样例解释:拿 k = 20 块糖放入篮子。20 ≥ 7,所有小朋友获得一块糖,剩 13;13 ≥ 7,再各拿一块,剩 6 < 7,这 6 块就是奖励。奖励数量不可能超过 6,故答案为 6。

【样例 2】

输入
10 14 18
输出
8

样例解释:此时任何 k ∈ [14,18] 都满足 n ≤ k < 2n,所有小朋友拿一轮后剩下的就是 k−10,故取 k = 18 得 8。

【数据范围】

对于所有数据,保证 2 ≤ n ≤ L ≤ R ≤ 10^9。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

数学 + 分类讨论。核心是看清“奖励糖果数量”就是 k mod n(当 k ≥ n 时反复每轮减 n,剩下的就是 k 除以 n 的余数)。

【问题转化】

设拿了 k 块糖(L ≤ k ≤ R)。分糖过程就是不断让篮子里减去 n,直到不足 n 块。所以“奖励数量”恰好等于 k mod n。

于是问题变成:在区间 [L, R] 中选一个 k,使 k mod n 最大。

【分类讨论】

情况一:区间 [L, R] 内跨越了 n 的某个倍数,即 ⌊R / n⌋ > ⌊L / n⌋。此时一定能取到某个 k 满足 k ≡ n−1 (mod n),答案为 n−1(这是 k mod n 的理论最大值)。

情况二:区间内没有 n 的倍数,即 ⌊R / n⌋ == ⌊L / n⌋。此时所有的 k 都有相同的商,k mod n 随 k 单调递增,取最大的 k = R,答案为 R mod n。

合并写法:若 R / n > L / n 则输出 n − 1,否则输出 R % n。

【复杂度分析】

O(1)。

【易错点提示】

1. 三个数都可达 10^9,用 int 存储足够(10^9 < 2^31−1 ≈ 2.1×10^9),但 n−1、R%n 都安全;

2. 不要忘了题目给的条件 n ≤ L,因此 k 一定 ≥ n,不会出现“一轮都没分”的边界;

3. 判断“是否跨越 n 的倍数”只能用整除比较,不能写 (R % n) < (L % n) 之类的错误判据。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

int main() {
    freopen("candy.in", "r", stdin);
    freopen("candy.out", "w", stdout);

    long long n, L, R;
    scanf("%lld%lld%lld", &n, &L, &R);

    if (R / n > L / n)          // 区间跨过了 n 的倍数
        printf("%lld\n", n - 1);
    else                        // 同一段内,取最大的 k
        printf("%lld\n", R % n);

    return 0;
}
2022

乘方 pow

普及- 文件:pow.in / pow.out 模拟快速幂边界处理数学
✓已掌握

【题目描述】

小文同学刚刚接触了信息学竞赛,有一天她遇到了这样一个题:给定正整数 a 和 b,求 a^b 的值是多少。a^b 即 b 个 a 相乘的值,例如 2^3 即为 3 个 2 相乘,结果为 2 × 2 × 2 = 8。

“简单!”小文心想,同时很快就写出了一份程序,可是测试时却出现了错误。小文很快意识到,她的程序里的变量都是 int 类型的。在大多数机器上,int 类型能表示的最大数为 2^31 − 1,因此只要计算结果超过这个数,她的程序就会出现错误。

由于小文刚刚学会编程,她担心使用 int 计算会出现问题。因此她希望你在 a^b 的值超过 10^9 时,输出一个 -1 进行警示,否则就输出正确的 a^b 的值。然而小文还是不知道怎么实现这份程序,因此她想请你帮忙。

【输入格式】

从文件 pow.in 中读入数据。输入共一行,两个正整数 a, b。

【输出格式】

输出到文件 pow.out 中。输出共一行,如果 a^b 的值不超过 10^9,则输出 a^b 的值,否则输出 -1。

【输入输出样例】

输入
10 9
输出
1000000000
输入
23333 66666
输出
-1

【数据范围】

对于 10% 的数据,保证 b = 1。

对于 30% 的数据,保证 b ≤ 2。

对于 60% 的数据,保证 b ≤ 30,a^b ≤ 10^18。

对于 100% 的数据,保证 1 ≤ a, b ≤ 10^9。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

模拟 + 边界处理。看似是最简单的求幂,但真正的考点是“**如何在不溢出的前提下判断结果是否超过 10^9**”。

【算法思路】

用一个 long long 变量 ans 从 1 开始,循环 b 次每次乘 a,并在每次乘法后立即判断:若 ans > 10^9,就直接输出 -1 并结束程序。由于我们每乘一次就检查,ans 最大只会短暂达到 10^9 级别再乘一次 a(不超过 10^18),long long(上限约 9.2×10^18)完全够用。

也可以提前剪枝:若 a == 1,则结果恒为 1;若 a > 10^9 且 b ≥ 2,直接 -1。这些优化不是必须的,但能让程序更清晰。

【为什么不能直接算】

a, b 可达 10^9,a^b 是天文数字,无法存储。必须“边算边卡上限”,一旦超过阈值立即返回。

【复杂度分析】

循环次数最多约 30 次(因为 2^30 > 10^9,最多乘 30 次就会超过阈值返回),时间复杂度 O(log_{a}(10^9)),实际 O(1)。

【易错点提示】

1. 判断用的是“超过 10^9”,所以等于 10^9 时要正常输出(样例 1 即 10^9,输出 1000000000);

2. 乘法前判断还是乘法后判断都可以,但必须用 long long 承接中间结果;

3. 若在循环中提前 return 0,别忘了这是正常结束。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const ll LIMIT = 1000000000LL;

int main() {
    freopen("pow.in", "r", stdin);
    freopen("pow.out", "w", stdout);

    ll a, b;
    scanf("%lld%lld", &a, &b);

    ll ans = 1;
    for (ll i = 1; i <= b; ++i) {
        ans *= a;              // 最多到 1e9 * 1e9 = 1e18,long long 安全
        if (ans > LIMIT) {     // 一旦超过 1e9 立即报告
            printf("-1\n");
            return 0;
        }
    }
    printf("%lld\n", ans);
    return 0;
}
2023

小苹果 apple

普及- 文件:apple.in / apple.out 数学模拟递推数论
✓已掌握

【题目描述】

小 Y 的桌子上放着 n 个苹果从左到右排成一列,编号为从 1 到 n。小苞是小 Y 的好朋友,每天她都会从中拿走一些苹果。

每天在拿的时候,小苞都是从左侧第 1 个苹果开始、每隔 2 个苹果拿走 1 个苹果。随后小苞会将剩下的苹果按原先的顺序重新排成一列。

小苞想知道,多少天能拿完所有的苹果,而编号为 n 的苹果是在第几天被拿走的?

【输入格式】

从文件 apple.in 中读入数据。输入的第一行包含一个正整数 n,表示苹果的总数。

【输出格式】

输出到文件 apple.out 中。输出一行包含两个正整数,两个整数之间由一个空格隔开,分别表示小苞拿走所有苹果所需的天数以及拿走编号为 n 的苹果是在第几天。

【样例 1】

输入
8
输出
5 5

样例解释:桌上一共 8 个苹果。第 1 天拿走编号 1、4、7;第 2 天拿走 2、6;第 3 天拿走 3;第 4 天拿走 5;第 5 天拿走 8。共 5 天,编号 8 在第 5 天被拿走。

【数据范围】

对于所有测试数据有:1 ≤ n ≤ 10^9。

测试点n ≤特殊性质
1~210无
3~510^3无
6~710^6有
8~910^6无
1010^9无

特殊性质:小苞第一天就取走编号为 n 的苹果。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

数学 + 递推。n 最大 10^9,绝不能真的去模拟 n 个苹果,必须找到每一步“规模”的递推公式。

【观察一:每天剩下的苹果数】

当桌上有 m 个苹果时,被拿走的正是位置 1, 4, 7, 10, …(即位置 ≡ 1 (mod 3)),共 ⌈m / 3⌉ = ⌊(m + 2) / 3⌋ 个。于是剩下 m − ⌈m/3⌉ = ⌊2m / 3⌋ 个。

每天剩余数量按 m ← ⌊2m/3⌋ 递减,形成等比递减(比例约 2/3),所以总共只会经历 O(log_{1.5} n) ≈ 52 天(n ≤ 10^9 时)。用 while 循环数天数即可。

【观察二:编号 n 的苹果在哪天被拿走】

设当前这一轮开始时共有 m 个苹果,而我们要跟踪的那个苹果(最初是第 n 个)现在位于位置 p(初始 p = n)。在一轮中,被拿走的是位置 1, 4, 7, …,所以:

· 若 p ≡ 1 (mod 3),说明它本轮就被拿走了,答案就是当前天数;

· 否则它会被顺延到下一轮,新位置 p ← p − ⌈p / 3⌉(因为排在它前面、位置 ≡ 1 (mod 3) 的那些苹果都被拿走了)。

同时 m ← ⌊2m / 3⌋。两个变量一起循环递推,直到 p == 1(此刻 p ≡ 1 (mod 3) 成立)。

【样例验证】n = 8】

第 1 天:m=8, p=8(8 mod 3 = 2,未拿走)→ m=5, p=5;

第 2 天:m=5, p=5(5 mod 3 = 2)→ m=3, p=3;

第 3 天:m=3, p=3(3 mod 3 = 0)→ m=2, p=2;

第 4 天:m=2, p=2(2 mod 3 = 2)→ m=1, p=1;

第 5 天:m=1, p=1(1 mod 3 = 1)→ 拿走,答案第 5 天。总天数也是 5。

【复杂度分析】

两个递推都是每次乘 2/3,循环次数 O(log n) ≈ 52 次,时间与空间均为 O(1)。

【易错点提示】

1. 天数和“第 n 个苹果的天数”必须用两个变量分别递推(虽然在本例中恰好相等,但不能假设相等);

2. 取整方向要小心:每轮拿走 ⌈m/3⌉ 个,剩余 ⌊2m/3⌋;“顺延后的新位置”是 p 减去“排在它前面且被拿走的个数” = ⌈p/3⌉;

3. 用 long long,虽然 n ≤ 10^9 用 int 也不溢,但习惯上更稳。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int main() {
    freopen("apple.in", "r", stdin);
    freopen("apple.out", "w", stdout);

    ll n;
    scanf("%lld", &n);

    ll m = n, p = n;      // m: 当前苹果总数; p: 目标苹果当前位置
    ll day = 0, ansDay = 0;

    while (m > 0) {
        ++day;
        if (p % 3 == 1 && ansDay == 0) ansDay = day;   // 本轮被拿走

        m -= (m + 2) / 3;          // 每轮拿走 ceil(m/3) 个
        p -= (p + 2) / 3;          // 目标的新位置(减去排在它前面被拿走的)
    }
    printf("%lld %lld\n", day, ansDay);
    return 0;
}
2024

扑克牌 poker

普及- 文件:poker.in / poker.out 模拟集合去重编码
✓已掌握

【题目描述】

小 P 从同学小 Q 那儿借来一副 n 张牌的扑克牌。本题中我们不考虑大小王,此时每张牌具有两个属性:花色和点数。花色共有 4 种:方片、草花、红桃和黑桃。点数共有 13 种,从小到大分别为 A 2 3 4 5 6 7 8 9 T J Q K。注意:点数 10 在本题中记为 T。

我们称一副扑克牌是完整的,当且仅当对于每一种花色和每一种点数,都恰好有一张牌具有对应的花色和点数,由此,一副完整的扑克牌恰好有 4 × 13 = 52 张牌。

图 1:一副完整的扑克牌(共 52 张)
图 1:一副完整的扑克牌(共 52 张)

小 P 借来的牌可能不是完整的,为此小 P 准备再向同学小 S 借若干张牌。可以认为小 S 每种牌都有无限张,因此小 P 可以任意选择借来的牌。小 P 想知道他至少得向小 S 借多少张牌,才能让从小 S 和小 Q 借来的牌中,可以选出 52 张牌构成一副完整的扑克牌。

为了方便你的输入,我们使用字符 D 代表方片,字符 C 代表草花,字符 H 代表红桃,字符 S 代表黑桃,这样每张牌可以通过一个长度为 2 的字符串表示,其中第一个字符表示这张牌的花色,第二个字符表示这张牌的点数,例如 CA 表示草花 A,ST 表示黑桃 T(黑桃 10)。

【输入格式】

从文件 poker.in 中读入数据。输入的第一行包含一个整数 n 表示牌数。接下来 n 行:每行包含一个长度为 2 的字符串描述一张牌,其中第一个字符描述其花色,第二个字符描述其点数。

【输出格式】

输出到文件 poker.out 中。输出一行一个整数,表示最少还需要向小 S 借几张牌才能凑成一副完整的扑克牌。

【样例 1】

输入
1
SA
输出
51

【样例 2】

输入
4
DQ
H3
DQ
DT
输出
49

样例解释:这副牌中包含两张方片 Q、一张方片 T(方片 10)以及一张红桃 3,还需借除红桃 3、方片 T 和方片 Q 以外的 49 张牌。

【数据范围】

对于所有测试数据,保证:1 ≤ n ≤ 52,输入的 n 个字符串每个都代表一张合法的扑克牌,即字符串长度为 2,且第一个字符为 D C H S 中的某个字符,第二个字符为 A 2 3 4 5 6 7 8 9 T J Q K 中的某个字符。

测试点编号n ≤特殊性质
11A
2~452无(A)
5~752B
8~1052无

特殊性质 A:保证输入的 n 张牌两两不同。特殊性质 B:保证所有牌按照点数从小到大依次输入,点数相同时按照方片、草花、红桃、黑桃的顺序依次输入。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

签到题,考集合去重。要拼齐 52 张牌,需要补齐的正是“手上还没有的那些牌”,所以答案 = 52 − (手上不同牌的种类数)。注意重复的牌只能算一次。

【算法思路】

方法一:直接把每张牌的两字符字符串丢进 set / unordered_set,最后输出 52 − set.size()。最简单,n 只有 52,效率无压力。

方法二:给每张牌编码成一个 0~51 的整数——花色映射到 0~3,点数映射到 0~12,编号 = 花色 × 13 + 点数,用 bool 数组判重并统计种类数。

两种做法都行。注意样例 2 中 DQ 出现了两次,只算一张(所以是 52 − 3 = 49,而不是 52 − 4 = 48)。

【复杂度分析】

时间 O(n),空间 O(1)(最多 52 种牌)。

【易错点提示】

1. 重复的牌要去重,不要简单地用 52 − n;

2. 点数 10 输入为字符 T,不要写成 10。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

int main() {
    freopen("poker.in", "r", stdin);
    freopen("poker.out", "w", stdout);

    int n;
    scanf("%d", &n);

    set<string> cards;
    char s[5];
    for (int i = 0; i < n; ++i) {
        scanf("%s", s);
        cards.insert(string(s));
    }
    printf("%d\n", 52 - (int)cards.size());
    return 0;
}

/* ---- 另一种写法:把牌编码成 0~51 的整数 ----
#include <bits/stdc++.h>
using namespace std;
bool have[55];
int main() {
    freopen("poker.in", "r", stdin);
    freopen("poker.out", "w", stdout);
    const string COL = "DCHS";          // 花色
    const string RANK = "A23456789TJQK"; // 点数
    int n; scanf("%d", &n);
    char s[5]; int cnt = 0;
    for (int i = 0; i < n; ++i) {
        scanf("%s", s);
        int c = COL.find(s[0]);
        int r = RANK.find(s[1]);
        int id = c * 13 + r;
        if (!have[id]) { have[id] = true; ++cnt; }
    }
    printf("%d\n", 52 - cnt);
    return 0;
}
*/
2025

拼数 number

普及- 文件:number.in / number.out 字符串排序模拟
✓已掌握

【题目描述】

小 R 正在学习字符串处理。小 X 给了小 R 一个字符串 s,其中 s 仅包含小写英文字母及数字,且包含至少一个 1~9 中的数字。小 X 希望小 R 使用 s 中的任意多个数字,按任意顺序拼成一个正整数。注意:小 R 可以选择 s 中相同的数字,但每个数字只能使用一次。例如,若 s 为 1a01b,则小 R 可以同时选择第 1, 3, 4 个字符,分别为 1, 0, 1,拼成正整数 101 或 110;但小 R 不能拼成正整数 111,因为 s 仅包含两个数字 1。

小 R 想知道,在他所有能拼成的正整数中,最大的是多少。你需要帮助小 R 求出他能拼成的正整数的最大值。

【输入格式】

输入文件名为 number.in。输入的第一行包含一个字符串 s。

【输出格式】

输出文件名为 number.out。输出一行一个正整数,表示小 R 能拼成的正整数的最大值。

【样例 1】

输入
5
输出
5

样例解释:s 仅包含一个数字 5,因此小 R 仅能拼成正整数 5。

【样例 2】

输入
290es1q0
输出
92100

样例解释:s 包含数字 2, 9, 0, 1, 0,可以证明能拼成的最大正整数为 92100。

【数据规模与约定】

设 |s| 为字符串 s 的长度。对于所有测试数据,保证 1 ≤ |s| ≤ 10⁶;s 仅包含小写英文字母及数字,且包含至少一个 1~9 中的数字。

特殊性质 A:s 仅包含数字;特殊性质 B:s 仅包含不超过 10³ 个数字。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

签到题。从字符串里提取出所有数字字符,把它们排成一个尽可能大的正整数。考查「提取数字 + 排序(或计数)+ 处理前导零」。

【算法思路】

1. 遍历字符串 s,把所有数字字符('0'~'9')收集出来,忽略小写字母。

2. 要让拼出的正整数最大,显然应该让大的数字放在更靠前(高位)的位置,即把所有数字从大到小降序排列。

3. 排序后可能出现「前导 0」——例如 s 里只有一个 5 和若干个 0。此时最前面的 0 不能作为最高位(否则就不是正整数)。处理办法:因为题目保证至少有一个 1~9 的数字,排序后第一个非 0 的数字一定存在;把开头的所有 0 都挪到「第一个非零数字」的后面即可。等价地,直接降序输出,但如果开头是 0,就把这些 0 放到末尾。

更简洁的实现:把数字降序排好后,找到第一个非零位置 p(若 p>0 说明前面全是 0),交换第 0 位和第 p 位,即可保证最高位非 0 且整体仍最大。

【复杂度分析】

提取 O(|s|),排序 O(k log k)(k 为数字个数,k ≤ |s| ≤ 10⁶),总复杂度 O(|s| log |s|)。由于数字只有 0~9 十种,也可用计数排序做到 O(|s|)。

【易错点提示】

1. 必须忽略小写字母,只取数字字符;

2. 正整数不能有前导 0,排序后要注意处理全是 0 在前的情况;

3. 输出不要带空格或换行(除末尾换行外)。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

int main() {
    freopen("number.in", "r", stdin);
    freopen("number.out", "w", stdout);

    string s, d;
    cin >> s;
    for (char c : s)
        if (c >= '0' && c <= '9')
            d.push_back(c);

    // 数字从大到小降序,保证拼出的数最大
    sort(d.begin(), d.end(), greater<char>());

    // 处理前导 0:把开头连续的 0 挪到第一个非零数字之后
    int p = 0;
    while (p < (int)d.size() && d[p] == '0') p++;
    if (p > 0 && p < (int)d.size()) {
        swap(d[0], d[p]);
    }

    cout << d << endl;
    return 0;
}
📙
T2

T2 · 第二题 专项

本组共 7 题,来自 2019–2025 年 CSP-J 第二轮认证。

2019

公交换乘 transfer

普及/提高- 文件:transfer.in / transfer.out 模拟队列贪心双指针
✓已掌握

【题目描述】

著名旅游城市 B 市为了鼓励大家采用公共交通方式出行,推出了一种地铁换乘公交车的优惠方案:

1. 在搭乘一次地铁后可以获得一张优惠票,有效期为 45 分钟,在有效期内可以消耗这张优惠票,免费搭乘一次票价不超过地铁票价的公交车。在有效期内指开始乘公交车的时间与开始乘地铁的时间之差小于等于 45 分钟,即:t_bus − t_subway ≤ 45。

2. 搭乘地铁获得的优惠票可以累积,即可以连续搭乘若干次地铁后再连续使用优惠票搭乘公交车。

3. 搭乘公交车时,如果可以使用优惠票一定会使用优惠票;如果有多张优惠票满足条件,则优先消耗获得最早的优惠票。

现在你得到了小轩最近的公共交通出行记录,你能帮他算算他的花费吗?

【输入格式】

输入文件名为 transfer.in。

输入文件的第一行包含一个正整数 n,代表乘车记录的数量。

接下来的 n 行,每行包含 3 个整数,相邻两数之间以一个空格分隔。第 i 行的第 1 个整数代表第 i 条记录乘坐的交通工具,0 代表地铁,1 代表公交车;第 2 个整数代表第 i 条记录乘车的票价 price_i;第三个整数代表第 i 条记录开始乘车的时间 t_i(距 0 时刻的分钟数)。

我们保证出行记录是按照开始乘车的时间顺序给出的,且不会有两次乘车记录出现在同一分钟。

【输出格式】

输出文件名为 transfer.out。

输出文件有一行,包含一个正整数,代表小轩出行的总花费。

【输入输出样例 1】

输入
6
0 10 3
1 5 46
0 12 50
1 3 96
0 5 110
1 6 135
输出
36

样例说明:第 1 条记录花 10 元坐地铁;第 2 条记录可用第 1 条记录的优惠票,不花钱;第 3 条花 12 元坐地铁;第 4 条距第 3 条地铁已超过 45 分钟,优惠票失效,花 3 元;第 5 条花 5 元坐地铁;第 6 条票价 6 元高于第 5 条地铁票价 5 元,不能使用优惠票,花 6 元。总共 36 元。

【输入输出样例 2】

输入
6
0 5 1
0 20 16
0 7 23
1 18 31
1 4 38
1 7 68
输出
32

【数据规模与约定】

对于 30% 的数据,n ≤ 1000,t_i ≤ 10^6。

另有 15% 的数据,t_i ≤ 10^7,price_i 都相等。

另有 15% 的数据,t_i ≤ 10^9,price_i 都相等。

对于 100% 的数据,n ≤ 10^5,t_i ≤ 10^9,1 ≤ price_i ≤ 1000。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

模拟 + 队列(单调时间序)+ 双指针优化。核心在于“多张可用优惠票时,优先用最早获得的那张”,这天然对应队列的“先进先出”性质。

【算法思路】

把每次坐地铁看作往队列里压入一张优惠票,记录两个信息:这张票的“失效时刻”(t + 45) 和“可抵扣的最高票价”(price)。遇到公交车时:从队头开始找第一张满足“未失效 且 票价上限 ≥ 本次票价 且 尚未被使用”的票;找到就免费,标记为已用并跳出;找不到就自费。

关键优化:由于记录按时间递增给出,所以“失效时刻”也是递增的。于是用一个指针 x 指向“队列中第一张还没过期的票”。每次坐公交前,先把 x 向右推,跳过所有已过期的票——这些票以后永远不可能再用到,所以可以直接丢弃,无须回看。这样每张票最多被扫描常数次,总复杂度降到线性。

【为什么不能用“最便宜的票”或“最贵的票”】

题目的规则明确规定“优先消耗获得最早的”,所以必须严格按时间顺序挑选,不能贪心地挑最贵或最便宜的。

【复杂度分析】

时间复杂度 O(n),空间复杂度 O(n)。

【易错点提示】

1. 有效期判定是 t_bus − t_subway ≤ 45,写代码时建议存成 stop_time = t + 45,判断 t_bus ≤ stop_time;

2. 一张票只能用一次,必须有 used 标记;

3. 票价相等时也算“不超过票价”,可以使用。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
int type[MAXN], price[MAXN], t[MAXN];
int stopTime[MAXN];      // 优惠票失效时刻 = 乘地铁时刻 + 45
bool used[MAXN];         // 优惠票是否已被使用

int main() {
    freopen("transfer.in", "r", stdin);
    freopen("transfer.out", "w", stdout);

    int n;
    scanf("%d", &n);
    long long ans = 0;   // 总花费可能较大,用 long long 更稳
    int head = 1, tail = 0;   // 队列头尾指针

    for (int i = 1; i <= n; ++i) {
        scanf("%d%d%d", &type[i], &price[i], &t[i]);
        if (type[i] == 0) {                 // 坐地铁:花钱并产出一张优惠票
            ans += price[i];
            ++tail;
            stopTime[tail] = t[i] + 45;
            price[tail]   = price[i];
            used[tail]    = false;
        } else {                            // 坐公交
            while (head <= tail && stopTime[head] < t[i]) ++head;  // 丢弃过期票
            bool ok = false;
            for (int j = head; j <= tail; ++j) {
                if (!used[j] && price[j] >= price[i]) {  // 未用 + 票价足够
                    used[j] = true; ok = true; break;    // 用最早的那张
                }
            }
            if (!ok) ans += price[i];       // 无可用票则自费
        }
    }
    printf("%lld\n", ans);
    return 0;
}
2020

直播获奖 live

普及/提高- 文件:live.in / live.out 模拟桶排序堆排序
✓已掌握

【题目描述】

NOI2130 即将举行。为了增加观赏性,CCF 决定逐一评出每个选手的成绩,并直播即时的获奖分数线。本次竞赛的获奖率为 w%,即当前排名前 w% 的选手的最低成绩就是即时的分数线。

更具体地,若当前已评出了 p 个选手的成绩,则当前计划获奖人数为 max(1, ⌊p × w%⌋),其中 w 是获奖百分比,⌊x⌋ 表示对 x 向下取整,max(x, y) 表示 x 和 y 中较大的数。如有选手成绩相同,则所有成绩并列的选手都能获奖,因此实际获奖人数可能比计划中多。

作为评测组的技术人员,请你帮 CCF 写一个直播程序。

【输入格式】

输入文件名为 live.in。第 1 行两个正整数 n, w,分别代表选手总数与获奖率。第 2 行有 n 个非负整数,依次代表逐一评出的选手成绩。

【输出格式】

输出文件名为 live.out。只有一行,包含 n 个非负整数,依次代表选手成绩逐一评出后,即时的获奖分数线。相邻两个整数间用一个空格分隔。

【输入输出样例 1】

输入
10 60
200 300 400 500 600 600 0 300 200 100
输出
200 300 400 400 400 500 400 400 300 300

样例说明:第 9 名选手的成绩评出之后,计划获奖人数为 5 人,但由于有并列,因此实际会有 6 人获奖。

【输入输出样例 2】

输入
10 30
100 100 600 100 100 100 100 100 100 100
输出
100 100 600 600 600 600 100 100 100 100

【数据范围与提示】

对于所有测试点,每个选手的成绩均为不超过 600 的非负整数,获奖百分比 w 是一个正整数且 1 ≤ w ≤ 99。

测试点编号n
1~3= 10
4~6= 500
7~10= 2000
11~17= 10000
18~20= 100000

在计算计划获奖人数时,如用浮点类型的变量(如 float、double)存储获奖比例 w%,则计算 5 × 60% 时的结果可能为 3.000001,也可能为 2.999999,向下取整后的结果不确定。因此,建议仅使用整型变量,以计算出准确值。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

在线(增量)求第 k 大值。n 最大 10 万,成绩范围只有 [0, 600],这是一个非常关键的提示——可以用桶(计数数组)来维护。

【算法一:桶 + 倒序扫描(推荐,O(600n))】

开一个计数数组 cnt[score],表示当前成绩等于 score 的人数。每读入一个成绩 x,就令 cnt[x]++。然后计划获奖人数 k = max(1, p × w / 100)(用整数除法 p*w/100 避免浮点误差)。从分数 600 向下枚举到 0,累计人数 s += cnt[score],一旦 s ≥ k,当前分数就是分数线,直接输出并跳出。

总复杂度 O(600 × n) = 6 × 10^7,在 1 秒内可过。这是最贴合题目“成绩 ≤ 600”提示的做法。

【算法二:对顶堆(O(n log n))】

维护两个堆:大根堆 L 存“分数较高的那一半”,小根堆 R 存“分数较低的那一半”,使 L 的大小恰好等于当前的 k。每次新来一个成绩,先压入 L,再按大小关系调整两个堆,使 L 恰好有 k 个元素,此时 L 的堆顶就是分数线。此法与成绩取值范围无关,是通用做法。

【算法三:插入排序思路】

维护已排好序的数组,每加入一个元素就向前做邻项交换,代价最坏 O(n),总复杂度 O(n^2),只能拿部分分。这提示我们“增量维护有序性”不是本题正解。

【易错点提示】

1. 一定要用整数除法 p*w/100,不要用 double,否则 5×60% 可能算成 2.999999 取整得 2;

2. k 的下界是 1(max(1, ...)),当 p×w/100 = 0 时取 1;

3. 输出的是“当前排名前 k% 的最低成绩”,即第 k 大的分数,注意并列时不需要额外处理(倒序累计到 s ≥ k 自动包含并列)。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

int cnt[605];   // 成绩桶:0 ~ 600

int main() {
    freopen("live.in", "r", stdin);
    freopen("live.out", "w", stdout);

    int n, w;
    scanf("%d%d", &n, &w);

    for (int p = 1; p <= n; ++p) {
        int x; scanf("%d", &x);
        cnt[x]++;

        int k = p * w / 100;          // 整数除法,避免浮点误差
        if (k < 1) k = 1;             // max(1, ...)

        int s = 0, ans = 0;
        for (int sc = 600; sc >= 0; --sc) {   // 从高到低累计
            s += cnt[sc];
            if (s >= k) { ans = sc; break; }
        }
        printf("%d ", ans);
    }
    printf("\n");
    return 0;
}
2021

插入排序 sort

普及/提高- 文件:sort.in / sort.out 模拟排序插入排序性质均摊分析
✓已掌握

【题目描述】

插入排序是一种非常常见且简单的排序算法。小 Z 是一名大一的新生,今天 H 老师刚刚在上课的时候讲了插入排序算法。

假设比较两个元素的时间为 O(1),则插入排序可以以 O(n²) 的时间复杂度完成长度为 n 的数组的排序。不妨假设这 n 个数字分别存储在 a1, a2, …, an 之中,则如下伪代码给出了插入排序算法的一种最简单的实现方式:

for (int i = 1; i <= n; i++)
    for (int j = i; j >= 2; j--)
        if (a[j] < a[j-1]) {
            int t = a[j-1];
            a[j-1] = a[j];
            a[j] = t;
        }

为了帮助小 Z 更好的理解插入排序,小 Z 的老师 H 老师留下了这么一道家庭作业:

H 老师给了一个长度为 n 的数组 a,数组下标从 1 开始,并且数组中的所有元素均为非负整数。小 Z 需要支持在数组 a 上的 Q 次操作,操作共两种,参数分别如下:

· 1 x v:这是第一种操作,会将 a 的第 x 个元素 a_x 的值修改为 v。保证 1 ≤ x ≤ n,1 ≤ v ≤ 10^9。注意这种操作会改变数组的元素,修改得到的数组会被保留,也会影响后续的操作。

· 2 x:这是第二种操作,假设 H 老师按照上面的伪代码对 a 数组进行排序,你需要告诉 H 老师原来 a 的第 x 个元素 a_x,在排序后的新数组所处的位置。保证 1 ≤ x ≤ n。注意这种操作不会改变数组的元素,排序后的数组不会被保留,也不会影响后续的操作。

H 老师不喜欢过多的修改,所以他保证类型 1 的操作次数不超过 5000。

【输入格式】

从文件 sort.in 中读入数据。输入的第一行包含两个正整数 n, Q,表示数组长度和操作次数。保证 1 ≤ n ≤ 8000,1 ≤ Q ≤ 2 × 10^5。

输入的第二行包含 n 个空格分隔的非负整数,其中第 i 个非负整数表示 a_i。保证 1 ≤ a_i ≤ 10^9。接下来 Q 行,每行 2~3 个正整数,表示一次操作,操作格式见题目描述。

【输出格式】

输出到文件 sort.out 中。对于每一次类型为 2 的询问,输出一行一个正整数表示答案。

【样例 1】

输入
3 4
3 2 1
2 3
1 3 2
2 2
2 3
输出
1
1
2

样例解释:修改前对 [3,2,1] 做稳定插入排序得到 [1,2,3],原序列三个元素 a1, a2, a3 排序后所处的位置分别是 3, 2, 1,所以询问 2 3 回答 1。把 a3 改为 2 后数组变成 [3,2,2],稳定插入排序得到 [a2, a3, a1],三个元素的位置分别是 3, 1, 2,于是询问 2 2 回答 1、询问 2 3 回答 2。注意此时 a2 = a3 = 2,但不能把它们视为相同的元素(插入排序是稳定的,下标小者在前)。

【数据范围】

对于所有测试数据,满足 1 ≤ n ≤ 8000,1 ≤ Q ≤ 2 × 10^5,1 ≤ x ≤ n,1 ≤ v, a_i ≤ 10^9。保证在所有 Q 次操作中,至多有 5000 次操作属于类型 1。

测试点nQ特殊性质
1,2,3,4≤10≤10无
5~9≤300≤300无
10~13≤1500≤1500无
14~16≤8000≤8000保证所有输入的 a_i, v 互不相同
17~19≤8000≤2×10^5保证所有输入的 a_i, v 互不相同
20~22≤8000≤2×10^5无(≤5000 次修改)
23~25≤8000≤2×10^5无
▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【关键观察:插入排序是稳定的】

题目给出的伪代码只在 a[j] < a[j−1] 时交换,即相等时绝不交换。因此这个插入排序是稳定排序:值相同的元素,在排序后仍保持原有的相对先后顺序。

推论:对下标 i 定义关键值 key(i) = (a[i], i)。排序后的第 1 名到第 n 名,恰好就是按 key 从小到大排好序的下标序列。于是 a_x 在排序后的位置就是:

rank(x) = 1 + #{ y : a_y < a_x } + #{ y : a_y == a_x 且 y < x }。

【暴力与优化】

若每次修改后都 O(n log n) 重排一次,总共 5000 × 8000 × log ≈ 5×10^8,风险较大。注意到“至多 5000 次修改”这个保证,我们其实可以用 O(n) 处理每次修改:

修改 a_x 为 v 时,只有 key(x) 发生了变化,其余 key(y) 不变。因此对任意 y ≠ x,rank(y) 的变化量只有 −1 / 0 / +1:

delta_y = [newKey(x) < key(y)] − [oldKey(x) < key(y)]。

含义:如果 x 从“排在 y 前面”变成了“排在 y 后面”,则 y 的排名 −1;反之 +1;没跨过则不变。同时,rank(x) 直接重新数一遍所有 z ≠ x 中满足 key(z) < newKey(x) 的个数即可。

整个修改 O(n),查询 O(1)。总复杂度 O(n log n + 5000·n + Q),约 4×10^7,稳稳通过。

【另一种等价写法】

维护一个按 key 有序的下标序列(如 vector 或数组 + 逐个比较),修改时把 x 从原位置删除、按新 key 插入到正确位置,同时更新所有被“挤动”的元素的 rank。这也是 O(n) 的,本质与上面的 delta 写法一致。

【复杂度分析】

初始化 O(n log n),每次修改 O(n),每次询问 O(1)。空间 O(n)。

【易错点提示】

1. 相等元素的处理是本题的“陷阱”:必须用下标打破平局(稳定排序),不能当成相同元素处理;

2. 类型 2 操作不会改变数组,不要真的去排序;

3. 修改后要把 a[x] 更新为新值,否则后续 delta 计算会错。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 8005;
ll a[MAXN];
int rk[MAXN];      // rk[i]: 下标 i 的元素在稳定排序后的名次(1-based)
int ord[MAXN];     // 排序用的临时下标数组

int main() {
    freopen("sort.in", "r", stdin);
    freopen("sort.out", "w", stdout);

    int n, Q;
    scanf("%d%d", &n, &Q);
    for (int i = 1; i <= n; ++i) scanf("%lld", &a[i]);

    // 初始化:按 key(i) = (a[i], i) 稳定排序,得到初始名次
    for (int i = 1; i <= n; ++i) ord[i] = i;
    sort(ord + 1, ord + n + 1, [](int x, int y) {
        if (a[x] != a[y]) return a[x] < a[y];
        return x < y;
    });
    for (int r = 1; r <= n; ++r) rk[ord[r]] = r;

    while (Q--) {
        int op; scanf("%d", &op);
        if (op == 2) {
            int x; scanf("%d", &x);
            printf("%d\n", rk[x]);
        } else {
            int x; ll v; scanf("%d%lld", &x, &v);
            ll oldv = a[x];

            // 计算 delta:对每个 y != x,判断 x 是否跨过了 y
            int cntSmaller = 0;   // 新的 key(x) 之前有多少个元素
            for (int y = 1; y <= n; ++y) {
                if (y == x) continue;
                // cmp(va, ia, vb, ib): (va,ia) < (vb,ib)
                auto lessKey = [](ll v1, int i1, ll v2, int i2) {
                    if (v1 != v2) return v1 < v2;
                    return i1 < i2;
                };
                bool oldBefore = lessKey(oldv, x, a[y], y);   // 旧 key(x) < key(y)
                bool newBefore = lessKey(v,    x, a[y], y);   // 新 key(x) < key(y)

                if (oldBefore && !newBefore) rk[y] -= 1;      // x 从前面挪到了后面
                if (!oldBefore && newBefore) rk[y] += 1;      // x 从后面挪到了前面

                if (lessKey(a[y], y, v, x)) ++cntSmaller;     // 统计比新 key(x) 小的
            }
            rk[x] = cntSmaller + 1;   // x 的新名次
            a[x] = v;                 // 更新数组
        }
    }
    return 0;
}
2022

解密 decode

普及/提高- 文件:decode.in / decode.out 数学一元二次方程完全平方数判定
✓已掌握

【题目描述】

给定一个正整数 k,有 k 次询问,每次给定三个正整数 n_i, e_i, d_i,求两个正整数 p_i, q_i,使:n_i = p_i × q_i,e_i × d_i = (p_i − 1)(q_i − 1) + 1。

【输入格式】

从文件 decode.in 中读入数据。第一行一个正整数 k,表示有 k 次询问。接下来 k 行,第 i 行三个正整数 n_i, d_i, e_i。

【输出格式】

输出到文件 decode.out 中。输出 k 行,每行两个正整数 p_i, q_i 表示答案。为使输出统一,你应当保证 p_i ≤ q_i。如果无解,请输出 NO。

【样例 1】

输入
10
770 77 5
633 1 211
545 1 499
683 3 227
858 3 257
723 37 13
572 26 11
867 17 17
829 3 263
528 4 109
输出
2 385
NO
NO
NO
11 78
3 241
2 286
NO
NO
6 88

【数据范围】

记 m = n − e × d + 2。对于 100% 的数据,1 ≤ k ≤ 10^5,对于任意的 1 ≤ i ≤ k,1 ≤ n_i ≤ 10^18,1 ≤ e_i × d_i ≤ 10^18,1 ≤ m ≤ 10^9。

测试点编号k ≤n ≤m ≤特殊性质
110^310^310^3保证有解
210^310^310^3无
310^310^96×10^4保证有解
410^310^96×10^4无
510^310^910^9保证有解
610^310^910^9无
710^510^1810^9保证若有解则 p = q
810^510^1810^9保证有解
910^510^1810^9无
1010^510^1810^9无
▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

数学推导题。表面是 RSA 的背景,实质是解一个二元方程组,考点在于**如何避免枚举**以及**完全平方数的精确判定**。

【第一步:把条件化简成 p + q 和 p × q】

已知 n = p × q,且 e·d = (p−1)(q−1) + 1 = pq − p − q + 2 = n − (p+q) + 2。

于是 p + q = n − e·d + 2。题目正好把 n − e·d + 2 记作 m,所以:

p + q = m,p × q = n。

【第二步:由和与积反解 p, q】

由一元二次方程求根公式,p 与 q 是方程 x² − m·x + n = 0 的两根,判别式 Δ = m² − 4n = (p − q)²。

因此:若 Δ < 0 则无解;否则令 s = √Δ,要求 s 为整数(即 Δ 是完全平方数),且 (m − s) 与 (m + s) 都能被 2 整除(等价于 m 与 s 同奇偶),且都 > 0。此时 p = (m − s)/2,q = (m + s)/2。

【第三步:完全平方数怎么判才准】

m ≤ 10^9,m² ≤ 10^18,用 long long 可以表示(10^18 < 9.22×10^18)。用 sqrtl 或 (long long)sqrtl((long double)Δ) 得到近似根 s,然后 **用 while 循环微调**:把 s 调整到满足 s² ≤ Δ < (s+1)² 为止,再判断 s² == Δ。

切勿直接比较“sqrt 的返回值是否等于真正的根”而不做微调——浮点误差会导致 ±1 的偏差,这是本题最容易失分的地方。

【复杂度分析】

每次询问 O(1),总复杂度 O(k),k ≤ 10^5,非常快。

【易错点提示】

1. 题目输入顺序是 n, d, e(不是 n, e, d),别读反了;

2. Δ = m² − 4n 可能为负,先判负再开方;

3. 要求 p, q 为正整数,p = 0 也要输出 NO;

4. 保证输出 p ≤ q,即先输出较小的根。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// 返回 x 的整数平方根(向下取整),保证精确
ll isqrt(ll x) {
    if (x < 0) return -1;
    ll s = (ll)sqrtl((long double)x);
    while (s > 0 && s * s > x) --s;
    while ((s + 1) * (s + 1) <= x) ++s;
    return s;
}

int main() {
    freopen("decode.in", "r", stdin);
    freopen("decode.out", "w", stdout);

    int k;
    scanf("%d", &k);
    while (k--) {
        ll n, d, e;
        scanf("%lld%lld%lld", &n, &d, &e);

        ll m = n - e * d + 2;          // = p + q
        ll delta = m * m - 4 * n;      // = (p - q)^2

        if (delta < 0) { printf("NO\n"); continue; }
        ll s = isqrt(delta);
        if (s * s != delta) { printf("NO\n"); continue; }   // 不是完全平方数

        ll p = (m - s) / 2, q = (m + s) / 2;
        if ((m - s) % 2 != 0 || (m + s) % 2 != 0) { printf("NO\n"); continue; }
        if (p <= 0 || q <= 0) { printf("NO\n"); continue; }

        printf("%lld %lld\n", p, q);
    }
    return 0;
}
2023

公路 road

普及/提高- 文件:road.in / road.out 贪心单调栈前缀和
✓已掌握

【题目描述】

小苞准备开着车沿着公路自驾。公路上一共有 n 个站点,编号为从 1 到 n。其中站点 i 与站点 i+1 的距离为 v_i 公里。

公路上每个站点都可以加油,编号为 i 的站点一升油的价格为 a_i 元,且每个站点只出售整数升的油。

小苞想从站点 1 开车到站点 n,一开始小苞在站点 1 且车的油箱是空的。已知车的油箱足够大,可以装下任意多的油,且每升油可以让车前进 d 公里。问小苞从站点 1 开到站点 n,至少要花多少钱加油?

【输入格式】

从文件 road.in 中读入数据。输入的第一行包含两个正整数 n 和 d,分别表示公路上站点的数量和车每升油可以前进的距离。

输入的第二行包含 n − 1 个正整数 v_1, v_2, …, v_{n−1},分别表示站点间的距离。输入的第二行包含 n 个正整数 a_1, a_2, …, a_n,分别表示在不同站点加油的价格。

【输出格式】

输出到文件 road.out 中。输出一行,仅包含一个正整数,表示从站点 1 开到站点 n,小苞至少要花多少钱加油。

【样例 1】

输入
5 4
10 10 10 10
9 8 9 6 5
输出
79

样例解释:最优方案下,小苞在站点 1 买了 3 升油,在站点 2 购买了 5 升油,在站点 4 购买了 2 升油。

【数据范围】

对于所有测试数据保证:1 ≤ n ≤ 10^5,1 ≤ d ≤ 10^5,1 ≤ v_i ≤ 10^5,1 ≤ a_i ≤ 10^5。

测试点n ≤特殊性质
1~58无
6~1010^3无
11~1310^5A
14~1610^5B
17~2010^5无

特殊性质 A:站点 1 的油价最低。特殊性质 B:对于所有 1 ≤ i < n,v_i 为 d 的倍数。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

贪心 + 单调栈。经典“开车加油最少花费”模型:油可以在任意站点买、可以带着走,要决定在哪儿买、买多少。

【贪心策略】

核心结论:在站点 i,如果前方存在一个油价更低的站点 j(a_j < a_i),那么“在 i 处只要买到能撑到 j 的油量”就是最优的——因为 j 之后可以买到更便宜的油,在这里多买是浪费。

反之,如果前方没有任何更便宜的站点,那就说明后面的油都不比 i 便宜,此时应当在 i 处把油买够,一路开到终点 n。

于是算法如下(把“油量”折算成“可行驶里程”便于处理整数升):

1. 用单调栈求出每个站点 i 右边第一个油价更低的站点 nxt[i](不存在则记为 n+1,即终点);

2. 用前缀和求出从 i 到 nxt[i] 的总距离 need_dist;

3. 维护 cur = 当前油箱还能行驶多少公里。若 cur < need_dist,则补买 ⌈(need_dist − cur) / d⌉ 升(花费 ×a_i,cur 增加相应公里数);

4. 从站点 i 开到站点 i+1,cur 减去 v_i。逐站推进即可。

【样例验证】d = 4,a = [9,8,9,6,5],v = [10,10,10,10]】

站点 1:下一个更便宜的是站点 2,距离 10 km;cur=0 < 10,买 ⌈10/4⌉=3 升花 27 元,cur=12;开到站点 2 后 cur=2。

站点 2:下一个更便宜的是站点 4,距离 20 km;cur=2 < 20,买 ⌈18/4⌉=5 升花 40 元,cur=22;开到站点 3 后 cur=12。

站点 3:下一个更便宜的是站点 4,距离 10 km;cur=12 ≥ 10,不买;开到站点 4 后 cur=2。

站点 4:下一个更便宜的是站点 5,距离 10 km;cur=2 < 10,买 ⌈8/4⌉=2 升花 12 元,cur=10;开到站点 5 后 cur=0。

总计 27 + 40 + 12 = 79,与样例一致。

【复杂度分析】

单调栈 O(n),全程一次扫描 O(n),总时间 O(n),空间 O(n)。

【易错点提示】

1. 油只能整升购买,所以补油量必须向上取整;

2. 建议用“可行驶公里数”而不是“升数”作为状态,这样开过一站直接减 v_i,避免分数带来的误差;

3. 单调栈求的是“右边第一个严格更便宜”的站点,注意弹出条件是 a[st.top()] ≥ a[i];

4. 答案可能很大(10^5 × 10^5 × ...),用 long long。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 100005;
ll v[MAXN];       // 站点 i 到 i+1 的距离
ll a[MAXN];       // 站点 i 的油价
ll pre[MAXN];     // 距离前缀和
int nxt[MAXN];    // 右边第一个更便宜的站点

int main() {
    freopen("road.in", "r", stdin);
    freopen("road.out", "w", stdout);

    int n; ll d;
    scanf("%d%lld", &n, &d);
    for (int i = 1; i <= n - 1; ++i) scanf("%lld", &v[i]);
    for (int i = 1; i <= n; ++i) scanf("%lld", &a[i]);

    for (int i = 1; i <= n - 1; ++i) pre[i] = pre[i - 1] + v[i];

    // 单调栈:求 i 右边第一个油价严格更低的站点
    stack<int> st;
    for (int i = n; i >= 1; --i) {
        while (!st.empty() && a[st.top()] >= a[i]) st.pop();
        nxt[i] = st.empty() ? n + 1 : st.top();
        st.push(i);
    }

    ll ans = 0, cur = 0;      // cur: 当前还能行驶的公里数
    for (int i = 1; i < n; ++i) {
        int j = nxt[i];                       // 目标站(可能是 n+1 表示终点)
        ll dist = pre[j - 1] - pre[i - 1];    // 从 i 到 j 的总距离
        if (cur < dist) {
            ll liters = (dist - cur + d - 1) / d;   // 需要补的整升油
            ans += liters * a[i];
            cur += liters * d;
        }
        cur -= v[i];                          // 开到下一站
    }
    printf("%lld\n", ans);
    return 0;
}
2024

地图探险 explore

普及- 文件:explore.in / explore.out 模拟方向数组二维坐标
✓已掌握

【题目描述】

小 A 打算前往一片丛林去探险。丛林的地理环境十分复杂,为了防止迷路,他先派遣了一个机器人前去探路。

丛林的地图可以用一个 n 行 m 列的字符表来表示。我们将第 i 行第 j 列的位置的坐标记作 (i, j)(1 ≤ i ≤ n, 1 ≤ j ≤ m)。如果这个位置的字符为 x,即代表这个位置上有障碍,不可通过。反之,若这个位置的字符为 .,即代表这个位置是一片空地,可以通过。

这个机器人的状态由位置和朝向两部分组成。其中位置由坐标 (x, y)(1 ≤ x ≤ n, 1 ≤ y ≤ m)刻画,它表示机器人处在地图上第 x 行第 y 列的位置。而朝向用一个 0~3 的整数 d 表示,其中 d = 0 代表向东,d = 1 代表向南,d = 2 代表向西,d = 3 代表向北。

初始时,机器人的位置为 (x0, y0),朝向为 d0。保证初始时机器人所在的位置为空地。接下来机器人将要进行 k 次操作。每一步,机器人将按照如下的模式操作:

1. 假设机器人当前处在的位置为 (x, y),朝向为 d。则它的方向上的下一步的位置 (x′, y′) 定义如下:若 d = 0,则令 (x′, y′) = (x, y + 1);若 d = 1,则令 (x′, y′) = (x + 1, y);若 d = 2,则令 (x′, y′) = (x, y − 1);若 d = 3,则令 (x′, y′) = (x − 1, y)。

2. 接下来,机器人判断它下一步的位置是否在地图内,且是否为空地。若条件成立,则机器人会向前走一步;若条件不成立,则它会执行“向右转”操作,即令 d′ = (d + 1) mod 4,且它所处的位置保持不变,但朝向由 d 变为 d′。

小 A 想要知道,在机器人执行完 k 步操作之后,地图上所有被机器人经过的位置(包括起始位置)有几个。

【输入格式】

从文件 explore.in 中读入数据。本题有多组测试数据。输入的第一行包含一个正整数 T,表示数据组数。接下来包含 T 组数据,每组数据的格式如下:

第一行包含三个正整数 n, m, k。第二行包含两个正整数 x0, y0 和一个非负整数 d0。接下来 n 行,每行包含一个长度为 m 的字符串。

【输出格式】

输出到文件 explore.out 中。对于每组数据:输出一行包含一个正整数,表示地图上所有被机器人经过的位置(包括起始位置)的个数。

【样例 1】

输入
2
1 5 4
1 1 2
....x
5 5 20
1 1 0
.....
.xxx.
.x.x.
..xx.
x....
输出
3
13

样例解释:第一组数据中机器人四步之后经过 (1,1),(1,2),(1,3) 三个位置。第二组数据机器人最终经过 13 个不同位置。

【数据范围】

对于所有测试数据,保证:1 ≤ T ≤ 5,1 ≤ n, m ≤ 10^3,1 ≤ k ≤ 10^6,1 ≤ x0 ≤ n,1 ≤ y0 ≤ m,0 ≤ d0 ≤ 3,且机器人的起始位置为空地。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

纯模拟。考点只有一个:把题目的移动规则准确翻译成代码,用方向数组避免写四段重复代码。

【算法思路】

1. 用方向数组 dx[] = {0, 1, 0, -1}、dy[] = {1, 0, -1, 0}(分别对应东、南、西、北)。

2. 用一个 bool 数组 vis[x][y] 记录该位置是否被经过,初始把起点标记为已访问。

3. 重复 k 次:计算下一步 (x + dx[d], y + dy[d]);若越界或是障碍('x'),则 d = (d + 1) % 4(右转,位置不变);否则移动过去并把新位置标记为已访问。

4. 最后统计 vis 中为 true 的格子数。

优化:k 最大 10^6,T ≤ 5,总步数最多 5 × 10^6,直接模拟完全够。如果担心重复,也可以只统计“首次到达”的数量(每走一步若是新格子就 ans++),省掉最后的全图扫描。

【复杂度分析】

时间 O(T·k + T·nm),空间 O(nm)。

【易错点提示】

1. 坐标是“行 x,列 y”,先判 x 再判 y,很容易写反;

2. 右转是 d = (d + 1) % 4,注意本题 d=1 是南(向下),与数学坐标系不同;

3. 起点也算“经过”,别忘了标记;

4. 多组数据要清空 vis 数组。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1005;
char mp[MAXN][MAXN];
bool vis[MAXN][MAXN];
// 东(d=0) 南(d=1) 西(d=2) 北(d=3)
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};

int main() {
    freopen("explore.in", "r", stdin);
    freopen("explore.out", "w", stdout);

    int T;
    scanf("%d", &T);
    while (T--) {
        int n, m, k;
        scanf("%d%d%d", &n, &m, &k);
        int x, y, d;
        scanf("%d%d%d", &x, &y, &d);
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j)
                vis[i][j] = false, mp[i][j] = 0;
        for (int i = 1; i <= n; ++i) scanf("%s", mp[i] + 1);

        int ans = 0;
        vis[x][y] = true; ++ans;              // 起点算经过

        for (int step = 0; step < k; ++step) {
            int nx = x + dx[d], ny = y + dy[d];
            if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && mp[nx][ny] == '.') {
                x = nx; y = ny;               // 前进
                if (!vis[x][y]) { vis[x][y] = true; ++ans; }
            } else {
                d = (d + 1) % 4;              // 右转,原地不动
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}
2025

座位 seat

普及/提高- 文件:seat.in / seat.out 模拟排序数学
✓已掌握

【题目描述】

CSP-J 2025 第二轮正在进行。小 R 所在的考场共有 n×m 名考生,其中所有考生的 CSP-J 2025 第一轮成绩互不相同。所有 n×m 名考生将按照第一轮成绩由高到低蛇形分配座位,排列成 n 行 m 列。

具体地,设成绩从高到低分别为 s₁ > s₂ > … > s_{n×m},则成绩为 s₁ 的考生座位为第 1 列第 1 行,s₂ 为第 1 列第 2 行,……,s_n 为第 1 列第 n 行;s_{n+1} 为第 2 列第 n 行,……,s_{2n} 为第 2 列第 1 行;s_{2n+1} 为第 3 列第 1 行,以此类推(蛇形)。

给定座位的行数 n、列数 m,以及所有考生第一轮成绩 a₁, a₂, …, a_{n×m},其中 a₁ 为小 R 的成绩,你需要帮助小 R 求出他的座位是第几列第几行。

【输入格式】

输入文件名为 seat.in。第一行两个正整数 n, m,表示座位的行数与列数。第二行 n×m 个正整数 a₁, a₂, …, a_{n×m},表示所有考生的成绩,其中 a₁ 为小 R 的成绩。

【输出格式】

输出文件名为 seat.out。输出一行两个正整数 c, r,表示小 R 的座位为第 c 列第 r 行。

【样例 1】

输入
2 2
99 100 97 98
输出
1 2

按成绩从高到低:100 在第 1 列第 1 行,99 在第 1 列第 2 行,98 在第 2 列第 2 行,97 在第 2 列第 1 行。小 R 成绩 99,座位为第 1 列第 2 行。

【样例 2】

输入
2 2
98 99 100 97
输出
2 2

小 R 成绩 98,座位为第 2 列第 2 行。

【样例 3】

输入
3 3
94 95 96 97 98 99 100 93 92
输出
3 1

【数据规模与约定】

对于所有测试数据,保证 1 ≤ n ≤ 10,1 ≤ m ≤ 10;对于所有 1 ≤ i ≤ n×m,均有 1 ≤ aᵢ ≤ 100,且 a₁, a₂, …, a_{n×m} 互不相同。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

模拟题。关键在于理解「蛇形分配」:第 1 列从上往下(行 1→n),第 2 列从下往上(行 n→1),第 3 列又从上往下……奇数列向下、偶数列向上。小 R 的成绩是 a₁,需要算出 a₁ 在所有成绩中的排名,再换算成 (列, 行)。

【算法思路】

1. 先确定 a₁ 的排名 rank:统计有多少个成绩严格大于 a₁,即 rank = 1 + count(aᵢ > a₁)。

2. 由排名换算座位。第 (rank-1) 个人(0 起始编号 k = rank-1):列号 c = k / n + 1;在该列内的行号 t = k % n + 1。

3. 若 c 为奇数列(1,3,5…),行号 r = t;若 c 为偶数列(2,4,6…),行号 r = n - t + 1(蛇形折返)。

4. 输出 c 和 r。

【复杂度分析】

O(n×m),n, m ≤ 10,最多 100 个数,完全足够。

【易错点提示】

1. 排名要从 1 开始数,且是「严格大于 a₁」的个数 + 1;

2. 偶数列是倒着数的(蛇形折返),行号要翻转;

3. 输出顺序是「列 c、行 r」,不要写反。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

int main() {
    freopen("seat.in", "r", stdin);
    freopen("seat.out", "w", stdout);

    int n, m;
    cin >> n >> m;
    vector<int> a(n * m);
    for (int i = 0; i < n * m; ++i) cin >> a[i];

    int me = a[0];
    // 排名 = 严格大于 a1 的个数 + 1
    int rank = 1;
    for (int i = 0; i < n * m; ++i)
        if (a[i] > me) rank++;

    int k = rank - 1;        // 0 起始编号
    int c = k / n + 1;       // 第几列
    int t = k % n + 1;       // 列内从第 1 行数起的位置
    int r;
    if (c % 2 == 1) r = t;   // 奇数列:从上往下
    else            r = n - t + 1;  // 偶数列:从下往上(蛇形)

    cout << c << " " << r << endl;
    return 0;
}
📕
T3

T3 · 第三题 专项

本组共 7 题,来自 2019–2025 年 CSP-J 第二轮认证。

2019

纪念品 souvenir

普及+/提高 文件:souvenir.in / souvenir.out 动态规划完全背包
✓已掌握

【题目描述】

小伟突然获得一种超能力,他知道未来 T 天 N 种纪念品每天的价格。某个纪念品的价格是指购买一个该纪念品所需的金币数量,以及卖出一个该纪念品换回的金币数量。

每天,小伟可以进行以下两种交易无限次:1. 任选一个纪念品,若手上有足够金币,以当日价格购买该纪念品;2. 卖出持有的任意一个纪念品,以当日价格换回金币。

每天卖出纪念品换回的金币可以立即用于购买纪念品,当日购买的纪念品也可以当日卖出换回金币。当然,一直持有纪念品也是可以的。

T 天之后,小伟的超能力消失。因此他一定会在第 T 天卖出所有纪念品换回金币。小伟现在有 M 枚金币,他想要在超能力消失后拥有尽可能多的金币。

【输入格式】

输入文件名为 souvenir.in。第一行包含三个正整数 T, N, M,相邻两数之间以一个空格分开,分别代表未来天数 T,纪念品数量 N,小伟现在拥有的金币数量 M。

接下来 T 行,每行包含 N 个正整数,第 i 行的 N 个正整数分别为 P_{i,1} … P_{i,N},其中 P_{i,j} 表示第 i 天第 j 种纪念品的价格。

【输出格式】

输出文件名为 souvenir.out。输出仅一行,包含一个正整数,表示小伟在超能力消失后最多能拥有的金币数量。

【输入输出样例 1】

输入
6 1 100
50
20
25
20
25
50
输出
305

【输入输出样例 2】

输入
3 3 100
10 20 15
15 17 13
15 25 16
输出
217

【数据规模与约定】

对于 10% 的数据,T = 1。

对于 30% 的数据,T ≤ 4, N ≤ 4, M ≤ 100,所有价格 10 ≤ P_{i,j} ≤ 100。

另有 15% 的数据,T ≤ 100, N = 1。

另有 15% 的数据,T = 2, N ≤ 100。

对于 100% 的数据,T ≤ 100, N ≤ 100, M ≤ 10^3,所有价格 1 ≤ P_{i,j} ≤ 10^4,数据保证任意时刻小伟手上的金币数不可能超过 10^4。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

动态规划(完全背包)。难点在于把“跨天持有”这件事拆解成“相邻两天之间做一次套利”,从而化归为完全背包模型。

【核心转化:分天独立】

假设我们只考虑第 i 天和第 i+1 天。若第 i 天买、第 i+1 天卖某个纪念品 j,则每投入 P_{i,j} 枚金币可换回 P_{i+1,j} 枚,净赚 (P_{i+1,j} − P_{i,j})。

关键结论:任意一种最优策略,都可以等价地看成“每天结束时清仓,第二天重新开始买”的序列。因为如果在第 i 天买入、第 k 天(k>i+1)才卖掉,那么中间的每一天价格无论如何变化,我们都可以把“第 i 天买、第 i+1 天卖、第 i+1 天再买、……”这一串操作串起来,收益完全相同(因为买卖同价,中间对冲掉了)。所以问题被分解为 T−1 个互相独立的两天子问题,且金币在子问题之间顺次累积。

【完全背包建模】

对每个子问题(第 i 天 → 第 i+1 天):把第 i 天的价格 P_{i,j} 看成物品 j 的“花费”,把收益 (P_{i+1,j} − P_{i,j}) 看成“价值”。每种纪念品可以买无限个(完全背包),背包容量是当前持有的金币数 m。跑一次完全背包得到本子问题的最大收益 gain,令 m += gain。

注意只考虑 P_{i+1,j} > P_{i,j} 的纪念品(有正收益);其余的“价值”≤ 0,不会买。

【复杂度分析】

每个子问题 O(N·M),共 T−1 个子问题,总复杂度 O(T·N·M) = 100 × 100 × 1000 = 10^7,轻松通过。

【易错点提示】

1. 完全背包的容量循环必须“从小到大”(正序),才能让同一物品被重复选取;01 背包才是倒序;

2. dp 数组要按当前金币数 m 的上界开(M 会随收益增长,但题目保证不超过 10^4);

3. 每一天结束后要把 dp 数组清零再进入下一轮。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

const int MAXT = 105, MAXN = 105, MAXP = 10005;
int T, N, M;
int p[MAXT][MAXN];   // p[day][item]
int dp[MAXP];        // 完全背包

int main() {
    freopen("souvenir.in", "r", stdin);
    freopen("souvenir.out", "w", stdout);

    scanf("%d%d%d", &T, &N, &M);
    for (int i = 1; i <= T; ++i)
        for (int j = 1; j <= N; ++j)
            scanf("%d", &p[i][j]);

    for (int day = 1; day < T; ++day) {          // T-1 个子问题
        memset(dp, 0, sizeof(dp));
        for (int j = 1; j <= N; ++j) {           // 第 j 种纪念品
            int cost = p[day][j];
            int gain = p[day + 1][j] - p[day][j];
            if (gain <= 0) continue;             // 无收益不买
            for (int k = cost; k <= M; ++k)      // 完全背包:正序!
                dp[k] = max(dp[k], dp[k - cost] + gain);
        }
        M += dp[M];                              // 金币累积
    }
    printf("%d\n", M);
    return 0;
}
2020

表达式 expr

普及+/提高 文件:expr.in / expr.out 表达式树栈树形 DFS后缀表达式
✓已掌握

【题目描述】

小 C 热衷于学习数理逻辑。有一天,他发现了一种特别的逻辑表达式。在这种逻辑表达式中,所有操作数都是变量,且它们的取值只能为 0 或 1,运算从左往右进行。如果表达式中有括号,则先计算括号内的子表达式的值。特别的,这种表达式有且仅有以下几种运算:

1. 与运算(&):当且仅当两个参与运算的值都为 1 时,该表达式的值为 1;其余情况该表达式的值为 0。

2. 或运算(|):当且仅当两个参与运算的值都为 0 时,该表达式的值为 0;其余情况该表达式的值为 1。

3. 取反运算(!):当且仅当参与运算的值为 0 时,该表达式的值为 1;其余情况为 0。

小 C 想知道,给定一个逻辑表达式和其中每一个操作数的初始取值后,再取反某一个操作数的值时,原表达式的值为多少。

为了化简对表达式的处理,表达式将采用后缀表达式的形式输入。后缀表达式的定义:若 E 是一个操作数,则 E 的后缀表达式是它本身;若 E 是 E1 op E2 形式的表达式,则 E 的后缀表达式是 E1 E2 op;若 E 是 !E1 形式的表达式,则 E 的后缀表达式是 E1 !。

同时为了方便,输入中:a) 与运算符(&)、或运算符(|)、取反运算符(!)的左右均有一个空格,但表达式末尾没有空格;b) 操作数由小写字母 x 与一个正整数拼接而成,正整数表示这个变量的下标。例如 x10 表示下标为 10 的变量。数据保证每个变量在表达式中出现恰好一次。

【输入格式】

输入文件名为 expr.in。第一行包含一个字符串 s,表示表达式。第二行包含一个正整数 n,表示表达式中变量的数量。表达式中变量的下标为 1, 2, …, n。第三行包含 n 个整数,第 i 个整数表示变量 i 的初值。第四行包含一个正整数 q,表示询问的个数。接下来 q 行,每行一个正整数,表示需要取反的变量的下标。注意,每一个询问的修改都是临时的,即之前询问中的修改不会对之后的询问造成影响。数据保证输入的表达式合法。变量的初值为 0 或 1。

【输出格式】

输出文件名为 expr.out。输出一共有 q 行,每行一个 0 或 1,表示该询问下表达式的值。

【输入输出样例 1】

输入
x1 x2 & x3 |
3
1 0 1
3
1
2
3
输出
1
1
0

样例 1 说明:后缀表达式 x1 x2 & x3 | 的中缀形式为 (x1 & x2) | x3。第一次询问翻转 x1,此时 (0&0)|1=1;第二次翻转 x2,(1&1)|1=1;第三次翻转 x3,(1&0)|0=0。

【输入输出样例 2】

输入
x1 ! x2 x4 | x3 x5 ! & & ! &
5
0 1 0 1 1
3
1
3
5
输出
0
1
1

样例 2 说明:该表达式的中缀形式为 (!x1) & (!((x2 | x4) & (x3 & (!x5))))。三次询问分别翻转 x1、x3、x5,对应输出为 0、1、1。

【数据范围与提示】

对于 20% 的数据,表达式中有且仅有与运算(&)或者或运算(|)。

对于另外 30% 的数据,|s| ≤ 1000,q ≤ 1000,n ≤ 1000。

对于另外 20% 的数据,变量的初值全为 0 或全为 1。

对于 100% 的数据,1 ≤ |s| ≤ 1 × 10^6,1 ≤ q ≤ 1 × 10^5,2 ≤ n ≤ 1 × 10^5。其中,|s| 表示字符串 s 的长度。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

表达式树 + 树形 DP(标记下传)。如果每次询问都重新求一遍表达式,复杂度 O(q·|s|) 会超时(10^5 × 10^6 不可接受)。观察:每次只把“一个叶子”翻转,绝大多数节点的值不会变。我们要把这个“牵一发动全身”的影响范围提前算出来。

【第一步:用栈建表达式树】

从左到右扫描后缀表达式,遇到变量 x_k 就建立叶结点 value = a_k 并入栈;遇到 ! 就弹出栈顶作为它的唯一儿子,建立结点并计算值;遇到 & 或 | 就弹出两个结点作为左右儿子(注意先弹出的是右儿子),建结点、计算值、入栈。最后栈中唯一结点就是根。

【第二步:标记“无效结点”】

定义 tag[u] = 1 表示“结点 u 的值无论如何变化,都不会影响根结点的值”。显然根结点 tag 为 0。自顶向下传播:

· 若某个结点 u 已被标记为无效(tag[u]=1),那么它的两个儿子也一定无效——无论子树怎么变,根都不会变。

· 若 u 是 &:若它的某个儿子值为 0,那么另一个儿子的值不影响结果(因为 0 & anything = 0),把那个“无关”的儿子标记为无效;如果两个儿子都是 1,则两个儿子的变化都会影响结果,都不标记。

· 若 u 是 |:若它的某个儿子值为 1,另一个儿子的值不影响结果(1 | anything = 1),把无关的儿子标记为无效;若两个儿子都是 0,则都不标记。

· 若 u 是 !:儿子必须标记「变化会影响」,即不额外标记。

【第三步:回答询问】

对于询问“把变量 x 取反”:如果叶子 x 已经被标记为无效,说明翻转它不影响根的值,直接输出根的原值;否则输出根值取反。这一步 O(1)。

直观理解:从叶子 x 到根的路径上,只要有一个结点被标记为无效,那么这一串“翻转”的连锁反应就会被“短路”掉,根的最终值不变;反之,若路径上没有被标记的点,翻转会一路上传,正好把根取反。

【为什么标记可以下传】

若 u 的值不影响根,则 u 的任意子树的值变化也不影响根,故 u 的两个儿子都可标记。这是标记下传的合法性依据。

【复杂度分析】

建树 O(|s|),标记一次 O(|s|),共 q 次 O(1) 查询。总复杂度 O(|s| + q),空间 O(|s|)。

【易错点提示】

1. 建树时弹栈顺序:先弹出的作为右儿子,后弹出的作为左儿子(对 & 和 | 都满足交换律,但写成左/右更规范);

2. 字符串含空格,务必用 getchar 或 getline 整行读入,然后在扫描时跳过空格;

3. 变量下标可能多位(x10、x100),读数字时要循环读直到非数字字符;

4. 数组要按 |s| 的最大规模开到 10^6 级别。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

const int MAXS = 1000005;
int val[MAXS];        // 每个结点的值
int lc[MAXS], rc[MAXS];   // 左右儿子(0 表示无)
char op[MAXS];        // 结点的运算符:'&' '|' '!' 或 0 表示叶子
bool dead[MAXS];      // dead=1 表示该结点的值变化不影响根结点
int a[MAXS];          // 变量初值
char s[MAXS];

// 自顶向下打标记
void dfs(int u, bool flag) {   // flag: 该结点是否“无关”
    if (dead[u]) return;
    if (flag) { dead[u] = true; }
    if (lc[u] == 0 && rc[u] == 0) { if (flag) dead[u] = true; return; }
    if (op[u] == '!') {
        if (flag) dead[u] = true;
        dfs(lc[u], flag);
    } else if (op[u] == '&') {
        if (val[lc[u]] == 0) dfs(rc[u], true);   // 右儿子无关
        else                 dfs(rc[u], flag);
        if (val[rc[u]] == 0) dfs(lc[u], true);   // 左儿子无关
        else                 dfs(lc[u], flag);
    } else { // '|'
        if (val[lc[u]] == 1) dfs(rc[u], true);   // 右儿子无关
        else                 dfs(rc[u], flag);
        if (val[rc[u]] == 1) dfs(lc[u], true);   // 左儿子无关
        else                 dfs(lc[u], flag);
    }
}

int main() {
    freopen("expr.in", "r", stdin);
    freopen("expr.out", "w", stdout);

    // 读入整行表达式(第一行)
    int len = 0, c;
    while ((c = getchar()) != '\n' && c != EOF) s[++len] = (char)c;

    int n; scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);

    int cnt = 0;                 // 结点编号
    int st[MAXS], top = 0;       // 栈

    for (int i = 1; i <= len; ++i) {
        if (s[i] == ' ') continue;
        if (s[i] == 'x') {                       // 变量:读下标
            int k = 0, j = i + 1;
            while (j <= len && isdigit(s[j])) k = k * 10 + (s[j] - '0'), ++j;
            i = j - 1;
            int u = k;                           // 直接用变量下标作叶结点编号
            val[u] = a[k];
            op[u] = 0; lc[u] = rc[u] = 0;
            st[++top] = u;
        } else if (s[i] == '!') {                // 单目
            ++cnt;
            int u = MAXS - cnt;                  // 从数组尾部倒着分配编号
            lc[u] = st[top--]; rc[u] = 0;
            op[u] = '!';
            val[u] = !val[lc[u]];
            st[++top] = u;
        } else {                                 // 双目 & 或 |
            ++cnt;
            int u = MAXS - cnt;
            int b = st[top--], aa = st[top--];   // 先弹出的是右操作数
            lc[u] = aa; rc[u] = b;
            op[u] = s[i];
            val[u] = (s[i] == '&') ? (val[aa] & val[b]) : (val[aa] | val[b]);
            st[++top] = u;
        }
    }
    int root = st[top];
    int origin = val[root];

    dfs(root, false);

    int q; scanf("%d", &q);
    while (q--) {
        int x; scanf("%d", &x);
        if (dead[x]) printf("%d\n", origin);        // 翻转无影响
        else         printf("%d\n", origin ^ 1);    // 翻转根也翻转
    }
    return 0;
}
2021

网络连接 network

普及+/提高 文件:network.in / network.out 字符串处理模拟哈希表 map
✓已掌握

【题目描述】

TCP/IP 协议是网络通信领域的一项重要协议。今天你的任务,就是尝试利用这个协议,还原一个简化后的网络连接场景。

在本问题中,计算机分为两大类:服务机(Server)和客户机(Client)。服务机负责建立连接,客户机负责加入连接。需要进行网络连接的计算机共有 n 台,编号为 1~n,这些机器将按编号递增的顺序,依次发起一条建立连接或加入连接的操作。

每台机器在尝试建立或加入连接时需要提供一个地址串。一个符合规范的地址串应当具有以下特征:

1. 必须形如 a.b.c.d:e 的格式,其中 a, b, c, d, e 均为非负整数;

2. 0 ≤ a, b, c, d ≤ 255,0 ≤ e ≤ 65535;

3. a, b, c, d, e 均不能含有多余的前导 0。

例如,地址串 192.168.0.255:80 是符合规范的,但 192.168.0.999:80、192.168.00.1:10、192.168.0.1:088、192:168:0:1.233 均是不符合规范的。

如果服务机或客户机在发起操作时提供的地址串不符合规范,这条操作将被直接忽略。由于网络阻塞等原因,不允许两台服务机使用相同的地址串,如果此类现象发生,后一台尝试建立连接的服务机将会无法成功建立连接;除此之外,凡是提供符合规范的地址串的服务机均可成功建立连接。

如果某台提供符合规范的地址的客户机在尝试加入连接时,与先前某台已经成功建立连接的服务机提供的地址串相同,这台客户机就可以成功加入连接,并称其连接到这台服务机;如果找不到这样的服务机,则认为这台客户机无法成功加入连接。

【输入格式】

从文件 network.in 中读入数据。第 1 行,一个正整数 n。接下来 n 行,每行 2 个字符串 op, ad,按照编号从小到大给出每台计算机的类型及地址串。其中 op 保证为字符串 Server 或 Client 之一,ad 为一个长度不超过 25 的、仅由数字、字符 . 和字符 : 组成的非空字符串。

【输出格式】

输出到文件 network.out 中。输出共 n 行,每行一个正整数或字符串表示第 i 台计算机的连接状态。

如果第 i 台计算机为服务机,则:提供符合规范的地址串且成功建立连接,输出 OK;地址符合规范但有相同地址串的服务机在先,输出 FAIL;地址不合规范,输出 ERR。

如果第 i 台计算机为客户机,则:地址符合规范且成功加入连接,输出所连接到的服务机编号(正整数);地址符合规范但无法加入,输出 FAIL;地址不合规范,输出 ERR。

【样例 1】

输入
5
Server 192.168.1.1:8080
Server 192.168.1.1:8080
Client 192.168.1.1:8080
Client 192.168.1.1:80
Client 192.168.1.1:99999
输出
OK
FAIL
1
FAIL
ERR

【样例 2】

输入
10
Server 192.168.1.1:80
Client 192.168.1.1:80
Client 192.168.1.1:8080
Server 192.168.1.1:80
Server 192.168.1.1:8080
Server 192.168.1.999:0
Client 192.168.1.1.8080
Client 192.168.1.1:8080
Client 192.168.1.1:80
Client 192.168.1.999:0
输出
OK
1
FAIL
FAIL
OK
ERR
ERR
5
1
ERR

【数据范围】

对于 100% 的数据,保证 1 ≤ n ≤ 1000。存在若干特殊性质测试点:性质 1 保证所有地址串均符合规范;性质 2 保证同类机器地址串互不相同;性质 3 保证任意一台服务机的编号都小于所有客户机;性质 4 保证地址串形如 a.b.c.d:e 且五个数均不超过 10^9 且无前导 0;性质 5 保证五个数均为只含数字的非空字符串。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

字符串解析 + 哈希表(map)。本题是典型的“大模拟 + 细节判断”题,考的是把题目规则一丝不漏地翻译成代码的能力,是 CSP-J 第三题的常见风格。

【第一步:规范性判定 check(ad)】

一个合法地址必须是“四个点分数字 + 一个冒号 + 一个数字”。解析思路:

1. 按顺序解析:依次读出由数字组成的段,每两段之间必须恰好是一个 '.',共 4 段;第 4 段之后必须是 ':',然后读出最后一段数字(端口),且最后一段必须紧贴字符串末尾,不能再有形如 '.1' 这样的多余内容。

2. 每段必须非空;若某段长度 > 1 且首字符为 '0',说明有前导 0,非法。

3. 前四段的值必须 ≤ 255;最后一段(端口)必须 ≤ 65535。

实现上推荐用一个指针 p 从前向后走,循环 4 次:读数字串 → 判断合法性 → 若 p 未到末尾且 s[p] == '.' 则 p++ 继续;第 4 段之后再要求 s[p] == ':',p++,读最后一段,且读完后 p 必须等于 len。

【第二步:用 map 维护服务器】

维护 unordered_map<string, int> server,键为地址串,值为成功建立连接的服务机编号。逐台处理:

· Server:若地址不合规范 → ERR;否则若 map 中已存在该地址 → FAIL;否则 server[ad] = i,输出 OK。

· Client:若地址不合规范 → ERR;否则若 map 中存在 → 输出 server[ad];否则输出 FAIL。

注意“不合法地址的操作被直接忽略”,也就是说它不会占用地址,后续机器不受影响。

【复杂度分析】

每台机器处理 O(|ad| + log n),总复杂度 O(n·|ad|),n ≤ 1000,非常宽裕。

【易错点提示】

1. 前导 0 的判定要对全部五段都做(含端口),「0」本身是合法的;

2. 端口上限是 65535 而不是 65536;

3. 冒号和点的数量要恰好:4 个点、1 个冒号,且顺序必须先点后冒号;

4. 只有当 Server 地址合法且未被占用时才登记进 map,被 FAIL 的地址以后也不算已占用;

5. 地址可能很长(数字可以很大),需要用字符串原样比较,不能转成整数。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

string s;

// 判断地址串 ad 是否规范
bool check(const string &ad) {
    int len = ad.size(), p = 0;
    int seg[5];

    for (int i = 0; i < 4; ++i) {
        int st = p;
        while (p < len && isdigit(ad[p])) ++p;
        if (p == st) return false;                       // 空段
        if (p - st > 1 && ad[st] == '0') return false;   // 前导 0
        long long v = 0;
        for (int k = st; k < p; ++k) v = v * 10 + (ad[k] - '0');
        if (v > 255) return false;
        seg[i] = (int)v;

        if (i < 3) {                                     // 前 3 段后必须是 '.'
            if (p >= len || ad[p] != '.') return false;
            ++p;
        }
    }
    if (p >= len || ad[p] != ':') return false;          // 第 4 段后必须是 ':'
    ++p;

    int st = p;
    while (p < len && isdigit(ad[p])) ++p;
    if (p == st) return false;                           // 端口为空
    if (p != len) return false;                          // 端口后不能有多余字符
    if (p - st > 1 && ad[st] == '0') return false;       // 端口前导 0
    long long e = 0;
    for (int k = st; k < p; ++k) e = e * 10 + (ad[k] - '0');
    if (e > 65535) return false;

    return true;
}

int main() {
    freopen("network.in", "r", stdin);
    freopen("network.out", "w", stdout);

    int n;
    scanf("%d", &n);

    unordered_map<string, int> server;   // 地址 -> 成功建立连接的服务机编号

    for (int i = 1; i <= n; ++i) {
        char op[20];
        scanf("%s", op);
        cin >> s;

        bool ok = check(s);
        if (!strcmp(op, "Server")) {
            if (!ok) { printf("ERR\n"); continue; }
            if (server.count(s)) { printf("FAIL\n"); }
            else { server[s] = i; printf("OK\n"); }
        } else {
            if (!ok) { printf("ERR\n"); continue; }
            if (server.count(s)) printf("%d\n", server[s]);
            else printf("FAIL\n");
        }
    }
    return 0;
}
2022

逻辑表达式 expr

普及+/提高 文件:expr.in / expr.out 表达式求值递归下降表达式树栈
✓已掌握

【题目描述】

逻辑表达式是计算机科学中的重要概念和工具,包含逻辑值、逻辑运算、逻辑运算优先级等内容。在一个逻辑表达式中,元素的值只有两种可能:0(表示假)和 1(表示真)。元素之间有多种可能的逻辑运算,本题中只需考虑如下两种:“与”(符号为 &)和“或”(符号为 |)。其运算规则如下:

0 & 0 = 0 & 1 = 1 & 0 = 0,1 & 1 = 1;

0 | 0 = 0,0 | 1 = 1 | 0 = 1 | 1 = 1。

在一个逻辑表达式中还可能有括号。规定在运算时,括号内的部分先运算;两种运算并列时,& 运算优先于 | 运算;同种运算并列时,从左向右运算。

比如,表达式 0|1&0 的运算顺序等同于 0|(1&0);表达式 0&1&0|1 的运算顺序等同于 ((0&1)&0)|1。

此外,在 C++ 等语言的有些编译器中,对逻辑表达式的计算会采用一种“短路”的策略:在形如 a&b 的逻辑表达式中,会先计算 a 部分的值,如果 a = 0,那么整个逻辑表达式的值就确定为 0,故无需再计算 b 部分的值;同理,在形如 a|b 的逻辑表达式中,会先计算 a 部分的值,如果 a = 1,那么整个逻辑表达式的值就确定为 1,无需再计算 b 部分的值。

现在给你一个逻辑表达式,你需要计算出它的值,并且统计出在计算过程中,两种类型的“短路”各出现了多少次。需要注意的是,如果某处“短路”包含在更外层被“短路”的部分内则不被统计,如表达式 1|(0&1) 中,尽管 0&1 是一处“短路”,但由于外层的 1|(0&1) 本身就是一处“短路”,无需再计算 0&1 部分的值,因此不应当把这里的 0&1 计入一处“短路”。

【输入格式】

从文件 expr.in 中读入数据。输入共一行,一个非空字符串 s 表示待计算的逻辑表达式。

【输出格式】

输出到文件 expr.out 中。输出共两行,第一行输出一个字符 0 或 1,表示这个逻辑表达式的值;第二行输出两个非负整数,分别表示计算上述逻辑表达式的过程中,形如 a&b 和 a|b 的“短路”各出现了多少次。

【样例 1】

输入
0&(1|0)|(1|1|1&0)
输出
1
1 2

样例解释:0&(1|0)|(1|1|1&0) = (0&(1|0))|((1|1)|(1&0)) = 0|((1|1)|(1&0)) —— 第一次短路(&型);= 0|(1|(1&0)) —— 第二次短路(|型);= 0|1 —— 第三次短路(|型);= 1。

【样例 2】

输入
(0|1&0|1|1|(1|1))&(0&1&(1|0)|0|1|0)&0
输出
0
2 3

【数据范围】

设 |s| 为字符串 s 的长度。对于所有数据,1 ≤ |s| ≤ 10^6。保证 s 中仅含有字符 0、1、&、|、(、) 且是一个符合规范的逻辑表达式。保证输入字符串的开头、中间和结尾均无额外的空格。保证 s 中没有重复的括号嵌套(即没有形如 ((a)) 形式的子串,其中 a 是符合规范的逻辑表达式)。

测试点编号|s| ≤特殊条件
1~23无
3~45无
520001
620002
720003
8~102000无
11~1210^61
13~1410^62
15~1710^63
18~2010^6无

其中:特殊性质 1 为:保证 s 中没有字符 &;特殊性质 2 为:保证 s 中没有字符 |;特殊性质 3 为:保证 s 中没有字符 ( 和 )。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

表达式求值(递归下降 / 表达式树)+ 短路计数。考点有两个:一是正确处理 & 高于 | 的优先级与括号;二是理解“短路不重复计数”的含义,把计数规则准确地翻译成递归。

【文法与递归下降】

按优先级写出文法(从低到高):

expr := term ( '|' term )*

term := factor ( '&' factor )*

factor := '0' | '1' | '(' expr ')'

用一个全局指针 p 扫描字符串,就能自然地实现“& 优先于 |”以及括号嵌套。

【短路计数的关键】

在 term 里处理 a & b(以及多个 & 串联):先求左操作数的值 v。若 v == 0,说明发生一次“&型短路”,此时**直接返回 0,并跳过右操作数(不进入它的计算,也就不会统计它内部的任何短路)**;否则继续求右操作数。

在 expr 里处理 a | b(多个 | 串联同理):先求左操作数的值 v。若 v == 1,发生一次“|型短路”,**直接返回 1,跳过右边**;否则继续。

“跳过右边”这一步正是题目要求的“外层短路会屏蔽内层短路统计”——因为我们根本不会进入被短路的子树去递归计算,自然不会误计数。

【实现细节】

· 字符串长度可达 10^6,递归深度可能很大(例如 100 万个连续 |),需要把栈开大(局部静态数组比较占空间,建议用全局),或者改用显式栈 + 表达式树的迭代写法。

· 在本机(Windows)调试时若爆栈,可把 s 声明为全局字符数组并使用更大栈的评测机提交。

· 计数的两个变量用 long long(最大短路次数不超过 10^6,int 也够,但用 long long 更保险)。

【复杂度分析】

每个字符最多被访问常数次,时间 O(|s|),空间 O(|s|)。

【易错点提示】

1. 短路后必须“返回值但不继续算右边”,不能先算右边再丢弃;

2. 输出第一行是表达式的值,第二行是“&短路数 |短路数”,顺序别写反;

3. 样例 1 的解析共 3 次短路:1 次 & 型 + 2 次 | 型,输出 “1 2”,注意对应关系。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 2000005;
char s[MAXN];
int lc[MAXN], rc[MAXN], val[MAXN];
char op[MAXN];              // '0' '1' '&' '|',0 表示叶子
int cnt = 0;                // 结点计数(叶子与内部结点共用编号,子结点编号一定小于父结点)
char ops[MAXN]; int optop = 0;      // 运算符栈,用 '(' 作分隔标记
int nodeStk[MAXN]; int ntop = 0;    // 操作数(结点)栈
bool reach[MAXN];           // 该结点是否真的会被求值(未被短路)
ll cntAnd = 0, cntOr = 0;

int prec(char c) { return c == '&' ? 2 : (c == '|' ? 1 : 0); }

void reduce() {             // 出栈一个运算符,合并两个操作数
    char c = ops[optop--];
    int r = nodeStk[ntop--];
    int l = nodeStk[ntop--];
    ++cnt;
    lc[cnt] = l; rc[cnt] = r; op[cnt] = c;
    val[cnt] = (c == '&') ? (val[l] & val[r]) : (val[l] | val[r]);
    nodeStk[++ntop] = cnt;
}

int main() {
    freopen("expr.in", "r", stdin);
    freopen("expr.out", "w", stdout);

    scanf("%s", s);
    int n = strlen(s);

    // ===== 第一步:中缀转表达式树(处理优先级与左结合)=====
    for (int i = 0; i < n; ++i) {
        char c = s[i];
        if (c == '0' || c == '1') {
            ++cnt; op[cnt] = c; val[cnt] = c - '0';
            lc[cnt] = rc[cnt] = 0;
            nodeStk[++ntop] = cnt;
        } else if (c == '(') {
            ops[++optop] = '(';
        } else if (c == ')') {
            while (optop > 0 && ops[optop] != '(') reduce();
            --optop;                       // 弹出 '('
        } else {                           // '&' 或 '|'
            while (optop > 0 && ops[optop] != '('
                   && prec(ops[optop]) >= prec(c)) reduce();
            ops[++optop] = c;
        }
    }
    while (optop > 0) reduce();
    int root = nodeStk[ntop];

    // ===== 第二步:自顶向下标记“哪些结点会被真正求值” =====
    reach[root] = true;
    for (int u = cnt; u >= 1; --u) {       // 编号从大到小 => 先父后子
        if (!reach[u]) continue;
        if (lc[u] == 0 && rc[u] == 0) continue;   // 叶子,无子可标
        reach[lc[u]] = true;                      // 左操作数总要算
        if (op[u] == '&') {
            if (val[lc[u]] == 0) { ++cntAnd; reach[rc[u]] = false; }  // a=0 → &型短路
            else                 { reach[rc[u]] = true; }
        } else {                                  // '|'
            if (val[lc[u]] == 1) { ++cntOr;  reach[rc[u]] = false; }  // a=1 → |型短路
            else                 { reach[rc[u]] = true; }
        }
    }

    printf("%d\n%lld %lld\n", val[root], cntAnd, cntOr);
    return 0;
}
2023

一元二次方程 uqe

普及+/提高 文件:uqe.in / uqe.out 数学数论gcd约分平方因子提取分类讨论
✓已掌握

【题目背景】

众所周知,对一元二次方程 ax² + bx + c = 0(a ≠ 0),可以用下述方式求实数解:计算 Δ = b² − 4ac,则:若 Δ < 0,则该一元二次方程无实数解;否则 Δ ≥ 0,此时该一元二次方程有两个实数解 x_{1,2} = (−b ± √Δ) / (2a)。其中 √Δ 表示 Δ 的算术平方根;特别的,当 Δ = 0 时,这两个实数解相等;当 Δ > 0 时,这两个实数解互异。

在题面描述中 a 和 b 的最大公因数使用 gcd(a, b) 表示。例如 12 和 18 的最大公因数是 6,即 gcd(12, 18) = 6。

【题目描述】

现在给定一个一元二次方程的系数 a, b, c,其中 a, b, c 均为整数且 a ≠ 0。你需要判断一元二次方程 ax² + bx + c = 0 是否有实数解,并按要求输出。

在本题中输出有理数 v 时须遵循以下规则:由有理数的定义,存在唯一的两个整数 p 和 q,满足 q > 0,gcd(p, q) = 1 且 v = p/q。若 q = 1,则输出 {p};否则输出 {p}/{q}。例如:当 v = −0.5 时,应输出 −1/2;当 v = 0 时,应输出 0。

对于方程的求解,分两种情况讨论:1. 若 Δ = b² − 4ac < 0,则表明方程无实数解,此时你应当输出 NO;2. 否则 Δ ≥ 0,此时方程有两解(可能相等),记其中较大者为 x,则:(1) 若 x 为有理数,则按有理数的格式输出 x。(2) 否则根据上文公式,x 可以被唯一表示为 x = q1 + q2√r 的形式,其中 q1, q2 为有理数,且 q2 > 0;r 为正整数且 r > 1,且不存在正整数 d > 1 使 d² | r(即 r 不应是 d² 的倍数)。

此时:1. 若 q1 ≠ 0,则按照有理数的格式输出 q1,再输出一个加号 +;2. 否则跳过这一步输出。随后:1. 若 q2 = 1,则输出 sqrt({r});2. 否则若 q2 为整数,则输出 {q2}*sqrt({r});3. 否则若 q3 = 1/q2 为整数,则输出 sqrt({r})/{q3};4. 否则可以证明存在唯一整数 c, d 满足 c, d > 1, gcd(c, d) = 1 且 q2 = c/d,此时输出 {c}*sqrt({r})/{d}。

上述表示中 {n} 代表整数 n 的值,详见样例。如果方程有实数解,则按要求的格式输出两个实数解中的较大者。否则输出 NO。

【输入格式】

从文件 uqe.in 中读入数据。输入的第一行包含两个正整数 T, M,分别表示方程数和系数绝对值的上界;接下来 T 行,每行包含三个整数 a, b, c。

【输出格式】

输出到文件 uqe.out 中。输出 T 行,每行包含一个字符串,表示对应询问的答案,格式如题面所述。每行输出的字符串中间不应包含任何空格。

【样例 1】

输入
9 1000
1 -1 0
-1 -1 -1
1 -2 1
1 5 4
4 4 1
1 0 -432
1 -3 1
2 -4 1
1 7 1
输出
1
NO
1
-1
-1/2
12*sqrt(3)
3/2+sqrt(5)/2
1+sqrt(2)/2
-7/2+3*sqrt(5)/2

【数据范围】

对于所有测试数据有:1 ≤ T ≤ 5000,1 ≤ M ≤ 10^3,|a|, |b|, |c| ≤ M,a ≠ 0。

测试点编号M ≤特殊性质 A特殊性质 B特殊性质 C
11是是是
220否否否
310^3是否是
410^3是否否
510^3否是是
610^3否是否
7,810^3否否是
9,1010^3否否否

特别地:特殊性质 A 保证 b = 0;特殊性质 B 保证 c = 0;特殊性质 C 保证如果方程有解,那么方程的两个解都是整数。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

数学 + 分类讨论 + 分数化简。题目本身的计算量不大,难在**把输出格式的每一条规则都不走样地实现**,是典型的“代码量不大、细节极多”的数论题。

【第一步:判断判别式与取较大根】

Δ = b² − 4ac。若 Δ < 0 输出 NO。否则两根为 x = (−b ± √Δ) / (2a)。

取较大根的关键:√Δ ≥ 0,所以 (±√Δ)/(2a) 的符号由 a 决定。因此

· 若 a > 0,较大根取 +√Δ;· 若 a < 0,较大根取 −√Δ。

统一写法:令 σ = (a > 0 ? 1 : −1),则较大根 x = (−b + σ·√Δ) / (2a)。

【第二步:Δ 是完全平方数 → 有理根】

若 Δ = m²,则 x = (−b + σ·m) / (2a) 是有理数。把分子分母约分:设分子 num = −b + σ·m,分母 den = 2a,g = gcd(|num|, |den|)(注意 gcd(0, x) = x),约分后若分母为负则分子分母同乘 −1,使分母 > 0。最后按“q == 1 输出 p,否则输出 p/q”输出。

【第三步:Δ 不是完全平方数 → 无理根,需拆出平方因子】

核心是把 √Δ 写成 s√r 的形式,其中 r 不含平方因子(r 是 Δ 的“无平方因子部分”)。做法:从 i = 2 开始枚举,只要 i² 仍整除 Δ 就令 Δ ← Δ / i²、s ← s × i;枚举到 i² > Δ 结束(此时剩下的 Δ 就是 r)。也可以通过分解质因数,把每个质因子的指数取 floor(指数/2) 累乘得到 s。

于是 x = (−b + σ·s·√r) / (2a) = q1 + q2·√r,其中 q1 = (−b) / (2a)、q2 = (σ·s) / (2a),两者都要各自约分成最简分数。

【第四步:照抄输出规则】

设约分后 q2 = c2 / d2(d2 > 0),则:

· 若 q1 ≠ 0,先输出 q1 的有理数格式,再输出一个 '+';

· 若 q2 == 1 → 输出 sqrt({r});

· 否则若 d2 == 1(q2 是整数)→ 输出 {c2}*sqrt({r});

· 否则若 c2 == 1(1/q2 是整数 d2)→ 输出 sqrt({r})/{d2};

· 否则 → 输出 {c2}*sqrt({r})/{d2}。

注意规则顺序:必须先判 q2 == 1,再判 c2 == 1,否则会把 sqrt(r) 误写成 1*sqrt(r)。

【样例演练:1 0 −432】

Δ = 0 + 1728 = 1728 = 24² × 3,所以 s = 24, r = 3;a = 1 > 0 故 σ = 1。q1 = 0/(2) = 0(跳过),q2 = 24/2 = 12(整数)→ 输出 12*sqrt(3)。

【复杂度分析】

每次询问 O(√M · α) 级别(提取平方因子),M ≤ 10^3 时最多几百次操作;T ≤ 5000,整体轻松通过。

【易错点提示】

1. 一定先判 Δ 是否完全平方数;完全平方时是“有理根”,不能走无理格式;

2. 拆平方因子要拆干净:1728 = 64×27 还不够,27 里还能拆出 9,最终 s = 24, r = 3;

3. 分母必须化为正数,q2 的取值范围题目要求 q2 > 0(我们取的是较大根,天然满足);

4. 输出中不能有任何空格。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll a, b, c, sigma;   // sigma = 较大根对应的符号

ll gcdll(ll x, ll y) { return y == 0 ? x : gcdll(y, x % y); }

// 输出有理数 num/den(已保证 den > 0),要求约分
string frac2str(ll num, ll den) {
    if (num == 0) return "0";
    ll g = gcdll(abs(num), abs(den));
    num /= g; den /= g;
    if (den < 0) { num = -num; den = -den; }
    if (den == 1) return to_string(num);
    return to_string(num) + "/" + to_string(den);
}

// 判断完全平方数并返回其平方根
bool perfectSqrt(ll x, ll &root) {
    if (x < 0) return false;
    ll s = (ll)sqrtl((long double)x);
    while (s > 0 && s * s > x) --s;
    while ((s + 1) * (s + 1) <= x) ++s;
    if (s * s == x) { root = s; return true; }
    return false;
}

int main() {
    freopen("uqe.in", "r", stdin);
    freopen("uqe.out", "w", stdout);

    int T; ll M;
    scanf("%d%lld", &T, &M);

    while (T--) {
        scanf("%lld%lld%lld", &a, &b, &c);
        sigma = (a > 0 ? 1 : -1);

        ll delta = b * b - 4 * a * c;
        if (delta < 0) { printf("NO\n"); continue; }

        ll root;
        if (perfectSqrt(delta, root)) {
            // ===== 有理根 =====
            ll num = -b + sigma * root;
            ll den = 2 * a;
            if (num == 0) { printf("0\n"); continue; }
            ll g = gcdll(abs(num), abs(den));
            num /= g; den /= g;
            if (den < 0) { num = -num; den = -den; }
            if (den == 1) printf("%lld\n", num);
            else          printf("%lld/%lld\n", num, den);
        } else {
            // ===== 无理根:拆分 Δ = s^2 * r =====
            ll rr = delta, s = 1;
            for (ll i = 2; i * i <= rr; ++i) {
                while (rr % (i * i) == 0) { rr /= i * i; s *= i; }
            }
            ll r = rr;                    // 无平方因子部分,r > 1

            // q1 = -b / (2a),q2 = sigma * s / (2a)
            ll q1n = -b,        q1d = 2 * a;
            ll q2n = sigma * s, q2d = 2 * a;

            string out = "";
            if (q1n != 0) out += frac2str(q1n, q1d) + "+";

            // 约分 q2
            if (q2n != 0) {
                ll g = gcdll(abs(q2n), abs(q2d));
                q2n /= g; q2d /= g;
            }
            if (q2d < 0) { q2n = -q2n; q2d = -q2d; }

            if (q2n == 1 && q2d == 1) {
                out += "sqrt(" + to_string(r) + ")";
            } else if (q2d == 1) {
                out += to_string(q2n) + "*sqrt(" + to_string(r) + ")";
            } else if (q2n == 1) {
                out += "sqrt(" + to_string(r) + ")/" + to_string(q2d);
            } else {
                out += to_string(q2n) + "*sqrt(" + to_string(r) + ")/" + to_string(q2d);
            }
            printf("%s\n", out.c_str());
        }
    }
    return 0;
}
2024

小木棍 sticks

普及+/提高 文件:sticks.in / sticks.out 贪心构造数学数论
✓已掌握

【题目描述】

小 S 喜欢收集小木棍。在收集了 n 根长度相等的小木棍之后,他闲来无事,便用它们拼起了数字。用小木棍拼每种数字的方法如下图所示。

图 2:每种数字的小木棍拼法
图 2:每种数字的小木棍拼法

现在小 S 希望拼出一个正整数,满足如下条件:拼出这个数恰好使用 n 根小木棍;拼出的数没有前导 0;在满足以上两个条件的前提下,这个数尽可能小。

小 S 想知道这个数是多少,可 n 很大,把木棍整理清楚就把小 S 折腾坏了,所以你需要帮他解决这个问题。如果不存在正整数满足以上条件,你需要输出 -1 进行报告。

【输入格式】

从文件 sticks.in 中读入数据。本题有多组测试数据。输入的第一行包含一个正整数 T,表示数据组数。接下来包含 T 组数据,每组数据一行包含一个整数 n,表示木棍数。

【输出格式】

输出到文件 sticks.out 中。对于每组数据:输出一行,如果存在满足题意的正整数,输出这个数;否则输出 -1。

【样例 1】

输入
5
1
2
3
6
18
输出
-1
1
7
6
208

样例解释:1 根小木棍摆不出任何数字,输出 -1;6 根小木棍可以摆出 6、9、41、111 等,但 6 最小;18 根小木棍的最小方案是 208(5+6+7 = 18),注意 006 虽然有 18 根但有前导零,不合法。

【数据范围】

对于所有测试数据,保证:1 ≤ T ≤ 50,1 ≤ n ≤ 10^5。

测试点编号n ≤特殊性质
120无
250无
310^3A
4, 510^5A
610^3B
7, 810^5B
910^3无
1010^5无

特殊性质 A:保证 n 是 7 的倍数且 n ≥ 100。特殊性质 B:保证存在整数 k 使得 n = 7k + 1,且 n ≥ 100。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

贪心 + 构造。考点有两个:一是把“恰好用 n 根木棍”转化为“数字位数与各位木棍数的约束”;二是在满足约束的前提下构造出数值最小的数。

【每种数字的耗棍数】

数字0123456789
木棍数6255456376

关键观察:除 0 以外,数字 1 最省(2 根),数字 8 最费(7 根)。

【第一步:确定位数】

设答案是 d 位数。因为首位不能是 0,而其余位可以取到任意的耗棍数 2~7(1→2、7→3、4→4、2/3/5→5、0/6/9→6、8→7),所以 d 位数的耗棍总数可以取遍区间 [2d, 7d] 中的**每一个整数**(相邻差值 1 都能凑出来)。

为了让数尽可能小,位数越少越好——因为任何 d 位数都小于任何 (d+1) 位数。于是取最小的 d 满足 2d ≤ n ≤ 7d,即 d = ⌈n / 7⌉(只需检查 2d ≤ n 是否也成立)。

当 n = 1 时,2 × 1 = 2 > 1,不存在这样的 d,输出 -1。n ≥ 2 时 d = ⌈n/7⌉ 恒满足 2d ≤ n。

【第二步:逐位贪心构造最小值】

从最高位开始,每一位都尝试填入尽可能小的数字(首位从 1 开始试,其余位从 0 开始试):设已放置 i 位、还剩 n 根木棍、后面还有 rest 位,选择数字 c(其耗棍数为 w),要求剩余的木棍数 n − w 能被后面的 rest 位消化,即 2·rest ≤ n − w ≤ 7·rest。选择满足条件的最小 c,填进去,更新 n ← n − w,进入下一位。

由于区间可达性成立,这种贪心一定能走到最后。得到的即为字典序最小(等价于数值最小,因为位数固定)的答案。

【样例验证:n = 18】

d = ⌈18/7⌉ = 3。首位试 1(2 根):剩 16 根、还要放 2 位,需要 4 ≤ 16 ≤ 14,不成立;试 2(5 根):剩 13,需 4 ≤ 13 ≤ 14 ✓,填 2。第二位试 0(6 根):剩 7,需 2 ≤ 7 ≤ 7 ✓,填 0。第三位需恰好 7 根 → 8。答案 208 ✓。

【复杂度分析】

位数 d ≤ n/2,每位最多试 10 个数字,单组 O(d × 10) ≤ O(n),T ≤ 50 时总量可控,可通过。

【易错点提示】

1. 判定区间是 [2·rest, 7·rest],两端都要判,只判上界会构造出长度不够的数;

2. 首位不能是 0,否则会出现前导零(如 n = 6 时必须输出 6 而不是 0);

3. n = 1 是唯一输出 -1 的情况,其余 n ≥ 2 都有解。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

// 每个数字需要的小木棍数
const int cost[10] = {6, 2, 5, 5, 4, 5, 6, 3, 7, 6};

int main() {
    freopen("sticks.in", "r", stdin);
    freopen("sticks.out", "w", stdout);

    int T;
    scanf("%d", &T);
    while (T--) {
        int n;
        scanf("%d", &n);

        if (n == 1) { printf("-1\n"); continue; }

        int d = (n + 6) / 7;              // 最少位数 = ceil(n / 7)
        string ans;
        int left = n;
        for (int i = 0; i < d; ++i) {
            int rest = d - i - 1;          // 后面还剩多少位
            for (int c = (i == 0 ? 1 : 0); c <= 9; ++c) {   // 首位从 1 开始
                int nl = left - cost[c];
                if (nl >= 2 * rest && nl <= 7 * rest) {     // 剩余木棍可被消化
                    ans += char('0' + c);
                    left = nl;
                    break;
                }
            }
        }
        printf("%s\n", ans.c_str());
    }
    return 0;
}
2025

异或和 xor

普及+/提高 文件:xor.in / xor.out 贪心前缀和哈希表位运算
✓已掌握

【题目描述】

小 R 有一个长度为 n 的非负整数序列 a₁, a₂, …, a_n。定义一个区间 [l, r](1 ≤ l ≤ r ≤ n)的权值为 a_l ⊕ a_{l+1} ⊕ … ⊕ a_r 的二进制按位异或和,其中 ⊕ 表示按位异或。

小 X 给了小 R 一个非负整数 k。小 X 希望小 R 选择序列中尽可能多的不相交的区间,使得每个区间的权值均为 k。两个区间相交当且仅当同时包含至少一个相同的下标。

你需要帮助小 R 求出他能选出的区间数量的最大值。

【输入格式】

输入文件名为 xor.in。第一行两个非负整数 n, k,分别表示序列长度和目标整数。第二行 n 个非负整数 a₁, a₂, …, a_n。

【输出格式】

输出文件名为 xor.out。输出一行一个非负整数,表示能选出的区间数量的最大值。

【样例 1】

输入
4 2
2 1 0 3
输出
2

可以选择区间 [1,1](权值 2)和 [2,4](权值 1⊕0⊕3 = 2),共 2 个区间。

【样例 2】

输入
4 3
2 1 0 3
输出
2

可以选择 [1,2](1⊕2=3)和 [4,4](3),共 2 个。

【样例 3】

输入
4 0
2 1 0 3
输出
1

只能选 [3,3](异或和 0),不能同时选 [3,3] 和 [1,4]。

【数据规模与约定】

对于所有测试数据,保证 1 ≤ n ≤ 5×10⁵,0 ≤ k < 2²⁰;对于所有 1 ≤ i ≤ n,均有 0 ≤ aᵢ < 2²⁰。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

贪心 + 前缀异或 + 哈希表。区间异或和可用前缀异或表示:令 pre[i] = a₁⊕…⊕aᵢ,则区间 [l,r] 的异或和为 pre[r] ⊕ pre[l-1]。要它等于 k,等价于 pre[r] = pre[l-1] ⊕ k。

【关键观察(贪心正确性)】

我们希望选出尽可能多「不相交」的区间。直觉上,「越早结束的合法区间越好」——因为越早结束,留给后面的空间越大。可以用经典的「区间调度」贪心:从左到右扫描,一旦当前位置能凑出一个异或和为 k 的区间,就立刻截断并计数,然后清空状态从下一个位置重新开始。

具体地:维护当前前缀异或 cur 和一个「已经出现过的前缀值」集合 S(初始只含 0,代表空前缀)。从左到右遍历每个 aᵢ:cur ^= aᵢ。若 cur ⊕ k 已经出现在 S 中,说明存在一个之前的位置 p,使得 (pre[p] 到 pre[i]) 这段异或和 = k,于是我们得到一个合法区间,ans++,然后「重置」:清空 S = {0},cur = 0(相当于把这个区间的右端当作新的起点,之后只能选它右边的区间)。否则把 cur 加入 S。

【为什么贪心是对的】

交换论证:假设最优解中第一个区间的右端点为 r*,我们的贪心找到的第一个区间右端点为 r ≤ r*。把最优解的第一个区间换成贪心的 [l, r],剩余部分(r+1 之后)不受影响且依然能放下原来 r* 之后的所有区间,因此贪心不会让答案变差。于是「每次选最早结束」的策略能取得最大值。

【复杂度分析】

O(n),用哈希表(unordered_set)存前缀值,每个元素 O(1) 查询/插入,总 O(n)。

【易错点提示】

1. 前缀异或的初值 0 要先放进集合(代表空前缀,即从位置 1 开始的区间);

2. 找到一个合法区间后要「重置」集合与 cur,因为区间必须不相交;

3. 判断条件是 cur ⊕ k 是否出现过(不是 cur 本身)。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

int main() {
    freopen("xor.in", "r", stdin);
    freopen("xor.out", "w", stdout);

    int n, k;
    cin >> n >> k;
    unordered_set<int> S;
    S.insert(0);          // 空前缀异或为 0

    int cur = 0, ans = 0;
    for (int i = 1; i <= n; ++i) {
        int x; cin >> x;
        cur ^= x;
        if (S.count(cur ^ k)) {   // 存在 p 使 pre[p] = cur ^ k
            ans++;                // 得到一段异或和为 k 的区间
            S.clear();            // 重置:区间不能相交
            S.insert(0);
            cur = 0;
        } else {
            S.insert(cur);
        }
    }

    cout << ans << endl;
    return 0;
}
📘
T4

T4 · 第四题 专项

本组共 7 题,来自 2019–2025 年 CSP-J 第二轮认证。

2019

加工零件 work

提高+/省选- 文件:work.in / work.out 图论广度优先搜索 BFS奇偶分层最短路
✓已掌握

【题目描述】

凯凯的工厂正在有条不紊地生产一种神奇的零件。工厂里有 n 位工人,工人们从 1 ~ n 编号。某些工人之间存在双向的零件传送带。保证每两名工人之间最多只存在一条传送带。

如果 x 号工人想生产一个被加工到第 L (L > 1) 阶段的零件,则所有与 x 号工人有传送带直接相连的工人,都需要生产一个被加工到第 L−1 阶段的零件(但 x 号工人自己无需生产第 L−1 阶段的零件)。

如果 x 号工人想生产一个被加工到第 1 阶段的零件,则所有与 x 号工人有传送带直接相连的工人,都需要为 x 号工人提供一个原材料。

轩轩是 1 号工人。现在给出 q 张工单,第 i 张工单表示编号为 a_i 的工人想生产一个第 L_i 阶段的零件。轩轩想知道对于每张工单,他是否需要给别人提供原材料。

【输入格式】

输入文件名为 work.in。第一行两个正整数 n, m 和 q,分别表示工人的数目、传送带的数目和工单的数目。

接下来 m 行,每行两个正整数 u 和 v,表示编号为 u 和 v 的工人之间存在一条零件传输带。保证 u ≠ v。

接下来 q 行,每行两个正整数 a 和 L,表示编号为 a 的工人想生产一个第 L 阶段的零件。

【输出格式】

输出文件名为 work.out。共 q 行,每行一个字符串 “Yes” 或者 “No”。如果需要编号为 1 的轩轩提供原材料,则输出 “Yes”,否则输出 “No”。注意输出不含引号。

【输入输出样例 1】

输入
3 2 6
1 2
2 3
1 1
2 1
3 1
1 2
2 2
3 2
输出
No
Yes
No
Yes
No
Yes
图 1:样例 1 的传送带结构(1−2−3 链)
图 1:样例 1 的传送带结构(1−2−3 链)
图 2:样例 1 的另一种图示
图 2:样例 1 的另一种图示

【输入输出样例 2】

输入
5 5 5
1 2
2 3
3 4
4 5
1 5
1 1
1 2
1 3
1 4
1 5
输出
No
Yes
No
Yes
Yes

【数据规模与约定】

共 20 个测试点,1 ≤ u, v, a ≤ n。

测试点 1~4:1 ≤ n, m ≤ 1000,q = 3,L = 1。

测试点 5~8:1 ≤ n, m ≤ 1000,q = 3,1 ≤ L ≤ 10。

测试点 9~12:1 ≤ n, m, L ≤ 1000,1 ≤ q ≤ 100。

测试点 13~16:1 ≤ n, m, L ≤ 1000,1 ≤ q ≤ 10^5。

测试点 17~20:1 ≤ n, m, q ≤ 10^5,1 ≤ L ≤ 10^9。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

图论 + BFS 求奇偶最短路。题目看似是“递归展开”,本质问的是:从 1 号点出发,是否存在一条长度为 L 的路径(可以重复走点、走边)到达 a 号点。

【模型转化】

把一个“第 L 阶段的零件需求”想象成从 a 出发向外传播的需求波:第 L 阶段传给邻居 → 邻居生产 L−1 阶段 → 再传给它们的邻居……直到 L = 1 时,需要 a 的邻居们提供原材料。

反过来看“1 号工人要不要提供原材料”:沿着需求传播的路径,从 a 走到 1,路径长度恰好是 L。也就是说:轩轩(1 号)需要提供原材料 ⟺ 存在一条从 a 出发、长度为 L 的路径(允许重复经过点和边)到达 1 号点。

因为每条传送带是双向的、走一次耗时 1 单位,并且可以来回走(在某条边上反复横跳会让路径长度 +2),所以答案只与“从 a 到 1 的最短路长度的奇偶性”有关。

【奇偶分层 BFS】

定义 dist[v][0] 表示从 1 号点出发到 v 的偶数长度最短路,dist[v][1] 表示奇数长度最短路。在图上做 BFS,状态是 (点 v, 奇偶性 p),转移时 p → 1−p。可以用 0-1 权 BFS / 普通 BFS 分层即可(每条边权重为 1)。

处理询问 (a, L):若 L 是偶数且 L ≥ dist[a][0],或 L 是奇数且 L ≥ dist[a][1],则输出 Yes;否则 No。

之所以只要“≥”就够,是因为可以在这条路径上任意来回横跳来把长度补成任意更大的同奇偶值(每次 +2)。

【为什么可以来回横跳】

路径上任意一条边 u−v,走 u→v→u 就多走了 2 步,且回到原点。所以只要存在长度 d 的路径,就存在任意长度 d + 2k 的同奇偶路径。

【复杂度分析】

BFS 状态数 2n,每个状态只入队一次,时间 O(n + m);每次询问 O(1),总复杂度 O(n + m + q),可通过 L ≤ 10^9 的满分数据。

【易错点提示】

1. 必须分奇偶两个维度存距离,只求一个最短路会错;

2. 当 a = 1 时,需要长度为 L 的路径从 1 回到 1,且路径必须“走一步再回来”,所以 L 至少为 2(L=1 时 No)。用 L ≥ dist 判据自然可以处理;

3. 图可能不连通,不可达时 dist 为 INF,判据自然输出 No。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
const int INF  = 0x3f3f3f3f;

vector<int> g[MAXN];
int dist[MAXN][2];      // dist[v][0]: 从 1 到 v 的偶最短路; [1]: 奇最短路

int main() {
    freopen("work.in", "r", stdin);
    freopen("work.out", "w", stdout);

    int n, m, q;
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= m; ++i) {
        int u, v; scanf("%d%d", &u, &v);
        g[u].push_back(v);
        g[v].push_back(u);
    }

    memset(dist, 0x3f, sizeof(dist));
    queue<pair<int,int>> Q;          // (点, 奇偶)
    dist[1][0] = 0;
    Q.push({1, 0});
    while (!Q.empty()) {
        int u = Q.front().first, p = Q.front().second; Q.pop();
        for (int v : g[u]) {
            if (dist[v][p ^ 1] > dist[u][p] + 1) {
                dist[v][p ^ 1] = dist[u][p] + 1;
                Q.push({v, p ^ 1});
            }
        }
    }

    while (q--) {
        int a, L; scanf("%d%d", &a, &L);
        int p = L & 1;                        // 需要的奇偶性
        if (L >= dist[a][p]) printf("Yes\n");
        else                 printf("No\n");
    }
    return 0;
}
2020

方格取数 number

提高+/省选- 文件:number.in / number.out 动态规划网格双向转移记忆化搜索
✓已掌握

【题目描述】

设有 n × m 的方格图,每个方格中都有一个整数。现有一只小熊,想从图的左上角走到右下角,每一步只能向上、向下或向右走一格,并且不能重复经过已经走过的方格,也不能走出边界。小熊会取走所有经过的方格中的整数,求它能取到的整数之和的最大值。

【输入格式】

输入文件名为 number.in。第 1 行两个正整数 n, m。接下来 n 行每行 m 个整数,依次代表每个方格中的整数。

【输出格式】

输入文件名为 number.out。一个整数,表示小熊能取到的整数之和的最大值。

【输入输出样例 1】

输入
3 4
1 -1 3 2
2 -1 4 -1
-2 2 -3 -1
输出
9
1-132
2-14-1
-22-3-1

样例说明:取到的数之和为 1 + 2 + (−1) + 4 + 3 + 2 + (−1) + (−1) = 9。

【输入输出样例 2】

输入
2 5
-1 -1 -3 -2 -7
-2 -1 -4 -1 -2
输出
-10

样例说明:取到的数之和为 (−1)+(−1)+(−3)+(−2)+(−1)+(−2) = −10。因此请注意,取到的数之和的最大值也可能是负数。

【数据范围与提示】

对于 20% 的数据,n, m ≤ 5。

对于 40% 的数据,n, m ≤ 50。

对于 70% 的数据,n, m ≤ 300。

对于 100% 的数据,1 ≤ n, m ≤ 1000。方格中整数的绝对值不超过 10^4。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

网格上的动态规划。移动方式是“上、下、右”,不能走回头路、不能出界。经典的错误做法是定义 f[i][j] = 到 (i,j) 的最大值——因为同列内可以上下移动,单纯两个维度会有后效性(同列上下互相依赖)。

【正确的阶段划分】

以“列”为阶段,从左到右推。因为在同一列内只可能竖直移动,且一旦从某列走到右列就再也不会回来(不能向左走),所以列与列之间是天然有序的。每次处理一列时,列内的行走路径只有两种情况:

· 进入这一列后“从上往下”走(记为状态 0):从上一列到达第 j 列第 i 行,可能是从上一行的“从上往下”状态继续下来,也可能是在第 j 列第 i 行处由上一列“横跨”进入。

· 进入这一列后“从下往上”走(记为状态 1):同理,从下一行往上推。

【状态设计与转移】

设 f[i][j][0] 表示:以第 j 列第 i 行的格子为“终点”,且在第 j 列内是自上而下走到 (i,j) 时,能取到的最大值;f[i][j][1] 表示第 j 列内自下而上走到 (i,j) 的最大值;f[i][j][2] = max(f[i][j][0], f[i][j][1])。

边界(第 1 列,只能从上往下走):f[1][1][0] = f[1][1][1] = f[1][1][2] = a[1][1],f[i][1][0] = f[i][1][1] = f[i][1][2] = f[i−1][1][0] + a[i][1]。

转移(j ≥ 2):

f[1][j][0] = f[1][j−1][2] + a[1][j];

f[i][j][0] = max(f[i][j−1][2], f[i−1][j][0]) + a[i][j](i ≥ 2,从上边下来 或 从左边横跨进来);

f[n][j][1] = f[n][j−1][2] + a[n][j];

f[i][j][1] = max(f[i][j−1][2], f[i+1][j][1]) + a[i][j](i ≤ n−1,从下边上来 或 从左边横跨进来);

f[i][j][2] = max(f[i][j][0], f[i][j][1])。

答案:f[n][m][2]。

【为什么这样就不会有后效性】

把“列内竖直移动”拆成两种单调方向后,同一列内的依赖关系变成了单向的(自上而下只依赖上一行,自下而上只依赖下一行),于是可以在一列内先算状态 0(从上到下扫一遍)、再算状态 1(从下到上扫一遍),两者取 max 得到状态 2,完全消除了环。

【另一种写法:记忆化搜索】

定义 dfs(x, y, t):t=0 表示来到 (x,y) 时是“从上往下”方向,t=1 表示“从下往上”方向。转移为 dfs(x,y,0) = max(dfs(x−1,y,0), dfs(x,y−1,0), dfs(x,y−1,1)) + a[x][y];dfs(x,y,1) = max(dfs(x+1,y,1), dfs(x,y−1,0), dfs(x,y−1,1)) + a[x][y]。用记忆化数组避免重复计算。记忆化搜索本质就是 DP 的另一种组织形式。

【复杂度分析】

状态数 O(nm),转移 O(1),时间 O(nm);空间 O(nm)。n, m ≤ 1000 时可过。

【易错点提示】

1. 答案可能是负数(全负矩阵),dp 初值不要设成 0,要用 −∞(如 0x3f3f3f3f 取负);

2. 数据范围 1000×1000 且数值 10^4,总和最大约 10^10,必须用 long long;

3. 不要写成 f[i][j] 二维朴素 DP,那样在第 1 列之外的列内上下移动会互相依赖,是错的。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 1005;
const ll NEG = -(1LL << 60);

int n, m;
int a[MAXN][MAXN];
ll f[MAXN][MAXN][3];

int main() {
    freopen("number.in", "r", stdin);
    freopen("number.out", "w", stdout);

    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= m; ++j)
            scanf("%d", &a[i][j]);

    // 第 1 列:只能自上而下
    f[1][1][0] = f[1][1][1] = f[1][1][2] = a[1][1];
    for (int i = 2; i <= n; ++i) {
        f[i][1][0] = f[i][1][1] = f[i][1][2] = f[i - 1][1][0] + a[i][1];
    }

    for (int j = 2; j <= m; ++j) {
        // 状态 0:自上而下到 (i,j)
        f[1][j][0] = f[1][j - 1][2] + a[1][j];
        for (int i = 2; i <= n; ++i)
            f[i][j][0] = max(f[i][j - 1][2], f[i - 1][j][0]) + a[i][j];

        // 状态 1:自下而上到 (i,j)
        f[n][j][1] = f[n][j - 1][2] + a[n][j];
        for (int i = n - 1; i >= 1; --i)
            f[i][j][1] = max(f[i][j - 1][2], f[i + 1][j][1]) + a[i][j];

        // 状态 2:两者取 max
        for (int i = 1; i <= n; ++i)
            f[i][j][2] = max(f[i][j][0], f[i][j][1]);
    }

    printf("%lld\n", f[n][m][2]);
    return 0;
}
2021

小熊的果篮 fruit

提高+/省选- 文件:fruit.in / fruit.out 模拟双向链表队列块合并
✓已掌握

【题目描述】

小熊的水果店里摆放着一排 n 个水果。每个水果只可能是苹果或桔子,从左到右依次用正整数 1、2、3、……、n 编号。连续排在一起的同一种水果称为一个“块”。小熊要把这一排水果挑到若干个果篮里,具体方法是:每次都把每一个“块”中最左边的水果同时挑出,组成一个果篮。重复这一操作,直至水果用完。注意,每次挑完一个果篮后,“块”可能会发生变化。比如两个苹果“块”之间的唯一桔子被挑走后,两个苹果“块”就变成了一个“块”。请帮小熊计算每个果篮里包含的水果。

【输入格式】

从文件 fruit.in 中读入数据。输入的第一行包含一个正整数 n,表示水果的数量。输入的第二行包含 n 个空格分隔的整数,其中第 i 个数表示编号为 i 的水果的种类,1 代表苹果,0 代表桔子。

【输出格式】

输出到文件 fruit.out 中。输出若干行。第 i 行表示第 i 次挑出的水果组成的果篮。从小到大排序输出该果篮中所有水果的编号,每两个编号之间用一个空格分隔。

【样例 1】

输入
12
1 1 0 0 1 1 1 0 1 1 0 0
输出
1 3 5 8 9 11
2 4 6 12
7
10

样例解释:初始有 6 个块。第 1 次挑出 1,3,5,8,9,11;剩下 1 0 1 1 1 0(4 个块);第 2 次挑出 2,4,6,12;剩下 1 1(1 个块);第 3 次挑出 7;剩下 1;第 4 次挑出 10。

【样例 2】

输入
20
1 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 0 0 0 0
输出
1 5 8 11 13 14 15 17
2 6 9 12 16 18
3 7 10 19
4 20

【数据范围】

对于 10% 的数据,n ≤ 5;对于 30% 的数据,n ≤ 1000;对于 70% 的数据,n ≤ 50000;对于 100% 的数据,n ≤ 2 × 10^5。提示:由于数据规模较大,建议 C/C++ 选手使用 scanf 和 printf 语句输入、输出。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

模拟 + 双向链表 + 块合并。直接按定义“每轮重新扫描求所有块”的复杂度是 O(n²),n = 2×10^5 会超时(最坏 4×10^10)。需要维护“块”的动态变化。

【算法思路:双向链表维护块 + 队列记录块头】

核心观察:每一轮挑走的都是“当前每个块的最左边那个水果”。挑走之后,只可能发生一种结构变化——某个块被削掉头部后,如果它和它的后继块颜色相同,就合并成一个块。

具体做法:

1. 用双向链表 nxt[i] / pre[i] 维护相邻水果的关系(初始 nxt[i] = i+1)。

2. 找出初始所有“块头”(即 i = 1 或 a[i] ≠ a[i−1] 的位置),放入一个数组/队列。

3. 每一轮:取出当前所有块头,输出它们(块头编号天然从小到大,正好满足输出要求),然后从链表中删除这些块头。

4. 删除后,检查每个被删块头原来所在块的下一个元素(即 nxt 指向的元素)是否成为了新的“块头”:若它的前驱被删除,且它与新的前驱颜色不同(或没有前驱),它就成为块头;若它与新的前驱颜色相同,则它们属于同一块,不产生新块头。

5. 把本轮新产生的块头收集起来,作为下一轮的输入。若为空则结束。

【为什么块头编号天然有序】

因为链表保持了原始编号顺序,且块头是按位置从左到右排列的,所以每轮输出的块头编号本身就是递增的,不需要额外排序。

【另一种视角】

也可以把“每个位置被挑走的轮次” dp[i] 求出来:块头在第一轮被挑走;某元素被挑走的轮次等于“它左侧最近的不同色元素被挑走的轮次 + 1”。有了轮次后,按轮次分组输出即可。这个视角更容易理解,但链表的实现更直接。

【复杂度分析】

每个元素最多被删除一次、被访问常数次,总复杂度 O(n),空间 O(n)。

【易错点提示】

1. 删除时要同时更新 nxt 和 pre 指针,注意边界(前驱不存在时用 0 号哨兵);

2. 判断“是否成为新块头”要看删除后的新前驱颜色,而不是删除前的;

3. 尽量用 scanf/printf,输出量大(最坏 n 个数),用 cin/cout 可能超时。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;
int a[MAXN];
int nxt[MAXN], pre[MAXN];   // 双向链表,0 表示空
bool alive[MAXN];

int main() {
    freopen("fruit.in", "r", stdin);
    freopen("fruit.out", "w", stdout);

    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);

    for (int i = 1; i <= n; ++i) {
        nxt[i] = (i == n ? 0 : i + 1);
        pre[i] = i - 1;
        alive[i] = true;
    }
    a[0] = -1;                 // 哨兵颜色,保证 1 号一定是块头

    // 初始所有块头
    vector<int> cur, nxtRound;
    for (int i = 1; i <= n; ++i)
        if (a[i] != a[pre[i]]) cur.push_back(i);

    while (!cur.empty()) {
        // 本轮块头即答案(编号天然递增)
        for (size_t k = 0; k < cur.size(); ++k)
            printf("%d%c", cur[k], k + 1 == cur.size() ? '\n' : ' ');

        // 按编号升序删除本轮块头;此时 pre[x] 一定指向“仍然存活”的前驱
        for (size_t k = 0; k < cur.size(); ++k) {
            int x = cur[k];
            int nx = nxt[x], pr = pre[x];
            if (pr) nxt[pr] = nx;      // pr 存活,安全
            else    nxt[0]  = nx;
            if (nx) pre[nx] = pr;
            alive[x] = false;          // 删除 x(x 的 nxt 保持不变,供回溯用)
        }

        // 为每个被删块头找“右边第一个存活元素”,它可能是下一轮的块头
        nxtRound.clear();
        for (size_t k = 0; k < cur.size(); ++k) {
            int y = nxt[cur[k]];
            while (y && !alive[y]) y = nxt[y];     // 跳过本轮被删的块头
            if (y == 0) continue;
            if (pre[y] == 0 || a[pre[y]] != a[y])  // 与新的前驱颜色不同 → 新块头
                nxtRound.push_back(y);
        }
        sort(nxtRound.begin(), nxtRound.end());
        nxtRound.erase(unique(nxtRound.begin(), nxtRound.end()), nxtRound.end());
        cur.swap(nxtRound);
    }
    return 0;
}
2022

上升点列 point

提高+/省选- 文件:point.in / point.out 动态规划排序坐标变换最长上升子序列
✓已掌握

【题目描述】

在一个二维平面内,给定 n 个整数点 (x_i, y_i),此外你还可以自由添加 k 个整数点。你在自由添加 k 个点后,还需要从 n + k 个点中选出若干个整数点并组成一个序列,使得序列中任意相邻两点间的欧几里得距离恰好为 1 而且横坐标、纵坐标值均单调不减,即 x_{i+1} − x_i = 1, y_{i+1} = y_i 或 y_{i+1} − y_i = 1, x_{i+1} = x_i。请给出满足条件的序列的最大长度。

【输入格式】

从文件 point.in 中读入数据。第一行两个正整数 n, k 分别表示给定的整点个数、可自由添加的整点个数。接下来 n 行,第 i 行两个正整数 x_i, y_i 表示给定的第 i 个点的横纵坐标。

【输出格式】

输出到文件 point.out 中。输出一个整数表示满足要求的序列的最大长度。

【样例 1】

输入
8 2
3 1
3 2
3 3
3 6
1 2
2 2
5 5
5 3
输出
8

【样例 2】

输入
4 100
10 10
15 25
20 20
30 30
输出
103

【数据范围】

保证对于所有数据满足:1 ≤ n ≤ 500,0 ≤ k ≤ 100。对于所有给定的整点,其横纵坐标 1 ≤ x_i, y_i ≤ 10^9,且保证所有给定的点互不重合。对于自由添加的整点,其横纵坐标不受限制。

测试点编号n ≤k ≤x_i, y_i ≤
1~210010
3~410100100
5~71010010^9
8~10500010^9
11~15500100100
15~2050010010^9
▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

动态规划 + 排序。把“可以自由添加点”理解为“可以用新点把两个原点之间的曼哈顿路径补满”,于是相邻两个原点之间的“代价”就是它们的曼哈顿距离减 1。

【关键观察】

序列要求每一步只走 1 个单位(横向或纵向),且坐标单调不减。因此从点 A 走到点 B 所经过的点数恰好是曼哈顿距离 d = |x_A − x_B| + |y_A − y_B| 加 1,其中 A、B 之间的 d − 1 个点都需要是“自由添加”的点(若 d = 1 则相邻,不需要加点)。

注意:只有当 x_A ≤ x_B 且 y_A ≤ y_B 时 A 才能走到 B;否则永远不行。

【状态设计】

先把所有给定点按 (x, y) 排序(这样后续点必然在排序前点的右上方或右下方)。定义 f[i][j] 表示:以第 i 个点作为链的“最后一个给定点”、且整条链一共使用了 j 个自由添加点时,链的最大长度(包含链上所有点:给定点 + 添加点)。

【转移】

f[i][j] 的初值:f[i][j] = j + 1。含义是:这条链从某个自由点出发,用 j 个添加点一路走到点 i,长度为 j + 1(因为自由点坐标不受限制,总能构造出来)。

转移:枚举前驱给定点 p(排序在 i 之前,且 x_p ≤ x_i、y_p ≤ y_i),令 d = (x_i − x_p) + (y_i − y_p)。若 d − 1 ≤ j,则

f[i][j] = max(f[i][j], f[p][j − (d − 1)] + d)。

含义:从 p 走到 i 需要新增 d − 1 个点,链长度增加 d(这 d 个位置里包含终点 i)。

【答案】

遍历所有 (i, j),答案 = max( f[i][j] + (k − j) )。多出来的 (k − j) 个自由点可以全部接在链的末尾(继续向右或向上延伸),每加一个点链长 +1,所以直接加上即可。

【复杂度分析】

状态数 O(nk),转移枚举前驱 O(n),总复杂度 O(n²k) = 500 × 500 × 100 = 2.5×10^7,可通过。

【易错点提示】

1. 必须先排序再转移,否则会漏解;排序后仍要显式判断 x_p ≤ x_i 且 y_p ≤ y_i;

2. f 的初值要给到 j + 1,不能只给 f[i][0] = 1;

3. 最后要加上剩余的自由点数 (k − j),这一步很容易漏;

4. 坐标达 10^9,但只用于算差值,用 long long 或 int 均可(差不超过 2×10^9,建议 long long)。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 505, MAXK = 105;
struct Pt { ll x, y; } pt[MAXN];
int f[MAXN][MAXK];    // f[i][j]: 以 i 结尾、共使用 j 个自由点的最长链长

int main() {
    freopen("point.in", "r", stdin);
    freopen("point.out", "w", stdout);

    int n, k;
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i) scanf("%lld%lld", &pt[i].x, &pt[i].y);

    sort(pt + 1, pt + n + 1, [](const Pt &A, const Pt &B) {
        if (A.x != B.x) return A.x < B.x;
        return A.y < B.y;
    });

    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= k; ++j) {
            f[i][j] = j + 1;               // 初值:从自由点一路走到 i
            for (int p = 1; p < i; ++p) {
                if (pt[p].x > pt[i].x || pt[p].y > pt[i].y) continue;   // 不可达
                ll d = (pt[i].x - pt[p].x) + (pt[i].y - pt[p].y);       // 曼哈顿距离
                int need = (int)d - 1;                                  // 需要补的点数
                if (need < 0) need = 0;
                if (need <= j)
                    f[i][j] = max(f[i][j], f[p][j - need] + (int)d);
            }
            ans = max(ans, f[i][j] + (k - j));   // 剩余自由点全部接在末尾
        }
    }
    printf("%d\n", ans);
    return 0;
}
2023

旅游巴士 bus

提高+/省选- 文件:bus.in / bus.out 图论最短路分层图Dijkstra同余最短路
✓已掌握

【题目描述】

小 Z 打算在国庆假期期间搭乘旅游巴士去一处他向往已久的景点旅游。旅游景点的地图共有 n 处地点,在这些地点之间连有 m 条道路。其中 1 号地点为景区入口,n 号地点为景区出口。我们把一天当中景区开门营业的时间记为 0 时刻,则从 0 时刻起,每隔 k 单位时间便有一辆旅游巴士到达景区入口,同时有一辆旅游巴士从景区出口驶离景区。

所有道路均只能单向通行。对于每条道路,游客步行通过的用时均为恰好 1 单位时间。

小 Z 希望乘坐旅游巴士到达景区入口,并沿着自己选择的任意路径走到景区出口,再乘坐旅游巴士离开景区,这意味着他到达和离开景区的时间都必须是 k 的非负整数倍。由于节假日客流众多,小 Z 在坐旅游巴士离开景区前只想一直沿着景区道路移动,而不想在任何地点(包括景区入口和出口)或者道路上逗留。

出发前,小 Z 忽然得知:景区采取了限制客流的方法,对于每条道路均设置了一个“开放时间”a_i,游客只有不早于 a_i 时刻才能通过这条道路。

请你帮助小 Z 设计一个旅游方案,使得他乘坐旅游巴士离开景区的时间尽量地早。

【输入格式】

从文件 bus.in 中读入数据。输入的第一行包含 3 个正整数 n, m, k,表示旅游景点的地点数、道路数,以及旅游巴士的发车间隔。

输入的接下来 m 行,每行包含 3 个非负整数 u_i, v_i, a_i,表示第 i 条道路从地点 u_i 出发,到达地点 v_i,道路的“开放时间”为 a_i。

【输出格式】

输出到文件 bus.out 中。输出一行,仅包含一个整数,表示小 Z 最早乘坐旅游巴士离开景区的时刻。如果不存在符合要求的旅游方案,输出 −1。

【样例 1】

输入
5 5 3
1 2 0
2 5 1
1 3 0
3 4 3
4 5 1
输出
6
图:样例 1 的景点道路图(1→2→5 与 1→3→4→5)
图:样例 1 的景点道路图(1→2→5 与 1→3→4→5)

样例解释:小 Z 可以在 3 时刻到达景区入口,沿 1 → 3 → 4 → 5 的顺序走到景区出口,并在 6 时刻离开。

【数据范围】

对于所有测试数据有:2 ≤ n ≤ 10^4,1 ≤ m ≤ 2 × 10^4,1 ≤ k ≤ 100,1 ≤ u_i, v_i ≤ n,0 ≤ a_i ≤ 10^6。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

图论 + 分层图最短路(同余最短路)。本题的“时间”维度是无限的,必须利用“只关心时刻模 k 的余数”这一点,把状态空间压到 k·n。

【先把题读成三句话】

1. 小 Z 在入口乘大巴进入景区,到达入口的时刻 t0 必须是 k 的非负整数倍;离开出口的时刻 T 也必须是 k 的非负整数倍。

2. 进入景区后【一步都不能停】,每条道路步行恰好花 1 个单位时间。因此若某条路是他走的第 j 条边,那么正着通过它的时刻恰好是 t0 + j − 1。

3. 每条道路有一个开放时间 a_j,正着通过它的时刻必须 ≥ a_j。

【关键观察一:为什么不能只跑普通最短路】

设路径长度为 L、入口出发时刻为 t0,那么离开时刻 T = t0 + L。t0 是 k 的倍数,所以要让 T 也是 k 的倍数,必须有 L ≡ 0 (mod k)。

以样例为例:1 → 2 → 5 这条路长度 L = 2,而 2 不是 3 的倍数,于是无论 t0 取哪个 3 的倍数,到达出口的时刻都 ≡ 2 (mod 3),根本不可能在 k 的倍数时刻离开——这条路是【无效】的。只有 1 → 3 → 4 → 5 这条 L = 3 的路才有资格成为答案。

再加上开放时间的存在:“出发得越晚越容易走通”,所以也不能只保留“最早到达某点的时刻”这一个量。

【关键观察二:倒着走,把“无限的时间”变成“有限的状态”】

把整个行程倒过来看:小 Z 从出口 n 出发、时刻为 T(k 的倍数),逆着道路往入口 1 走,每逆走一条边时间减 1,最终必须落在 (1, 0) 上(在入口、且时刻是 k 的倍数)。

设 f[v][r]:倒着走的过程中位于 v,且当前时刻 t ≡ r (mod k) 时,t 至少要是多少,才能保证一路上正着通过每条道路的时刻都不早于它对应的开放时间。答案就是 f[n][0]。

边界:若倒着走到了入口(v = 1)且余数 r = 0,任务已经完成,约束最松,记 f[1][0] = 0。

【转移】

考虑原图中的一条道路 u → v(开放时间 a)。若倒着走时已经站在 u、此刻时刻为 t,那么在这一步【之前】它一定是站在 v、时刻为 t + 1(从 v 逆着道路走到 u,时间减少 1)。于是有两个条件:

① 正着通过 u → v 的时刻是 (t + 1) − 1 = t,必须 ≥ a;

② 倒着到达 u 后的时刻 t 又要满足已经求出的下界 f[u][t mod k]。

实现时我们反过来沿着正向边把这个下界从 u 推到 v:在 v 处的下界 newT 需要满足 newT ≥ max(a + 1, t + 1),并且 newT ≡ (r + 1) (mod k)(因为从 u 往前退一步,余数加 1)。每个状态的取值只会单调不减、且沿转移严格增大,所以可以直接用堆优化 Dijkstra 标号。

【样例验证】n = 5, m = 5, k = 3】

取 1 → 3 → 4 → 5:入口出发 t0 = 3(3 的倍数)。走 1 → 3 时 t = 3 ≥ a = 0 ✓,4 时刻到达 3;走 3 → 4 时 t = 4 ≥ a = 3 ✓,5 时刻到达 4;走 4 → 5 时 t = 5 ≥ a = 1 ✓,6 时刻到达出口,且 6 是 3 的倍数,可以立刻上车离开,故答案为 6。

对照 DP:f[1][0] = 0 → f[3][1] = 1 → f[4][2] = 5 → f[5][0] = 6;而 1 → 2 → 5 只能得到 f[5][2] = 2,永远落在余数 2 上,不会被当成答案。两条路都被 DP 正确地记录下来。

【复杂度分析】

状态数 k·n ≤ 100 × 10^4 = 10^6,转移总数 k·m ≤ 2 × 10^6。堆优化 Dijkstra 时间 O((kn + km) log(kn)),空间 O(kn),均可通过。

【易错点提示】

1. 不能“原地等待”:景区内不允许停留,唯一能等的地方是入口(等价于把 t0 取成更大的 k 的倍数),所以状态里只能靠余数推进,不能加“等待边”;

2. 不能只保留“最早到达时刻”:由于有开放时间,晚到反而更容易满足约束,必须按 (地点, 余数) 分层;

3. 求的不是最短步数,而是“与目标余数同余的最小时刻”,别忘记向上取整到指定余数;

4. 若 f[n][0] 始终为 INF,输出 −1。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 10005, MAXK = 105;
const ll INF = (1LL << 60);

struct Edge { int to; ll a; };
vector<Edge> g[MAXN];          // 原图:正向道路 u -> v,开放时间 a

// f[v][r]:倒着走时走到 v、当前时刻 t ≡ r (mod k),则 t 至少要多大,
//          才能保证一路正着通过道路的时刻都不早于各自的开放时间。
ll f[MAXN][MAXK];

struct Node {
    ll t; int v, r;
    bool operator<(const Node &o) const { return t > o.t; }   // 小根堆
};

// 求最小的 >= x 且满足 ≡ r (mod k) 的数
ll ceilToMod(ll x, int r, int k) {
    ll add = ((r - x % k) % k + k) % k;
    return x + add;
}

int main() {
    freopen("bus.in", "r", stdin);
    freopen("bus.out", "w", stdout);

    int n, m, k;
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= m; ++i) {
        int u, v; ll a;
        scanf("%d%d%lld", &u, &v, &a);
        g[u].push_back({v, a});
    }

    for (int i = 1; i <= n; ++i)
        for (int r = 0; r < k; ++r)
            f[i][r] = INF;

    priority_queue<Node> pq;
    f[1][0] = 0;                     // 倒着走到入口且余数为 0:已完成,下界为 0
    pq.push({0, 1, 0});

    while (!pq.empty()) {
        Node cur = pq.top(); pq.pop();
        int u = cur.v, r = cur.r;
        ll t = cur.t;
        if (t > f[u][r]) continue;   // 懒惰删除

        for (const Edge &e : g[u]) {
            int nv = e.to;
            int nr = (r + 1) % k;    // 往前退一步,余数 +1
            // 在 v 处的下界:既满足开放时间(t >= a),又比 u 处多走一步
            ll cand = ceilToMod(max(e.a + 1, t + 1), nr, k);
            if (cand < f[nv][nr]) {
                f[nv][nr] = cand;
                pq.push({cand, nv, nr});
            }
        }
    }

    if (f[n][0] >= INF) printf("-1\n");
    else                printf("%lld\n", f[n][0]);
    return 0;
}
2024

接龙 chain

提高+/省选- 文件:chain.in / chain.out 动态规划滑动窗口图论建模状态压缩
✓已掌握

【题目描述】

在玩惯了成语接龙之后,小 J 和他的朋友们发明了一个新的接龙规则。总共有 n 个人参与这个接龙游戏,第 i 个人会获得一个整数序列 S_i 作为他的词库。

一次游戏分为若干轮,每一轮规则如下:

· n 个人中的某个人 p 带着他的词库 S_p 进行接龙。若这不是游戏的第一轮,那么这一轮进行接龙的人不能与上一轮相同,但可以与上上轮或更往前的轮相同。

· 接龙的人选择一个长度在 [2, k] 的 S_p 的连续子序列 A 作为这一轮的接龙序列,其中 k 是给定的常数。若这是游戏的第一轮,那么 A 需要以元素 1 开头,否则 A 需要以上一轮的接龙序列的最后一个元素开头。

序列 A 是序列 S 的连续子序列当且仅当可以通过删除 S 的开头和结尾的若干元素(可以不删除)得到 A。

为了强调合作,小 J 给了 n 个参与游戏的人 q 个任务,第 j 个任务需要这 n 个人进行一次游戏,在这次游戏里进行恰好 r_j 轮接龙,且最后一轮的接龙序列的最后一个元素恰好为 c_j。为了保证任务的可行性,小 J 请来你判断这 q 个任务是否可以完成的。

【输入格式】

从文件 chain.in 中读入数据。本题有多组测试数据。输入的第一行包含一个正整数 T,表示数据组数。接下来包含 T 组数据,每组数据的格式如下:

第一行包含三个整数 n, k, q,分别表示参与游戏的人数、接龙序列长度上限以及任务个数。接下来 n 行:第 i 行包含 (l_i + 1) 个整数 l_i, S_{i,1}, S_{i,2}, …, S_{i,l_i},其中第一个整数 l_i 表示序列 S_i 的长度。接下来 q 行:第 j 行包含两个整数 r_j, c_j,描述一个任务。

【输出格式】

输出到文件 chain.out 中。对于每个任务:输出一行包含一个整数,若任务可以完成输出 1,否则输出 0。

【样例 1】

输入
1
3 3 7
5 1 2 3 4 1
3 1 2 5
3 5 1 6
1 2
1 4
2 4
3 4
6 6
1 1
7 7
输出
1
0
1
0
1
0
0

样例解释:第一组询问 p1 = 1、A1 = 12 可行;第三组询问 {p_i} = {2,1}、{A_i} = {12, 234} 可行;第五组询问 {p_i} = {1,2,3,1,2,3}、{A_i} = {12, 25, 51, 12, 25, 516} 可行。

【数据范围】

对于所有测试数据,保证:1 ≤ T ≤ 5;1 ≤ n ≤ 10^5,2 ≤ k ≤ 2 × 10^5,1 ≤ q ≤ 10^5;1 ≤ l_i ≤ 2 × 10^5,1 ≤ S_{i,j} ≤ 2 × 10^5;1 ≤ r_j ≤ 10^2,1 ≤ c_j ≤ 2 × 10^5;设 Σl 为单组测试数据内所有 l_i 的和,则 Σl ≤ 2 × 10^5。

测试点n ≤r ≤Σl ≤q ≤特殊性质
110^312,00010^3无
2, 31052010^2无
4, 510^3102,00010^3A
610^510^22×10^510^5A
7, 810^3102,00010^3B
9, 1010^510^22×10^510^5B
11, 1210^3102,00010^3C
13, 1410^510^22×10^510^5C
15~1710^3102,00010^3无
18~2010^510^22×10^510^5无

特殊性质 A:保证 k = 2 × 10^5。特殊性质 B:保证 k ≤ 5。特殊性质 C:保证在单组测试数据中,任意一个字符在词库中出现次数之和均不超过 5。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

动态规划 + 滑动窗口 + 状态设计。本题的关键难点不是算法框架,而是**如何刻画“相邻两轮不能是同一个人”这一约束**——如果直接把“上一轮是谁”放进状态,状态数会爆炸。

【关键观察:只要有两种可能就不会被卡住】

设某一轮结束时最后一个元素是值 v。如果存在**至少两个不同的人**都能让这一轮以 v 结尾,那么下一轮由谁来接都无所谓:无论下一轮的人是谁,我们总能在上一轮里挑一个不是他的“结尾者”。

反之,如果只有唯一一个人 p 能让这一轮以 v 结尾,那么下一轮就不能由 p 来接。

于是,我们只需要记录“能以 v 结尾的人有多少种”——把它压缩成三种状态即可!

【状态设计】

设 dp[r][v] 表示“进行 r 轮接龙、且第 r 轮的接龙序列以值 v 结尾”这个状态的属性:

· dp[r][v] = −1:不可能实现;

· dp[r][v] = i(i ≥ 1):只有第 i 个人能让第 r 轮以 v 结尾;

· dp[r][v] = −2:有两个人或更多人能让第 r 轮以 v 结尾。

初始(第 0 轮):dp[0][1] = −2,其余为 −1。含义是“游戏的起点是值 1,任何一个人都可以来开第一轮”。

【转移:在每个人的词库里滑窗】

对于第 r 轮,我们枚举每一个人 i,在他的词库 S_i 上从左到右扫描。设当前要把 S_i[j] 作为本轮的“结尾值”,那么本轮序列的起点必须是 S_i 中位置 t ∈ [j − k + 1, j − 1] 上的某个元素(长度 ≥ 2 且 ≤ k),并且上一轮能以值 S_i[t] 结尾且**允许第 i 个人来接**:

允许的条件是 dp[r−1][S_i[t]] == −2,或者 dp[r−1][S_i[t]] = p 且 p ≠ i。

因此在扫描时维护一个滑窗计数器 cnt = “窗口 [j−k+1, j−1] 内满足上述条件的起点个数”。每次 j 前进时,加入位置 j−1、移除位置 j−k(若越界则忽略)。只要 cnt > 0,就说明第 i 个人可以用 S_i[j] 作为本轮结尾,于是更新 dp[r][S_i[j]]:

· 若 dp[r][v] == −1 → 设为 i;· 若 dp[r][v] == i → 不变;· 否则设为 −2。

【回答询问】

把每一轮中“dp[r][v] ≠ −1”的值记录下来(可以用一个 ok[r][v] 布尔表),询问 (r, c) 直接查表输出 1/0。注意 r ≤ 100,所以最多只需要推进 100 轮。

【复杂度分析】

轮数 R ≤ 100,每轮对所有人各扫一遍共 O(Σl);再加上每轮把当前轮的结果表刷一遍 O(V)(V 为值域上限)。总时间 O(R·(Σl + V)) ≈ 100 × (2×10^5 + 2×10^5) = 4×10^7,空间 O(R·V) 用布尔表约 20MB,可通过。

【易错点提示】

1. dp 的“−2”含义是“≥ 2 人”,不是“无解”,初始 dp[0][1] = −2 是必要的,它表示起点对所有玩家开放;

2. 滑窗的左边界是 j − k + 1(对应子串长度恰好 k),别错写成 j − k;

3. 同一轮内,dp[r] 只读取 dp[r−1],不能边算边读当前轮;

4. 询问的 c_j 可能大于所有词库中出现的最大值,直接输出 0;

5. 多组测试数据要把 dp 与 ok 表清干净。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;

const int MAXV = 200005, MAXR = 105;

vector<int> S[100005];
int buf1[MAXV], buf2[MAXV];      // 滚动数组:dp[r-1]、dp[r]
char ok[MAXR][MAXV];             // ok[r][v] = (dp[r][v] != -1)

int main() {
    freopen("chain.in", "r", stdin);
    freopen("chain.out", "w", stdout);

    int T;
    scanf("%d", &T);
    while (T--) {
        int n, k, q;
        scanf("%d%d%d", &n, &k, &q);

        int maxv = 1;
        for (int i = 1; i <= n; ++i) {
            int l; scanf("%d", &l);
            S[i].resize(l);
            for (int j = 0; j < l; ++j) {
                scanf("%d", &S[i][j]);
                maxv = max(maxv, S[i][j]);
            }
        }

        int *pre = buf1, *cur = buf2;
        fill(pre, pre + maxv + 2, -1);
        pre[1] = -2;                                  // 第 0 轮:起点为 1,任何玩家都可以开第一轮

        memset(ok, 0, (size_t)MAXR * (maxv + 2));

        for (int r = 1; r <= 100; ++r) {
            fill(cur, cur + maxv + 2, -1);

            for (int i = 1; i <= n; ++i) {
                int l = (int)S[i].size();
                int cnt = 0;                          // 窗口内合法起点的个数
                for (int j = 0; j < l; ++j) {
                    if (j - 1 >= 0) {                 // 加入位置 j-1
                        int pv = pre[S[i][j - 1]];
                        if (pv == -2 || (pv >= 1 && pv != i)) ++cnt;
                    }
                    if (j - k >= 0) {                 // 移除位置 j-k
                        int pv = pre[S[i][j - k]];
                        if (pv == -2 || (pv >= 1 && pv != i)) --cnt;
                    }
                    if (cnt > 0) {                    // 第 i 个人可以让本轮以 S[i][j] 结尾
                        int v = S[i][j];
                        if (cur[v] == -1)                 cur[v] = i;
                        else if (cur[v] != i && cur[v] != -2) cur[v] = -2;
                    }
                }
            }

            for (int v = 1; v <= maxv; ++v)
                ok[r][v] = (cur[v] != -1) ? 1 : 0;

            swap(pre, cur);                           // 进入下一轮
        }

        while (q--) {
            int r, c;
            scanf("%d%d", &r, &c);
            if (r >= 1 && r <= 100 && c >= 1 && c <= maxv && ok[r][c]) printf("1\n");
            else                                                       printf("0\n");
        }
    }
    return 0;
}
2025

多边形 polygon

提高+/省选- 文件:polygon.in / polygon.out 组合数学前缀和排序计数
✓已掌握

【题目描述】

小 R 有 n 根小木棍,第 i 根长度为 aᵢ。小 X 希望小 R 从中选出若干根,按任意顺序首尾相连拼成一个多边形。能拼成多边形的条件:选出的 m 根小木棍(长度 l₁,…,l_m)满足 m ≥ 3 且所有长度之和大于最大长度的两倍,即 Σlᵢ > 2 × max(lᵢ)。

问有多少种选择小木棍的方案(按下标集合区分)能拼成多边形,答案对 998244353 取模。

【输入格式】

输入文件名为 polygon.in。第一行一个正整数 n。第二行 n 个正整数 a₁, a₂, …, a_n。

【输出格式】

输出文件名为 polygon.out。输出一行一个非负整数,表示方案数对 998244353 取模。

【样例 1】

输入
5
1 2 3 4 5
输出
9

【样例 2】

输入
5
2 2 3 8 10
输出
6

【数据规模与约定】

对于所有测试数据,保证 3 ≤ n ≤ 5000;对于所有 1 ≤ i ≤ n,均有 1 ≤ aᵢ ≤ 5000。

▶ 在线做题 内置离线 C++ 模拟器 · 编译运行 · 提交判定 · 无需安装环境
代码(可编辑)
输入
输出

      
题解与解析 (点击展开)

【题意与考点】

计数 + 排序 + 截断背包(正难则反)。核心是「多边形条件」的转化:选出的边中,只要最长边小于其余边之和(即总和 > 2×最大边),就能拼成多边形。答案与顺序无关,先排序。

【关键转化】

排序后从小到大枚举「最长边」aᵢ(它一定被选入方案)。此时其余边只能从前 i-1 根(都 ≤ aᵢ)中选。设其余边之和为 S,合法条件总和 > 2aᵢ 等价于 S > aᵢ。注意:因为其余边都 ≤ aᵢ,单根不可能 > aᵢ,所以「至少选 2 根」的条件自然满足,无需额外判断。

【正难则反 + 截断背包】

对每个 i,从前 i-1 根选子集的总方案数是 2^(i-1)。其中「不合法」的是那些和 ≤ aᵢ 的方案(S ≤ aᵢ 就不能构成多边形)。于是贡献 = 2^(i-1) − (和 ≤ aᵢ 的方案数)。

维护 dp[j] = 从前 i-1 根选子集、和为 j 的方案数。关键是**截断**:由于 aᵢ ≤ maxA ≤ 5000,任何和 > maxA 的子集对任意阈值都必然合法,所以 dp 只需维护到 maxA+1,把超过 maxA 的和「并」到 maxA+1 这一格。这样 dp 数组大小 O(maxA),每次转移 O(maxA)。

具体流程:① 对第 i 根,累加贡献 ans += 2^(i-1) − Σ_{j=0}^{aᵢ} dp[j](其中「和 ≤ aᵢ」的不合法方案,注意 j 从 0 到 aᵢ,包含选 0 根和选 1 根的情况,它们恰好都不合法);② 把 aᵢ 加入背包:逆序 dp[j+aᵢ] += dp[j],若 j+aᵢ > maxA 则并入 maxA+1。

【复杂度分析】

O(n · maxA),其中 maxA ≤ 5000,n ≤ 5000,最坏 2.5×10⁷,配合取模可通过。空间 O(maxA)。

【易错点提示】

1. 必须先排序,否则「最长边是 aᵢ、其余边 ≤ aᵢ」的前提不成立;

2. 截断上界是 maxA(全局最大木棍长度),不是当前 aᵢ;

3. 2 的幂次用快速幂或递推预处理,减法取模记得 +MOD;

4. 若用 O(n·Σaᵢ) 的朴素背包(和上界开到总和),会 TLE/MLE,必须截断。

参考代码(C++)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 998244353;
const int MAXN = 5005;

int n;
ll a[MAXN], dp[MAXN];   // dp[j]: 当前前缀中选子集、和为 j 的方案数(j 截断到 maxA+1)

ll qpow(ll base, ll e) {
    ll r = 1;
    while (e) {
        if (e & 1) r = r * base % MOD;
        base = base * base % MOD;
        e >>= 1;
    }
    return r;
}

int main() {
    freopen("polygon.in", "r", stdin);
    freopen("polygon.out", "w", stdout);

    cin >> n;
    int mx = 0;
    for (int i = 1; i <= n; ++i) { cin >> a[i]; mx = max(mx, (int)a[i]); }
    sort(a + 1, a + n + 1);

    int M = mx + 1;        // 截断上界:任何和 > mx 都并到 mx+1
    dp[0] = 1;
    ll ans = 0, tot = 1;   // tot = 2^(i-1),当前前缀子集总数

    for (int i = 1; i <= n; ++i) {
        int x = a[i];
        // 不合法方案:和 <= x 的方案数(含选 0/1 根,它们都 <= x)
        ll bad = 0;
        for (int j = 0; j <= x; ++j) bad = (bad + dp[j]) % MOD;
        ll add = (tot - bad + MOD) % MOD;   // 合法方案 = 总方案 - 不合法
        ans = (ans + add) % MOD;

        // 把 a[i] 加入背包(逆序,超过 M 的并入 M)
        for (int j = M; j >= 0; --j) {
            if (!dp[j]) continue;
            int u = j + x;
            if (u > M) u = M;
            dp[u] = (dp[u] + dp[j]) % MOD;
        }
        tot = tot * 2 % MOD;
    }

    cout << ans % MOD << endl;
    return 0;
}
🎯
第二部分

洛谷配套练习

按题号对应的知识点与难度精选 150 题,点击题号即可在洛谷打开完整题目。

💡
难度对应:普及− → T1,普及/提高− → T2,普及+/提高 → T3,提高+/省选− → T4。下表已按此梯度逐题匹配,可用「知识点」标签或搜索框快速定位。

对应 CSP-J 复赛 T1:以「模拟 / 枚举 / 字符串 / 基础数学」为主,训练读题与把规则直译成代码的基本功。这些题目通常代码量小、边界清晰,是稳固入门的必刷题。

T1配套练习

共 30 题
🔍
#题号题目名称难度核心知识点
1P1014 ↗[NOIP 1999 普及组] Cantor 表普及−模拟、枚举、数学
2P1067 ↗[NOIP 2009 普及组] 多项式输出普及−模拟、字符串、数学
3P2141 ↗[NOIP 2014 普及组] 珠心算测验普及−模拟、枚举
4P1093 ↗[NOIP 2007 普及组] 奖学金普及−模拟、数学、排序
5P1088 ↗[NOIP 2004 普及组] 火星人普及−模拟、数学、排序
6P1015 ↗[NOIP 1999 普及组] 回文数普及−模拟、进制、高精度
7P5016 ↗[NOIP 2018 普及组] 龙虎斗普及−模拟、枚举
8P1029 ↗[NOIP 2001 普及组] 最大公约数和最小公倍数问题普及−枚举、数学
9P2241 ↗统计方形(数据加强版)普及−枚举、数学
10P1100 ↗高低位交换普及−进制、字符串、数学
11P2118 ↗[NOIP 2014 普及组] 比例简化普及−枚举、数学
12P5657 ↗[CSP-S 2019] 格雷码普及−进制、数学、位运算
13P1458 ↗[USACO2.1] 顺序的分数 Ordered Fractions普及−枚举、数学、排序
14B4494 ↗[语言月赛 202602] 弹珠方阵普及−枚举、数组、循环结构
15P3955 ↗[NOIP 2017 普及组] 图书管理员普及−字符串、排序
16P1062 ↗[NOIP 2006 普及组] 数列普及−进制、数学
17P1072 ↗[NOIP 2009 提高组] Hankson 的趣味题普及−枚举、数学
18P5833 ↗[USACO19DEC] Livestock Lineup B普及−枚举、字符串
19P1051 ↗[NOIP 2005 提高组] 谁拿了最多奖学金普及−字符串、排序
20P1017 ↗[NOIP 2000 提高组] 进制转换普及−进制、数学
21B4067 ↗[GESP202412 三级] 打印数字普及−循环结构、字符串、分支结构
22P1009 ↗[NOIP 1998 普及组] 阶乘之和普及−数学、高精度
23P1044 ↗[NOIP 2003 普及组] 栈普及−数学、递推
24P1030 ↗[NOIP 2001 普及组] 求先序排列普及−字符串
25P1087 ↗[NOIP 2004 普及组] FBI 树普及−字符串
26P1075 ↗[NOIP 2012 普及组] 质因数分解普及−数学
27P1010 ↗[NOIP 1998 普及组] 幂次方普及−数学
28P1781 ↗宇宙总统普及−字符串、排序、高精度
29B4499 ↗[GESP202603 三级] 二进制回文串普及−字符串(入门)、进制
30P2957 ↗[USACO09OCT] Barn Echoes G普及−字符串、前缀和
没有匹配的题目

对应 CSP-J 复赛 T2:以「模拟 + 贪心 / 排序 / 前缀和差分 / 双指针 / 简单 DP」为主,开始要求选手把朴素思路优化到线性或对数复杂度。

T2配套练习

共 30 题
🔍
#题号题目名称难度核心知识点
1P4653 ↗[CEOI 2017] Sure Bet普及/提高−贪心、双指针 two-pointer、前缀和
2P1016 ↗[NOIP 1999 普及组/提高组] 旅行家的预算普及/提高−贪心、模拟
3P4105 ↗[HEOI2014] 南园满地堆轻絮普及/提高−贪心、模拟、前缀和
4P12733 ↗磨合普及/提高−贪心、前缀和、排序
5P2058 ↗[NOIP 2016 普及组] 海港普及/提高−模拟、双指针 two-pointer、队列
6P1083 ↗[NOIP 2012 提高组] 借教室普及/提高−差分、前缀和、二分
7P1057 ↗[NOIP 2008 普及组] 传球游戏普及/提高−模拟、递推、动态规划 DP
8P1095 ↗[NOIP 2007 普及组] 守望者的逃离普及/提高−贪心、模拟、动态规划 DP
9P3143 ↗[USACO16OPEN] Diamond Collector S普及/提高−贪心、双指针 two-pointer、枚举
10P2879 ↗[USACO07JAN] Tallest Cow S普及/提高−模拟、差分、前缀和
11P9050 ↗[PA 2021] Sumy普及/提高−贪心、前缀和、排序
12P1022 ↗[NOIP 2000 普及组] 计算器的改良普及/提高−模拟、字符串、数学
13P1158 ↗[NOIP 2010 普及组] 导弹拦截普及/提高−模拟、排序、枚举
14P1058 ↗[NOIP 2008 普及组] 立体图普及/提高−模拟、字符串、枚举
15P4085 ↗[USACO17DEC] Haybale Feast G普及/提高−双指针 two-pointer、前缀和、二分
16P6191 ↗[USACO09FEB] Bulls And Cows S普及/提高−前缀和、递推、数学
17P4792 ↗[BalticOI 2018] 火星人的 DNA普及/提高−双指针 two-pointer、前缀和、字符串
18P1314 ↗[NOIP 2011 提高组] 聪明的质监员普及/提高−前缀和、二分、数学
19P5686 ↗[CSP-S 2019 江西] 和积和普及/提高−前缀和、递推、数学
20P7992 ↗[USACO21DEC] Convoluted Intervals S普及/提高−差分、前缀和、数学
21P1719 ↗最大加权矩形普及/提高−前缀和、递推、枚举
22P1638 ↗逛画展普及/提高−双指针 two-pointer、二分、队列
23P1309 ↗[NOIP 2011 普及组] 瑞士轮普及/提高−模拟、排序
24P4086 ↗[USACO17DEC] My Cow Ate My Homework S普及/提高−前缀和、递推、枚举
25P10450 ↗[USACO03MAR] Best Cow Fences G普及/提高−前缀和、二分、动态规划 DP
26P16796 ↗[蓝桥杯 2026 国 B] 灯带修补普及/提高−双指针 two-pointer、前缀和、二分
27P16723 ↗[GKS 2019 #A] Training普及/提高−差分、前缀和、排序
28P1096 ↗[NOIP 2007 普及组] Hanoi 双塔问题普及/提高−递推、数学、高精度
29P3662 ↗[USACO17FEB] Why Did the Cow Cross the Road II S普及/提高−前缀和、队列、枚举
30P3029 ↗[USACO11NOV] Cow Lineup S普及/提高−双指针 two-pointer、队列、离散化
没有匹配的题目

对应 CSP-J 复赛 T3:以「动态规划 / 搜索 / 二分 / 图论基础 / 数论」为主,注重建模能力——把题意抽象成 DP 状态或图上问题。

T3配套练习

共 30 题
🔍
#题号题目名称难度核心知识点
1P1018 ↗[NOIP 2000 提高组] 乘积最大普及+/提高搜索、动态规划 DP、线性 DP
2P1948 ↗[USACO08JAN] Telephone Lines S普及+/提高搜索、深度优先搜索 DFS、广度优先搜索 BFS
3P3956 ↗[NOIP 2017 普及组] 棋盘普及+/提高搜索、深度优先搜索 DFS、广度优先搜索 BFS
4P5020 ↗[NOIP 2018 提高组] 货币系统普及+/提高动态规划 DP、数学、贪心
5P1363 ↗幻象迷宫普及+/提高搜索、深度优先搜索 DFS、广度优先搜索 BFS
6P1849 ↗[USACO12MAR] Tractor S普及+/提高搜索、广度优先搜索 BFS、贪心
7P9187 ↗[USACO23OPEN] Field Day S普及+/提高动态规划 DP、数学、广度优先搜索 BFS
8P12027 ↗[USACO25OPEN] Ski Slope S普及+/提高动态规划 DP、深度优先搜索 DFS、二分
9P1073 ↗[NOIP 2009 提高组] 最优贸易普及+/提高搜索、动态规划 DP、图论
10P2890 ↗[USACO07OPEN] Cheapest Palindrome G普及+/提高动态规划 DP、递推、记忆化搜索
11P3914 ↗染色计数普及+/提高动态规划 DP、深度优先搜索 DFS、数学
12P10956 ↗金字塔普及+/提高动态规划 DP、深度优先搜索 DFS、记忆化搜索
13P1525 ↗[NOIP 2010 提高组] 关押罪犯普及+/提高二分、贪心、图论
14P2119 ↗[NOIP 2016 普及组] 魔法阵普及+/提高数学、前缀和、枚举
15P9186 ↗[USACO23OPEN] Milk Sum S普及+/提高数学、二分、前缀和
16P10804 ↗[CEOI 2024] 玩具谜题普及+/提高深度优先搜索 DFS、广度优先搜索 BFS、单调队列
17P1082 ↗[NOIP 2012 提高组] 同余方程普及+/提高数论、数学
18P7075 ↗[CSP-S 2020] 儒略日普及+/提高数学、二分
19P6146 ↗[USACO20FEB] Help Yourself G普及+/提高二分、递推、前缀和
20P8792 ↗[蓝桥杯 2022 国 A] 最大公约数普及+/提高数论、二分、贪心
21P4275 ↗萃香的请柬普及+/提高数学、二分、递推
22P15573 ↗[USACO26FEB] Clash! S普及+/提高二分、贪心、前缀和
23P13376 ↗[GCJ 2011 #2] Expensive Dinner普及+/提高数论、数学、二分
24P17044 ↗[NWERC 2021] 辣度升温 / Heating Up普及+/提高二分、贪心、栈
25P2870 ↗[USACO07DEC] Best Cow Line G普及+/提高二分、贪心、字符串
26P3745 ↗[六省联考 2017] 期末考试普及+/提高数学、贪心、前缀和
27P3917 ↗异或序列普及+/提高数学、前缀和、位运算
28P11855 ↗[CSP-J 2022 山东] 部署普及+/提高差分、前缀和
29P3663 ↗[USACO17FEB] Why Did the Cow Cross the Road III S普及+/提高深度优先搜索 DFS、广度优先搜索 BFS
30P10492 ↗[ICPC 2003 Aizu R] Weather Forecast普及+/提高深度优先搜索 DFS、广度优先搜索 BFS、记忆化搜索
没有匹配的题目

对应 CSP-J 复赛 T4:以「复杂 DP(树形 / 状压 / 区间)/ 最短路与分层图 / 数据结构 / 数论」为主,是拉开差距的关键,建议在 T1~T3 稳定后再集中突破。

T4配套练习

共 30 题
🔍
#题号题目名称难度核心知识点
1P1967 ↗[NOIP 2013 提高组] 货车运输提高+/省选−图论、并查集、贪心
2P3119 ↗[USACO15JAN] Grass Cownoisseur G提高+/省选−最短路、图论、动态规划 DP
3P1099 ↗[NOIP 2007 提高组] 树网的核提高+/省选−最短路、动态规划 DP、单调队列
4P2371 ↗[国家集训队] 墨墨的等式提高+/省选−最短路、图论、动态规划 DP
5P1783 ↗海滩防御提高+/省选−最短路、图论、并查集
6P14422 ↗[JOISC 2014] 水壶 / Water Bottle提高+/省选−图论、广度优先搜索 BFS、并查集
7P9235 ↗[蓝桥杯 2023 省 A] 网络稳定性提高+/省选−图论、并查集、贪心
8P1295 ↗[TJOI2011] 书架提高+/省选−动态规划 DP、线段树、单调队列
9P2901 ↗[USACO08MAR] Cow Jogging G提高+/省选−最短路、动态规划 DP、广度优先搜索 BFS
10P2680 ↗[NOIP 2015 提高组] 运输计划提高+/省选−图论、二分、最近公共祖先 LCA
11P3957 ↗[NOIP 2017 普及组] 跳房子提高+/省选−动态规划 DP、单调队列、二分
12P2672 ↗[NOIP 2015 普及组] 推销员提高+/省选−线段树、树状数组、贪心
13P1514 ↗[NOIP 2010 提高组] 引水入城提高+/省选−动态规划 DP、广度优先搜索 BFS、贪心
14P2272 ↗[ZJOI2007] 最大半连通子图提高+/省选−图论、动态规划 DP、记忆化搜索
15P3132 ↗[USACO16JAN] Angry Cows G提高+/省选−动态规划 DP、单调队列、二分
16P15576 ↗[USACO26FEB] Good Cyclic Shifts G提高+/省选−线段树、树状数组、差分
17P10357 ↗[PA 2024] Żelki提高+/省选−最短路、图论、动态规划 DP
18P3629 ↗[APIO2010] 巡逻提高+/省选−动态规划 DP、树形 DP、贪心
19P3092 ↗[USACO13NOV] No Change G提高+/省选−状压 DP、单调队列、二分
20P2727 ↗[USACO3.2] 01串 Stringsobits提高+/省选−动态规划 DP、二分、数学
21P4269 ↗[USACO18FEB] Snow Boots G提高+/省选−线段树、并查集、单调队列
22P3071 ↗[USACO13JAN] Seating G提高+/省选−线段树、单调队列、二分
23P3104 ↗[USACO14MAR] Counting Friends G提高+/省选−图论、二分、贪心
24P3127 ↗[USACO15OPEN] Trapped in the Haybales G提高+/省选−线段树、并查集、二分
25P3605 ↗[USACO17JAN] Promotion Counting P提高+/省选−线段树、树状数组、离散化
26P4873 ↗[USACO14DEC] Cow Jog G提高+/省选−树状数组、二分、贪心
27P12056 ↗[THUPC 2025 决赛] 石墨烯提高+/省选−并查集、单调队列、二分
28P1966 ↗[NOIP 2013 提高组] 火柴排队提高+/省选−树状数组、贪心、离散化
29P5021 ↗[NOIP 2018 提高组] 赛道修建提高+/省选−二分、贪心、最近公共祖先 LCA
30P6172 ↗[USACO16FEB] Load Balancing P提高+/省选−树状数组、二分、倍增
没有匹配的题目

2025 年真题专项配套:围绕最新一届(拼数 / 座位 / 异或和 / 多边形)四道题的考点精选,难度梯度与考题一致(普及− → 提高+/省选−),适合考后复盘与同源巩固。已与前面 T1~T4 通用题去重。

2025真题专项练习

共 30 题
🔍
#题号题目名称难度核心知识点
1P1055 ↗[NOIP 2008 普及组] ISBN 号码普及−字符串、模拟
2P1042 ↗[NOIP 2003 普及组] 乒乓球普及−字符串、模拟
3P1308 ↗[NOIP 2011 普及组] 统计单词数普及−字符串、模拟
4P1104 ↗生日普及−字符串、模拟、排序
5P1862 ↗输油管道问题普及−模拟、排序、枚举
6P1059 ↗[NOIP 2006 普及组] 明明的随机数普及−模拟、排序
7P1068 ↗[NOIP 2009 普及组] 分数线划定普及−模拟、排序
8P1097 ↗[NOIP 2007 提高组] 统计数字普及−模拟、排序
9P12648 ↗[KOI 2024 Round 2] 路灯普及/提高−模拟、数学、排序
10P1167 ↗刷题普及/提高−模拟、数学、排序
11P1555 ↗[USACO11NOV] 尴尬的数字 Awkward Digits B普及/提高−模拟、数学、枚举
12P2239 ↗[NOIP 2014 普及组] 螺旋矩阵普及/提高−模拟、数学
13P5682 ↗[CSP-J 2019 江西] 次大值普及/提高−数学、排序
14P1023 ↗[NOIP 2000 普及组] 税收与补贴问题普及/提高−数学、枚举
15P7667 ↗[JOI 2018 Final] 美术展览 / Art Exhibition普及/提高−数学、枚举、贪心
16P13416 ↗[COCI 2012/2013 #4] RAZLIKA普及/提高−数学、排序、贪心
17P2114 ↗[NOI2014] 起床困难综合症普及+/提高贪心、位运算、动态规划 DP
18P4181 ↗[USACO18JAN] Rental Service S普及+/提高贪心、前缀和
19P6878 ↗[JOI 2020 Final] JJOOII 2普及+/提高贪心、前缀和、动态规划 DP
20P15255 ↗[USACO26JAN2] Moo Hunt B普及+/提高位运算、数学、动态规划 DP
21P17152 ↗[ICPC 2017 Xi'an R] Sum of xor sum普及+/提高位运算、前缀和
22P2671 ↗[NOIP 2015 普及组] 求和普及+/提高前缀和
23P2312 ↗[NOIP 2014 提高组] 解方程普及+/提高哈希 hashing、数学
24P1066 ↗[NOIP 2006 提高组] 2^k 进制数提高+/省选−组合数学、数学、递推
25P1987 ↗摇钱树提高+/省选−背包 DP、动态规划 DP、排序
26P16714 ↗玲珑提高+/省选−背包 DP、动态规划 DP、数学
27P1450 ↗[HAOI2008] 硬币购物提高+/省选−背包 DP、动态规划 DP、前缀和
28P2606 ↗[ZJOI2010] 排列计数提高+/省选−组合数学、动态规划 DP、数学
29P6046 ↗纯粹容器提高+/省选−组合数学、动态规划 DP、数学
30P5017 ↗[NOIP 2018 普及组] 摆渡车提高+/省选−动态规划 DP、前缀和
没有匹配的题目
CSP-J 复赛真题集训手册 · 2019–2025 · 共 28 道真题 + 150 道配套练习