【字符串】【P5830】 【模板】失配树

【字符串】【P5830】 【模板】失配树

Description

给定一个长度为 n 的字符串 S,有 m 次询问,每次询问给定 S 的两个前缀,求它们的最长公共 border 的长度。

最长公共 border 的含义为,对于一个字符串 T,设其 Border 集合为所有既是 S 的前缀子串又是 S 的后缀子串的集合,两个字符串的最长公共 border 为两个字符串的 Border 集合的交集中长度最长的字符串。

Limitations

1n106

1m105

Solution

注意,这篇题解不是这个模板的标准做法,也不是最简单的做法。

两个前缀的最长公共 border 即为他们在 border 树上的 LCA

因为刚起床就被 fa姐姐 拉来验题,脑袋昏昏忘记了这个结论,只能再口胡一个铁憨憨做法。

注意到所求的 border 一定既是第一个字符串的后缀,又是第二个字符串的后缀,因此一定是两个字符串的公共后缀 ,同时注意到由于这两个字符串的前缀是相同的,所以如果一个字符串 T 既是其中任意一个串的 border,又是两个串的公共后缀,那么它一定是两个串的公共 border。并且这个条件显然也是必要条件,因此我们在求出两串的 lcp 以后只需要在其中任意一个串上找到其最长的长度不超过 lcp 长度的 border,那么该串即为两串的最长公共 border

假设我们已经求出了两串的 lcp 长度,那么问题就只剩下对一个字符串求其最长的长度不超过某数的 border

我们考虑对每个前缀,将它向它的最长 border 连一条边,那么显然这个图有 (n+1) 个节点, n 条边,又因为这个图是联通的,根据树的判定定理,这个图是一棵树,若规定 0 是这棵树的根,数学归纳可得每个节点的父节点为该节点所代表的前缀的最长 border。因为一个节点的 border 显然比该节点的长度小,所以任何一个节点到根所在的链上,若将节点按深度从小到大排列,则其所代表的前缀长度一定是单调递增的。因此我们只需要对整棵树进行 dfs,同时用一个栈维护当前节点到根的链,然后在栈里二分即可找到所求的串。

border 的方法见 【P3375】KMP字符串匹配

而求两个前缀的 lcp,可以对原串建立一个 SAM,两个前缀在 parent 树上所对应节点的 LCA 即为他们的 lcp。也可以将原串反过来,转化为求两个后缀的最长公共前缀,求出 SA 后用 height 数组解决。

但是扶苏既不愿意将原串反过来求 SA 在写个 ST,也担心毒瘤出题人卡了空间以后 SAM 建出来会爆空间,因此扶苏选择了 二分+hash 求出其 lcp

显然公共后缀的长度满足二分性,因此只要选择一个满足前缀可减性的 hash 函数就可以 O(1) check 了。

考虑时间复杂度:二分求 lcp 的复杂度是 O(mlogn),在 border 树上二分的复杂度是 O(mlogn),因此总时间复杂度 O(n+mlogn)

Code

本来扶苏写了个四模数 hash,然后被卡常了就尝试减少模数个数,最后发现单模数就可以了(雾

#include <cstdio>
#include <vector>
#include <algorithm>

const int maxh = 4;
const int maxm = 100005;
const int maxn = 1000005;

const int MOD[] = {998244353, 1000000007, 1000000009, 1145141};

int n, m, top = -1;
char S[maxn];
int border[maxn], ans[maxm], stk[maxn];
std::vector<int> son[maxn], query[maxn];

struct HASH {
  int md;
  ll hash[maxn], inv[maxn];

  ll mpow(const int a, int d, const int p) {
    ll ret = 1, tmp = a;
    while (d) {
      if (d & 1) {
        (ret *= tmp) %= p;
      }
      (tmp *= tmp) %= p;
      d >>= 1;
    }
    return ret;
  }

  void build(const int x) {
    md = x;
    ll tmp = 1, iv = mpow(100, x - 2, x);
    inv[0] = 1;
    for (int i = 1; i <= n; ++i) {
      hash[i] = (hash[i - 1] + (S[i] - 'a') * tmp) % md;
      inv[i] = inv[i - 1] * iv % md;
      (tmp *= 100) %= md;
    }
  }

  bool check(const int x, const int y, const int len) {
    ll h1 = (hash[x] - hash[x - len]) * inv[x - len] % md, h2 = (hash[y] - hash[y - len]) * inv[y - len] % md;
    if (h1 < 0) h1 += md;
    if (h2 < 0) h2 += md;
    if (h1 != h2) {
      return false;
    } else {
      return true;
    }
  }
};
HASH h[maxh];

int ReadStr(char *p);
void dfs(const int u);

int main() {
  freopen("1.in", "r", stdin);
  n = ReadStr(S);
  for (int i = 0; i < maxh; ++i) {
    h[i].build(MOD[i]);
  }
  for (int i = 2, j = 0; i <= n; ++i) {
    while (j && (S[j + 1] != S[i])) {
      j = border[j];
    }
    if (S[j + 1] == S[i]) {
      ++j;
    }
    son[border[i] = j].push_back(i);
  }
  son[0].push_back(1);
  qr(m);
  for (int p, q, Ans, i = 1; i <= m; ++i) {
    p = q = Ans = 0; qr(p); qr(q);
    for (int l = 1, r = std::min(p, q) - 1, mid = (l + r) >> 1; l <= r; mid = (l + r) >> 1) {
      bool flag = true;
      for (int i = 0; i < maxh; ++i) if ((flag = h[i].check(p, q, mid)) == false) {
        break;
      }
      if (flag) {
        l = (Ans = mid) + 1;
      } else {
        r = mid - 1;
      }
    }
    ans[i] = Ans;
    query[std::min(p, q)].push_back(i);
  }
  dfs(0);
  for (int i = 1; i <= m; ++i) {
    qw(ans[i], '\n', true);
  }
  return 0;
}

int ReadStr(char *p) {
  auto beg = p;
  do *(++p) = IPT::GetChar(); while ((*p >= 'a') && (*p <= 'z'));
  *p = 0;
  return p - beg - 1;
}

void dfs(const int u) {
  stk[++top] = u;
  for (auto v : query[u]) {
    int w = ans[v]; ans[v] = 0;
    for (int l = 1, r = top, mid = (l + r) >> 1; l <= r; mid = (l + r) >> 1) if (stk[mid] <= w) {
      ans[v] = stk[mid];
      l = mid + 1;
    } else {
      r = mid - 1;
    }
  }
  for (auto v : son[u]) {
    dfs(v);
  }
  --top;
}

posted @   一扶苏一  阅读(373)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示