BZOJ 4556: [Tjoi2016&Heoi2016]字符串(后缀数组 + 二分答案 + 主席树 + ST表 or 后缀数组 + 暴力)

Posted zjp_shadow

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BZOJ 4556: [Tjoi2016&Heoi2016]字符串(后缀数组 + 二分答案 + 主席树 + ST表 or 后缀数组 + 暴力)相关的知识,希望对你有一定的参考价值。

题意

一个长为 \\(n\\) 的字符串 \\(s\\),和 \\(m\\) 个询问。每次询问有 \\(4\\) 个参数分别为 \\(a,b,c,d\\)

要你告诉它 \\(s[a...b]\\) 中的所有子串 和 \\(s[c...d]\\) 的 最长公共前缀 \\((\\mathrm{LCP})\\) 的最大值。

\\((1\\le n,m\\le 10^5, a\\le b,c\\le d,1\\le a,b,c,d\\le n)\\)

题解

一开始看错了题 以为是 \\([a,b]\\) 中所有子串 和 \\([c,d]\\) 中所有子串的 \\(\\mathrm{LCP}\\) 这怎么能做啊!!!

仔细观察了一下 发现是 \\([a,b]\\) 的所有子串 和 \\([c,d]\\) 。。。。

那么题目就变 简单 了一点。。。

首先我们考虑与 \\([c,d]\\) 有最长 \\(\\mathrm{LCP}\\) 的在哪里

不难发现 就是后缀排序后 \\(rk[i]\\)\\(rk[c]\\) 最靠近的 \\(i\\)

那么我们可以转化求 \\([a,b]\\) 中的这个 \\(i\\) 就行了qwq

答案表示出来大概是这样子的。

\\[\\displaystyle \\mathrm{ans}=\\min(d-c+1,\\max_{i=a}^{b} \\{\\min(\\mathrm{LCP}(i, c),b-i+1)\\}) \\]

我们发现 直接求这个 \\(i\\) 会被后面的 \\(b-i+1\\) 限制掉 所以不能直接这样求

但我们可以考虑转化一下 我们考虑 二分答案 如果判断一个答案是否存在就容易一些了

我们考虑二分这个长度 假设是 \\(len\\) 那么前面的 \\(i\\) 就只能存在于 \\([a,b-len+1]\\) 这个区间内

然后看 \\(rk[c]\\) 周围连续的 \\(height[q]\\ge len\\) 可以延伸到哪个范围 这个东西 直接用 \\(\\mathrm{ST}\\) 可以实现

怎么实现呢 类似于倍增的思想 就是把那段距离看成一串二进制 然后从高到低去消掉一个个数就行了

得到这个区间 \\([sl,sr]\\) 后 我们就需要查找里面是否存在 \\([a,b-len+1]\\) 的元素 这个东西就直接上 主席树 就行了

最后时间复杂度就是 \\(O(n \\log^2 n)\\) 咯(令 \\(n,q\\) 同级)

思路就很清晰了 但是代码就一点也不好写。。。 先挂出来吧。。

代码

/**************************************************************
    Problem: 4556
    User: zjp_shadow
    Language: C++
    Result: Time_Limit_Exceed
****************************************************************/
 
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
using namespace std;
 
inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}
 
inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == \'-\') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * fh;
}
 
void File() {
#ifdef zjp_shadow
    freopen ("4556.in", "r", stdin);
    freopen ("4556.out", "w", stdout);
#endif
}
 
const int N = 2e6 + 1e3;
struct Suffix_Array {
    int sa[N], tmp[N], rk[N], n, m, c[N];
    char str[N];
 
    void Init(int n, char bas[]) { this -> n = n; Cpy(str, bas); }
 
    inline void Radix_Sort() {
        For (i, 1, m) c[i] = 0;
        For (i, 1, n) ++ c[rk[i]];
        For (i, 1, m) c[i] += c[i - 1];
        Fordown (i, n, 1) sa[c[rk[tmp[i]]] --] = tmp[i];
    }
 
    inline void Build_Sa() {
        For (i, 1, n) rk[i] = str[i], tmp[i] = i;
        m = 255; Radix_Sort();
        for (register int k = 1, p; k <= n; k <<= 1) {
            p = 0;
            For (i, n - k + 1, n) tmp[++ p] = i;
            For (i, 1, n) if (sa[i] > k) tmp[++ p] = sa[i] - k;
            Radix_Sort(); swap(rk, tmp);
            rk[sa[1]] = 1, m = 1;
            For (i, 2, n) rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? m : ++ m;
            if (m >= n) return ;
        }
    }
 
