字符串

字符串太菜了,补一补。


哈希

P8819 [CSP-S 2022] 星战

首先如果保证了每个点出度都为 \(1\),那每个点一定可以走到一个环(原因显然)。

暴力维护时 \(O(nq)\) 的。考虑出度不好维护,我们维护入度。每个点随机一个权值 \(w_u\),每个点的入度哈希函数 \(s_u\) 为其入边的 \(w_u\) 之和 以及 所有点的 \(s_u\) 之和。四个操作如下:

  • 废除 \(u \to v\)\(s_v \leftarrow s_v - w_u\)

  • 废除点 \(u\)\(s_u \leftarrow 0\)

  • 加入 \(u \to v\)\(s_v \leftarrow s_v + w_u\)

  • 加入点 \(u\)\(s_u \leftarrow t_u\)\(t_u\) 是一开始的 \(s_u\));

然后你会发现合法当且仅当 \(S = \sum_{i = 1}^n t_u\)。正确性是比较显然的。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define ll long long
using namespace std;
const int N = 5e5 + 5;
int n, m, q; vector<int> G[N]; ll S, p, w[N], s[N], a[N]; mt19937 R(time(0));
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n >> m; int op, u, v;
	_for (i, 1, n)  p += w[i] = R();
	_for (i, 1, m)  cin >> u >> v, a[v] += w[u], s[v] += w[u], S += w[u]; cin >> q;
	while (q -- ) {
		cin >> op >> u;
		if (op == 1)  cin >> v, s[v] -= w[u], S -= w[u];
		else if (op == 2)  S -= s[u], s[u] = 0;
		else if (op == 3)  cin >> v, s[v] += w[u], S += w[u];
		else  S += a[u] - s[u], s[u] = a[u]; cout << (S == p ? "YES" : "NO") << "\n";
	}
	return 0;
}

P4503 [CTSC2014] 企鹅 QQ

由于每个字符串的长度只有 \(200\),枚举每一位,然后用哈希枚举字符串判断即可。复杂度 \(O(nL)\)

注意一下枚举顺序,如果先枚举串再枚举位的话会荣获 TLE / MLE(也有可能是我实现太劣了)。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define ll long long
#define ull unsigned long long
using namespace std;
const int N = 3e4 + 5, M = 205, B = 917120411;
int n, L, S; char s[N][M]; ll ans; ull H[N], pw[M]; unordered_map<ull, int> cnt[65], sum;
inline int id(char c) { return 'a' <= c && c <= 'z' ? c - 'a' + 1 : ('A' <= c && c <= 'Z' ? c - 'A' + 27 : ('0' <= c && c <= '9' ? c - '0' + 53 : (c == '_' ? 63 : 64))); }
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n >> L >> S; pw[0] = 1;
	_for (i, 1, M - 1)  pw[i] = pw[i - 1] * B;
	_for (i, 1, n) {
		cin >> (s[i] + 1);
		_for (j, 1, L)  H[i] += pw[j] * id(s[i][j]);
	} _for (j, 1, L) {
		_for (i, 1, n)  ans += sum[H[i] - pw[j] * id(s[i][j])] - cnt[id(s[i][j])][H[i] - pw[j] * id(s[i][j])], cnt[id(s[i][j])][H[i] - pw[j] * id(s[i][j])] ++ , sum[H[i] - pw[j] * id(s[i][j])] ++ ; sum.clear();
		_for (i, 1, 64)  cnt[i].clear();
	} cout << ans << "\n";
	return 0;
}   sder

AT_abc312_h [ABC312Ex] snukesnuke

对于每个串记录他上一次枚举到的 \(k\) 和哈希值,直接从上一次的地方开始枚举即可。

不会证明复杂度,但是最慢点不到 \(100\) 毫秒。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define ull unsigned long long
using namespace std;
const int N = 4e5 + 5, B = 917120411;
int T, n; unordered_map<ull, int> lst, vis; unordered_map<ull, ull> lstH; char s[N]; ull H, cur, pw[N];
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> T, pw[0] = 1;
	_for (i, 1, N - 1)  pw[i] = pw[i - 1] * B;
	_for (_, 1, T) {
		cin >> (s + 1), n = strlen(s + 1), H = 0;
		_for (i, 1, n)  H = H * B + (s[i] - 'a' + 1); cur = lstH[H] * pw[n] + H;
		while (vis[cur])  lst[H] ++ , cur = cur * pw[n] + H; vis[cur] = 1, lst[H] ++ , lstH[H] = cur; cout << lst[H] << " ";
	}
	return 0;
}=

回文自动机(PAM)

求以每个位置结尾的回文串个数。

考虑将字符依次加入。以 \(i\) 结尾的回文串有若干个,把长度较小的对长度最长的对称一下,发现这些回文串都会在之前出现过。所以一个串的不同回文串只有 \(O(n)\)

考虑建 Trie。不同于之前的 Trie,一个节点表示的字符串是他到根再到他路径上所组成的串。由于回文串分为奇数和偶数,建两个源点分别表示奇数的根,偶数的根。对于每个 Trie 上的节点,记录 \(len_i\) 表示 \(i\) 节点代表的回文串的长度。

考虑类似 ACAM 求出 fail 数组,指向每个串的最长后缀回文串。求 fail 和父亲时,只需不断跳 \(i - 1\) 的 fail 直到回文串前面的字符和 \(s_i\) 相等,这样可以再之前的回文串两段加一个相同字符组成一个新的回文串。

上面证明过复杂度 \(O(n)\)

警告:strlen() 复杂度是 \(o(n)\),不要在循环里面用 strlen()!!!

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
using namespace std;
const int N = 2e6 + 5;
int n; char s[N];
struct Palindrom_Automation {
	int ncnt, fail[N], len[N], tot[N], tr[N][26];
	inline int gfail(int x, int i) { while (s[i - len[x] - 1] ^ s[i])  x = fail[x]; return x; }
	inline void init() { fail[0] = ncnt = 1, len[1] = - 1; }
	inline void insert() {
		int pos, cur = 0, c;
		_for (i, 1, n) {
			s[i] = (s[i] - 97 + tot[cur]) % 26 + 97, pos = gfail(cur, i), c = s[i] - 'a';
			if ( ! tr[pos][c])  fail[ ++ ncnt] = tr[gfail(fail[pos], i)][c], tr[pos][c] = ncnt, len[ncnt] = len[pos] + 2, tot[ncnt] = tot[fail[ncnt]] + 1; cur = tr[pos][c], cout << tot[cur] << " ";
		} cout << "\n";
	}
} PAM;
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s + 1), n = strlen(s + 1), PAM.init(), PAM.insert();
	return 0;
}

P5496 【模板】回文自动机(PAM)

板子题。

P3649 [APIO2014] 回文串

简单题。考虑先建出 PAM,然后在 fail 树上倒着(从叶子到根)求出每个串的出现次数。复杂度 \(O(n)\)

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
using namespace std;
const int N = 3e5 + 5;
int n; char s[N];
inline void chkmax(ll & x, ll y) { x = x > y ? x : y; }
struct Palindrom_Automation {
	int ncnt, fail[N], sz[N], len[N], tr[N][26]; ll ans;
	inline void init() { ncnt = fail[0] = 1, len[1] = - 1; }
	inline int gfail(int x, int i) { while (s[i - len[x] - 1] ^ s[i])  x = fail[x]; return x; }
	inline void build() {
		int pos, cur = 0, c; init();
		_for (i, 1, n) {
			pos = gfail(cur, i), c = s[i] - 'a';
			if ( ! tr[pos][c])  fail[ ++ ncnt] = tr[gfail(fail[pos], i)][c], tr[pos][c] = ncnt, len[ncnt] = len[pos] + 2; cur = tr[pos][c], sz[cur] ++ ;
		}
	} inline ll gans() { _all (i, ncnt, 1)  sz[fail[i]] += sz[i], chkmax(ans, 1ll * len[i] * sz[i]); return ans; }
} PAM;
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s + 1), n = strlen(s + 1), PAM.build(), cout << PAM.gans() << "\n";
	return 0;
}

P5555 秩序魔咒

建两个 PAM,在两棵 Trie 树上 dfs,如果有相同的儿子节点就往下走,并更新答案。

