\(\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
樹和遞迴找字串,導致常數太大,真的氣死人了。