在工作中学算法——基于日志的系统故障预测

在现代IT运维和监控中有一个很常见的场景:通过日志分析来预测系统故障

一个由成百上千台服务器组成的复杂系统每秒钟都会产生海量的日志,记录着各种各样的操作和状态,这就是 events 数组。

这个列表就像是系统的心电图或运行记录。里面的每一个数字ID都代表一个具体的事件,例如:

  • 1: 用户登录成功
  • 2: 数据库查询超时
  • 3: 内存使用率超过90%
  • 4: 端口扫描警报
  • 5: 缓存未命中
  • ... 等等。

在绝大多数时间里,这些事件的发生都是正常的。

通过长期的经验和数据分析,运维专家们发现了一些"故障前兆模式"。这些模式是由一系列关键事件(即"特征事件")按特定顺序发生构成的。当这个模式出现时,系统很可能在不久的将来发生故障(比如服务器宕机、服务无响应或安全漏洞被利用)。这个模式就是 traits 数组。

例如,专家发现模式 [4, 3, 2],即 [端口扫描警报, 内存超限, 数据库超时] 的相继出现,往往预示着一次恶意的攻击或资源耗尽型故障。

我们应该在一个庞大的实时事件流中,尽快地(最早)找到一个包含所有故障预警信号的、最紧凑的(最短)时间窗口,以便进行故障预警和修复。

  • 为什么要找一个 events 的"连续子序列"? 因为故障的预兆通常发生在一个相对集中的时间窗口内。周一的端口扫描和周五的数据库超时可能毫无关联,但如果它们在几分钟内连续发生,危险性就大大增加。这个"连续子序列"就代表了我们要分析的那个时间窗口。
  • 为什么是"匹配特征序列"?(traits 是它的子序列) 因为在真实日志中,两个关键的特征事件之间,必然会夹杂着大量的其他正常日志(如用户访问、数据读写等)。我们只关心特征事件是否按顺序出现了,而不在乎它们之间隔了多少其他无关事件。这正是"子序列"的定义。
  • 为什么要找"最短"的连续子序列? 一个更短的时间窗口内集齐了所有故障前兆,意味着事件之间的因果关系可能更强,问题更紧急,定位问题也更容易。如果在一个长达数小时的日志中才凑齐特征序列,其中可能包含太多干扰信息,不利于快速诊断。
  • 为什么要找"最早"的最短连续子序列? 这是为了争分夺秒。越早发现故障前兆,就能越早介入处理(如重启服务、流量限制、安全补丁),从而避免或减轻实际故障带来的损失。

于是我们抽象出了下述问题。

给定两个整数数组:一个主数组 S (对应 events)和一个模式数组 P (对应 traits)。

其中:

  • 主数组 S 的元素可以重复。
  • 模式数组 P 的元素保证互不重复。

任务是 :在主数组 S 中,找到一个连续子数组 S_sub,这个子数组必须满足以下条件:

  1. 匹配规则: 模式数组 PS_sub 的一个子序列 (Subsequence)

    • "子序列"的含义是:可以从 S_sub 中删除零个或多个元素,使得剩下的元素在不改变相对顺序的情况下,与 P 完全相同。
  2. 优化目标: 在所有满足上述匹配规则的 S_sub 中,找到长度最短的一个。

  3. 平局规则 (Tie-breaker): 如果存在多个长度相同的最短子数组,则选择那个在主数组 S起始位置最靠前的子数组。

最终返回这个找到的最优子数组。

输入

第一个参数是数组 events,代表原始事件序列,1 < events.length <= 1000

第二个参数是数组 traits,代表特征序列,1 <= traits.length <= 20

0 <= eventsi, traitsi <= 1000

注:输入保证至少存在一个匹配的连续子序列。

输出

一个连续子序列

输入:

4, 8, 4, 3, 6, 6, 8

4, 6, 8

输出:

4, 3, 6, 6, 8

解释:

如 event 中的一个连续子序列 4, 3, 6, 6, 8,去除其中元素 3 和 任一个 6 后、且余下元素保持相对顺序不变,所形成的新序列 4, 6, 8 和特征序列相同,因此所选择的连续子序列 4, 3, 6, 6, 8 是匹配特征序列的。

java 复制代码
import java.util.Arrays;
import java.util.Scanner;
import java.util.StringJoiner;

