







第二题:平衡分割------数字蛋糕切切乐
一、故事背景:怎样把数字蛋糕切得最公平?
同学们,第一题我们学会了数字进制转换。
现在,数字王国又送来了一块特殊的数字蛋糕!
蛋糕上排着一串数字:
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++语法,而是能不能理解程序的整体思路。
遇到空格时,不要急着猜选项。先问自己:
-
这个变量保存的是什么?
-
这段循环正在完成什么任务?
-
当前操作之后,程序应该处于什么状态?
-
下一步需要哪些信息?
只要把这些问题想清楚,很多看起来复杂的代码填空题,就会变成一道道可以逐步破解的小问题。🌟