更深地理解了 SA 和 SAM 。
Address
洛谷:https://www.luogu.org/problemnew/show/P2178
BZOJ:https://www.lydsy.com/JudgeOnline/problem.php?id=4199
UOJ:http://uoj.ac/submissions?submitter=xyz32768
Meaning
输入
n
n
和一个长度为 的字符串,再输入
n
n
个 整数(可能为负),第 个整数为
ai
a
i
,对于每个
i∈[0,n)
i
∈
[
0
,
n
)
,求:(
LCP(p,q)
LCP
(
p
,
q
)
为以
p
p
开始的后缀和以 开始的后缀的最长公共前缀长度)
(1)满足
的二元组 (p,q) ( p , q ) 的个数。
(2)
n≤3×105 n ≤ 3 × 10 5 。
First Of All
[BZOJ3238][AHOI2013]差异 的升级版。
同样是一道字符串题。
可以使用 后缀数组 / 后缀自动机 来做。
Solution 1
Algorithm:后缀数组+单调栈
考虑把问题拆开,对于每个
i
i
求出所有满足 的二元组
(p,q)
(
p
,
q
)
的个数以及
ap×aq
a
p
×
a
q
的最大值,然后个数做一遍前缀后缀和,最大值求一遍后缀最大值。
我们知道,一个字符串的
height
h
e
i
g
h
t
求出之后,任意两个不同后缀的最长共前缀都能用
height
h
e
i
g
h
t
中一段区间的最小值来表示。
而显然
height
h
e
i
g
h
t
的最小值最多只有
n−1
n
−
1
个取值。
对于每个
2≤i≤n
2
≤
i
≤
n
,用单调栈预处理出:
pre[i]
p
r
e
[
i
]
为满足
j<i
j
<
i
且
height[j]≤height[i]
h
e
i
g
h
t
[
j
]
≤
h
e
i
g
h
t
[
i
]
的最大的
j
j
。
为满足
j>i
j
>
i
且
height[j]<height[i]
h
e
i
g
h
t
[
j
]
<
h
e
i
g
h
t
[
i
]
的最小的
j
j
。
注意,一个为 另一个为
height[j]<height[i]
h
e
i
g
h
t
[
j
]
<
h
e
i
g
h
t
[
i
]
是为了避免重复。
这样,如果
l∈[pre[i]+1,i],r∈[i,nxt[i]−1]
l
∈
[
p
r
e
[
i
]
+
1
,
i
]
,
r
∈
[
i
,
n
x
t
[
i
]
−
1
]
,那么区间
[l,r]
[
l
,
r
]
的最小值(相同情况下位置编号最小)为
height[i]
h
e
i
g
h
t
[
i
]
。由于这样的区间最小值只有一个,因此这样就不会重复也不遗漏了。
于是,我们找到了
(i−pre[i])×(nxt[i]−i)
(
i
−
p
r
e
[
i
]
)
×
(
n
x
t
[
i
]
−
i
)
个
LCP
LCP
长度为
height[i]
h
e
i
g
h
t
[
i
]
的后缀对。上面所说的区间
[l,r]
[
l
,
r
]
对应原串中的后缀对就是
(sa[l]−1,sa[r])
(
s
a
[
l
]
−
1
,
s
a
[
r
]
)
。于是就可以以
arank[i]
a
r
a
n
k
[
i
]
为关键字建立一个序列,每次给出两个区间
[u,v][l,r]
[
u
,
v
]
[
l
,
r
]
,求:
可以用 rmq 实现这个询问。但要注意,由于 a a 有负数,因此不能盲目地在区间 中各选一个最大值相乘,而应该分别选出最大值和最小值后两两相乘。
时间复杂度 O(nlogn) O ( n log n ) 。
Code
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Rof(i, a, b) for (i = a; i >= b; i--)
#define Pow(i, a, b) for (i = a; i <= b; i <<= 1, swap(x, y))
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
typedef long long ll; const int N = 3e5 + 5, E = 21;
int n, m, a[N], sa[N], rank[N], height[N], w[N], rmq[N][E], rnq[N][E], Log[N], top,
stk[N], pre[N], nxt[N]; ll _cnt[N], _max[N]; char s[N];
void Noi2015() {
int i, j, k, *x = rank, *y = height; m = 26;
For (i, 1, n) w[x[i] = s[i] - 'a' + 1]++;
For (i, 2, m) w[i] += w[i - 1]; For (i, 1, n) sa[w[x[i]]--] = i;
Pow (k, 1, n - 1) {
int tt = 0; For (i, n - k + 1, n) y[++tt] = i;
For (i, 1, n) if (sa[i] > k) y[++tt] = sa[i] - k;
memset(w, 0, sizeof(w)); For (i, 1, n) w[x[i]]++;
For (i, 2, m) w[i] += w[i - 1]; Rof (i, n, 1)
sa[w[x[y[i]]]--] = y[i]; m = 0; For (i, 1, n) {
int u = sa[i], v = sa[i - 1];
y[u] = x[u] != x[v] || x[u + k] != x[v + k] ? ++m : m;
}
if (m == n) break;
}
if (y != rank) copy(y, y + n + 1, rank); k = 0; For (i, 1, n) {
if (k) k--; int u = sa[rank[i] - 1];
while (s[u + k] == s[i + k]) k++; height[rank[i]] = k;
}
Log[0] = -1; For (i, 1, n) Log[i] = Log[i >> 1] + 1;
For (i, 1, n) rmq[rank[i]][0] = rnq[rank[i]][0] = a[i];
For (j, 1, 19) For (i, 1, n - (1 << j) + 1)
rmq[i][j] = max(rmq[i][j - 1], rmq[i + (1 << j - 1)][j - 1]),
rnq[i][j] = min(rnq[i][j - 1], rnq[i + (1 << j - 1)][j - 1]);
stk[top = 0] = 1; For (i, 2, n) {
while (top && height[stk[top]] > height[i]) top--;
pre[stk[++top] = i] = stk[top - 1];
}
stk[top = 0] = n + 1; Rof (i, n, 2) {
while (top && height[stk[top]] >= height[i]) top--;
nxt[stk[++top] = i] = stk[top - 1];
}
}
int qmax(int l, int r) {
int x = Log[r - l + 1]; return max(rmq[l][x], rmq[r - (1 << x) + 1][x]);
}
int qmin(int l, int r) {
int x = Log[r - l + 1]; return min(rnq[l][x], rnq[r - (1 << x) + 1][x]);
}
int main() {
int i; n = read(); scanf("%s", s + 1); For (i, 1, n) a[i] = read();
For (i, 0, n - 1) _max[i] = -(1ll << 62);
Noi2015(); For (i, 2, n) {
_cnt[height[i]] += 1ll * (i - pre[i]) * (nxt[i] - i);
int lmax = qmax(pre[i], i - 1), lmin = qmin(pre[i], i - 1),
rmax = qmax(i, nxt[i] - 1), rmin = qmin(i, nxt[i] - 1);
_max[height[i]] = max(_max[height[i]], max(max(1ll * lmax * rmax,
1ll * lmin * rmin), max(1ll * lmax * rmin, 1ll * lmin * rmax)));
}
Rof (i, n - 2, 0) _cnt[i] += _cnt[i + 1], _max[i] = max(_max[i], _max[i + 1]);
For (i, 0, n - 1)
if (_cnt[i]) printf("%lld %lld\n", _cnt[i], _max[i]);
else puts("0 0"); return 0;
}
Solution 2
Algorithm:后缀数组+并查集
出题人要求
LCP(p,q)≥i
LCP
(
p
,
q
)
≥
i
而不是
=i
=
i
,这肯定有玄机。
还是求出
height
h
e
i
g
h
t
数组,但不像 Solution 1 那样把问题拆开。
我们考虑能不能设法使得求
≥i
≥
i
的结果时,
height
h
e
i
g
h
t
里面只有
≥i
≥
i
的数。
于是可以将
height
h
e
i
g
h
t
数组里的数从大到小插入,按照
i
i
从大到小的顺序,解决每个 的问题,保证解决到
≥i
≥
i
的问题时,被插入的数包含且仅包含所有
≥i
≥
i
的数。
这时候,如果已经插入
height
h
e
i
g
h
t
的数构成了
m
m
个连续区间,第 个区间为
[li,ri]
[
l
i
,
r
i
]
,那么满足
LCP(p,q)≥i
LCP
(
p
,
q
)
≥
i
的二元组
(p,q)
(
p
,
q
)
的个数为:
考虑到插入一个数时,插入的数左右的区间会被合并起来。因此用并查集维护连续区间,对于每个区间(联通集合),记录区间长度 size s i z e ,那么把两个集合 x x 和 合并时,就增加了 size[x]×size[y] s i z e [ x ] × s i z e [ y ] 对二元组。同时,也可以维护乘积最大值,即记录下联通块内 a a 的最值,在 和 y y 所在的区间内分别选最大值或最小值相乘。
复杂度仍然为 。
Code
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Rof(i, a, b) for (i = a; i >= b; i--)
#define Pow(k, n) for (k = 1; k < n; k <<= 1, swap(x, y))
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
typedef long long ll; const int N = 3e5 + 5; const ll INF = 1ll << 62;
int n, sa[N], rank[N], height[N], m, w[N], a[N], fa[N], mina[N], minb[N], sze[N],
maxa[N], maxb[N]; char s[N]; struct cyx {int val, id;} col[N]; bool pyz[N];
ll __, ___ = -INF, i__[N], i___[N];
inline bool comp(const cyx &a, const cyx &b) {return a.val > b.val;}
int cx(int x) {if (fa[x] != x) fa[x] = cx(fa[x]); return fa[x];}
void zm(int x, int y) {
x = cx(x); y = cx(y); __ += 1ll * sze[x] * sze[y]; sze[x] += sze[y];
___ = max(___, max(1ll * maxa[x] * maxb[y], 1ll * mina[x] * minb[y]));
___ = max(___, max(1ll * mina[x] * maxb[y], 1ll * maxa[x] * minb[y]));
mina[x] = min(mina[x], mina[y]); maxa[x] = max(maxa[x], maxa[y]);
minb[x] = min(minb[x], minb[y]); maxb[x] = max(maxb[x], maxb[y]);
sze[y] = mina[y] = maxa[y] = minb[y] = maxb[y] = 0; fa[y] = x;
}
void cyxisdalao() {
int i, k, *x = rank, *y = height; For (i, 1, n) w[x[i] = s[i] - 'a' + 1]++;
m = 26; For (i, 2, m) w[i] += w[i - 1]; For (i, 1, n)
sa[w[x[i]]--] = i; Pow(k, n) {
int tt = 0; For (i, n - k + 1, n) y[++tt] = i; For (i, 1, n)
if (sa[i] > k) y[++tt] = sa[i] - k; memset(w, 0, sizeof(w));
For (i, 1, n) w[x[i]]++; For (i, 2, m) w[i] += w[i - 1];
Rof (i, n, 1) sa[w[x[y[i]]]--] = y[i]; m = 0; For (i, 1, n) {
int u = sa[i], v = sa[i - 1];
y[u] = x[u] != x[v] || x[u + k] != x[v + k] ? ++m : m;
}
if (m == n) break;
}
if (y != rank) copy(y, y + n + 1, rank); k = 0; For (i, 1, n) {
if (k) k--; int u = sa[rank[i] - 1];
while (s[u + k] == s[i + k]) k++; height[rank[i]] = k;
}
For (i, 2, n) col[col[i].id = i].val = height[i];
sort(col + 2, col + n + 1, comp); For (i, 2, n) fa[i] = i;
}
int main() {
int i, sp = 2; n = read(); scanf("%s", s + 1); For (i, 1, n) a[i] = read();
cyxisdalao(); Rof (i, n - 1, 0) {
while (sp <= n && col[sp].val >= i) {
int x = col[sp].id; sze[x] = 1; mina[x] = maxa[x] = a[sa[x - 1]];
minb[x] = maxb[x] = a[sa[x]]; pyz[x] = 1; __++;
___ = max(___, 1ll * a[sa[x - 1]] * a[sa[x]]);
if (pyz[x - 1]) zm(x - 1, x); if (pyz[x + 1]) zm(x, x + 1); sp++;
}
i__[i] = __; i___[i] = ___;
}
For (i, 0, n - 1) if (!i__[i]) puts("0 0");
else printf("%lld %lld\n", i__[i], i___[i]); return 0;
}
Solution 3
Algorithm:后缀自动机
还是和 Solution 1 一样,把问题拆开。
但这时候,要把字符串和
a
a
都反转过来,这样后缀的最长公共前缀就变成了前缀的最长公共后缀。然后从后缀自动机建立 树。
而如果要求
LCS(p,q)
LCS
(
p
,
q
)
,那么只需要找到前缀
p
p
对应的节点 和前缀
q
q
对应的节点 ,求
Parent
P
a
r
e
n
t
树上的
LCA(u,v)=w
LCA
(
u
,
v
)
=
w
,
Maxlw
M
a
x
l
w
就是最长公共后缀的长度。由于状态数是
O(n)
O
(
n
)
的,因此只会有
O(n)
O
(
n
)
种不同的最长公共后缀。枚举
LCS
LCS
代表的状态节点
u
u
,这时候和 AHOI 2013 差异那道题一样,对 贡献的二元组个数为:
还没结束!还需要算出的一个东西是,假设树上的一部分点有权(如果一个点表示的是某个前缀,且是以 i i 结尾的前缀,那么权值为 ),那么需要对于每个 u u ,在 的子树中选出两个有权值的点 v,w v , w ,且不存在 u u 的一个子节点 使得 x x 的子树同时包含了 (否则 v v 和 的 LCA LCA 不是 u u ),求 的权值与 w w 的权值积的最大值。
可以使用与树的直径类似的方法,利用最大值和次大值来求得。然而由于 有负数,因此要将 a a 按照正负分类处理,才能求得最大乘积。
时间复杂度 ,常数略大。
Code
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define For(i, a, b) for (i = a; i <= b; i++)
#define Rof(i, a, b) for (i = a; i >= b; i--)
#define SAM(i, orz) for (; i && orz; i = T[i].fa)
#define Edge(u) for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
using namespace std;
inline int read() {
int res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
return bo ? ~res + 1 : res;
}
typedef long long ll; const int N = 3e5 + 5, M = 6e5 + 5, Inf = 0x3f3f3f3f;
const ll INF = 1ll << 62;
struct cyx {
int maxl, go[26], fa, ri, id; void init() {
maxl = fa = ri = id; memset(go, 0, sizeof(go));
}
} T[M];
int n, a[N], lst, QAQ, ecnt, nxt[M], adj[M], go[M], _maxa[M], _mina[M],
_maxb[M], _minb[M]; char s[N]; ll _cnt[N], _max[N];
void add_edge(int u, int v) {
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
}
void extend(int x) {
int c = s[x] - 'a', i = lst; T[lst = ++QAQ].init(); T[lst].ri = 1;
T[lst].maxl = T[lst].id = x; SAM (i, !T[i].go[c]) T[i].go[c] = lst;
if (!i) T[lst].fa = 1; else {
int j = T[i].go[c]; if (T[i].maxl + 1 == T[j].maxl) T[lst].fa = j;
else {
int p; T[p = ++QAQ] = T[j]; T[p].ri = T[p].id = 0;
T[lst].fa = T[j].fa = p; T[p].maxl = T[i].maxl + 1;
SAM (i, T[i].go[c] == j) T[i].go[c] = p;
}
}
}
void dfs(int u) {
_maxa[u] = _maxb[u] = -Inf; _mina[u] = _minb[u] = Inf;
if (T[u].id) {
if (a[T[u].id] >= 0) _maxa[u] = _mina[u] = a[T[u].id];
else _maxb[u] = _minb[u] = a[T[u].id];
}
int __maxa = -Inf, __maxb = -Inf, __mina = Inf, __minb = Inf;
Edge(u) dfs(v), T[u].ri += T[v].ri; _cnt[T[u].maxl] +=
1ll * T[u].ri * (T[u].ri - 1) >> 1; Edge(u) {
if (_maxa[v] > _maxa[u]) __maxa = _maxa[u], _maxa[u] = _maxa[v];
else if (_maxa[v] > __maxa) __maxa = _maxa[v];
if (_maxb[v] > _maxb[u]) __maxb = _maxb[u], _maxb[u] = _maxb[v];
else if (_maxb[v] > __maxb) __maxb = _maxb[v];
if (_mina[v] < _mina[u]) __mina = _mina[u], _mina[u] = _mina[v];
else if (_mina[v] < __mina) __mina = _mina[v];
if (_minb[v] < _minb[u]) __minb = _minb[u], _minb[u] = _minb[v];
else if (_minb[v] < __minb) __minb = _minb[v];
_cnt[T[u].maxl] -= 1ll * T[v].ri * (T[v].ri - 1) >> 1;
}
if (_maxa[u] != -Inf && __maxa != -Inf)
_max[T[u].maxl] = max(_max[T[u].maxl], 1ll * _maxa[u] * __maxa);
if (_minb[u] != Inf && __minb != Inf)
_max[T[u].maxl] = max(_max[T[u].maxl], 1ll * _minb[u] * __minb);
if (_mina[u] != Inf && _maxb[u] != -Inf)
_max[T[u].maxl] = max(_max[T[u].maxl], 1ll * _mina[u] * _maxb[u]);
}
void orzcyxdalao() {
int i; T[lst = QAQ = 1].init(); For (i, 1, n) extend(i);
For (i, 2, QAQ) add_edge(T[i].fa, i); For (i, 0, n - 1) _max[i] = -INF;
dfs(1); Rof (i, n - 2, 0) _cnt[i] += _cnt[i + 1],
_max[i] = max(_max[i], _max[i + 1]);
}
int main() {
int i; n = read(); scanf("%s", s + 1); Rof (i, n, 1) a[i] = read();
For (i, 1, n >> 1) swap(s[i], s[n - i + 1]); orzcyxdalao();
For (i, 0, n - 1) if (!_cnt[i]) puts("0 0");
else printf("%lld %lld\n", _cnt[i], _max[i]); return 0;
}
Compare
以下是三种做法在 洛谷、BZOJ、UOJ 三个 OJ 上的 代码长度、时间、空间 三方面的比较。第三行为 Solution 1 的做法,第二行为 Solution 2 的做法,第一行为 Solution 3 。
In Luogu
In BZOJ
In UOJ
In A Word
由此可见:
代码长度: Solution 2 短于 Solution 1 , Solution 1 短于 Solution 3 。
空间效率: Solution 2 优于 Solution 1 , Solution 1 优于 Solution 3 。
运行速度: Solution 3 快于 Solution 2 , Solution 2 快于 Solution 1 。
,