注意奇数点和偶数点都要 dfs。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
using namespace std;
const int N = 4e5 + 5;
int n[2], ans1, ans2; char s[2][N];
struct Palindrom_Automation {
	int o, ncnt, fail[N], len[N], tot[N], tr[N][26];
	inline void init(int t) { o = t, fail[0] = ncnt = 1, len[1] = - 1; }
	inline int gfail(int x, int i) { while (s[o][i - len[x] - 1] ^ s[o][i])  x = fail[x]; return x; }
	inline void build(int t) {
		int pos, cur = 0, c; init(t);
		_for (i, 1, n[o]) {
			pos = gfail(cur, i), c = s[o][i] - 'a';
			if ( ! tr[pos][c])  fail[ ++ ncnt] = tr[gfail(fail[pos], i)][c], tr[pos][c] = ncnt, len[ncnt] = len[pos] + 2, tot[ncnt] = tot[fail[ncnt]] + 1; cur = tr[pos][c];
		}
	}
} PAM1, PAM2;
void dfs(int u, int v) {
	if (PAM1.len[u] > ans1)  ans1 = PAM1.len[u], ans2 = 1;
	else if (PAM1.len[u] == ans1)  ans2 ++ ;
	_for (i, 0, 25)  if (PAM1.tr[u][i] && PAM2.tr[v][i])  dfs(PAM1.tr[u][i], PAM2.tr[v][i]);
}
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n[0] >> n[1] >> (s[0] + 1) >> (s[1] + 1), PAM1.build(0), PAM2.build(1), dfs(0, 0), dfs(1, 1), cout << ans1 << " " << ans2 << "\n";
	return 0;
}

P5685 [JSOI2013] 快乐的 JYY

和 P5555 非常像。建两个 PAM,求出每个回文串的出现次数,然后 dfs 两棵树计算答案即可(每个串的贡献是两个串的出现次数乘积)。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
using namespace std;
const int N = 4e5 + 5;
int n[2]; char s[2][N]; ll ans;
string ztysb;
struct Palindrom_Automation {
	int o, ncnt, fail[N], len[N], sz[N], tr[N][26];
	inline int gfail(int x, int i) { while (s[o][i - len[x] - 1] ^ s[o][i])  x = fail[x]; return x; }
	inline void build(int t) {
		int pos, cur = 0, c; o = t, fail[0] = ncnt = 1, len[1] = - 1;
		_for (i, 1, n[o]) {
			pos = gfail(cur, i), c = s[o][i] - 'A';
			if ( ! tr[pos][c])  fail[ ++ ncnt] = tr[gfail(fail[pos], i)][c], tr[pos][c] = ncnt, len[ncnt] = len[pos] + 2; sz[cur = tr[pos][c]] ++ ;
		} _all (i, ncnt, 1)  sz[fail[i]] += sz[i];
	}
} PAM[2];
void dfs(int u, int v) {
	if (u >= 2 && v >= 2)  ans += 1ll * PAM[0].sz[u] * PAM[1].sz[v];
	_for (i, 0, 25)  if (PAM[0].tr[u][i] && PAM[1].tr[v][i])  dfs(PAM[0].tr[u][i], PAM[1].tr[v][i]);
} signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s[0] + 1) >> (s[1] + 1), n[0] = strlen(s[0] + 1), n[1] = strlen(s[1] + 1), PAM[0].build(0), PAM[1].build(1), dfs(0, 0), dfs(1, 1), cout << ans << "\n";
	return 0;
}

CF17E Palisection

糖糖题。相交的回文串对数不好做,考虑反过来求不交的回文串数。

用 PAM 似乎不太好做,考虑 manacher,我们可以快速求出以 \(i\) 开头 / 结尾的回文串个数和。

然后算贡献是简单的。复杂度 \(O(n)\),区间加可以通过差分实现。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
#define ld long double
#define PII pair<int, int>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
using namespace std;
const int N = 4e6 + 5, P = 51123987;
int n, m, sum, ans, pal[N], f[N], g[N]; char s[N], t[N];
inline int mul(int x, int y) { return 1ll * x * y % P; }
inline void Add(int & x, int y) { x = x + y >= P ? x + y - P : x + y; }
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n >> (t + 1); int l = 1, r = - 1, k;
	_for (i, 1, n)  s[ ++ m] = '#', s[ ++ m] = t[i]; s[ ++ m] = '#';
	_for (i, 1, m) {
		k = i > r ? 1 : min(pal[l + r - i], r - i + 1);
		while (1 <= i - k && i + k <= m && s[i - k] == s[i + k])  k ++ ; pal[i] = k -- ;
		if (i + k > r)  l = i - k, r = i + k;
	} _for (i, 1, m)  if (i - pal[i] + 2 <= i + pal[i] - 2)  f[(i - pal[i] + 2) / 2 - 1] -- , f[i / 2] ++ , g[i / 2 - (s[i] != '#') + 1] ++ , g[(i + pal[i] - 2) / 2 + 1] -- , Add(sum, pal[i] / 2);
	_for (i, 1, n)  Add(g[i], g[i - 1]);
	_all (i, n, 1)  Add(f[i], f[i + 1]);
	_all (i, n, 1)  Add(f[i], f[i + 1]);
	_for (i, 1, n - 1)  Add(ans, mul(g[i], f[i + 1])); cout << ((1ll * sum * (sum - 1) / 2) % P - ans + P) % P << "\n";
	return 0;
}

CF932G Palindrome Partition

有点牛的 *2900。

考虑将序列转化成:\(s_1s_ns_2s_{n - 1} \cdots s_{\frac{n}{2}}s_{\frac{n}{2} + 1}\)

这样你会发现原问题的要求转化为了:将序列划分成若干个长度为偶数的回文串,求方案数(证明是简单的)。

考虑 DP:设 \(f_i\) 表示已经划分了前 \(i\) 个字符的方案数。转移 \(f_i \leftarrow \sum_{s_{[j + 1, i]} \text{ is palindrome}} f_j\)

暴力在 PAM 的 fail 树上跳,复杂度最高可达 \(O(n^2)\),考虑优化。

有结论:PAM 的 fail 树上每个节点到根的路径上的 \(len\) 之差序列一定是一些等差数列,且个数不超过 \(\log n\)

证明和 border 有关,不难推出。这里略了。

于是可以考虑将一个等差数列的贡献放到一起计算。设 \(g_u\) 表示 fail 树上节点 \(u\) 到他所在等差数列的顶端(记为 \(top_u\))的 \(f\) 值之和。

然后你考虑从 \(g_{fail_u}\) 推到 \(g_u\)(当 \(u\)\(fail_u\) 处于同一个等差数列之中时)。你会发现,关于最长回文串对称后,\(g_u\) 只比 \(g_{fail_u}\) 多出了起始位置为 \(i - len_{top_u} - (len_u - len_{fail_u})\) 的贡献。

所以我们可以在建 SAM 的时候 \(O(1)\) 维护每个节点的 \(g\) 值,然后将连续一段等差数列集体贡献给当前加入的位置 \(i\)。这个过程在每个 \(i\) 只会做 \(\log n\) 次,因为只会向上跳 \(\log n\) 个等差数列。

总复杂度 \(O(n \log n)\)

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
using namespace std;
const int N = 1e6 + 5, P = 1e9 + 7;
int n, f[N]; char s[N], t[N];
inline void Add(int & x, int y) { x = x + y >= P ? x + y - P : x + y; }
struct Palindrome_Automation {
	int ncnt, f[N], g[N], fail[N], len[N], diff[N], top[N], tr[N][26];
	inline int gfail(int x, int i) { while (s[i - len[x] - 1] ^ s[i])  x = fail[x]; return x; }
	inline void build() {
		int pos, cur = 0, c, now; fail[0] = ncnt = f[0] = top[0] = 1, len[1] = - 1;
		_for (i, 1, n) {
			pos = gfail(cur, i), c = s[i] - 'a';
			if ( ! tr[pos][c])  fail[ ++ ncnt] = tr[gfail(fail[pos], i)][c], tr[pos][c] = ncnt, len[ncnt] = len[pos] + 2, diff[ncnt] = len[ncnt] - len[fail[ncnt]], top[ncnt] = diff[ncnt] == diff[fail[ncnt]] ? top[fail[ncnt]] : fail[ncnt]; cur = now = tr[pos][c];
			while (now >= 2) {
				g[now] = f[i - len[top[now]] - diff[now]];
				if (top[now] ^ fail[now])  Add(g[now], g[fail[now]]);
				if ( ! (i & 1))  Add(f[i], g[now]); now = top[now];
			}
		}
	}
} PAM;
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (t + 1), n = strlen(t + 1);
	_for (i, 1, n / 2)  s[i * 2 - 1] = t[i];
	_for (i, n / 2 + 1, n)  s[(i - n / 2) * 2] = t[n + n / 2 + 1 - i]; PAM.build(), cout << PAM.f[n] << "\n";
	return 0;
}

