具代表性的面試主題

程式面試:如何實作 Suffix Automaton 並解釋 clone 狀態?

程式題困難
Offer.cc 編輯團隊發佈 更新

題幹

請實作 Suffix Automaton 的 extend 操作,解釋 suffix link、clone 和線性狀態上界,並給出一個實際查詢。

題目

給定字串 s,線上建構 Suffix Automaton(SAM)。實作 extend(c),說明每個狀態的 lenlink、轉移與 endpos 含義,並用它計算不同子串數量、模式出現次數或最長共同子串。要求解釋為什麼狀態數是 O(n),以及何時必須建立 clone。

面試官考察點

  • 能否把狀態解釋成一組具有相同 endpos 的子串,而不是普通 Trie 節點。
  • 能否正確區分連續轉移、直接連結和 clone 三種建構分支。
  • 能否維護 len[link[v]] < len[v]、轉移確定性和 suffix-link 樹不變量。
  • 能否把結構性質轉成計數、匹配和複雜度結論。

參考答案

SAM 是辨識原字串所有子串的最小部分 DFA。狀態 v 保存該等價類中最長子串長度 len[v]link[v] 指向最長的、屬於另一等價類的後綴。狀態代表的長度區間是 (len[link[v]], len[v]],所以一個狀態可以涵蓋多個不同長度的子串。

追加字元時先建立 cur,沿 suffix link 向後尋找缺少該轉移的狀態並補邊。若遇到已有狀態 qlen[p]+1 == len[q],直接令 link[cur]=q;若不相等,複製 q 的轉移建立 clone,把 clone 的 len 設為 len[p]+1,再沿 suffix link 把指向 q 的相關邊改到 clone,並令 link[q]link[cur] 都指向 clone。clone 保證每個狀態的長度區間連續。

所有非根狀態對不同子串的貢獻是 len[v] - len[link[v]]。若先給原字串前綴對應狀態計數 1,再按 len 降序把計數累加到 link,即可得到每個等價類的出現次數。

實作範例

下面的偽代碼展示核心建構;轉移可以用雜湊表或有序映射。

text
extend(c):
  cur = new state
  len[cur] = len[last] + 1
  p = last
  while p != -1 and c not in next[p]:
    next[p][c] = cur
    p = link[p]
  if p == -1:
    link[cur] = root
  else:
    q = next[p][c]
    if len[p] + 1 == len[q]:
      link[cur] = q
    else:
      clone = copy(q)
      len[clone] = len[p] + 1
      while p != -1 and next[p][c] == q:
        next[p][c] = clone
        p = link[p]
      link[q] = link[cur] = clone
  last = cur

計算不同子串數量時遍歷非根狀態累加 len[v] - len[link[v]]。計算模式出現次數時沿轉移走到對應狀態,再讀取按長度降序傳播後的計數。

常見誤區

  • 把 SAM 當成只接受後綴的普通 Trie,忽略它實際壓縮了所有子串的 endpos 等價類。
  • clone 複製了轉移卻忘記複製或重新設定 link,導致後續長度區間斷裂。
  • 修改轉移時沒有沿 suffix link 回溯到第一個不再指向 q 的狀態。
  • 給 clone 計入一次前綴出現次數,造成所有出現次數偏大。
  • 用固定陣列儲存巨大字元集,卻沒有說明空間成本和字元編碼邊界。

複雜度取捨

在固定字母表或雜湊轉移下,建構時間和空間為 O(n),狀態最多約 2n-1。若每個狀態的轉移使用平衡樹,複雜度會增加到與字母表操作相關的對數因子。SAM 適合固定文本上的大量子串查詢;需要字典序遍歷、LCP 或後綴排序時,後綴陣列可能更易控制記憶體和存取區域性。

SAM 的線性上界依賴線上追加模型。若要在字串中間插入、刪除或雙端更新,通常需要不同結構,不能直接沿用 extend 的不變量。

先用空字串、單字元、重複字元和觸發 clone 的 abbb 做逐步斷言,再隨機生成字串,與暴力集合比較不同子串數量和模式出現次數。最長共同子串測試可把第二個字串逐字元送入 SAM,並在失配時沿 suffix link 回退。

參考資料

  • CP-algorithms 的 SAM 講義:狀態區間、clone 建構和查詢公式。
  • Blumer 等人的最小子串自動機論文:狀態與轉移上界的理論來源。
  • Carnegie Mellon 字串演算法講義:後綴陣列與後綴自動機的適用場景比較。

追問

為什麼每個狀態代表連續長度區間?

同一 endpos 等價類中的子串按長度排列時不會出現空洞;最長子串的後綴會逐步落入包含它的更大等價類。link 指向區間左側的邊界,因此區間正好是 (len[link[v]], len[v]]

什麼時候必須建立 clone?

當已有轉移目標 qlen[q] 大於 len[p]+1 時,q 同時承載了兩個不連續的長度範圍。複製轉移並把邊界拆出 clone,才能恢復連續區間不變量。

為什麼不同子串數量是這些區間長度的總和?

每個狀態涵蓋的長度區間不重疊,且區間中的每個長度對應一個不同子串。把每個狀態的區間大小相加,就得到全部非空不同子串數。

SAM 與 Aho–Corasick 如何選擇?

SAM 面向一個文本的全部子串結構和多種統計;Aho–Corasick 面向一組已知模式在文本中的批量匹配。需求是模式集合固定還是文本結構固定,決定了建構方向。

如何擴展到最長共同子串?

先為字串 S 建 SAM,再掃描 T。沿轉移前進並維護目前匹配長度;失配時沿 suffix link 回退並繼續嘗試,記錄過程中出現的最大長度即可。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具