public class Solution {
    /**
     * 在 events 数组中找到一个最短的、最早出现的连续子序列,该子序列包含 traits 数组作为其子序列。
     *
     * @param events 原始事件序列
     * @param traits 特征序列
     * @return 匹配到的最短、最早的连续子序列
     */
    public int[] findShortestMatch(int[] events, int[] traits) {
        // --- 1. 初始化 ---
        // 处理边界情况,如果特征序列为空,则最短匹配子序列也为空
        if (traits == null || traits.length == 0) {
            return new int[0];
        }

        int n = events.length; // 主序列长度
        int m = traits.length; // 特征序列长度

        int minLength = Integer.MAX_VALUE; // 记录找到的最短有效子序列的长度
        int resultStart = -1;              // 记录该最短子序列的起始索引

        // --- 2. 核心算法:遍历所有可能的起始点 ---
        // 外层循环:将 events 中的每一个位置 i 都作为潜在最短子序列的起始点
        for (int i = 0; i <= n - m; i++) { // 优化:起始点 i 最多到 n-m 即可
            // --- 3. 对于每个起始点 i,寻找能完整匹配 traits 的最短结束点 j ---
            int eventPtr = i;       // 指针,用于在 events 中从 i 开始向右扫描
            int traitPtr = 0;       // 指针,用于在 traits 中匹配元素

            // 内层循环:从 i 开始扫描,直到 traits 的所有元素都被找到或 events 扫描完毕
            while (eventPtr < n && traitPtr < m) {
                // 如果 events 中的当前元素与 traits 中需要匹配的元素相同
                if (events[eventPtr] == traits[traitPtr]) {
                    // 匹配成功,traits 指针向后移动,准备匹配下一个元素
                    traitPtr++;
                }
                // 无论是否匹配成功,events 指针都向后移动,继续寻找
                eventPtr++;
            }

            // --- 4. 检查是否找到了一个完整的匹配 ---
            // 如果 traitPtr 等于 traits 的长度,说明 traits 的所有元素都按顺序被找到了
            if (traitPtr == m) {
                // 匹配成功!
                // 此时的 eventPtr 指向匹配完成后下一个位置
                // 所以子序列的结束索引是 eventPtr - 1。
                int currentLength = eventPtr - 1 - i + 1;

                // 检查当前找到的子序列是否比之前记录的最短子序列更短
                if (currentLength < minLength) {
                    // 如果更短,则更新记录
                    minLength = currentLength;
                    resultStart = i;
                }
                // 如果长度相同,我们不做任何事。因为题目要求返回"最早匹配到的",
                // 而我们是从左到右遍历起始点 i 的,所以第一个找到的自然就是最早的。
            }
            // 如果 traitPtr < m,说明从 i 开始无法构成一个完整的匹配,继续尝试下一个起始点 i+1
        }

        // --- 5. 构建并返回最终结果 ---
        // 题目保证至少存在一个匹配,所以 resultStart 不会是 -1。
        // 但为代码健壮性考虑,可以处理找不到的情况。
        if (resultStart == -1) {
            return new int[0]; // 或者根据要求返回 null
        }

        // 根据记录的最佳起始索引和最短长度,从原数组中截取子数组
        return Arrays.copyOfRange(events, resultStart, resultStart + minLength);
    }
}

class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        // 读取两行以空格分隔的整数
        String eventsLine = scanner.nextLine();
        String traitsLine = scanner.nextLine();
        scanner.close();

        // --- 解析输入字符串为 int 数组 ---
        // 使用 Stream API 简洁地处理
        int[] events = parseLineToIntArray(eventsLine);
        int[] traits = parseLineToIntArray(traitsLine);

        // --- 调用核心算法 ---
        Solution solution = new Solution();
        int[] result = solution.findShortestMatch(events, traits);

        // --- 格式化输出 ---
        // 使用 StringJoiner 方便地在数字间添加空格
        StringJoiner sj = new StringJoiner(" ");
        for (int num : result) {
            sj.add(String.valueOf(num));
        }
        System.out.println(sj.toString());
    }

    /**
     * 辅助方法,用于将一行以空格分隔的数字字符串解析为 int 数组。
     * @param line 输入行
     * @return 解析后的 int 数组
     */
    private static int[] parseLineToIntArray(String line) {
        String[] parts = line.trim().split("\\s+");
        // 处理空行输入,split 会产生一个包含空字符串的数组
        if (parts.length == 1 && parts[0].isEmpty()) {
            return new int[0];
        }
        // 使用 Stream API 将 String[] 转换为 int[]
        return Arrays.stream(parts)
                     .mapToInt(Integer::parseInt)
                     .toArray();
    }
}
相关推荐
Jerry1 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry3 小时前
LeetCode 459. 重复的子字符串
算法
闲猫5 小时前
Spring AI 对接Deepseek ChatModel 聊天对话
java·前端·spring
海石5 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石5 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
自信的未来7 小时前
JSON 工具|Web Worker 工程化打包 + 语法自动修复 + 多语言代码生成实战
java·前端·json
Brookty7 小时前
【JavaEE】线程安全(一).4:写块串行保安全、CAS
java·开发语言·java-ee·多线程·线程安全
Jerry7 小时前
LeetCode 151. 反转字符串中的单词
算法
怕孤单的草丛7 小时前
缓存管理面临的主要问题
java·数据库·缓存