题解 P5211 【[ZJOI2017]字符串】
zghtyarecrenj · · 题解
Upd 2020/06/15:优化了 \overline 太丑的问题,添上代码。
ZJOI2017 字符串
前置芝士:Lyndon 分解,Significant Suffixes,线段树,字符串哈希,分块。
如果你会 Significant Suffixes 相关知识,请阅读 [0 Marks & Facts] 后直接跳到后面的 进入正题。
0 Marks & Facts
- 我们定义两个字符串
a 和b ,如果a 的字典序<b ,则我们称a < b 。 - 如果
a 是b 的前缀且a \ne b ,则我们称a \sqsubset b 。 - 如果
a 是b 的前缀,则我们称a \sqsubseteq b 。 - 如果
a < b 且a 不是b 的前缀,则我们称a \triangleleft b 。即a \triangleleft b \Longleftrightarrow (a < b) \wedge (a \not\sqsubseteq b) 。 -
-
a^n$ 表示 $n$ 个 $a$ 拼接在一起。e.g. ${a^2b} = {aab} -
- 我们定义字符集为
\Sigma ,组成的字符串为\Sigma^* ,\Sigma^+ = \Sigma^* \setminus \{\epsilon\} -
-
\operatorname{pref}^+(a) = \operatorname{pref}(a) \setminus \{a,\epsilon\},\ \operatorname{suf}^+(a) = \operatorname{suf}^+(a) \setminus \{a, \epsilon\}
一些非常显然的 Fact:
- 如果
a \triangleleft b ,则{au} < {bv} 。
1 Lyndon Words
1.1 Definition
Lyndon Word:一个串是一个 Lyndon Word 当且仅当
Theory 1.2.2 Existence of CFL
这个结论和 [Theory 1.2.3] Uniqueness of CFL 是两个很有趣的结论。
对于任意的串
s ,\operatorname{CFL}(s) 一定存在。
我们考虑,单个的字母一定是 Lyndon Word。
根据 [Theory 1.2.1 Lyndon Concatanation],我们可以把字典序小的两个 Lyndon Word 并起来,所以我们把所有的字典序单增的序列都并起来,剩下的就是一个合法的 CFL。
Theory 1.2.3 Uniqueness of CFL
对于任意的串
s ,\operatorname{CFL}(s) 一定唯一。
反证法,假设有两种方案。我们取第一个不同的位置,可以很容易地得到矛盾,因为这个和 CFL 的定义矛盾了。
Q: 为什么不写详细点呢…… A;因为我懒!!!
然后我们就得到了 CFL 存在且唯一。由此有两个推论:
Theory 1.2.4 Lyndon Suffixes and Lyndon Prefixes
好玩且显然的 Fact。
反证法,因为如果
Theory 1.2.5 Theory of Minsuf
其实这是一道题,要求
一个字符串
s 的最小后缀是w_k 。
反证法,假设最小后缀是
我们有
1.3 Duval's Algorithm
时间复杂度
2 Significant Suffixes
2.1 Definition
我们令
Significant Suffixes:
同理如果
Theory 2.2.2 Significant Suffixes Theory
\Lambda(u)\subseteq \{s_i | i \in [1,n]\}
反证法:如果这个命题不成立,则我们分类讨论
i) 假设有一个串
2.3 Other Theories
Theory 2.3.1 Lambda Subset Theory
如果有
2 个串u 和v ,满足|u| \le |v| ,则我们有\begin{aligned}\Lambda(uv) &\subseteq \Lambda(v) \cup \{\operatorname{maxsuf}^R(u, v)\} \\&= \Lambda(u) \cup{\max _{s \in \Lambda(u)}}^R \{sv\}\end{aligned}
理由很简单,因为
Theory 2.3.2 Significant Suffixes Log Theory
一个字符串
S 的 Significant Suffixes 至多有\log n 个。
原命题可以很容易地转化为:(感谢 yhx 的证明)
如果两个 Significant Suffixes
u ,v 满足|u| < |v| ,那么2|u| < |v| 。
反证法。设存在
所以我们可以非常容易地知道,
所以,
由于
而
2.4 Facts
我们知道
-
\Lambda(u) = \{s_{\lambda}, \cdots, s_{n+1}\} -
\operatorname{minsuf}(u) = s_n -
\operatorname{maxsuf}^R(u) = s_\lambda -
{x_\lambda}^\infty > \cdots > {x_m}^\infty - 我们有一个串
v ,{x_i}^\infty > v > {x_{i+1}}^\infty 。则{s_\lambda v} > \cdots > {s_{i+1}v} < \cdots < {s_kv} 。 - 对于两个串
u 和v ,有|u|<|v| ,\Lambda({uv}) \subseteq \{\operatorname{maxsuf}^R(u, v)\} \cup \Lambda(v) = \{\min_{w \in \Lambda(u)}{wv}\} \cup \Lambda(v) 。
这里的 Proof 先咕着吧。贴个 Reference:
- Tomohiro, I., Nakashima, Y., Inenaga, S., Bannai, H., & Takeda, M. (2016). Faster Lyndon factorization algorithms for SLP and LZ78 compressed text. Theor. Comput. Sci., 656, 215-224.
- Kociumaka, T. (2016). Minimal Suffix and Rotation of a Substring in Optimal Time. ArXiv, abs/1601.08051
进入正题。
我们先考虑不带修的情况。
由 [Theory 2.3.1 Lambda Subset Theory],我们可以很容易地想到考虑建一棵线段树来维护 Significant Suffixes。
细节:如果线段树的 mid = l + r >> 1,则左边的区间比右边长一些。但是上面的这个结论对于 mid = l + r + 1 >> 1,使得左儿子总不比右儿子短)
可以存一下当前代表的串的所有 Significant Suffixes,然后直接考虑合并(把右边的所有的直接加进来,左边的都循环一遍,字典序最长的加进去)得到父节点的 Significant Suffixes 即可。(看不懂的看代码)
由 [Theory 2.3.2 Lambda Log Theory] 可知,每一个集合都是
接下来考虑带修的情况。
我们需要快速地求两个串的 LCP,又有一个线段树,所以可以很自然地想到一个线段树+字符串哈希+二分LCP的算法。复杂度
我们考虑分块维护一些哈希,分
Q: 道理我都懂,但是为什么我挂了? A: 你是用了自然溢出哈希吧,换个双哈希试试。
code
const int N = 200000;
const int B = 450;
const int base1 = 233;
const int base2 = 5e8;
const int mod = 1e9 + 9;
int n, q, c[N];
std::vector<int> tree[N << 2];
namespace Block {
int bs, bdelta[B], power[N + 1], psum[B + 1], intrah[B][B], interh[B];
inline int getc(int i) { return c[i] + bdelta[i / bs]; }
inline int geth(int i) {
if (i == -1)
return 0;
const int bid = i / bs;
i = i % bs;
return ((ll)interh[bid] * power[i + 1] + intrah[bid][i] + (ll)bdelta[bid] * psum[i + 1]) % mod;
}
void init() { // 初始化分块和哈希
bs = ceil(sqrt(1.0 * n));
power[0] = 1, psum[0] = 0;
for (int i = 1; i <= n; ++i)
power[i] = (ll)power[i - 1] * base1 % mod;
for (int i = 1; i <= bs; ++i)
psum[i] = (psum[i - 1] + power[i - 1]) % mod;
for (int bid = 0, s = 0; s < n; ++bid, s += bs) {
interh[bid] = geth(s - 1);
int h = 0;
for (int r = 0; r < bs && s + r < n; ++r) {
h = ((ull)h * base1 + (c[s + r] + base2)) % mod;
intrah[bid][r] = h;
}
}
}
void hadd(int a, int b, int d) { // 对哈希修改
for (int bid = 0, s = 0; s < n; ++bid, s += bs) {
interh[bid] = geth(s - 1);
if (a <= s && s + bs <= b)
bdelta[bid] += d;
else if (s < b && a < s + bs) {
int h = 0;
for (int r = 0; r < bs && s + r < n; ++r) {
c[s + r] += bdelta[bid] + (a <= s + r && s + r < b ? d : 0);
h = ((ull)h * base1 + (c[s + r] + base2)) % mod;
intrah[bid][r] = h;
}
bdelta[bid] = 0;
}
}
}
template <bool flag = 1> bool cmp(int i, int j, int r) { // 比较两个串
int hi = geth(i - 1), hj = geth(j - 1);
int low = 0, high = r - j + 1;
while (low < high) {
int middle = low + high + 1 >> 1;
if (((ull)(hi + mod - hj) * power[middle] + geth(j + middle - 1) + mod - geth(i + middle - 1)) % mod == 0)
low = middle;
else
high = middle - 1;
}
return j + low - 1 == r ? flag : getc(i + low) < getc(j + low);
}
} // namespace Block
namespace Sgt {
inline void pushup(int k, int l, int r) { // 合并子节点信息
auto &sigsuf = tree[k] = tree[k * 2 + 1]; // 右子节点直接加进来
int best = -1;
for (int i : tree[k * 2]) // 左子节点遍历一遍
if (best == -1 || Block::cmp(i, best, r)) best = i; // 最“重要”的一个加进来
sigsuf.push_back(best);
}
void build(int k, int l, int r) {
if (l == r) return (void)(tree[k] = {l});
int mid = (l + r + 1) / 2; // 左边比右边大
build(k * 2, l, mid - 1);
build(k * 2 + 1, mid, r);
pushup(k, l, r);
}
void modify(int k, int l, int r, int a, int b) {
if (b < l || r < a || (a <= l && r <= b)) return;
int mid = (l + r + 1) / 2;
if (a < mid) modify(k * 2, l, mid - 1, a, b);
if (b >= mid) modify(k * 2 + 1, mid, r, a, b);
pushup(k, l, r);
}
void query(int k, int l, int r, int a, int b, int &best) {
if (b < l || r < a) return;
if (a <= l && r <= b) {
for (int v : tree[k])
if (best == -1 || Block::cmp<0>(v, best, b))
best = v;
return;
}
int mid = (l + r + 1) / 2;
if (b >= mid) query(k * 2 + 1, mid, r, a, b, best);
if (a < mid) query(k * 2, l, mid - 1, a, b, best);
}
} // namespace Sgt
主程序随便写,注意我的下标是从 0 开始的。