CSP-J 2026 初赛试题解析(第三部分:完善程序题(第二题))精讲


第二题:平衡分割------数字蛋糕切切乐

一、故事背景:怎样把数字蛋糕切得最公平?

同学们,第一题我们学会了数字进制转换。

现在,数字王国又送来了一块特殊的数字蛋糕!

蛋糕上排着一串数字:

复制代码
016A

这里的A不是普通的字母,而是十六进制中的一个数字,代表10。

所以这串数字实际上是:

复制代码
0、1、6、10

国王要求我们把这块蛋糕切成若干段。

每一段都必须是连续的,不能把中间的数字跳过去。

例如:

复制代码
0 | 1 | 6 | 10

可以切成4段。

也可以切成:

复制代码
0、1 | 6、10

切成2段。

每一段都要计算平均值。

国王希望:

所有分段的平均值中,最大值与最小值之间的差尽可能小。

我们要找出这个最小的差值。

这就是"平衡分割"问题。


二、先用一个简单例子理解题意

假设数字序列是:

复制代码
1、3、5、7

我们可以切成两段:

复制代码
1、3 | 5、7

第一段平均值:

(1+3)/2 = 2

第二段平均值:

(5+7)/2 = 6

最大平均值是6,最小平均值是2。

所以差值:

6−2=4

如果切成:

复制代码
1 | 3、5、7

第一段平均值是1。

第二段平均值是:

(3+5+7)/3 = 5

差值:

5−1=4

如果切成:

复制代码
1、3、5 | 7

第一段平均值是3。

第二段平均值是7。

差值也是:

7−3=4

程序需要尝试所有合法切分方案,找出差值最小的那一种。

这就引出了本题的核心思想:

枚举所有切分方式,比较每种方案的最大平均值与最小平均值之差。


三、先理解十六进制字符转换函数

原题有一个函数:

复制代码
int get_val(char c) {
    return ①;
}

它的任务是把字符转换成对应的数值。

例如:

复制代码
'0' → 0
'1' → 1
'9' → 9
'A' → 10
'B' → 11
......
'F' → 15

1. 数字字符怎样转换?

在ASCII编码中,数字字符是连续排列的。

因此:

复制代码
c - '0'

就可以得到数字字符对应的数值。

例如:

复制代码
'7' - '0' = 7

2. 字母字符怎样转换?

字母A代表10。

而:

复制代码
'A' - 'A' = 0

所以要加上10:

复制代码
c - 'A' + 10

例如:

复制代码
'B' - 'A' + 10 = 11

3. 合并成一个表达式

如果字符不大于'9',说明它是数字字符。

否则,它是A~F中的一个字母。

所以可以写成:

复制代码
c <= '9' ? c - '0' : c - 'A' + 10

第39题答案

A。


四、认识递归函数 split

原题中的核心函数是:

复制代码
void split(int l, int cnt, double minb, double maxb) {
    if (l > n) {
        if (cnt == 0) return;

        ans = min(ans, maxb - minb);
        return;
    }

    int sum = 0;

    for (②) {
        sum += ③;

        double nwb = ④;

        split(⑤);
    }
}

我们先认识它的四个参数。

参数 含义
l 当前准备处理的区间左端点
cnt 当前已经形成的分段数量减1
minb 已经完成的分段中,最小平均值
maxb 已经完成的分段中,最大平均值

注意:cnt 的初始值是-1。

每完成一段,cnt 就加1。

所以:

  • 没有完成任何一段时,cnt = -1

  • 完成一段后,cnt = 0

  • 完成两段后,cnt = 1

  • 完成三段后,cnt = 2

程序在全部处理完成后,使用:

复制代码
if (cnt == 0) return;

排除只分成一段的情况。

因为题目要求至少切一次,也就是至少分成两段。


五、第40题:②处应该填什么?

原题选项:

A. int r = l; r <= n; r++

B. int r = l; r < n; r++

C. int r = 1; r <= n; r++

D. int r = l; r++ < n


1. 这个循环的任务是什么?

复制代码
for (②) {
    sum += ③;

    double nwb = ④;

    split(⑤);
}

当前区间的起点是 l

我们要尝试不同的结束位置 r

例如:

复制代码
数字:1、3、5、7

如果当前从第1个数字开始,可以尝试:

复制代码
[1,1]:只选1
[1,2]:选1、3
[1,3]:选1、3、5
[1,4]:选1、3、5、7

所以:

复制代码
r = l

然后逐渐增加,直到:

复制代码
r = n

也就是最后一个数字。


答案

第40题:A。

复制代码
for (int r = l; r <= n; r++)

六、第41题:③处应该填什么?

原题选项:

A. get_val(s[r])

B. get_val(s[r-1])

C. s[r-1]

D. s[r]


1. 我们需要计算什么?

程序中:

复制代码
sum += ③;

sum 保存当前分段的数字总和。

例如:

复制代码
数字:1、3、5、7

当前分段是:

复制代码
3、5、7

那么:

