LOJ2313 「HAOI2017」供给侧改革
题目大意
有一个随机生成的、长度为 (n) 的 (01) 串 (S)。
- 定义 (mathrm{suf}(i) = S[i, n]),即以 (i) 开头的后缀。
- 定义 (mathrm{lcp}(i, j) = max {kmid 0leq kleq min{n - i + 1, n - j + 1}, S[i,i + k - 1] = S[j, j + k - 1]})。
- 定义 (f(l, r) = max{mathrm{lcp}(i, j)mid l leq i < jleq r})。注意此处 (i, j) 不能相等。
(q) 次询问,每次给出 (L, R),求:
数据范围:(1leq n, qleq 10^5)。
本题题解
求 (mathrm{lcp}),可以借助后缀树(即反串的 sam 的 parent tree):两后缀的 (mathrm{lcp}),就是它们在后缀树上对应节点的 (mathrm{lca}) 的最长串长度(以下简称为点权)。
因为 (S) 随机生成,有两个很好的性质:(1) 后缀树树高是 (mathcal{O}(log n)) 级别的;(2) 任意一对点的 (mathrm{lcp}) 是 (mathcal{O}(log n)) 级别的。
将询问离线。从小到大枚举右端点 (R)。将 (f(i, R)) 简写为 (f(i))。设 (g(i) = max{mathrm{lcp}(i, j) mid i < j leq R})。则 (f(i) = max{g(j) mid jgeq i}),也就是 (g) 数组的后缀最大值。从 (R - 1) 变化到 (R) 时,只需要让所有 (g(i)) ((i < R)) 对 (mathrm{lcp}(i, R)) 取 (max),那么相当于让 (f) 数组的 ([1, i]) 这段前缀对 (mathrm{lcp}(i, R)) 取 (max)。
记以位置 (i) 开头的后缀在后缀树上的节点为 (mathrm{pos}(i))。考虑每个 (i)。因为 (mathrm{lcp}(i, R)) 就是两节点 (mathrm{lca}) 的点权,所以 (g(i)) 的值,只会在 (mathrm{pos}(i)) 的祖先的点权里取到。某个祖先 (u),能贡献到 (g(i)) 里(令 (g(i)) 对 (u) 的点权取 (max)),当且仅当 (u) 子树里包含了一个 (j),形式化地说:(exist jin(i, R]) 使得 (mathrm{pos}(j)) 在 (u) 的子树里。为了保证是 (mathrm{lca}),其实这里本来应该要求 (mathrm{pos}(i)) 和 (mathrm{pos}(j)) 来自不同的儿子子树,但是因为是取 (max),比 (mathrm{lca}) 更高的祖先,点权一定更小,所以不会影响答案。可以这样理解整个过程:越高的祖先,点权越小,但越有可能包含 (j)(只要包含了某个 (j),就能贡献到 (g(i)) 里)。每次 (R) 变化时,相当于加入了一个 (j = R),随着 (j) 的加入,(i) 的祖先里包含 (j) 的节点就可以越降越低,因此 (g(i)) 越来越大。
从小到大枚举 (R),对所有已经扫描过的 (i)(也就是 (i < R)),将 (i) 挂在 (mathrm{pos}(i)) 的所有祖先上,记节点 (u) 上挂的 (i) 的集合为 (S(u)),因为后缀树树高为 (mathcal{O}(log n)) 级别,所以每次暴力挂上去就好。
对每个 (R),可以用它去更新一些 (i) 的 (g(i)) 的值。考虑枚举 (i) 和 (R) 的公共祖先(前面说过,因为是取 (max),所以不必保证是最近公共祖先)。具体来说就是访问 (R) 的所有祖先,记为 (u),考虑 (S(u)) 里的每个 (i):令 (g(i)) 对 (u) 的点权取 (max)(对 (f) 的影响是让前缀 ([1, i]) 对它取 (max)),然后就可以将 (i) 从 (S(u)) 里删掉了(因为 (u) 的点权已经向 (g(i)) 里贡献过了,之后显然不会再影响 (g(i)))。更准确地说,我们访问完成后,会将 (R) 的所有祖先的 (S(u)) 清空。因为每个 (i) 只会在它的所有祖先里被加入一次,访问一次并直接被删除,所以总访问量是 (mathcal{O}(nlog n)) 的。
为了维护 (f),我们需要一个数据结构,支持区间(一段前缀)对某个值取 (max);区间求和。线段树就可以胜任,时间复杂度 (mathcal{O}(nlog^2 n))。
但注意到我们要取 (max) 的值(也就是 (mathrm{lcp}) 长度)是 (mathcal{O}(log n)) 级别的,并且只会对一段前缀(而不是任意区间)操作,所以有更好的方法。对每个值 (v),维护它能贡献到的最大位置,记为 (p(v))。一次修改操作,假设是让 ([1, i]) 对 (v) 取 (max),则我们直接让 (p(v)) 对 (i) 取 (max)。询问时,从大到小枚举 (v),记前面所有(更大的)(v) 的 (p(v)) 的最大值为 (t),若当前 (p(v)leq t),则对答案无贡献;否则说明 ([t + 1, p(v)]) 的这段 (f) 值为 (v),这样我们就以划分出 (mathcal{O}(log n)) 个等值连续段的方式,刻画出了 (f) 数组。此时求一段区间的和,自然也就非常简单了。
此外,注意到在我们转化后,我们只关心取到每个值的最大的 (i)。所以 (S(u)) 里不必存整个集合,只需要记录其中最大的 (i) 即可。
时间复杂度 (mathcal{O}(nlog n)),空间复杂度 (mathcal{O}(n))。
参考代码
// problem: P3732
#include <bits/stdc++.h>
using namespace std;
#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 1e5;
int n, m;
char s[MAXN + 5];
int ans[MAXN + 5];
vector<pii> vq[MAXN + 5];
int cnt, ed, mp[MAXN * 2 + 5][2], fa[MAXN * 2 + 5], len[MAXN * 2 + 5], pos[MAXN + 5];
void insert(int c, int idx) {
int p = ed;
ed = ++cnt;
pos[idx] = ed;
len[ed] = len[p] + 1;
for (; p && !mp[p][c]; p = fa[p]) {
mp[p][c] = ed;
}
if (!p) {
fa[ed] = 1;
} else {
int q = mp[p][c];
if (len[q] == len[p] + 1) {
fa[ed] = q;
} else {
++cnt;
len[cnt] = len[p] + 1;
mp[cnt][0] = mp[q][0], mp[cnt][1] = mp[q][1];
fa[cnt] = fa[q];
fa[q] = fa[ed] = cnt;
for (; mp[p][c] == q; p = fa[p]) {
mp[p][c] = cnt;
}
}
}
}
int val_max_pos[100], max_val;
int mxi[MAXN * 2 + 5];
void update_as_r(int idx) {
int u = pos[idx];
while (u) {
// 对于所有祖先, 更新这个祖先子树里所有 i 的答案
// u 节点上的每个 i, 对答案的更新, 相当于是让 data[1, i] 对 len[u] 取 max
// 故只需要保留 u 节点上最大的 i. 记为 mxi[u]
// 又因为数据随机, len[u] 很小, 故可以考虑对每个 len[u] 的值记录其出现的最大位置 i, 也就是 val_max_pos[len[u]] = i
if (mxi[u] != 0) {
ckmax(val_max_pos[len[u]], mxi[u]); // 每种值对应的最大的 i
ckmax(max_val, len[u]);
}
u = fa[u];
}
}
void insert_as_i(int idx) {
int u = pos[idx];
while (u) {
// 对于所有祖先, 在这个祖先子树里插入一个 i
ckmax(mxi[u], idx);
u = fa[u];
}
}
int query(int l) {
int lst = 0;
int res = 0;
for (int i = max_val; i >= 1; --i) {
if (val_max_pos[i] > lst) {
int cl = lst + 1, cr = val_max_pos[i];
// [cl, cr] 这段区间的值等于 i
if (cr >= l) {
if (cl < l) cl = l;
res += (cr - cl + 1) * i;
}
lst = val_max_pos[i];
}
}
return res;
}
int main() {
cin >> n >> m;
cin >> (s + 1);
for (int i = 1; i <= m; ++i) {
int l, r;
cin >> l >> r;
vq[r].push_back(mk(i, l));
}
cnt = ed = 1;
for (int i = n; i >= 1; --i) {
insert(s[i] - '0', i);
}
for (int i = 1; i <= n; ++i) {
update_as_r(i);
insert_as_i(i);
for (int _ = 0; _ < SZ(vq[i]); ++_) {
int id = vq[i][_].fi;
int l = vq[i][_].se;
ans[id] = query(l);
}
}
for (int i = 1; i <= m; ++i) {
cout << ans[i] << endl;
}
return 0;
}