HDU7084/2021“MINIEYE杯”中国大学生算法设计超级联赛(10)1008. Pty loves string(Border/KMP Fail树/DFS序/主席树)

Problem Description

Pty has a string S of length n consisting of lowercase English letters. He denotes the value of string T as the number of occurrences of T in string S.

Now he has Q queries, for each query he gives you x,y. Let the string T be the concatenation of the prefix of length x in the string S and its suffix of length y. He wants you to tell him the value of T.

Input

An integer T in the first line indicates the number of tests.

For each test,first line contains two integer n,Q - the length of S and the number of queries.

The second line contains string S of length n consisting of lowercase English letters.

For the next Q lines, each line contains two integers x,y.

1≤T≤5,1≤n,Q≤2×105,1≤x,y≤n

Output

For each test,output Q lines in total.

For each query, print the answer in one line.

Sample Input

1
6 3
ababaa
1 1
2 1
3 1

Sample Output

1
2
1

Debug一年终于把这个题过了,原因在于询问次数q打成了n。。。

首先一看问的是串的出现次数,可能会想到kmp,但暴力kmp显然不可做。又注意到每次的询问给出的x和y和原串前后缀有关,就考虑从前缀和后缀这里入手。对于这个题,设给定的字符串为\(s\),每个询问给出一对前后缀\(s[1,x]\)和\(s[n - y + 1, n]\),不妨设其拼起来后在\(s\)的\([p, q]\)这个位置出现了,那么\(s[1,x]\)和\(s[p, p + x - 1]\)相等,\(s[n - y + 1, n]\)和\(s[q - y + 1,q]\)相等,同时有\(q - y + 1 = (p + x - 1) + 1\)。直接的思想就是找所有和\(s[1, x]\)相等的子串以及所有和\(s[n - y + 1, n]\)相等的子串检查他们能否拼接在一起。那么怎么找呢?这时就需要用到\(Border\)这个知识点了,相关知识可以参考洛谷P5829这道题。我们正着和反着分别建出\(next\)树以后,会发现\(p + x - 1\)一定在\(x\)的子树,\(q - y + 1\)一定在\(n - y + 1\)的子树(因为y表示的是后缀长度)。举个例子,设\(s=‘ababab’\),\(x = 2\),那么\(next[4]\)和\(next[6]\)都为2,都在2的子树里,观察也可以发现后两个ab都和第一个ab(前缀)一样。也可以自己构造一下加深理解(关键是要知道\(Border\)的\(Border\)还是\(Border\)以及理解\(next\)树的具体含义)。

所以现在问题转化为给出两棵树,树上每个点都有一个值,然后每次询问给出x和y两个子树,问两棵子树中有多少个点对\((a, b)\)满足\(a + 1 = b\)(因为是正着和倒着建的next树,所以有这个关系)。因为树上问题可以通过\(dfs\)序转化为序列问题,这就类似给出两个序列,询问子列的交集。此时就需要用到主席树了。建出两棵\(next\)树以后分别进行\(dfs\)求出来\(dfs\)序,然后我们沿着第一棵树的\(dfs\)序建主席树。这里有一个很关键的问题就是两棵树的形状可能不一样,怎么处理节点相等这个对应关系。这里有一个很巧妙的方式就是在处理\(dfs\)序的时候同时获得第一棵树上某个dfs序所对应的真实的值(即下标)存到id数组里,然后对于1~n + 1遍历的时候(为什么是n + 1?因为next树的根是0号节点,所以dfs序是1~n + 1),增加的位置是\(dfn2[id1[i] + 1]\)。\(id1[i] + 1\)就是对应的\(a + 1 = b\)这个关系,然后\(dfn2[id1[i] + 1]\)就把对应的b转到第二棵树的相应的dfs序位置了。建好树以后不断读入查询即可,查询的就是在x这个子树对应的区间里,有多少y这个子树对应区间内的\(dfn\)值落进去了(因为已经把下标的对应关系转为了\(dfn\)值的对应关系),常规的主席树套路。

