zoukankan      html  css  js  c++  java
  • 【做题记录】后缀数组应用

    本文中用 (he) 代替 (height)(he_i=operatorname{lcp}(sa[i],sa[i-1]))。若无特殊说明,时间复杂度指除去求后缀数组的时间复杂度。

    【模板】后缀排序

    不同子串个数

    (sa[i])(n-i+1) 个前缀,与 (sa[i-1]) 重复了 (he_i) 个,对答案的贡献就是 (n-i+1-he_i)

    (sumlimits_{i=1}^n n-i+1-he_i=n^2+n-dfrac{n(n+1)}{2}-sum he_i=dfrac{n(n+1)}{2}-sum he_i)

    [USACO5.1] 乐曲主题Musical Themes

    题意:求出现至少两次(不可重叠)的子串的最长长度。

    转调其实就是让我们求一个差分数组。

    考虑二分答案 (mid),如果 (he) 中存在都不小于 (mid) 的连续段 ([L,R]),此段中原串中最左边的位置(原串中,下同)与最右边的位置之差 (>=mid)(max{sa[L-1..R]}-min{sa[L-1..R]}>=mid)(此题中应为 (>mid),因为若在差分数组中刚好相邻,原串中就会有重叠),说明(mid)合法。时间复杂度 (operatorname{O}(nlog n))

    [AHOI2013] 差异

    先不管 lcp。长度为 (l) 的后缀出现在 (T_i) 的次数是 (l-1),出现在 (T_j) 的次数是 (n-l),那么总和是 (sumlimits_{l=1}^nl(l-1+n-l)=(n-1)sumlimits_{l=1}^nl=frac{1}{2}n(n-1)(n+1))

    接下来要求两两后缀的 lcp。我们知道 (operatorname{lcp}(i,j)=minlimits_{rnk_i<kle rnk_j}{he_k}),但即使用 st 表也是 (operatorname{O}(n^2)) 的。考虑每个 (he_i) 的贡献。用 单调栈 求出 (L_i,R_i),分别是 (i) 往前数、往后数第一个 (he) 值小于 (he_i)的,此时 (minlimits_{L_i<k<R_i}{he_k}=he_i),那么 (he_i) 对答案的贡献就是 (he_i(R_i-i)(i-L_i))。在原串中相当于 (sa[L_i..i-1])(sa[i..R_i-1]) 两两配对,(operatorname{lcp}) 都是 (he_i)。这一部分时间复杂度 (operatorname{O}(n))

    [USACO06DEC] Milk Patterns G

    题意:求字符串中出现至少 (k) 次(可重叠)的子串的最长长度。

    假设 (he) 中有区间 ([L,R])(代表原串中 (sa[L-1..R])),要求出现至少 (k) 次,需满足 (R-(L-1)+1ge k)(R-L+1ge k-1)。如果区间 ([L,R]) 向左或向右扩展一点,(operatorname{lcp}) 一定不会增大。所以只需要对所有长度为 (k-1) 的区间的 (he) 最小值求最大值即可,用单调队列 (operatorname{O}(n)),st 表(operatorname{O}(nlog n))

    Long Long Message

    题意:求两字符串的最长公共子串。

    用 SA 解决多串问题时,可以考虑把字符串首尾相连。用特殊字符隔开(每个特殊字符互不相同)可以避免 lcp 横跨两个字符串的问题。

    假设连接后字符串为 (S[1..n]),分隔字符下标为 (sep),题目转化成求 (maxlimits_{i<sep<j}{operatorname{lcp}(i,j)})。根据排名离得越远,(operatorname{lcp}) 不会更大,可知排名相邻(且来自不同字符串)的两个后缀的 (operatorname{lcp}) 才有可能是答案。

    [SDOI2008]Sandy 的卡片

    题意:求多个字符串的最长公共子串(此题需先差分)。

    沿用上题的套路,把所有字符串拼接在一起(字符串间需用不同字符隔开),并记录第 (i) 个字符对应第几个字符串,记为 (id_i)

    然后二分答案,判定的标准是 (he) 中存在一段连续的、大于 (mid) 且每个字符串都在 (id) 中出现过(这个用桶来做)。

    [SDOI2016]生成魔咒

    题意:初始时有一空串,每次在末尾加入一个字符,并求出当前串的不同子串数。

    SA 中插入不太好搞,反过来想,把字符串翻转,每次从开头删去一个字符。

    如果一个字符被删去,我们称以这个字符开头的后缀被删去。记 (pre_i) 为排名在 (i) 之前且最大的后缀排名,(nxt_i) 同理(模拟一个链表)。求出 (he) 后,我们把 (he_i) 的定义改为 (operatorname{lcp}(pre_i,i))

    前面提到过长度为 (n) 的字符串的不同子串数为 (dfrac{n(n+1)}{2}-sum he_i)。维护一下当前的 (sum he_i),删去排名为 (i) 的后缀就等同于在链表中删去第 (i) 个节点。同时维护一下 (he_{nxt_i})(显然只会对这个产生影响)。

    【模板】后缀自动机(SAM)

    题意:求子串长度 ( imes) 出现次数的最大值。

    首先不难想到这个子串的长度一定是 (he_i) 之一。

    那么就可以用单调栈求出排名为 (i) 的子串出现了多少次。


    以下题目都有一定难度,请谨慎食用

    P3975 [TJOI2015]弦论

    题意:求第 (k) 小子串,分相同子串算一个 / 多个 两种情况。

    相同子串算一个的很好处理,这里就不说了。

    相同子串算多个的情况用 SA 做确实有点复杂 不过既然写在这里肯定是可以哒

    排名为 (i) 的后缀,有 (n-sa_i+1) 个前缀

    再对这东西做一个前缀和,记为 (sum_i)

    然后建一个 st 表。

    考虑对 (he) 分组,要求每组只有第一个 (he)(0)

    (he=0) 代表这两个后缀第一个字母都不相同,显然它们不会有任何前缀是相同的。那么就可以判断答案在哪个组里。

    设这个组的左右端点(后缀排名)分别是 (L, R),要求的排名是 (k)

    先找到区间内 (min{operatorname{lcp}}) 的位置,记为 (mid)

    容易发现排名在 ([L,R]) 中的后缀,它们长度为 (he_{mid}) 的前缀都相同

    于是可以判断答案是否在这些子串里,即判断 (k) 是否 (leq (R-L+1) imes he_{mid})。如果是,进而可以推出长度是多少。

    如果不是,判断答案在 (mid) 左还是右(除去这些长度为 (he_{mid}))的子串

    然后更新一下 (k,L,R) 就好了。

    注意要记录上一次的 (he_{mid}),然后处理一些细节。

    每次区间长度至少减少 (1),这一部分时间复杂度 (O(n))

    inline int get_min(int x, int y) {
    	return he[x] < he[y] ? x : y;
    }
    
    /*
    SA
    */
    
    void solve0() {
    	rep(i, 1, n) {
    		if(k > n - sa[i] + 1 - he[i]) k -= n - sa[i] + 1 - he[i];
    		else {
    			print(sa[i], sa[i] + he[i] + k - 1);
    			return ;
    		}
    	}
    	puts("-1");
    }
    
    void build_st() {
    	rep(i, 1, n) sum[i] = sum[i - 1] + n - sa[i] + 1;
    	rep(i, 2, n) lg[i] = lg[i / 2] + 1;
    	rep(i, 1, n) st[i][0] = i;
    	rep(len, 1, lg[n]) rep(i, 1, n - (1 << len) + 1)
    		st[i][len] = get_min(st[i][len - 1], st[i + (1 << len - 1)][len - 1]);
    }
    
    inline ll get_sum(int l, int r) {
    	return sum[r] - sum[l - 1];
    }
    
    inline int query(int l, int r) {
    	l ++ ;
    	int k = lg[r - l + 1];
    	return get_min(st[l][k], st[r - (1 << k) + 1][k]);
    }
    
    void solve1() {
    	int l = 1, r, mid, now = 0;
    	ll tmp;
    	for(; l <= n; ++ l)
    		if(he[l] == 0) {
    			for(r = l; he[r + 1] > 0; ++ r) ;
    			if(k <= get_sum(l, r)) break;
    			k -= get_sum(l, r);
    			l = r;
    		}
    	while(l < r) {
    		mid = query(l, r);
    		tmp = 1ll * (r - l + 1) * (he[mid] - now); // 注意计算个数时要减去上一次的长度
    		if(he[mid] > now && k <= tmp) {
    			print(sa[l], sa[l] + now + (k - 1) / (he[mid] - now));
    			return ;
    		}
    		k -= tmp;
    		tmp = get_sum(l, mid - 1) - 1ll * he[mid] * (mid - l);
    		if(k <= tmp) r = mid - 1;
    		else k -= tmp, l = mid;
    		now = he[mid];
    	}
    	print(sa[l], sa[l] + now + k - 1);
    }
    
    signed main() {
    	scanf("%s", s + 1);
    	n = strlen(s + 1);
    	scanf("%d%d", &T, &k);
    	SA();
    	if(!T) solve0();
    	else if(1ll * n * (n + 1) / 2 < k) puts("-1");
    	else {
    		build_st();
    		solve1();
    	}
    	return 0;
    }
    

    [HEOI2016/TJOI2016]字符串

    考虑二分答案。设当前答案为 (len)

    那么要判断是否存在一个后缀 (p),使得 (ale ple b-len+1)(operatorname{lcp}(p,c)ge len)

    考虑一个后缀的排名 (k)

    (k<rnk_c) 时,如果 (k) 越小,(minlimits_{k<ile rnk_c}{he_i}) 单调不增。

    (k>rnk_c) 时同理。

    那么,使得 (operatorname{lcp}(p,c)ge len) 的后缀 (p)排名 一定是一段区间。可以用 st 表加二分求出这个排名区间。

    把下标看成第一维,排名看成第二维,变成了一个二维数点问题。再用一个主席树即可。

    时间复杂度 (O(nlog^2n))

    int build(int l, int r) {
    	int p = ++ tot;
    	if(l == r) return p;
    	int mid = l + r >> 1;
    	lc[p] = build(l, mid);
    	rc[p] = build(mid + 1, r);
    	return p;
    }
    
    int modify(int rt, int l, int r, int t) {
    	int p = ++ tot;
    	sum[p] = sum[rt] + 1;
    	if(l == r) return p;
    	int mid = l + r >> 1;
    	if(t <= mid) {
    		lc[p] = modify(lc[rt], l, mid, t);
    		rc[p] = rc[rt];
    	} else {
    		rc[p] = modify(rc[rt], mid + 1, r, t);
    		lc[p] = lc[rt];
    	}
    	return p;
    }
    
    int query(int p, int l, int r, ci &tl, ci &tr) {
    	if(tl <= l && r <= tr) return sum[p];
    	int mid = l + r >> 1, res = 0;
    	if(tl <= mid) res += query(lc[p], l, mid, tl, tr);
    	if(mid < tr) res += query(rc[p], mid + 1, r, tl, tr);
    	return res;
    }
    
    inline int query(int x, int y, int l, int r) {
    	return query(rt[y], 1, n, l, r) - query(rt[max(x - 1, 0)], 1, n, l, r);
    }
    
    void solve() {
    	int a, b, c, d, rnkL, rnkR, lenl, lenr, len, ans = 0;
    	scanf("%d%d%d%d", &a, &b, &c, &d);
    	lenl = 0; lenr = min(d - c + 1, b - a + 1);
    	while(lenl <= lenr) {
    		len = lenl + lenr >> 1;
    		rnkL = rnkR = rnk[c];
    		per(i, lg[rnk[c] - 1], 0)
    			if(lcp(rnkL - (1 << i), rnk[c]) >= len)
    				rnkL -= 1 << i;
    		per(i, lg[n - rnk[c] + 1], 0)
    			if(lcp(rnk[c], rnkR + (1 << i)) >= len)
    				rnkR += 1 << i;
    		if(query(a, b - len + 1, rnkL, rnkR)) ans = len, lenl = len + 1;
    		else lenr = len - 1;
    	}
    	printf("%d
    ", ans);
    }
    
    signed main() {
    	scanf("%d%d", &n, &q);
    	scanf("%s", s + 1);
    	SA();
    	get_height();
    	build_st();
    	rt[0] = build(1, n);
    	rep(i, 1, n) rt[i] = modify(rt[i - 1], 1, n, rnk[i]);
    	for(; q; -- q) solve();
    	return 0;
    }
    
  • 相关阅读:
    系统设计的一些原则
    分层开发思想与小笼包
    工作与生活
    Microsoft .NET Pet Shop 4 架构与技术分析
    用人之道(二) 如何管理软件开发团队
    也谈很多开发人员的毛病
    《3S新闻周刊》第10期,本期策划:“超女”营销带来的启示
    浅析ArcIMS
    MapX的坐标问题
    应用ArcIMS构建GMap风格的地图应用
  • 原文地址:https://www.cnblogs.com/creating-2007/p/14809428.html
Copyright © 2011-2022 走看看