2026 CSP-J真题附解析
- 2026-11-02 23:12:18













T1 旅游(travel)
核心思路
纯条件判断,四位答案依次判断:
是否同车厢:c1 == c2
是否同车厢同排:c1 == c2 && s1.substr(0,2) == s2.substr(0,2)
是否同车厢同排同侧:在2 的基础上,两个座位字母同属左侧 {A,B,C} 或同属右侧 {D,F}
是否相邻:在3 的基础上,两个座位在该侧序列中相邻。
座位位置映射可以这样:
A=0, B=1, C=2(左侧)
D=3, F=4(右侧)
同侧且abs(pos1-pos2)==1 即相邻。注意 C 和 D 虽然位置差 1,但中间是过道,不同侧,所以不能相邻。
部分分
四个问题相互独立,可以一位一位判断。先保证前两位、第三位写对,第四位再处理相邻逻辑。
满分做法
cppchar ch1 = s1[2], ch2 = s2[2];auto pos = [](char c)->int {if(c=='A') return 0;if(c=='B') return 1;if(c=='C') return 2;if(c=='D') return 3;return 4; // F};bool sameCar = (c1 == c2);bool sameRow = sameCar && (s1.substr(0,2) == s2.substr(0,2));bool sameSide = sameRow && ((pos(ch1)<=2) == (pos(ch2)<=2));bool adjacent = sameSide && abs(pos(ch1)-pos(ch2))==1;cout << sameCar << sameRow << sameSide << adjacent << '\n';
易错点
输入是四个字符串,正常用cin >> c1 >> s1 >> c2 >> s2。
不要忘记先判断同车厢、同排,再判断同侧和相邻。
D 和 F 是相邻的,映射成 3,4 刚好差 1。
T2 回文串(string)
核心思路
字符串被压缩成若干段(a_i, c_i),总长可能到 2e5 * 1e9,不能展开。
回文要求对称位置字符相同。我们从左右两端同时处理:
维护左段指针l、右段指针 r,以及当前左右段剩余长度 remL, remR。
每次取len = min(remL, remR),这 len 对对称位置中:
若c[l] == c[r],不用修改;
若c[l] != c[r],这 len 对全部需要改,答案加 len。
然后消耗长度,移动指针。
当l == r 时,中间剩余部分自己和自己对称,不需要处理。
部分分
小数据可以直接展开字符串,然后双指针统计s[i] != s[L-1-i] 的个数。
n=1 直接输出 0。
全部字符相同也输出0。
满分做法
cpplong long ans = 0;int l = 0, r = n-1;long long remL = a[0], remR = a[n-1];while(l < r) {long long len = min(remL, remR);if(c[l] != c[r]) ans += len;remL -= len; remR -= len;if(remL == 0) { l++; if(l<=r) remL = a[l]; }if(remR == 0) { r--; if(l<=r) remR = a[r]; }}cout << ans << '\n';
易错点
答案要用long long,可能达到 1e14 级别。
当l == r 时不要继续配对,中间段自己对称,修改数为 0。
消耗完一段后移动指针,注意不要越界。
T3 按位与(and)
核心思路
经典按位贪心。
从高位到低位(a_i < 2^30,所以从 29 到 0)尝试答案的当前位能否为 1。
维护已经确定的位mask。对于当前位 b:
cppcand = mask | (1 << b);cnt = 0;for each a[i]:if ((a[i] & cand) == cand) cnt++;if (cnt >= m) mask = cand;
含义:如果至少有m 个数包含 cand 的所有 1 位,那么我们可以从中选 m 个,它们的按位与在 cand 的位上全为 1,因此当前位可以保留。否则当前位只能为 0。
部分分
m == 1:答案就是所有数的最大值。
m == n:答案就是所有数的按位与。
小数据可以暴力枚举选m 个的组合,取按位与最大值。
满分做法
cppint mask = 0;for(int b = 29; b >= 0; b--) {int cand = mask | (1 << b);int cnt = 0;for(int i = 1; i <= n; i++) {if((a[i] & cand) == cand) cnt++;}if(cnt >= m) mask = cand;}cout << mask << '\n';
易错点
不要从30 开始,a_i < 2^30,最高位是 29。
判断条件是(a[i] & cand) == cand,不是 a[i] & cand 非零。
复杂度O(30n),n=2e5 完全够。
T4 纸牌游戏(card)
这是今年最难、最区分的一题。暴力模拟只能拿小数据,满分需要发现一个关键性质。
关键观察
对于第i 局,从 a[i] 开始处理。第一张牌 a[i] 永远不会被删掉,因为只有再次打出 a[i] 才会删除它,但删除后又会立刻把 a[i] 放回最右端,所以它始终存在,并且最终在桌面最左端。
设last[x] 为点数 x 在整个序列中最后出现的位置。
处理到a[i] 的最后一次出现后,桌面只剩 a[i],后面的过程与从 last[a[i]]+1 开始独立处理完全一样。因此:
第i 局最终桌面上的点数序列,就是从 i 开始不断跳转:

取a[i], a[f(i)], a[f(f(i))], ...,直到超过 n。
也就是说,最终桌面就是一条“跳转链”上的点数。
转化成树
对每个位置i,连边:

若f(i) > n,则视为指向 0。因为 f(i) > i,所以所有边都向右,形成森林。
对于位置i,定义 sz[i] 为:有多少个起始局 j 的跳转链会经过 i。
在反向树中,sz[i] 就是节点 i 的子树大小(包括自己)。
由于f(i) > i,可以按 i 从 1 到 n 累加:
cppsz[i] 初始为 1;for i = 1..n:if(f(i) <= n) sz[f(i)] += sz[i];
这样每个节点的sz 就统计好了。
最后对于点数v,它在最终桌面中出现的局数:

因为一个起始链不可能经过两个相同点数的位置(经过第一个后,会跳到该点数最后出现位置之后,直接跳过后面同色位置),所以同色位置直接相加不会重复。
满分代码框架
cppvector<int> last(m+1, 0);for(int i=1; i<=n; i++) last[a[i]] = i;vector<int> f(n+1);for(int i=1; i<=n; i++) {f[i] = last[a[i]] + 1;}vector<longlong> sz(n+1, 1);for(int i=1; i<=n; i++) {if(f[i] <= n) sz[f[i]] += sz[i];}vector<longlong> ans(m+1, 0);for(int i=1; i<=n; i++) {ans[a[i]] += sz[i];}for(int v=1; v<=m; v++) cout << ans[v] << " \n"[v==m];
部分分策略
暴力模拟:每局用一个vector 维护桌面,每次查找点数 k 是否在桌面:
不在就push_back;
在就删除该位置及其右侧,再push_back。
复杂度最坏O(n^2),可以拿小数据点(例如 n ≤ 2000/5000 的部分)。
特殊性质A:a 非降,可以尝试找规律,但性价比不如直接写满分算法。
特殊性质B:至多 4999 个相邻不同,可以压缩连续相同,但也不是必须。
易错点
f(i) 是 last[a[i]] + 1,不是 last[i]+1。
sz 和答案要用 long long,虽然答案不超过 n,但中间累加建议用 long long。
输出是m 个整数,对应点数 1..m。
这个题的关键是不要真的模拟每局,而是发现跳转链和反向树。
分数策略
目标二等(约130~150):T1、T2 必须稳,T3 写位贪心(其实很简单),T4 写暴力模拟拿小数据。这样 T1 100 + T2 100 + T3 100 + T4 小数据,已经比较稳。
目标一等(约210~230+):T1、T2、T3 尽量满分,T4 至少拿到特殊性质或小数据部分分;如果 T4 能写出跳转链+树形累加,一等非常稳。
时间分配:前1.5 小时解决 T1、T2、T3,留 1.5~2 小时给 T4。T4 即使想不到满分,也要先把暴力模拟写对,拿稳部分分。
总体看,2026 年对基础扎实的选手更友好:前三题门槛不高,但 T4 的思维量明显上升。稳住前三题,T4 能拿多少拿多少,是今年最合理的策略。