- 学术
CSP2025普及组题解
- @ 2025-11-2 3:03:42
T1 拼数
题意概括
给定一个长度为 () 的字符串 ,它由小写字母和数字组成,且至少包含一个 的数字。要求从 中选取任意多个数字字符(每个字符最多使用一次),按任意顺序拼接成一个正整数。求能拼成的最大正整数。
题目分析
这是一个经典的贪心问题。我们的目标是构造一个值最大的正整数。一个整数的值由两个核心因素决定:它的位数(长度)和高位上的数字大小。
-
位数最大化: 为了使数值尽可能大,我们应该使用 中所有可用的数字字符。如果舍弃任何一个数字,构成的数位数必然 使用所有数字构成的数,因此其值通常更小(除非舍弃的是 ,但保留 总是优于或等于不保留 )。
-
高位优先: 在位数相同(即使用所有 个可用数字)的前提下,要使数值最大,必须将越大的数字放在越靠前(越高)的数位上。
设 为从 中提取出的所有数字字符构成的多重集(multiset)。我们的目标是用 中的所有元素 (其中 ) 构造一个 位数 ,使其值最大。
根据贪心策略,我们应该将 中的元素按非递增(降序)排列,得到序列 ,满足 。
构造的数 (这里表示字符串拼接) 即为最大值。
合法性(正整数): 题目保证 至少包含一个 的数字。因此,提取出的数字集合 必然包含至少一个非 字符。当我们将 降序排序后,最大的元素 必然 (即 )。因此,拼接得到的数 绝不会有前导 (除非 ,但本题保证 不全为 ),且 必然是正整数。
算法流程:
- 遍历输入字符串 。
- 将所有数字字符(
'0'到'9')提取到一个新的字符串 中。 - 将字符串 按降序排序。
- 输出排序后的字符串 。
复杂度分析: 设 , 为 中数字的个数 ()。
- 时间复杂度: 遍历 提取数字 + 排序数字。由于 ,总时间复杂度为 。
- 空间复杂度: 存储提取的数字。最坏情况 ,空间复杂度为 。
代码
#include<bits/stdc++.h>
using namespace std;using ll=long long;
void sol() {
string s;
cin >> s;
string d;
// 遍历s,提取所有数字字符
for (char c : s) {
if (c >= '0' && c <= '9') { // 判断是否为数字
d.push_back(c);
}
}
// 贪心策略:要使数字最大,必须将大的数字放在高位
// 故对所有提取的数字进行降序排序
sort(d.rbegin(), d.rend());
// 输出拼接成的最大整数
cout << d << '\n';
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
sol();
return 0;
}
T2 座位
题意概括
给定 个互不相同的分数 。将它们按降序排名 。座位按列蛇形分配:第 1 列从 到 (行 ),第 2 列从 到 (行 ),第 3 列从 到 (行 ),以此类推。求分数 所在的座位 (列, 行)。 数据范围:, 。
题目分析
核心任务是确定 (小 R 的分数)在所有 个分数中的降序排名 。我们可以读入 ,然后读入其余 个分数,统计其中大于 的数量 ,则 的 -based 排名 。得到 后,我们分析座位分配规律。
座位分配以 个人(一整列)为一个周期。我们将排名 转换为 -based 索引 。-based 列索引 可以通过 计算得到。在 列中的 -based 行偏移 (从该列的起始方向算起) 为 。蛇形分配的规律取决于列索引 的奇偶性:1. 如果 为偶数(),分配方向为自顶向下(行 )。此时 -based 行索引 。2. 如果 为奇数(),分配方向为自底向上(行 )。
此时 -based 行索引 。最后,将 -based 的 转换为 -based 的 ,即 ,。由于 ,总人数 。计算排名 的时间复杂度为 ,计算 的时间复杂度为 。总时间复杂度 ,空间复杂度 。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
int my;
cin >> my; // 小 R 的分数
int k = 1; // 1-based 排名
int tot = n * m;
for (int i = 1; i < tot; ++i) {
int x;
cin >> x;
if (x > my) {
k++; // 统计有多少人分数比小 R 高
}
}
k--; // 转换为 0-based 排名 (k')
int c = k / n; // 0-based 列 (c')
int r = k % n; // 0-based 行偏移
if (c % 2 == 1) {
// 如果是奇数列 (c' = 1, 3, ...),方向自底向上
r = n - 1 - r;
}
// 输出 1-based 结果
cout << c + 1 << " " << r + 1 << '\n';
return 0;
}
T3 异或和
题意概括
给定一个长度为 的非负整数序列 和一个非负整数 。你需要选择序列中尽可能多的不相交的区间 ,使得每个区间的异或和 均等于 。求这个最大数量。 数据范围:, , 。
题目分析
本题要求最大数量的不相交区间,是典型的动态规划 (DP) 问题。 我们定义 为序列 的前缀异或和,并令 。根据异或的性质,区间 的异或和为 。 我们要找的区间 满足 ,这等价于 。
定义 表示考虑序列的前 个元素 ,能选出的满足条件的不相交区间的最大数量。 计算 时,我们有两种决策:
- 不选择以 结尾的区间。这种情况下,最优解与 的最优解相同,即 。
- 选择一个以 结尾的区间 ,且其异或和为 。为了使区间总数最大,我们应在 中也取最优解。设 ,则我们需要找到 且 ,使得 最大。
综合两种情况,DP 转移方程为:
$$dp[i] = \max(dp[i-1], \max_{j < i, \text{s.t. } s_j = s_i \oplus k} \{dp[j] + 1\}) $$直接计算 会导致 的复杂度,需要优化。 我们发现,在 递增的过程中,对于 这个值,我们只需要找到一个 使得 且 最大。 我们可以使用一个辅助数组(或哈希表) ,其中 存储 ,其中 满足 且 在当前 之前。 在遍历 从 到 的过程中,我们维护 :
- 令 ( 初始为 )。
- 计算 (对应决策 1)。
- 计算目标前缀和 。
- 在 中查找 。如果 存在(例如,我们用 初始化 ,则检查 ),说明存在 使得 。我们用决策 2 更新 :。
- 更新 :。将 存入 ,供后续的 使用。
由于 和 均小于 ,所有的 和 也都小于 。因此 可以是一个大小为 的数组。 时间复杂度:,其中 (用于 的初始化和访问)。 空间复杂度:,用于 数组和 数组。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int n, k;
int a[500005]; // 存储序列a
int dp[500005]; // dp[i] 表示前i个元素的最大区间数
int mp[1 << 20]; // 映射:mp[x] = max(dp[j]) s.t. s[j] = x
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
// 初始化 mp 数组,-1 表示该前缀和尚未出现
memset(mp, -1, sizeof(mp));
// s[0] = 0, dp[0] = 0
mp[0] = 0;
int s = 0; // 当前的前缀异或和 s[i]
for (int i = 1; i <= n; i++) {
s ^= a[i]; // 计算 s[i]
// 决策1:不选以 i 结尾的区间
dp[i] = dp[i - 1];
// 决策2:选择以 i 结尾的区间 [l, i]
int v = s ^ k; // 寻找目标 s[l-1],即 s[j]
if (mp[v] != -1) {
// 如果存在这样的 j = l-1
dp[i] = max(dp[i], mp[v] + 1);
}
// 更新 mp[s[i]] 为 dp[i] 的最大值
// 供 i 之后的 j' (j' > i) 使用
mp[s] = max(mp[s], dp[i]);
}
cout << dp[n] << '\n';
return 0;
}
T4 多边形
题意概括
给定 根木棍,长度分别为 。需要选出一个下标子集 ,设 且 。该子集 能构成多边形的条件是: 且 。 求有多少个这样的下标子集 满足条件,答案对 998,244,353 取模。
数据范围:,。
题目分析
我们考虑使用总方案数减去不合法的方案数。总方案数为 。不合法的方案分为两类:
-
选出的木棍数 。这类方案数
$$\binom{n}{0} + \binom{n}{1} + \binom{n}{2} = 1 + n + \frac{n(n-1)}{2} $$ -
选出的木棍数 ,但不满足 ,即 。 设 是选出的木棍集合,。令 (如果最大值有多个,任取其一)。不合法条件 等价于 ,即 。
为了不重不漏地统计第二类不合法方案,我们首先将 根木棍按长度 从小到大排序。 我们迭代 从 到 ,并假设 是当前不合法集合 中 下标最大 的那个最大值元素。 我们需要在 之前(即下标 )的元素 中选取一个子集 ,使得 ,并且 ,即 。
这可以看作一个01背包计数问题。我们维护一个 数组, 表示从 中选取子集,使其元素和恰好为 的方案数。 当我们从 迭代到 时, 数组需要被更新,以包含 作为可选元素。 设 。我们初始化 数组(大小 ),,其余为 。 我们按 从 到 循环:
-
此时 存储的是使用 构成和为 的方案数。
-
我们计算 。 表示从 中选取和 的子集 的总方案数。
-
包含了 (空集,1种) 和 (共 种) 的情况。
-
因此,满足 且 的方案数为 。我们将此计入不合法方案总数 。
-
将 加入背包,更新 数组,为下一次迭代 做准备:
$$dp[k] = (dp[k] + dp[k - a_i]) \pmod{MOD} \quad (\text{for } k \text{ from } A \text{ down to } a_i) $$总不合法方案数 即为 的方案数加上所有 迭代中累加的 。 最终答案为 。
时间复杂度:排序 ,DP 过程 次迭代,每次迭代 查询和 更新,总复杂度 。 空间复杂度:。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll mod = 998244353;
// 快速幂 p(a, b) = a^b % mod
ll p(ll a, ll b) {
ll res = 1;
a %= mod;
while (b > 0) {
if (b & 1) res = (res * a) % mod;
a = (a * a) % mod;
b >>= 1;
}
return res;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
sort(a.begin(), a.end());
// 总方案数 2^n
ll ans = p(2, n);
// 不合法方案数 m < 3
ll inv = (1 + n + (ll)n * (n - 1) / 2 % mod) % mod;
int m = a[n - 1]; // 最大ai
vector<ll> dp(m + 1, 0);
dp[0] = 1;
for (int i = 0; i < n; ++i) {
int mx = a[i];
// 计算 S' 子集和 <= mx 的方案数
ll tot = 0;
for (int k = 0; k <= mx; ++k) {
tot = (tot + dp[k]) % mod;
}
// c1 是 |S'|=1 的方案数
ll c1 = i;
// sub 是 |S'| >= 2 的方案数
ll sub = (tot - 1 - c1 + mod + mod) % mod;
// 计入不合法方案 (m >= 3 的情况)
inv = (inv + sub) % mod;
// 将 a[i] 加入背包
for (int k = m; k >= a[i]; --k) {
dp[k] = (dp[k] + dp[k - a[i]]) % mod;
}
}
// (总方案 - 不合法方案) % mod
cout << (ans - inv + mod) % mod << '\n';
return 0;
}
京公网安备11010802045784号
YIZHIYANG 一只羊 LV 8