【LOJ #6198】谢特

Description

Link:LOJ 6198

给出一个长度为 \(n\) 仅包含小写字符的字符串 \(s\)

定义后缀 \(i\) 的权值为 \(w_i\),定义两个不同后缀 \(i, j(i \neq j)\) 的贡献为 \(\mathrm{LCP}(i, j) + (w_i \operatorname{xor} w_j)\)。其中 \(\mathrm{LCP}(i, j)\) 表示后缀 \(i\) 和后缀 \(j\) 的最长公共前缀长度。

你需要求出任意两个不同后缀 \(i, j(i \neq j)\) 的贡献最大值。

数据范围:\(1 \leq n \leq 10^5\)\(0 \leq w_i < n\)

时空限制:\(1\)s / \(512\)MiB。

Solution

对于 \(\mathrm{LCP}\) 问题,有一个观点:

  • 若使用 SA,就是将字符串问题转化成了序列问题(height 数组)。
  • 若使用 SAM,就是将字符串问题转化成了树上问题(parent 树)。

本题的结构相当广泛。许多题目都是类似的:使用 "height 数组" 或 "parent 树" 确定一个 \(\mathrm{LCP}\) 大小,然后启发式合并处理二元关系。

SA 需要用 ST 表或笛卡尔树处理 height 数组,注意这里的笛卡尔树只需要处理 \(2 \sim n\),计算答案的时候左端点需要减一;而 SAM 有个好处就是不用写 ST 表或笛卡尔树。

算法一:SA + 可持久化 0/1 trie

对字符串 \(s\) 做一遍 SA,将 height 数组求出。

