1. 程式人生 > 實用技巧 >P4081 [USACO17DEC]Standing Out from the Herd P

P4081 [USACO17DEC]Standing Out from the Herd P

知識點: 廣義 SAM

原題面 Luogu


「扯」

隨便「口胡」一下居然「過」了。
比較考驗「程式碼能力」,第一次感覺「大模擬」沒有白寫(((
還有這個「符號」實在是太「上頭」了。


前置知識

線上構造廣義 SAM,推薦:【學習筆記】字串—廣義字尾自動機 - 辰星凌


題意簡述

給定 \(n\) 個僅包含小寫字母的字串 \(S_1\sim S_n\)
定義字串 \(S_i\) 的 「獨特值」為只屬於該串的本質不同的非空子串的個數。
求字串 \(S_1\sim S_n\) 的「獨特值」。
\(1\le n\le 10^5, 1\le \sum|S_i|\le 10^5\)


分析題意

多串子串問題,考慮廣義 SAM。

\(n\) 較小,直接維護每個狀態 包含幾個串的資訊。
parent 樹上 DP 更新祖先資訊,直接更新答案即可。
\(n\le 10^5\) 空間爆炸,做不得,考慮亂搞一波。


用 string 存字串,建廣義 SAM。
在動態建立 SAM 時,\(\operatorname{only}_i\) 記錄每個狀態 \(i\) 包含哪一個串的資訊。
若某狀態包含多個串資訊,則\(\operatorname{only}_i = - 1\)
parent 樹上 DP 更新祖先資訊。

先考慮一波 無腦暴力:
對於 \(S_i\),列舉其所有子串,在 SAM 上跑出對應狀態 \(u\)
若有 \(\operatorname{only}_u\not = 1\)

, 對應狀態只維護了一個串的資訊,一定為該子串。
這樣的子串數,即為 \(S_i\) 的「獨特值」。


上述演算法瓶頸是列舉子串。發現字首有一些好性質:

  1. 連續字首 對應狀態在 SAM 上也是連續的,實現時直接把串扔到 SAM 上跑 即得對應狀態。
  2. 字首對應狀態到 parent 樹根的鏈上 包含該字首所有後綴,可以包含所有子串資訊。

考慮僅列舉字首,列舉複雜度變為線性。


發現一些結論:

  1. parent 樹上一條從葉到根的鏈,維護的串的個數,是單調不減的。
  2. 若某狀態 \(i\)\(\operatorname{only}_i\not = -1\), 則對於其子樹中所有狀態 \(j\),有 \(\operatorname{only}_j=\operatorname{only}_i\)

考慮維護狀態 \(i\) 的 parent 樹上距它最遠的,\(\operatorname{only}\not= -1\) 的祖先 \(\operatorname{top}_i\)
由結論 1,若某狀態 \(i\)\(\operatorname{only}_i=-1\)\(\operatorname{top}_i=0\)

由結論 2,跑到的 \(\operatorname{only}\not= - 1\) 的節點對答案有貢獻,在其子樹中所有節點都會有貢獻。
可預處理子樹 \(\operatorname{len}(i)-\operatorname{len}(\operatorname{link}(i))\) 之和 \(\operatorname{sum}\),即某子樹中的子串數量。

統計答案時,統計 跑到的狀態 \(u\)\(\operatorname{sum}(\operatorname{top}_u)\),一個 \(\operatorname{top}_u\) 只能統計一次,去重可用排序實現。
這樣為什麼是對的?考慮字首的性質 2 感性理解一下。

每個串都在 SAM 上進行一匹配 並進行一次排序,總複雜度大概是 \(O(\sum\limits_{i}^{n} |S_i|\log |S_i|)\),可過。


程式碼實現

//知識點:廣義 SAM
/*
By:Luckyblock  
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#define ll long long
#define pr std::pair
#define mp std::make_pair
const int kMaxn = 2e5 + 10;
const int kMaxm = 26;
//=============================================================
std :: string S[kMaxn];
int cnt[kMaxn], id[kMaxn << 1];
int only[kMaxn << 1], top[kMaxn << 1], sum[kMaxn << 1];
int num, node_num = 1, ch[kMaxn << 1][kMaxm], len[kMaxn <<1], link[kMaxn << 1];
int edge_num, head[kMaxn], v[kMaxn << 1], ne[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 AddEdge(int u_, int v_) {
  v[++ edge_num] = v_, ne[edge_num] = head[u_], head[u_] = edge_num;
}
int Insert(int c_, int last_) {
  if (ch[last_][c_]) {
    int p = last_, q = ch[p][c_];
    if (len[p] + 1 == len[q]) {
      only[q] = - 1;
      return q;
    }
    int newq = ++ node_num;
    memcpy(ch[newq], ch[q], sizeof(ch[q])); 
    len[newq] = len[p] + 1; 
    link[newq] = link[q];
    link[q] = newq; 
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
    only[newq] = num;
    return newq;
  }
  int p = last_, now = ++ node_num;
  only[now] = num;
  len[now] = len[p] + 1;
  for (; p && ! ch[p][c_]; p = link[p]) ch[p][c_] = now;
  if (! p) {link[now] = 1; return now;} 
  int q = ch[p][c_];
  if (len[q] == len[p] + 1) {link[now] = q; return now;}
  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;
  return now;
}
void Dfs1(int u_) {
  for (int i = head[u_]; i; i = ne[i]) {
    Dfs1(v[i]);
    if (only[u_] == - 1) continue ;
    if (! only[u_]) {
      only[u_] = only[v[i]]; 
    } else if (only[u_] != only[v[i]]) {
      only[u_] = - 1; 
    }
  }
}
void Dfs2(int u_, int top_) {
  if (! top_ && only[u_] != - 1) top_ = u_;
  top[u_] = top_;
  if (top_) sum[u_] += len[u_] - len[link[u_]]; //你的字首和
  for (int i = head[u_]; i; i = ne[i]) {
    Dfs2(v[i], top_);
    sum[u_] += sum[v[i]]; 
  }
}
void Work(std :: string S_) {
  std :: vector <int> node;
  ll ans = 0;
  int n = S_.length(), now = 1;
  for (int i = 0; i < n; ++ i) {
    now = ch[now][S_[i] - 'a'];
    if (only[now] != - 1) node.push_back(top[now]);
  }
  std :: sort(node.begin(), node.end());
  for (int i = 0, n = node.size(); i < n; ++ i) {
    if (i != 0) {
      if (node[i] == node[i - 1]) continue;
    }
    ans += sum[node[i]];
  }
  printf("%lld\n", ans);
}
//=============================================================
int main() {
  int T = read();
  for (num = 1; num <= T; ++ num) {
    std :: cin >> S[num];
    int n = S[num].length(), last = 1;
    for (int j = 0; j < n; ++ j) last = Insert(S[num][j] - 'a', last);
  }
  for (int i = 2; i <= node_num; ++ i) AddEdge(link[i], i);
  Dfs1(1), Dfs2(1, 0);
  for (int i = 1; i <= T; ++ i) Work(S[i]);
  return 0; 
}