CF906E Reverses

略难于 CF932G,主播不想输方案。

类似上题把序列变为 \(a_1b_1a_2b_2 \cdots a_nb_n\),转化为了最小偶回文划分(长度为 \(2\) 不需计数)。

输出方案的话就记录一下转移点即可。也是 \(O(n \log n)\)


后缀数组(SA)

P3809 【模板】后缀排序

后缀排序板子。

考虑暴力做是 \(O(n ^ 2 \log n)\) 的。如何优化呢?

考虑第一次把 \(s_{[i, i]}\) 排序,第二次把 \(s_{[i, i + 1]}\) 排序,第三次把 \(s_{[i, i + 3]}\) 排序。换句话说,我们以倍增长度的方式将每个后缀排序。比如你已经排序了长度 \(2^k\),你可以按照 \(s_{[i, i + 2^k - 1]}\) 为第一关键字,\(s_{[i + 2^k, i + 2^{k + 1} - 1]}\) 为第二关键字排序,这样就能排序长度 \(2^{k + 1}\) 了。

于是复杂度就变成了 \(O(n \log^2 n)\),瓶颈在于每一次都要 sort。把 sort 换成计数排序即可,先按照第二关键字记排,再按第一关键字记排,复杂度降到了 \(O(n \log n)\)

但你会发现这个东西常数非常大,以下有一些常数优化:

  • 倍增的过程中,记录本质不同的串个数(是递增的),不用每次枚举值域都枚举到 \(n\)

  • 如果你已经把所有后缀排好了(也就是说本质不同串个数已经达到了 \(n\)),你可以直接结束过程(实测 3s \(\to\) 0.7s)。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
using namespace std;
const int N = 2e6 + 5;
int n, lim = 'z', id[N], sa[N], rnk[N], old_rnk[N], cnt[N]; char s[N];
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s + 1), n = strlen(s + 1); int tot;
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i];
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old_rnk, rnk, sizeof(rnk));
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i]; tot = 0;
		_for (i, 1, n) {
			if (old_rnk[sa[i]] == old_rnk[sa[i - 1]] && old_rnk[sa[i] + k] == old_rnk[sa[i - 1] + k])  rnk[sa[i]] = tot;
			else  rnk[sa[i]] = ++ tot;
		} if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  cout << sa[i] << " "; cout << "\n";
	return 0;
}

P4051 [JSOI2007] 字符加密

对于这种类似串循环的问题,考虑把串复制一遍。就变成后缀排序板子了。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
using namespace std;
const int N = 4e5 + 5;
int n, tot, num, lim = 300, sa[N], rnk[N], id[N], cnt[N], old_rnk[N]; char s[N];
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s + 1), n = strlen(s + 1);
	_for (i, 1, n)  s[i + n] = s[i]; n <<= 1;
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i];
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old_rnk, rnk, sizeof(old_rnk)), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old_rnk[sa[i]] == old_rnk[sa[i - 1]] && old_rnk[sa[i] + k] == old_rnk[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  if (sa[i] <= n / 2 && num < n / 2)  num ++ , cout << s[sa[i] + (n / 2) - 1]; cout << "\n";
	return 0;
}

P2870 [USACO07DEC] Best Cow Line G

设当前剩下的区间为 \([l, r]\),考虑策略:

  • 如果 \(s_l < s_r\),选择 \(l\)

  • 如果 \(s_l > s_r\),选择 \(r\)

  • 如果 \(s_l = s_r\),那么我们比较 \(s_ls_{l + 1}s_{l + 2} \cdots s_n\)\(s_rs_{r - 1}s_{r - 2}\cdots s_1\),选择较小的即可。

如何快速比较一段前缀和后缀的大小呢?

考虑把原串翻转过来并拼在原串后面,问题就变成比较两个后缀了。后缀排序即可。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
using namespace std;
const int N = 2e6 + 5;
int n, tot, lim = 'Z', l, r, cur, cnt[N], sa[N], rnk[N], id[N], old[N]; char s[N], ans[N];
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n, l = 1, r = n;
	_for (i, 1, n)  cin >> s[i], s[n * 2 + 1 - i] = s[i]; n <<= 1;
	_for (i, 1, n)  rnk[i] = s[i], sa[i] = i;
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old, rnk, sizeof(rnk)), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} while (l <= r) {
		if (s[l] < s[r])  ans[ ++ cur] = s[l ++ ];
		else if (s[l] > s[r])  ans[ ++ cur] = s[r -- ];
		else  ans[ ++ cur] = rnk[l] < rnk[n + 1 - r] ? s[l ++ ] : s[r -- ];
	} _for (i, 1, n / 80)  _for (j, 1, 80)  cout << ans[(i - 1) * 80 + j] << (j == 80 ? "\n" : "");
	_for (i, n / 80 * 80 + 1, n)  cout << ans[i]; cout << "\n";
	return 0;
}

P2408 不同子串个数

利用 SA 求 \(height\) 数组:\(height_i = \text{LCP}(sa_i, sa_{i - 1})\)

将子串想象成后缀的前缀。考虑所有子串 \(\frac{n(n + 1)}{2}\) 减去重复的子串。

后缀排序,相邻两个串的重复子串个数可以通过 \(height\) 求出。答案即为 \(\frac{n(n + 1)}{2} - \sum_{i = 2}^n height_i\)

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
using namespace std;
const int N = 4e5 + 5;
int n, tot, cur, lim = 'z', sa[N], rnk[N], cnt[N], id[N], height[N], old[N]; char s[N]; ll ans;
inline void chkmin(int & x, int y) { x = x < y ? x : y; }
inline void chkmax(int & x, int y) { x = x > y ? x : y; }
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n >> (s + 1), ans = 1ll * n * (n + 1) / 2;
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i];
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old, rnk, sizeof(rnk)), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  if (rnk[i]) {
		if (cur)  cur -- ;
		while (s[i + cur] == s[sa[rnk[i] - 1] + cur])  cur ++ ; height[rnk[i]] = cur;
	} _for (i, 2, n)  ans -= height[i]; cout << ans << "\n";
	return 0;
}

P1117 [NOI2016] 优秀的拆分

好题啊。

你考虑求出 \(f_i, g_i\) 分别表示以 \(i\) 结尾 / 开头的形如 \(\text{AA}\) 的串的个数。答案即为 \(\sum_{i = 1}^n f_ig_{i + 1}\)

然后你考虑如何求出 \(f\)\(g\)。考虑枚举 \(len\) 表示 \(\text{AA}\)\(\text{A}\) 的长度。将序列中提取出若干个关键点:\(len, 2len, \cdots\)。而显然一个符合条件的 \(\text{AA}\) 必须覆盖恰好两个关键点。

枚举每个关键点的复杂度只有 \(O(n \ln n)\),所以考虑对相邻两个点 \(i, j\) 计算贡献。

我们求出 \(\text{Lcp} = \text{lcp}(suf_i, suf_j)\)\(\text{Lcs} = \text{lcs}(pre_{i - 1}, pre_{j - 1})\)。那么 \([i, i + Lcp - 1]\)\([j - Lcs, j]\) 的公共部分就可以作为两个 \(\text{A}\) 的分界点(手摸可知)。

这里就可以通过二分 + 哈希做到 \(O(T n \ln n \log n)\) 的复杂度了。

