Codeforces Round 943 (Div. 3) G2. Division + LCP (hard version) 二分+Z函数

文章讨论了一个编程问题,涉及计算给定字符串如何按照指定数量分割成子串,使得这些子串的最长公共前缀最大。使用了z函数和分治策略解决此问题,输出特定范围内LCP的最大值列表。

Division + LCP (hard version)

题目描述

这是问题的困难版本。在这个版本中 l ≤ r l\le r lr

给你一个字符串 s s s 。对于固定的 k k k ,考虑将 s s s 恰好分成 k k k 个连续的子串 w 1 , … , w k w_1,\dots,w_k w1,,wk 。设 f k f_k fk 是所有分割中最大可能的 L C P ( w 1 , … , w k ) LCP(w_1,\dots,w_k) LCP(w1,,wk)

L C P ( w 1 , … , w m ) LCP(w_1,\dots,w_m) LCP(w1,,wm) 是字符串 w 1 , … , w m w_1,\dots,w_m w1,,wm 的最长公共前缀的长度。

例如,如果 s = a b a b a b c a b s=abababcab s=abababcab k = 4 k=4 k=4 ,可能的除法是 a b a b a b c a b \color{red}{ab}\color{blue}{ab}\color{orange}{abc}\color{green}{ab} abababcab 。由于 a b ab ab 是这四个字符串的最长公共前缀,因此 L C P ( a b , a b , a b c , a b ) LCP(\color{red}{ab},\color{blue}{ab},\color{orange}{abc},\color{green}{ab}) LCP(ab,ab,abc,ab) 就是 2 2 2

您的任务是找到 f l , f l + 1 , … , f r f_l,f_{l+1},\dots,f_r fl,fl+1,,fr

输入描述

第一行包含一个整数 t t t ( 1 ≤ t ≤ 1 0 4 1 \le t \le 10^4 1t104 ) - 测试用例数。

每个测试用例的第一行包含两个整数 n n n , l l l , r r r ( 1 ≤ l ≤ r ≤ n ≤ 2 ⋅ 1 0 5 1 \le l \le r \le n \le 2 \cdot 10^5 1lrn2105 ) - 字符串长度和给定范围。

每个测试用例的第二行包含长度为 n n n 的字符串 s s s ,所有字符均为小写英文字母。

保证所有测试用例中 n n n 的总和不超过 2 ⋅ 1 0 5 2\cdot 10^5 2105

输出描述

对于每个测试用例,输出 r − l + 1 r-l+1 rl+1 个值即 f l , … , f r f_l,\dots,f_r fl,,fr

样例输入

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

样例输出

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 

原题

CF——传送门

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAX = 2e5 + 6;
int ans[MAX];
vector<int> z_function(string s) // z函数
{
    int n = (int)s.length();
    vector<int> z(n);
    for (int i = 1, l = 0, r = 0; i < n; ++i)
    {
        if (i <= r && z[i - l] < r - i + 1)
        {
            z[i] = z[i - l];
        }
        else
        {
            z[i] = max(0, r - i + 1);
            while (i + z[i] < n && s[z[i]] == s[i + z[i]])
                ++z[i];
        }
        if (i + z[i] - 1 > r)
            l = i, r = i + z[i] - 1;
    }
    return z;
}
int f(vector<int> &z, int len) // 计算len长度前缀在字符串中的数量
{
    int n = z.size();
    int cnt = 1;
    for (int i = len; i < n;)
    {
        if (z[i] >= len)
            cnt++, i += len;
        else
            i++;
    }
    return cnt;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);

    int t;
    cin >> t;
    while (t--)
    {
        int n, Left, Right;
        cin >> n >> Left >> Right;
        string s;
        cin >> s;
        vector<int> z = z_function(s);
        // 分两种情况处理
        // 先处理k>√n
        // 对于k>√n,由于k*LCP<=n,所以LCP<=√n,于是就可以处理每个<=√n的LCP,找到对应可满足的最大的k
        for (int len = ceil(sqrt(n)); len >= 1; len--)
        {
            int num = f(z, len);
            ans[num] = max(ans[num], len);
        }
        // 由可满足的最大的k的LCP来更新较小的k的LCP
        for (int i = n - 1; i >= 1; i--)
        {
            ans[i] = max(ans[i], ans[i + 1]);
        }
        // 再处理k≤√n
        for (int k = 1; k <= ceil(sqrt(n)); k++)
        {
            int l = 0, r = n / k;
            while (l < r)
            {
                int mid = (l + r + 1) / 2; // 这里要 l + r +1 要不然会死循环
                if (f(z, mid) >= k)
                {
                    l = mid; // mid这个位置 满足条件之后 查找 [mid , right]的位置, 所以l移到mid的位置
                }
                else
                {
                    r = mid - 1; // [mid,r] 不满足条件, 所以要移到满足条件的一方, r = mid - 1
                }
            }
            // 最后的l,r是答案
            ans[k] = l;
        }
        // 输出答案
        for (int i = Left; i <= Right; i++)
        {
            if (i == Right)
                cout << ans[i] << '\n';
            else
                cout << ans[i] << ' ';
        }
        // 清空该测试用例的ans
        for (int i = 1; i <= n; i++)
        {
            ans[i] = 0;
        }
    }

    return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值