后缀自动机SAM练习笔记 (二)

练习题

[NOI2018] 你的名字(扫描线)

题意:
给你一个长度为 \(n\) 的字符串 \(S\)\(q\) 次询问,每次给你一个长度为 \(m\) 的字符串 \(T\) 和一个区间 \([l, r]\),你需要求出 \(T\) 中有多少个本质不同的子串不是 \([S_l, S_r]\) 的子串。

\(1 \leq n \leq 5 \times 10^5,1\leq |T| \leq 5 \times 10^5, \sum |T| \leq 10^6\) ,字符集为小写字母。

分析:
首先来思考每次能将 \([S_l, S_r]\) 取出来该怎么做。

先将取出的子串 \(S'\) 建出后缀自动机,然后对 \(|T|\) 以每一个位置为结尾求出最长公共子串。考虑 \(|T|\) 内部的去重:将 \(T\) 建立后缀自动机,规定相同的子串结尾位置最小的那一个是 有用的,那么以每个位置为结尾的有用的子串是一段前缀,这个可以在建后缀自动机的过程求出。就可以每个位置为结尾且不在 \(S'\) 中的有用子串数量,这个可以用区间 \([ll, rr]\) 表示左端点的范围,那么让答案加上 \(rr - ll + 1\) 即可。

现在的问题在于:不能取出 \([S_l, S_r]\),需要重新思考怎么对每个位置求 最长公共子串
按照 \(r\) 将询问排序,每次扫描线将 \(S_{r'}\) 拓展到当前的 \(S_{r}\)。我们肯定是希望相同的字符串用最靠后的出现位置匹配的,因此实际上需要对每个等价类求出最大的 \(ednpos\)
放在 \(SAM\)\(parent\) 树上看这是一个将某个顶点到根的路径取 \(\max\) 的操作。我们想到差分的思路:单点修改,子树查 \(\max\)
但是有一个问题: \(parent\) 树的结构是不断变化的,我们需要用 \(LCT\) 维护吗?
注意到 \(parent\) 树每次变化前后两个节点的相对关系是不变,因此只需要先建一遍 \(SAM\) 求出每个点最后的 \(dfs\) 序 然后在最终形态上差分就行了。
所以现在求最长公共子串的过程变成什么了呢?
设当前询问区间为 \([q_l, q_r]\)
维护上一个位置的匹配长度 \(nl\),以及所在节点 \(p\),每次判断 \(node_{p, c}\) 是否存在,如果不存在就暴力往父亲跳, \(nl\) 变成现在 \(p\) 的最长长度。
如果存在 \(node_{p, c}\),判断 \(node_{p, c}\) 的最大 \(endpos\)(假设为 \(x\)) 减去最小长度是否小于等于 \(q_l - 1\),如果大于,那么显然没有用,继续往上跳。如果小于等于,那么再往上跳长度会比最小长度还小,肯定不优,因此 \(break\)。最后的答案是 \(nl + 1\)\(x - q_l + 1\)\(\min\)

总复杂度就是 \(O((|S| + \sum |T|) \log |S|)\)
CODE:

#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
typedef long long LL;
const int NN = 1e5 + 10;
const int N = 5e5 + 10;
const int M = 1e6 + 10;
LL ans[NN];
int q, n, m, odr[NN], l[NN], r[NN], L[NN], R[NN], tot;
int len[M];
char s[N], t[M], h[N];
vector< int > E[N * 2];
int ld[N * 2], rd[N * 2], dfc;
struct SegmentTree {
	int l, r, mx;
	#define l(x) tree[x].l
	#define r(x) tree[x].r
	#define mx(x) tree[x].mx
} tree[N * 2 * 4];
void update(int p) {mx(p) = max(mx(p << 1), mx(p << 1 | 1));}
void build(int p, int l, int r) {
	l(p) = l, r(p) = r;
	if(l == r) return ;
	int mid = (l + r >> 1);
	build(p << 1, l, mid); build(p << 1 | 1, mid + 1, r);
}
void ins(int p, int pos, int c) {
	if(l(p) == r(p)) {mx(p) = c; return ;}
	int mid = (l(p) + r(p) >> 1);
	if(pos <= mid) ins(p << 1, pos, c);
	else ins(p << 1 | 1, pos, c);
	update(p);
}
int ask(int p, int l, int r) {
	if(l <= l(p) && r >= r(p)) return mx(p);
	int mid = (l(p) + r(p) >> 1);
	if(r <= mid) return ask(p << 1, l, r);
	else if(l > mid) return ask(p << 1 | 1, l, r);
	else return max(ask(p << 1, l, r), ask(p << 1 | 1, l, r));
}
int query(int p) {return ask(1, ld[p], rd[p]);} 
struct SAM {
	struct Node {
		int fa, len;
		int ch[26];
	} node[N * 2];
	int tot = 1, last = 1;
	inline void clr() {
		for(int i = 1; i <= tot; i ++ ) memset(node[i].ch, 0, sizeof node[i].ch);
		tot = last = 1;
	}
	void dfs(int x) {
		ld[x] = ++ dfc;
		for(auto v : E[x]) dfs(v);
		rd[x] = dfc;
	}
	inline int extend(int c, int pos, int tp) {
		int p = last, np = last = ++ tot;
		if(tp == 1) ins(1, ld[np], pos); // 加入这个endpos 
		node[np].len = node[p].len + 1;
		for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
		if(!p) node[np].fa = 1;
		else {
			int q = node[p].ch[c];
			if(node[q].len == node[p].len + 1) node[np].fa = q;
			else {
				int nq = ++ tot; 
				node[nq] = node[q]; node[nq].len = node[p].len + 1;
				node[np].fa = node[q].fa = nq;
				for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
			}
		}
		return np;
	}
} S, T;
bool cmp(int x, int y) {return r[x] < r[y];}
LL calc(int l, int r, int lim) { // 首先求出每个位置为结尾与当前s的最长公共子串,然后往上跳到 [l, r] 里面 
	int nl = 0, p = 1;
	for(int i = l; i <= r; i ++ ) {
		int c = t[i] - 'a';
		while(p && (!S.node[p].ch[c] || query(S.node[p].ch[c]) - S.node[S.node[S.node[p].ch[c]].fa].len < lim)) {
			p = S.node[p].fa; 
			nl = S.node[p].len;
		}
		if(!p) p = 1;
		else {
			p = S.node[p].ch[c];
			nl = min(nl + 1, query(p) - lim + 1);
		}
		len[i] = nl;
	}
	// 然后需要处理重复的问题, 可以把 给定串也建出sam解决 
	T.clr();
	LL res = 0;
	for(int i = l; i <= r; i ++ ) {
		p = T.extend(t[i] - 'a', i, 0);
		int ll = T.node[T.node[p].fa].len + 1, rr = i - l + 1;
		ll = max(ll, len[i] + 1);
		res += rr - ll + 1;
	}
	return res;
}
int main() {
	scanf("%s", s + 1); n = strlen(s + 1);
	for(int i = 1; i <= n; i ++ ) S.extend(s[i] - 'a', i, 0);
	for(int i = 1; i <= S.tot; i ++ ) E[S.node[i].fa].pb(i);
	S.dfs(1); build(1, 1, dfc);
	scanf("%d", &q);
	for(int i = 1; i <= q; i ++ ) {
		scanf("%s", h + 1); m = strlen(h + 1);
		L[i] = tot + 1;
		for(int j = 1; j <= m; j ++ ) t[++ tot] = h[j];
		R[i] = tot;
		scanf("%d%d", &l[i], &r[i]);
		odr[i] = i;
	}
	sort(odr + 1, odr + q + 1, cmp);
	S.clr(); int now = 1;
	for(int i = 1; i <= q; i ++ ) {
		int o = odr[i];
		while(now <= r[o]) S.extend(s[now] - 'a', now, 1), now ++;
		ans[o] = calc(L[o], R[o], l[o]);
	}
	for(int i = 1; i <= q; i ++ ) printf("%lld\n", ans[i]);
	return 0;
}

[八省联考 2018] 制胡窜(线段树合并维护edp,分讨)

题意:
给你一个字符串 \(s\)\(q\) 次询问,每次询问会给出 \(s\) 的一个子串 \(s_{l, r}\),你需要求出有多少对 \((i, j)\) 满足 \(1 \leq i < j \leq n\)\(i + 1 < j\) 满足 \(s_{1, i},s_{i + 1, j - 1}, s_{j, n}\) 三个字符串中至少有一个包含 \(s_{l, r}\)

\(1 \leq |s| \leq 10^5, 1 \leq q \leq 3 \times 10^5\)

分析:
套路题,犯蠢了写的俩 \(\log\),但是好像跑的比一 \(\log\) 还快。
建出 \(SAM\),那么可以倍增求出 \(s_{l, r}\) 所在的等价类,显然我们只关心所有的 \(endpos\)
分三种情况:
\(s_{1, i}\) 里包含 \(s_{l, r}\)\(s_{1, i}\) 里不包含 \(s_{l, r}\)\(s_{j, n}\) 包含。 \(s_{1, i}, s_{j, n}\) 都不包含 \(s_{l, r}\)\(s_{i + 1, j - 1}\) 包含。
不难发现,对于第一种情况和第二种情况,我们只需要知道 最大的 \(endpos\) 以及 最小的 \(endpos\),那么两种情况的方案数就是一个等差数列求和。
第三种情况比较复杂:
设最大 \(endpos\)\(mx\),最小的为 \(mn\)
我们考虑枚举 \(j\) 的过程,那么首先 \(j\) 应该大于 \(mx - (r - l)\),这样才能保证右半段不包含 \(s_{l, r}\)。其次 \(j\) 应该大于 \(mn\),这样才能保证 \([i + 1, j - 1]\) 能包含 \(s_{l, r}\)
\(s = \max(mx - (r - l) + 1, mn + 1)\),那么答案就是:

\[\sum\limits_{j = s}^{n} \min(pre_{j} - (r - l + 1), mn - 1) \]

其中 \(pre_j\) 代表 \(j\) 小于 \(j\) 的最大 \(endpos\)
那么取到 \(pre_{j} - (r - l + 1)\) 的是一段区间,只需要维护每个 \(endpos\) 与前驱的差 乘 前驱 的和即可。
线段树合并可以一只 \(\log\) 维护这个过程:每个节点维护存在的 \(endpos\) 与前面的贡献,第一个没有贡献。向上 \(pushup\) 加上右儿子最前面那个的贡献即可。

线段树合并不能可持久化,因此离线回答询问即可。
复杂度 \(O((n + q) \log n)\)但是我写成了俩 \(\log\)

#include<bits/stdc++.h>
#define MP make_pair
#define pb emplace_back
using namespace std;
typedef pair< int, int > PII;
typedef long long LL;
const int N = 1e5 + 10;
int mx[N * 2], mn[N * 2];
int n, q, pos[N], fat[N * 2][18]; // 每个前缀对应的节点 
char s[N];
LL ans[N * 3];
vector< int > E[N * 2];
vector< int > edp[N * 2];
struct query {
	int l, r, idx;
};
vector< query > qry[N * 2];
namespace SAM {
	struct Node {
		int ch[10];
		int fa, len;
	} node[N * 2];
	int tot = 1, last = 1;
	inline void extend(int c, int u) {
		int p = last, np = last = ++ tot;
		node[np].len = node[p].len + 1;
		pos[u] = np; edp[np].pb(u);
		for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
		if(!p) node[np].fa = 1;
		else {
			int q = node[p].ch[c];
			if(node[q].len == node[p].len + 1) node[np].fa = q;
			else {
				int nq = ++ tot;
				node[nq] = node[q]; node[nq].len = node[p].len + 1;
				node[q].fa = node[np].fa = nq;
				for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
			}
		}
	}
	inline void build(char *s) {
		memset(mx, 0xcf, sizeof mx);
		memset(mn, 0x3f, sizeof mn);
		int n = strlen(s + 1);
		for(int i = 1; i <= n; i ++ ) extend(s[i] - '0', i);
		for(int i = 1; i <= tot; i ++ ) fat[i][0] = node[i].fa, E[node[i].fa].pb(i);
		for(int i = 1; (1 << i) <= tot; i ++ ) 
			for(int j = 1; j <= tot; j ++ ) 
				fat[j][i] = fat[fat[j][i - 1]][i - 1];
	}
}
int Find(int l, int r) {
	int u = pos[r]; int len = (r - l + 1);
	for(int i = 17; i >= 0; i -- ) {
		if(SAM::node[fat[u][i]].len >= len) u = fat[u][i];
	}
	return u;
}
inline void Set(int l, int r, int idx) {
	int u = Find(l, r);
	qry[u].pb((query) {l, r, idx});
}
struct SegmentTree {
	int ls, rs, mn, mx; 
	LL sum; // 当前区间里面所有存在 endpos 与它前面的endpos差 乘上 前面的endpos 的值 
	#define ls(x) tree[x].ls
	#define rs(x) tree[x].rs
	#define mn(x) tree[x].mn
	#define mx(x) tree[x].mx
	#define sum(x) tree[x].sum
}tree[N * 20];
int root[N * 2], tot;
void update(int p) {
	mn(p) = min(mn(ls(p)), mn(rs(p)));
	mx(p) = max(mx(ls(p)), mx(rs(p)));
	sum(p) = sum(ls(p)) + sum(rs(p));
}
int pre(int p, int lp, int rp, int pos) { // 找到 <= pos 的 最后一个 endpos, 没有返回 -1 
	if(mn(p) > pos) return -1;
	if(lp == rp) return lp;
	int mid = (lp + rp >> 1);
	if(mn(rs(p)) <= pos) return pre(rs(p), mid + 1, rp, pos);
	else return pre(ls(p), lp, mid, pos);
}
int nxt(int p, int lp, int rp, int pos) { // 找到 >= pos 的第一个endpos 没有返回 -1 
	if(mx(p) < pos) return -1;
	if(lp == rp) return lp;
	int mid = (lp + rp >> 1);
	if(mx(ls(p)) >= pos) return nxt(ls(p), lp, mid, pos);
	else return nxt(rs(p), mid + 1, rp, pos);
}
void change(int p, int lp, int rp, int pos, LL c) {
	if(lp == rp) {sum(p) = c; return ;}
	int mid = (lp + rp >> 1);
	if(pos <= mid) change(ls(p), lp, mid, pos, c);
	else change(rs(p), mid + 1, rp, pos, c);
	update(p);
}
LL ask(int p, int lp, int rp, int l, int r) {
	if(!p) return 0;
	if(l <= lp && r >= rp) return sum(p);
	int mid = (lp + rp >> 1);
	if(r <= mid) return ask(ls(p), lp, mid, l, r);
	else if(l > mid) return ask(rs(p), mid + 1, rp, l, r);
	else return ask(ls(p), lp, mid, l, r) + ask(rs(p), mid + 1, rp, l, r);
}
void ins(int &p, int lp, int rp, int pos) {
	if(!p) p = ++ tot;
	if(lp == rp) {
		mn(p) = mx(p) = pos;
		return ;
	}
	int mid = (lp + rp >> 1);
	if(pos <= mid) ins(ls(p), lp, mid, pos);
	else ins(rs(p), mid + 1, rp, pos);
	update(p);
}
int lastp, lastq;
int Merge(int p, int q, int lp, int rp) {
	if(!p || !q) {
		if(p) {
			if(lastq > lastp) {
				int u = mn(p);
				change(p, lp, rp, u, 1LL * lastq * (u - lastq));
			}
			lastp = max(lastp, mx(p));
		}
		else if(q) {
			if(lastp > lastq) {
				int u = mn(q);
				change(q, lp, rp, u, 1LL * lastp * (u - lastp));
			}
			lastq = max(lastq, mx(q));
		}
		return p ^ q;
	}
	int mid = (lp + rp >> 1);
	ls(p) = Merge(ls(p), ls(q), lp, mid);
	rs(p) = Merge(rs(p), rs(q), mid + 1, rp);
	update(p);
	return p;
}
inline LL calc(int l, int r, int p) { 
	int mn = mn(p), mx = mx(p);
	LL ret = 0;
	ret += 1LL * (n - mn - 1) * (n - mn) / 2LL; // [1, i] 包含的情况
	int pp = mx - (r - l) - 2, qq = mn - 1; // [1, i] 不包含, [j, n] 包含的情况 
	if(pp > 0) {
		if(qq < pp) ret += 1LL * (qq - 1) * qq / 2LL + 1LL * qq * (pp - qq + 1);
		else ret += 1LL * pp * (pp + 1) / 2LL;
	}
	// 两边都不包含,中间段包含的情况
	int s = max(mn + 1, mx - (r - l) + 1); // j \in [s, n]
	int t = nxt(p, 1, n, s);
	t = (t == -1 ? n : t);
	ret += 1LL * (t - s + 1) * min(pre(p, 1, n, s - 1) - (r - l + 1), mn - 1);
	s = t + 1;
	if(s > n) return ret;
	int o = nxt(p, 1, n, mn + (r - l));
	if(o == -1) o = n, ret += 1LL * (n - mx) * mx; // 找不到,说明所有 pre 都比 mn - 1 小 
	if(o >= s) ret += ask(p, 1, n, s, o) - 1LL * (o - s + 1) * (r - l + 1); // [s, o] 这一部分是 edp - (r - l) 比较小 
	ret += 1LL * (n - max(o + 1, s) + 1) * (mn - 1); // 这一部分是 mn - 1 比较小 
	return ret;
}
void dfs(int x) {
	for(auto p : edp[x]) {
		root[0] = 0;
		ins(root[0], 1, n, p);
		lastp = lastq = -1;
		root[x] = Merge(root[x], root[0], 1, n);
	}
	for(auto v : E[x]) {
		dfs(v);
		lastp = lastq = -1;
		root[x] = Merge(root[x], root[v], 1, n);
	}
	for(auto v : qry[x]) {
		int l = v.l, r = v.r, idx = v.idx;
		ans[idx] = calc(l, r, root[x]);
	}
}
int main() {
	scanf("%d%d", &n, &q);
	scanf("%s", s + 1);
	SAM::build(s);
	for(int i = 1; i <= q; i ++ ) {
		int l, r; scanf("%d%d", &l, &r);
		Set(l, r, i);
	}
	mx(0) = 0; mn(0) = n + 1; 
	dfs(1); 
	for(int i = 1; i <= q; i ++ ) printf("%lld\n", ans[i]);
	return 0;
} 

[十二省联考 2019] 字符串问题(拓扑排序,线段树优化建图)

题意:
给你一个字符串 \(S\),同时给你 \(n_a\)\(A\) 类串 和 \(n_b\)\(B\) 类串。这 \(n_a + n_b\) 个串都用 \([l, r]\) 表示 \(S\) 的子串 \(S_{l, r}\) 描述。
现在有 \(m\) 个支配关系,每个支配关系用二元组 \((x, y)\) 描述表示第 \(x\)\(A\) 类串支配第 \(y\)\(B\) 类串。
你需要求出一个最长的字符串 \(T\),满足 \(T\) 可以划分成若干个 \(A\) 类串拼接的形式 \(T = t_1 + t_2 + \dots + t_k\),且 \(\forall i < k\),存在某个被 \(t_{i}\) 支配的 \(B\) 类串 \(s'\)\(t_{i + 1}\) 的前缀。输出 \(T\) 的长度,如果 \(T\) 可以无限长,那么输出 \(-1\)

\(1 \leq |S| \leq 2 \times 10^5, 1 \leq n_a, n_b \leq 2 \times 10^5\)\(T\) 组询问, \(T \leq 10\)

分析:
套路题,就是有点难写。
首先转化为图上问题:
每个 \(A\) 类串向它所支配的 \(B\) 类串连一条有向边,每个 \(B\) 类串向它能作为其前缀的 \(A\) 类串连一条有向边。定义 \(A \to B\) 边的边权为 \(A\) 类串的长度,那么只需要求这张图的最长路就是答案。
显然有环一定是无限长,没有环跑拓扑排序即可。
难点在于怎么连边:
\(A \to B\) 的边直接连即可,一共有 \(O(m)\) 条。
\(B \to A\) 的边:前缀不太好,我们将字符串翻转,变成后缀。考虑建出 \(SAM\),首先将每个 \(B\) 类串定位到 \(parent\) 树的节点上,那么它能作为那些 \(A\) 类串的后缀呢?显然是定位到它的子树里的 \(A\) 类串。这里需要特殊处理和它定位到一个节点的 \(A\) 类串:只有比它长的 \(A\) 类串才能被连边。

那么我们就有了一个想法:将每个串定位后根据 \(parent\) 树的 \(dfs\) 序连边,一棵子树对应了一段区间因此可以线段树优化建图。对于定位到相同节点的 \(A,B\) 串,将 \(A\) 串按照长度从小到大排序,那么每个 \(B\) 类串指向的是一段后缀,拉出一条链后缀优化建图即可。

定位可以在 \(parent\) 树上倍增,总边数 \(O(n \log n)\),总时间复杂度 \(O(n \log n)\)

CODE:

#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
char s[N];
int T, n, na, nb, m, la[N], ra[N], lb[N], rb[N], odr[N], Dfn[N];
int pos[N], fat[N * 2][20], dfn[N * 2], L[N * 2], R[N * 2], dfc;
vector< int > G[N * 2], Idx[N * 2], chain[N * 2];
int tot, Len;
int root, ls[N * 8], rs[N * 8];
struct edge {
	int v, last;
} E[N + N * 25];
int head[N * 8];
int in[N * 8];
LL dis[N * 8];
void add(int u, int v) {
	E[++ Len] = (edge) {v, head[u]};
	head[u] = Len; in[v] ++;
}
namespace SAM {
	struct Node {
		int ch[26];
		int fa, len;
	} node[N * 2];
	int tot, last;
	void clr() {
		for(int i = 1; i <= tot; i ++ ) {
			G[i].clear(); Idx[i].clear(); chain[i].clear();
			for(int j = 0; j < 26; j ++ ) node[i].ch[j] = 0;
		}
		tot = last = 1;
	}
	inline void extend(int c, int u) {
		int p = last, np = last = ++ tot; pos[u] = np;
		node[np].len = node[p].len + 1;
		for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
		if(!p) node[np].fa = 1;
		else {
			int q = node[p].ch[c];
			if(node[q].len == node[p].len + 1) node[np].fa = q;
			else {
				int nq = ++ tot;
				node[nq] = node[q]; node[nq].len = node[p].len + 1;
				node[q].fa = node[np].fa = nq;
				for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
			}
		}
	}
	void dfs(int x, int fa) {
		dfn[x] = ++ dfc, L[x] = dfc;
		fat[x][0] = fa; for(int i = 1; i <= 19; i ++ ) fat[x][i] = fat[fat[x][i - 1]][i - 1];
		for(auto v : G[x]) dfs(v, x);
		R[x] = dfc;
	}
	inline void build(char *s) {
		clr(); // 清空 
		int n = strlen(s + 1);
		for(int i = 1; i <= n; i ++ ) extend(s[i] - 'a', i);
		for(int i = 1; i <= tot; i ++ ) G[node[i].fa].pb(i);
		dfs(1, 0);
	}
}
inline int Find(int l, int r) {
	int u = pos[r];
	for(int i = 19; i >= 0; i -- ) 
		if(SAM::node[fat[u][i]].len >= r - l + 1) u = fat[u][i];
	return u;
}
void build(int &p, int l, int r) {
	if(l == r) {
		p = odr[l];
		return ;
	}
	p = ++ tot;
	int mid = (l + r >> 1);
	build(ls[p], l, mid); 
	build(rs[p], mid + 1, r);
	add(p, ls[p]); add(p, rs[p]);
}
inline void add_edge(int p, int lp, int rp, int l, int r, int o) {
	if(l <= lp && r >= rp) {add(o, p); return ;}
	int mid = (lp + rp >> 1);
	if(l <= mid) add_edge(ls[p], lp, mid, l, r, o);
	if(r > mid) add_edge(rs[p], mid + 1, rp, l, r, o);
}
inline bool cmp(int x, int y) {return (Dfn[x] < Dfn[y]) || (Dfn[x] == Dfn[y] && ra[x] - la[x] < ra[y] - la[y]);} // dfn 序相同短的排前面 
inline void Pre() { // 每个等价类里面的 a 排序 
	for(int i = 1; i <= SAM::tot; i ++ ) {
		sort(Idx[i].begin(), Idx[i].end(), cmp); // 建一个链状结构辅助连边 
		for(int j = Idx[i].size() - 1; j >= 0; j -- ) {
			int p = ++ tot; add(p, Idx[i][j]);
			chain[i].pb(p); int sz = chain[i].size();
			if(sz != 1) add(p, chain[i][sz - 2]);
		}
		reverse(chain[i].begin(), chain[i].end());
	}
}
inline void Link(int x) { // 将第 x 个 b串 指向以它为后缀的 a 串 
	int u = Find(lb[x], rb[x]); 
	int l = L[u] + 1, r = R[u];
	int ll = 1, rr = na, mid, lres = n + 1;
	while(ll <= rr) {
		mid = (ll + rr >> 1);
		if(Dfn[odr[mid]] >= l) lres = mid, rr = mid - 1;
		else ll = mid + 1;
	}
	ll = 1, rr = na, mid; int rres = 0;
	while(ll <= rr) {
		mid = (ll + rr >> 1);
		if(Dfn[odr[mid]] <= r) rres = mid, ll = mid + 1;
		else rr = mid - 1;
	}
	if(lres <= rres) add_edge(root, 1, na, lres, rres, na + x);
	// 接下来是内部的连边 
	if(!Idx[u].empty()) { // 连的是一段后缀 
		int len = rb[x] - lb[x];
		int ll = 0, rr = Idx[u].size() - 1, mid, res = -1;
		while(ll <= rr) {
			mid = (ll + rr >> 1);
			if(ra[Idx[u][mid]] - la[Idx[u][mid]] >= len) res = mid, rr = mid - 1;
			else ll = mid + 1;
		}
		if(res != -1) add(na + x, chain[u][res]);
	}
}
LL w(int x) {return x <= na ? ra[x] - la[x] + 1 : 0;}
inline LL toopsort() { // 拓扑排序 
	queue< int > q;
	for(int i = 1; i <= tot; i ++ ) {
		if(!in[i]) q.push(i), dis[i] = w(i);
	}
	while(!q.empty()) {
		int u = q.front(); q.pop();
		for(int i = head[u]; i; i = E[i].last) {
			int v = E[i].v; 
			dis[v] = max(dis[v], dis[u] + w(v));
			in[v] --; if(!in[v]) q.push(v);
		}
	}
	LL ret = 0;
	for(int i = 1; i <= tot; i ++ ) {
		if(in[i]) return -1;
		ret = max(ret, dis[i]);
	}
	return ret;
}
inline void Clear() {
	for(int i = 1; i <= tot; i ++ ) {
		in[i] = 0; head[i] = 0; dis[i] = 0;
	}
	Len = 0; dfc = 0; tot = 0;
}
void solve() {
	scanf("%s", s + 1); n = strlen(s + 1);
	reverse(s + 1, s + n + 1);
	scanf("%d", &na);
	for(int i = 1; i <= na; i ++ ) {
		scanf("%d%d", &ra[i], &la[i]);
		la[i] = n - la[i] + 1, ra[i] = n - ra[i] + 1;
		odr[i] = i;
	}
	scanf("%d", &nb);
	for(int i = 1; i <= nb; i ++ ) {
		scanf("%d%d", &rb[i], &lb[i]);
		lb[i] = n - lb[i] + 1, rb[i] = n - rb[i] + 1;
	}
	SAM::build(s);
	for(int i = 1; i <= na; i ++ ) { // 对每个 a 类串确定dfs序 
		int u = Find(la[i], ra[i]);
		Dfn[i] = dfn[u];
		Idx[u].pb(i);
	}
	sort(odr + 1, odr + na + 1, cmp);
	root = 0; tot = na + nb; // 前这么多不能用 
	build(root, 1, na); // 按照这个顺序建立一棵线段树 
	Pre();
	for(int i = 1; i <= nb; i ++ ) Link(i); // B -> A 的边 
	scanf("%d", &m);
	for(int i = 1; i <= m; i ++ ) {
		int x, y; scanf("%d%d", &x, &y);
		add(x, y + na); // A -> B 的边 
	}
	printf("%lld\n", toopsort());
	Clear(); // 清空 
}
int main() {
	scanf("%d", &T);
	while(T -- ) solve();
	return 0;
}

广义后缀自动机(GSAM)

一篇非常好的学习笔记
概述的来说,广义后缀自动机是能够接受字符串 \(s_1,\dots s_n\) 的所有后缀的自动机。
建立的过程是在 \(trie\) 树上建立后缀自动机。
建立的方法分为两种: 离线法在线法

离线建立一般是 \(bfs\) 按照深度依次往 \(SAM\) 中加入 \(trie\) 树的每一层点,这种方式下 \(extend\) 函数不用特判特殊情况,正常写法就是对的。复杂度 \(O(n)\)\(n\)\(tire\) 树的节点数。

在线建立是每次将 \(last\) 设为 \(1\),然后暴力插入字符串 \(s_i\)。但是为了防止产生空节点,因此 \(extend\) 函数中需要加特判。复杂度 \(O(m)\)\(m\) 是所有字符串的长度之和。

特判的情况上面的博客中都有。下面的代码是 \(bfs\) 离线版本:

// 广义 SAM 模板 bfs 离线版本 
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
LL f[N * 2];
bool vis[N * 2];
int n;
char s[N];
int tot = 1, pos[N];
struct trie {int c, fa, ch[26];} tr[N];
void ins(char *s) {
	int len = strlen(s + 1);
	int p = 1;
	for(int i = 1; i <= len; i ++ ) {
		if(!tr[p].ch[s[i] - 'a']) tr[p].ch[s[i] - 'a'] = ++ tot, tr[tot].fa = p, tr[tot].c = s[i] - 'a';
		p = tr[p].ch[s[i] - 'a'];
	}
}
namespace GSAM { // 广义后缀自动机 
	struct Node {
		int fa, len;
		int ch[26];
	} node[N * 2];
	int tot = 1;
	int extend(int c, int last) {
		int p = last, np = ++ tot;
		node[np].len = node[p].len + 1;
		for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
		if(!p) node[np].fa = 1;
		else {
			int q = node[p].ch[c];
			if(node[q].len == node[p].len + 1) node[np].fa = q;
			else {
				int nq = ++ tot;
				node[nq] = node[q]; node[nq].len = node[p].len + 1;
				node[q].fa = node[np].fa = nq;
				for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
			}
		}
		return np;
	}
	void build() {
		queue< int > q; pos[1] = 1;
		for(int i = 0; i < 26; i ++ ) 
			if(tr[1].ch[i]) q.push(tr[1].ch[i]);
		while(!q.empty()) {
			int u = q.front(); q.pop();
			pos[u] = extend(tr[u].c, pos[tr[u].fa]);
			for(int i = 0; i < 26; i ++ ) 
				if(tr[u].ch[i]) q.push(tr[u].ch[i]);
		}
	}
	void dfs(int x) {
		if(vis[x]) return ;
		vis[x] = 1; f[x] = 1;
		for(int i = 0; i < 26; i ++ ) 
			if(node[x].ch[i]) dfs(node[x].ch[i]), f[x] += f[node[x].ch[i]];
	}
} 
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%s", s + 1);
		ins(s);
	}
	GSAM::build();
	GSAM::dfs(1); // 求路径条数
	cout << f[1] - 1 << endl << GSAM::tot << endl;
	return 0;
}

CF666E Forensic Examination(广义SAM,线段树合并维护edp)

题意:
给你一个长度为 \(n\) 的字符串 \(s\)\(m\) 个字符串 \(t_i\)\(q\) 次询问,每次询问给你一个 \(s\) 的子串 \([l, r]\) 和询问区间 \([ql, qr]\),你需要求出 \(s_{l, r}\)\(t_{ql} \dots t_{qr}\) 中的哪一个出现次数最多,输出那个 \(t_i\) 的编号以及出现次数。如果在多个 \(t_i\) 中出现次数达到最大值,那么输出编号最小的那个。

\(1 \leq n \leq 5 \times 10^5\)\(1 \leq q \leq 5 \times 10^5\)\(\sum |t_i| \leq 5 \times 10^4\)\(1 \leq m \leq 5 \times 10^4\)

分析:
看到与 子串 有关的问题可以考虑 \(SAM\)。 像这种 需要同时考虑多个字符串的子串 的问题,考虑广义 \(SAM\)

首先离线建出 \(GSAM\),然后可以类似最长公共子串的思路将 \(s\) 的每个前缀 \([1, r]\) 定位到 \(GSAM\) 的节点上,并能求出最长匹配长度,那么每次询问就可以 倍增 将询问子串 \([l, r]\) 定位到 \(GSAM\) 的一个等价类 \(p\) 上。

我们关注每个等价类在每个 \(t_i\) 的出现次数。
考虑 \(GSAM\) 的等价类的定义:重新定义一个字符串\(s\)\(endpos\) 集合: \(trie\) 树的根到某个节点 \(p\) 的路径所能形成的字符串 \(str_p\)\(s\) 能作为 \(str_p\) 的后缀的节点 \(p\) 的集合。

由于 \(trie\) 树是将所有字符串共用公共前缀形成的结构,那么 \(trie\) 树的每个节点实际上包含了多个字符串的前缀信息,也可以理解成它同时包含了多个字符串的相同 \(endpos\),此时的 \(endpos\) 与原来的含义相同。

那么可以将每种字符串的信息放到 \(trie\) 树的节点上,然后一个等价类的信息就是 \(endpos\) 集合中节点信息的合并。

具体可以这样做:建立 \(trie\) 树时,对每个 \(trie\) 树节点开一个值域线段树,然后插入字符串 \(t_i\) 时就让经过的节点 \(p\) 的值域线段树上 \(i\) 位置加 \(1\)。然后建立 \(GSAM\) 时把节点的线段树信息放到状态节点上(就是把根给这个状态),然后从下到上合并值域线段树就能求出每一个等价类在每种字符串里的出现次数了。求答案就是求区间最大值,这个是简单的。

复杂度 \(O((q + \sum|t_i|) \log (\sum |t_i|))\)
CODE:

// 广义SAM + 线段树合并 
#include<bits/stdc++.h>
#define MP make_pair
#define pb emplace_back
using namespace std;
typedef pair< int, int > PII;
const int N = 5e5 + 10;
const int M = 5e4 + 10;
int n, m, q, ql[N], qr[N];
PII ans[N];
char s[N], t[M];
struct SegmentTree {
	struct Seg {
		int ls, rs;
		PII mx;
		#define ls(x) tree[x].ls
		#define rs(x) tree[x].rs
		#define mx(x) tree[x].mx
	} tree[M * 20];
	int tot = 0;
	PII Max(PII x, PII y) {
		if(x.first != y.first) return x.first > y.first ? x : y;
		else return x.second < y.second ? x : y;
	}
	inline void update(int p) {mx(p) = Max(mx(ls(p)), mx(rs(p)));}
	void ins(int &p, int lp, int rp, int pos) {
		if(!p) p = ++ tot;
		if(lp == rp) {mx(p).first ++; mx(p).second = lp; return ;}
		int mid = (lp + rp >> 1);
		if(pos <= mid) ins(ls(p), lp, mid, pos);
		else ins(rs(p), mid + 1, rp, pos);
		update(p);
	}
	int Merge(int p, int q, int lp, int rp) {
		if(!p || !q) return p ^ q;
		if(lp == rp) {mx(p).first += mx(q).first; return p;}
		int mid = (lp + rp >> 1);
		ls(p) = Merge(ls(p), ls(q), lp, mid);
		rs(p) = Merge(rs(p), rs(q), mid + 1, rp);
		update(p);
		return p;
	}
	PII ask(int p, int lp, int rp, int l, int r) { // 查 [l, r] 里面的最大值以及最小位置 
		if(l <= lp && r >= rp) return mx(p);
		int mid = (lp + rp >> 1);
		if(r <= mid) return ask(ls(p), lp, mid, l, r);
		else if(l > mid) return ask(rs(p), mid + 1, rp, l, r);
		else return Max(ask(ls(p), lp, mid, l, r), ask(rs(p), mid + 1, rp, l, r));
	}
} sgt;
int tot = 1;
struct trie {int fa, c, ch[26];} tr[M];
int tr_root[M], sam_root[M * 2];
vector< int > qry[M * 2]; // 询问编号 
vector< int > E[M * 2];
int fat[M * 2][18];
inline void ins(char *s, int idx) {
	int p = 1, len = strlen(s + 1);
	for(int i = 1; i <= len; i ++ ) {
		if(!tr[p].ch[s[i] - 'a']) {
			tr[p].ch[s[i] - 'a'] = ++ tot;
			tr[tot].fa = p; tr[tot].c = s[i] - 'a';
		}
		p = tr[p].ch[s[i] - 'a'];
		sgt.ins(tr_root[p], 1, m, idx);
	}
}
namespace GSAM {
	int pos[M * 2], tot = 1;
	struct Node {
		int fa, len;
		int ch[26];
	} node[M * 2];
	inline int extend(int c, int last) {
		int p = last, np = ++ tot;
		node[np].len = node[p].len + 1;
		for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
		if(!p) node[np].fa = 1;
		else {
			int q = node[p].ch[c];
			if(node[q].len == node[p].len + 1) node[np].fa = q;
			else {
				int nq = ++ tot;
				node[nq] = node[q]; node[nq].len = node[p].len + 1;
				node[q].fa = node[np].fa = nq;
				for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
			}
		}
		return np;
	}
	inline void build() {
		queue< int > q; pos[1] = 1; 
		for(int i = 0; i < 26; i ++ ) 
			if(tr[1].ch[i]) q.push(tr[1].ch[i]);
		while(!q.empty()) {
			int u = q.front(); q.pop();
			pos[u] = extend(tr[u].c, pos[tr[u].fa]);
			sam_root[pos[u]] = tr_root[u];
			for(int i = 0; i < 26; i ++ ) 
				if(tr[u].ch[i]) q.push(tr[u].ch[i]);
		}
		for(int i = 1; i <= tot; i ++ ) {
			E[node[i].fa].pb(i); fat[i][0] = node[i].fa;
		}
		for(int i = 1; i <= 17; i ++ ) 
			for(int x = 1; x <= tot; x ++ ) 
				fat[x][i] = fat[fat[x][i - 1]][i - 1];
	}
}
int pre_pos[N], len[N]; // 前缀定位 
inline void pre() {
	int p = 1, nl = 0;
	for(int i = 1; i <= n; i ++ ) {
		while(p && !GSAM::node[p].ch[s[i] - 'a']) p = GSAM::node[p].fa, nl = GSAM::node[p].len;
		if(!p) p = 1, nl = 0;
		else p = GSAM::node[p].ch[s[i] - 'a'], nl ++;
		pre_pos[i] = p; len[i] = nl;
	}
}
inline int Find(int l, int r) {
	int u = pre_pos[r];
	if(len[r] < r - l + 1) return -1;
	for(int i = 17; i >= 0; i -- ) 
		if(GSAM::node[fat[u][i]].len >= r - l + 1) u = fat[u][i];
	return u;
}
inline void Set(int l, int r, int idx) {
	int u = Find(l, r);
	if(u != -1) qry[u].pb(idx);
}
void dfs(int x) {
	for(auto v : E[x]) {
		dfs(v);
		sam_root[x] = sgt.Merge(sam_root[x], sam_root[v], 1, m);
	}
	for(auto v : qry[x]) ans[v] = sgt.ask(sam_root[x], 1, m, ql[v], qr[v]);
}
int main() {
	scanf("%s", s + 1); n = strlen(s + 1);
	scanf("%d", &m);
	for(int i = 1; i <= m; i ++ ) {
		scanf("%s", t + 1);
		ins(t, i);
	}
	GSAM::build();
	pre();
	scanf("%d", &q);
	for(int i = 1; i <= q; i ++ ) {
		int l, r; scanf("%d%d%d%d", &ql[i], &qr[i], &l, &r); 
		Set(l, r, i); // 定位到 GSAM 的节点上 
	}
	dfs(1);
	for(int i = 1; i <= q; i ++ ) {
		printf("%d %d\n", max(ans[i].second, ql[i]), ans[i].first);
	}
	return 0;
}

P6292 区间本质不同子串个数(树上颜色段均摊)

题意:
给你一个长度为 \(n\) 仅包含小写字母的字符串 \(S\)\(m\) 次询问 \(S_{L, R}\) 包含多少个本质不同的子串。

\(1 \leq n \leq 10^5, 1 \leq m \leq 2 \times 10^5, 1 \leq L \leq R \leq n\)

分析:
考虑将询问离线,对 \(R\) 扫描线。
那么对于当前扫到的 \(R\),我们希望每种本质不同子串出现位置越靠右越好,相当于我们需要它们的 \(endpos\) 尽量大。考虑想办法维护每种子串的最大 \(endpos\),然后用数据结构维护它们能出现的最大左端点。如果令这些左端点 \(+1\),那么 \([L, R]\) 的答案就是区间 \([L, R]\) 的和。
每次加入一个 \(S_R\),可以得到一个 \(endpos\) 包含 \(R\) 的状态节点 \(np\)。考虑在 最终的 \(parent\) 树上修改 当前存在的 所有子串的最大 \(endpos\)
不难发现,当前存在的 所有子串就是 最终的 \(parent\) 树上,所有 \(np\) 到根路径上的状态所表示的字符串的并集。那么每次得到一个 \(np\),我们只需要将 \(np\) 到根路径上所有状态的最大 \(endpos\) 设成 \(R\) 即可。然后一个等价类贡献给的 \(L\) 是一段区间,具体来说是 \([R - maxlen + 1, R - minlen + 1]\)
我们需要 撤销 每个等价类原有的贡献,考虑到如果原来一段的 \(endpos\) 相同,那么它们贡献的 \(L\) 一定是一段区间(\(Parent\) 树上父亲与儿子含有的字符串长度连续),因此可以一次撤销它们的代价。
\(endpos\) 看作颜色,发现撤销的复杂度基于路径颜色段数,然后每次修改 \(endpos\) 是路径推平操作。这实际上是 颜色段均摊 的过程。有重要结论: 树上颜色段均摊 的势能为 \(O(n \log n)\)。证明:树剖,然后每条重链上可看作序列上颜色段均摊。一次点到根的路径推平会增加 \(\log n\) 个颜色段,总势能 \(O(n \log n)\)
那么就可以暴力跳颜色段维护。具体来说,将 最终的 \(Parent\) 树剖后求出 \(dfs\) 序列。我们每次跳一条重链上,就暴力撤销当前位置到链头的贡献,这个是一个区间 \(-1\) (知道了终点位置和长度范围),然后将它们的颜色推平成 \(R\)。最后只需要将 \([1, R]\) 区间加 \(+1\)
支持区间 \(+1\),区间查和,树状数组维护即可。
每次修改查询复杂度 \(O(\log n)\),由于势能为 \(O(n \log n)\),这一部分复杂度 \(O(n \log^2 n)\)
同时每次 \(split\) 复杂度 \(\log n\),总共跳重链 \(n \log n\) 次,因此复杂度也是 \(O(n \log^2 n)\) 的。
总复杂度 \(O(n \log^2 n)\)
CODE:

// 每个等价类只关心颜色与长度。在最后的形态下树剖 
#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
const int M = 3e5 + 10;
char s[N];
int n, m, l[M], r[M];
vector< int > qry[N];
int dep[N * 2], sz[N * 2], fat[N * 2], big[N * 2], bel[N * 2], bot[N * 2], dfn[N * 2], dfc, ID[N * 2];
LL ans[M];
vector< int > E[N * 2];
struct range { // 连续段 
	int l, r, col;
	friend bool operator < (range a, range b) {return a.l < b.l;}
};
int mnlen[N * 2], mxlen[N * 2]; // 每个状态的长度 
set< range > odt;
struct BIT { // 考虑树状数组 
	LL c[N];
	inline int lowbit(int x) {return x & -x;}
	inline void add(int x, int y) {for(; x < N; x += lowbit(x)) c[x] += y;}
	inline LL ask(int x) {LL res = 0; for(; x; x -= lowbit(x)) res += c[x]; return res;} 
} T[2];
inline LL ask(int l, int r) {
	return T[0].ask(l - 1) * (r - l + 1) + (T[0].ask(r) - T[0].ask(l - 1)) * (r + 1) - (T[1].ask(r) - T[1].ask(l - 1));
}
inline void change(int l, int r, int c) { // l + c, r + 1  - c
	if(l > r) return ;
	T[0].add(l, c); T[0].add(r + 1, -c);
	T[1].add(l, c * l), T[1].add(r + 1, -c * (r + 1));
}
auto split(int x) { // 将 [l, r] 变成 [l, x) 和 [x, r] 并返回后者的指针 
	auto it = --odt.upper_bound((range) {x, x, 0});
	if(it->l == x) return it;
	int l = (it->l), r = (it->r), col = (it->col);
	odt.erase(it);
	odt.insert((range) {l, x - 1, col});
	return odt.insert((range) {x, r, col}).first;
}
inline void assign(int l, int r, int c) { // [l, r] 区间修改 
	auto itr = split(r + 1), itl = split(l);
	for(auto it = itl; it != itr; it ++ ) {
		if(it->col != 0) {
			int L = mnlen[ID[it->l]], R = mxlen[ID[it->r]];
			change(it->col - R + 1, it->col - L + 1, -1);
		}
	}
	odt.erase(itl, itr);
	odt.insert((range) {l, r, c});
}
inline void Ccol(int x, int c);
namespace SAM {
	struct Node {
		int fa, len;
		int ch[26];
	} node[N * 2];
	int tot = 1, last = 1;
	inline void clr() {
		for(int i = 1; i <= tot; i ++ ) 
			for(int j = 0; j < 26; j ++ ) 
				node[i].ch[j] = 0;
		tot = last = 1;
	}
	inline void extend(int c, int pos, int op) { // op = 1 表示需要修改颜色    将操作看作单点长度改, 链推平 
		int p = last, np = last = ++ tot;
		node[np].len = node[p].len + 1;
		for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
		if(!p) node[np].fa = 1;
		else {
			int q = node[p].ch[c];
			if(node[q].len == node[p].len + 1) node[np].fa = q;
			else {
				int nq = ++ tot;
				node[nq] = node[q]; node[nq].len = node[p].len + 1;
				node[q].fa = node[np].fa = nq;
				for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
			}
		}
		if(op) Ccol(np, pos); // 改到根路径的颜色 
	}
	inline void build(char *s) {
		int n = strlen(s + 1);
		for(int i = 1; i <= n; i ++ ) extend(s[i] - 'a', i, 0);
		for(int i = 1; i <= tot; i ++ ) E[node[i].fa].pb(i);
	}
}
inline void Ccol(int x, int c) { // 修改到根路径上的颜色 
	while(bel[x] != bel[1]) { // 每次跳重链 
		int l = dfn[bel[x]], r = dfn[x];
		assign(l, r, c);
		x = fat[bel[x]];
	}
	int l = dfn[1], r = dfn[x];
	assign(l, r, c);
	change(1, c, 1);
}
void dfs0(int x, int fa) {
	dep[x] = dep[fa] + 1; fat[x] = fa; sz[x] = 1;
	for(auto v : E[x]) {
		dfs0(v, x); sz[x] += sz[v];
		if(sz[v] > sz[big[x]]) big[x] = v;
	}
}
void dfs1(int x, int b) {
	bel[x] = b; bot[b] = x; dfn[x] = ++ dfc; ID[dfc] = x;
	if(big[x]) dfs1(big[x], b);
	for(auto v : E[x]) {
		if(v != big[x]) dfs1(v, v);
	}
}
int main() {
	scanf("%s", s + 1); n = strlen(s + 1);
	SAM::build(s);
	dfs0(1, 0); // 重剖, 给每个点一个 dfn 序 
	dfs1(1, 1);
	scanf("%d", &m);
	for(int i = 1; i <= m; i ++ ) {
		scanf("%d%d", &l[i], &r[i]);
		qry[r[i]].pb(i);
	} 
	for(int i = 1; i <= SAM::tot; i ++ ) mxlen[i] = SAM::node[i].len, mnlen[i] = SAM::node[SAM::node[i].fa].len + 1;
	for(int i = 1; i <= SAM::tot; i ++ ) 
		if(bel[i] == i) odt.insert((range) {dfn[i], dfn[bot[i]], 0}); // 插入重链 
	SAM::clr(); 
	for(int i = 1; i <= n; i ++ ) {
		SAM::extend(s[i] - 'a', i, 1); 
		for(auto v : qry[i]) ans[v] = ask(l[v], r[v]);
	}
	for(int i = 1; i <= m; i ++ ) printf("%lld\n", ans[i]);
	return 0;
}

P4218 [CTSC2010] 珠宝商(点分治,根号分治,分块的重要应用)

神仙题。这个题不仅让我见到了逆天的根号分治,还让我学会了分块的一个重要应用。

题意:
给你一棵 \(n\) 个点的无向树,每个点都有一个字符 \(c_i\)。定义 \(str_{u, v}\)\(u \to v\) 的简单路径上每个点的字符顺次拼接形成的字符串。给你一个长度为 \(m\) 的字符串 \(S\),定义 \(cnt(T)\)\(T\)\(S\) 中的 出现次数。你需要求出:

\[\sum\limits_{u=1}^{n}\sum\limits_{v=1}^{n} cnt(str_{u, v}) \]

\(1 \leq n, m \leq 50000\),保证只有小写字符。

分析:
首先遇到这种静态路径查询的问题,肯定是要 点分治 的。难点在于怎么合并两条重心到某个点路径的信息。
观察到两条路径的字符串会在重心的位置重合,相当于是在 \(T\) 上两个字符串的 \(endpos\)\(beginpos\) 重合。因此我们考虑枚举 \(T\) 的一个位置 \(i\),计算 \(i\) 作为 拼接中心 得到方案数。
设重心为 \(u\),子树内的一个点为 \(x\)。 不难发现只要我们求出有多少条链 \(str_{x, u}\) 能以 \(i\)\(endpos\) 和多少条链 \(str_{u, x}\) 能以 \(i\)\(beginpos\) 就可以求出答案。具体来说,设前者数量为 \(f_i\),后者数量为 \(g_i\),那么 \(f_i \times g_i\) 就是 \(i\) 的贡献。
现在的问题是怎么求 \(f_i\)\(g_i\)
对于 \(f_i\),相当于是每次往一个字符串 \(s\) 前面加一个字符 \(c\),将所有 \(c + s\)\(T\) 中的 \(endpos\)\(+1\)
对于 \(g_i\),相当于每次往一个字符串 \(s\) 后面加一个字符 \(c\),将所有 \(s + c\)\(T\) 中的 \(beginpos\)\(+1\)
那么实际上将 \(T\) 翻转一下 \(g_i\) 的含义就和 \(f_i\) 相同了,因此我们先考虑怎么求 \(f_i\)
建出 \(T\)\(SAM\),那么每次往前面加字符就是 \(Parent\) 树上某个点跳到儿子的过程(也可能留在原地)。我们每次希望将现在所在的节点的所有 \(endpos\)\(+1\),但是直接枚举复杂度肯定不对。考虑到包含每个 \(endpos\) 的前缀节点肯定在当前节点的子树中,因此我们用差分的思想,每次在当前节点处 \(+1\),最后做一遍推平。那么查询 \(f_i\) 就只需要查 \([1, i]\) 对应的前缀节点的答案即可。
但是这样可能会有两条链都在一棵子树的情况,直接用相同方法将每个儿子的不合法方案容斥即可。
\(g_i\) 的过程同理。

但是我们会发现一个问题:单次求一个连通块的答案是 \(O(size + m)\) 的,由于一共会递归 \(O(n)\) 个连通块,因此复杂度为 \(O(nm + n\log n)\)

有一个很神仙的解决方法:
首先我们会一个 \(O(n^2)\) 求答案的做法:枚举一个点为起点,然后暴力往子树里拓展字符串,每次 \(O(1)\) 求当前字符串的出现次数。

那么只需要 根号分治 就可以将刚才的做法变正确:在 \(size > B\)\(O(m)\) 求答案,\(\leq B\) 时跑平方暴力。
复杂度证明:
对于 \(size > B\) 的连通块,我们将分治的过程倒过来建出合并树,容易发现这些连通块与合并出的大连通块的个数之和不超过 \(\frac{n}{B}\),因此复杂度 \(O(\frac{nm}{B})\)
对于 \(size \leq B\) 的连通块,最劣为 \(O(\frac{n}{B} \times B^2 = nB)\)
\(B = \sqrt{m}\) 最优,复杂度 \(O(n \sqrt{m})\)
需要注意一点:当儿子子树的大小 \(\leq B\) 时不能跑 \(O(m)\) 的容斥,应该跑 \(O(size^2)\)。否则一个菊花就能卡掉。

其实还有一种很厉害的 分块 处理,虽然复杂度不如这个,但是更加套路!!
考虑怎么将求答案的复杂度基于连通块大小:
首先 \(f_i \times g_i\) 可以看作维护 \(f_i\) ,每次将 \(g_i\)\(c\) 时都将答案加上 \(c \times f_i\)
维护 \(f_i\) 相当于在正串的 \(Parent\) 树上做子树加,求 \(g_i\) 贡献时相当于在反串的 \(Parent\) 树上做子树查。因此考虑在 \(dfs\) 序上维护。
但是比较大的问题是同一个 \(i\) 在正串和反串的 \(Parent\) 树上对应了不同节点,也就对应了不同的 \(dfs\) 序。我们将每个位置 \(i\) 在正反串 \(Parent\) 树上对应节点的 \(dfs\) 序看作一个二元组 \((x, y)\),那么可以理解成每次修改将 \(x\) 在一段区间的二元组 \(+c\),每次查询就是问 \(y\) 在一段区间的二元组的和。
\(y\) 从小到大排开,改成 \(a_y = x\) 的形式,那么问题就是每次将 值在一段区间的下标加,每次查一段下标的和
这个可以分块:分别对值域和下标两个数组分块,将贡献拆成 整块对区间,散块对整块,散块对散块

  • 整块对区间:预处理值域上每个块对下标的每个前缀的贡献系数(前缀里有多少个值在块里的数)。每次修改整块打标记,查询时每个整块对区间的贡献就是标记乘系数(前缀相减)。
  • 散块对整块:预处理值域的每个前缀对下标的每个块的贡献系数,每次修改散块对每个整块的贡献可以 \(O(1)\) 求出。查询时求区间内的整块和。
  • 散块对散块:每次修改时暴力遍历散块将桶的对应位置 \(+c\),然后查询时枚举散块加上桶的权值。

这样每次修改和查询复杂度都是 \(\sqrt{m}\),总共会修改查询 \(O(n \log n)\) 次,因此总复杂度 \(O(n\sqrt{m} \log n )\)

下面的代码时根号分治做法的:

#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
typedef long long LL;
const int N = 5e4 + 10;
int n, m, B;
vector< int > E[N];
char s[N], t[2][N];
LL res;
struct SAM {
	vector< int > G[N * 2];
	int mxlen[N * 2], mnlen[N * 2];
	int E[N * 2][26]; // 最后的一棵 parent 树 
	int pos[N * 2], f[N * 2], P[N], g[N * 2];
	struct Node {
		int fa, len;
		int ch[26];
	} node[N * 2];
	int tot = 1, last = 1;
	inline void extend(int c, int o) {
		int p = last, np = last = ++ tot;
		node[np].len = node[p].len + 1;	
		pos[np] = o; P[o] = np;
		for(; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
		if(!p) node[np].fa = 1;
		else {
			int q = node[p].ch[c];
			if(node[q].len == node[p].len + 1) node[np].fa = q;
			else {
				int nq = ++ tot;
				node[nq] = node[q]; node[nq].len = node[p].len + 1;
				node[q].fa = node[np].fa = nq; pos[nq] = o;
				for(; p && node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
			}
		}
	}
	void dfs(int x) {
		for(int i = 0; i < 26; i ++ ) 
			if(E[x][i]) dfs(E[x][i]), g[x] += g[E[x][i]];
	}
	inline void build(char *s) {
		int n = strlen(s + 1);
		for(int i = 1; i <= n; i ++ ) extend(s[i] - 'a', i);
		for(int i = 2; i <= tot; i ++ ) {
			int c = s[pos[i] - node[node[i].fa].len] - 'a';
			G[node[i].fa].pb(i);
			E[node[i].fa][c] = i;
			mxlen[i] = node[i].len, mnlen[i] = node[node[i].fa].len + 1;
		}
		for(int i = 1; i <= n; i ++ ) g[P[i]] ++;
		dfs(1);
	}
	void roll(int x) {for(auto v : G[x]) f[v] += f[x], roll(v);}
	inline void add(int p) {/*cerr << p << ' ' << endl;*/ f[p] ++;}
	inline void clr() {for(int i = 0; i <= tot; i ++ ) f[i] = 0;} // 清空 
} T[2]; // 翻转前,翻转后 
bool vis[N];
int root, all, sz[N], Maxn[N];
LL ans;
void get_root(int x, int fa) {
	sz[x] = 1; Maxn[x] = 0;
	for(auto v : E[x]) {
		if(v == fa || vis[v]) continue;
		get_root(v, x); sz[x] += sz[v];
		Maxn[x] = max(Maxn[x], sz[v]);
	}
	Maxn[x] = max(Maxn[x], all - sz[x]);
	if(Maxn[x] < Maxn[root]) root = x;
}
void get_sz(int x, int fa) {
	sz[x] = 1;
	for(auto v : E[x]) {
		if(v == fa || vis[v]) continue;
		get_sz(v, x); sz[x] += sz[v]; 
	}
}
void calc(int x, int fa, int p) { // p 表示对应状态机的节点
	p = T[0].node[p].ch[s[x] - 'a'];
	if(!p) return ;
	ans += T[0].g[p]; 
	for(auto v : E[x]) {
		if(vis[v] || v == fa) continue;
		calc(v, x, p);
	}
}
inline void bf(int x, int fa) {
	calc(x, 0, 1);
	for(auto v : E[x]) {
		if(vis[v] || v == fa) continue;
		bf(v, x);
	}
}
void dfs1(int x, int fa, int p, int l, int o) { // 知道 p, 知道当前长度可以知道下一个 
	if(l < T[o].mxlen[p]) {
		int c = t[o][T[o].pos[p] - l] - 'a';
		if(s[x] - 'a' == c) ;
		else p = 0;
	}
	else p = T[o].E[p][s[x] - 'a'];
	if(!p) return ;
	T[o].add(p); l ++;
	for(auto v : E[x]) {
		if(vis[v] || v == fa) continue;
		dfs1(v, x, p, l, o);
	}
}
void dfs3(int x, int fa, int p) { // 这个是往后加 
	p = T[0].node[p].ch[s[x] - 'a'];
	if(!p) return ;
	ans -= T[0].g[p];
	for(auto v : E[x]) {
		if(vis[v] || v == fa) continue;
		dfs3(v, x, p);
	}
}
void dfs2(int x, int fa, int st, int p, int l) { // 这个是往前加 
	if(l < T[0].mxlen[p]) {
		int c = t[0][T[0].pos[p] - l] - 'a';
		if(s[x] - 'a' == c) ;
		else p = 0;
	}
	else p = T[0].E[p][s[x] - 'a'];
	if(!p) return ;
	l ++;
	dfs3(st, 0, p); // 往后补, 每次的贡献是 g 
	for(auto v : E[x]) {
		if(vis[v] || v == fa) continue;
		dfs2(v, x, st, p, l);
	}
}
inline LL get(int op) { // O(m) 求出现的次数和   op = 1 必须要拼 
	LL ret = 0;
	T[0].roll(1); T[1].roll(1);
	for(int i = 1; i <= m; i ++ ) {
		if(op == 0) ret += T[0].f[T[0].P[i]];
		if(i < m) ret += 1LL * T[0].f[T[0].P[i]] * T[1].f[T[1].P[m - i]];
	}
	return ret;
}
void solve(int rt) { // 点分治 
	if(sz[rt] <= B) {res = ans; bf(rt, 0); return ;} // 暴力 
	else {
		res = ans;
		vis[rt] = 1;
		T[0].clr(); T[1].clr();
		int p = T[0].node[1].ch[s[rt] - 'a'];
		dfs1(rt, 0, 1, 0, 0); // 正串加 
 		for(auto v : E[rt]) {
			if(vis[v]) continue;
			dfs1(v, rt, 1, 0, 1); // 反串加   相乘是跨过, 直接加是一条链 
		}
		ans += get(0); // O(m) 求答案 
		for(auto v : E[rt]) { // 考虑容斥 
			if(vis[v]) continue;
			if(sz[v] <= B) { // 平方的容斥 
				dfs2(v, rt, v, p, 1); // 第一条链起点在 x 终点在子树里, 第二条链起点在 v,中点在子树里 
			}
			else { // O(m) 的容斥 
				T[0].clr(); T[1].clr();
				dfs1(v, rt, p, 1, 0);
				dfs1(v, rt, 1, 0, 1);
				ans -= get(1);
			}
		}
		res = ans - res;
		for(auto v : E[rt]) { // 继续点分治 
			if(vis[v]) continue;
			all = sz[v], root = 0;
			get_root(v, 0);
			get_sz(root, 0);
			solve(root);
		}
	}
}
int main() {
	scanf("%d%d", &n, &m); B = sqrt(n);
	for(int i = 1; i < n; i ++ ) {
		int u, v; scanf("%d%d", &u, &v);
		E[u].pb(v); E[v].pb(u);
	}
	scanf("%s", s + 1); 
	scanf("%s", t[0] + 1); T[0].build(t[0]);
	for(int i = 1; i <= m; i ++ ) t[1][i] = t[0][i];
	reverse(t[1] + 1, t[1] + m + 1); T[1].build(t[1]);
	all = n; root = 0; Maxn[0] = n + 1;
	get_root(1, 0);
	get_sz(root, 0);
	solve(root);
	cout << ans << endl;
	return 0;
}
posted @ 2026-09-10 14:55  Czl2007  阅读(5)  评论(0)    收藏  举报