[CmdOI2019] 口头禅 题解
一道综合性相当强的题目,用到了许多算法。
首先看到公共子串可以想到广义 SAM,先建出广义 SAM,在每个节点处记录它被那些串覆盖过,当询问 \([l,r]\) 时,即查找至少被 \([l,r]\) 区间内所有串覆盖的节点的 \(\mathrm{len}\) 的最大值,问题就是处理每个字串被那些区间覆盖以及快速查询。
回忆我们建广义 SAM 的过程,一共分为两步,首先是建出字典树,然后再连接 \(\textrm{fail}\),考虑在这两部分中分别处理,首先我们可以用一个 set
用类似珂朵莉树的方法维护区间集合,此时插入单个区间并完成合并可以做到均摊 \(\mathcal O\left(\log n\right)\),在建字典树的过程中,我们可以先给所有的字典树中的节点维护好,以便我们下一步操作,此时的总时间复杂度是 \(\mathcal O\left(\sum|S|\log n\right)\)。
下一步就是建 \(\mathrm{fail}\) 指针,直接边建边维护显然不太对,因为我们一个点被覆盖的区间是 \(\mathrm{fail}\) 树的子孙及自己的区间并,所以可以先把 \(\mathrm{fail}\) 指针全部建好,然后沿叶子到根自底向上拓扑维护,一般使用线段树合并,但是这里前面用的是珂朵莉树,且此处是要取出所有的区间,线段树合并不好维护,可以考虑启发式合并,即每次将小的往大的合并,无非会破坏儿子的集合,之后处理的时候我们就会发现无所谓,时间复杂度证明是老套路,每对时间复杂度有一次贡献就会让所在集合大小翻倍,没几次就到上界了,如果写 Splay 时间复杂度似乎能压到 \(\mathcal O\left(\sum|S|\log n\right)\),但是因为插入的操作关系到区间合并,直接用 set
更加简单,所以用 set
实现,时间复杂度是 \(\mathcal O\left(\sum|S|\log^2n\right)\)。
接下来就是解决另一个难题,即快速查询,考虑将所有询问离线下来用线段树维护,将所有询问按左端点放在线段树叶子结点上,每个叶子节点上所有询问按右端点从小到大排序,我们发现在广义 SAM 上自底向上维护区间集合的过程中 \(\mathrm{len}\) 单调递减,即如果一个节点的区间包含了一个询问,则这个询问可以直接被回答,基于这个性质,每次我们处理完一个节点对应的区间集合,把每个区间扔到线段树上询问,我们上面的维护方法保证了在线段树上当前区间被询问区间包含后,只有当子树内询问右端点最小值比询问区间右端点小时才往下继续走,每次走到叶子一定能回答至少一个询问,时间复杂度得到保证。
最后时间复杂度就是 \(\mathcal O\left(\sum|S|\log^2n+m\log n\right)\),记得回收 set
的空间,稍微有点卡常,开了 O2 后可过。
代码
#include<bits/stdc++.h>
using namespace std;
#define Reimu inline void // 灵梦赛高
#define Marisa inline int // 魔理沙赛高
#define Sanae inline bool // 早苗赛高
typedef long long LL;
typedef unsigned long long ULL;
typedef pair<int, int> Pii;
typedef tuple<int, int, int> Tiii;
#define fi first
#define se second
template<typename Ty>
Reimu clear(Ty &x) { Ty _; x.swap(_); }
const int N = 20010, M = 100010, K = 400010;
int n, m;
int ans[M];
struct SegTree {
int mn[N << 2];
vector<Pii> S[N];
Reimu insert(int L, int R, int id, int p = 1, int l = 1, int r = n) {
if (l == r) return void(S[L].emplace_back(R, id));
int mid = l + r >> 1, lp = p << 1, rp = lp | 1;
if (L <= mid) insert(L, R, id, lp, l, mid);
else insert(L, R, id, rp, mid + 1, r);
}
Reimu build(int p = 1, int l = 1, int r = n) {
if (l == r) return sort(S[l].rbegin(), S[l].rend()), mn[p] = S[l].empty() ? INT_MAX : S[l].back().fi, void();
int mid = l + r >> 1, lp = p << 1, rp = lp | 1;
build(lp, l, mid); build(rp, mid + 1, r);
mn[p] = min(mn[lp], mn[rp]);
}
Reimu query(int L, int R, int res, int p = 1, int l = 1, int r = n) {
if (l == r) {
while (!S[l].empty() && S[l].back().fi <= R) ans[S[l].back().se] = res, S[l].pop_back();
mn[p] = S[l].empty() ? INT_MAX : S[l].back().fi;
return;
}
int mid = l + r >> 1, lp = p << 1, rp = lp | 1;
if (L <= mid && R >= mn[lp]) query(L, R, res, lp, l, mid);
if (R >= mn[rp]) query(L, R, res, rp, mid + 1, r);
mn[p] = min(mn[lp], mn[rp]);
}
} sgt;
Reimu push(set<Pii> &S, int l, int r) {
if (S.empty()) S.emplace(l, r);
auto it = S.upper_bound({r + 1, INT_MAX});
if (it == S.begin() || (--it)->se < l - 1) return void(S.emplace(l, r));
if (it->fi <= l && it->se >= r) return;
while (it->se >= l - 1) {
l = min(l, it->fi); r = max(r, it->se);
it = S.erase(it);
if (it == S.begin()) break;
--it;
}
S.emplace(l, r);
}
Reimu merge(set<Pii> &A, set<Pii> &B) {
if (A.size() < B.size()) A.swap(B);
for (auto [l, r]: B) push(A, l, r);
clear(B);
}
struct SAM {
#define fail(p) tr[p].fail
#define ch(p, o) tr[p].ch[o]
int sz = 1;
int c[K], id[K << 1];
set<Pii> S[K << 1];
struct Node { int len, fail, ch[2]; } tr[K << 1];
Reimu insert(string s, int k) {
int p = 1;
for (char c: s) {
if (!ch(p, c & 1)) ch(p, c & 1) = ++sz;
push(S[p = ch(p, c & 1)], k, k);
}
}
Reimu insert(int p, int o) {
int las = ch(p, o);
tr[las].len = tr[p].len + 1;
for (p = fail(p); p && !ch(p, o); p = fail(p)) ch(p, o) = las;
if (p) {
int q = ch(p, o);
if (tr[q].len ^ tr[p].len + 1) {
tr[++sz] = {tr[p].len + 1, fail(q)};
for (int o: {0, 1}) if (tr[ch(q, o)].len) ch(sz, o) = ch(q, o);
fail(las) = fail(q) = sz;
while (p && ch(p, o) == q) ch(p, o) = sz, p = fail(p);
} else fail(las) = ch(p, o);
} else fail(las) = 1;
}
Reimu build() {
queue<int> Q; Q.emplace(1);
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int o: {0, 1}) if (ch(x, o)) insert(x, o), Q.emplace(ch(x, o));
}
}
Reimu solve() {
for (int i = 1; i <= sz; ++i) ++c[tr[i].len];
for (int i = 1; i <= sz; ++i) c[i] += c[i - 1];
for (int i = 1; i <= sz; ++i) id[c[tr[i].len]--] = i;
for (int i = sz; i; --i) {
int k = id[i];
for (auto [l, r]: S[k]) if (l ^ r) sgt.query(l, r, tr[k].len);
merge(S[fail(k)], S[k]);
}
}
#undef fail
#undef ch
} sam;
int main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
string s; cin >> s;
sam.insert(s, i);
}
sam.build();
for (int i = 1, l, r; i <= m; ++i) cin >> l >> r, sgt.insert(l, r, i);
sgt.build(); sam.solve();
for (int i = 1; i <= m; ++i) cout << ans[i] << '\n';
return 0;
}