    int height[N];
    inline void Get_Height() {
        for (int i = 1, j, k = 0; i <= n; ++ i) {
            if (k) -- k;
            j = sa[rk[i] - 1];
            while (str[i + k] == str[j + k]) ++ k;
            height[rk[i]] = k;
        }
    }
} SA;
 
struct Chairman_Tree {
    int ls[N], rs[N], tot[N], Size, rt[N];
 
    void Insert(int &o, int pre, int l, int r, int up) {
        o = ++ Size; ls[o] = ls[pre]; rs[o] = rs[pre];
        tot[o] = tot[pre] + 1; if (l == r) return;
        int mid = (l + r) >> 1;
        if (up <= mid) Insert(ls[o], ls[pre], l, mid, up);
        else Insert(rs[o], rs[pre], mid + 1, r, up);
    }
 
    bool Query(int s, int t, int l, int r, int ql, int qr) {
        int now = tot[t] - tot[s];
        if (!now) return false;
        if (ql <= l && r <= qr) return true;
        int mid = (l + r) >> 1;
        if (ql <= mid && Query(ls[s], ls[t], l, mid, ql, qr)) return true;
        if (qr > mid && Query(rs[s], rs[t], mid + 1, r, ql, qr)) return true;
        return false;
    }
} CT;
 
struct Sparse_Table {
    int minv[N][20], Log[N];
 
    void Build(int n, int a[]) {
        For (i, 1, n) 
            minv[i][0] = a[i], Log[i] = Log[i >> 1] + 1;
        For (j, 1, Log[n])
            For (i, 1, n - (1 << j) + 1) 
                minv[i][j] = min(minv[i][j - 1], minv[i + (1 << (j - 1))][j - 1]);
    }
} ST1, ST2;
 
int n, m;
 
inline bool Check(int len, int a, int b, int c, int d) {
    int sl = SA.rk[c], sr = SA.rk[c];
    Fordown (i, ST2.Log[sl], 0) 
        if (ST2.minv[n - sl + 1][i] >= len) sl -= (1 << i);
    Fordown (i, ST1.Log[n - sr + 1], 0) 
        if (ST1.minv[sr + 1][i] >= len) sr += (1 << i);
    int ql = a, qr = b - len + 1;
    return CT.Query(CT.rt[ql - 1], CT.rt[qr], 1, n, sl, sr);
}
 
inline int Get_Ans(int a, int b, int c, int d) {
    int l = 1, r = min(b - a + 1, d - c + 1), ans = 0;
    while (l <= r) {
        int mid = (l + r) >> 1;
        if (Check(mid, a, b, c, d)) l = mid + 1, ans = mid;
        else r = mid - 1;
    }
    return ans;
}
 
int val1[N], val2[N];
char str[N];
 
int main () {
    File();
    n = read(); m = read();
    scanf ("%s", str + 1);
    SA.Init(n, str);
    SA.Build_Sa();
    SA.Get_Height();
 
    For (i, 1, n) {
        CT.Insert(CT.rt[i], CT.rt[i - 1], 1, n, SA.rk[i]);
        val1[i] = val2[n - i + 1] = SA.height[i];
    }
    ST1.Build(n, val1);
    ST2.Build(n, val2);
 
    For (i, 1, m) {
        int a = read(), b = read(), c = read(), d = read();
        printf ("%d\\n", Get_Ans(a, b, c, d));
    }
    //cerr << (double) clock() /CLOCKS_PER_SEC << endl;
    return 0;
}

彩蛋

细心的读者肯定发现了 这个代码的 result\\(\\mathrm{TLE}\\) 2333

为什么呢 本人常数巨大啊!!!

但这份代码交到 \\(luogu\\) 上不开 \\(O2\\) 是 8000ms 开了是 4000ms

我突然想看看别人怎么写的 然后查找一波最优解 诶 有个叫 yyb_test 的神犇 只要 400ms

我仔细翻阅了一波大佬的代码 惊了 这不是暴力吗!!!

这才有了我现在的标题 (后缀数组 + 暴力)

我们继续考虑之前答案的那个式子

\\[\\displaystyle \\mathrm{ans}=\\min(d-c+1,\\max_{i=a}^{b} \\{\\min(\\mathrm{LCP}(i, c),b-i+1)\\}) \\]

我们考虑向 \\(rk[c]\\) 前后去扫一下得到答案

其中如果此处 \\(sa[i]\\)\\([a,b]\\) 之中的话我们就计入答案就行了。

然后就有一个史诗级优化 就是当前扫的 \\(height\\)\\(\\min\\) 值 如果不优于当前的 \\(\\mathrm{ans}\\)

