2019 CCPC 网络选拔 Kth-occurrence

题意

给出一个字符串,每次询问其一个子串\([S_l,S_r]\)在原串中第\(k\)次出现所在的位置(开头位置)


解法

题意很简洁,思路也很简洁

就是代码巨难打

总之这道题还是让我很大程度上加深了对于\(SAM\)的认识啦,还去学了一下线段树合并

首先,根据后缀自动机的性质我们能知道题目所要求的的实际上是

\([S_l,S_r]\)所代表串所在后缀自动机的结点\(endpos\)集合中的第\(k\)个数

求区间第\(k\)大?权值线段树上啦

我们知道,对于后缀自动机上的某个结点,其\(endpos\)集合是它在\(parent\)树上所有儿子\(endpos\)集合的并

我们对于\(parent\)树上的每个节点,开一颗权值线段树,每个前缀初始化为其尾位置

那么对于我们想要求出某个结点的\(endpos\)集合,只要将其儿子的权值线段树合并到它上面来就行了

为了节省空间,这里每颗权值线段树都是动态开点的

那么我们如何查询一个子串\([S_l,S_r]\)在原串的后缀自动机上对应的节点呢?

我们可以采用树上倍增

先预处理所有前缀所对应的结点编号,建出\(parent\)树,预处理倍增数组\(f\)并进行一轮线段树合并求出所有节点的\(endpos\)集合

那么在查询\([S_l,S_r]\)时,我们先找到\([1,S_r]\)对应的结点,然后开始倍增跳

\([S_l,S_r]\)对应节点所代表的的最长串一定是\([x,S_r]\),我们需要找到一个最大的\(x\)使得\([S_l,S_r]\)\([x,S_r]\)的后缀

倍增判断即可。其实我们完全可以把倍增理解为一种不断二分的过程,一次次的接近答案(因为在跳\(fa\)的过程中\(len\)是单调递减的)

找到对应的结点后在那个节点的权值线段树上查询第\(k\)大即可


代码

都封装了,打得巨好理解

#include <cstdio>
#include <cstring>

using namespace std;

const int N = 1e6 + 10;

int T, n, q;

char a[N];

struct segTree {
	
	int sz;
	int ls[N << 2], rs[N << 2], val[N << 2];
	
	void clear() {
		sz = 0;
	}
	
	void update(int cur) {
		val[cur] = val[ls[cur]] + val[rs[cur]];	
	}
	
	int newnode() {
		++sz;
		val[sz] = ls[sz] = rs[sz] = 0;
		return sz;
	}
	
	void mkchain(int& cur, int l, int r, int k) {
		if (!cur)	cur = newnode();
		if (l == r)	return val[cur]++, void();
		int mid = l + r >> 1;
		if (k <= mid)	
			mkchain(ls[cur], l, mid, k);
		else 
			mkchain(rs[cur], mid + 1, r, k);
		update(cur);
	}
	
	int merge(int x1, int x2) {
		if (!x1 || !x2)	return x1 + x2;
		int x = ++sz;
		ls[x] = merge(ls[x1], ls[x2]);
		rs[x] = merge(rs[x1], rs[x2]);
		val[x] = val[x1] + val[x2];
		return x;
	}
			
	int query(int cur, int l, int r, int k) {
		if (l == r)
			return l;
		int mid = l + r >> 1;
		if (k <= val[ls[cur]])	
			return query(ls[cur], l, mid, k);
		else if (k <= val[cur])
			return query(rs[cur], mid + 1, r, k - val[ls[cur]]);
		else 
			return -1;
	}
	
} tr;

struct SAM {
	
	int sz, lst;
	int len[N], fa[N], ch[N][30];
	
	int pos[N], dep[N], rt[N], f[N][30];
	
	int cap;
	int head[N], to[N << 1], nxt[N << 1];
	
	void clear() {
		sz = lst = 1, cap = 0;
		memset(head, 0, sizeof head);
		memset(len, 0, sizeof len);
		memset(fa, 0, sizeof fa);
		memset(ch, 0, sizeof ch);
		memset(rt, 0, sizeof rt);
	}
	
	void add(int x, int y) {
		to[++cap] = y, nxt[cap] = head[x], head[x] = cap;	
	}
	
	void insert(int po, int c) {
		int cur = ++sz, p = lst;
		pos[po] = cur, len[cur] = po;
		for (; p && !ch[p][c]; p = fa[p])	ch[p][c] = cur;
		if (!p)	
			fa[cur] = 1;
		else {
			int q = ch[p][c];
			if (len[q] == len[p] + 1)	fa[cur] = q;
			else {
				int nq = ++sz;
				fa[nq] = fa[q], len[nq] = len[p] + 1;
				for (; p && ch[p][c] == q; p = fa[p])	ch[p][c] = nq;
				memcpy(ch[nq], ch[q], sizeof ch[q]);
				fa[q] = fa[cur] = nq;
			}
		}
		lst = cur;
		tr.mkchain(rt[cur], 1, n, po);
 	}
	
 	void DFS(int cur) {
 		for (int i = 1; i <= 20; ++i)	f[cur][i] = f[f[cur][i - 1]][i - 1];
	 		for (int i = head[cur]; i; i = nxt[i]) {
			f[to[i]][0] = cur;
			DFS(to[i]);
			rt[cur] = tr.merge(rt[cur], rt[to[i]]);
		}
 	}
 	
 	void link() {
 		for (int i = 2; i <= sz; ++i)	add(fa[i], i);
		DFS(1);	
 	}
 	
 	int solve(int l, int r, int k) {
 		int cur = pos[r];
 		for (int i = 20; i >= 0; --i) {
 			int p = f[cur][i];
			if (l + len[p] - 1 >= r)	cur = p;			
 		} 
 		int ans = tr.query(rt[cur], 1, n, k);
 		return (ans == -1) ? ans : ans - (r - l);
 	}
	
} sam;

int main() {
	
	scanf("%d", &T);
	
	while (T--) {
		
		scanf("%d%d", &n, &q);
		scanf("%s", a + 1);
		
		sam.clear(), tr.clear();
		for (int i = 1; i <= n; ++i)	sam.insert(i, a[i] - 'a' + 1);		
		
		sam.link();
		
		int l, r, k;
		while (q--) {
			scanf("%d%d%d", &l, &r, &k);
			printf("%d\n", sam.solve(l, r, k));
		}
		
	}
	
	return 0;
}

posted @ 2019-09-02 22:29  四季夏目天下第一  阅读(311)  评论(0编辑  收藏  举报