String
資訊讀書會的字串字串
雜湊(哈希)
唬爛(但他是好的
Hash
字典樹啦~
北市賽有考
trie
另一個字串匹配
似乎比較好寫?
Zalgorithm
好的字串匹配
Failure Function
KMP
跟Z很像
專門處理迴文
Manacher
複習一下
字串的語法小整理
什麼是字串
我是一條字串~
我也是喔
I am a string too.
就字元的陣列啦
一些字串的語法
#include<iostream>
#include<string>
using namespace std;
int main(){
string a;
cin >> a;
getline(cin, a);
int l=a.length();
a+="abc";
a[1]='e';
string b=a.substr(3, 5);
}
字串匹配
什麼是字串匹配
在字串裡面找字串
有點像command+f (ctrl+f)
abaabaaababb
欲找aab
字串匹配
abaabaaababb
方法#1
就直接兩個for迴圈開下去了啊
複雜度O(N^2)
int main(){
string a, b;
cin >> a >> b;
for(int i=0;i<=a.length()-b.length();i++){
bool find=1;
for(int j=0;j<b.length;j++){
if(a[i+j]!=b[j]) find=0;
}
if(find) cout << i << endl;
}
}
好慢喔
其實可以跑更快
等一下會講O(N)
雜湊
什麼是雜湊
隨便湊一湊
把字串當數字在用
f(string)=int
雜湊Hash
阿字串這麼長怎麼變整數
---> 資料壓縮
把長度為\(N\)的字串當成是\(N\)位數
並以某數\(p\)進位,最後再模\(M\)
常用雜湊法
\(\left ( a_{0}\times p^{n-1}+a_{1}\times p^{n-2}+...+a_{n-1}\times p^{0} \right )\%M\)
大概長這樣兒
\(\left ( a_{0}\times p^{0}+a_{1}\times p^{1}+...+a_{n-1}\times p^{n-1} \right )\%M\)
當然也可以這樣
阿上面提到的\(p\)跟\(M\)
最好要是兩個質數
p要比字元個數還要大\((29, 257)\)
M就選個很大的質數\((10^9+7, 998244353 )\)
常用雜湊法
這時候要比對兩字串是否相同
就直接比較兩字串Hash出來的數字就好了
字串匹配
雜湊是好的要基於:
同一個字串只會Hash出一個數字
且我們相信不同的字串很難湊出同樣的數字
因為我們剛才的雜湊函數是多項式形式
可以對每個字串的前綴都記錄一次
這樣要取子字串時只要用前綴相減取區間和
但要注意得出來的雜湊值可能多乘過p的幾次
再把他除掉還原回來即可
字串匹配
扣的
#include <iostream>
#define ll long long
using namespace std;
const ll p=257, M=1e9+7;
ll shash[1000005 ], thash[1000005 ], pp=1, ans=0;
int main(){
string s, t;
cin >> s >> t;
ll n=s.length(), m=t.length();
shash[0]=thash[0]=0;
for(int i=1;i<=n;i++){
shash[i]=(shash[i-1]+pp*s[i-1])%M;
pp=(pp*p)%M;
}
pp=1;
for(int i=1;i<=m;i++){
thash[i]=(thash[i-1]+pp*t[i-1])%M;
pp=(pp*p)%M;
}
pp=1;
for(int i=0;i<=n-m;i++){
if((shash[i+m]-shash[i]+M)%M==thash[m]*pp%M) ans++;
pp=(pp*p)%M;
}
cout << ans << endl;
return 0;
}
題目
字典樹
就是查字典
平常查英文字典的時候
我們會找第一個字母、第二個字母......
直到我們找到要找的字串
把每次找尋看成一條邊
---> 字典樹
字典樹trie
字典樹trie
tea : 3
ted : 4
in : 5
to : 7
inn : 9
i : 11
ten : 12
A : 15
實作
就實作一棵樹出來
每個節點存他可以連到的節點(偽指標?
這部分可以用map或直接用陣列
用map多帶log,用陣列多吃空間
題目
KMP
Knuth Morris Pratt algorithm
KMP字串匹配演算法的精髓
定義如下:
\(\pi [k]=\underset{i=0,1,...,k}{max}i\)
\(i:s[0\sim i-1]=s[k-(i-1) \sim k]\)
Failure function
剛才的東西用文字說明就是:
k-前綴字串的「次長共同前後綴」
次長共同前後綴
aabaaab
babbabba
Failure function
abaabaaab
\(\pi_{s}:0, 0, 1, 1, 2, 3, 4, 1, 2\)
怎麼找出這個表
暴力做可能需要\(O(N^3)\)
N個前綴,N次比較,每次比較長度為N
超級慢的啦
讓我們觀察一下怎麼樣讓他變好
觀察性質1-1
\(\pi[i+1]\leq\pi[i]+1\)
如果 \(\pi[i+1]=k\)
則代表\(s[0\sim k-1]=s[i-k+2\sim i+1]\)成立
那麼\(s[0\sim k-2]=s[i-k+2\sim i]\)必成立
故 \(\pi[i]\geq k-1\)
觀察性質1-2
這個性質可以讓我們的複雜度
從\(O(N^3)\)降低成\(O(N^2)\)
因為每一項到下一項最多只增加一
整個陣列最多只會增加\(N\)次(也最多減少\(N\)次
故每次從前一項加一往下找
只會匹配不到最多\(N\)次
觀察性質2-1
延續我們剛才的想法
什麼情況下\(\pi[i+1]=\pi[i]+1\)呢
就是\(s[i+1]=s[\pi [i]]\)的時候啊
那如果\(s[i+1]\neq s[\pi [i]]\)的時候怎麼辦
觀察性質2-2
我們希望找到最大的\(j\)使得
\(s[0\sim j-1]=s[i-j+2 \sim i+1]\)
我們只要找所有符合這個條件的\(j\)
再分別判斷\(s[j-1]=s[i+1]\)是否成立即可
這件事情成立的必要條件之一為
\(s[0\sim j-2]=s[i-j+2 \sim i]\)
觀察性質2-3
達成此條件最大的\(j_{max}=\pi [i]+1\)
\(s[0\sim j_{max}-2]=s[i-j_{max}+2 \sim i]\)
那麼剩下的\(j\)應該要符合以下條件
\(s[0\sim j-2]=s[i-j+2 \sim i]=s[j_{max}-j \sim j_{max}-2]\)
也就是下一個\(j\)值正巧就是
\(j=\pi [j_{max}-2]+1=\pi [\pi[i]-1]+1\)
那演算法就出來了欸
-
建陣列\(\pi [i]\),存第\(i\)個前綴的次長共同前後綴
-
初始化\(\pi [0]=0\)
-
若已經做完\(\pi [0]\sim \pi [i]\),令\(j=\pi [i]\)
-
若\(s[i+1]=s[j]\),則\(\pi [i+1]=j+1\)
-
否則更新\(j=\pi [j]\)
-
若\(j=0\),則\(\pi [i+1]=0\),否則重複執行迴圈
複雜度
根據前面性質1知道\(\pi [i]\)變化只有\(O(N)\)次
也就是説性質2中那個\(j\)的更新只需要\(O(N)\)次
又性質二只有每次更新\(j\)的時候才需要檢查字元
且每次只要檢查一個字元
故整體複雜度為\(O(N)\)
來字串匹配
簡單的版本:
要在字串\(s\)裏面找\(t\)
就對 \(t+\)"\(\$\)"\(+s\) 字串做一次failure function
然後觀察s中有哪些地方之\(\pi [i]=t.length()\)
\(O(n+m)\)字串匹配完成
另外一個版本
什麼是failure function,失敗在哪裡?
其實這個失敗函數的原意就是字串匹配失敗時
往前找尋下一個要從哪裡開始匹配的函數
就是原本已經匹配好的部分就不要再重複找了
那麼原本的字串匹配要怎麼做呢?
從s中一個字元一個字元跑
如果某個s中的字元匹配不到t中的下一個字元
就尋找t中最長的已經匹配好重複的區間
也就是次長共同前後綴
講得清楚一點
我們令一個變數 \(i\) 遍歷字串 \(s\)
並再令一個變數 \(j\) 表示 \(s[i]\) 應匹配的 \(t[j]\)
初始化 \(i=j=0\)
接下來只要 \(s[i]=t[j]\) 就 \(i, j\) 各加上 \(1\)
否則 \(j=\pi[j-1]\) 繼續匹配
複雜度證明
\(i, j\) 正好會增加 \(N\) 次
故 \(j\) 也只能減少 \(N\) 次
兩種操作皆只做 \(N\) 次
均攤複雜度 \(O(n)\)
扣的
#include <iostream>
#include <vector>
using namespace std;
#define ll long long
int main(){
vector<ll> ff;
ll n, m, cnt=0;
string a, b;
cin >> a;
n=a.length();
for(int w=0;w<1;w++){
cin >> b;
m=b.length();
ff.clear();
ff.pb(0);
for(int i=1;i<m;i++){
ff.pb(0);
ll j=i-1;
while(j>=0){
if(b[ff[j]]==b[i]){ff[i]=ff[j]+1;break;}
else j=ff[j]-1;
}
}
ll j=0;
for(int i=0;i<n;i++){
while(a[i]!=b[j] && j>0) j=ff[j-1];
if(j==0 && a[i]!=b[j]) continue;
j++;
if(j==m){
cnt++;
j=ff[j-1];
}
}
}
cout << cnt << endl;
return 0;
}
題目
Z-algorithm
Z-algorithm字串匹配演算法的精髓
定義如下:
\(z[k]=\underset{i=0,1,...,n-k+1}{max}i\)
\(i:s[0\sim i-1]=s[k \sim k+i-1]\)
Z-function
WTF
這不是跟剛才那個
幾乎一模一樣嗎
剛才的東西用文字說明就是:
原字串跟從k開始的後綴的「最長共同前綴」
最長共同前綴
aabaaab
babbabba
Z-function
abaabaaab
\(z_{s}:0, 0, 1, 4, 0, 1, 1, 2, 0\)
怎麼找出這個表
暴力做可能需要\(O(N^2)\)
N個後綴,每次往後匹配長度為N
超級慢的啦
讓我們觀察一下怎麼樣讓他變好
通靈一下
我們先預設是跟failure function一樣由左往右找
可以用一個變數 \(r\) 記錄目前匹配到的最大右界
也就是 \(r=\underset{i=0, 1, ..., k-1}{max} (i+z[i]-1)\)
再記錄一下最大值發生時的左界 \(l\)
這可以幹嘛
於是我們會發現
假設我們目前要找 \(z[k]\)
如果\(k\leq r\)
\(z[k]\geq r-k+1 \cdots \cdots(1)\)
\(z[k]=z[k-l] \cdots \cdots(2)\)
\((1), (2)\) 至少有一個成立
也就是 \(z[k]\geq min(r-k+1, z[k-l])\)
白話文
因為\(s[0\sim r-l]=s[l\sim r]\)
當 \(l\leq k\leq r\) 時
如果 \(k-l+z[k-l]>r-l\)
那麼 \(z[k]\) 顯然大於等於 \(r-k+1\)
否則因爲 \(k-l+z[k-l]\) 不超過 \(r-l\)
那麼 \(z[k]\) 應該要等於 \(z[k-l]\)
所以呢
知道這個性質之後就直接暴力匹配就好了
啊為什麼會是好的
右界 \(r\) 最多只會增加 \(N\) 次
\(r\) 不增加的情況只有 \(k+z[k-l]<r\)
又此時我們可以 \(O(1)\) 算出\(z[k]=z[k-l]\)
故均攤 \(O(N)\)
來字串匹配
要做Z-function的字串匹配只有這個版本:
要在字串\(s\)裏面找\(t\)
就對 \(t+\)"\(\$\)"\(+s\) 字串做一次Z-function
然後觀察s中有哪些地方之\(z [i]=t.length()\)
\(O(n+m)\)字串匹配完成
扣的
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
#define ll long long
int main() {
ios_base::sync_with_stdio(0);cin.tie(0);
string s;
cin >> s;
ll n=s.length();
ll z[n], l=0, r=0;
fill(z, z+n, 0);
z[0]=0;
for(int i=1;i<n;i++){
if(i<=r) z[i]=min(r-i+1, z[i-l]);
while(i+z[i]<n && s[z[i]]==s[i+z[i]]) z[i]++;
if(i+z[i]-1>r) l=i, r=i+z[i]-1;
}
for(int i=0;i<n;i++) cout << z[i] << " ";cout << endl;
return 0;
}
題目
Manacher
輸入一個字串,找出其中最長的回文子字串
\(|S|<10^6\)
CSES
Longest Palindrome
就正反Hash一下
枚舉迴文中心
二分搜迴文長度
阿不就做完了
複雜度\(O(nlogn)\)
你可能會想說
但是其實有更好的做法
要分成奇數迴文和偶數迴文好麻煩
那就先在每個字元中間夾一個特殊字元吧
首先考慮
abbabaaaba
*a*b*b*a*b*a*a*a*b*a*
接下來只需做迴文中心在字元上的case就好了
跟Z一樣建個怪陣列
*a*b*b*a*b*a*a*a*b*a*
\(f_{s}:0,1,0,1,2,1,0,1,0,3,0,1,2,7,2,1,0,1,0,1,0\)
\(f[i]=\) 以 \(s_{i}\) 為中心的最長迴文半徑
然後就跟Z很像了
紀錄 \(r\) 為目前找到迴文的最大右界
也就是 \(r=\underset{i=0, 1, ..., k-1}{max} (i+f[i])\)
並額外記錄最大值發生時的 \(mid=i\)
我們發現
假設我們目前要找 \(f[k]\)
如果\(k\leq r\)
\(f[k]\geq r-k \cdots \cdots(1)\)
\(f[k]=f[mid-(k-mid)] \cdots \cdots(2)\)
\((1), (2)\) 至少有一個成立
也就是 \(f[k]\geq min(r-k, f[mid-(k-mid)])\)
同樣的證明方法
右界 \(r\) 最多只會增加 \(N\) 次
\(r\) 不增加的情況只有 \(k+f[mid-(k-mid)]<r\)
又此時我們可以 \(O(1)\) 算出\(f[k]=f[mid-(k-mid)]\)
故均攤 \(O(N)\)
扣的
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
int main() {
ios_base::sync_with_stdio(0);cin.tie(0);
string s, t;
cin >> s;
int n=s.length();
t=".";
for(int i=0;i<n;i++) t+=s[i],t+=".";
n=t.length();
int p[n], mid=-1, r=-1, ans=0;
fill(p, p+n, 0);
for(int i=0;i<n;i++){
if(i<=r) p[i]=min(p[2*mid-i], r-i);
for(int j=p[i]+1;j<=min(i,n-i-1);j++){
if(t[i+j]!=t[i-j]) break;
p[i]++;
}
if(p[i]>p[ans]) ans=i;
if(i+p[i]>r) r=i+p[i], mid=i;
}
for(int i=ans-p[ans]+1;i<=ans+p[ans];i+=2) cout << t[i];cout << endl;
return 0;
}
題目
字串啦
By ck_platypus
字串啦
- 754