大厂真题 / 荣耀
荣耀 2026-8-25 笔试真题 - 数据分析岗
本场考试概述
考试时间:2026 年 8 月 25 日
考试岗位:数据分析岗
难度评级:中等
考点分析:
- 第 1 题:循环字母变换、字典序与字符串贪心(中等)
- 第 2 题:多表连接、分组聚合与
COUNT(DISTINCT)(中等)
建议策略:
- 第 1 题依次确定区间左端点、统一变换次数和右端点;字典序问题中,第一个不同位置具有最高优先级。
- 第 2 题先明确统计口径:分子是流水总条数,分母是该档位下至少有一条流水的去重学员数。
- 学员卡表中的
answer_cnt是汇总字段,不应代替按题目档位统计的作答流水。
第 1 题:报文后继抬升
题目描述
给定一条长度为 $m$ 的报文字符串 $w$,其中只含小写英文字母。
可以选择一个非空连续子串,并对该子串内的每个字符同时执行相同次数的后继变换,变换次数可以为 $0$。一次后继变换的规则为:
\[a\to b,\ b\to c,\ \ldots,\ z\to a.\]请输出经过一次上述操作后,字典序最大的字符串。
输入描述
第一行输入一个整数 $m$,表示字符串长度,满足 $1\le m\le 2\times10^5$。
第二行输入一个长度为 $m$ 的字符串 $w$,仅由小写英文字母组成。
输出描述
输出经过操作后能够得到的字典序最大字符串。
样例
输入
5
cccab
输出
zzzxy
样例解释
从第一个字符开始选择整个字符串,并统一执行 $23$ 次后继变换:c 变为 z,a 变为 x,b 变为 y,最终得到 zzzxy。
思路分析
将 a 到 z 映射为 $0$ 到 $25$。整个决策可以按字典序优先级分成三步。
1. 确定左端点
字符串开头连续的 z 已经是最大字符。若对其中任意一个 z 执行非零次变换,它会回绕成更小的字符,结果必然变差。
因此,应跳过前导 z,把区间左端点放在第一个非 z 字符的位置 $p$。如果整个字符串都是 z,原串已经最大,选择任意非空子串并执行 $0$ 次变换即可。
2. 确定变换次数
设位置 $p$ 的字符数值为 $x$。区间左端点固定后,结果首先由位置 $p$ 决定,因此必须把它提升到 z。唯一需要考虑的模 $26$ 位移为
任何其他位移都会使位置 $p$ 小于 z,后面的字符再大也无法弥补。
3. 确定右端点
考虑位置 $p$ 右侧某个字符,其数值为 $y$:
- 若 $y\le x$,则 $y+shift\le25$,不会回绕,变换后的字符严格变大;
- 若 $y>x$,则变换会越过
z并回绕,结果严格小于原字符。
区间必须连续,所以从 $p$ 开始向右扩展:连续遇到不大于 $x$ 的字符时都应纳入;遇到第一个大于 $x$ 的字符时必须停止。若继续扩展,该位置会成为第一个变小的位置,右侧收益无法补偿字典序损失。
正确性证明
引理 1:若字符串不全为 z,最优操作的左端点是第一个非 z 字符的位置 $p$。
证明:若区间从 $p$ 左侧开始并执行非零位移,某个前导 z 会变小;若区间从 $p$ 右侧开始,或在 $p$ 前结束,则位置 $p$ 保持为小于 z 的原字符。相比之下,从 $p$ 开始并把它变为 z,既保留全部前导 z,又使位置 $p$ 最大,因此严格更优。引理得证。
引理 2:左端点固定为 $p$ 后,最优位移为 $25-x$。
证明:位置 $p$ 是操作后可能产生差异的最早位置。位移 $25-x$ 将它变为最大字符 z,其他模 $26$ 位移都会使它小于 z,故不可能更优。引理得证。
引理 3:在位移固定后,最优区间恰好扩展到第一个数值大于 $x$ 的字符之前。
证明:对于数值不大于 $x$ 的字符,纳入区间会使该字符严格变大,且不影响更早位置,所以必须纳入。对于第一个数值大于 $x$ 的字符,纳入后会发生回绕并严格变小;它将成为两种方案的第一个不同位置,因此必须在它之前停止。引理得证。
由三个引理,算法依次作出的左端点、位移和右端点选择均为最优,因此最终字符串是所有合法操作结果中字典序最大的字符串。
题解代码
import sys
def solve() -> None:
data = sys.stdin.buffer.read().split()
if not data:
return
length = int(data[0])
message = data[1].decode()
start = 0
while start < length and message[start] == "z":
start += 1
if start == length:
print(message)
return
base = ord(message[start]) - ord("a")
shift = 25 - base
answer = list(message)
end = start
while end < length and ord(message[end]) - ord("a") <= base:
value = ord(message[end]) - ord("a")
answer[end] = chr(ord("a") + value + shift)
end += 1
print("".join(answer))
if __name__ == "__main__":
solve()
复杂度分析
时间复杂度:$O(m)$,寻找左端点和扩展区间均只进行线性扫描。
空间复杂度:$O(m)$,用于保存可修改的结果字符数组。
易错点
- 变换次数对 $26$ 取模后统一作用于整个子串,不能为每个字符选择不同次数。
- 前导
z不能参与非零变换,否则最早位置会变小。 - 右端点判据是字符数值不大于起点字符,而不是“不等于
z”。 - 全部字符都是
z时应直接输出原串。
第 2 题:夜校分档人均作答次数
题目描述
某夜校教务系统包含三张表:
- 学员卡表
c21_hnr_trainee_card:关键字段包括pad_id(学员唯一标识)和campus(校区),另有answer_cnt等汇总字段; - 作答流水表
c21_hnr_drill_flow:字段包括id、pad_id、item_id、result; - 题目档位表
c21_hnr_drill_item:字段包括id、item_id、grade,其中档位为hard、medium或easy。
请只统计校区为“海风夜校”的学员,按题目档位计算人均作答条数:
\[\text{人均作答条数}=\frac{\text{该校学员在该档位的流水总条数}}{\text{在该档位至少有一条流水的该校学员人数}}.\]结果保留 $4$ 位小数。该校完全没有作答流水的档位不输出,最终按 grade 升序排列,返回字段依次为 campus、grade、avg_answer_cnt。
输入描述
输入为三张表中的数据。以下是样例数据:
INSERT INTO c21_hnr_trainee_card VALUES
(1, 7001, 'male', 22, '江左书院', 3.1, 6, 2, 8),
(2, 7002, 'female', 24, '青禾学堂', 3.7, 11, 4, 16),
(3, 8801, 'male', 26, '海风夜校', 3.5, 18, 12, 40),
(4, 7004, 'female', 21, '江左书院', 3.0, 4, 1, 3),
(5, 8802, 'male', 29, '海风夜校', 3.2, 14, 6, 20),
(6, 7006, 'female', 23, '星河夜校', 3.9, 9, 5, 22);
INSERT INTO c21_hnr_drill_flow VALUES
(1, 7001, 501, 'wrong'), (2, 7002, 502, 'wrong'), (3, 7002, 501, 'wrong'),
(4, 7001, 504, 'right'), (5, 7004, 506, 'right'), (6, 7004, 505, 'right'),
(7, 7004, 503, 'wrong'), (8, 8801, 503, 'wrong'), (9, 8801, 502, 'wrong'),
(10, 8802, 501, 'right'), (11, 8801, 501, 'wrong'), (12, 7004, 506, 'right'),
(13, 7004, 505, 'right'), (14, 7004, 503, 'wrong'), (15, 8801, 503, 'wrong'),
(16, 8801, 502, 'wrong'), (17, 8802, 501, 'right'), (18, 8801, 501, 'wrong'),
(19, 7004, 503, 'wrong'), (20, 8801, 503, 'wrong'), (21, 8801, 502, 'wrong'),
(22, 8802, 501, 'right'), (23, 8801, 501, 'wrong');
INSERT INTO c21_hnr_drill_item VALUES
(1, 501, 'easy'), (2, 502, 'medium'), (3, 503, 'easy'),
(4, 504, 'hard'), (5, 505, 'medium'), (6, 506, 'easy');
输出描述
输出 campus、grade、avg_answer_cnt 三列。
样例
输入
使用题目给出的三张表及样例数据
输出
campus grade avg_answer_cnt
海风夜校 easy 4.5000
海风夜校 medium 3.0000
思路分析
1. 先限定校区
从学员卡表出发,通过 WHERE t.campus = '海风夜校' 只保留目标校区的学员。样例中对应的 pad_id 为 8801 和 8802。
2. 连接流水与题目档位
作答流水通过 pad_id 关联学员,通过 item_id 关联题目档位。这里使用 INNER JOIN:只有真实存在流水的档位才会进入分组,因此海风夜校没有作答记录的 hard 档位不会出现在结果中。
3. 同时计算分子和分母
按校区和档位分组后:
COUNT(*)统计流水总条数,同一学员的多次作答都要计入;COUNT(DISTINCT f.pad_id)统计该档位下至少作答过一次的去重学员数。
样例中:
easy档位共有 $9$ 条流水,涉及 $2$ 名学员,人均为 $9/2=4.5000$;medium档位共有 $3$ 条流水,只有学员8801作答,人均为 $3/1=3.0000$。
最后将商转换为 DECIMAL(20, 4),既完成四舍五入,也稳定保留四位小数。
正确性说明
三表内连接得到的每一行恰好对应一条属于海风夜校学员、且能够匹配题目档位的作答流水。对每个 grade 分组后,COUNT(*) 因而等于该档位的流水总数;COUNT(DISTINCT f.pad_id) 恰好等于该档位至少有一条流水的学员人数。两者相除与题目定义完全一致。
由于使用内连接,只有至少包含一条流水的档位才能形成分组,所以无需额外排除零流水档位。按 grade 升序排序后,输出顺序也满足要求。
SQL 代码
SELECT
t.campus,
i.grade,
CAST(
COUNT(*) * 1.0 / NULLIF(COUNT(DISTINCT f.pad_id), 0)
AS DECIMAL(20, 4)
) AS avg_answer_cnt
FROM c21_hnr_trainee_card AS t
INNER JOIN c21_hnr_drill_flow AS f
ON f.pad_id = t.pad_id
INNER JOIN c21_hnr_drill_item AS i
ON i.item_id = f.item_id
WHERE t.campus = '海风夜校'
GROUP BY t.campus, i.grade
ORDER BY i.grade ASC;
复杂度分析
时间复杂度:逻辑上为 $O(T+F+I)$,其中 $T$、$F$、$I$ 分别是参与扫描的学员、流水和题目记录数;实际执行代价取决于数据库索引和执行计划。
空间复杂度:逻辑上为 $O(G+U)$,其中 $G$ 是档位数,$U$ 是各分组中用于去重统计的学员数;实际由数据库聚合实现决定。
易错点
- 分母不是海风夜校的全部学员数,而是当前档位中至少有一条流水的学员数。
answer_cnt是学员卡上的汇总字段,不能作为按档位统计的流水数量。COUNT(DISTINCT f.item_id)统计的是不同题目数,不是作答流水条数。- 使用
LEFT JOIN可能额外产生零流水档位;本题用INNER JOIN更直接。 - 默认
pad_id与item_id在各自维表中唯一;若维表存在重复键,连接会放大流水数,应先治理重复数据。
本场总结
本场两题都要求先明确比较或统计口径:
- 字符串题利用字典序“最早差异优先”,依次锁定左端点、统一位移和右端点,把枚举压缩为一次线性扫描。
- SQL 题要严格区分流水条数与去重作答人数,并让过滤、连接、聚合和排序各自承担正确职责。