「NOI2011」阿狸的打字机

知识点:ACAM,离线,树状数组

原题面:LojLuogu

简述

建议先阅读原题面后再阅读简述题面。

通过奇怪的方法给定 \(n\) 个字符串 \(s_1\sim s_n\),给定 \(m\) 次询问。
每次询问给定参数 \(x\)\(y\),求在字符串 \(s_y\)\(s_x\) 的出现次数。
\(1\le n,m,|\sum s_i|\le 10^5\)
1S,256MB。

首先可以发现,题中给出的打字的过程与 Trie 的插入过程类似,由此可以直接构建出所有串的 Trie。

对 Trie 建立 ACAM 后,先考虑如何暴力查询。
对于每一次询问,都将字符串 \(s_y\) 扔到 ACAM 上匹配。每匹配到一个状态,就暴力上跳考察其在 \(\operatorname{fail}\) 树上的祖先中是否包含 \(s_x\) 对应状态。若包含则证明 \(s_x\) 作为当前匹配部分的一个后缀出现了,贡献累计即为答案。
总复杂度可以达到 \(O(T|\sum| + m|s_i|)\) 级别。其中 \(T\) 为 ACAM 节点数量,其上限为 \(\sum |s_i|\)

注意到每次匹配的文本串都是模式串,这说明在匹配过程中,不会出现失配情况,且各状态不重复。即匹配过程中经过的路径是 Trie 中的一条自根向下的链。

观察暴力的过程,询问 \((x,y)\) 的答案即为祖先包括 \(s_x\) 状态的 \(s_y\) 的状态数。
由上述性质,这也可以理解为 \(\operatorname{fail}\) 树上祖先包括 \(s_x\) 的,自根至 \(s_y\) 的 Trie 上的链上的节点数量。
更具体地,考虑建立 \(\operatorname{fail}\) 树,答案为 \(s_x\) \(\operatorname{fail}\) 的子树中自根到 \(s_y\) 对应状态的链上的节点数量。


如何实现?对于询问 \((x,y)\),考虑大力标记 \(s_y\) 对应的所有状态,再查询 \(\operatorname{fail}\) 树上 \(s_x\) 的子树中被标记点数。上述过程可通过 dfn 序 + 树状数组完成。

如果对每次询问都做一次上面的过程,显然是非常浪费的。考虑离线所有询问,在每次询问的状态 \(s_y\) 上打一个询问 \(s_x\) 的标记。
之后在 Trie 上 dfs,每第一次访问到一个节点,就令树状数组中对应 dfn 位置 \(+1\),表示标记该节点。从该节点回溯时再 \(-1\)
可以发现,dfs 到状态 \(u\) 时,被标记的节点恰好组成了自根至 \(s_y\) 的 Trie 上的链上的节点。则访问到 \(u\) 即可直接查询离线下来的询问。

总时间负责度 \(O(T|\sum| + m\log T)\),其中 \(T\) 为 ACAM 节点数量,其上限为 \(\sum |s_i|\)

实现细节详见代码,注意映射关系。

代码

//知识点:ACAM,BIT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, ans[kN], pos[kN];
char s[kN];
std::vector <int> query1[kN], query2[kN];
//=============================================================
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 Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace BIT {
  #define low(x) (x&-x)
  int Lim, t[kN];
  void Init(int lim_) {
    Lim = lim_;
  }
  void Insert(int pos_, int val_) {
    for (int i = pos_; i <= Lim; i += low(i)) {
      t[i] += val_;
    }
  }
  int Sum(int pos_) {
     int ret = 0;
    for (int i = pos_; i; i -= low(i)) {
      ret += t[i];
    }
    return ret;
  }
  int Query(int l_, int r_) {
    return Sum(r_) - Sum(l_ - 1);
  }
  #undef low
}
namespace ACAM {
  int node_num, fa[kN], tr[kN][26], fail[kN];
  int e_num, head[kN], v[kN], ne[kN];
  int dfn_num, dfn[kN], size[kN];
  std::vector <int> trans[kN]; //原 Trie 树上的转移。因为建立了 Trie 图,需要把它记录下来,
  void Read(char *s_) { //按照读入建立 Trie
    int now = 0;
    for(int i = 1, lim = strlen(s_ + 1); i <= lim; ++ i) {
      if (s_[i] == 'P') {
        pos[++ n] = now;
      } else if (s_[i] == 'B') {
        now = fa[now];
      } else {
        if (!tr[now][s_[i] - 'a']) {
          tr[now][s_[i] - 'a'] = ++ node_num;
          trans[now].push_back(node_num);
          fa[node_num] = now;
        }
        now = tr[now][s_[i] - 'a'];
      }
    }
  }
  void Add(int u_, int v_) {
    v[++ e_num] = v_;
    ne[e_num] = head[u_];
    head[u_] = e_num;
  }
  void Dfs(int u_) {
    dfn[u_] = ++ dfn_num;
    size[u_] = 1;
    for (int i = head[u_]; i; i = ne[i]) {
      int v_ = v[i];
      Dfs(v_);
      size[u_] += size[v_];
    }
  }
  void Build(char *s_) {
    Read(s_);
    std::queue <int> q;
    for (int i = 0; i < 26; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      int now = q.front(); q.pop();
      for (int i = 0; i < 26; ++ i) {
        if (tr[now][i]) {
          fail[tr[now][i]] = tr[fail[now]][i];
          q.push(tr[now][i]);
        } else {
          tr[now][i] = tr[fail[now]][i];
        }
      }
    }
    for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
    Dfs(0);
    BIT::Init(node_num + 1);
  }
  void Query(int u_) { //dfs 回答询问到 u_
    BIT::Insert(dfn[u_], 1); //标记
    for (int i = 0, lim = query1[u_].size(); i < lim; ++ i) { //枚举此时可以回答的询问
      int x = query1[u_][i], id = query2[u_][i]; //查询 x 的子树中标记点的个数
      ans[id] = BIT::Query(dfn[x], dfn[x] + size[x] - 1);
    }
    for (int i = 0, lim = trans[u_].size(); i < lim; ++ i) Query(trans[u_][i]);
    BIT::Insert(dfn[u_], -1); //去除标记
  }
}
//=============================================================
int main() { 
  scanf("%s", s + 1);
  ACAM::Build(s);
  int m = read();
  for (int i = 1; i <= m; ++ i) { //离线询问
    int x = read(), y = read();
    query1[pos[y]].push_back(pos[x]);
    query2[pos[y]].push_back(i);
  }
  ACAM::Query(0);
  for (int i = 1; i <= m; ++ i) printf("%d\n", ans[i]);
  return 0; 
}
posted @ 2021-01-09 09:50  Luckyblock  阅读(89)  评论(0编辑  收藏  举报