首页 > 技术文章 > 「NOI2018」你的名字

luckyblock 2020-08-22 22:14 原文

知识点: SAM,线段树合并

原题面 Loj Luogu


抄着题解水过去了。
这就是 noi 吗/fad


题意简述

给定模式串 \(S\),有 \(Q\) 次询问。
每次询问给定询问串 \(T\),参数 \(l,r\)
\(T\) 的本质不同子串中 没有在 \(S[l:r]\) 中出现过的个数。
\(1\le |S|\le 5\times 10^5\)\(1\le l\le r\le |S|\)\(1\le Q\le 10^5\)\(1\le \sum |T|\le 10^6\)
特殊性质:有 \(68\%\) 的测试点满足询问的 \(l=1, r=|S|\)


分析题意

\(T\) 的本质不同子串 没有\(S[l:r]\) 中出现过的个数,等价于 总个数减去出现过的个数。

建出 \(T\) 的 SAM,本质不同子串的总个数 = \(\sum\operatorname{len}(i) - \operatorname{len}(\operatorname{link}(i))\)
考虑如何快速求得出现过的子串个数。


先考虑特殊性质 \(l=1, r=|S|\)。即需要求得 \(T\) 的本质不同子串 在 \(S\) 中出现的次数。
考虑对于 \(T\) 的每一个前缀 \(T[:i]\),求得其在 \(S\) 中出现的最长的后缀 \(\operatorname{match}_i\),可在 \(S\) 的 SAM 上匹配 \(T\) 求得。
具体怎么求详见:如何用 SAM 过 SP1811

对于每一个 SAM 的状态,其维护的所有串的 \(\operatorname{endpos}\) 相同。
维护 \(T\) 的 SAM 的每个状态 \(i\)\(\operatorname{endpos}\) 中最小的值,设为 \(\operatorname{firend}_i\)

考虑 \(T\) 的 SAM 的状态 \(i\) 维护的 \(T\) 的子串,它们互为后缀,且其长度在 \([\operatorname{len}(\operatorname{link}(i)) + 1, \operatorname{len}(i)]\) 内。
\(\operatorname{match}_{{\operatorname{firend}}_i}\ge \operatorname{len}(\operatorname{link}(i)) + 1\),表示状态 \(i\) 维护的串中小于等于 \(\operatorname{match}_{{\operatorname{firend}}_i}\) 的在 \(S\) 中出现过。则 \([\operatorname{len}(\operatorname{link}(i)) + 1, \operatorname{match}_{\operatorname{firend}_i}]\) 内的串,均在 \(S\) 中出现过,其它串均在 \(S\) 内未出现。

考虑枚举 \(T\) 的 SAM 的状态,则 \(T\) 的在 \(S\) 中出现过的,本质不同的子串个数,为:

\[\sum_{i=2}{\large{\{}}\max\{0, \operatorname{match}_{\operatorname{firend}_i}-\operatorname{len}(\operatorname{link}(i))\}{\large{\}}} \]

再考虑一开始的补集转化,由于可能存在 \(\operatorname{match}_{\operatorname{firpos}_i}>\operatorname{len}(i)\),则答案为:

\[\sum_{i=2}{\large{\{}} \max\left\{0, \operatorname{len}(i) -\max\left[\operatorname{match}_{\operatorname{firpos}_i}, \operatorname{len}(\operatorname{0link}(i))\right]\right\}{\large{\}}} \]


再考虑 \(l,r\) 任意的情况。
发现 \(l,r\) 的改变,只影响上述过程中对 \(\operatorname{match}\) 的预处理。
如果仍按照上述过程进行匹配,可能会匹配到一个不属于 \(S[l:r]\) 的子串。

考虑使用权值线段树合并维护 \(\operatorname{endpos}\) 即可。
合并后的 \(\operatorname{endpos}\) 意义与 SAM 中的原定义不同,代表以该状态代表的串 为后缀的 所有 位置。

设匹配的长度为 \(len\)。若当前匹配到的状态属于 \(S[l:r]\),等价于匹配到的状态的 \(\operatorname{endpos}\) 中存在 \([l+len-1,r]\) 中的数。

考虑在匹配时增加判断:
设当前状态匹配的长度为 \(len\),若应该按照对应字符转移到的下一个状态 的 \(\operatorname{endpos}\) 中存在 \([l+len,r]\),则可转移。否则令 \(len-1\),再进行检查。
\(len = \operatorname{len}(\operatorname{link}(i))\),令当前匹配的状态转移到 \(\operatorname{link}(i)\),再重复进行上述检查过程。

复杂度 \(O(n\log n)\) 级别。


爆零小技巧

SAM 分裂状态时,注意状态信息的继承。


代码实现

