CSP-J 2025 题解


第一题:拼数 (number)

思路分析

题目要求使用一个字符串中包含的数字字符拼成一个最大的正整数。

要组成最大的数,我们的策略应该是把越大的数字放在越高的位(即越靠前的位置)。例如,用数字 \(9, 2, 1, 0, 0\) 能拼成的最大数就是 \(92100\)

因此,解题思路非常直接:

  1. 从输入的字符串中提取出所有的数字字符。
  2. 将这些数字字符作为一个字符集合,进行降序排列('9' > '8' > ... > '0')。
  3. 将排序后的数字字符依次输出,拼接成最终的字符串,即为所求的最大正整数。

AC 代码

#include <bits/stdc++.h>

using namespace std;

// 用于文件输入输出重定向
void setup_io() {
    freopen("number.in", "r", stdin);
    freopen("number.out", "w", stdout);
}

int main() {
    // 提高C++ I/O效率
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    setup_io();

    string s;
    cin >> s;

    vector<char> digits;
    // 遍历输入字符串,提取所有数字字符
    for (char c : s) {
        if (isdigit(c)) {
            digits.push_back(c);
        }
    }

    // 将数字字符按从大到小的顺序排序
    sort(digits.begin(), digits.end(), greater<char>());

    // 依次输出排序后的数字
    for (char d : digits) {
        cout << d;
    }
    cout << endl;

    return 0;
}

第二题:座位 (seat)

思路分析

题目要求根据考生的成绩确定其在一个 \(n\)\(m\) 列考场中的“蛇形”座位。座位排布规则为:

  • 第1, 3, 5, ... (奇数)列,从上到下排(行号 1 -> n)。
  • 第2, 4, 6, ... (偶数)列,从下到上排(行号 n -> 1)。

解题步骤如下:

  1. 读入 \(n\), \(m\) 和全部 \(n*m\) 个考生的成绩。根据题目和样例,输入的第一个成绩 \(a1\) 就是我们要找的目标考生的成绩。
  2. 将所有成绩进行降序排序,以确定每个成绩的排名。
  3. 在排序后的列表中找到目标考生成绩的位置。这个位置就是他的排名(从0开始计数),我们称之为 \(rank\)
  4. 根据排名 \(rank\) 计算座位的行列号(均从0开始计数):
    • 考生所在的列号 \(col\_0 = rank / n\)
    • 考生在该列中的位置(从该列的起始方向数起)\(pos\_in\_col = rank % n\)
  5. 根据列号的奇偶性,结合 \(pos\_in\_col\) 计算最终的行号:
    • 如果 \(col\_0\) 是偶数(对应第1, 3...列),方向是从上到下,所以行号 \(row\_0 = pos\_in\_col\)
    • 如果 \(col\_0\) 是奇数(对应第2, 4...列),方向是从下到上,所以行号 \(row\_0 = n - 1 - pos\_in\_col\)
  6. 最后,将从0开始的行列号 \(col\_0\), \(row\_0\) 分别加1,得到题目要求的从1开始的行列号并输出。

AC 代码

#include <bits/stdc++.h>

using namespace std;

void setup_io() {
    freopen("seat.in", "r", stdin);
    freopen("seat.out", "w", stdout);
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    setup_io();

    int n, m;
    cin >> n >> m;

    int total_students = n * m;
    vector<int> scores(total_students);
    for (int i = 0; i < total_students; ++i) {
        cin >> scores[i];
    }
    
    // 根据题意,输入的第一个成绩 a1 是小 R 的成绩
    int target_score = scores[0];

    // 复制一份用于排序,以确定排名
    vector<int> sorted_scores = scores;
    sort(sorted_scores.begin(), sorted_scores.end(), greater<int>());

    // 找到目标分数的排名 (从0开始)
    int rank = -1;
    for (int i = 0; i < total_students; ++i) {
        if (sorted_scores[i] == target_score) {
            rank = i;
            break;
        }
    }

    // 计算0-indexed的列号和行号
    int col_0 = rank / n;
    int pos_in_col = rank % n;
    int row_0;

    if (col_0 % 2 == 0) { // 奇数列 (1, 3, ...),从上到下
        row_0 = pos_in_col;
    } else { // 偶数列 (2, 4, ...),从下到上
        row_0 = n - 1 - pos_in_col;
    }

    // 输出1-indexed的结果
    cout << col_0 + 1 << " " << row_0 + 1 << endl;

    return 0;
}

