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
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse + ≡
穿越