复制代码
sum = 3 + 5 + 7 = 15

2. 为什么不能直接加字符?

如果:

复制代码
sum += s[r];

那么加进去的是字符的ASCII编码,而不是它代表的数值。

例如字符'7'的ASCII编码不是整数7。

因此,需要调用:

复制代码
get_val()

把字符转换成真正的数字。

又因为字符串从:

复制代码
s[1]

开始存储,所以当前第r个数字对应:

复制代码
s[r]

答案

第41题:A。

复制代码
sum += get_val(s[r]);

七、第42题:④处应该填什么?

原题选项:

A. sum/(r-l+1)

B. 1.0*sum/(r-l+1)

C. 1.0*sum/(r-l)

D. sum*1.0/n


1. 平均值怎么算?

当前分段的范围是:

复制代码
l、l+1、......、r

其中一共有:

r − l + 1

个数字。

所以平均值应该是:


2. 为什么需要乘以1.0?

这是C++中的一个重要陷阱!

假设:

复制代码
int sum = 5;
int count = 2;

如果写:

复制代码
sum / count

那么两个变量都是整数,进行的是整数除法。

结果:

复制代码
5 / 2 = 2

小数部分会被舍去。

但是平均值应该是:

复制代码
2.5

所以需要让计算变成浮点数运算:

复制代码
1.0 * sum / count

这样才能保留小数部分。


答案

第42题:B。

复制代码
double nwb = 1.0 * sum / (r - l + 1);

记忆口诀

求平均值,先算个数;
想保留小数,记得乘1.0!


八、第43题:⑤处应该填什么?

原题选项:

A. r+1, cnt+1, min(minb,nwb), max(maxb,nwb)

B. r, cnt+1, minb, maxb

C. r+1, cnt, nwb, nwb

D. r, cnt, min(minb,nwb), max(maxb,nwb)

这是本题最重要的一个空。


我们要弄清楚:

当前分段处理完后,递归函数应该从哪里继续?

1. 当前分段已经处理到哪里?

假设:

复制代码
数字:1、3、5、7

当前选择的区间是:

复制代码
[1,2]

也就是:

复制代码
1、3

这段已经处理完了。

下一段应该从第3个数字开始。

因此,下一次递归的起点应该是:

复制代码
r + 1

2. 分段数量如何变化?

当前刚刚完成了一段。

因此:

复制代码
cnt + 1

3. 最小平均值如何更新?

原来的最小平均值是:

复制代码
minb

新分段的平均值是:

复制代码
nwb

新的最小平均值应该是两者中较小的那个:

复制代码
min(minb, nwb)

4. 最大平均值如何更新?

同理:

复制代码
max(maxb, nwb)

5. 合并起来

因此,递归调用应该是:

复制代码
split(
    r + 1,
    cnt + 1,
    min(minb, nwb),
    max(maxb, nwb)
);

答案

第43题:A。


九、理解递归的结束条件

原题:

复制代码
if (l > n) {
    if (cnt == 0) return;

    ans = min(ans, maxb - minb);
    return;
}

我们来逐行理解。


1. l > n 表示什么?

假设:

复制代码
n = 4

数字有:

复制代码
1、3、5、7

当我们处理完第4个数字后:

复制代码
r = 4

下一次递归从:

复制代码
l = r + 1 = 5

开始。

此时:

复制代码
l > n

说明所有数字都已经分段完成。


2. 为什么 cnt == 0 时直接返回?

因为:

复制代码
cnt = 0

意味着只完成了一段。

题目要求:

复制代码
1 ≤ k < n

也就是必须至少切一次。

所以只分成一段是不合法的方案,必须排除。


3. 如何更新答案?

对于一种合法的切分方案:

复制代码
maxb - minb

就是所有分段平均值中的最大值减去最小值。

我们希望这个差值越小越好。

所以:

复制代码
ans = min(ans, maxb - minb);

意思是:

  • 如果当前方案比之前的答案更小,就更新答案。

  • 如果当前方案更大,就保留原来的答案。


十、第二题完整答案与代码

答案汇总

题号 空格 正确选项 填入内容
39 A c <= '9' ? c - '0' : c - 'A' + 10
40 A int r = l; r <= n; r++
41 A get_val(s[r])
42 B 1.0*sum/(r-l+1)
43 A r+1, cnt+1, min(minb,nwb), max(maxb,nwb)

这些填空对应原题第39~43题。

填空后的完整程序

复制代码
#include <algorithm>
#include <iomanip>
#include <iostream>

using namespace std;

constexpr int N = 25;
int n;
char s[N];
double ans = 1e100;

int get_val(char c) {
    return c <= '9' ? c - '0' : c - 'A' + 10;
}

void split(int l, int cnt, double minb, double maxb) {
    if (l > n) {
        if (cnt == 0) return;

        ans = min(ans, maxb - minb);
        return;
    }

    int sum = 0;

    for (int r = l; r <= n; r++) {
        sum += get_val(s[r]);

        double nwb = 1.0 * sum / (r - l + 1);

        split(
            r + 1,
            cnt + 1,
            min(minb, nwb),
            max(maxb, nwb)
        );
    }
}