继续优化。考虑将原串和反串求 SA,可以 \(O(1)\) 求出 \(\text{Lcp}\)\(\text{Lcs}\)。总复杂度 \(O(T n \log n)\)

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
using namespace std;
const int N = 4e5 + 5;
int n, f[N], g[N]; char s[N]; ll ans;
struct Suffix_Array {
	int tot, lim, cur, cnt[N], sa[N], id[N], old[N], height[N], Log[N], ST[20][N], rnk[N];
	inline void clear() {
		lim = 'z', cur = 0;
		_for (i, 0, n << 1) {
			cnt[i] = sa[i] = id[i] = old[i] = height[i] = Log[i] = rnk[i] = 0;
			_for (j, 0, 19)  ST[j][i] = 0;
		}
	} inline void build_ST(int * a) {
		Log[0] = - 1;
		_for (i, 1, n)  Log[i] = Log[i >> 1] + 1, ST[0][i] = a[i];
		_for (k, 1, Log[n])  _for (i, 1, n - (1 << k) + 1)  ST[k][i] = min(ST[k - 1][i], ST[k - 1][i + (1 << (k - 1))]);
	} inline int query(int i, int j) { int l = min(rnk[i], rnk[j]) + 1, r = max(rnk[i], rnk[j]), k = Log[r - l + 1]; return l <= r ? min(ST[k][l], ST[k][r - (1 << k) + 1]) : 0; }
	inline void build() {
		_for (i, 1, n)  sa[i] = i, rnk[i] = s[i]; lim = 'z', cur = 0;
		for (int k = 1; k < n; k <<= 1) {
			iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old, rnk, sizeof(rnk)), tot = 0;
			_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
			_for (i, 1, lim)  cnt[i] += cnt[i - 1];
			_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
			_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
			_for (i, 1, lim)  cnt[i] += cnt[i - 1];
			_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
			_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
			if (tot == n)  break; lim = tot;
		} _for (i, 1, n)  if (rnk[i]) {
			if (cur)  cur -- ;
			while (s[i + cur] == s[sa[rnk[i] - 1] + cur])  cur ++ ; height[rnk[i]] = cur;
		} build_ST(height);
	}
} SA[2];
inline void solve() {
	cin >> (s + 1), n = strlen(s + 1), SA[0].clear(), SA[1].clear();
	_for (i, 0, n << 1)  f[i] = g[i] = 0; SA[0].build(), reverse(s + 1, s + n + 1), SA[1].build(), ans = 0; int lcs, lcp, x;
	_for (len, 1, n / 2)  for (int i = len, j = len * 2; j <= n; i += len, j += len) {
		lcp = min(len, SA[0].query(i, j)), lcs = min(len - 1, SA[1].query(n - i + 2, n - j + 2)), x = lcp + lcs - len + 1;  // x - number of possible position of the first A
		if (lcp + lcs >= len)  f[j + lcp - x] ++ , f[j + lcp] -- , g[i - lcs] ++ , g[i - lcs + x] -- ;
	} _for (i, 1, n)  f[i] += f[i - 1], g[i] += g[i - 1];
	_for (i, 1, n - 1)  ans += 1ll * f[i] * g[i + 1]; cout << ans << "\n";
}
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	int T; cin >> T; while (T -- )  solve();
	return 0;
}

P2178 [NOI2015] 品酒大会

独立切出来了,尽管调了很久

题面写的很绕,“\(r\) 相似” 其实是 LCP 长度为 \(r\)。由于 “\(r\) 相似” 也是 “\(< r\) 相似” 的,考虑先求出 LCP \(= r\),再做后缀和 / 后缀 max。

然后就变成了:有多少对后缀串,使得 LCP 长度为 \(r\)

显然的 SA,求出 height,然后再变成:有多少个区间的 \(\min\)\(r\)。这个乱维护一下即可。

我用了二分(单调栈也是可以的),复杂度是 \(O(n \log n)\) 的。要注意边界。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
#define PII pair<int, int>
#define fi first
#define se second
#define mp make_pair
using namespace std;
const int N = 6e5 + 5, INF = 2e9 + 5;
int n, tot, lim = 'z', cur, A[N], a[N], cnt[N], sa[N], id[N], rnk[N], old[N], height[N], Log[N], lst[N]; char s[N]; ll ans1[N], ans2[N];
inline void chkmax(ll & x, ll y) { x = x > y ? x : y; }
struct Sparse_Table {
	int ST[20][N];
	inline void build(int * a) {
		_for (i, 1, n)  ST[0][i] = a[i];
		_for (k, 1, Log[n])  _for (i, 1, n - (1 << k) + 1)  ST[k][i] = min(ST[k - 1][i], ST[k - 1][i + (1 << (k - 1))]);
	} inline int query(int l, int r) { int k = Log[r - l + 1]; return min(ST[k][l], ST[k][r - (1 << k) + 1]); }
} ST;
inline PII operator + (PII u, PII v) {
	PII res; res.fi = max(u.fi, v.fi);
	if (u.fi > v.fi)  res.se = max(u.se, v.fi);
	else if (u.fi < v.fi)  res.se = max(u.fi, v.se);
	else  res.se = u.fi; return res;
} struct Segment_Tree {
	#define lc (p << 1)
	#define rc (p << 1 | 1)
	#define mid ((tr[p].l + tr[p].r) >> 1)
	struct Tree { int l, r; PII val; } tr[N << 2];
	inline int len(int p) { return tr[p].r - tr[p].l + 1; }
	inline bool In(int p, int l, int r) { return l <= tr[p].l && tr[p].r <= r; }
	inline void push_up(int p) { tr[p].val = tr[lc].val + tr[rc].val; }
	void build(int p, int l, int r) {
		tr[p].l = l, tr[p].r = r;
		if (l == r)  return tr[p].val = mp(A[l], - INF), void(); build(lc, l, mid), build(rc, mid + 1, r), push_up(p);
	} PII query(int p, int l, int r) {
		if (l > r)  return mp( - INF, - INF);
		if (In(p, l, r))  return tr[p].val;
		if (r <= mid)  return query(lc, l, r);
		if (l > mid)  return query(rc, l, r); return query(lc, l, r) + query(rc, l, r);
	}
	#undef lc
	#undef rc
	#undef mid
} SGT[2];
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n >> (s + 1), Log[0] = - 1; int l, r, mid, posl, posr; PII x, y;
	_for (i, 1, n)  cin >> a[i], rnk[i] = s[i], sa[i] = i, Log[i] = Log[i >> 1] + 1, ans2[i] = - (ll)1e18 + 5;
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old, rnk, sizeof(rnk)), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  if (rnk[i]) {
		if (cur)  cur -- ;
		while (s[i + cur] == s[sa[rnk[i] - 1] + cur])  cur ++ ; height[rnk[i]] = cur;
	} ST.build(height);
	_for (i, 1, n)  A[rnk[i]] = a[i]; SGT[0].build(1, 1, n);
	_for (i, 1, n)  A[i] = - A[i]; SGT[1].build(1, 1, n);
	_for (i, 1, n) {
		if (i >= 2) {
			l = lst[height[i]] + 1, r = i, posl = i;
			while (l <= r) {
				mid = (l + r) >> 1;
				if (ST.query(mid, i) == height[i])  posl = mid, r = mid - 1;
				else  l = mid + 1;
			} l = i, r = n, posr = i;
			while (l <= r) {
				mid = (l + r) >> 1;
				if (ST.query(i, mid) == height[i])  posr = mid, l = mid + 1;
				else  r = mid - 1;
			} x = SGT[0].query(1, max(1, posl - 1), posr), y = SGT[1].query(1, max(1, posl - 1), posr), y = mp( - y.fi, - y.se), ans1[height[i]] += 1ll * (i - posl + 1) * (posr - i + 1), chkmax(ans2[height[i]], max(max(1ll * x.fi * x.se, 1ll * x.fi * y.fi), max(1ll * y.fi * y.se, 1ll * y.fi * x.fi)));
		} lst[height[i]] = i;
	} _all (i, n - 1, 0)  ans1[i] += ans1[i + 1], chkmax(ans2[i], ans2[i + 1]);
	_for (i, 0, n - 1)  cout << ans1[i] << " " << (ans1[i] ? ans2[i] : 0) << "\n";
	return 0;
}

P4248 [AHOI2013] 差异

和上一道题本质相同。

