CSP-J 2026 算法比赛题解
作者: | 更新日期:
模拟、双指针、按位贪心与动态规划
本文首发于公众号:天空的代码世界,微信号:tiankonguse
零、背景
今天 2026 年 CSP-J 的比赛结束了,快速做了一下,分享一下题解

本场题型概览如下。
第一题:模拟
第二题:双指针
第三题:按位贪心
第四题:动态规划
PS:pdf 题目和源码已经上传网盘,后台回复 CSP-J-2026 获取题目与源码。
一、旅游(travel)
题意:列车有 17 节车厢,编号 01 到 17,其中 01、16 是一等座车厢,其余是二等座车厢。
每节二等座车厢有 18 排,每排 5 个座位,从左到右依次是 A、B、C、D、F,C 与 D 之间是过道。
两个座位相邻,当且仅当在同一节车厢的同一排、位于过道的同一侧、且在该侧的座位序列中相邻。
现在给两张车票(车厢号、座位号),依次判断四个问题:是否在同一节车厢、是否在同一排、是否在过道的同一侧、是否相邻,输出一个长度为 4 的 01 字符串。
数据范围:车厢号是 01 到 17 之间除 01、16 外的两位车厢号;座位号由两位排号(01~18)和一位字母(A、B、C、D、F)组成;两张车票不是同一个座位。
思路:模拟
题目已经给出四个递进的判断条件,依次判断即可,前面不满足时后面必然是 0。
所以这道题的关键是:如何把座位号转化为方便比较的数据。
子问题一:把座位号解析成结构化数据。
车厢号、排号都是两位数字,直接解析即可。
座位字母需要特殊处理:A、B、C、D、F 中 C 与 D 之间是过道,为了让「同侧」和「相邻」都能用数值判断,可以把字母映射成列号 0、1、2、4、5(中间空一格表示过道)。
这样左侧座位 A、B、C 的列号小于 3,右侧座位 D、F 的列号大于等于 3,「同侧」等价于列号都小于 3 或都大于等于 3。
「相邻」等价于「同侧且列号差为 1」——C(2) 与 D(4) 的列号差为 2,不会误判成相邻。
子问题二:四个条件递进判断。
先判断车厢号,不相同直接输出 0000;
相同再判断排号,不同输出 1000;
再判断侧边,不同输出 1100;
最后判断列号差是否为 1,决定最后一位。
每个判断都是 O(1),总复杂度 O(1)。
struct Seat {
int no;
int row;
int col;
int side; // 0 在过道左侧,1 在过道右侧
void Parse(char* c, char* s) {
no = (c[0] - '0') * 10 + (c[1] - '0');
row = (s[0] - '0') * 10 + (s[1] - '0');
col = s[2] - 'A';
if (s[2] == 'D') {
col = 'E' - 'A'; // ABCDF -> 0 1 2 4 5,中间空一格表示过道
}
if (col < 3) {
side = 0;
} else {
side = 1;
}
}
} A, B;
char c1[5], s1[5], c2[5], s2[5];
string ans;
int Dis(int a, int b) { return abs(a - b); }
void Parse() {
A.Parse(c1, s1);
B.Parse(c2, s2);
ans.resize(4, '0');
if (A.no != B.no) return;
ans[0] = '1';
if (A.row != B.row) return;
ans[1] = '1';
if (A.side != B.side) return;
ans[2] = '1';
if (Dis(A.col, B.col) > 1) return;
ans[3] = '1';
}
void Solver() { //
scanf("%s%s%s%s", c1, s1, c2, s2);
Parse();
printf("%s\n", ans.c_str());
}
二、回文串(string)
题意:字符串 S 由 n 段构成,第 i 段由 a_i 个字符 c_i 组成,依次拼接得到 S。
小 R 可以把任意位置的字符修改成任意小写英文字母,问最少修改多少个位置,才能把 S 改成回文串。
数据范围:1 ≤ n ≤ 2e5;1 ≤ a_i ≤ 1e9,c_i 是小写字母。
S 的总长度可达 2e14,不能展开成字符串。
思路:双指针
子问题一:展开后的最少修改数怎么算?
回文串要求对称位置的字符相同。
对于一对对称位置,若字符相同则不用修改;若不同,则必须修改其中一个,花费 1 次。
所以最少修改数等于「对称位置中字符不同的对数」。
展开后可以用双指针从两端向中间扫,逐对比较。
子问题二:S 太长无法展开,怎么办?
我们直接在两端的段上做双指针。
维护左右两个段指针 l、r,每轮取出两端段中较短的长度 lrMin = min(a[l], a[r]),一次性处理这 lrMin 对字符:
若两端字符不同,说明这 lrMin 对对称位置都不同,需要修改,答案累加 lrMin;
若相同,则都不需要修改。
然后两端各自消耗 lrMin 长度,哪一端段耗尽就移动哪一端的段指针。
这样一次批量处理一整段,复杂度从 O(L) 降为 O(n)。
int n;
vector<pair<ll, char>> nums;
ll v;
char str[2];
ll SolverAns() {
if (n == 1) return 0;
ll ans = 0;
int l = 0, r = n - 1;
while (l < r) {
auto& [ln, lc] = nums[l];
auto& [rn, rc] = nums[r];
ll lrMin = min(ln, rn);
if (lc != rc) {
// 两端字符不同,这 lrMin 对都要各改 1 个
ans += lrMin;
}
ln -= lrMin;
rn -= lrMin;
if (ln == 0) l++;
if (rn == 0) r--;
}
return ans;
}
void Solver() { //
scanf("%d", &n);
nums.resize(n);
for (int i = 0; i < n; i++) {
scanf("%lld%s", &v, str); // 字符串输入,避免空白问题
nums[i] = {v, str[0]};
}
printf("%lld\n", SolverAns());
}
三、按位与(and)
题意:小 R 有 n 个非负整数,要求选出恰好 m 个,使得选出的 m 个数的二进制按位与的值最大,输出这个最大值。
数据范围:1 ≤ m ≤ n ≤ 2e5;0 ≤ a_i < 2^30。
思路:按位贪心
子问题一:答案的每一位如何决策?
答案的每一位独立决策,且高位优先。
从最高位向最低位贪心:第 b 位能为 1,当且仅当「在满足所有更高位约束的前提下,至少存在 m 个数的第 b 位为 1」。
于是可以维护一个候选集合:
若候选集中第 b 位为 1 的数不少于 m 个,则答案第 b 位为 1,第 b 位为 0 的数不可能参与最优解,可以淘汰;
否则答案第 b 位只能为 0。
子问题二:如何快速统计第 b 位为 1 的候选数个数?
显然,需要按第 b 位进行排序,1 在前面,0 在后面。
排序后,统计第 b 位为 1 的数的个数即可。
复杂度:O(32 n log(n))
排序优化:为何要按第 b 位排序,而不能按原值排序?
因为高位某一位不足 m 个 1 时,这些位时无效的,不能参与决策。
既然如此,那就可以对无效的位进行清理,这样就可以按原值排序了。
优化1:只有清理无效位时打破数组的有序性,需要重新排序,其他情况无需排序。
优化2:值域只有 0 与 1,其实可以使用双指针来排序,复杂度降低到 O(32 n)
int n, m;
vector<ll> nums;
void Solver() { //
scanf("%d%d", &n, &m);
nums.resize(n);
for (int i = 0; i < n; i++) {
scanf("%lld", &nums[i]);
}
sort(nums.begin(), nums.end(), greater<ll>());
for (int b = 31; b >= 0; b--) {
if (nums[0] & (1LL << b)) {
// 第 b 位有 1,检查是否至少 m 个数的第 b 位也是 1
int i = 0;
while (i < n && (nums[i] & (1LL << b))) {
i++;
}
if (i >= m) {
// 够了,后面的数字第 b 位都是 0,选择不会更优,都删除
while (nums.size() > i) {
nums.pop_back();
}
} else {
// 不够,这一位怎么选择都是 0,清理这一位的 1
while (i > 0) {
i--;
nums[i] ^= (1LL << b);
}
sort(nums.begin(), nums.end(), greater<ll>());
}
}
if (nums.size() == m) {
break; // 恰好剩余 m 个,必须都选择
}
}
ll ans = nums[0];
for (int i = 1; i < m; i++) {
ans &= nums[i];
}
printf("%lld\n", ans);
}
四、纸牌游戏(card)
题意:n 张牌从上到下点数依次为 a_1, a_2, …, a_n,点数 1 到 m 每种至少出现一次。
共进行 n 局游戏,第 i 局跳过前 i-1 张牌,依次打出第 i 张到第 n 张。
打出一张点数为 k 的牌时:若桌面没有 k,放在最右端;否则收走桌面上点数为 k 的牌及其右侧所有牌,再把这张 k 放最右端。
问对于每个点数 i,在所有 n 局游戏中,有多少局结束时桌面上留有点数为 i 的牌。
数据范围:2 ≤ n ≤ 5e5,1 ≤ m ≤ 3e5;每种点数至少出现一次。
思路:动态规划
子问题一:先分析单局游戏的性质。
性质一:第 i 局第一张牌 x(位置 L)必然保留到结束。
因为开始时桌面为空,x 直接放上桌面;之后再打出 x 后会保留一个 x,这样 x 一直位于桌面最左侧,不会被收走。
性质二:x 的最后一次出现位置是 R,则打出位置 R 的 x 时,桌面会被清成只剩 x,即 [L, R] 之间出现的其他点数全部被消除。
性质三:位置 R+1 之后的操作与「从 R+1 重新开始一局」等价(x 已经固定保留,不影响后续的收走操作)。
所以一局游戏等价于:x=nums[L] 保留,跳到 x=R+1 重复,直到超过 n。
一局结束时桌面就是这些保留下来的点数。
子问题二:暴力做法与瓶颈。
对每个起点 i 模拟上述跳转:
void ForceSolver() { // 枚举起始位置,复杂度 O(n*m)
for (int i = 0; i < n; i++) {
int cur = i;
while (cur < n) {
const int v = nums[cur];
ans[v]++;
cur = lastPos[v] + 1;
}
}
}
每局保留的牌数不超过 m,总复杂度 O(n·m),只能得 68 分。
子问题三:如何去掉重复运算?
观察性质三:所有游标落在同一个值 v 上的局,下一跳都会跳到同一个位置 lastPos[v]+1,存在公共后缀,可以状态合并。
定义状态 dp[i]:有多少局游戏的当前游标落在位置 i。
初始时,第 i 局从位置 i 开始,dp[i] 至少为 1;。
如果当前位置的数字不是最后一个,则可以合并到最后一个位置,即 dp[lastPos[v]] += dp[i]。
如果当前位置的数字是最后一个,则下个位置可以重新开始消除,即 dp[lastPos[v]+1] += dp[i]。
如何计算答案呢?
所有的数字都会跳到值为这个数字的最后一个位置。
故答案为 ans[v] = dp[lastPos[v]]。
这样每个位置只处理一次,复杂度 O(n)。
int n, m;
vector<int> nums;
vector<int> lastPos; // 每种点数的最后一次出现位置
vector<int> ans;
vector<int> dp; // dp[i]:有多少局游戏的游标落在位置 i
void SkipSolver() {
// 利用动态规划,避免重复运算
for (int i = 0; i < n; i++) {
const int v = nums[i]; // 作为第一张,肯定会保留下来
const int last = lastPos[v];
dp[i]++;
if (last == i) { // v 的最后一次出现,更新答案
ans[v] = dp[i];
dp[last + 1] += dp[i];
} else { // v 还会再出现,状态合并到 last
dp[last] += dp[i];
}
}
}
void Solver() {
scanf("%d%d", &n, &m);
nums.resize(n);
lastPos.resize(m + 1, n);
dp.resize(n + 1, 0);
for (int i = 0; i < n; i++) {
scanf("%d", &nums[i]);
lastPos[nums[i]] = i;
}
ans.resize(m + 1, 0);
SkipSolver();
for (int i = 1; i <= m; i++) {
printf("%d%c", ans[i], i == m ? '\n' : ' ');
}
}
五、最后
以上就是 2026 年 CSP-J 第二轮算法比赛的全部题解。
总结一下:
第一题:难度为入门,模拟,解析座位后递进判断。
第二题:难度为普及-,双指针,在段上批量比较对称字符。
第三题:难度为普及,按位贪心,注意直接排序取前 m 个是错误的。
第四题:难度为普及+/提高−,动态规划,暴力枚举只能得 68 分。
第三题与第四题可能需要仔细推导一下,其他题目都比较常规。
《完》
-EOF-
本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号 ID:tiankonguse-code
本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。
