Minlexes題解

Z_drj發表於2024-03-31

\(\texttt{Problem Link}\)

簡要題意

在一個字串 \(s\) 中,對於每個字尾,任意刪掉一些相鄰的相同的字元,使得字串字典序最小。

注意:刪掉之後拼起來再出現的相鄰相同字元不能夠刪除。

思路

倍增好題

發現存在區域性最優解(最優子結構),並且可以轉移到其它結點,可以考慮使用 dp

那就設 \(f _ i\) 表示 \([i , n]\) 經過一些操作,達成的字典序最小的字串(求字尾最優解,從後往前遍歷)。

可以得到狀態轉移:

\[ f_i = \begin{cases} f_{i+1} + s_i, & s_i \neq s_{i+1},\\ \min \{f_{i+1} + s_i , f_{i+2}\} & s_i = s_{i+1}. \\ \end{cases} \]

\(s_i\) 表示第 \(i\) 個字元,\(\min\) 表示字典序更小的那個。

邊界條件:\(f_n = s_n\)

但是這樣做的複雜度是 \(\mathcal{O}(n^2)\)

字典序比較最佳化

瓶頸在於比較字典序。

考慮對字典序比較進行最佳化。

回顧字典序比較的過程,

過程是對於兩個字串,從頭到尾一個個字元進行比較,遇到第一個字元不同時,就返回答案。

那麼就可以有一個想法透過一些操作,快速找到第一個不同的字元。

可以考慮使用倍增最佳化,把兩個串比較時,透過倍增找到 hash 值第一個不同的地方,這樣字串比較就能最佳化到 \(\mathcal{O}(\log n)\)

輸出最佳化

接下來的問題就是輸出,

因為輸出長字元只要輸出前 \(5\) 個和最後 \(2\) 個。

所以可以對於前面的字元直接輸出,後面的字元也可以寫個倍增往後跳到需要的。

最後總的複雜度就是 \(\mathcal{O}(n \log n)\)

Code

#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>

using i64 = long long ;
using ui64 = unsigned long long ;

const int N = 1e5 + 5 ;
const int base = 131 ;

char s[N];
int f[N] , g[N] , h[N];
ui64 Pow[100];
ui64 Hash[20][N];//自然溢位
int nxt[20][N];
int n;

void updata(int u, int v){
    v = h[v];
    h[u] = u;
    g[u] = g[v] + 1;//記錄當前的長度
    nxt[0][u] = v;
    Hash[0][u] = s[u] - 'a';

    for(int i = 1; i <= 19; i++)
        nxt[i][u] = nxt[i-1][nxt[i-1][u]] , Hash[i][u] = Hash[i-1][u] * Pow[i - 1] + Hash[i-1][nxt[i-1][u]]; //處理hash倍增
// nxt是方便向後跳2^k的
}

int min(int x, int y){
    int tx = x , ty = y;
    
    x = h[x] , y = h[y];

    for(int i = 19; i >= 0; i--)
        if(nxt[i][x] && nxt[i][y] && Hash[i][x] == Hash[i][y])
            x = nxt[i][x] , y = nxt[i][y];//找到第一個不同的字元
        
    return Hash[0][x] < Hash[0][y]? tx: ty;//小細節不能寫 <= 寫 <= 會導致部分少刪除
}

int main(){

    scanf("%s",s+1);

    n = strlen(s+1);

    Pow[0] = base;
    for(int i = 1; i <= 90; i++)
        Pow[i] = Pow[i - 1] * Pow[i - 1];//預處理 base 的 2^i 次方,方便將hash值拼起來

    for(int i = n; i >= 1; i--) {
        updata(i,i+1);//預設是接上字元

        if(i < n && s[i] == s[i+1] && min(i,i + 2) == i + 2) {//刪除更優
            h[i] = h[h[i + 2]];
            g[i] = g[h[i + 2]];
        }
    }

    for(int i = 1; i <= n; i++) {
        printf("%d ",g[i]);

        int id = h[i];
        if(g[i] <= 10) {
            for(int j = id; j && j <= n; j = nxt[0][j])
                putchar(s[j]);
        } else {
            for(int j = 1; j <= 5; j++ , id = nxt[0][id])//前5個字元直接暴力找
                putchar(s[id]);
            
            printf("...");

            id = h[i];
            int len = g[i] - 2 ;

            for(int i = 19; i >= 0; i--)
                if(nxt[i][id] && (1<<i) <= len) len -= 1<<i , id = nxt[i][id];//倍增找最後兩個字元
            
            for(int j = 1; j <= 2; j++ , id = nxt[0][id])
                putchar(s[id]);
        }

        puts("");
    }
    return 0;
}

牢騷

本來思路是完全正確的,但是我用了一個 Trie 樹和遞迴找字串,導致常數太大,真的氣死人了。