【华为OD机试真题】日志解析(Java/Go)
一、题目
题目描述:
你是一个运维工程师,你同时负责n个系统的运维工作,已知每个系统每天会都从现场采集大量的现网运行日志(错误日志、接口日志等)下来生成一个日志文件,每个系统采集下来的日志文件大小均不相同。为了解析这些日志,你给每个系统配备了一台默认服务器进行日志解析,且此台服务器只能给本系统使用,由于所配置的服务器规则均相同,因为解析日志的速度也是相同的,即每秒钟可以解析defaultCnt条日志。
现在你发现解析的速度达不到预期,但你手头上还有一部分额外的资源可以使用,这些资源可以在任意时刻配置给任意一台服务器。但有个限制,那就是同一时刻只能配给其中一台服务器器,且服务器器是能整合全部额外资源,当然在下一秒钟即可配备给另外一台服务器。某一台服务器配备了额外资源以后,则每秒钟会增加解析extraCnt条日志,即每秒可解析(defaultCnt+extraCnt)条日志。
输入描述
输入一共2行第一行为3个正整数n、defaultCnt、extraCnt,
第二行为n个正整数,a1,a2,…,an,分别表示每个系统采集的日志条目数
已知:1 ≤ n ≤ 1×10⁵,1 ≤ defaultCnt, extraCnt ≤ 100,1 ≤ ai ≤ 1000
输出描述
一个正整数,表示解析完成全部日志的最少时间。
示例1输入:
3 2 1
1 2 3输出:
1
说明:
每个服务器每秒可解析2条日志,直接将额外的资源配备给第三台服务器器,则第三台服务器每秒可解析(2+1=3)条日志,则只需1秒即可解析完三个系统的全部日志。
示例2
输入:
1 1 1
4输出:
2
这是一道经典的“最小化最大值”(Minimize the Maximum)问题。这类问题通常具有单调性:如果时间 TT 能完成任务,那么任何大于 TT 的时间也一定能完成;反之,如果 TT 不能完成,小于 TT 的时间也一定完不成。因此,最优解法是二分答案(Binary Search on Answer)。
二、解题思路
1. 核心逻辑转化
题目问的是“最少需要多少秒”。我们可以反过来思考:“给定一个时间 TT ,我们能否在 TT 秒内解析完所有日志?”
- 默认能力:每台服务器在 TT 秒内,默认能解析 T×defaultCntT×defaultCnt 条日志。
- 缺口计算:对于第 ii 个系统,如果日志量 ai>T×defaultCntai>T×defaultCnt ,说明默认速度不够,存在缺口:
缺口i=ai−(T×defaultCnt)缺口i=ai−(T×defaultCnt)
- 额外资源需求:为了填补这个缺口,我们需要使用额外资源。额外资源每秒能多解析 extraCntextraCnt 条。
需要的额外秒数(即需要占用额外资源的时长)为:
需额外秒数i=⌈缺口iextraCnt⌉需额外秒数i=⌈extraCnt缺口i⌉
(注意:这里向上取整,因为不足一秒也需要占用一整秒的额外资源额度)
- 全局约束:题目规定“同一时刻只能配给其中一台服务器”。这意味着,所有系统需要的“额外秒数”之和,不能超过总时间 TT 。
∑需额外秒数i≤T∑需额外秒数i≤T
如果满足该不等式,说明时间 TT 是可行的。
2. 二分查找设计
- 左边界 ( LL ):最小可能是 1 秒(或者 min(ai)/(defaultCnt+extraCnt)min(ai)/(defaultCnt+extraCnt) ,但直接从 1 开始最安全)。
- 右边界 ( RR ):最坏情况是不使用任何额外资源,只靠默认速度处理最大的那个日志文件。
R=⌈max(ai)defaultCnt⌉R=⌈defaultCntmax(ai)⌉
由于 ai≤1000,defaultCnt≥1ai≤1000,defaultCnt≥1 ,最大时间不超过 1000,范围很小。
- 判断函数
check(time):
遍历所有 aiai ,累加需要的额外秒数。如果总和 ≤time≤time ,返回true,否则false。
3. 算法流程
- 读取输入 n,defaultCnt,extraCntn,defaultCnt,extraCnt 和数组 aa 。
- 确定二分范围 [L,R][L,R] 。
- 当 L≤RL≤R :
- 取中点 mid=L+(R−L)/2mid=L+(R−L)/2 。
- 调用
check(mid)。 - 若
true:记录答案 ans=midans=mid ,尝试更短时间 R=mid−1R=mid−1 。 - 若
false:时间不够,需延长时间 L=mid+1L=mid+1 。
- 输出 ansans 。
三、Code实现
1、Java ✅
import java.util.Scanner;
public class LogParserSolution {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
if (!sc.hasNext()) return;
// 1. 读取基础参数
int n = sc.nextInt();
long defaultCnt = sc.nextLong();
long extraCnt = sc.nextLong();
// 2. 读取日志数量数组
long[] logs = new long[n];
long maxLog = 0;
for (int i = 0; i < n; i++) {
logs[i] = sc.nextLong();
if (logs[i] > maxLog) {
maxLog = logs[i];
}
}
// 3. 确定二分边界
// 左边界:最少1秒
long left = 1;
// 右边界:最坏情况,不用额外资源,只处理最大的那个文件
// 向上取整公式:(a + b - 1) / b
long right = (maxLog + defaultCnt - 1) / defaultCnt;
long ans = right;
// 4. 二分查找
while (left <= right) {
long mid = left + (right - left) / 2;
if (canFinish(mid, logs, defaultCnt, extraCnt)) {
ans = mid; // 记录可行解
right = mid - 1; // 尝试更小的时间
} else {
left = mid + 1; // 时间不够,增大时间
}
}
System.out.println(ans);
}
/**
* 检查在 targetTime 秒内是否能完成所有任务
*/
private static boolean canFinish(long targetTime, long[] logs, long defaultCnt, long extraCnt) {
long totalExtraSecondsNeeded = 0;
for (long logCount : logs) {
// 默认情况下,targetTime 秒能处理的日志数
long defaultCapacity = targetTime * defaultCnt;
if (logCount > defaultCapacity) {
long remaining = logCount - defaultCapacity;
// 计算需要多少秒的额外资源来填补剩余部分 (向上取整)
long secondsNeeded = (remaining + extraCnt - 1) / extraCnt;
totalExtraSecondsNeeded += secondsNeeded;
}
// 剪枝优化:如果累计需要的额外时间已经超过总时间,直接返回 false
if (totalExtraSecondsNeeded > targetTime) {
return false;
}
}
return totalExtraSecondsNeeded <= targetTime;
}
}
2、Go ✅
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 使用 bufio 提高读取效率
scanner := bufio.NewScanner(os.Stdin)
// 读取第一行:n, defaultCnt, extraCnt
if !scanner.Scan() {
return
}
var n int
var defaultCnt, extraCnt int64
// 格式化扫描一行中的三个整数
fmt.Sscanf(scanner.Text(), "%d %d %d", &n, &defaultCnt, &extraCnt)
// 读取第二行:日志数组
// 注意:如果数据量很大,可能需要循环 scanner,但题目描述是一行
// 为了稳健,我们直接使用 fmt.Fscan 从 stdin 逐个读取后续数字
logs := make([]int64, n)
maxLog := int64(0)
for i := 0; i < n; i++ {
if !scanner.Scan() {
// 如果换行了,继续扫描直到读完 n 个数
// 实际上 fmt.Fscan 会自动跳过空白符(包括换行),所以直接用 Fscan 更方便
break
}
// 上面的 Scan 只是预读,实际解析用 Fscan 更简单,这里重构一下读取逻辑
}
// 重新整理读取逻辑,适配多行或单行输入
reader := bufio.NewReader(os.Stdin)
// 重置变量以便重新读取(如果上面没读成功)
// 在实际机考环境中,通常输入流是连续的,直接用 Fscan 即可
fmt.Fscan(reader, &n, &defaultCnt, &extraCnt)
logs = make([]int64, n)
maxLog = 0
for i := 0; i < n; i++ {
fmt.Fscan(reader, &logs[i])
if logs[i] > maxLog {
maxLog = logs[i]
}
}
// 二分查找边界
left := int64(1)
// 右边界:(maxLog + defaultCnt - 1) / defaultCnt
right := (maxLog + defaultCnt - 1) / defaultCnt
ans := right
for left <= right {
mid := left + (right-left)/2
if canFinish(mid, logs, defaultCnt, extraCnt) {
ans = mid
right = mid - 1 // 尝试更小
} else {
left = mid + 1 // 需要更多时间
}
}
fmt.Println(ans)
}
// 检查函数
func canFinish(targetTime int64, logs []int64, defaultCnt, extraCnt int64) bool {
totalExtraSecondsNeeded := int64(0)
for _, logCount := range logs {
defaultCapacity := targetTime * defaultCnt
if logCount > defaultCapacity {
remaining := logCount - defaultCapacity
// 向上取整:(a + b - 1) / b
secondsNeeded := (remaining + extraCnt - 1) / extraCnt
totalExtraSecondsNeeded += secondsNeeded
}
// 剪枝
if totalExtraSecondsNeeded > targetTime {
return false
}
}
return totalExtraSecondsNeeded <= targetTime
}
四、复杂度分析
| 指标 | 分析 |
|---|---|
| 时间复杂度 | O(N⋅log(max(A)))O(N⋅log(max(A))) 其中 NN 是系统数量, max(A)max(A) 是最大日志量。 二分次数为 log(max(A)/defaultCnt)log(max(A)/defaultCnt) ,每次检查遍历 NN 个元素。 对于 N=105,A=1000N=105,A=1000 ,运算量极小,完全满足要求。 |
| 空间复杂度 | O(N)O(N) 用于存储日志数组。 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)