第三题:异或和 (xor)

思路分析

题目要求在一个序列中找出最多数量的、互不相交的、且区间内所有元素的异或和都等于 \(k\) 的区间。

这是一个经典的动态规划问题。为了高效地计算任意区间的异或和,我们可以使用 前缀异或和

  • \(prefix\_xor[i]\) 表示序列 \(a\) 从第1个元素到第 \(i\) 个元素的异或和。
  • 那么,区间 \([j, i]\) 的异或和就是 \(prefix\_xor[i] \oplus prefix\_xor[j-1]\)
  • 我们要求 \(prefix\_xor[i] \oplus prefix\_xor[j-1] = k\),这等价于 \(prefix\_xor[j-1] = prefix\_xor[i] \oplus k\)

现在定义 \(dp[i]\) 为:考虑序列的前 \(i\) 个元素,所能选出的最大满足条件的区间数量

在计算 \(dp[i]\) 时,有两种选择:

  1. 不选择以 \(i\) 结尾的任何区间:此时 \(dp[i] = dp[i-1]\)
  2. 选择一个以 \(i\) 结尾的区间 \([j, i]\):此时,我们能得到的区间总数是 \(1 + dp[j-1]\)

为避免低效地遍历所有可能的 \(j\),我们用一个 \(map\) (哈希表) \(max\_dp\_for\_pxor\) 来优化查找。\(max\_dp\_for\_pxor[val]\) 记录的是:当某个前缀异或和为 \(val\) 时,其对应的最大 \(dp\) 值。

最终算法:

  1. 初始化 \(dp\) 数组为0,\(dp[0]=0\)。初始化 \(map\),放入 \(max\_dp\_for\_pxor[0] = 0\) (对应空前缀)。
  2. 遍历 \(i\) 从 1 到 \(n\)
    a. \(dp[i]\) 首先继承 \(dp[i-1]\) 的值。
    b. 计算当前的前缀异或和 \(current_pxor\),并找到需要匹配的目标前缀和 \(target = current\_pxor \oplus k\)
    c. 如果 \(map\) 中存在 \(target\),说明可以构成一个以 \(i\) 结尾的有效区间。用 \(max\_dp\_for\_pxor[target] + 1\) 来尝试更新 \(dp[i]\)
    d. 用 \(dp[i]\) 的值更新 \(map\) 中关于 \(current\_pxor\) 的记录,以便后续的计算使用。
  3. 最终答案为 \(dp[n]\)

AC 代码

#include <bits/stdc++.h>

using namespace std;

void setup_io() {
    freopen("xor.in", "r", stdin);
    freopen("xor.out", "w", stdout);
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    setup_io();

    int n;
    int k;
    cin >> n >> k;

    vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
    }

    vector<int> dp(n + 1, 0);
    // map[pxor] = max_dp_value, 记录前缀异或和为pxor时,能达到的最大dp值
    map<int, int> max_dp_for_pxor;
    max_dp_for_pxor[0] = 0; // 空前缀异或和为0,dp值为0

    int current_prefix_xor = 0;
    for (int i = 1; i <= n; ++i) {
        current_prefix_xor ^= a[i - 1];
        
        // 策略1: 不选择以 a[i-1] 结尾的区间
        dp[i] = dp[i - 1];

        // 策略2: 寻找一个以 a[i-1] 结尾的合法区间
        int target_pxor = current_prefix_xor ^ k;
        if (max_dp_for_pxor.count(target_pxor)) {
            dp[i] = max(dp[i], max_dp_for_pxor[target_pxor] + 1);
        }

        // 更新 map,为后续的计算提供信息
        if (max_dp_for_pxor.count(current_prefix_xor)) {
            max_dp_for_pxor[current_prefix_xor] = max(max_dp_for_pxor[current_prefix_xor], dp[i]);
        } else {
            max_dp_for_pxor[current_prefix_xor] = dp[i];
        }
    }

    cout << dp[n] << endl;

    return 0;
}

第四题:多边形 (polygon)

思路分析

