2026年梧州学院算法校队选拔赛题解
A、马里奥的金币统计
第一步:读题与数据范围分析
题目要求:给定两个整数 a 与 b,输出它们的和 a + b。
输入规模:。两个数的范围均在 32 位有符号整型可表示范围内,但相加后可能达到
,该值仍在 32 位有符号整型范围内(32 位 int 的范围约
)。
时间复杂度要求:由于仅需一次加法和一次输出,因此 即可满足要求。
空间复杂度要求:仅需常数个变量,因此 即可满足要求。
第二步:建模
将问题转化为严格的数学模型:
- 变量:
- 目标函数:S=a+b,其中 S 为输出值。
- 约束条件:无。
模型可归约为单次加法运算的标准形式,不涉及任何复杂结构。
第三步:模拟(朴素实现)
最直接的实现方法:读取两个整数,计算它们的和,然后输出。
时间复杂度 O(1),空间复杂度 O(1),完全满足题目要求。因此,朴素实现的复杂度已经达到最优,无需进一步优化。
#include <iostream>
using namespace std;
using ll = long long;
int main() {
ll a, b;
cin >> a >> b;
cout << a + b << endl;
return 0;
}
第四步:优化
由于朴素实现的时间复杂度 O(1) 和空间复杂度 O(1) 均已满足第一步推导的复杂度上限,且该问题不存在任何可优化的时间或空间瓶颈,因此跳过优化步骤。
第五步:最终c++代码
#include <iostream>
using namespace std;
using ll = long long;
int main() {
ll a, b;
cin >> a >> b;
// 计算两数之和
cout << a + b << endl;
return 0;
}
B、星露谷种子大采购
第一步:读题与数据范围分析
题目要求:给定 n 种种子的单价和持有的金币数 m,统计单价不超过 m 的种子种类数。
输入规模:,
。
时间复杂度要求:由于 最大仅为 1000,即使遍历所有元素也只需
时间,完全满足
解法(甚至
也无压力)的要求。
空间复杂度要求:需要存储 个整数,
空间即可,满足要求。
第二步:建模
将问题转化为数学模型:
- 变量:持有金币数
m,种子种类数n,种子单价数组a[1..n](或a[0..n-1])。 - 目标函数:
- 约束条件:
。
模型可归约为“遍历数组并计数满足条件的元素个数”的标准形式。
第三步:模拟(朴素实现)
最直接的实现方法:读取 n 和 m,然后遍历所有种子单价,若单价不大于 m 则计数器加一,最后输出计数器。
用户提供的代码正是这个朴素实现:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
int n, m;
cin >> n >> m;
vector<ll> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
ll ans = 0;
for (int i = 0; i < n; i++) {
if (a[i] <= m) {
ans++;
}
}
cout << ans << endl;
return 0;
}
该代码的核心步骤清晰:两次线性扫描(一次读取,一次计数),时间复杂度为 O(n),空间复杂度为 O(n)。
在当前数据范围(n ≤ 1000)下,该复杂度完全可行,且没有任何实际优化必要。对 n=1000 的规模,任何 O(n) 算法都能在 1ms 内完成。因此朴素实现已是最终方案,无需第四步优化。
第四步:优化
朴素实现已是最终方案,无需第四步优化。
第五步:最终 C++ 代码
#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
int main() {
// 读取种子种类数 n 和持有的金币数 m
int n, m;
cin >> n >> m;
// 存储每种种子单价
vector<ll> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
// 统计单价不超过 m 的种子种类数
ll ans = 0;
for (int i = 0; i < n; ++i) {
if (a[i] <= m) {
++ans;
}
}
// 输出结果
cout << ans << endl;
return 0;
}
C、Rating群内排行榜
第一步:读题与数据范围分析
题目要求:给定 名群友的昵称、报分顺序(输入顺序)和 Rating 值,按照 Rating 从高到低排序,Rating 相同的按报分顺序(即输入顺序)先后排序,输出排序后的名单。
输入规模:,Rating
,昵称长度
。
时间复杂度要求:由于 最大可达
,最终算法的时间复杂度必须不超过
,否则无法在限制时间内完成。
空间复杂度要求:需要存储 条记录,
空间是必需的且完全可行。
第二步:建模
将问题转化为排序模型:
- 变量:设
为报分顺序(从 0 开始计数),
为昵称字符串,
为整数分数。
- 目标:对集合
进行排序。
- 排序键:主键
降序,次键
升序(即报分顺序早的在前)。
- 约束:排序必须正确实现双键比较,且保证同 Rating 时维持原始顺序。
模型归约为经典的结构体排序问题。排序的严格定义是找到一个排列 ,使得对于任意
,有
或
。
第三步:模拟(朴素实现)
基于第二步的排序定义,最直接的朴素实现是选择一种简单但效率较低的排序算法。冒泡排序是基于比较的直观排序方法,此处采用冒泡排序进行模拟。
核心步骤:
-
定义结构体
Node,包含name、rating和id。 -
读入
N,循环读入每条记录并存入数组。 -
使用冒泡排序:重复遍历数组,每次比较相邻两个元素,若不满足排序规则则交换。排序规则为:若
a.rating < b.rating则交换(使高 Rating 上浮);若a.rating == b.rating且a.id > b.id则交换(使小 id 上浮)。 -
经过
N-1轮后数组有序,依次输出。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
struct Node {
string name;
int rating;
int id;
};
bool should_swap(const Node& a, const Node& b) {
if (a.rating != b.rating) {
// rating 高的应在前面,如果 a.rating < b.rating 则需要交换
return a.rating < b.rating;
}
// rating 相同,id 小的在前,如果 a.id > b.id 则需要交换
return a.id > b.id;
}
int main() {
int N;
cin >> N;
vector<Node> arr(N);
for (int i = 0; i < N; ++i) {
cin >> arr[i].name >> arr[i].rating;
arr[i].id = i;
}
// 冒泡排序
for (int i = 0; i < N - 1; ++i) {
for (int j = 0; j < N - 1 - i; ++j) {
if (should_swap(arr[j], arr[j+1])) {
swap(arr[j], arr[j+1]);
}
}
}
for (const auto& x : arr) {
cout << x.id + 1 << " " << x.name << " " << x.rating << endl;
}
return 0;
}
时间复杂度分析:冒泡排序有两重循环,每次比较和可能的交换为 ,因此总时间复杂度为
。空间复杂度:使用数组存储,
。
在当前数据范围 下,
的时间复杂度将导致运行次数约为
级别,远超常见时间限制(1~2 秒),因此朴素实现不可行,必须进行优化。
第四步:优化(性质驱动,逐步降复杂度)
优化点 1:使用更高效的比较排序算法(如快速排序、归并排序)
- 性质依据:由于题目仅要求按指定关键字排序,属于基于比较的排序问题,而基于比较的排序算法理论下界为
。冒泡排序的
复杂度是因为存在大量冗余比较。使用更优的排序算法可以直接将时间复杂度降至下界。
- 优化操作:将冒泡排序替换为 C++ 标准库中的
std::sort函数,该函数通常实现为快速排序的变体(内省排序),时间复杂度为。同时,提供自定义比较函数以实现双键排序。
- 复杂度验证:优化后时间复杂度降为
,对于
,运算量约为
,完全可接受。空间复杂度仍为
。已满足第一步推导的复杂度上限,优化终止。
因此,最终方案采用 std::sort 实现。
第五步:最终 C++ 代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct node{
string name;
int score;
int id;
bool operator<(const node &a)const{
if(score!=a.score){
return score>a.score;
}
return id<a.id;
}
};
int main() {
int n;
cin>>n;
vector <node> a;
for(int i=0;i<n;i++){
string name;
int score;
cin>>name>>score;
a.push_back({name,score,i});
}
sort(a.begin(),a.end());
for(auto [name,score,id]:a){
cout<<id+1<<" "<<name<<" "<<score<<endl;
}
return 0;
}
注意: std::sort的是不稳定排序,假设排序方式是
bool operator<(const node &a)const{
return score>a.score;
}
那么当时,他的顺序是不能确定的,假如第一个报分的人是A1,rating是16000,第二个报分的人是A2,rating是16000,
则A1在前面与A2在前面都有可能出现。
所以推荐std::stable_sort,或者在std::sort排序规则添加一个唯一的键,让他保证只按照我们规定的排序顺序排序,不会随机排序。
D、罗德岛干员搭配
第一步:读题与数据范围分析
题目要求:给定 个整数的数组
cost 和一个目标差值 target,统计有多少对不同的下标对 ,满足
cost[i] - cost[j] = target。
输入规模:,
cost[i] ,
。
时间复杂度要求:由于 最大可达
,最终算法的时间复杂度必须不超过
,否则无法在限制时间内完成。
空间复杂度要求:需要存储 个元素,
空间是完全可行的。
第二步:建模
给定数组 和目标差值
,要求统计不同且无序的下标对
个数,满足
cost[i] - cost[j] = target。
这里的下标对是有顺序的:先选干员 A(编号 ),再选干员 B(编号
),要求
cost[i] - cost[j] = target。因此, 和
是不同的组合,除非
(但题目要求不同的干员,所以
)。根据示例,输入的数组中可能包含重复的值,且不同下标视为不同的干员,因此需要基于下标计数。
变量:
:数组长度
:目标差值
:第
个干员的部署费用
目标:
此问题可归约为“两数之和”问题的变种:遍历数组,对于当前元素 cost[i],需要查找之前已遍历的元素中,有多少个满足与 cost[i] 的差为 target。
将等式变形:
- 即
(情况一:当前元素作为被减数)
- 或者
,即
(情况二:当前元素作为减数)
因此,在遍历过程中,对于当前元素 ,需要查询其
和
的值在之前出现的次数。
第三步:模拟(朴素实现)
最直接的解法是使用双重循环枚举所有 且
的组合,统计满足
的对数。
#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
int main() {
int n, target;
cin >> n >> target;
vector<ll> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
ll ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (i == j) continue; // 跳过同一个干员
if (a[i] - a[j] == target) {
++ans;
}
}
}
cout << ans << endl;
return 0;
}
时间复杂度分析:双重循环枚举所有对,时间复杂度为 。空间复杂度分析:仅使用数组存储,
。
由于 最大可达
,
的时间复杂度在最坏情况下需要进行
次运算,1秒内最多能进行
2 秒),因此朴素实现不可行,必须进行优化。次运算,远超常规时间限制(通常为 1
第四步:优化(性质驱动,逐步降复杂度)
优化点 1:哈希表优化(利用等式变形与在线查询)
- 性质依据:由于等式
cost[i] - cost[j] = target可变形为cost[j] = cost[i] - target,说明对于每一个,我们只需要知道之前已遍历的元素中,有多少个等于
cost[i] - target即可统计所有以当前元素作为被减数的有效对。同时,还需考虑当前元素作为减数的情况,即cost[j] = cost[i] + target。由于哈希表可以在平均时间内完成查询和插入,因此利用哈希表存储已遍历元素的频次,可以将单次查询和更新操作降至
。
- 优化操作:遍历数组时,维护一个
unordered_map(哈希表),键为元素值,值为该值出现的次数。对于当前元素a[i],在更新哈希表之前,先查询a[i] - target和a[i] + target在哈希表中的出现次数,并累加至答案。这样做的依据是:当遍历到时,哈希表中记录的是所有
的元素频次,对于这些
,
都在选定的前一个干员位置,因此
。而题目要求的是对不同的干员组合
,因此
和
的前后顺序在遍历时自然分开。同时,因为两数可以互换位置,因此在累积答案时需要同时考虑
+target和-target两种情况。查询完毕后,再将当前元素a[i]加入哈希表。这样,一次遍历即可完成统计。 - 复杂度验证:优化后时间复杂度为
,因为每个元素只进行常数次哈希表操作。空间复杂度为
,用于存储哈希表。已满足第一步推导的复杂度上限,优化终止。
第五步:最终 C++ 代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
//a[i]-a[j]=m
//a[i]=m+a[j]
//a[j]=a[i]-m
int main() {
int n,m;
cin>>n>>m;
vector <ll> a(n);
unordered_map <ll,ll> maps;
for(int i=0;i<n;i++){
cin>>a[i];
}
ll ans=0;
for(int i=0;i<n;i++){
ans+=maps[a[i]-m];
ans+=maps[a[i]+m];
maps[a[i]]++;
}
cout<<ans<<endl;
return 0;
}
E、MC骨粉机误触事件
第一步:读题与数据范围分析
题目要求:给定 格耕地,初始值为
,执行
次区间加法操作(每次对
加
),最后输出所有耕地中的最大值。
数据范围:,
。
最坏情况:若所有 次操作均覆盖全部
格,每格累加总量为
。该值超出32位int范围(约
),因此必须使用64位整数类型(long long,上限约
)存储中间结果。
时间复杂度要求: 和
均为
级别。朴素方法对每次操作遍历区间内所有元素进行加法,时间复杂度为
,远超时间限制。因此算法时间复杂度必须达到
级别。
第二步:建模
本题是典型的“多次区间修改、最后一次单点查询”问题。
数学模型:
- 原数组:
,初始全0,存储每格耕地的最终生长阶段值。
次操作:第
次操作为
,表示
区间内所有元素增加
。
- 目标:
。
差分数组模型:
差分数组的核心原理是通过记录相邻元素的差值,将区间修改转化为两个端点的单点修改,再通过前缀和还原原始数组。
- 构造差分数组
:
(定义
)。
- 区间更新操作:对原数组区间
加
,等价于:
(从
开始,后续元素在还原时均增加
)
(从
开始,后续元素在还原时不再增加
)
- 还原操作:通过前缀和从差分数组还原原数组,即
。
因此,,差分数组通过前缀和还原为原数组。
第三步:模拟(朴素实现)
最直接的朴素实现是直接按照题目描述,对每次操作的 [L, R] 区间内的每个元素逐一加上 K。
#include <iostream>
#include <vector>
using namespace std;
using ll = long long;
int main() {
int n, m;
cin >> n >> m;
vector<ll> a(n + 1, 0);
for (int i = 0; i < m; i++) {
int l, r;
ll k;
cin >> l >> r >> k;
for (int j = l; j <= r; j++) {
a[j] += k;
}
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
if (a[i] > ans) ans = a[i];
}
cout << ans << endl;
return 0;
}
时间复杂度:每次操作需要 时间遍历区间,
次操作总共
。在
和
均为
时,
的运算量远超限制,因此朴素实现不可行,必须进行优化。
空间复杂度:,用于存储数组。
第四步:优化(性质驱动,逐步降复杂度)
优化点 1:差分数组优化
- 性质依据:由于题目具有“多次区间修改后一次性查询”的特性,与差分数组的核心应用场景完全重合。差分数组能够将区间修改操作从
降低到
,因为每次修改只需要更新两个端点的差分值。同时,由于查询发生在所有修改完成之后,符合差分数组“离线处理”的要求。
- 优化操作:引入差分数组
diff,将区间更新转化为端点更新: 对于每个操作,只需执行:
diff[L] += Kdiff[R+1] -= K所有操作完成后,通过一次前缀和扫描还原原数组并同时记录最大值。
- 复杂度验证:
- 时间复杂度:
次操作每次
,前缀和还原
,总计
。
时运算量约
,完全满足要求。
- 空间复杂度:差分数组需
空间,满足要求。
- 优化已达到复杂度上限,无需进一步优化。
- 时间复杂度:
第五步:最终 C++ 代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
int n,m;
cin>>n>>m;
vector <ll> a(n+5);
for(int i=0;i<m;i++){
ll l,r,k;
cin>>l>>r>>k;
a[l]+=k;
a[r+1]-=k;
}
ll ans=0;
for(int i=1;i<=n;i++){
a[i]+=a[i-1];
ans=max(ans,a[i]);
}
cout<<ans<<endl;
return 0;
}
F、猫猫虫的质数蠕动
第一步:读题与数据范围分析
题目要求:猫猫虫每次蠕动前进的距离必须是一个质数(可重复使用同一质数)。有 次独立询问,每次给定总距离
(
),求最少蠕动次数使总距离恰好为
。
数据范围:,
。
时间复杂度上限:由于询问次数很大,回答单次询问的复杂度必须为 或均摊
,预处理时间可接受
或
。空间复杂度
可行。
第二步:建模
该问题等价于:将正整数 N 拆分为若干质数之和,求项数的最小值。形式化如下:
- 给定
N ∈ ℕ,N ≥ 2。 - 求最小整数
k,使得存在质数序列p₁, p₂, …, p_k满足∑ pᵢ = N。
该模型为标准的最少质数拆分问题。由于拆分与顺序无关,且同一质数可无限使用,问题本质是“用质数凑出面值 N 的无界背包问题,要求物品个数最少”。其数学结构可用动态规划或图论(BFS)描述。
第三步:模拟(朴素实现)
最直接的朴素实现是根据问题定义进行暴力搜索或动态规划。
1.DFS 暴力枚举:
尝试所有可能的质数组合,对每一种拆分方案进行深度优先搜索,记录使用质数个数最少的方案。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int INT_MAX = 0x3f3f3f3f;
const int MAXN = 100000;
vector<int> primes;
vector<bool> isp(MAXN + 1, true);
int min_cnt;
void init() {
isp[0] = isp[1] = false;
for (int i = 2; i <= MAXN; ++i) {
if (isp[i]) {
primes.push_back(i);
for (int j = i * 2; j <= MAXN; j += i)
isp[j] = false;
}
}
}
void dfs(int remain, int cnt) {
if (remain == 0) {
if (cnt < min_cnt) min_cnt = cnt;
return;
}
// 剪枝:若当前 cnt 已经不小于已知最小次数,停止搜索
if (cnt >= min_cnt) return;
// 尝试所有权值不超过 remain 的质数
for (int p : primes) {
if (p > remain) break;
dfs(remain - p, cnt + 1);
}
}
int main() {
init();
int Q;
cin >> Q;
while (Q--) {
int N;
cin >> N;
min_cnt = INT_MAX;
dfs(N, 0);
cout << min_cnt << endl;
}
return 0;
}
该方法的搜索空间极大,每个状态可选择的质数数量约为 N / log N,树的分支因子巨大,最坏复杂度为指数级,N = 10^5 时完全不可行。
2.动态规划(完全背包):
定义 dp[x] 为凑出距离 x 所需的最少质数个数,初始化 dp[0] = 0,其余为无穷大。对于每个质数 p,正向更新:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 100000;
const int INF = 1e9;
int main() {
// 质数筛
vector<bool> isp(MAXN + 1, true);
vector<int> primes;
isp[0] = isp[1] = false;
for (int i = 2; i <= MAXN; ++i) {
if (isp[i]) {
primes.push_back(i);
for (int j = i * 2; j <= MAXN; j += i)
isp[j] = false;
}
}
// 完全背包
vector<int> dp(MAXN + 1, INF);
dp[0] = 0;
for (int p : primes) {
for (int x = p; x <= MAXN; ++x) {
dp[x] = min(dp[x], dp[x - p] + 1);
}
}
int Q;
cin >> Q;
while (Q--) {
int N;
cin >> N;
cout << dp[N] << endl;
}
return 0;
}
- 预处理所有
范围内的质数需要
(埃筛)或
(线性筛),DP 状态数
,每个状态转移考虑所有质数,质数个数约为
,因此 DP 时间复杂度为
。当
时,运算量约为
,过于庞大,无法通过 1~2 秒时限。
因此,朴素 DFS 与 DP 实现均因复杂度过高而不可行,必须依赖更深层的数学性质进行优化。
第四步:优化(性质驱动,逐步降复杂度)
优化点 1:基于哥德巴赫猜想的数学归类
- 性质依据:由于问题域为“将整数拆分为质数之和求最少项数”,该问题在
范围内拥有严格的数学结论。根据哥德巴赫猜想(在该范围内已验证),任意大于 2 的偶数可写成两个质数之和;任意大于 5 的奇数可写成三个质数之和。因此最少项数
ans必然属于。这一性质与“质数拆分”问题的最优解结构性重合,可以用条件判断直接得出答案。
- 优化操作:
- 若
本身是质数,
ans = 1。 - 若
是大于 2 的偶数,
ans = 2(根据哥德巴赫猜想,必定存在两个质数之和)。 - 若
是奇数且
是质数,则
,
ans = 2。 - 否则,奇数合数必定拆为三个质数(
必合法,因为此时
不是质数,
可继续拆或本身为质数),
ans = 3。 为支持质数判定,提前使用欧拉线性筛预处理
内的质数表。
- 若
- 复杂度验证:预处理质数表
,单次询问仅需常数次判断,总时间复杂度
,完全满足第一步的复杂度上限。空间复杂度
为存储布尔质数表,达标。该优化将指数级 /
的搜索降为
判定,优化终止。
第五步:最终 C++ 代码
1.最快方法:
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 100000;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 欧拉线性筛预处理质数表
vector<bool> is_prime(MAXN + 1, true);
vector<int> primes;
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= MAXN; ++i) {
if (is_prime[i]) primes.push_back(i);
for (int p : primes) {
if (1LL * i * p > MAXN) break;
is_prime[i * p] = false;
if (i % p == 0) break;
}
}
int Q;
cin >> Q;
while (Q--) {
int N;
cin >> N;
// 情况1:N 本身为质数 → 只需 1 次 , 可以使用复杂度 $\sqrt{N}$ 的素数判断方法
if (is_prime[N]) {
cout << "1\n";
continue;
}
// 情况2:N 是偶数 → 哥德巴赫猜想保证可用 2 个质数
if (N % 2 == 0) {
cout << "2\n";
continue;
}
// 情况3:N 是奇数且 N-2 为质数 → 2 + (N-2) 共 2 次, 可以使用复杂度为 $\sqrt{N}$ 的素数判断方法
if (is_prime[N - 2]) {
cout << "2\n";
continue;
}
// 情况4:其余奇数合数 → 3 次
cout << "3\n";
}
return 0;
}
2.比较简单的能通过的最终代码:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
bool isprime(int n){ //判断是否是质数,复杂度 $\mathcal{O}(\sqrt{n})$
for(int i=2;i*i<=n;i++){
if(n%i==0){
return 0;
}
}
return 1;
}
int main() {
int t;
cin>>t;
while(t--){
int n;
cin>>n;
if(isprime(n)){
cout<<1<<endl;
continue;
}
if(n%2==0){
cout<<2<<endl;
continue;
}
if(isprime(n-2)){
cout<<2<<endl;
continue;
}
cout<<3<<endl;
}
return 0;
}
因此,利用哥德巴赫猜想的数学结论,就能证明只有选择1、2、3个质数这三种情况,并没有上面的暴力模拟的一大行代码这么吓人。

京公网安备 11010502036488号