int main() {
    cin >> n >> s + 1;

    split(1, -1, 1e100, -1e100);

    cout << fixed << setprecision(6) << ans;

    return 0;
}

三、第二题的递归过程:用一棵搜索树理解

为了让大家真正理解递归,我们用4个数字:

复制代码
1、3、5、7

来观察搜索过程。

从第1个数字开始,第一段可以选择:

复制代码
[1,1]
[1,2]
[1,3]
[1,4]

如果第一段选择:

复制代码
[1,1]

那么剩余数字是:

复制代码
3、5、7

下一段可以选择:

复制代码
[2,2]
[2,3]
[2,4]

如果第二段选择:

复制代码
[2,3]

那么当前方案就是:

复制代码
1 | 3、5 | 7

程序会继续处理最后一段,然后在所有数字都处理完毕时计算:

复制代码
最大平均值 - 最小平均值

可以把整个过程想象成一棵不断分叉的搜索树:

复制代码
                    开始
                     |
          第一段可以有4种选择
          /     /      \      \
        [1]   [1,3]  [1,3,5] [1,3,5,7]
         |
      剩余数字
         |
     继续分段
     /   |    \
   [3] [3,5] [3,5,7]
         |
       继续

每次选择一个区间,就相当于在搜索树上走出一条分支。

当所有数字都被处理完,就到达叶子节点。

程序会检查这个叶子节点对应的方案是否合法,然后计算它的差值。

最后,所有合法方案中最小的差值,就是答案。


四、两道完善程序题的考点总结

第一题:进制减半

这道题最核心的不是死记硬背代码,而是理解:

一个大数可以用数组保存,并且可以通过逐位处理完成进制转换。

需要记住:

复制代码
b[j] = b[j - 1] * m;

表示原来的数字整体乘以m并向高位移动。

复制代码
b[0] = x;

表示加入新读入的数位。

复制代码
b[j + 1] += b[j] / n;

表示把进位加到高位。

复制代码
b[j] %= n;

表示当前位留下余数。

复制代码
while (len > 1 && b[len - 1] == 0)

表示删除多余的前导零,但至少保留一位。


第二题:平衡分割

这道题的核心思想是:

枚举所有连续分段方案,计算每种方案的平均值,再寻找最大平均值与最小平均值之差的最小值。

需要记住:

复制代码
for (int r = l; r <= n; r++)

表示枚举当前分段的结束位置。

复制代码
sum += get_val(s[r]);

表示把当前数字加入分段总和。

复制代码
1.0 * sum / (r - l + 1)

表示计算当前分段平均值,并保留小数。

复制代码
min(minb, nwb)

表示更新最小平均值。

复制代码
max(maxb, nwb)

表示更新最大平均值。

复制代码
ans = min(ans, maxb - minb);

表示保留所有合法方案中的最小差值。


五、送给同学们的考试口诀

进制转换口诀

大数拆成数组存,

从低到高逐位算;

满n进位余数留,

去掉前导零再输出。


递归分割口诀

左端固定,右端枚举;

一段一段,递归深入;

记录最大与最小,

到达终点算差值;

所有方案比一比,

最小差值就是答案。


给同学们的寄语:

完善程序题真正考验的,不只是C++语法,而是能不能理解程序的整体思路。

遇到空格时,不要急着猜选项。先问自己:

  1. 这个变量保存的是什么?

  2. 这段循环正在完成什么任务?

  3. 当前操作之后,程序应该处于什么状态?

  4. 下一步需要哪些信息?

只要把这些问题想清楚,很多看起来复杂的代码填空题,就会变成一道道可以逐步破解的小问题。🌟


相关推荐
kukubuzai1 小时前
双指针系列二--末尾篇(3道题)
c++·算法·leetcode
汉克老师1 小时前
CSP-J 2026 初赛试题解析(第二部分:阅读程序题(第二题))精讲
c++·csp-j·小学生·学c++编程
神仙别闹1 小时前
基于 C++ 实现(控制台)关键字检索系统
c++
重生之小比特2 小时前
【初阶C++】stack和queue
开发语言·c++·vs·queue
qq_199886872 小时前
第8板块·第1节:主机与设备内存管理基础与统一内存
c++·人工智能·gpu算力·cuda
h_a_o777oah2 小时前
【图论】网络流:Dinic 算法模板实现原理及解题技巧
c++·算法·图论·acm·网络流·最小割·dinic算法
千谦阙听2 小时前
【 C++篇】:模板初阶——泛型编程、函数模板与类模板
开发语言·c++·学习·visual studio
我不会起名字3222 小时前
一天一道力扣Hot100(37):深度优先算法--括号生成
java·数据结构·c++·后端·python·算法·go
一只小小的芙厨2 小时前
CSP-J 第一轮总结
csp-j·csp·ccf