2025 年 6 月温州市赛小学组详细题解
声明:本题解为国科英才科创学院李春杰教练原创,未经允许,严禁搬运用于商业性质活动
做题网址:国科信息学评测系统点击跳转
A. 必须想象时间紧迫
题意简述
给定三个候选方案的时间成本 a , b , c a,b,c a,b,c,求三者中的最小值。
解题思路
直接比较三个数即可。答案为:
min ( a , b , c ) \min(a,b,c) min(a,b,c)
C++ 中可以写成:
cpp
min(a, min(b, c))
算法步骤
- 读入 a , b , c a,b,c a,b,c。
- 计算三者最小值。
- 输出答案。
正确性说明
题目要求在三个方案中选择时间成本最小的方案。程序直接对三个数取最小值,与题目定义完全一致,因此正确。
复杂度分析
- 时间复杂度: O ( 1 ) O(1) O(1)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
参考代码
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int a, b, c;
cin >> a >> b >> c;
cout << min(a, min(b, c)) << '\n';
return 0;
}
B. 最简缩写术语
题意简述
给定三个单词 a , b , c a,b,c a,b,c,判断它们的首字母是否按顺序组成 MST。
也就是说,需要判断:
a 1 = M , b 1 = S , c 1 = T a_1=\texttt{M},\qquad b_1=\texttt{S},\qquad c_1=\texttt{T} a1=M,b1=S,c1=T
如果满足,输出 YES,否则输出 NO。
解题思路
每组数据中虽然给出了三个单词长度 n , m , p n,m,p n,m,p,但判断首字母时不需要使用长度,只要读取字符串后检查下标 0 0 0 的字符。
算法步骤
- 读入测试组数 t t t。
- 对每组数据:
- 读入 n , m , p n,m,p n,m,p;
- 读入三个字符串 a , b , c a,b,c a,b,c;
- 判断
a[0]=='M' && b[0]=='S' && c[0]=='T'; - 成立输出
YES,否则输出NO。
正确性说明
题目要求三个单词的首字母依次为 M、S、T。程序逐个检查三个字符串的首字符,全部满足时输出 YES,否则输出 NO,与题目条件完全一致,所以正确。
复杂度分析
每组数据只检查三个字符。
- 时间复杂度: O ( t ) O(t) O(t)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
参考代码
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while(t--){
int n, m, p;
string a, b, c;
cin >> n >> m >> p;
cin >> a >> b >> c;
if(a[0] == 'M' && b[0] == 'S' && c[0] == 'T'){
cout << "YES\n";
}else{
cout << "NO\n";
}
}
return 0;
}
C. 演唱会
题意简述
有:
- a a a 首 1 1 1 分钟歌曲;
- b b b 首 2 2 2 分钟歌曲;
- c c c 首 3 3 3 分钟歌曲。
需要把所有歌曲分到两场演唱会中,使两场演唱会总时长差的绝对值尽可能小。求这个最小差值。
解题思路
总时长为:
S = a + 2 b + 3 c S=a+2b+3c S=a+2b+3c
把歌曲分成两组,本质上是选出一部分歌曲,使其中一场的总时长为 x x x,另一场为 S − x S-x S−x。
两场的差为:
∣ x − ( S − x ) ∣ = ∣ 2 x − S ∣ |x-(S-x)|=|2x-S| ∣x−(S−x)∣=∣2x−S∣
显然最理想的情况是让 x x x 尽量接近 S 2 \frac{S}{2} 2S。
如果可以取到:
- x = S 2 x=\frac{S}{2} x=2S,则答案为 0 0 0;
- x = ⌊ S 2 ⌋ x=\lfloor \frac{S}{2}\rfloor x=⌊2S⌋ 或 x = ⌈ S 2 ⌉ x=\lceil \frac{S}{2}\rceil x=⌈2S⌉,则答案为 1 1 1。
所以答案只可能与总时长的奇偶性有关。
为什么一定可以凑出接近一半的时长
题目保证:
a , b , c ≥ 1 a,b,c\ge 1 a,b,c≥1
也就是至少有一首 1 1 1 分钟歌、一首 2 2 2 分钟歌、一首 3 3 3 分钟歌。
我们可以使用一个常见结论:
如果当前已经可以凑出所有 0 0 0 到 R R R 的整数时长,下一首歌时长为 w w w,并且:
w ≤ R + 1 w\le R+1 w≤R+1
那么加入这首歌后,可以凑出所有 0 0 0 到 R + w R+w R+w 的整数时长。
原因是:
- 不用这首歌,可以凑出 0 ∼ R 0\sim R 0∼R;
- 用这首歌,可以凑出 w ∼ R + w w\sim R+w w∼R+w;
- 当 w ≤ R + 1 w\le R+1 w≤R+1 时,两个区间没有空隙。
现在从一首 1 1 1 分钟歌开始,可以凑出:
0 ∼ 1 0\sim 1 0∼1
加入 2 2 2 分钟歌后,因为:
2 ≤ 1 + 1 2\le 1+1 2≤1+1
可以凑出:
0 ∼ 3 0\sim 3 0∼3
之后所有歌曲长度都不超过 3 3 3,而当前 R ≥ 3 R\ge 3 R≥3,所以每加入一首歌都不会产生空隙。最终可以凑出从 0 0 0 到 S S S 的所有整数时长。
因此一定能凑出最接近 S 2 \frac{S}{2} 2S 的整数时长。
于是:
答案 = S m o d 2 \text{答案}=S\bmod 2 答案=Smod2
即:
- S S S 为偶数,答案为 0 0 0;
- S S S 为奇数,答案为 1 1 1。
算法步骤
对每组测试数据:
- 读入 a , b , c a,b,c a,b,c。
- 计算总时长:
S = a + 2 b + 3 c S=a+2b+3c S=a+2b+3c
- 输出 S m o d 2 S\bmod 2 Smod2。
复杂度分析
每组数据只做常数次运算。
- 时间复杂度: O ( t ) O(t) O(t)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
由于 a , b , c ≤ 10 9 a,b,c\le 10^9 a,b,c≤109,总和可能达到 6 × 10 9 6\times 10^9 6×109,需要使用 long long。
参考代码
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while(t--){
ll a, b, c;
cin >> a >> b >> c;
ll S = a + 2 * b + 3 * c;
cout << (S & 1) << '\n';
}
return 0;
}
D. 前进之旅
题意简述
有 n n n 个落脚点,位置为 a 1 , a 2 , ... , a n a_1,a_2,\dots,a_n a1,a2,...,an,起点在 0 0 0。每次可以:
- 普通跳跃,最多跳 x x x 米,不消耗技能;
- 使用一次技能,最多弹射 y y y 米,技能次数加 1 1 1。
要求到达最远的落脚点。如果无法到达,输出 -1;否则输出最少使用技能次数。
解题思路
先把所有落脚点按位置从小到大排序。设排序后为:
p o s 1 < p o s 2 < ⋯ < p o s n pos_1<pos_2<\cdots<pos_n pos1<pos2<⋯<posn
同时令:
p o s 0 = 0 pos_0=0 pos0=0
定义动态规划:
d p i = 到达 p o s i 所需的最少技能次数 dp_i=\text{到达 }pos_i\text{ 所需的最少技能次数} dpi=到达 posi 所需的最少技能次数
其中:
d p 0 = 0 dp_0=0 dp0=0
要到达 p o s i pos_i posi,可以从某个已经能到达的 p o s j pos_j posj 转移过来,其中 j < i j<i j<i。
状态转移
如果使用普通跳跃,则需要:
p o s i − p o s j ≤ x pos_i-pos_j\le x posi−posj≤x
此时技能次数不变:
d p i = min ( d p i , d p j ) dp_i=\min(dp_i,dp_j) dpi=min(dpi,dpj)
如果使用技能弹射,则需要:
p o s i − p o s j ≤ y pos_i-pos_j\le y posi−posj≤y
此时技能次数加 1 1 1:
d p i = min ( d p i , d p j + 1 ) dp_i=\min(dp_i,dp_j+1) dpi=min(dpi,dpj+1)
所以完整转移为:
d p i = min ( min p o s i − p o s j ≤ x d p j , min p o s i − p o s j ≤ y ( d p j + 1 ) ) dp_i= \min\left( \min_{pos_i-pos_j\le x} dp_j, \min_{pos_i-pos_j\le y} (dp_j+1) \right) dpi=min(posi−posj≤xmindpj,posi−posj≤ymin(dpj+1))
直接枚举所有 j j j 是 O ( n 2 ) O(n^2) O(n2),无法通过 n ≤ 10 5 n\le 10^5 n≤105。
用单调队列优化
对于每个 i i i,我们需要快速得到:
min p o s i − p o s j ≤ x d p j \min_{pos_i-pos_j\le x} dp_j posi−posj≤xmindpj
和:
min p o s i − p o s j ≤ y d p j \min_{pos_i-pos_j\le y} dp_j posi−posj≤ymindpj
第二个式子最终再加 1 1 1 即可。
由于位置已经排序,随着 i i i 增大,合法的 j j j 区间也是向右移动的滑动窗口。因此可以用两个单调队列:
qx:维护满足普通跳跃距离 x x x 的候选点;qy:维护满足技能弹射距离 y y y 的候选点。
队列中按照 dp 从小到大维护。这样队首就是当前窗口里 dp 最小的位置。
单调队列维护方法
处理第 i i i 个点时:
- 删除
qx队首中不满足:
p o s i − p o s j ≤ x pos_i-pos_j\le x posi−posj≤x
的点。
- 删除
qy队首中不满足:
p o s i − p o s j ≤ y pos_i-pos_j\le y posi−posj≤y
的点。
- 若
qx非空,可以用dp[qx.front()]更新。 - 若
qy非空,可以用dp[qy.front()]+1更新。 - 如果 d p i dp_i dpi 有效,就把 i i i 加入两个队列。加入前弹出队尾中
dp不优于 d p i dp_i dpi 的点,从而保持队列单调。
正确性说明
动态规划转移枚举了到达 p o s i pos_i posi 的最后一步:要么普通跳跃,要么技能弹射。所有可能的前驱点都在对应距离限制的窗口中。单调队列始终保存当前窗口中 dp 最小的候选点,因此能得到与枚举所有前驱相同的最优转移值。由于按位置从小到大处理,所有前驱 j < i j<i j<i 的 dp 已经计算完成,所以整个 DP 正确。
复杂度分析
排序需要:
O ( n log n ) O(n\log n) O(nlogn)
每个点最多进入和弹出每个队列一次,因此 DP 为:
O ( n ) O(n) O(n)
总复杂度:
O ( n log n ) O(n\log n) O(nlogn)
空间复杂度: O ( n ) O(n) O(n)。
参考代码
cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn = 100000 + 5;
const int INF = 1e9;
int pos[maxn], dp[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while(T--){
int n, x, y;
cin >> n >> x >> y;
for(int i = 1; i <= n; i++) cin >> pos[i];
sort(pos + 1, pos + n + 1);
pos[0] = 0;
deque<int> qx, qy;
qx.push_back(0);
qy.push_back(0);
dp[0] = 0;
for(int i = 1; i <= n; i++){
while(!qx.empty() && pos[i] - pos[qx.front()] > x) qx.pop_front();
while(!qy.empty() && pos[i] - pos[qy.front()] > y) qy.pop_front();
dp[i] = INF;
if(!qx.empty()) dp[i] = min(dp[i], dp[qx.front()]);
if(!qy.empty()) dp[i] = min(dp[i], dp[qy.front()] + 1);
if(dp[i] < INF){
while(!qx.empty() && dp[qx.back()] >= dp[i]) qx.pop_back();
qx.push_back(i);
while(!qy.empty() && dp[qy.back()] >= dp[i]) qy.pop_back();
qy.push_back(i);
}
}
if(dp[n] >= INF) cout << -1 << '\n';
else cout << dp[n] << '\n';
}
return 0;
}
E. 子序列问题
题意简述
给定序列 A A A,要求选出一个最长子序列 C C C,使它是"好"的。
将 C C C 从小到大排序后得到:
D = ( d 1 , d 2 , ... , d k ) D=(d_1,d_2,\dots,d_k) D=(d1,d2,...,dk)
其中位数为:
d ⌈ k / 2 ⌉ d_{\lceil k/2\rceil} d⌈k/2⌉
要求:
d 1 + d k 2 = d ⌈ k / 2 ⌉ \frac{d_1+d_k}{2}=d_{\lceil k/2\rceil} 2d1+dk=d⌈k/2⌉
也就是:
最小值和最大值的平均数 = 中位数 \text{最小值和最大值的平均数} = \text{中位数} 最小值和最大值的平均数=中位数
求最长长度。
关键观察一:原序列顺序不影响答案
子序列虽然要求保留相对顺序,但本题的"好"只与选出元素的多重集合有关,与这些元素在子序列中的先后顺序无关。
对任意一种选择,只要确定每个数值选了多少个,就一定可以在原序列中选出对应次数的出现位置,形成一个子序列。
因此我们只需要关心每个数的出现次数,可以先排序并做频次统计。
关键观察二:固定最小值、最大值、中位数
设选出的好子序列的:
- 最小值为 x x x;
- 最大值为 z z z;
- 中位数为 y y y。
根据题意:
x + z 2 = y \frac{x+z}{2}=y 2x+z=y
等价于:
x + z = 2 y x+z=2y x+z=2y
所以我们可以枚举中位数 y y y,再寻找满足:
x + z = 2 y x+z=2y x+z=2y
的一对最小值和最大值。
压缩数组
将所有数排序后压缩成不同值数组:
v a l 0 < v a l 1 < ⋯ < v a l U − 1 val_0<val_1<\cdots<val_{U-1} val0<val1<⋯<valU−1
其中 cnt[i] 表示 v a l i val_i vali 出现了多少次。
再做频次前缀和:
p r e i + 1 = p r e i + c n t i pre_{i+1}=pre_i+cnt_i prei+1=prei+cnti
这样可以 O ( 1 ) O(1) O(1) 求出某一段不同值区间内的元素总数。
固定中位数位置 k k k
设:
y = v a l k y=val_k y=valk
用双指针找所有满足:
v a l i + v a l j = 2 v a l k val_i+val_j=2val_k vali+valj=2valk
的 ( i , j ) (i,j) (i,j),其中:
i < k < j i<k<j i<k<j
若找到了这样一组 ( i , k , j ) (i,k,j) (i,k,j),那么:
- 最小值可以取 v a l i val_i vali;
- 最大值可以取 v a l j val_j valj;
- 中位数必须为 v a l k val_k valk。
此时可以选择的元素只来自区间 [ i , j ] [i,j] [i,j]。
定义:
L m a x = ∑ t = i k − 1 c n t t L_{max}=\sum_{t=i}^{k-1} cnt_t Lmax=t=i∑k−1cntt
表示小于中位数的可选元素最多有多少个。
E = c n t k E=cnt_k E=cntk
表示等于中位数的元素个数。
G m a x = ∑ t = k + 1 j c n t t G_{max}=\sum_{t=k+1}^{j} cnt_t Gmax=t=k+1∑jcntt
表示大于中位数的可选元素最多有多少个。
如果最终选择:
- L L L 个小于 y y y 的数;
- E E E 个等于 y y y 的数;
- G G G 个大于 y y y 的数;
那么总长度为:
N = L + E + G N=L+E+G N=L+E+G
因为多选中位数不会让中位数变坏,所以等于 y y y 的元素一定全部选上。
中位数条件转化
排序后,前面有 L L L 个数小于 y y y,中间有 E E E 个数等于 y y y,后面有 G G G 个数大于 y y y。
中位数位置为:
r = ⌈ N 2 ⌉ r=\left\lceil \frac{N}{2}\right\rceil r=⌈2N⌉
要让中位数等于 y y y,必须满足:
L < r ≤ L + E L<r\le L+E L<r≤L+E
将它转化成更好用的不等式:
第一部分:
L < ⌈ L + E + G 2 ⌉ L<\left\lceil\frac{L+E+G}{2}\right\rceil L<⌈2L+E+G⌉
等价于:
L < E + G L<E+G L<E+G
也就是:
L ≤ E + G − 1 L\le E+G-1 L≤E+G−1
第二部分:
⌈ L + E + G 2 ⌉ ≤ L + E \left\lceil\frac{L+E+G}{2}\right\rceil\le L+E ⌈2L+E+G⌉≤L+E
等价于:
G ≤ L + E G\le L+E G≤L+E
所以中位数为 y y y 的条件是:
L ≤ E + G − 1 \boxed{L\le E+G-1} L≤E+G−1
且:
G ≤ L + E \boxed{G\le L+E} G≤L+E
同时要有:
1 ≤ L ≤ L m a x , 1 ≤ G ≤ G m a x 1\le L\le L_{max},\qquad 1\le G\le G_{max} 1≤L≤Lmax,1≤G≤Gmax
因为必须至少选出一个最小值 v a l i val_i vali 和一个最大值 v a l j val_j valj。
如何在固定 ( i , k , j ) (i,k,j) (i,k,j) 后求最优长度
我们要最大化:
L + E + G L+E+G L+E+G
其中:
L ≤ L m a x , G ≤ G m a x L\le L_{max},\qquad G\le G_{max} L≤Lmax,G≤Gmax
并满足:
G ≤ L + E G\le L+E G≤L+E
以及:
L ≤ E + G − 1 L\le E+G-1 L≤E+G−1
可以分两种情况讨论。
情况一:让 G G G 取到中位数限制上界
如果令:
G = L + E G=L+E G=L+E
此时总长度为:
L + E + G = 2 ( L + E ) L+E+G=2(L+E) L+E+G=2(L+E)
要求:
L + E ≤ G m a x L+E\le G_{max} L+E≤Gmax
为了让长度最大,应取:
L = min ( L m a x , G m a x − E ) L=\min(L_{max},G_{max}-E) L=min(Lmax,Gmax−E)
只要 L ≥ 1 L\ge 1 L≥1,就可以更新答案。
情况二:让 G G G 取到数量上界
如果令:
G = G m a x G=G_{max} G=Gmax
则需要满足:
G m a x ≤ L + E G_{max}\le L+E Gmax≤L+E
也就是:
L ≥ G m a x − E L\ge G_{max}-E L≥Gmax−E
同时还要满足:
L ≤ E + G m a x − 1 L\le E+G_{max}-1 L≤E+Gmax−1
再加上:
1 ≤ L ≤ L m a x 1\le L\le L_{max} 1≤L≤Lmax
因此 L L L 的范围为:
max ( 1 , G m a x − E ) ≤ L ≤ min ( L m a x , E + G m a x − 1 ) \max(1,G_{max}-E)\le L\le \min(L_{max},E+G_{max}-1) max(1,Gmax−E)≤L≤min(Lmax,E+Gmax−1)
若范围非空,为了让长度最大,取最大的 L L L 即可。
特殊情况:全相等
如果选出的子序列全都相等,那么最小值、最大值、中位数都相同,一定是好子序列。
所以答案至少为任意一个数的出现次数:
a n s ≥ max i c n t i ans\ge \max_i cnt_i ans≥imaxcnti
代码中先用这个值初始化答案。
算法步骤
对每组数据:
- 读入 n n n 和数组 A A A。
- 将数组排序。
- 压缩成
val和cnt。 - 建立频次前缀和
pre。 - 用最大频次初始化答案,处理全相等情况。
- 枚举中位数下标 k k k:
- 目标和为:
t a r g e t = 2 v a l k target=2val_k target=2valk
- 用双指针
i=0,j=U-1寻找满足val[i]+val[j]==target且i<k<j的配对; - 对每一组配对计算 L m a x , E , G m a x L_{max},E,G_{max} Lmax,E,Gmax;
- 按两种情况更新答案。
- 输出答案。
复杂度分析
设不同数的个数为 U U U,显然:
U ≤ n U\le n U≤n
排序复杂度为:
O ( n log n ) O(n\log n) O(nlogn)
对每个 k k k,双指针最多移动 O ( U ) O(U) O(U) 次,因此总复杂度为:
O ( U 2 ) O(U^2) O(U2)
由于题目中 n ≤ 3000 n\le 3000 n≤3000,可以通过。
空间复杂度: O ( n ) O(n) O(n)。
参考代码
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while(T--){
int n;
cin >> n;
vector<ll> a(n);
for(int i = 0; i < n; i++) cin >> a[i];
sort(a.begin(), a.end());
vector<ll> val;
vector<int> cnt;
for(int i = 0; i < n; i++){
if(val.empty() || val.back() != a[i]){
val.push_back(a[i]);
cnt.push_back(1);
}else{
cnt.back()++;
}
}
int U = (int)val.size();
vector<int> pre(U + 1, 0);
for(int i = 0; i < U; i++){
pre[i + 1] = pre[i] + cnt[i];
}
int ans = 1;
for(int i = 0; i < U; i++){
ans = max(ans, cnt[i]);
}
for(int k = 0; k < U; k++){
ll target = 2LL * val[k];
int i = 0, j = U - 1;
while(i < k && j > k){
ll s = val[i] + val[j];
if(s < target){
i++;
}else if(s > target){
j--;
}else{
int Lmax = pre[k] - pre[i];
int E = cnt[k];
int Gmax = pre[j + 1] - pre[k + 1];
if(Lmax > 0 && Gmax > 0){
int t = Gmax - E;
if(t >= 1){
int L = min(Lmax, t);
if(L >= 1){
ans = max(ans, 2 * (L + E));
}
}
int lo = max(1, Gmax - E);
int hi = min(Lmax, E + Gmax - 1);
if(lo <= hi){
int L = hi;
ans = max(ans, L + E + Gmax);
}
}
i++;
j--;
}
}
}
cout << ans << '\n';
}
return 0;
}