考虑把式子拆开,前面每个后缀的 \(len\) 会贡献 \(n - 1\) 次,后面的 LCP 用 height 计算贡献即可。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
#define ld long double
#define PII pair<int, int>
#define mp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const int N = 1e6 + 5;
int n, tot, cur, lim = 'z', cnt[N], rnk[N], sa[N], id[N], old[N], height[N], lst[N], Log[N]; char s[N]; ll ans;
inline void chkmin(int & x, int y) { x = x < y ? x : y; }
inline void chkmax(int & x, int y) { x = x > y ? x : y; }
struct Sparse_Table {
	int ST[20][N];
	inline void build(int * a) {
		_for (i, 1, n)  ST[0][i] = a[i];
		_for (k, 1, Log[n])  _for (i, 1, n - (1 << k) + 1)  ST[k][i] = min(ST[k - 1][i], ST[k - 1][i + (1 << (k - 1))]);
	} inline int query(int l, int r) { int k = Log[r - l + 1]; return min(ST[k][l], ST[k][r - (1 << k) + 1]); }
} ST;
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s + 1), n = strlen(s + 1); int l, r, mid, posl, posr; Log[0] = - 1;
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i], ans += 1ll * (n - i + 1) * (n - 1), Log[i] = Log[i >> 1] + 1;
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old, rnk, sizeof(old)), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  if (rnk[i]) {
		if (cur)  cur -- ;
		while (s[i + cur] == s[sa[rnk[i] - 1] + cur])  cur ++ ; height[rnk[i]] = cur;
	} ST.build(height);
	_for (i, 1, n) {
		if (i >= 2) {
			l = lst[height[i]] + 1, r = i, posl = i;
			while (l <= r) {
				mid = (l + r) >> 1;
				if (ST.query(mid, i) == height[i])  posl = mid, r = mid - 1;
				else  l = mid + 1;
			} l = i, r = n, posr = i;
			while (l <= r) {
				mid = (l + r) >> 1;
				if (ST.query(i, mid) == height[i])  posr = mid, l = mid + 1;
				else  r = mid - 1;
			} ans -= 2ll * (i - posl + 1) * (posr - i + 1) * height[i];
		} lst[height[i]] = i;
	} cout << ans << "\n";
	return 0;
}

P3181 [HAOI2016] 找相同字符

如果是一个串,和上面两题一模一样了。

但是有两个串,于是考虑把他俩拼起来(中间接一个非小写字母字符)算一遍答案,然后减去两个串分别的答案即可。

这样显然是对的。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
#define ld long double
#define PII pair<int, int>
#define mp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const int N = 1e6 + 5;
int n1, n2, tot, cur, lim, sa[N], rnk[N], id[N], cnt[N], old[N], height[N], lst[N], Log[N]; char s1[N], s2[N], s3[N];
inline void chkmin(int & x, int y) { x = x < y ? x : y; }
inline void chkmax(int & x, int y) { x = x > y ? x : y; }
struct Sparse_Table {
	int ST[20][N];
	inline void build(int * a, int n) {
		_for (i, 1, n)  ST[0][i] = a[i];
		_for (k, 1, Log[n])  _for (i, 1, n - (1 << k) + 1)  ST[k][i] = min(ST[k - 1][i], ST[k - 1][i + (1 << (k - 1))]);
	} inline int query(int l, int r) { int k = Log[r - l + 1]; return min(ST[k][l], ST[k][r - (1 << k) + 1]); }
} ST;
inline ll gans(char * s, int n) {
	_for (i, 0, N - 1)  sa[i] = rnk[i] = id[i] = cnt[i] = old[i] = height[i] = lst[i] = 0; cur = tot = 0, lim = 'z';
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i]; ll res = 0; int l, r, mid, posl, posr;
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old, rnk, sizeof(old)), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  if (rnk[i]) {
		if (cur)  cur -- ;
		while (s[i + cur] == s[sa[rnk[i] - 1] + cur])  cur ++ ; height[rnk[i]] = cur;
	} ST.build(height, n);
	_for (i, 1, n) {
		if (i >= 2) {
			l = lst[height[i]] + 1, r = i, posl = i;
			while (l <= r) {
				mid = (l + r) >> 1;
				if (ST.query(mid, i) == height[i])  posl = mid, r = mid - 1;
				else  l = mid + 1;
			} l = i, r = n, posr = i;
			while (l <= r) {
				mid = (l + r) >> 1;
				if (ST.query(i, mid) == height[i])  posr = mid, l = mid + 1;
				else  r = mid - 1;
			} res += 1ll * (i - posl + 1) * (posr - i + 1) * height[i];
		} lst[height[i]] = i;
	} return res;
} signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s1 + 1) >> (s2 + 1), n1 = strlen(s1 + 1), n2 = strlen(s2 + 1), s3[n1 + 1] = '#', Log[0] = - 1;
	_for (i, 1, N - 1)  Log[i] = Log[i >> 1] + 1;
	_for (i, 1, n1)  s3[i] = s1[i];
	_for (i, 1, n2)  s3[n1 + 1 + i] = s2[i]; cout << gans(s3, n1 + n2 + 1) - gans(s1, n1) - gans(s2, n2) << "\n";
	return 0;
}

P5546 [POI 2000] 公共串

UVA760 DNA Sequencing

SP1811 LCS - Longest Common Substring

SP1812 LCS2 - Longest Common Substring II

SP10570 LONGCS - Longest Common Substring

SP32577 ADAPHOTO - Ada and Terramorphing

全是给一车串,让你求最长公共子串(除此之外还有若干道类似题目,没有列举了)。

把所有串拼在一起,然后建 SA,取一个包含所有串的区间 chkmax 即可。复杂度是 \(O(n \log n)\)\(\log\) 来自 sort 和 ST 表。

这里放一下 UVA760 的代码,这题不仅要输出答案还要要求输出所有位置。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
using namespace std;
const int N = 1205;
int n, n1, n2, anslen, anscnt, Case, tot, cur, lim = 'z', op[N], ans[N], cnt[N], sa[N], id[N], rnk[N], old[N], height[N], lst[N], pre[N], nxt[N], Log[N], vis[N]; char s1[N], s2[N], s[N];
struct Sparse_Table {
	int ST[15][N];
	inline void build(int * a) {
		_for (i, 1, n)  ST[0][i] = a[i];
		_for (k, 1, Log[n])  _for (i, 1, n - (1 << k) + 1)  ST[k][i] = min(ST[k - 1][i], ST[k - 1][i + (1 << (k - 1))]);
	} inline int query(int l, int r) { int k = Log[r - l + 1]; return min(ST[k][l], ST[k][r - (1 << k) + 1]); }
} ST;
inline void solve() {
	n1 = strlen(s1 + 1), n2 = strlen(s2 + 1), Case ++ , anslen = anscnt = 0, Log[0] = - 1; int flag = 0;
	_for (i, 1, n1)  s[i] = s1[i]; s[n1 + 1] = '#';
	_for (i, 1, n2)  s[n1 + 1 + i] = s2[i]; n = n1 + n2 + 1;
	if (Case > 1)  cout << "\n";
	_for (i, 0, N - 1)  cnt[i] = sa[i] = id[i] = rnk[i] = old[i] = height[i] = lst[i & 1] = pre[i] = nxt[i] = vis[i] = op[i] = 0;
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i], Log[i] = Log[i >> 1] + 1; tot = cur = 0, lim = 'z';
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old, rnk, sizeof(old)), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  if (rnk[i]) {
		if (cur)  cur -- ;
		while (s[i + cur] == s[sa[rnk[i] - 1] + cur])  cur ++ ; height[rnk[i]] = cur;
	} ST.build(height);
	_for (i, 1, n)  if (sa[i] ^ (n1 + 1))  pre[i] = lst[sa[i] > n1], lst[sa[i] <= n1] = i; lst[0] = lst[1] = 0;
	_all (i, n, 1)  if (sa[i] ^ (n1 + 1))  nxt[i] = lst[sa[i] > n1], lst[sa[i] <= n1] = i;
	_for (i, 1, n)  if (sa[i] <= n1) {
		if (pre[i]) {
			if (ST.query(pre[i] + 1, i) > anslen)  anslen = ST.query(pre[i] + 1, i), anscnt = 0;
			if (ST.query(pre[i] + 1, i) == anslen)  ans[ ++ anscnt] = sa[i], op[anscnt] = 1;
		} if (nxt[i]) {
			if (ST.query(i + 1, nxt[i]) > anslen)  anslen = ST.query(i + 1, nxt[i]), anscnt = 0;
			if (ST.query(i + 1, nxt[i]) == anslen)  ans[ ++ anscnt] = sa[i], op[anscnt] = 2;
		}
	} sort(ans + 1, ans + anscnt + 1), anscnt = unique(ans + 1, ans + anscnt + 1) - ans - 1, sort(ans + 1, ans + anscnt + 1, [&] (int x, int y) { return rnk[x] < rnk[y]; });
	if ( ! anslen)  cout << "No common sequence.\n";
	else {
		_for (i, 2, anscnt)  if (ST.query(rnk[ans[i - 1]] + 1, rnk[ans[i]]) >= anslen)  vis[i] = 1;
		_for (i, 1, anscnt)  if ( ! vis[i]) {
			assert(ans[i] + anslen - 1 <= n1);
			_for (j, ans[i], ans[i] + anslen - 1)  cout << s[j]; flag = 1; cout << "\n";
		} if ( ! flag)  cout << "No common sequence.\n";
	} _for (i, 0, N - 1)  s1[i] = s2[i] = s[i] = '\0';
} signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	while (cin >> (s1 + 1) >> (s2 + 1))  solve();
	return 0;
}

