洛谷-P2178 學習筆記

LightningCreeper發表於2024-03-21

題面

[NOI2015] 品酒大會

題目描述

一年一度的“幻影閣夏日品酒大會”隆重開幕了。大會包含品嚐和趣味挑戰 兩個環節,分別向優勝者頒發“首席品酒家”和“首席獵手”兩個獎項,吸引了眾多品酒師參加。

在大會的晚餐上,調酒師 Rainbow 調製了 \(n\) 杯雞尾酒。這 \(n\) 杯雞尾酒排成一行,其中第 \(n\) 杯酒 (\(1 ≤ i ≤ n\)) 被貼上了一個標籤 \(s_i\) ,每個標籤都是 \(26\) 個小寫 英文字母之一。設 \(str(l, r)\) 表示第 \(l\) 杯酒到第 \(r\) 杯酒的 \(r - l + 1\) 個標籤順次連線構成的字串。若 \(str(p, p_0) = str(q, q_0)\),其中 \(1 ≤ p ≤ p_0 ≤ n\), \(1 ≤ q ≤ q_0 ≤ n\), \(p ≠ q\)\(p_0-p+1 = q_0 - q + 1 = r\) ,則稱第 \(p\) 杯酒與第 \(q\) 杯酒是“ \(r\) 相似” 的。當然兩杯“ \(r\) 相似”(\(r > 1\))的酒同時也是“ \(1\) 相似”、“ \(2\) 相似”、……、“ \((r - 1)\) 相似”的。特別地,對於任意的 \(1 ≤ p ,q ≤ n,p ≠ q\),第 \(p\) 杯酒和第 \(q\) 杯酒都 是“ \(0\) 相似”的。

在品嚐環節上,品酒師 Freda 輕鬆地評定了每一杯酒的美味度,憑藉其專業的水準和經驗成功奪取了“首席品酒家”的稱號,其中第 \(i\) 杯酒 (\(1 ≤ i ≤ n\)) 的 美味度為 \(a_i\) 。現在 Rainbow 公佈了挑戰環節的問題:本次大會調製的雞尾酒有一個特點,如果把第 \(p\) 杯酒與第 \(q\) 杯酒調兌在一起,將得到一杯美味度為 \(a_p\times a_q\) 的 酒。現在請各位品酒師分別對於 \(r = 0,1,2,⋯,n-1\) ,統計出有多少種方法可以 選出 \(2\) 杯“ \(r\) 相似”的酒,並回答選擇 \(2\) 杯“\(r\) 相似”的酒調兌可以得到的美味度的最大值。

輸入格式

\(1\) 行包含 \(1\) 個正整數 \(n\) ,表示雞尾酒的杯數。

\(2\) 行包含一個長度為 \(n\) 的字串 \(S\),其中第 \(i\) 個字元表示第 \(i\) 杯酒的標籤。

\(3\) 行包含 \(n\) 個整數,相鄰整數之間用單個空格隔開,其中第 \(i\) 個整數表示第 \(i\) 杯酒的美味度 \(a_i\)

輸出格式

包括 \(n\) 行。

\(i\) 行輸出 \(2\) 個整數,中間用單個空格隔開。第 \(1\) 個整 數表示選出兩杯“ \((i - 1)\) 相似”的酒的方案數,第 2 個整數表示選出兩杯 “ \((i - 1)\) 相似”的酒調兌可以得到的最大美味度。若不存在兩杯“ \((i - 1)\) 相似” 的酒,這兩個數均為 \(0\)

樣例 #1

樣例輸入 #1

10
ponoiiipoi
2 1 4 7 4 8 3 6 4 7

樣例輸出 #1

45 56
10 56
3 32
0 0
0 0
0 0
0 0
0 0
0 0
0 0

樣例 #2

樣例輸入 #2

12
abaabaabaaba
1 -2 3 -4 5 -6 7 -8 9 -10 11 -12

樣例輸出 #2

66 120
34 120
15 55
12 40
9 27
7 16
5 7
3 -4
2 -4
1 -4
0 0
0 0

提示

【樣例說明 1】

用二元組 \((p, q)\) 表示第 \(p\) 杯酒與第 \(q\) 杯酒。

\(0\) 相似:所有 \(45\) 對二元組都是 \(0\) 相似的,美味度最大的是 $8 × 7 = 56 $。

\(1\) 相似: $(1,8) (2,4) (2,9) (4,9) (5,6) (5,7) (5,10) (6,7) (6,10) (7,10) $,最大的 \(8 × 7 = 56\)

\(2\) 相似: \((1,8) (4,9) (5,6)\) ,最大的 \(4 × 8 = 32\)

沒有 \(3,4,5, ⋯ ,9\) 相似的兩杯酒,故均輸出 \(0\)

【時限1s,記憶體512M】

簡要題意

  • \(n\) 的字串 \(s\)

  • 對於 \(r \in [0, n)\)

    • \(\forall 1\le i < j \le n \space lcp(i, j) \ge r\) 的對數。

    • 以及 滿足 \(lcp(i, j) \ge r\)\(a_i \times a_j\) 的最大值。

思想

Step 1

看到 \(LCP\) 想到 \(Suffix\space Array\)

