Codeforces-1063F String Journey
Description
定义“Journey”为一个字符串序列 \(\{t_1, t_2, \cdots, t_k\}\),满足 \(\forall i\in [1, k)\),\(t_i\) 为 \(t_{i+1}\) 的子串且 \(|t_i| < |t_{i+1}|\)。\(k\) 为 Journey 的长度。
定义一个字符串 \(s\) 的“Journey”为一个字符串序列 \(\{t_1, t_2, \cdots, t_k\}\),存在一组 \(\{u_1, u_2, \cdots, u_{k+1}\}\) 满足 \(s=u_1 t_1 u_2 t_2 \cdots u_k t_k u_{k+1}\),其中 \(u_i\) 可为空。
给定一个字符串 \(s\),求它的最长 Journey。
Hint
\(|s| \le 5\times 10^5\)
Solution
一个很重要的性质:存在一个最优解,它的字符串序列长度为 \(\{k, k-1, \cdots , 2, 1\}\),即 相邻两个长度相差一。这个不难理解:对于一个其他相差超过一的解,也可以强制将长度大的子串削减,从而得到一个形如 \(\{k, k-1, \cdots , 2, 1\}\) 的解,而且显然也是最优的。那么这就提供给我们很大的便利:我们需要考虑的长度其实固定了,而且每次只会有一个字符的移除,因此只用考虑头或尾其一。
然后我们又可以通过将 \(s\) 整串翻转,使得我们考虑的长度是递增的,即 \(\{1, 2, \cdots, k-1, k\}\),也就是说本来在头尾删除字符,现在变成了添加。
接下来考虑一个 dp:\(f_i\) 表示以第 \(i\) 个位置结尾的 Journey,最后一个串的最大长度。然而这里与常规 dp 思路不同的是:我们并不考虑 \(f_i\) 如何转移而来,而是对于一个猜测的 \(f_i\) 的值,判断其可行性。
由于存在 单调性,即较大的 \(f_i\) 可以,那么更小的一定可以。于是考虑二分答案 \(f_i\)。对于当前二分得到的 \(f_i=k\),需使之成立需要什么条件?根据定义可知答案的第 \(k\) 个是 \(S[i-k+1:i]\),而我们需要一个在其之前的子串 \(t\),满足 \(t=S[i-k+1:i-1]\)(\(t\) 在后面加字符得到 \(S[i-k+1:i]\))或 \(t=S[i-k+2:i]\)(前面加字符)。但是,这还不够。存在这样的子串,还得要求其末尾位置 \(j\) 的 \(f\) 值 \(\ge k-1\)。
对于子串,考虑使用 SAM 来判断。比如说一个子串 \(t\),设它是前缀 \(S[:p]\) 的一个后缀,那么我们找到这个前缀在 SAM 中的位置,然后一直跳后缀链接,直到它对应的长度区间包含了 \(|t|\),然后对于 Parent Tree 上这个结点的子树(反向跳后缀链接相当于在前面添加字符)中的所有可以表示前缀的状态,对应的 dp 值的最大值如果 \(\ge k-1\),那么说明存在一个可以转移到 \(f_i=k\) 的前驱 dp 状态。(事实上,可以表示前缀的状态必然是 Parent Tree 的叶子,因为前缀无法在前面加字符)。
跳后缀链接的过程可以用 倍增 加速,而子树求和配合 Dfs 序就可以套 线段树 了。
还有些细节:首先有 \(f_i-f_{i-1} \le 1\),那么我们可以先钦定 \(f_i=f_{i-1}+1\),然后逐步减小,这样可以去掉二分的一个 \(\log\)。同时,为了让我们找到前面的子串与当前的不相交,我们先空出一段 不更新 dp 值。然后在逐步减小是慢慢更新过来。
时间复杂度 \(O(n\log n)\)。
Code
/*
* Author : _Wallace_
* Source : https://www.cnblogs.com/-Wallace-/
* Problem : Codeforces 1063F String Journey
*/
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
const int N = 1e6 + 5;
const int logN = 23;
const int S = 26;
int ch[N][S], link[N], len[N];
int total = 1, last = 1;
int extend(int c) {
int p = last, np = last = ++total;
len[np] = len[p] + 1;
for (; p && !ch[p][c]; p = link[p]) ch[p][c] = np;
if (!p) { link[np] = 1; return np; }
int q = ch[p][c];
if (len[q] == len[p] + 1) { link[np] = q; return np; }
int nq = ++total;
memcpy(ch[nq], ch[q], S << 2), link[nq] = link[q];
len[nq] = len[p] + 1, link[np] = link[q] = nq;
for (; p && ch[p][c] == q; p = link[p]) ch[p][c] = nq;
return np;
}
std::vector<int> adj[N];
int fa[N][logN], indfn[N], oudfn[N], timer(0);
void dfs(int x) {
indfn[x] = timer, fa[x][0] = link[x];
for (int j = 1; j < logN; j++) fa[x][j] = fa[fa[x][j - 1]][j - 1];
if (adj[x].empty()) { oudfn[x] = ++timer; return; }
for (auto y : adj[x]) dfs(y);
oudfn[x] = timer;
}
namespace segt {
#define mid ((l + r) >> 1)
int val[N << 2];
void update(int x, int l, int r, int p, int v) {
if (l == r) { val[x] = v; return; }
if (p <= mid) update(x << 1, l, mid, p, v);
else update(x << 1 | 1, mid + 1, r, p, v);
val[x] = std::max(val[x << 1], val[x << 1 | 1]);
}
int query(int x, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) return val[x];
if (ql > r || l > qr) return 0;
return std::max(query(x << 1, l, mid, ql, qr), query(x << 1 | 1, mid + 1, r, ql, qr));
}
#undef mid
}
int n, loc[N], f[N];
char s[N];
int upto(int x, int tar_l) {
for (int j = logN - 1; ~j; j--)
if (fa[x][j] && len[fa[x][j]] >= tar_l) x = fa[x][j];
return x;
}
signed main() {
scanf("%d%s", &n, s + 1);
std::reverse(s + 1, s + 1 + n);
for (int i = 1; i <= n; i++) loc[i] = extend(s[i] - 'a');
for (int i = 2; i <= total; i++) adj[link[i]].push_back(i);
dfs(1);
for (int i = 1, j = 0; i <= n; i++) {
f[i] = f[i - 1] + 1;
while (true) {
int x = upto(loc[i], f[i] - 1), y = upto(loc[i - 1], f[i] - 1);
if (segt::query(1, 1, n, indfn[x] + 1, oudfn[x]) >= f[i] - 1) break;
if (segt::query(1, 1, n, indfn[y] + 1, oudfn[y]) >= f[i] - 1) break;
--f[i], ++j, segt::update(1, 1, n, oudfn[loc[j]], f[j]);
}
}
printf("%d\n", *std::max_element(f + 1, f + 1 + n));
return 0;
}
本文来自博客园,作者:-Wallace-,转载请注明原文链接:https://www.cnblogs.com/-Wallace-/p/cf1063f.html