AIGC标识 3.17 模拟赛总结(虚树求交,FWT/容斥, 后缀数组SA)

时间安排与反思

  • \(7:30 - 7:50\) 首先通读了一遍题目。发现 \(T1\) 的距离会受到上一个点选择的位置的影响,感觉非常困难。 \(T2\) 是那种类似 \(FMT\) 的题目,感觉不一定会正解,但是应该可以拿不少部分分。 \(T3\) 是个字符串,一眼 \(SA\)
  • \(7:50 - 8:30\) 选择从看起来最简单的 \(T3\) 开:难点在于怎么同时考虑两个位置的 \(lcp\)\(lcs\) 以及怎么满足限制。然后想到可以考虑从两个字符串的 \(lcs\) 开头考虑计算贡献,那么这样的开头只需要满足上一个位置的字符不相等。然后长度限制就相当于一段区间为中间点合法。那么这题做完了??!于是先去写了一个字符集大小为 \(2\) 的,然后交上去得了 \(15pts\)
  • \(8:30 - 9:13\) 意识到我的做法是对的,然后将字符集改成 \(26\) 也是非常简单的,先交了一个暴力的 \(26^2 \times n\) 的。发现 \(WA\) 了??!!然后就一直调,最后发现当贡献为 \(0\) 时我没合并两个连通块的信息,而且还有一些别的小错误。然后就过了。感觉这题数据非常水,错误的能有 \(80pts\),而且 \(26^2n\) 就过了。
  • \(9:15 - 9:30\) 尝试思考 \(T1\) 无果,然后就准备先去写 \(T2\)
  • \(9:30 - 10:13\) 发现 \(T2\) 暴力 + 性质就有 \(30pts\),写了一发,中间调了调就过了。
  • \(10:13 - 10:36\) 想了一小会儿会了 \(m = 1\) 的,写了一发每测数据就过了。
  • \(10:36 - 11:00\) 又想了想 \(T2\),感觉不会任何别的分了就去看 \(T1\) 了。
  • \(11:00 - 12:02\) 经过 \(1h\) 的鏖战猜了一手结论,写了链的部分分。发现是对的。那感觉有好多分就可以拿了啊!!但是时间不太够了。
  • \(12:02 - 12:20\) 尝试再写 \(T1\)\(25pts\) 失败了。

总得分就是:15 + 40 + 100 = 155
由于是 \(IOI\) 赛制因此不存在挂分现象。
\(T1,T2\) 确实都是很难的题,并且 \(T1\) 非常难写。

反思:
本场考试节奏还是比较不错的,主要的原因是开题顺序把握对了。因此打模拟赛一定要先把题都看一遍从最有把握做出来的题开。
\(T1\) 没有再多拿一些分还是比较可惜的。原因在于猜结论的能力不够强,这方面还要多锻炼。

题解

T1.悲伤(Doloris)

题意:
在这里插入图片描述
在这里插入图片描述

分析:
写了 \(7.7k\) 的大芬。

首先有一个结论:答案一定是连续的一段。这是因为每条边是连续的,可以考虑一个过程:首先所有点都在最小值的局面,然后将它们向最大值的局面逼近,那么答案一定是连续增大的,因此答案连续。

那么只需要求出最大值和最小值就好了。

对最大值:
首先有一个结论:一定存在一种情况满足每一组的点都取在叶子上,且能取到最大值。
证明是考虑如果有一组不在叶子上,那么可以向某个叶子移动使得答案不会变劣。
因此就有了一种暴力的 \(dp\)\(dp_{i, x}\) 表示第 \(i\) 组的点取到 \(x\) 的最大值。暴力转移是 \(O(n^2)\) 的。
优化的话可以每次对相邻两组建出虚树然后在虚树上跑换根 \(dp\)。也可以用数据结构维护。下面介绍题解给的妙妙做法:
对第 \(i\) 组虚树而言,将叶子 \(x\) 向外连出一条长度为 \(dp_{i, x}\) 的边,这里需要建出一个新点。然后求出这棵虚树的直径,那么下一组的 \(dp\) 值就是从直径的两个端点转移。求直径可以不用建出虚树,只需要维护出当前直径端点,每次往点集里加点是用新加入的点更新直径即可。

