G2. Division + LCP (hard version)

G2. Division + LCP (hard version)

This is the hard version of the problem. In this version lr.

You are given a string s. For a fixed k, consider a division of s into exactly k continuous substrings w1,,wk. Let fk be the maximal possible LCP(w1,,wk) among all divisions.

LCP(w1,,wm) is the length of the Longest Common Prefix of the strings w1,,wm.

For example, if s=abababcab and k=4, a possible division is abababcab. The LCP(ab,ab,abc,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 fl,fl+1,,fr.

Input

The first line contains a single integer t (1t104) — the number of test cases.

The first line of each test case contains two integers n, l, r (1lrn2105) — 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 2105.

Output

For each test case, output rl+1 values: fl,,fr.

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(n2logn)。可以反过来考虑,如果每个子串的 LCP 都是 k,最多可以划分成几个子串?记为 fk

  当求出 fk(1kn) 后,对于每个 i(lir) 答案就是满足 fki 的最大的 k(显然可以通过减少子串的数量使得 LCP 仍是 k)。随着 i 的增大 k 必然是逐渐变小的(这是因为满足 fkik 的数量只会减少而不增加)。为此满足单调性,可以用双指针求出每个 i 的答案。

  现在的问题是如何求出 fk。对于固定的 k,显然 s0sk1 就是每个子串的 LCP,贪心的思路也很明显,如果有 sisi+k1=s0sk1,接下来应该从 i+k 开始继续找下一个与 s0sk1 匹配且最近的子串。因为需要与前缀匹配,可以用 Z Algorithm,若 zik 则说明从 i 开始长度为 k 的子串与 s0sk1 匹配。

  如果对于每个 k 都用上述做法求出 fk 时间复杂度是 O(n2),要优化的地方应该是找到下一个与前缀匹配的位置,即从某个位置 i 开始满足 zjk(ij<n) 的最小的位置 j。我们可以先把所有满足 zik(0i<n) 的下标存储到 std::set,这样就可以通过二分求得下一个位置了。为此我们可以倒叙枚举 k,从而维护出满足 zik 的下标。

  当 LCP 等于 k 时,最多可以划分出 nk 个子串,因此最多需要二分的次数就是 k=1nnknlogn

  AC 代码如下,时间复杂度为 O(nlog2n)

#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=ns,所以可以根据 s 的大小选择不同的做法。

  当 sn 时,可以直接二分答案,记作 ansi 表示划分成 i 个子串时的 LCP。这部分的时间复杂度为 O(nnlogn)

  而当 s>n 时,k=ns<n。直接暴力枚举 k(1kn),然后找到下一个满足 zik 的位置也直接暴力枚举。这部分的时间复杂度为 O(nn)。考虑如何更新答案,假如当 LCP 等于 k 时最多可以划分成 c 个子串,按理来说对于每个 1ic 应该更新 ansimax{ansi,k}。为了避免超时可以先更新 ansc,最后枚举完所有的 k 时,倒叙枚举来更新 ansimax{ansi,ansi+1}

  AC 代码如下,时间复杂度为 O(nnlogn)

#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

posted @   onlyblues  阅读(92)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Web Analytics
点击右上角即可分享
微信分享提示