G2. Division + LCP (hard version)
G2. Division + LCP (hard version)
This is the hard version of the problem. In this version $l\le r$.
You are given a string $s$. For a fixed $k$, consider a division of $s$ into exactly $k$ continuous substrings $w_1,\dots,w_k$. Let $f_k$ be the maximal possible $LCP(w_1,\dots,w_k)$ among all divisions.
$LCP(w_1,\dots,w_m)$ is the length of the Longest Common Prefix of the strings $w_1,\dots,w_m$.
For example, if $s=abababcab$ and $k=4$, a possible division is $\color{red}{ab}\color{blue}{ab}\color{orange}{abc}\color{green}{ab}$. The $LCP(\color{red}{ab},\color{blue}{ab},\color{orange}{abc},\color{green}{ab})$ is $2$, since $ab$ is the Longest Common Prefix of those four strings. Note that each substring consists of a continuous segment of characters and each character belongs to exactly one substring.
Your task is to find $f_l,f_{l+1},\dots,f_r$.
Input
The first line contains a single integer $t$ ($1 \le t \le 10^4$) — the number of test cases.
The first line of each test case contains two integers $n$, $l$, $r$ ($1 \le l \le r \le n \le 2 \cdot 10^5$) — the length of the string and the given range.
The second line of each test case contains string $s$ of length $n$, all characters are lowercase English letters.
It is guaranteed that the sum of $n$ over all test cases does not exceed $2\cdot 10^5$.
Output
For each test case, output $r-l+1$ values: $f_l,\dots,f_r$.
Example
input
7
3 1 3
aba
3 2 3
aaa
7 1 5
abacaba
9 1 6
abababcab
10 1 10
aaaaaaawac
9 1 9
abafababa
7 2 7
vvzvvvv
output
3 1 0
1 1
7 3 1 1 0
9 2 2 2 0 0
10 3 2 1 1 1 1 1 0 0
9 3 2 1 1 0 0 0 0
2 2 1 1 1 0
解题思路
比 E 简单()
如果对每个 $k$ 都用 Division + LCP (easy version) 的方法二分出答案,时间复杂度是 $O(n^2 \log{n})$。可以反过来考虑,如果每个子串的 LCP 都是 $k$,最多可以划分成几个子串?记为 $f_k$。
当求出 $f_k \, (1 \leq k \leq n)$ 后,对于每个 $i \, (l \leq i \leq r)$ 答案就是满足 $f_k \geq i$ 的最大的 $k$(显然可以通过减少子串的数量使得 LCP 仍是 $k$)。随着 $i$ 的增大 $k$ 必然是逐渐变小的(这是因为满足 $f_k \geq i$ 的 $k$ 的数量只会减少而不增加)。为此满足单调性,可以用双指针求出每个 $i$ 的答案。
现在的问题是如何求出 $f_k$。对于固定的 $k$,显然 $s_0 \sim s_{k-1}$ 就是每个子串的 LCP,贪心的思路也很明显,如果有 $s_i \sim s_{i+k-1} = s_0 \sim s_{k-1}$,接下来应该从 $i+k$ 开始继续找下一个与 $s_0 \sim s_{k-1}$ 匹配且最近的子串。因为需要与前缀匹配,可以用 Z Algorithm,若 $z_i \geq k$ 则说明从 $i$ 开始长度为 $k$ 的子串与 $s_0 \sim s_{k-1}$ 匹配。
如果对于每个 $k$ 都用上述做法求出 $f_k$ 时间复杂度是 $O(n^2)$,要优化的地方应该是找到下一个与前缀匹配的位置,即从某个位置 $i$ 开始满足 $z_j \geq k \, (i \leq j < n)$ 的最小的位置 $j$。我们可以先把所有满足 $z_i \geq k \, (0 \leq i < n)$ 的下标存储到 std::set
,这样就可以通过二分求得下一个位置了。为此我们可以倒叙枚举 $k$,从而维护出满足 $z_i \geq k$ 的下标。
当 LCP 等于 $k$ 时,最多可以划分出 $\frac{n}{k}$ 个子串,因此最多需要二分的次数就是 $\sum\limits_{k=1}^{n}{\frac{n}{k}} \approx n \log{n}$。
AC 代码如下,时间复杂度为 $O(n \log^2{n})$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;
char s[N];
int z[N];
vector<int> p[N];
int f[N];
void solve() {
int n, l, r;
scanf("%d %d %d %s", &n, &l, &r, s);
memset(z, 0, n << 2);
z[0] = n;
for (int i = 1, j = 1; i < n; i++) {
if (i < j + z[j]) z[i] = min(z[i - j], j + z[j] - i);
while (i + z[i] < n && s[i + z[i]] == s[z[i]]) {
z[i]++;
}
if (i + z[i] > j + z[j]) j = i;
}
for (int i = 0; i <= n; i++) {
p[i].clear();
}
for (int i = 0; i < n; i++) {
p[z[i]].push_back(i);
}
set<int> st({n});
for (int i = n; i; i--) {
st.insert(p[i].begin(), p[i].end());
f[i] = 0;
int x = 0;
while (x < n) {
x = *st.lower_bound(x);
if (x < n) f[i]++;
x += i;
}
}
for (int i = l, j = n; i <= r; i++) {
while (j && f[j] < i) {
j--;
}
printf("%d ", j);
}
printf("\n");
}
int main() {
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
官方题解还给了根号分治的做法。主要是当划分的子串数量为 $s$ 时,LCP 为最大不超过 $k = \frac{n}{s}$,所以可以根据 $s$ 的大小选择不同的做法。
当 $s \leq \sqrt{n}$ 时,可以直接二分答案,记作 $\text{ans}_i$ 表示划分成 $i$ 个子串时的 LCP。这部分的时间复杂度为 $O(n \sqrt{n} \log{n})$。
而当 $s > \sqrt{n}$ 时,$k = \frac{n}{s} < \sqrt{n}$。直接暴力枚举 $k \, (1 \leq k \leq \sqrt{n})$,然后找到下一个满足 $z_i \geq k$ 的位置也直接暴力枚举。这部分的时间复杂度为 $O(n \sqrt{n})$。考虑如何更新答案,假如当 LCP 等于 $k$ 时最多可以划分成 $c$ 个子串,按理来说对于每个 $1 \leq i \leq c$ 应该更新 $\text{ans}_i \gets \max\{ \text{ans}_i, k \}$。为了避免超时可以先更新 $\text{ans}_c$,最后枚举完所有的 $k$ 时,倒叙枚举来更新 $\text{ans}_i \gets \max\{ \text{ans}_i, \text{ans}_{i+1} \}$。
AC 代码如下,时间复杂度为 $O(n \sqrt{n} \log{n})$:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5, B = 450;
int n, l, r;
char s[N];
int z[N];
int ans[N];
int get(int len) {
int s = 0;
for (int i = 0; i < n; i++) {
if (z[i] >= len) {
s++;
i += len - 1;
}
}
return s;
}
void solve() {
scanf("%d %d %d %s", &n, &l, &r, s);
memset(z, 0, n << 2);
z[0] = n;
for (int i = 1, j = 1; i < n; i++) {
if (i < j + z[j]) z[i] = min(z[i - j], j + z[j] - i);
while (i + z[i] < n && s[i + z[i]] == s[z[i]]) {
z[i]++;
}
if (i + z[i] > j + z[j]) j = i;
}
memset(ans, 0, n + 2 << 2);
for (int i = 1; i <= B; i++) {
int l = 0, r = n / i;
while (l < r) {
int mid = l + r + 1 >> 1;
if (get(mid) >= i) l = mid;
else r = mid - 1;
}
ans[i] = l;
}
for (int i = 1; i <= B; i++) {
int c = get(i);
ans[c] = max(ans[c], i);
}
for (int i = n; i; i--) {
ans[i] = max(ans[i], ans[i + 1]);
}
for (int i = l; i <= r; i++) {
printf("%d ", ans[i]);
}
printf("\n");
}
int main() {
int t;
scanf("%d", &t);
while (t--) {
solve();
}
return 0;
}
参考资料
Codeforces Round 943 (Div. 3) A - G:https://zhuanlan.zhihu.com/p/695745337
Codeforces Round 943 (Div. 3) Editorial:https://codeforces.com/blog/entry/129096
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/18171549