#include <bits/stdc++.h>
#define N 1000005
using namespace std;
int n, q;
char s[N];
int nxt1[N], nxt2[N];
int head1[N], ver1[2 * N], Next1[2 * N], tot1 = 0;
int head2[N], ver2[2 * N], Next2[2 * N], tot2 = 0;
void add1(int x, int y) {
	ver1[++tot1] = y, Next1[tot1] = head1[x], head1[x] = tot1;
}
void add2(int x, int y) {
	ver2[++tot2] = y, Next2[tot2] = head2[x], head2[x] = tot2;
}
vector<int> node1[N], node2[N];
void getnxt1() {
	nxt1[1] = 0;
	int n = strlen(s + 1);
	for(int i = 2, j = 0; i <= n; i++) {
		while(j != 0 && s[i] != s[j + 1]) j = nxt1[j];
		if(s[i] == s[j + 1]) j++;
		nxt1[i] = j;
	}
	for(int i = 1; i <= n; i++) {
		add1(nxt1[i], i);
	}
}
void getnxt2() {
	int n = strlen(s + 1);
	nxt2[n] = n + 1;
	for(int i = n - 1, j = n + 1; i >= 1; i--) {
		while(j != n + 1 && s[i] != s[j - 1]) j = nxt2[j];
		if(s[i] == s[j - 1]) j--;
		nxt2[i] = j;
	}
	for(int i = 1; i <= n; i++) {
		add2(nxt2[i], i);
	}
}
int dfn1[N], dfn2[N], id1[N], id2[N], sz1[N], sz2[N], cnt1 = 0, cnt2 = 0;
int ed1[N], ed2[N];
void dfs1(int x, int pre) {
	sz1[x] = 1;
	dfn1[x] = ++cnt1;
	id1[cnt1] = x;
	for(int i = head1[x]; i; i = Next1[i]) {
		int y = ver1[i];
		if(y == pre) continue;
		dfs1(y, x);
		sz1[x] += sz1[y];
	}
	ed1[x] = cnt1;
}
void dfs2(int x, int pre) {
	sz2[x] = 1;
	dfn2[x] = ++cnt2;
	id2[cnt2] = x;
	for(int i = head2[x]; i; i = Next2[i]) {
		int y = ver2[i];
		if(y == pre) continue;
		dfs2(y, x);
		sz2[x] += sz2[y];
	}
	ed2[x] = cnt2;
}
int cnt = 0;//总的节点数
int sum[N * 20], L[N * 20], R[N * 20];//主席树一定不能吝啬空间
int T[N];
int build(int l, int r) {
	int rt = ++cnt;
	sum[rt] = 0;
	int mid = (l + r) >> 1;
	if(l < r) {
		L[rt] = build(l, mid);
		R[rt] = build(mid + 1, r);
	}
	return rt;
}
int modify(int pre, int l, int r, int x) {
	int rt = ++cnt;
	L[rt] = L[pre]; R[rt] = R[pre];
	sum[rt] = sum[pre] + 1;
	if(l < r) {
		int mid = (l + r) >> 1;
		if(x <= mid) L[rt] = modify(L[pre], l, mid, x);
		//注意这里是x <= mid, 而不是线段树常见写法
		//因为这是对权值建立的线段树!
		else R[rt] = modify(R[pre], mid + 1, r, x);
	}
	return rt;
}
int query(int u, int v, int LFT, int RT, int l, int r) {
	if(LFT > r || RT < l) return 0;
    int x = sum[v] - sum[u];
    if(l >= LFT && r <= RT) return x;
    int mid = (l + r) >> 1;
    return query(L[u], L[v], LFT, RT, l, mid) + query(R[u], R[v], LFT, RT, mid + 1, r);
}
int main() {
	int t;
	cin >> t;
	while(t--) {
		tot1 = tot2 = 0;
		cnt1 = cnt2 = 0;
		cnt = 0;
		memset(s, 0, sizeof(s));
		memset(head1, 0, sizeof(head1));
		memset(head2, 0, sizeof(head2));
		scanf("%d%d", &n, &q);
		scanf("%s", s + 1);
		getnxt1();
		getnxt2();
		dfs1(0, -1);
		dfs2(n + 1, -1);
		T[0] = build(1, n + 1);
		for(int i = 1; i <= n + 1; i++) {
			T[i] = modify(T[i - 1], 1, n + 1, dfn2[id1[i] + 1]);
		}
		for(int i = 1; i <= q; i++) {
			int x, y;
			cin >> x >> y;
			y = n - y + 1;
		 	cout << query(T[dfn1[x] - 1], T[ed1[x]], dfn2[y], ed2[y], 1, n + 1) << endl;
		}
	}
	return 0;
}
上一篇:459. 重复的子字符串(KMP算法)


下一篇:PHP实现KMP算法