[CmdOI2019]口头禅
猫树好题!!1
Description
给定 \(n\) 个模式串 \(s_i\) , \(q\) 个询问,求区间内所有字符串最长公共子串长度。
\(n \leq 2 \cdot 10 ^ 4,\ \ q\leq 10 ^ 5,\ \ \sum s_i \leq 4 \cdot 10 ^ 5\)
Analysis
区间 LCS2 ,并且没有修改。
Solution
看到 LCS2 的做法,好像这并不太能快速合并,似乎就用不了线段树之类的数据结构,但是发现没有修改。
考虑猫树。
那就对于一段区间 \([l,\ r]\) 里的所有字符串,以中点的串为基准串,向两边处理 LCS2 中的我们要处理的前缀答案。
但是我们发现如果以中点的串为基准串的话,一旦分到了一个比较长的串时间会退化的有点严重,所以又要有另外一个做法。
倍增分治,规定一个阈值 \(x\) ,把区间内所有长度在 \(2^{x}\) 到 \(2 ^ {x + 1}\) 里的所有字符串找出来,然后再取中点的串作为基准串去算。
然后继续分治。
看起来很时间很玄学,但是因为有 \(\sum len\) 的约束,所以这个部分的串数量是 \(\frac{\sum len}{2 ^ x}\) 的,并且整个区间的字符串只会贡献 \(\log n\) 次,基准串长度是 \(2 ^ x\) ,所以总体时间是 \(O(2 ^ x \cdot \frac{\sum len}{2 ^ x} \cdot \log n) = O(\sum len \cdot \log n)\) 的,总共 \(\log \sum len\) 层,所以总时间就是 \(O(\sum len \cdot \log n \cdot \log \sum len)\) 。
好办了,但是空间不允许,所以改一下,改成猫树分治,也就是说我们要在预处理猫树的时候就要算好对应的查询答案。
直接每次找对应的区间计算基准串的答案就行了,看起来很暴力,所以来分析一下时间:
每个区间只会被遍历 \(\log n\) 次;
其次,回想猫树查询的做法,是对所有左端点在左遍历区间,右端点在右遍历区间的区间们求得答案,然后在这个题目中一个区间的查询时间是遍历区间的基准串长度(因为是对于基准串每个位置答案求 \(\max\) )。
发现这个基准串在这个遍历区间中是属于长度最小的几个字符串之一,我们可以默认为 \(\min_{i = l} ^ r len(s_i)\) 。所以加入区间没有重复的话,要求总时间尽可能大的话(需要稍微意会一下),设总操作数是 \(q\) ,大概有这样的一个式子(默认 \(t_i\) 是 \(s_i\) 长度从大到小排序):
又因为有 \(\sum len\) 的约束,所以上式最大是在 \(O(n \cdot \sqrt{q})\) 。
总时间复杂度 \(O(\sum len \cdot \log n \cdot \log \sum len + n \cdot \sqrt{q})\) 。
Code
Code
/*
*/
#include
using namespace std;
typedef long long ll;
const int N = 2e4 + 10, M = 1e5 + 10, S = 3e6 + 10;
int n, m, len[N], cnt, pos[N], p[M], k, ip[M], ans[M];
char sp[S], *str[N], *st = sp;
int slen[S];
inline int read() {
char ch = getchar();
int s = 0, w = 1;
while (!isdigit(ch)) {if (ch == '-') w = -1; ch = getchar();}
while (isdigit(ch)) {s = (s << 3) + (s << 1) + (ch ^ 48); ch = getchar();}
return s * w;
}
struct mdzz {int l, r;} q[M];
struct SAM_mdzz {int len, link, ch[2];} a[S];
struct SAM {
int rt, las;
inline void init() {rt = las = ++cnt; memset(a[cnt].ch, 0, sizeof(a[cnt].ch));}
inline void SAM_stru(int c) {
int cur = ++cnt, p = las;
memset(a[cur].ch, 0, sizeof(a[cur].ch));
las = cur;
a[cur].len = a[p].len + 1;
while (p && !a[p].ch[c]) a[p].ch[c] = cur, p = a[p].link;
if (!p) {a[cur].link = rt; return ;}
int q = a[p].ch[c];
if (a[p].len + 1 == a[q].len) {a[cur].link = q; return ;}
int clo = ++cnt;
a[clo] = a[q];
a[clo].len = a[p].len + 1;
a[q].link = a[cur].link = clo;
while (p && a[p].ch[c] == q) a[p].ch[c] = clo, p = a[p].link;
}
} s[N];
struct immortalCO {
#define lson l, mid
#define rson mid + 1, r
int *f[N], *g;
inline void solve(int it, int mid, int sig) {
int v = s[it].rt, u = v, lth = 0;
for (int i = 0, c; i < len[mid]; ++i) {
c = str[mid][i] - '0';
if (!a[v].ch[c]) {
while (v != u && !a[v].ch[c]) v = a[v].link;
lth = a[v].len;
}
if (a[v].ch[c]) v = a[v].ch[c], ++lth;
f[it][i] = min(f[it + sig][i], lth);
}
}
inline void build(int lim, int l, int r, int L, int R) {
if (L > R) return ;
int pk = 0, ik, mid;
while (2333) {
for (int i = l; i <= r; ++i) {
if (len[i] <= lim) pos[++pk] = i;
}
if (pk) break;
lim <<= 1;
}
mid = pos[(pk + 1) >> 1];
g = slen;
for (int i = l; i <= r; ++i) {
f[i] = g, g += len[mid] + 1;
}//
for (int i = 0; i < len[mid]; ++i) f[mid][i] = i + 1;
for (int i = mid - 1; i >= l; --i) solve(i, mid, 1);
for (int i = mid + 1; i <= r; ++i) solve(i, mid, -1);
pk = L - 1; ik = 0;
for (int i = L, u; i <= R; ++i) {
u = p[i];
if (q[u].r < mid) p[++pk] = u;
else if (q[u].l > mid) ip[++ik] = u;
else {
int res = 0;
for (int j = 0; j < len[mid]; ++j) {
res = max(res, min(f[q[u].l][j], f[q[u].r][j]));
}
ans[u] = res;
}
}
for (int i = 1; i <= ik; ++i) p[i + pk] = ip[i];
R = pk + ik;
build(lim, l, mid - 1, L, pk); build(lim, mid + 1, r, pk + 1, R);
}
#undef lson
#undef rson
} ct;
int main() {
n = read(); m = read();
for (int i = 1; i <= n; ++i) {
scanf("%s", str[i] = st);
len[i] = strlen(str[i]);
s[i].init();
for (int j = 0; j < len[i]; ++j)
s[i].SAM_stru(str[i][j] - '0');
st += len[i];//
}
for (int i = 1; i <= m; ++i) {
q[i] = (mdzz) {read(), read()}; p[i] = i;
}
ct.build(10, 1, n, 1, m);
for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
return 0;
}