kmp
kmp 是模式串匹配的演算法,本來最壞時間複雜度可以達到 $\operatorname{O}(n\times m)$,但是 kmp 可以將複雜度最佳化到 $\operatorname{O}(n+m)$。
kmp 有個很重要的東西,叫做 $nxt$ 失配陣列。比如對於一個字串 $s$,它的失配陣列 $nxt_n$ 就是 $s$ 的最大字首等於字尾的長度。$\operatorname{O}(n\times m)$ 的演算法的劣勢在於每一次失配都要重頭,但是 kmp 是從上一次最大失配陣列 $nxt_{nxt_n}$ 開始匹配的,這樣會最佳化很多無意義匹配的時間。
設定一個 $pos$ 為匹配開始的位置,然後透過 $nxt$ 陣列來跳,可以達到 $\operatorname{O}(n+m)$ 的時間複雜度。
int len1=strlen(s1+1);
int len2=strlen(s2+1);
int pos=0;
for(int i=2;i<=len2;++i){
while(pos&&s2[i]!=s2[pos+1]){
pos=nxt[pos];//跳 nxt
}
if(s2[i]==s2[pos+1]){
++pos;//匹配成功
}
nxt[i]=pos;//存入 nxt
}
pos=0;
for(int i=1;i<=len1;++i){
while(pos&&s1[i]!=s2[pos+1]){
pos=nxt[pos];//跳 nxt
}
if(s1[i]==s2[pos+1]){
++pos;//匹配成功
}
if(pos==len2){
printf("%d %d\n",i,i+len2-1);//一個位置
}
}
例題
首先求出 nxt 陣列,然後在 nxt 陣列上 dp,設 $dp_i$ 表示字首 $i$ 的答案,只可能從 $dp_{nxt_i}$ 和 $dp_i$ 轉移過來,具體開桶實現。
#include<bits/stdc++.h>
#define MAXN 500005
using namespace std;
char s[MAXN];
int nxt[MAXN],dp[MAXN],mp[MAXN];
int main(){
scanf("%s",s+1);
int len=strlen(s+1);
int pos=0;
for(int i=2;i<=len;++i){
while(pos&&s[i]!=s[pos+1]){
pos=nxt[pos];
}
if(s[i]==s[pos+1]){
++pos;
}
nxt[i]=pos;
}
for(int i=1;i<=len;++i){
dp[i]=i;
if(mp[dp[nxt[i]]]>=i-nxt[i]){
dp[i]=dp[nxt[i]];
}
mp[dp[i]]=i;
}
printf("%d",dp[len]);
return 0;
}
字串雜湊
字串雜湊的思想是,透過把字串看做 $k$ 進位制數來進行儲存和比較。
- 優點:相較於直接字元比較,更加迅速,而且能夠 $\operatorname{O}(1)$ 查詢某段子區間的雜湊值。
- 缺點:容易衝突。
為了應對沖突,我們需要對雜湊進製做一些最佳化,模數也需要非常極品。下面,給出預處理模版程式碼。
typedef unsigned long long ull;
ull base[MAXN],pre[MAXN],suf[MAXN];//進位制、字首雜湊、字尾雜湊
int len;
char s[MAXN];
inline ull get_pre(int l,int r){//O(1) 查詢子區間雜湊值
return ((pre[r]-pre[l-1]*base[r-l+1])%MOD+MOD)%MOD;
}
inline ull get_suf(int l,int r){//O(1) 查詢子區間雜湊值
return ((suf[l]-suf[r+1]*base[r-l+1])%MOD+MOD)%MOD;
}
inline void prework(){
base[0]=1;
for(int i=1;i<MAXN;++i){
base[i]=(base[i-1]*HASH)%MOD;
}
for(int i=1;i<=len;++i){
pre[i]=(pre[i-1]*HASH+s[i]+DIF)%MOD;//加上偏移量防卡
}
for(int i=len;i>=1;--i){
suf[i]=(suf[i+1]*HASH+s[i]+DIF)%MOD;//加上偏移量防卡
}
}
有時候,可以採取雙模雜湊來進行防卡,這樣被卡的機率很小。
typedef unsigned long long ull;
ull base1[MAXN],pre1[MAXN],suf1[MAXN];
ull base2[MAXN],pre2[MAXN],suf2[MAXN];//進位制、字首雜湊、字尾雜湊
int len;
char s[MAXN];
inline ull get_pre1(int l,int r){
return ((pre1[r]-pre1[l-1]*base1[r-l+1])%MOD1+MOD1)%MOD1;
}
inline ull get_suf1(int l,int r){
return ((suf1[l]-suf1[r+1]*base1[r-l+1])%MOD1+MOD1)%MOD1;
}
inline ull get_pre2(int l,int r){//O(1) 查詢子區間雜湊值
return ((pre2[r]-pre2[l-1]*base2[r-l+1])%MOD2+MOD2)%MOD2;
}
inline ull get_suf2(int l,int r){
return ((suf2[l]-suf2[r+1]*base2[r-l+1])%MOD2+MOD2)%MOD2;
}
inline void prework(){
base1[0]=base2[0]=1;
for(int i=1;i<MAXN;++i){
base1[i]=(base1[i-1]*HASH1)%MOD1;
base2[i]=(base2[i-1]*HASH2)%MOD2;
}
for(int i=1;i<=len;++i){
pre1[i]=(pre1[i-1]*HASH1+s[i]+DIF1)%MOD1;
pre2[i]=(pre2[i-1]*HASH1+s[i]+DIF2)%MOD2;//加上偏移量防卡
}
for(int i=len;i>=1;--i){
suf1[i]=(suf1[i+1]*HASH1+s[i]+DIF1)%MOD1;
suf2[i]=(suf2[i+1]*HASH2+s[i]+DIF2)%MOD2;//加上偏移量防卡
}
}
例題
這一道題目可以先把雜湊值處理出來,然後發現可以列舉中間點,然後向左右二分。由於向左延伸 $n$ 格是迴文串,那麼 $n-1$ 格肯定也是迴文串。所以滿足單調性可以二分。由於本題有 Hack 資料卡自然溢,所以要打雙模。
#include<bits/stdc++.h>
#define MAXN 500005
#define HASH1 31
#define HASH2 29
#define MOD1 193910017
#define MOD2 1000000009
#define ADD 131
using namespace std;
typedef long long ull;
int len;
char s[MAXN];
ull base1[MAXN],pre1[MAXN],suf1[MAXN];
ull base2[MAXN],pre2[MAXN],suf2[MAXN];
inline ull get_pre1(int l,int r){
if(!l){
return 0;
}
return ((pre1[r]-pre1[l-1]*base1[r-l+1])%MOD1+MOD1)%MOD1;
}
inline ull get_suf1(int l,int r){
if(!l){
return 0;
}
return ((suf1[l]-suf1[r+1]*base1[r-l+1])%MOD1+MOD1)%MOD1;
}
inline ull get_pre2(int l,int r){
if(!l){
return 0;
}
return ((pre2[r]-pre2[l-1]*base2[r-l+1])%MOD2+MOD2)%MOD2;
}
inline ull get_suf2(int l,int r){
if(!l){
return 0;
}
return ((suf2[l]-suf2[r+1]*base2[r-l+1])%MOD2+MOD2)%MOD2;
}
int main(){
scanf("%d %s",&len,s+1);
base1[0]=base2[0]=1;
for(int i=1;i<=len;++i){
base1[i]=(base1[i-1]*HASH1)%MOD1;
base2[i]=(base2[i-1]*HASH2)%MOD2;
pre1[i]=(pre1[i-1]*HASH1+(s[i]-'0'+ADD))%MOD1;
pre2[i]=(pre2[i-1]*HASH2+(s[i]-'0'+ADD))%MOD2;
}
for(int i=len;i>=1;--i){
suf1[i]=(suf1[i+1]*HASH1+('1'-s[i]+ADD))%MOD1;
suf2[i]=(suf2[i+1]*HASH2+('1'-s[i]+ADD))%MOD2;
}
ull ans=0;
for(int i=1;i<len;++i){
int l=0,r=min(i,len-i),res=0;
if(s[i]==s[i+1]){
continue;
}
while(l<=r){
int mid=(l+r)>>1;
if(get_pre1(i-mid,i)==get_suf1(i+1,i+1+mid)&&get_pre2(i-mid,i)==get_suf2(i+1,i+1+mid)){
l=mid+1;
res=l;
}else{
r=mid-1;
}
}
ans+=res;
}
printf("%llu",ans);
return 0;
}
字典樹
字典樹是一種 $k$ 叉樹結構。原理是每一個節點都有一個布林值 $flag$,判斷是否是一個字串的結尾。每一條邊都有一個字元,表示前面的字元拼接起來就是字串。這種資料結構能夠 $\operatorname{O}(n)$ 查詢字首。
int cnt,trie[MAXN][MAXT],flag[MAXN];
inline int turn(char c);//字元對映函式
inline void insert(string s){
int root=0;
for(int i=0;i<s.size();++i){
int p=turn(s[i]);
if(trie[root][p]){//有沒有節點建立過
root=trie[root][p];//有就跳
}else{
root=trie[root][p]=++cnt;//沒有就建邊
}
}
flag[root]=true;//標記末尾
}
inline bool find(string s){
int root=0;
for(int i=0;i<s.size();++i){
int p=turn(s[i]);
if(!trie[root][p]){//如果沒有,直接返回
return false;
}
root=trie[root][p];//跳
}
return flag[root];//有沒有這個末尾
}
例題
這一道題目考慮貪心證明。如果在某一層,$u$ 需要比 $v$ 先,那麼就建一條邊。如果在下一層,出現了需要 $v$ 比 $u$ 先的情況,那就衝突了,不可能是最優。也就是判環或者用種類並查集。用 Topu 或者 Tarjan 都可以判環。
#include<bits/stdc++.h>
#define MAXN 30003
#define MAXM 26
#define MAXK MAXN*10
using namespace std;
struct node{
int nxt[MAXM];
bool end;
}tree[MAXK];
vector<string> ans;
int n,cnt,indeg[MAXM];
bool vis[MAXM][MAXM];
string s[MAXN];
inline void insert(string str){
int root=0;
for(int i=0;i<str.size();++i){
int dot=str[i]-'a';
if(!tree[root].nxt[dot]){
tree[root].nxt[dot]=++cnt;
}
root=tree[root].nxt[dot];
}
tree[root].end=true;
}
inline bool addedge(string str){
int root=0;
for(int i=0;i<str.size();++i){
int dot=str[i]-'a';
if(tree[root].end){
return false;
}
for(int j=0;j<MAXM;++j){
if(tree[root].nxt[j]&&dot!=j&&!vis[dot][j]){
++indeg[j];
vis[dot][j]=true;
}
}
root=tree[root].nxt[dot];
}
return true;
}
inline bool topusort(){
queue<int> q;
for(int i=0;i<MAXM;++i){
if(!indeg[i]){
q.push(i);
}
}
while(!q.empty()){
int front=q.front();
q.pop();
for(int i=0;i<MAXM;++i){
if(vis[front][i]){
--indeg[i];
if(!indeg[i]){
q.push(i);
}
}
}
}
for(int i=0;i<MAXM;++i){
if(indeg[i]){
return false;
}
}
return true;
}
int main(){
ios::sync_with_stdio(false);
int n;
cin>>n;
for(int i=1;i<=n;++i){
cin>>s[i];
insert(s[i]);
}
for(int i=1;i<=n;++i){
memset(indeg,0,sizeof(indeg));
memset(vis,0,sizeof(vis));
if(addedge(s[i])&&topusort()){
ans.push_back(s[i]);
}
}
cout<<ans.size();
for(int i=0;i<ans.size();++i){
cout<<endl<<ans[i];
}
return 0;
}