P2463 [SDOI2008] Sandy 的卡片

子串相同的定义是全部加上一个数之后相等。就是差分之后相等。

UVA1223 Editor

SA 的 height 数组板子。\(\max_{i = 2}^n height_i\) 即为答案。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
using namespace std;
const int N = 1e4 + 5;
int n, cur, tot, lim, ans, sa[N], rnk[N], id[N], cnt[N], old[N], height[N]; char s[N];
inline void chkmax(int & x, int y) { x = x > y ? x : y; }
inline void solve() {
	cin >> (s + 1), n = strlen(s + 1), cur = tot = ans = 0, lim = 'z';
	_for (i, 0, N - 1)  sa[i] = rnk[i] = id[i] = cnt[i] = old[i] = height[i] = 0;
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i];
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), memcpy(old, rnk, sizeof(rnk)), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  if (rnk[i]) {
		if (cur)  cur -- ;
		while (s[i + cur] == s[sa[rnk[i] - 1] + cur])  cur ++ ; chkmax(ans, height[rnk[i]] = cur);
	} cout << ans << "\n";
} signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	int T; cin >> T; while (T -- )  solve();
	return 0;
}

CF149E Martian Strings

正反求 SA,然后记录 \(st_i, ed_i\) 表示以 \(i\) 开头 / 结尾能匹配上的最长前缀 / 后缀。

然后随便判断即可。复杂度是 \(O(nm \log n)\),CF 上被卡常了。

Code (TLE on #22)
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
using namespace std;
const int N = 2.1e5 + 5, INF = 1e9 + 5;
int n1, n2, n, q, ans, cur, tot, lim, sa[N], rnk[N], id[N], cnt[N], old[N], height[N], st[N], ed[N]; char s1[N], s2[N], s[N];
inline void chkmin(int & x, int y) { x = x < y ? x : y; }
inline void chkmax(int & x, int y) { x = x > y ? x : y; }
inline void SA() {
	_for (i, 0, n << 1)  sa[i] = rnk[i] = id[i] = cnt[i] = old[i] = height[i] = 0;
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i]; cur = tot = 0, lim = 'Z';
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ , old[i] = rnk[i];
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	} _for (i, 1, n)  if (rnk[i]) {
		if (cur)  cur -- ;
		while (s[i + cur] == s[sa[rnk[i] - 1] + cur])  cur ++ ; height[rnk[i]] = cur;
	}
} inline int solve() {
	_for (i, 1, n1)  s[i] = s1[i]; s[n1 + 1] = '#', scanf("%s", s2 + 1), n2 = strlen(s2 + 1); int t, flag = 0;
	if (n2 <= 1 || n2 > n1)  return 0;
	_for (i, 1, n2)  s[n1 + 1 + i] = s2[i]; n = n1 + n2 + 1, SA(), t = INF;
	_all (i, rnk[n1 + 2] - 1, 1) { chkmin(t, height[i + 1]); if (sa[i] <= n1)  st[sa[i]] = t; } t = INF;
	_for (i, rnk[n1 + 2] + 1, n) { chkmin(t, height[i]); if (sa[i] <= n1)  st[sa[i]] = t; } reverse(s + 1, s + n + 1), SA(), t = INF;
	_all (i, rnk[1] - 1, 1) { chkmin(t, height[i + 1]); if (sa[i] >= n2 + 2)  ed[n - sa[i] + 1] = t; } t = INF;
	_for (i, rnk[1] + 1, n) { chkmin(t, height[i]); if (sa[i] >= n2 + 2)  ed[n - sa[i] + 1] = t; } t = 0;
	_all (i, n1 - n2 + 1, 1) { chkmax(t, ed[i + n2 - 1]); if (st[i] && t && t + st[i] >= n2) { flag = 1; break; } }
	_for (i, 0, n << 1)  st[i] = ed[i] = 0, s[i] = s2[i] = '\0'; return flag;
} signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	scanf("%s %d", s1 + 1, & q), n1 = strlen(s1 + 1);
	while (q -- )  ans += solve(); cout << ans << "\n";
	return 0;
}

CF204E Little Elephant and Strings

考虑把所有串拼在一起。由于必须要 \(\ge k\) 个串包含,考虑对于每个起始位置 \(l\) 求出最小的 \(r\) 使得 \([l, r]\) 恰好包含 \(k\) 个串,记为 \(r_l\)

容易双指针扫一遍维护出 \(O(n)\) 个区间 \([i, r_i]\),权值为这一段的 \(height\) 最小值(即 LCP 长度)。

然后每一位的贡献即包含他的区间的权值取 \(\max\)。使用 ST 表即可。

P9482 [NOI2023] 字符串

调了一天 /tuu。

首先把原串反过来拼在后面,建 SA,转化为一个二维偏序问题。

但是存在一个问题,就是说如果一个后缀字典序比另一个大,但并不一定能成为答案,因为在规定的长度 \(r\) 内他俩可能是相等的。考虑把这些多算的东西减掉。

观察性质,发现此时 \(s[i, i + l - 1]\)\(s[i + l, i + 2l - 1]\) 构成一个回文串。跑 Manacher,转化为斜线加,竖线求和。转化一下坐标系即可。

