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\),求:

\[\sum_{L \leq i < R} f(i, 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;
}
posted @ 2021-04-07 15:55  duyiblue  阅读(272)  评论(0编辑  收藏  举报