我们就可以轻易退出循环啦 这由于数据较为随机 所以 \\(height\\) 就比较降的比较快 所以就比较快了qwq

理论 时间复杂度 \\(O(nq)\\) 实际(随机数据) 时间复杂度 \\(O(\\frac{\\mathrm{std}}{10})\\) 23333

放个对比图2333

pic

BruteForce 代码

/**************************************************************
    Problem: 4556
    User: zjp_shadow
    Language: C++
    Result: Accepted
    Time:1768 ms
    Memory:59916 kb
****************************************************************/
 
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
using namespace std;
 
inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}
 
inline int read() {
    int x = 0, fh = 1; char ch = getchar();
    for (; !isdigit(ch); ch = getchar()) if (ch == \'-\') fh = -1;
    for (; isdigit(ch); ch = getchar()) x = (x * 10) + (ch ^ 48);
    return x * fh;
}
 
void File() {
#ifdef zjp_shadow
    freopen ("4556.in", "r", stdin);
    freopen ("4556.out", "w", stdout);
#endif
}
 
const int N = 2e6 + 1e3;
struct Suffix_Array {
    int sa[N], tmp[N], rk[N], n, m, c[N];
    char str[N];
 
    void Init(int n, char bas[]) { this -> n = n; Cpy(str, bas); }
 
    inline void Radix_Sort() {
        For (i, 1, m) c[i] = 0;
        For (i, 1, n) ++ c[rk[i]];
        For (i, 1, m) c[i] += c[i - 1];
        Fordown (i, n, 1) sa[c[rk[tmp[i]]] --] = tmp[i];
    }
 
    inline void Build_Sa() {
        For (i, 1, n) rk[i] = str[i], tmp[i] = i;
        m = 255; Radix_Sort();
        for (register int k = 1, p; k <= n; k <<= 1) {
            p = 0;
            For (i, n - k + 1, n) tmp[++ p] = i;
            For (i, 1, n) if (sa[i] > k) tmp[++ p] = sa[i] - k;
            Radix_Sort(); swap(rk, tmp);
            rk[sa[1]] = 1, m = 1;
            For (i, 2, n) rk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? m : ++ m;
            if (m >= n) return ;
        }
    }
 
    int height[N];
    inline void Get_Height() {
        for (int i = 1, j, k = 0; i <= n; ++ i) {
            if (k) -- k;
            j = sa[rk[i] - 1];
            while (str[i + k] == str[j + k]) ++ k;
            height[rk[i]] = k;
        }
    }
} SA;
 
int n, m;
inline int Get_Ans(int a, int b, int c, int d) {
    int ans = 0, len = min(b - a + 1, d - c + 1), tmp = len;
    Fordown (i, SA.rk[c], 1) {
        if (SA.sa[i] >= a && SA.sa[i] <= b)
            chkmax(ans, min(tmp, b - SA.sa[i] + 1));
        chkmin(tmp, SA.height[i]);
        if (tmp <= ans) break;
    }
    tmp = len;
    For (i, SA.rk[c] + 1, n) {
        chkmin(tmp, SA.height[i]);
        if (tmp <= ans) break;
        if (SA.sa[i] >= a && SA.sa[i] <= b)
            chkmax(ans, min(tmp, b - SA.sa[i] + 1));
    }
    return ans;
}
 
int val1[N], val2[N];
char str[N];
int main () {
    File();
    n = read(); m = read();
    scanf ("%s", str + 1);
    SA.Init(n, str);
    SA.Build_Sa();
    SA.Get_Height();
 
    For (i, 1, m) {
        int a = read(), b = read(), c = read(), d = read();
        printf ("%d\\n", Get_Ans(a, b, c, d));
    }
    //cerr << (double) clock() /CLOCKS_PER_SEC << endl;
    return 0;
}

以上是关于BZOJ 4556: [Tjoi2016&Heoi2016]字符串(后缀数组 + 二分答案 + 主席树 + ST表 or 后缀数组 + 暴力)的主要内容,如果未能解决你的问题,请参考以下文章

Bzoj4556: [Tjoi2016&Heoi2016]字符串 后缀数组

[BZOJ4556][Tjoi2016&Heoi2016]字符串 后缀数组+主席树

[BZOJ4556][TJOI2016&&HEOI2016]字符串(二分答案+后缀数组+RMQ+主席树)

bzoj 4556: [Tjoi2016&Heoi2016]字符串

[BZOJ4556][TJOI2016&&HEOI2016]字符串

Bzoj4556 [Tjoi2016&Heoi2016]字符串