复杂度是 \(O(Tn \log n)\) 的。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define pb push_back
using namespace std;
const int N = 4e5 + 5;
int n, q, m, tot, cur, lim, rt, ans[N], Log[N], cnt[N], old[N], sa[N], rnk[N], id[N], f[N]; char s[N], t[N];
inline void chkmin(int & x, int y) { x = x < y ? x : y; }
inline void chkmax(int & x, int y) { x = x > y ? x : y; }
struct Node { int x, id, op; }; vector<Node> qry[3][N], upd[N];
struct Fenwick_Tree {
	int tr[N];
	inline void clear() { _for (i, 0, n + 5)  tr[i] = 0; }
	inline int lowbit(int x) { return (x & ( - x)); }
	inline void update(int x, int k) { for (int i = x; i <= n; i += lowbit(i))  tr[i] += k; }
	inline int query(int x) { int res = 0; for (int i = x; i; i -= lowbit(i))  res += tr[i]; return res; }
	inline int query(int l, int r) { return l <= r ? (l ? query(r) - query(l - 1) : query(r)) : 0; }
} BIT[3];
inline void SA() {
	lim = 'z', cur = 0;
	_for (i, 1, n)  sa[i] = i, rnk[i] = s[i], Log[i] = Log[i >> 1] + 1;
	for (int k = 1; k < n; k <<= 1) {
		iota(id + 1, id + n + 1, 1), fill(cnt, cnt + lim + 1, 0), tot = 0;
		_for (i, 1, n)  cnt[rnk[i + k]] ++ , old[i] = rnk[i];
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  id[cnt[rnk[i + k]] -- ] = i; fill(cnt, cnt + lim + 1, 0);
		_for (i, 1, n)  cnt[rnk[id[i]]] ++ ;
		_for (i, 1, lim)  cnt[i] += cnt[i - 1];
		_all (i, n, 1)  sa[cnt[rnk[id[i]]] -- ] = id[i];
		_for (i, 1, n)  rnk[sa[i]] = old[sa[i]] == old[sa[i - 1]] && old[sa[i] + k] == old[sa[i - 1] + k] ? tot : ( ++ tot);
		if (tot == n)  break; lim = tot;
	}
} inline void Manacher() {
	int l = 1, r = - 1, k;
	_for (i, 1, m) {
		k = i > r ? 1 : min(f[l + r - i], r - i + 1);
		while (1 <= i - k && i + k <= m && t[i - k] == t[i + k])  k ++ ; f[i] = k -- ;
		if (r < i + k)  l = i - k, r = i + k;
	} _for (i, 1, n / 2) {
		k = f[i << 1 | 1] >> 1;
		if (k && k < i && s[i - k] > s[i + k + 1])  upd[i - k + 1].pb((Node){i + 1, 0, 1}), upd[i + 1].pb((Node){i + 1, 0, - 1});
	}
} inline void solve() {
	cin >> n >> q >> (s + 1), m = 0; int x, y; Log[0] = - 1;
	_for (i, 1, n)  s[n * 2 + 2 - i] = s[i]; s[n + 1] = '#', n = n * 2 + 1;
	_for (i, 1, n)  t[ ++ m] = '@', t[ ++ m] = s[i]; t[ ++ m] = '@'; Manacher(), SA();
	_for (i, 1, q)  cin >> x >> y, qry[(x & 1) ^ 1][n - x].pb((Node){rnk[x] + 1, i, 1}), qry[(x & 1) ^ 1][n + 1 - x - 2 * y].pb((Node){rnk[x] + 1, i, - 1}), qry[2][x].pb((Node){x + y, i, - 1});
	_for (i, 1, n) {
		if (s[i] ^ '#')  BIT[i & 1].update(rnk[i], 1);
		for (Node j : upd[i])  BIT[2].update(j.x, j.op);
		_for (k, 0, 2)  for (Node j : qry[k][i])  ans[j.id] += j.op * (k <= 1 ? BIT[k].query(j.x, n) : BIT[k].query(1, j.x));
	} _for (i, 1, q)  cout << ans[i] << "\n";
	_for (i, 0, max({n, m, q}) + 5)  cnt[i] = old[i] = sa[i] = rnk[i] = id[i] = s[i] = ans[i] = f[i] = 0, qry[0][i].clear(), qry[1][i].clear(), qry[2][i].clear(), upd[i].clear(); BIT[0].clear(), BIT[1].clear(), BIT[2].clear();
} signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	int T; cin >> T >> T; while (T -- )  solve();
	return 0;
}

后缀自动机(SAM)

SAM 可以理解为把一个字符串的所有子串压缩起来的一个结构,总点数和边数都是线性的。

具体怎么构建及原理不阐述,详见 OI-wiki模板

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define ll long long
#define pb push_back
using namespace std;
const int N = 2e6 + 5;
int n; char s[N]; vector<int> Tr[N]; ll ans;
inline void chkmax(ll & x, ll y) { x = x > y ? x : y; }
struct Suffix_Automation {
	int ncnt, lst, cur, val[N]; struct Node { int fa, len, ch[26]; } tr[N];
	inline int cre(int x) { tr[ ++ ncnt].fa = - 1, tr[ncnt].len = x; return ncnt; }
	inline void init() { ncnt = - 1, lst = cre(0); }
	inline int clone(int x) { return tr[ ++ ncnt] = tr[x], ncnt; }
	inline void ins(int c) {
		int p = lst, cur = cre(tr[lst].len + 1), q, nq; val[cur] = 1;
		while ( ~ p && ! tr[p].ch[c])  tr[p].ch[c] = cur, p = tr[p].fa;
		if ( ! ~ p)  tr[cur].fa = 0;
		else {
			q = tr[p].ch[c];
			if (tr[q].len == tr[p].len + 1)  tr[cur].fa = q;
			else {
				nq = clone(q), tr[nq].len = tr[p].len + 1, tr[cur].fa = tr[q].fa = nq;
				while ( ~ p && tr[p].ch[c] == q)  tr[p].ch[c] = nq, p = tr[p].fa;
			}
		} lst = cur;
	}
} SAM;
inline void dfs(int u) { for (int v : Tr[u])  dfs(v), SAM.val[u] += SAM.val[v]; chkmax(ans, SAM.val[u] == 1 ? 0 : 1ll * SAM.val[u] * SAM.tr[u].len); }
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s + 1), n = strlen(s + 1), SAM.init();
	_for (i, 1, n)  SAM.ins(s[i] - 'a');
	_for (i, 1, SAM.ncnt)  Tr[SAM.tr[i].fa].pb(i); dfs(0), cout << ans << "\n";
	return 0;
}

第一遍在 fa 树上 dfs 后可以求出 \(f_i\) 表示以节点 \(i\) 代表的串的出现次数。

然后进行第二轮在 DAG 上 DP。注意要分 \(t = 0 / 1\) 讨论。

求答案的话就是在 DAG 上 dfs。复杂度是线性的。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define ll long long
#define pb push_back
using namespace std;
const int N = 1e6 + 5;
int n, type, vis[N]; char s[N]; vector<int> Tr[N]; ll k, f[N], g[N];
struct Suffix_Automation {
	int ncnt, cur, lst; struct Node { int len, fa, ch[26]; } tr[N];
	inline int cre(int x) { tr[ ++ ncnt].fa = - 1, tr[ncnt].len = x; return ncnt; }
	inline void init() { ncnt = - 1, lst = cre(0); }
	inline int clone(int x) { return tr[ ++ ncnt] = tr[x], ncnt; }
	inline void ins(int c) {
		int p = lst, cur = cre(tr[lst].len + 1), q, nq; f[cur] = 1;
		while ( ~ p && ! tr[p].ch[c])  tr[p].ch[c] = cur, p = tr[p].fa;
		if ( ! ~ p)  tr[cur].fa = 0;
		else {
			q = tr[p].ch[c];
			if (tr[q].len == tr[p].len + 1)  tr[cur].fa = q;
			else {
				nq = clone(q), tr[nq].len = tr[p].len + 1, tr[cur].fa = tr[q].fa = nq;
				while ( ~ p && tr[p].ch[c] == q)  tr[p].ch[c] = nq, p = tr[p].fa;
			}
		} lst = cur;
	}
} SAM;
void dfs1(int u) { for (int v : Tr[u])  dfs1(v), f[u] += f[v]; }
void dfs2(int u) {
	if (vis[u])  return ; vis[u] = 1; int v;
	_for (i, 0, 25)  if ((v = SAM.tr[u].ch[i]))  dfs2(v), g[u] += g[v];
} void dfs3(int u, ll k) {
	if (k <= f[u])  return ; k -= f[u]; int v;
	_for (i, 0, 25)  if ((v = SAM.tr[u].ch[i])) {
		if (k > g[v])  k -= g[v];
		else  return cout << (char)(i + 'a'), dfs3(v, k);
	}
} signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s + 1) >> type >> k, n = strlen(s + 1), SAM.init();
	_for (i, 1, n)  SAM.ins(s[i] - 'a');
	_for (i, 1, SAM.ncnt)  if ( ~ SAM.tr[i].fa)  Tr[SAM.tr[i].fa].pb(i); dfs1(0);
	_for (i, 0, SAM.ncnt)  g[i] = type ? f[i] : (f[i] = 1); f[0] = g[0] = 0, dfs2(0);
	if (k > g[0])  cout << - 1 << "\n";
	else  dfs3(0, k), cout << "\n";
	return 0;
}

P4070 [SDOI2016] 生成魔咒

SAM 性质:每个节点的等价类字符串的长度是一段区间。

而没新增一个节点就代表多出了那么多个不同的子串。于是答案为 \(\sum_p len_p - len_{fa_p}\)

