1. 程式人生 > 實用技巧 >SP1812 LCS2 - Longest Common Substring II

SP1812 LCS2 - Longest Common Substring II

知識點: SAM

Luogu

題意簡述:

給定不超過 \(10\) 個字串 \(S_1, S_2, \cdots\),求它們最長公共子串的長度。
\(|S_i|\le 10^5\)


分析題意

多串最長公共子串問題,考慮 SAM。

如果只有兩個串:SP1811 LCS - Longest Common Substring

對第一個串建 SAM,用第二個串從起始節點開始,在 SAM 上進行匹配。

若當前狀態為 \(x\),如果有對應字元 \(s_i\) 的轉移,直接轉移即可,匹配長度 \(+1\)
如果沒有對應轉移,轉移到 \(\operatorname{link}(x)\),匹配長度 \(=\operatorname{len}(x)+1\)

檢查有無對應轉移,若沒有則繼續轉移到 \(\operatorname{link}(\operatorname{link}(x))\),直到存在對應轉移。
若始終找不到對應轉移,則從根開始重新匹配。

跳 parnet 樹相當於失配指標,繼續利用了已匹配的部分。
匹配過程中匹配的最長長度即為答案。


考慮多串,對第一個串 \(S_1\) 建 SAM,用其他串在 SAM 上匹配,設當前匹配到串 \(S_i\)
對於狀態 \(u\),維護轉移到它時最大的匹配長度 \(mx_u\),即以該狀態作為字尾時的公共子串的最長長度。
在匹配過程中進行維護即可。

考慮一個狀態 parent 樹上的所有祖先,若該狀態可被匹配到,則祖先也可被匹配到。
祖先的 \(mx\)

應為 其子樹中 \(mx\) 的最大值。
\(S_i\) 匹配完成後按拓撲序對祖先的資訊進行更新。

對於一個狀態 \(u\),將 \(S_2\cdots S_n\) 匹配時的 \(mx_u\)\(\min\),得到在所有字串中 轉移到 \(u\) 最長的匹配長度,即以 \(u\) 為字尾時 \(S_2\cdots S_n\) 公共子串的最長長度,設為 \(mi_u\)

所有的 \(mi_u\) 取最小值,即為答案。


小細節

最長公共子串不會超過最短串的長度,應對最短的串建 SAM,以保證複雜度。
注意每次匹配完一個字串時,都將 \(mx\) 清空。
更新祖先資訊時 應對祖先的 \(\operatorname{len}\)

\(\min\)


程式碼實現

//知識點:SAM
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
const int kMaxn = 3e5 + 10;
const int kMaxm = 26;
//=============================================================
char S[11][kMaxn];
int n, T, last = 1, node_num = 1, ans;
int cnt[kMaxn], id[kMaxn];
int ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
int minn[kMaxn << 1], maxx[kMaxn << 1];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void GetMax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void GetMin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
void Insert(int c_) {
  int p = last, now = last = ++ node_num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {link[now] = 1; return ;} 
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return ;}
  int newq = ++ node_num;
  memcpy(ch[newq], ch[q], sizeof(ch[q]));  
  link[newq] = link[q], len[newq] = len[p] + 1; 
  link[q] = link[now] = newq; 
  for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
}
void TopSort() {
  for (int i = 1; i <= node_num; ++ i) cnt[len[i]] ++;
  for (int i = 1; i <= node_num; ++ i) cnt[i] += cnt[i - 1];
  for (int i = 1; i <= node_num; ++ i) id[cnt[len[i]] --] = i;
}
void Work(char *S_) {
  int n = strlen(S_ + 1), now = 1, l = 0;
  for (int i = 1; i <= n; ++ i) {
    while (now && ! ch[now][S_[i] - 'a']) {
      now = link[now];
      l = len[now]; 
    }
    if (! now) {
      now = 1;
      l = 0;
      continue ;
    }
    ++ l;
    now = ch[now][S_[i] - 'a'];
    GetMax(maxx[now], l);
  }
  for (int i = node_num; i; -- i) {
    int u = id[i], fa = link[u];
    GetMax(maxx[fa], std :: min(maxx[u], len[fa]));
    GetMin(minn[u], maxx[u]); 
    maxx[u] = 0; //注意清空,準備下一次匹配。 
  }
}
//=============================================================
int main() {
  int min_len = 1e9 + 2077, min_pos;
  while (~ scanf("%s", S[++ T] + 1)) {
    int now_len = strlen(S[T] + 1);
    if (now_len < min_len) {
      min_len = now_len;
      min_pos = T;
    }
  }
  std :: swap(S[1], S[min_pos]);
  for (int i = 1; i <= min_len; ++ i) Insert(S[1][i] - 'a');
  TopSort();
  memset(minn, 63, sizeof (minn));
  for (int i = 2; i < T; ++ i) Work(S[i]);
  for (int i = 1; i <= node_num; ++ i) GetMax(ans, minn[i]);
  printf("%d", ans);
  return 0; 
}