//知识点:SAM,线段树合并
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define ll long long
#define max std::max
const int kMaxn = 1e6 + 10;
const int kMaxm = 26;
//=============================================================
char S[kMaxn], T[kMaxn];
//=============================================================
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(ll &fir, ll sec) {
  if (sec > fir) fir = sec;
}
struct SuffixAutomaton {
  int n, last = 1, node_num = 1;
  int firend[kMaxn << 1];
  int ch[kMaxn << 1][kMaxm], len[kMaxn << 1], link[kMaxn << 1];
  void Clear() {
    for (int i = 1; i <= node_num; ++ i) {
      link[i] = len[i] = firend[i] = 0;
      memset(ch[i], 0, sizeof (ch[i]));
    }
    last = node_num = 1;
  }
  void Insert(int c_, int pos_) {
    int p = last, now = last = ++ node_num;
    firend[now] = pos_; 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; 
    firend[newq] = firend[q]; //注意继承
    link[q] = link[now] = newq; 
    for (; p && ch[p][c_] == q; p = link[p]) ch[p][c_] = newq;
  }
};
struct SuffixAutomaton1 {
  #define ls lson[now_]
  #define rs rson[now_]
  SuffixAutomaton sam;
  int edge_num, head[kMaxn], v[kMaxn << 1], ne[kMaxn << 1];
  int node_num, root[kMaxn << 1];
  int lson[kMaxn << 5], rson[kMaxn << 5], sum[kMaxn << 5];
  void TreeInsert(int &now_, int L_, int R_, int pos_) {
    if (! now_) now_ = ++ node_num;
    sum[now_] ++;
    if (L_ == R_) return ;
    int mid = (L_ + R_) >> 1;
    if (pos_ <= mid) TreeInsert(ls, L_, mid, pos_);
    else TreeInsert(rs, mid + 1, R_, pos_);
  }
  int Merge(int x_, int y_, int L_, int R_) {
    if (! x_ || ! y_) return x_ + y_;
    int now_ = ++ node_num;
    sum[now_] = sum[x_] + sum[y_];
    if (L_ == R_) return now_;
    int mid = (L_ + R_) >> 1;
    ls = Merge(lson[x_], lson[y_], L_, mid);
    rs = Merge(rson[x_], rson[y_], mid + 1, R_);
    return now_;
  }
  int Query(int now_, int L_, int R_, int ql_, int qr_) {
    if (! now_) return 0;
    if (ql_ <= L_ && R_ <= qr_) return sum[now_];
    int mid = (L_ + R_) >> 1, ret = 0;
    if (ql_ <= mid) ret += Query(ls, L_, mid, ql_, qr_);
    if (qr_ > mid) ret += Query(rs, mid + 1, R_, ql_, qr_);
    return ret;
  }
  void AddEdge(int u_, int v_) {
    v[++ edge_num] = v_, ne[edge_num] = head[u_], head[u_] = edge_num;
  }
  void Dfs(int u_) {
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      Dfs(v_);
      root[u_] = Merge(root[u_], root[v_], 1, sam.n);
    }
  }
  void Build() {
    sam.n = strlen(S + 1);
    for (int i = 1; i <= sam.n; ++ i) {
      sam.Insert(S[i] - 'a', i);
      TreeInsert(root[sam.last], 1, sam.n, i);
    }
    for (int i = 2; i <= sam.node_num; ++ i) AddEdge(sam.link[i], i);
    Dfs(1);
  }
  #undef ls
  #undef rs
} sam1;

struct SuffixAutomaton2 {
  SuffixAutomaton sam;
  int match[kMaxn << 1];
  void GetMatch(int l_, int r_) {
    int now = 1, nowlen = 0;
    for (int i = 1; i <= sam.n; ++ i) {
      while (true) {
        if (sam1.sam.ch[now][T[i] - 'a'] && 
            sam1.Query(sam1.root[sam1.sam.ch[now][T[i] - 'a']], 
                            1, sam1.sam.n, l_ + nowlen, r_)) {
          now = sam1.sam.ch[now][T[i] - 'a'];
          nowlen ++;
          break;
        }
        if (! nowlen) break;
        nowlen --;
        if (nowlen == sam1.sam.len[sam1.sam.link[now]]) {
          now = sam1.sam.link[now];
        }
      }
      match[i] = nowlen;
    }
  }
  void Build() {
    sam.Clear();
    sam.n = strlen(T + 1);
    for (int i = 1; i <= sam.n; ++ i) {
      sam.Insert(T[i] - 'a', i);
    }
  }
  void Solve() {
    int l = read(), r = read();
    ll ans = 0ll;
    GetMatch(l, r);
    for (int i = 2; i <= sam.node_num; ++ i) {
      ans += max(0, sam.len[i] - max(match[sam.firend[i]], sam.len[sam.link[i]]));
    }
    printf("%lld\n", ans);
  }
} sam2;
//=============================================================
int main() {
  freopen("name.in", "r", stdin);
  freopen("name.out", "w", stdout);
  scanf("%s", S + 1);
  sam1.Build();
  int q = read();
  while (q --) {
    scanf("%s", T + 1);
    sam2.Build(); sam2.Solve();
  }
  return 0; 
}

推荐阅读