值域较大,离散化后开 umap 存储即可。

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define ll long long
using namespace std;
const int N = 2e5 + 5;
int n, t, a[N], b[N]; ll ans;
struct Suffix_Automation {
	int ncnt, cur, lst; struct Node { int len, fa; unordered_map<int, int> ch; } tr[N];
	inline int cre(int x) { tr[ ++ ncnt].fa = - 1, tr[ncnt].len = x; return ncnt; }
	inline void init() { ncnt = - 1, cre(0); }
	inline int clone(int x) { return tr[ ++ ncnt] = tr[x], ncnt; }
	inline void ins(int x) {
		int p = lst, cur = cre(tr[lst].len + 1), q, nq;
		while ( ~ p && ! tr[p].ch[x])  tr[p].ch[x] = cur, p = tr[p].fa;
		if ( ! ~ p)  tr[cur].fa = 0;
		else {
			q = tr[p].ch[x];
			if (tr[q].len == tr[p].len + 1)  tr[cur].fa = q;
			else {
				nq = clone(q), tr[nq].len = tr[p].len + 1, tr[cur].fa = tr[q].fa = nq;
				while ( ~ p && tr[p].ch[x] == q)  tr[p].ch[x] = nq, p = tr[p].fa;
			}
		} lst = cur, ans += tr[cur].len - tr[tr[cur].fa].len;
	}
} SAM;
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n, SAM.init();
	_for (i, 1, n)  cin >> a[i], b[i] = a[i]; sort(b + 1, b + n + 1), t = unique(b + 1, b + n + 1) - a - 1;
	_for (i, 1, n)  SAM.ins(lower_bound(b + 1, b + n + 1, a[i]) - b), cout << ans << "\n";
	return 0;
}

P5341 [TJOI2019] 甲苯先生和大中锋的字符串

也是板子题,求答案是一个区间加后求全局 \(\max\),直接差分即可。

复杂度是 \(O(n)\)

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
#define pb push_back
using namespace std;
const int N = 4e5 + 5;
int n, res, ans, cnt[N]; ll k, f[N]; char s[N]; vector<int> Tr[N];
struct Suffix_Automation {
	int ncnt, lst, cur; struct Node { int len, fa, ch[26]; } tr[N];
	inline int cre(int x) { tr[ ++ ncnt].fa = - 1, tr[ncnt].len = x; return ncnt; }
	inline void init() { ncnt = - 1, cre(0); }
	inline int clone(int x) { return tr[ ++ ncnt] = tr[x], ncnt; }
	inline void ins(int x) {
		int p = lst, cur = cre(tr[lst].len + 1), q, nq; f[cur] = 1;
		while ( ~ p && ! tr[p].ch[x])  tr[p].ch[x] = cur, p = tr[p].fa;
		if ( ! ~ p)  tr[cur].fa = 0;
		else {
			q = tr[p].ch[x];
			if (tr[q].len == tr[p].len + 1)  tr[cur].fa = q;
			else {
				nq = clone(q), tr[nq].len = tr[p].len + 1, tr[cur].fa = tr[q].fa = nq;
				while ( ~ p && tr[p].ch[x] == q)  tr[p].ch[x] = nq, p = tr[p].fa;
			}
		} lst = cur;
	} inline void clear() {
		lst = cur = 0;
		_for (i, 0, n)  cnt[i] = 0;
		_for (i, 0, ncnt) {
			tr[i].len = tr[i].fa = f[i] = 0, Tr[i].clear();
			_for (j, 0, 25)  tr[i].ch[j] = 0;
		} ncnt = 0;
	}
} SAM;
void dfs(int u) { for (int v : Tr[u])  dfs(v), f[u] += f[v]; }
inline void solve() {
	cin >> (s + 1) >> k, n = strlen(s + 1), ans = res = 0, SAM.init();
	_for (i, 1, n)  SAM.ins(s[i] - 'a');
	_for (i, 1, SAM.ncnt)  if ( ~ SAM.tr[i].fa)  Tr[SAM.tr[i].fa].pb(i); dfs(0);
	_for (i, 1, SAM.ncnt)  if (f[i] == k)  cnt[SAM.tr[i].len] ++ , cnt[SAM.tr[SAM.tr[i].fa].len] -- ;
	_all (i, n, 1)  if ((cnt[i] += cnt[i + 1]) > res)  res = cnt[i], ans = i; cout << (ans ? ans : - 1) << "\n", SAM.clear();
} signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	int T; cin >> T; while (T -- )  solve();
	return 0;
}

P6640 [BJOI2020] 封印

考虑对 \(s\) 串的每个位置求出 \(len_i\) 表示 \(s[i - len_i + 1, i]\)\(t\) 串的子串的最大的 \(len_i\)

如何求呢?对 \(t\) 串建 SAM,\(s\) 每加入一个字符就在 SAM 上跳 fa,跳到一个含有当前出边的节点并过去,最后节点的 \(len\) 就是所求(原因是显然的)。

预处理可以 \(O(n)\) 完成。

现在查询变成了:\(\max_{i = l}^r \min(len_i, i - l + 1)\)

考虑二分答案,设为 \(mid\),显然 \(l \le i \le l + mid - 2\)\(i\) 已经不可能成为答案了,所以只需保证 \(\max_{i = l + mid - 1}^r len_i \ge mid\) 即可。

复杂度是 \(O(n \log n)\)

Code
#include <bits/stdc++.h>
#define _for(i, a, b)  for (int i = (a); i <= (b); i ++ )
#define _all(i, a, b)  for (int i = (a); i >= (b); i -- )
#define ll long long
#define ld long double
#define PII pair<int, int>
#define mp make_pair
#define fi first
#define se second
#define pb push_back
using namespace std;
const int N = 4e5 + 5;
int n, m, q, ans, len[N]; char s[N], t[N];
inline void chkmin(int & x, int y) { x = x < y ? x : y; }
inline void chkmax(int & x, int y) { x = x > y ? x : y; }
struct Sparse_Table {
	int Log[N], ST[20][N];
	inline void init(int * a) {
		Log[0] = - 1;
		_for (i, 1, n)  Log[i] = Log[i >> 1] + 1, ST[0][i] = a[i];
		_for (k, 1, Log[n])  _for (i, 1, n - (1 << k) + 1)  ST[k][i] = max(ST[k - 1][i], ST[k - 1][i + (1 << (k - 1))]);
	} inline int query(int l, int r) { int k = Log[r - l + 1]; return max(ST[k][l], ST[k][r - (1 << k) + 1]); }
} ST;
struct Suffix_Automation {
	int ncnt, lst; struct Node { int len, fa, ch[26]; } tr[N];
	inline int cre(int x) { tr[ ++ ncnt].fa = - 1, tr[ncnt].len = x; return ncnt; }
	inline void init() { ncnt = - 1, cre(0); }
	inline int clone(int x) { return tr[ ++ ncnt] = tr[x], ncnt; }
	inline void ins(int x) {
		int p = lst, cur = cre(tr[lst].len + 1), q, nq;
		while ( ~ p && ! tr[p].ch[x])  tr[p].ch[x] = cur, p = tr[p].fa;
		if ( ! ~ p)  tr[cur].fa = 0;
		else {
			q = tr[p].ch[x];
			if (tr[q].len == tr[p].len + 1)  tr[cur].fa = q;
			else {
				nq = clone(q), tr[nq].len = tr[p].len + 1, tr[cur].fa = tr[q].fa = nq;
				while ( ~ p && tr[p].ch[x] == q)  tr[p].ch[x] = nq, p = tr[p].fa;
			}
		} lst = cur;
	}
} SAM;
signed main() {
	ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> (s + 1) >> (t + 1) >> q, n = strlen(s + 1), m = strlen(t + 1), SAM.init(); int p = 0, curlen = 0, l, r, L, R, mid;
	_for (i, 1, m)  SAM.ins(t[i] - 'a');
	_for (i, 1, n) {
		while (p && ! SAM.tr[p].ch[s[i] - 'a'])  curlen = SAM.tr[p = SAM.tr[p].fa].len;
		if (SAM.tr[p].ch[s[i] - 'a'])  p = SAM.tr[p].ch[s[i] - 'a'], curlen ++ ; len[i] = curlen;
	} ST.init(len);
	while (q -- ) {
		cin >> l >> r, L = 1, R = r - l + 1, ans = 0;
		while (L <= R)  mid = (L + R) >> 1, ST.query(l + mid - 1, r) >= mid ? (ans = mid, L = mid + 1) : (R = mid - 1); cout << ans << "\n";
	}
	return 0;
}
posted @ 2025-03-17 11:20  Ray_Wu  阅读(16)  评论(0)    收藏  举报