LOJ2313 「HAOI2017」供给侧改革
LOJ2313 「HAOI2017」供给侧改革
题目大意
有一个随机生成的、长度为 \(n\) 的 \(01\) 串 \(S\)。
- 定义 \(\mathrm{suf}(i) = S[i, n]\),即以 \(i\) 开头的后缀。
- 定义 \(\mathrm{lcp}(i, j) = \max \{k\mid 0\leq k\leq \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 < j\leq r\}\)。注意此处 \(i, j\) 不能相等。
\(q\) 次询问,每次给出 \(L, R\),求:
数据范围:\(1\leq n, q\leq 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 j\geq 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 j\in(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}(n\log n)\) 的。
为了维护 \(f\),我们需要一个数据结构,支持区间(一段前缀)对某个值取 \(\max\);区间求和。线段树就可以胜任,时间复杂度 \(\mathcal{O}(n\log^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}(n\log 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;
}