题目要求计算能拼成多边形的木棍子集的数量。条件是子集大小 \(m >= 3\) 且所有木棍长度和 \(S\) 大于最长木棍 \(L\) 的两倍 (\(S > 2L\))。这个条件等价于 最长边的长度必须小于其余各边长度之和

这是一个组合计数问题,可以使用动态规划解决,其思想类似于0-1背包。

算法步骤:

  1. 将所有木棍按长度从小到大排序。这是关键,它使得我们在遍历时,可以方便地将当前木棍 \(a[i]\) 视为最长边,而只从它之前的木棍 \(a[0]...a[i-1]\) 中进行选择。
  2. 我们用一个 \(dp\) 数组,\(dp[s]\) 表示从当前已处理过的木棍中,选出子集使其长度和为 \(s\) 的方案数。
  3. 整体流程如下:
    a. 对木棍数组 \(a\) 排序。
    b. 初始化 \(dp\) 数组,\(dp[0] = 1\)(代表和为0的方案只有一种,即空集)。
    c. 遍历 \(i\) 从 0 到 \(n-1\),将 \(a[i]\) 视为当前考虑的“最长边”:
    i. 在处理 \(a[i]\) 之前,\(dp\) 数组中存储的是从 \({a[0], ..., a[i-1]}\) 中选择子集的方案。
    ii. 我们需要计算从这个前缀集合中选出子集,其和大于 \(a[i]\) 的方案数。这个方案数等于 前缀集合的总子集数 减去 和小于等于 \(a[i]\) 的子集方案数
    iii. \({a[0], ..., a[i-1]}\) 构成的子集总共有 \(2^i\) 种。
    iv. 和小于等于 \(a[i]\) 的方案数就是 \(sum(dp[s])\) for \(s\) from 0 to \(a[i]\)
    v. 将 \((2^i - sum(dp[s]))\) 加入总答案 \(ans\)。这一步计算出的方案已自动满足“子集大小至少为2”的隐藏条件,因为和为0(空集)与和为\(a[j]\)(单个元素)都不可能大于\(a[i]\)
    vi. 将 \(a[i]\) “放入背包”,更新 \(dp\) 数组,为下一次迭代做准备。更新 \(dp\) 的方式是:\(for (s=...; s >= a[i]; --s) { dp[s] += dp[s - a[i]]; }\)
    d. 所有计算都在模 \(998,244,353\) 下进行。

AC 代码

#include <bits/stdc++.h>

using namespace std;

const int MOD = 998244353;

void setup_io() {
    freopen("polygon.in", "r", stdin);
    freopen("polygon.out", "w", stdout);
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    setup_io();

    int n;
    cin >> n;

    vector<int> a(n);
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
    }

    sort(a.begin(), a.end());

    long long total_ans = 0;
    // dp[s]: 和为s的方案数。a[i]最大5000,我们只需统计和不大于5000的方案,所以数组大小开到5001。
    vector<long long> dp(5001, 0); 
    dp[0] = 1;

    // 预计算2的幂,避免重复计算
    vector<long long> pow2(n + 1);
    pow2[0] = 1;
    for(int i = 1; i <= n; ++i) {
        pow2[i] = (pow2[i-1] * 2) % MOD;
    }

    long long current_prefix_sum = 0;
    for (int i = 0; i < n; ++i) {
        // Step 1: 以 a[i] 为最长边,从 a[0]...a[i-1] 中选边
        // 计算 sum(dp[s]) for s from 0 to a[i]
        long long subsets_le_ai = 0;
        for (int s = 0; s <= a[i]; ++s) {
            subsets_le_ai = (subsets_le_ai + dp[s]) % MOD;
        }
        
        // 总子集数是 2^i
        long long total_subsets_prefix = pow2[i];
        // 和 > a[i] 的子集数
        long long good_subsets_count = (total_subsets_prefix - subsets_le_ai + MOD) % MOD;
        
        total_ans = (total_ans + good_subsets_count) % MOD;

        // Step 2: 更新 dp 数组,将 a[i] 加入选择池
        current_prefix_sum += a[i];
        for (int s = min((long long)5000, current_prefix_sum); s >= a[i]; --s) {
            dp[s] = (dp[s] + dp[s - a[i]]) % MOD;
        }
    }

    cout << total_ans << endl;

    return 0;
}
posted @ 2025-11-01 14:59  surprise_ying  阅读(271)  评论(0)    收藏  举报