zoukankan      html  css  js  c++  java
  • LOJ2313 「HAOI2017」供给侧改革

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

    [sum_{L leq i < R} f(i, 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;
    }
    
  • 相关阅读:
    语义化单单的限定在html么?
    转WEB前端开发经验总结(5)
    JavaScript中的null和undefined
    文字上右下环绕广告的写法
    转自森林:最新CSS兼容方案
    转自森林:注释书写规范 Ghost
    【探讨】栈和队列
    转自森林:你是一个职业的页面重构工作者吗?
    Web标准:IE8新特性及IE8安装使用
    转载:09年腾讯校园招聘页面重构的2道面试题
  • 原文地址:https://www.cnblogs.com/dysyn1314/p/loj2313.html
Copyright © 2011-2022 走看看