問題轉化成滿足 \(\min \limits_{x=sa_i + 1}^{sa_j} height_x \ge r\) 的答案。

Step 2

大於等於很難受,我們把他轉化成等於再做字尾和與最大值就行了。

問題轉化成滿足 \(\min \limits_{x=sa_i + 1}^{sa_j} height_x = r\) 的答案。

Step 3

要求一個區間內某最小值等於某值

經典套路

一般使用笛卡爾樹或並查集。

這裡使用並查集。

從大到小列舉height,每次把左右兩個區間合併並計算答案。

Step 4

考慮合併的時候如何更新。

第一問的答案只有在橫跨了列舉的值時才會產生貢獻。

將左右區間的長度相乘即可。

第二問的答案就拿左邊的最大最小,右邊的最大最小乘,因為可能有負數。

Talk is cheap, show me the code

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

constexpr int MAXN = 3e5 + 5;
constexpr long long INF = 0x3f3f3f3f3f3f3f3f;

int cnt[MAXN], oldrk[MAXN * 2], id[MAXN], key1[MAXN];

bool cmp(int x, int y, int w) {
    return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w];
}

struct SuffixArray {
    int n, sa[MAXN], rk[MAXN], height[MAXN];
    void init(string s) {
        memset(cnt, 0, sizeof cnt);
        memset(oldrk, 0, sizeof oldrk);
        memset(id, 0, sizeof id);
        memset(key1, 0, sizeof key1);
        memset(sa, 0, sizeof sa);
        memset(rk, 0, sizeof rk);
        memset(height, 0, sizeof height);
        n = int(s.size());
        int m = 27, p;
        s = " " + s;
        for (int i = 1; i <= n; i++)
            cnt[rk[i] = s[i] - 'a' + 1]++;
        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 w = 1;; w <<= 1, m = p) {
            p = 0;
            for (int i = n; i > n - w; i--)
                id[++p] = i;
            for (int i = 1; i <= n; i++)
                if (sa[i] > w)
                    id[++p] = sa[i] - w;

            memset(cnt, 0, sizeof cnt);
            for (int i = 1; i <= n; i++)
                cnt[key1[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[key1[i]]--] = id[i];

            memcpy(oldrk + 1, rk + 1, n * sizeof(int));
            p = 0;
            for (int i = 1; i <= n; i++) {
                if (cmp(sa[i], sa[i - 1], w))
                    rk[sa[i]] = p;
                else
                    rk[sa[i]] = ++p;
            }
            if (p == n)
                break;
        }

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

    bool ask(int l, int r) {
        return height[l] > height[r];
    }
} sa;

int n;
string s;
int a[MAXN], idx[MAXN];
long long res1[MAXN], res2[MAXN];

struct Union {
    int n; // node sums
    int p[MAXN]; // parent
    long long ans[MAXN], mx[MAXN], mn[MAXN], sz[MAXN];

    void init(int _n) {
        n = _n;
        for (int i = 1; i <= n; i++)
            p[i] = i, sz[i] = 1, mx[i] = mn[i] = a[i], ans[i] = -INF;
    }

    int get(int x) {
        if (x == p[x])
            return x;
        return p[x] = get(p[x]);
    }

    void merge(int x, int y, int len) {
        x = get(x);
        y = get(y);
        p[y] = x;
        res1[len] += 1ll * sz[x] * sz[y];
        sz[x] += sz[y];
        ans[x] = max({ ans[x], ans[y], 1ll * mx[x] * mx[y], 1ll * mx[x] * mn[y], 1ll * mn[x] * mx[y], 1ll * mn[x] * mn[y] });
        mx[x] = max(mx[x], mx[y]);
        mn[x] = min(mn[x], mn[y]);
        res2[len] = max(res2[len], ans[x]);
    }
} u;

bool cmp2(int x, int y) {
    return sa.ask(x, y);
}

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

    cin >> n >> s;
    for (int i = 1; i <= n; i++)
        cin >> a[i], idx[i] = i;

    u.init(n);

    sa.init(s);

    sort(idx + 2, idx + n + 1, cmp2);

    memset(res1, 0, sizeof res1);
    memset(res2, 0xc0, sizeof res2);

    for (int i = 2; i <= n; i++)
        u.merge(sa.sa[idx[i]], sa.sa[idx[i] - 1], sa.height[idx[i]]);

    for (int i = n - 1; i >= 0; i--)
        res1[i] += res1[i + 1];
    for (int i = n - 1; i >= 0; i--)
        res2[i] = max(res2[i], res2[i + 1]);

    for (int i = 0; i < n; i++) {
        cout << res1[i] << " ";
        if (res1[i] == 0)
            cout << 0 << endl;
        else
            cout << res2[i] << endl;
    }

        return 0;
}

運用到的知識點&Trick

SA(知識點)

\(Suffix\space Array\) 中字尾 \(i\) 與 字尾 \(j\)\(LCP\)\(\min \limits_{x=sa_i + 1}^{sa_j} height_x\)

並查集(Trick)

求滿足區間最小/最大值等於某數的答案。

從大往小/從小往大列舉最小/最大值並把左右區間合併計算答案。