SWE · Second Coding

Snowflake 连续食材菜谱:KMP 匹配与流式状态机

Coding 题目给有序食材列表和连续序列菜谱,并追问 O(1) 空间、流式输入;本文用逐菜谱 KMP 状态处理精确连续匹配。

题目重点:Coding 题目给有序食材列表和连续序列菜谱,并追问 O(1) 空间、流式输入;本文用逐菜谱 KMP 状态处理精确连续匹配。

题意与假设

这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。

  • 菜谱是具体食材 token 的有序数组,完整且连续地出现在总列表中即返回 true;不允许跳过食材,也不要求覆盖总列表。
  • 空菜谱按常见字符串匹配约定为 true;每条菜谱只需知道是否至少出现一次,不返回全部位置。
  • 训练版 KMP 为每条非空菜谱保存前缀表 O(m);题目 O(1) 额外空间追问可用每条菜谱朴素滑窗复扫来满足,但时间退化。

解题路线

  1. 先澄清“连续”是菜谱 token 在输入中相邻出现;对每条菜谱用 KMP 前缀表描述部分匹配长度。
  2. 顺序消费食材;当前 token 不匹配时按 pi 回退状态,匹配时递增,达到菜谱长度即标记 found。食材可逐个到达,不需要保留历史输入。
  3. 若严格要求 O(1) 额外空间且输入可重读,对每个起点做朴素逐项对照,最坏 O(Nm);单向不可重放流要同时支持任意长模式,无法普遍保持 O(1) 状态。
  4. 多菜谱时为每条维护状态,时间 O(NR+Σm_i);若 R 很大,可把菜谱建成 Aho–Corasick 自动机共享扫描。
  5. 用穷举所有连续切片作独立参考,对 10,000 组重复 token、空列表和多菜谱随机数据验证。

手动走一遍

食材 [a,b,a,b,c],菜谱 [a,b,c] 与 [a,c]:前者从下标 2 开始连续出现,后者不存在。KMP 处理前两个 a,b 匹配失败后的前缀回退,再在后半段命中。

Python 参考实现

下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。

代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。

"""Match each recipe as an exact contiguous ingredient sequence."""


def prefix_table(pattern: list[str]) -> list[int]:
    pi = [0] * len(pattern)
    j = 0
    for i in range(1, len(pattern)):
        while j and pattern[i] != pattern[j]:
            j = pi[j - 1]
        if pattern[i] == pattern[j]:
            j += 1
        pi[i] = j
    return pi


class RecipeMatcher:
    def __init__(self, recipe: list[str]):
        self.recipe = recipe[:]
        self.pi = prefix_table(recipe)
        self.state = 0
        self.found = not recipe

    def feed(self, ingredient: str) -> bool:
        if self.found or not self.recipe:
            return self.found
        while self.state and ingredient != self.recipe[self.state]:
            self.state = self.pi[self.state - 1]
        if ingredient == self.recipe[self.state]:
            self.state += 1
        if self.state == len(self.recipe):
            self.found = True
        return self.found


def match_recipes(ingredients: list[str], recipes: list[list[str]]) -> list[bool]:
    matchers = [RecipeMatcher(recipe) for recipe in recipes]
    for ingredient in ingredients:
        for matcher in matchers:
            matcher.feed(ingredient)
    return [matcher.found for matcher in matchers]

复杂度

R 条菜谱总长度 M、输入长 N:前缀表 O(M),逐 token 更新所有状态 O(NR) 摊还时间、O(M+R) 空间;匹配一个菜谱为 O(N+m)。若只求一次性 O(1) 额外空间的朴素版,最坏 O(Nm)。

易错点与追问

容易失分的边界

  • 把有序但不相邻的子序列当成连续菜谱。
  • 声称 KMP 不保存任何额外状态;模式前缀表需要 O(m) 空间。
  • 流式场景把全部历史食材缓存在内存,失去流处理意义。
  • 有大量菜谱时逐条扫描却宣称总时间仍 O(N+M)。

继续深挖

  • 如果菜谱可包含通配符或可替代食材,自动机如何改变?
  • 如果要报告所有命中位置,找到首个后状态应怎样回退?
  • 百万条菜谱怎样构建共享索引并分片流处理?
  • 题面若真要求多个菜谱按顺序拼接成整个列表,与子串匹配有什么区别?
✓已复制微信号!