对最小值:
还是有结论:每次一定是从一个最小值点贪心的移动最少步数取到最小值。
证明的话感性理解。
那么可以这样做:
维护取到最小值的点集,这显然是一个连通块,记作 \(S\)
对于当前的虚树 \(T\),首先求出 \(S' = S \cap T\)。若 \(S'\) 不为空,则令 \(S = S'\)
否则求出 \(T\) 中距离 \(S\) 最近的点 \(x\),设最小距离为 \(d\),让答案加上 \(d\),更新 \(S = \{x\}\)

那么如果说某一次两棵虚树的交为空,往后 \(S\) 就一直是一个点了。那么只需要解决两个问题:

  1. 如何对两棵虚树求交 ?
  2. 如何求一个点 \(x\) 到一棵虚树 \(T\) 的最近距离以及最近点 \(y\)

对于第一个问题:
考虑将整棵树重剖一下。那么每条重链是独立的,每次分别求交即可。设虚树的叶子数为 \(c\),那么它会分布在 \(c \times \log_2 n\) 条重链上,每次对这些重链求交,其余的重链都要删掉,可以通过 时间戳 标记来判断是否已经被删掉了。时间复杂度 \(O(\sum k_i \times \log_2 n)\)

对于第二个问题:
设虚树的根为 \(root\)
由于每棵虚树只会被求一次,因此可以设计一种基于叶子数复杂度的做法。
还是考虑刚才的树剖:
\(x\)\(root\) 的子树中时,那么我们枚举这 \(c \times \log_2 n\) 条链,每次求出 \(x\) 与虚树在链上最下面的点的 \(lca\),那么这个 \(lca\) 一定在虚树上,并且每次用 \(x\)\(lca\) 的距离更新最小值就是答案。
\(x\) 不在 \(root\) 的子树中时,那么 \(root\) 就是距离 \(x\) 最近的点并且它们的距离就是最小值。

其实还有一个问题:当第一次两棵虚树不交时,怎么求出两棵虚树之间的最小距离以及在第二棵虚树上的点?
发现最小距离下一定有一棵虚树取到了根,两次都做一遍 点到虚树最小距离 取较小就是答案。

总时间复杂度 \(O(n \log_2 n + \sum k_i \log_2 n)\)

CODE:

#include<bits/stdc++.h>
#define pb emplace_back
#define MP make_pair
using namespace std;
const int N = 5e5 + 10;
typedef long long LL;
typedef __int128 Int;
typedef pair< int, int > PII;
void write(Int x) {
	if(x < 0) putchar('-'), x = -x;
	if(x > 9) write(x / 10);
	putchar(x % 10 + '0');
}
int n, m;
vector< int > node[N];
struct edge {
	int v, last; LL w;
} E[N * 2];
int tot, head[N];
inline void add(int u, int v, LL w) {
	E[++ tot] = (edge) {v, head[u], w};
	head[u] = tot;
}
Int Dis[N];
int dfn[N], dfc;
int dep[N], euc, euler[N * 2], st[20][N * 2], pos[N];
void dfs0(int x, int fa) {
	dfn[x] = ++ dfc;
	euler[++ euc] = x; dep[x] = dep[fa] + 1; pos[x] = euc;
	for(int i = head[x]; i; i = E[i].last) {
		int v = E[i].v; LL w = E[i].w;
		if(v == fa) continue;
		Dis[v] = Dis[x] + w; 
		dfs0(v, x); euler[++ euc] = x;
	}
}
void build_st() {
	for(int i = 1; i <= euc; i ++ ) st[0][i] = euler[i];
	for(int i = 1; (1 << i) <= euc; i ++ ) 
		for(int j = 1; j + (1 << i) - 1 <= euc; j ++ )
			st[i][j] = (dep[st[i - 1][j]] <= dep[st[i - 1][j + (1 << i - 1)]] ? st[i - 1][j] : st[i - 1][j + (1 << i - 1)]);
}
int query(int l, int r) {
	int k = log2(r - l + 1);
	return dep[st[k][l]] <= dep[st[k][r - (1 << k) + 1]] ? st[k][l] : st[k][r - (1 << k) + 1];
}
int lca(int x, int y) {
	int l = pos[x], r = pos[y];
	if(l > r) swap(l, r);
	return query(l, r);
}
Int dis(int x, int y) {
	int lc = lca(x, y);
	return Dis[x] + Dis[y] - 2 * Dis[lc];
}
void pre() {
	dfs0(1, 0);
	build_st();
}
namespace Part1 { // 求最大值  
	int tot, tmp[N * 2], len, bel[N * 2], rk;
	Int d[N * 2];
	PII dia[N]; // 直径
	bool vis[N * 2];
	vector< Int > dp[N];
	struct edge {
		int v, last; Int w;
	} E[N * 4];
	int head[N * 2];
	inline void Add(int u, int v, Int w) {
		E[++ tot] = (edge) {v, head[u], w};
		head[u] = tot;
	}
	bool cmp(int x, int y) {return dfn[x] < dfn[y];}
	void get_diameter(int x) {
		int s = tmp[1]; d[s] = 0; vis[s] = 1;
		queue< int > q; q.push(s);
		while(!q.empty()) {
			int u = q.front(); q.pop();
			for(int i = head[u]; i; i = E[i].last) {
				int v = E[i].v; Int w = E[i].w;
				if(vis[v]) continue;
				d[v] = d[u] + w; q.push(v); vis[v] = 1;
			}
 		}
 		Int mx = -1; int S, T;
 		for(int i = 1; i <= len; i ++ ) {
			if(d[tmp[i]] > mx) mx = d[tmp[i]], S = tmp[i];
		}
		for(int i = 1; i <= len; i ++ ) d[tmp[i]] = 0, vis[tmp[i]] = 0;
		q.push(S); vis[S] = 1;
		while(!q.empty()) {
			int u = q.front(); q.pop();
			for(int i = head[u]; i; i = E[i].last) {
				int v = E[i].v; Int w = E[i].w;
				if(vis[v]) continue;
				d[v] = d[u] + w; q.push(v); vis[v] = 1;
			}
		}
		mx = -1;
		for(int i = 1; i <= len; i ++ ) {
			if(d[tmp[i]] > mx) mx = d[tmp[i]], T = tmp[i];
		}
		for(int i = 1; i <= len; i ++ ) d[tmp[i]] = 0, vis[tmp[i]] = 0;
		dia[x] = MP(bel[S], bel[T]);		
	}
	void build_tree(int x) { // 建出虚树
		len = 0; tot = 0;
		for(int i = 0; i < node[x].size(); i ++ ) {
			tmp[++ len] = node[x][i]; bel[node[x][i]] = i;
		} 
		sort(tmp + 1, tmp + len + 1, cmp);
		int l = len;
		for(int i = 1; i < l; i ++ ) tmp[++ len] = lca(tmp[i], tmp[i + 1]);
		sort(tmp + 1, tmp + len + 1, cmp);
		len = unique(tmp + 1, tmp + len + 1) - (tmp + 1); // 去重
		for(int i = 1; i < len; i ++ ) {
			int lc = lca(tmp[i], tmp[i + 1]);
			Add(tmp[i + 1], lc, dis(tmp[i + 1], lc));
			Add(lc, tmp[i + 1], dis(tmp[i + 1], lc));
		}
		rk = n;
		for(int i = 0; i < node[x].size(); i ++ ) {
			rk ++; bel[rk] = i; tmp[++ len] = rk;
			Add(node[x][i], rk, dp[x][i]);
			Add(rk, node[x][i], dp[x][i]);
		}
		get_diameter(x);
		for(int i = 1; i <= len; i ++ ) head[tmp[i]] = 0;
	}
	Int solve() {
		for(int i = 1; i <= m; i ++ ) {
			if(i == 1) for(int j = 1; j <= node[i].size(); j ++ ) dp[i].pb(0);
			else {
				for(int j = 0; j < node[i].size(); j ++ ) {
					int u = node[i][j];
					Int dpv = max(dp[i - 1][dia[i - 1].first] + dis(u, node[i - 1][dia[i - 1].first]), dp[i - 1][dia[i - 1].second] + dis(u, node[i - 1][dia[i - 1].second]));
					dp[i].pb(dpv);
				}
			}
			build_tree(i);
		}
		Int mx = 0;
		for(int i = 0; i < dp[m].size(); i ++ ) mx = max(mx, dp[m][i]);
		return mx;
	}
}
namespace Part2 { // 求最小值  首先树剖, 那么一棵虚树只会被划分成 关键点数条链
	int sz[N], big[N], fat[N];
	int dfn[N], bel[N], bot[N], idx[N], dfc, rk, ID[N];
	void dfs0(int x, int fa) {
		sz[x] = 1; fat[x] = fa;
		for(int i = head[x]; i; i = E[i].last) {
			int v = E[i].v;
			if(v == fa) continue;
			dfs0(v, x); sz[x] += sz[v];
			if(sz[v] > sz[big[x]]) big[x] = v; 
		}
	}
	int nl[N], nr[N], tl[N], tr[N], L[N], R[N], cnt; // 每条重链当前的左右区间
	int tims[N]; // 相当于每条链的时间戳
	void dfs_chain(int x, int b) {
		dfn[x] = ++ dfc; ID[dfc] = x; bel[x] = b; bot[b] = x; if(x == b) idx[b] = ++ rk; 
		if(big[x]) dfs_chain(big[x], b);
		for(int i = head[x]; i; i = E[i].last) {
			int v = E[i].v;
			if(v == fat[x] || v == big[x]) continue;
			dfs_chain(v, v);
		}
	}
	bool cmp(int x, int y) {return dfn[x] < dfn[y];}
	int get_rt(int x) {
		int rt = node[x][0];
		for(int i = 1; i < node[x].size(); i ++ ) rt = lca(rt, node[x][i]);
		return rt;
	}
	struct range {
		int idx, l, r;
	};
	struct shortest {
		Int d; int x; // 距离和最近点
	};
	shortest Find(int x, vector< range > T, int rt) { // x 找到 T 里面距离最小的,并且返回距离和编号
		if(lca(x, rt) != rt) return (shortest) {dis(x, rt), rt};
		else { 
			Int mn = 1e18; int p;
			 for(auto v : T) {
				 int u = ID[v.r];
				 int lc = lca(u, x);
				 if(dis(x, lc) < mn) mn = dis(x, lc), p = lc;
			 }
			 return (shortest) {mn, p};
		}
	}
	Int solve() {
		Int ans = 0;
		dfs0(1, 0); // 树剖, 然后求出每条重联的编号
		dfs_chain(1, 1);
		for(int i = 1; i <= n; i ++ ) {
			if(bel[i] == i) {cnt ++; L[idx[i]] = dfn[i]; R[idx[i]] = dfn[bot[i]];}
		}
		for(int i = 1; i <= cnt; i ++ ) tl[i] = n + 1, tr[i] = 0;
		int nx = -1; // -1 表示还有点集
		for(int i = 1; i <= m; i ++ ) {
			vector< range > Idx;
			int rt = get_rt(i);
			for(auto x : node[i]) { // 从 x 暴力往上跳
				int y = x;
				while(dep[bel[y]] >= dep[rt]) {
					Idx.pb((range){idx[bel[y]], dfn[bel[y]], dfn[y]});
					y = fat[bel[y]];
				}
				if(dep[y] >= dep[rt]) Idx.pb((range) {idx[bel[y]], dfn[rt], dfn[y]});
			}
			if(nx == -1) {				
				for(auto v : Idx) tl[v.idx] = min(tl[v.idx], v.l), tr[v.idx] = max(tr[v.idx], v.r); 
				int num = 0;
				for(auto v : Idx) {
					if(tims[v.idx] == i - 1 && R[v.idx] >= L[v.idx]) {
						nr[v.idx] = min(R[v.idx], tr[v.idx]);
						nl[v.idx] = max(L[v.idx], tl[v.idx]);
						if(nr[v.idx] >= nl[v.idx]) num ++;
						tims[v.idx] = i;
					}
				}
				for(auto v : Idx) tl[v.idx] = n + 1, tr[v.idx] = 0;
				if(num == 0) { // 获取 nx, 那么发现一定是上一个取根这一个取对应最小 或者这一个取根上一个取对应最小
					vector< range > T; int mnid = 1e8;
					for(int j = 1; j <= cnt; j ++ ) {
						if(tims[j] >= i - 1 && L[j] <= R[j]) T.pb((range) {j, L[j], R[j]}), mnid = min(mnid, L[j]);
					}
					int root = ID[mnid];
					shortest res1 = Find(rt, T, root);
					shortest res2 = Find(root, Idx, rt);
					if(res1.d < res2.d) nx = rt, ans += res1.d;
					else nx = res2.x, ans += res2.d;
				}
				else {
					for(auto v : Idx) L[v.idx] = nl[v.idx], R[v.idx] = nr[v.idx];
				}
			}
			else { // 有一个最小值点,那么扫描每条链求一个最小值就是答案
				shortest res = Find(nx, Idx, rt);
				ans += res.d; nx = res.x;
			}
		} 
		return ans;
	}
}
int main() {
	freopen("doloris.in", "r", stdin);
	freopen("doloris.out", "w", stdout);
	scanf("%d", &n);
	for(int i = 1; i < n; i ++ ) {
		int u, v; LL w; scanf("%d%d%lld", &u, &v, &w);
		add(u, v, w); add(v, u, w);
	}
	scanf("%d", &m);
	for(int i = 1; i <= m; i ++ ) {
		int k; scanf("%d", &k);
		for(int j = 1; j <= k; j ++ ) {
			int x; scanf("%d", &x);
			node[i].pb(x);
		}
	}
	pre(); // 预处理 O(1) lca
	Int mx = Part1::solve();
	Int mn = Part2::solve();
	write(mx - mn + 1);
	return 0;
}

T2. 忘却(Oblivionis)

题意:
给定 \(n \times m\) 的矩阵 \(A_{1, 1},..., A_{n, m}\),每个 \(A_{i, j} \in \{0,1,2,..., 2^k - 1\}\)
定义函数 \(f(x) = [popcount(x) \in 2 \times Z + 1]\),即 \(x\)\(popcount\) 是奇数时为 \(1\) 否则为 \(0\)
现在你需要对于 \(x = 0,1,...,2^k - 1\),求出下面式子的值:

\[ans_{x} = \sum\limits_{i = 1}^{n}w_i \prod\limits_{j = 1}^{m}f(x \& A_{i, j}) \]

\(1 \leq n \leq 10^5\)\(1 \leq m \leq 10\)\(1 \leq k \leq 20\)

分析:
先来考虑 \(m = 1\) 怎么做:
\(g_{x} = \sum_{i = 1}^{n}w_i[A_{i, 1} = x]\)
注意到 \(FWTxor(g)_{i} = \sum\limits_{ j}(-1)^{|i \cap j|}g_j\),因此可以对 \(x\) 求出不合法的贡献减去合法的贡献。又因为合法与不合法的贡献和一定,因此可以求出合法的贡献。

对于 \(m > 1\)
先来考虑枚举 \(x\)
肯定是要想想怎么容斥的。对于 \(m = 1\),发现合法的数的系数是 \(-1\),不合法的数的系数是 \(1\)。如果一个集合的系数能是 \((-1)^{合法数个数}\),那么是不是就可以考虑二项式定理将存在不合法的数的集合的系数消成 \(0\) 呢?
也就是说,对于一个集合 \(|S|\) 而言,我们想让系数为 \(\prod\limits_{y \in S}(-1)^{|y \cap x|}\),并且让集合内的元素有联系。
考虑 \(FWTxor\) 里面的关系式: \((-1)^{|a \cap c|} \times (-1)^{|b \cap c|} = (-1)^{|(a \oplus b) \cap c|}\)
进一步的,能得到:\((-1)^{|S_1 \cap c|} \times (-1)^{|S_2 \cap c|} \times ,..., \times (-1)^{|S_k \cap c|} = (-1)^{|(S1 \oplus S_2 \oplus ... \oplus S_k) \cap c|}\)
这个可以一直运用关系式合并得到。
那么就有了 \(\prod\limits_{y \in S}(-1)^{|y \cap x|} = (-1)^{|B_S \cap x|}\),其中 \(B_s\) 表示集合 \(S\) 里面所有数的异或和。

假设第 \(i\) 行里面存在不合法的数,考虑怎么列一个式子把它的系数消成 \(0\)
\(B_{i, S}\) 表示第 \(i\) 行选择 \(S\) 位置集合上的数的异或和。\(C_{i, S} = (-1)^{|B_{i, S} \cap x|}\)
考虑到让不合法的为 \(-1\) 更加自然,因此我们乘一个 \((-1)^{|S|}\)
假设第 \(i\)\(l\) 个不合法的,那么有:
\(\sum\limits_{S}(-1)^{|S|} \times (-1)^{|B_{i, S} \cap x|}\)
\(=\sum\limits_{i = 0}^{l}\sum\limits_{j = 0}^{m-l}(-1)^{i +j} \times (-1)^{j}\)
\(=\sum\limits_{i = 0}^{l}\sum\limits_{j = 0}^{m-l}(-1)^{i}\)
\(=\sum\limits_{j = 0}^{m - l}0^l\)

\(l = 0\) 时值为 \(2^m\),否则为 \(0\)

因此这样就把不合法的容斥掉了。
那么答案:

\[h_{x} = \frac{\sum\limits_{i = 1}^{n}w_i \times \sum\limits_{S}(-1)^{|S|} \times (-1)^{|B_{i, S} \cap x|}}{2^m} \]

先不管 \(2^m\),最后除掉就行:

\[h_x = \sum\limits_{y}(-1)^{|y \cap x|} \sum\limits_{i = 1}^{n}w_i \times \sum\limits_{S}(-1)^{|S|}[B_{i, S} = y] \]

那么设 \(g(y) = \sum\limits_{i = 1}^{n}w_i \times \sum\limits_{S}(-1)^{|S|}[B_{i, S} = y]\),发现此时 \(g(y)\)\(x\) 已经没有关系了。可以在 \(O(n \times 2^m)\) 求出 \(g\)

那么

\[h(x) = \sum\limits_{y}(-1)^{|y\cap x|}g(y) \]

发现 \(h = FWTxor(g)\),因此可以在 \(O(2^k \times k)\) 的复杂度求出。

总复杂度 \(O(n \times 2^m + 2^k \times k)\)

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
const int M = 11;
typedef long long LL;
const LL mod = 998244353;
int n, m, K, a[N][M], b[1 << 10];
int bit[1 << 10], cnt[1 << 10];
LL w[N], inv, g[1 << 20], mi[1 << 20];
inline int lowbit(int x) {return x & -x;}
inline int sign(int x) {return (x & 1) ? -1 : 1;}
namespace FWT {
	inline void XOR(LL *f, int n, LL opt) {
		for(int o = 2, k = 1; o <= n; o <<= 1, k <<= 1 )
			for(int i = 0; i < n; i += o )
				for(int j = 0; j < k; j ++ ) 
					f[i + j] = f[i + j] + f[i + j + k],
				    f[i + j + k] = f[i + j] - 2 * f[i + j + k], 
				    f[i + j] = f[i + j] * opt, f[i + j + k] = f[i + j + k] * opt;
	}
}
int main() {
	freopen("oblivionis.in", "r", stdin);
	freopen("oblivionis.out", "w", stdout);
	scanf("%d%d%d", &n, &m, &K);
	for(int i = 0; i < m; i ++ ) bit[1 << i] = i;
	for(int i = 0; i < (1 << m); i ++ ) cnt[i] = cnt[i >> 1] + (i & 1);
	mi[0] = 1; for(int i = 1; i < (1 << K); i ++ ) mi[i] = mi[i - 1] * 3 % mod; 
	for(int i = 1; i <= n; i ++ ) scanf("%lld", &w[i]);
	for(int i = 1; i <= n; i ++ ) 
	    for(int j = 1; j <= m; j ++ ) 
	        scanf("%d", &a[i][j]);
	inv = 1; for(int i = 1; i <= m; i ++ ) inv = inv * 2;
	for(int i = 1; i <= n; i ++ ) {
		for(int j = 1; j < (1 << m); j ++ ) {
			b[j] = (b[j ^ lowbit(j)] ^ a[i][bit[lowbit(j)] + 1]);
		}
		for(int j = 0; j < (1 << m); j ++ ) g[b[j]] += sign(cnt[j]) * w[i];
	}
	FWT::XOR(g, 1 << K, 1);
	LL res = 0;
	for(int i = 0; i < (1 << K); i ++ ) res = (res + (((g[i] / inv) ^ i) % mod) * mi[i] % mod) % mod;
	cout << res << endl;
	return 0;
}

T3.死亡(Mortis)

原题链接

分析:
比较水的 \(T3\)
考虑两个位置 \(i, j\)\(LCS\) 是往前第一个不能匹配的位置,假设分别为 \(p, q\)。那么满足什么条件呢?
很容易想到 \(S_{p - 1} \ne S_{q - 1}\)
因此我们考虑对所有 \(p, q\) 计算答案。
那么可以将后缀按照字符集分成 \(26\) 种。只有不同的两种之间存在贡献。
可以求出来任意两个 \(p, q\)\(LCP\),那么题上给的限制相当于是中间点在一段区间里。
按照 \(height\) 从小到大合并连通块,合并过程中就维护了 \(lcp\),因此只需要记录每个连通块内每种后缀的数量。然后合并时就可以 \(O(1)\) 计算贡献。

复杂度 \(O(n \log n + 26n)\),考试的时候写的暴力枚举两种不同的不知道为啥也过了。

CODE:

#include<bits/stdc++.h>
#define pb emplace_back
using namespace std;
typedef long long LL;
typedef unsigned long long ull;
const int N = 3e5 + 10;
ull ans, f[N], cnt[N][27];
int n, A, B, bin[N];
int sa[N], height[N], rk[N];
int odr[N];
char str[N];
namespace SA {
	int m, x[N * 2], y[N * 2], c[N];
	inline void get_sa() {
		m = 122;
		for(int i = 1; i <= n; i ++ ) c[x[i] = str[i]] ++;
		for(int i = 1; i <= m; i ++ ) c[i] += c[i - 1];
		for(int i = n; i; i -- ) sa[c[x[i]] --] = i;
		for(int k = 1; k <= n; k <<= 1) {
			int num = 0;
			for(int i = n - k + 1; i <= n; i ++ ) y[++ num] = i;
			for(int i = 1; i <= n; i ++ ) 
			    if(sa[i] > k) y[++ num] = sa[i] - k;
			for(int i = 0; i <= m; i ++ ) c[i] = 0;
			for(int i = 1; i <= n; i ++ ) c[x[i]] ++;
			for(int i = 1; i <= m; i ++ ) c[i] += c[i - 1];
			for(int i = n; i; i -- ) sa[c[x[y[i]]] --] = y[i], y[i] = 0;
			swap(x, y);
			x[sa[1]] = 1, num = 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]) ? num : ++ num;
			if(num == n) break;
			m = num;
		}
	}
	inline void get_height() {
		for(int i = 1; i <= n; i ++ ) rk[sa[i]] = i;
		for(int i = 1, k = 0; i <= n; i ++ ) {
			if(rk[i] == 1) continue;
			if(k) k --;
			int j = sa[rk[i] - 1];
			while(i + k <= n && j + k <= n && str[i + k] == str[j + k]) k ++;
			height[rk[i]] = k;
		}
	}
}
bool cmp(int x, int y) {return height[x] > height[y];}
int Find(int x) {return x == bin[x] ? x : bin[x] = Find(bin[x]);}
vector< int > S[N];
void Merge(int x, int y, ull w) {
	int f1 = Find(x), f2 = Find(y);
	ull l = max(1, (int)w - A + 1); ull r = min((ull)(B), w);
	if(l > r) {
		bin[f1] = f2;
		for(int i = 0; i <= 26; i ++ ) cnt[f2][i] += cnt[f1][i];
		return ;
	}
	for(int i = 0; i <= 26; i ++ ) {
		for(int j = 0; j <= 26; j ++ ) {
			if(i == j) continue;
			ull c1 = cnt[f1][i], c2 = cnt[f2][j];
			ans += c1 * c2 * ((w + 1) * ((l + r) * (r - l + 1) / 2) - (f[r] - f[l - 1]));
		}
	}
	for(auto v : S[f1]) S[f2].pb(v);
	bin[f1] = f2;
	for(int i = 0; i <= 26; i ++ ) cnt[f2][i] += cnt[f1][i];
}
int main() {
	freopen("mortis.in", "r", stdin);
	freopen("mortis.out", "w", stdout);
	scanf("%s", str + 1); n = strlen(str + 1);
	scanf("%d%d", &A, &B);
	SA::get_sa(); SA::get_height();
	for(int i = 1; i < N; i ++ ) f[i] = f[i - 1] + (ull)(i) * (ull)(i);
	for(int i = 1; i <= n; i ++ ) {
		bin[i] = i;
		if(i == 1) cnt[i][0] ++;
		else cnt[i][str[i - 1] - 'a' + 1] ++;
		S[i].pb(i);
	}
	for(int i = 1; i <= n; i ++ ) odr[i] = i;
	sort(odr + 1, odr + n + 1, cmp);
	for(int i = 1; i <= n; i ++ ) {
		int o = odr[i];
		if(o == 1) continue;
		int x = sa[o - 1], y = sa[o];
		Merge(x, y, (ull)height[o]);
	}
	cout << ans << endl;
	return 0;
}
posted @ 2026-09-10 20:11  Czl2007  阅读(9)  评论(0)    收藏  举报