温州市第三届青少年程序设计竞赛(小学组)题解

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))

算法步骤

  1. 读入 a , b , c a,b,c a,b,c。
  2. 计算三者最小值。
  3. 输出答案。

正确性说明

题目要求在三个方案中选择时间成本最小的方案。程序直接对三个数取最小值,与题目定义完全一致,因此正确。


复杂度分析

  • 时间复杂度: 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 的字符。


算法步骤

  1. 读入测试组数 t t t。
  2. 对每组数据:
    • 读入 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

正确性说明

题目要求三个单词的首字母依次为 MST。程序逐个检查三个字符串的首字符,全部满足时输出 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。

算法步骤

对每组测试数据:

  1. 读入 a , b , c a,b,c a,b,c。
  2. 计算总时长:

S = a + 2 b + 3 c S=a+2b+3c S=a+2b+3c

  1. 输出 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 个点时:

  1. 删除 qx 队首中不满足:

p o s i − p o s j ≤ x pos_i-pos_j\le x posi−posj≤x

的点。

  1. 删除 qy 队首中不满足:

p o s i − p o s j ≤ y pos_i-pos_j\le y posi−posj≤y

的点。

  1. qx 非空,可以用 dp[qx.front()] 更新。
  2. qy 非空,可以用 dp[qy.front()]+1 更新。
  3. 如果 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

代码中先用这个值初始化答案。


算法步骤

对每组数据:

  1. 读入 n n n 和数组 A A A。
  2. 将数组排序。
  3. 压缩成 valcnt
  4. 建立频次前缀和 pre
  5. 用最大频次初始化答案,处理全相等情况。
  6. 枚举中位数下标 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]==targeti<k<j 的配对;
  • 对每一组配对计算 L m a x , E , G m a x L_{max},E,G_{max} Lmax,E,Gmax;
  • 按两种情况更新答案。
  1. 输出答案。

复杂度分析

设不同数的个数为 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;
}
相关推荐
Noushiki1 小时前
常见的排序算法
算法·排序算法
gumichef1 小时前
二叉树链式结构的实现
算法·链表·二叉树·队列
战南诚1 小时前
力扣 之 198.打家劫舍
python·算法·leetcode
AllData公司负责人1 小时前
亲测丝滑,体验跃迁|AllData通过集成开源项目StreamPark,实时流任务调度更省心!
java·大数据·数据库·人工智能·算法·实时计算·实时开发平台
计算机安禾2 小时前
【c++面向对象编程】第46篇:CRTP(奇异递归模板模式):静态多态的妙用
开发语言·c++·算法
广州灵眸科技有限公司2 小时前
瑞芯微(EASY EAI)RV1126B 音频电路
开发语言·人工智能·深度学习·算法·yolo·音视频
小的~~2 小时前
算法题:只出现一次的数字
数据结构·算法
灵智实验室2 小时前
PX4状态估计技术EKF2详解(六):EKF2 磁力计融合——从航向修正到 3D 姿态约束
算法·无人机·px 4
JieE2122 小时前
手把手带你用虚拟头节点实现单链表,搞定所有边界问题
javascript·算法