定义排名为 \(i\) 的后缀权值为 \(w'_i\) = \(w_{\mathrm{SA}_i}\),故排名为 \(i, j(i \neq j)\) 的后缀的贡献为

\[\min\limits_{i < k \leq j} \{ \mathrm{height}_k \} + \left( w'_i \operatorname{xor} w'_j \right) \]

考虑最值分治。定义分治函数 \(\mathrm{solve}(l, r)\) 表示计算 \(l \leq i < j \leq r\) 情况下的答案。

取分治重心 \(p\) 为区间 \((l, r]\)\(\mathrm{height}\) 值最小的位置(使用 ST 表或笛卡尔树求出)。先考虑计算 \(l \leq i < p \leq j \leq r\) 情况下的答案,随后调用 \(\mathrm{solve}(l, p - 1), \mathrm{solve}(p, r)\) 即可。

此时 \(\min\limits_{i < k \leq j} \{ \mathrm{height}_k \} = \mathrm{height}_p\),故只需考虑 \(w'_i \operatorname{xor} w'_j\) 的最大值。对于分治重心分出来的两个区间 \([l, p - 1], [p, r]\),我们选择长度较小的一段区间,穷举该区间里的每一个点,使用可持久化 0/1 trie 求出该点与另一段区间的异或最大值。

倒过来看这个过程,每次枚举一个区间,总区间长度就至少乘二,这本质上是一个启发式合并(故称作启发式分裂)。

时间复杂度 \(O(n \log^2 n)\)

#include <bits/stdc++.h>

#define debug(a) std::cout << #a << "=" << (a) << ' '

using s64 = long long;
using u64 = unsigned long long;

/* 取 min */
template <class T>
inline void chmin(T &x, const T &y) {
    if (x > y) {
        x = y;
    }
}
/* 取 max */
template <class T>
inline void chmax(T &x, const T &y) {
    if (x < y) {
        x = y;
    }
}

/* ----- ----- ----- 正文 ----- ----- ----- */

const int N = 100100;

int n;
std::string str;

int w[N];

int root[N];
namespace trie {
    int NodeCount;
    struct node {
        int trans[2];
        int cnt;
    } t[N * 32];

    void init() {
        NodeCount = 0;
    }

    void insert(int &p, int q, int d, int x) {
        p = ++ NodeCount, t[p] = t[q], t[p].cnt ++;
        if (d < 0) return;
        int v = x >> d & 1;
        insert(t[p].trans[v], t[q].trans[v], d - 1, x);
    }

    int ask(int p, int q, int d, int x) {
        if (d < 0) return 0;
        int v = x >> d & 1;
        if (t[t[q].trans[v ^ 1]].cnt - t[t[p].trans[v ^ 1]].cnt) {
            return ask(t[p].trans[v ^ 1], t[q].trans[v ^ 1], d - 1, x) + (1 << d);
        } else {
            return ask(t[p].trans[v], t[q].trans[v], d - 1, x);
        }
    }
    int ask(int l, int r, int x) {
        return ask(root[l - 1], root[r], 16, x);
    }
}

int ans;

namespace SA {
    int m;
    int sa[N], rk[N], height[N];
    int cnt[N], id[N], px[N];
    int tmprk[N * 2];

    int same(int x, int y, int k) {
        return tmprk[x] == tmprk[y] && tmprk[x + k] == tmprk[y + k];
    }

    void build() {
        if (n == 1) { sa[1] = rk[1] = 1, height[1] = 0; return; }

        for (int i = n + 1; i <= 2 * n; i ++) tmprk[i] = -1;

        m = 256;
        for (int i = 1; i <= n; i ++) rk[i] = str[i];

        for (int i = 1; i <= m; i ++) cnt[i] = 0;
        for (int i = 1; i <= n; i ++) cnt[rk[i]] ++;
        for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
        for (int i = n; i >= 1; i --) sa[cnt[rk[i]] --] = i;

        for (int k = 1, p = 0; k < n; k <<= 1, m = p) {
            p = 0;
            for (int i = n - k + 1; i <= n; i ++) id[++ p] = i;
            for (int i = 1; i <= n; i ++)
                if (sa[i] > k) id[++ p] = sa[i] - k;
            
            for (int i = 1; i <= m; i ++) cnt[i] = 0;
            for (int i = 1; i <= n; i ++) cnt[px[i] = rk[id[i]]] ++;
            for (int i = 1; i <= m; i ++) cnt[i] += cnt[i - 1];
            for (int i = n; i >= 1; i --) sa[cnt[px[i]] --] = id[i];

            for (int i = 1; i <= n; i ++) tmprk[i] = rk[i];

            p = 0;
            for (int i = 1; i <= n; i ++) rk[sa[i]] = same(sa[i - 1], sa[i], k) ? p : ++ p;

            if (p == n) break;
        }

        for (int i = 1, h = 0; i <= n; i ++) {
            if (h) h --;
            while (str[i + h] == str[sa[rk[i] - 1] + h]) h ++;
            height[rk[i]] = h;
        }
    }

    int rt;
    struct node {
        int lc, rc;
        int L, R;
    } t[N];

    int top, stk[N];
    void build_tree() {
        top = 0;
        for (int i = 2; i <= n; i ++) {
            t[i].lc = t[i].rc = 0, t[i].L = t[i].R = i;
        }

        for (int i = 2; i <= n; i ++) {
            int k = top;
            while (k && height[stk[k]] > height[i]) k --;

            if (k) t[stk[k]].rc = i;
            if (k < top) t[i].lc = stk[k + 1];

            stk[top = k + 1] = i;
        }
        rt = stk[1];
    }

    void solve(int p) {
        if (t[p].lc) solve(t[p].lc), chmin(t[p].L, t[t[p].lc].L);
        if (t[p].rc) solve(t[p].rc), chmax(t[p].R, t[t[p].rc].R);

        int l = t[p].L - 1, r = t[p].R;

        if (p - l < r - p + 1) {
            for (int i = l; i < p; i ++) {
                chmax(ans, height[p] + trie::ask(p, r, w[sa[i]]));
            }
        } else {
            for (int i = p; i <= r; i ++) {
                chmax(ans, height[p] + trie::ask(l, p - 1, w[sa[i]]));
            }
        }
    }
}

int main() {
    std::ios::sync_with_stdio(0);
    std::cin.tie(0);

    std::cin >> n;
    std::cin >> str, str = " " + str;

    for (int i = 1; i <= n; i ++) {
        std::cin >> w[i];
    }

    SA::build();

    trie::init();
    for (int i = 1; i <= n; i ++) {
        trie::insert(root[i], root[i - 1], 16, w[SA::sa[i]]);
    }

    SA::build_tree();

    ans = 0;
    SA::solve(SA::rt);

    std::cout << ans << '\n';

    return 0;
}

/**
 *  心中无女人
 *  比赛自然神
 *  模板第一页
 *  忘掉心上人
**/

算法二:SAM + 0/1 trie 合并

对字符串 \(s\)反串建出 SAM,并求出 parent 树。

设后缀 \(i\) 在反串建出的 SAM 上的代表节点为 \(e_i\),则 \(\mathrm{LCP}(i, j)\) 等于 parent 树上 \(\mathrm{LCA}(e_i, e_j)\) 的 maxl。

考虑遍历 parent 树。设当前节点为 \(u\),我们要计算两个后缀 \(i, j\) 代表节点的 \(\mathrm{LCA}\) 等于 \(u\) 时的贡献。

依次遍历 \(u\) 的所有儿子 \(v\),考虑计算后缀 \(i\) 来自子树 \(u\),后缀 \(j\) 来自子树 \(v\) 情况下的贡献。此时 \(\mathrm{LCP}(i, j) = \mathrm{maxl}_u\),故只需要考虑 \(w_i \operatorname{xor} w_j\) 的最大值。我们选择较小的一棵子树,穷举该子树内的每一个终止节点,使用 0/1 trie 求出该点与另一棵子树内终止节点的异或最大值(过程需要用 0/1 trie 合并维护)。

这仍然是一个启发式合并。

时间复杂度 \(O(n \log^2 n)\)

#include <bits/stdc++.h>

#define debug(a) std::cout << #a << "=" << (a) << ' '

using s64 = long long;
using u64 = unsigned long long;

/* 取 min */
template <class T>
inline void chmin(T &x, const T &y) {
    if (x > y) {
        x = y;
    }
}
/* 取 max */
template <class T>
inline void chmax(T &x, const T &y) {
    if (x < y) {
        x = y;
    }
}

/* ----- ----- ----- 正文 ----- ----- ----- */

const int N = 100100;
const int SIZE = N * 2;

int n;
std::string str;

int w[N];

int root[SIZE];
std::vector<int> g[SIZE];

namespace trie {
    int NodeCount;
    struct node {
        int trans[2];
    } t[SIZE * 32];

    int create() {
        int p = ++ NodeCount;
        t[p].trans[0] = t[p].trans[1] = 0;
        return p;
    }

    void init() {
        NodeCount = 0;
        for (int i = 1; i <= n * 2; i ++) {
            root[i] = 0;
            g[i].clear();
        }
    }

    void insert(int &p, int d, int x) {
        p = create();
        if (d < 0) return;
        int v = x >> d & 1;
        insert(t[p].trans[v], d - 1, x);
    }

    int merge(int p, int q) {
        if (!p || !q) return p ^ q;
        t[p].trans[0] = merge(t[p].trans[0], t[q].trans[0]);
        t[p].trans[1] = merge(t[p].trans[1], t[q].trans[1]);
        return p;
    }

    int ask(int p, int d, int x) {
        if (d < 0) return 0;
        int v = x >> d & 1;
        if (t[p].trans[v ^ 1]) {
            return ask(t[p].trans[v ^ 1], d - 1, x) + (1 << d);
        } else {
            return ask(t[p].trans[v], d - 1, x);
        }
    }
}

int ans;

namespace SAM {
    int NodeCount = 1, Last = 1;
    struct node {
        int trans[26];
        int link, maxl;
    } t[SIZE];

    int create() {
        int p = ++ NodeCount;
        for (int i = 0; i < 26; i ++) t[p].trans[i] = 0;
        t[p].link = t[p].maxl = 0;
        return p;
    }

    void init() {
        NodeCount = 0;
        Last = create();
    }

    void extend(int c, int i) {
        int p = Last,
            np = Last = create();
        
        t[np].maxl = t[p].maxl + 1;
        
        for (; p && t[p].trans[c] == 0; p = t[p].link) t[p].trans[c] = np;

        if (!p) {
            t[np].link = 1;
        } else {
            int q = t[p].trans[c];

            if (t[q].maxl == t[p].maxl + 1) {
                t[np].link = q;
            } else {
                int nq = ++ NodeCount; t[nq] = t[q]; t[nq].maxl = t[p].maxl + 1;
                t[np].link = t[q].link = nq;

                for (; p && t[p].trans[c] == q; p = t[p].link) t[p].trans[c] = nq;
            }
        }

        trie::insert(root[np], 16, w[i]);
        g[np].push_back(w[i]);
    }

    std::vector<int> son[SIZE];
    
    void build_tree() {
        for (int i = 1; i <= NodeCount; i ++) {
            son[i].clear();
        }
        for (int i = 2; i <= NodeCount; i ++) {
            son[t[i].link].push_back(i);
        }
    }

    void solve(int u) {
        for (int v : son[u]) {
            solve(v);

            if (g[u].size() < g[v].size()) {
                std::swap(g[u], g[v]);
                std::swap(root[u], root[v]);
            }

            for (int x : g[v]) {
                chmax(ans, t[u].maxl + trie::ask(root[u], 16, x));
                g[u].push_back(x);
            }
            root[u] = trie::merge(root[u], root[v]);
            g[v].clear();
        }
    }
}

int main() {
    std::ios::sync_with_stdio(0);
    std::cin.tie(0);

    std::cin >> n;
    std::cin >> str, str = " " + str;
    
    for (int i = 1; i <= n; i ++) {
        std::cin >> w[i];
    }

    trie::init();
    SAM::init();

    for (int i = n; i >= 1; i --) {
        SAM::extend(str[i] - 'a', i);
    }

    SAM::build_tree();

    ans = 0;
    SAM::solve(1);

    std::cout << ans << '\n';

    return 0;
}

/**
 *  心中无女人
 *  比赛自然神
 *  模板第一页
 *  忘掉心上人
**/
posted @ 2021-04-05 09:57  Calculatelove  阅读(186)  评论(0)    收藏  举报