省选模拟赛 T1 反攻密令 (后缀数组)

题意

给出 m m 和字符串 S S ,要求把 S S 分成 m m 部分使得每一部分的最大字典序子串中字典序最大的最小。并输出。
m 15 , S 1 0 5 m\le 15,|S|\le 10^5

思路

考试时想到直接二分所有子串,然后拿出来check,看至少要分成多少块才能保证每一块的最大子串小于等于二分到的串。如果至少需要的块数大于 m m 就不满足,否则满足,调整上下界继续二分。发现每一块的最大子串一定是一个后缀,所以送后往前 O ( n ) O(n) check,不满足就多分一块,就完了。

这也是正解的思路。

但是子串数量为 O ( n 2 ) O(n^2) ,所以就不会了。考试时就写了暴力+二分求LCP比大小。但是可能哪里写挂了或者是卡自然溢出,直接爆0。

下来看了题解说是用后缀数组找出所有本质不同的子串,然后二分。可怎么也没想明白怎么搞,子串数毕竟这么大。

然后跑去研究了一下后缀数组,用假说演绎法发现按后缀从小到大枚举,假设枚举到第 i i 小的后缀,那么左端点定位 s a [ i ] sa[i] ,右端点从 s a [ i ] + h e i g h t [ i ] sa[i]+height[i] 一直动到 n n 。这样输出出来惊喜地发现 就是所有不同子串从小到大排序。那么我们求一个 n ( s a [ i ] + h e i g h t [ i ] ) + 1 n-(sa[i]+height[i])+1 的前缀和,就可以通过二分快速找到第 k k 大子串了。那么我们二分子串的排名,后再二分一次就能得到对应的位置了。时间复杂度为 O ( n log n + log n ( log n + n ) ) = O ( log 2 n + n log n ) O(n\log n+\log n*(\log n+n))=O(\log^2n+n\log n)

前面的 ( n log n ) (n\log n) 是后缀数组的时间复杂度,中间的 log \log 是二分子串排名,最后的 log \log 是二分确定第 m i d mid 小子串的位置便于后面 O ( n ) O(n) 比较。

讲的有点抽象,看代码(其实代码也有点抽象)

CODE

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const int LOG = 18;
typedef long long LL;
int n, m, Lg[MAXN], x[MAXN], y[MAXN], sa[MAXN], rk[MAXN], c[MAXN], ht[MAXN], f[MAXN][LOG];
char s[MAXN];
LL ps[MAXN];

void Pre_Work(int n, int m) { //后缀数组板
	x[n+1] = s[n+1] = 0;
	for(int i = 1; i <= m; ++i) c[i] = 0;
	for(int i = 1; i <= n; ++i) ++c[x[i] = s[i]];
	for(int i = 2; i <= m; ++i) c[i] += c[i-1];
	for(int i = n; i >= 1; --i) sa[c[x[i]]--] = i;
	for(int k = 1; k <= n; k<<=1) {
		int p = 0;
		for(int i = n-k+1; i <= n; ++i) y[++p] = i;
		for(int i = 1; i <= n; ++i) if(sa[i] > k) y[++p] = sa[i]-k;
		for(int i = 1; i <= m; ++i) c[i] = 0;
		for(int i = 1; i <= n; ++i) ++c[x[i]];
		for(int i = 2; i <= m; ++i) c[i] += c[i-1];
		for(int i = n; i >= 1; --i) sa[c[x[y[i]]]--] = y[i];
		swap(x, y);
		x[sa[1]] = p = 1;
		for(int i = 2; i <= n; ++i)
			x[sa[i]] = (y[sa[i]] == y[sa[i-1]] && y[sa[i]+k] == y[sa[i-1]+k]) ? p : ++p;
		if((m=p) == n) break;
	}
	for(int i = 1; i <= n; ++i) rk[sa[i]] = i;
	for(int i = 1, k = 0, j; i <= n; ++i) if(rk[i] > 1) {
		j = sa[rk[i]-1]; k = k ? k-1 : 0;
		while(i+k <= n && j+k <= n && s[i+k] == s[j+k]) ++k;
		ht[rk[i]] = k;
	}
	Lg[0] = -1;
	for(int i = 1; i <= n; ++i) f[i][0] = ht[i], Lg[i] = Lg[i>>1] + 1;
	for(int j = 1; j < LOG; ++j)
		for(int i = 1; i <= n; ++i) if(i+(1<<j)-1 <= n)
			f[i][j] = min(f[i][j-1], f[i+(1<<(j-1))][j-1]);
}
inline int lcp(int l, int r) { //RMQ求最长公共前缀
	if(l == r) return n-l+1;
	l = rk[l], r = rk[r];
	if(l > r) swap(l, r); ++l;
	int t = Lg[r-l+1];
	return min(f[l][t], f[r-(1<<t)+1][t]);
}
void print(int l, int r) {
	for(int i = l; i <= r; ++i) putchar(s[i]);
	puts("");
}

bool cmp(int l, int r, int x, int y) { //比较字典序
	int len = min(lcp(l, x), min(r-l+1, y-x+1));
	if(len < r-l+1 && len < y-x+1) return s[l+len] < s[x+len];
	if(len == r-l+1 && len < y-x+1) return 1;
	return 0;
}

void get(int &l, int &r, LL k) {
	int L = 0, R = n-1, Mid; //通过在前缀和数组上二分找位置
	while(L < R) {
		Mid = (L + R + 1) >> 1;
		if(ps[Mid] < k) L = Mid;
		else R = Mid-1;
	}
	l = sa[L+1]; r = sa[L+1]+ht[L+1]+k-ps[L]-1;
}
int chk(LL mid) {
	int re = 0, l, r;
	get(l, r, mid); //确定排名为mid的子串[l,r]的位置
	for(int i = n, j; i >= 1; i = j){
		j = i; ++re;
		if(cmp(l, r, j, i)) return m+1; //这句话可以不用写因为保证了子串开头都是最大字母了
		while(--j >= 1 && !cmp(l, r, j, i)); //比较字典序
	}
	return re;
}
int main () {
	freopen("string.in", "r", stdin);
	freopen("string.out", "w", stdout);
	scanf("%d", &m);
	scanf("%s", s+1); n = strlen(s+1);
	int mx = 0;
	for(int i = 1; i <= n; ++i) mx = max(mx, (int)s[i]);
	Pre_Work(n, 260);
	for(int i = 1; i <= n; ++i) {
		if((int)s[sa[i]] == mx) ps[i] = n-(sa[i]+ht[i])+1; //小优化,答案只可能由最大字母开头
		ps[i] += ps[i-1];
	}
	long long l = 1, r = ps[n], mid;
	while(l < r) { //二分排名
		mid = (l + r) >> 1;
		if(chk(mid) <= m) r = mid;
		else l = mid+1;
	}
	int ansl, ansr; get(ansl, ansr, l);
	print(ansl, ansr);
}
发布了367 篇原创文章 · 获赞 239 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/Ike940067893/article/details/104109184
今日推荐