【筆記】字串
來自\(\texttt{SharpnessV}\)的省選複習計劃中的字串。
字串雜湊,一般 \(\rm H(S)=\sum\limits_{i=1}^{Len}bas^{Len-i}\times S_i \bmod P\)。這樣我們對一個字串預處理出它的字首雜湊值和 \(\rm bas\) 的次冪,可以做到 \(\rm O(1)\) 求子串的雜湊值。
眾所周知雜湊可以針對模數 \(P\) 進行 \(\rm Hack\),所以我們一般採用雙雜湊,即兩個不同的模數。
常用雜湊模數:\(998244353\ ,\ 1000000007\ ,\ 1000000009\ ,\ 998244853\ ,\ 1004535809\ ,\ 469762049\)
常用\(\rm bas\):\(131\ ,\ 127\ ,\ 137\ ,\ 151\)。
#include<bits/stdc++.h> #define rep(i,a,b) for(int i=a;i<=b;i++) #define pre(i,a,b) for(int i=a;i>=b;i--) #define P 998244853 #define Q 998244353 #define N 10005 #define bas 131 using namespace std; int n;pair<int,int>a[N];char s[N]; int main(){ scanf("%d",&n); rep(i,1,n){ scanf("%s",s+1); int X=0,Y=0,len=strlen(s+1); rep(i,1,len)X=(1LL*X*bas+s[i])%P,Y=(1LL*Y*bas+s[i])%Q; a[i]=make_pair(X,Y); } sort(a+1,a+n+1); int ans=0;rep(i,1,n)ans+=a[i]!=a[i-1]; printf("%d\n",ans); return 0; }
這裡我們運用字串雜湊做到 \(\rm O(1)\) 判斷兩個子串是否相等,然後用樹狀陣列維護字首和算貢獻。
本題出題人沒有刻意卡,自然溢位也能通過。時間複雜度\(\rm O(N\ln N)\)。
#include<bits/stdc++.h> #define rep(i,a,b) for(register int i=a;i<=b;i++) #define pre(i,a,b) for(register int i=a;i>=b;i--) #define N 1050005 #define bas 131 using namespace std; int c[27],n,d[27],nxt[N];char s[N];unsigned pw[N],h[N]; inline void add(int x){x++;for(;x<=27;x+=x&-x)c[x]++;} inline int ask(int x){x++;int sum=0;for(;x;x-=x&-x)sum+=c[x];return sum;} inline void solve(){ scanf("%s",s+1);n=strlen(s+1); rep(i,1,n)h[i]=h[i-1]*bas+s[i]; memset(c,0,sizeof(c));int tot=0;long long ans=0; memset(d,0,sizeof(d)); pre(i,n,1){ if(d[s[i]-'a']^=1)tot++;else tot--; nxt[i]=tot; } memset(d,0,sizeof(d)); d[s[1]-'a']^=(tot=1);add(1); rep(i,2,n-1){ unsigned cur = h[i]; int w[2]; w[1]=ask(nxt[i+1]); if(i+i<n)w[0]=ask(nxt[i+i+1]); rep(j,1,n){ if(i*j>=n)break; if(h[i*j]-pw[i]*h[i*j-i]!=cur)break; ans+=w[j&1]; } if(d[s[i]-'a']^=1)tot++;else tot--; add(tot); } printf("%lld\n",ans); } int main(){ pw[0]=1;rep(i,1,N-5)pw[i]=bas*pw[i-1]; int T;scanf("%d",&T); while(T--)solve(); return 0; }
這裡的字串只要相同字元出現次數相等即為相等。
我們重新設計雜湊函式\(\rm H(S)=\sum\limits_{i=1}^{Len}bas^{S_i}\)。
這裡的\(\rm bas\)一定要大於字符集,本題取\(1000037\)。然後用雙端佇列維護一下即可。
\(\rm KMP\)演算法的核心是失配陣列 \(\rm nxt\) 。\(\rm nxt_i\)表示整個串的字首與以 \(i\) 結尾的字尾的最長的真匹配長度。
#include<bits/stdc++.h>
using namespace std;
char a[1000005],b[1000005];
int n,m,nxt[1000005];
int main()
{
scanf("%s%s",a+1,b+1);
n=strlen(a+1);m=strlen(b+1);
nxt[1]=0;
for(int i=2,j=0;i<=m;i++){
while(j&&b[j+1]!=b[i])j=nxt[j];
if(b[j+1]==b[i])j++;
nxt[i]=j;
}
for(int i=1,j=0;i<=n;i++){
while(j&&a[i]!=b[j+1])j=nxt[j];
if(a[i]==b[j+1])j++;
if(j==m)printf("%d\n",i-m+1);
}
for(int i=1;i<=m;i++)printf("%d ",nxt[i]);
putchar('\n');return 0;
}
先求出 \(\rm nxt\) 陣列,然後再模擬一遍 \(\rm KMP\) 過程,不過現在\(2\times j> i\)就是失配狀態,需要跳指標。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 1000005
#define P 1000000007
using namespace std;
char s[N];
int num[N],nxt[N],n;
void work(){
scanf("%s",s+1);
n=strlen(s+1);
num[1]=1;nxt[1]=0;
int j=0;
rep(i,2,n){
while(j&&s[j+1]!=s[i])j=nxt[j];
if(s[j+1]==s[i])j++;
nxt[i]=j;num[i]=num[j]+1;
}
j=0;long long ans=1;
rep(i,2,n){
while(j&&s[j+1]!=s[i])j=nxt[j];
if(s[i]==s[j+1])j++;
while(j*2>i)j=nxt[j];
ans=(ans*(long long)(num[j]+1))%P;
}
printf("%lld\n",ans);
}
int main(){
int T;scanf("%d",&T);
while(T--)work();
return 0;
}
\(\rm KMP\) 上跑 \(\rm DP\)。
定義 \(f[i]\) 表示覆蓋字首 \(i\) 的最小長度。
如果\(f[i]\le \rm 2\times nxt[i]\),則\(f[i]=f[\rm nxt[i]]\)。因為我們先覆蓋前\(\rm nxt[i]\)個,然後覆蓋後 \(\rm nxt[i]\)個。
如果存在 \(f[j]=f[\rm nxt[i]]\) 並且 \(j+\rm nxt[i]\ge i\),則\(f[i]=f[nxt[i]]\)。因為我們先覆蓋前\(j\)個,然後覆蓋後 \(\rm nxt[i]\)個。
否則顯然 \(f[i]=i\)。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 500005
using namespace std;
char s[N];int n,nxt[N],f[N],pre[N];
int main(){
scanf("%s",s+1);
n=strlen(s+1);
int j=0;nxt[1]=0;
rep(i,2,n){
while(j&&s[j+1]!=s[i])j=nxt[j];
if(s[j+1]==s[i])j++;
nxt[i]=j;
}
rep(i,1,n){
f[i]=i;
if(nxt[i]&&pre[f[nxt[i]]]>=(i-nxt[i]))f[i]=f[nxt[i]];
pre[f[i]]=i;
}
printf("%d\n",f[n]);
return 0;
}
這裡我們定義狀態\(f[i][j]\),表示長短串分別匹配了 \(i\) 位和 \(j\) 位的方案數。
觀察到 \(N\) 很大,可以矩陣快速冪。
\(\rm manacher\) 演算法是優化的暴力。
如果我們列舉中點,然後暴力擴充套件長度,時間複雜度是\(\rm O(N^2)\)的。
考慮優化,我們記錄當前擴充套件過的最右邊界,和擴展出這個邊界的中點。不難發現具有對稱性。
這樣我們就將暴力優化至 \(\rm O(N)\)。
一個技巧是在原串的兩個字元之間插入相同的不在原串中的的字元 \(\texttt{0}\),使得所有的迴文串都變成奇數長度的。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 22000005
using namespace std;
char s[N],a[N];
int n,m,r[N];
int main(){
scanf("%s",s+1);
m=strlen(s+1);
a[0]='!';a[++n]='#';
rep(i,1,m)a[++n]=s[i],a[++n]='#';
int mx=0,mid=0;
rep(i,1,n){
if(i>mx){
r[i]=1;
while(a[i+r[i]]==a[i-r[i]])r[i]++;
mx=i+r[i]-1,mid=i;
}
else{
r[i]=min(r[(mid<<1)-i],mx-i+1);
while(a[i+r[i]]==a[i-r[i]])r[i]++;
if(i+r[i]-1>mx)mx=i+r[i]-1,mid=i;
}
}
int ans=0;
rep(i,1,n)ans=max(ans,r[i]-1);
printf("%d\n",ans);
return 0;
}
在\(\rm manacher\)的同時預處理出以 \(i\) 為左/右邊界的最長迴文串。然後列舉斷點得到最長雙迴文串。
P4683 [IOI2008] Type Printer 印表機
觀察一下印表機的基本操作就是在\(\rm Trie\)樹上移動的過程。
我們先建立出\(\rm Trie\)樹,然後從根出發,遍歷所有點後,在深度最深的節點結束。
不難設計\(\rm DP\)方程 \(f[i]\) 表示字首 \(i\) 是否能夠被解讀。
然後我們藉助\(\rm Trie\)樹可以快速判斷可以向後擴充套件多少。
加強後可以用\(\rm AC\)自動機解決。
我們可以在\(\rm Trie\)樹中插入萬用字元,完成字串的模糊匹配。
轉換一下模型:給定 \(N\) 個數,求異或和最大的二元組。
我們可以將每個數拆成二進位制,然後從高位到低位插入\(\rm Trie\)樹。
插入之前,先查詢與當前數的異或最大值。優先考慮高位,所以每一位的選擇是唯一的。
\(01\rm Trie\) 可以支援 \(+1\) ,查詢所有數異或和,查詢與 \(a\) 異或和 \(\le b\) 的數的個數。
\(\rm AC\)自動機是 \(\rm KMP\) 思想在 \(\rm Trie\) 上的延續。
我們可以從\(1\sim i-1\) 的 \(\rm nxt\) 值推出 \(\rm nxt_{i}\)的值,同理我們可以藉助\(\rm BFS\),從深度\(<\rm dep_i\)的 \(\rm fail\) 值推出 \(\rm fail_i\) 。
\(\rm AC\)自動機中有用的東西有兩樣,一個是轉移邊\(\rm nxt\),一個是失配指標\(\rm fail\)。
\(\rm nxt\) 構成一張圖,而 \(\rm fail\) 構成一棵樹。圖我們可以用來跑\(\rm DP\),樹我們可以用來統計貢獻。
#include<bits/stdc++.h>
using namespace std;
int n,ch[110005][27],tot;
int fail[110005],ed[110005],v[1600];
char s[1600][1000],t[1000005];
queue<int>q;
void bfs(){
memset(fail,0,sizeof(fail));
for(int i=0;i<26;i++)
if(ch[0][i])q.push(ch[0][i]),fail[ch[0][i]]=0;
while(!q.empty()){
int x=q.front();q.pop();
for(int i=0;i<26;i++)
if(ch[x][i])fail[ch[x][i]]=ch[fail[x]][i],q.push(ch[x][i]);
else ch[x][i]=ch[fail[x]][i];
}
}
int main()
{
//freopen("testdata (9).in","r",stdin);
scanf("%d",&n);
while(n){
memset(ch,0,sizeof(ch));
memset(ed,0,sizeof(ed));
tot=0;
for(int i=1;i<=n;i++){
scanf("%s",s[i]+1);
int l=strlen(s[i]+1),now=0;
for(int j=1;j<=l;j++){
if(!ch[now][s[i][j]-'a'])ch[now][s[i][j]-'a']=++tot;
now=ch[now][s[i][j]-'a'];
}
ed[now]=i;
}
scanf("%s",t+1);
bfs();int l=strlen(t+1),now=0,ans=0;
memset(v,0,sizeof(v));
for(int i=1;i<=l;i++){
now=ch[now][t[i]-'a'];
int x=now;
while(x){
v[ed[x]]++;
x=fail[x];
}
}
int mx=0;
for(int i=1;i<=n;i++)mx=max(mx,v[i]);
printf("%d\n",mx);
for(int i=1;i<=n;i++)if(mx==v[i])puts(s[i]+1);
scanf("%d",&n);
}
return 0;
}
對於每個\(S\)的每個字首,在自動機對應的節點上打上標記,表示對沿著\(fail\)樹一直到根的路徑產生貢獻。
最後\(\rm DFS\)一下\(\rm fail\)樹統計貢獻即可。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 400005
#define M 2000006
using namespace std;
int n;char s[M];
int ch[N][27],rt,tot,fail[N];
queue<int>q;
int h[N],tt,c[N],ed[N];
struct edge{
int to,nxt;
}e[N<<1];
void add(int x,int y){
e[++tt].nxt=h[x];h[x]=tt;e[tt].to=y;
}
void bfs(){
rep(i,0,25)if(ch[rt][i])fail[ch[rt][i]]=0,q.push(ch[rt][i]);
while(!q.empty()){
int x=q.front();q.pop();
add(fail[x],x);
rep(i,0,25)if(ch[x][i])
fail[ch[x][i]]=ch[fail[x]][i],q.push(ch[x][i]);
else ch[x][i]=ch[fail[x]][i];
}
}
void dfs(int x,int fa){
for(int i=h[x];i;i=e[i].nxt)if(e[i].to!=fa)
dfs(e[i].to,x),c[x]+=c[e[i].to];
}
int main(){
scanf("%d",&n);
rep(i,1,n){
scanf("%s",s+1);
int len=strlen(s+1);
int now=rt;
rep(j,1,len){
if(!ch[now][s[j]-'a'])ch[now][s[j]-'a']=++tot;
now=ch[now][s[j]-'a'];
}
ed[i]=now;
}
bfs();
scanf("%s",s+1);
int len=strlen(s+1);
int now=rt;
rep(i,1,len)now=ch[now][s[i]-'a'],c[now]++;
dfs(0,0);
rep(i,1,n)printf("%d\n",c[ed[i]]);
return 0;
}
題目已經將\(\rm Trie\)樹給出了,直接建立自動機即可。
對於一個串,它每個字首對應的節點,都會在到根的\(fail\)樹上產生貢獻。
我們先建出\(fail\)樹,對詢問離線,然後支援單點加和區間查詢即可。
先建立自動機。
不難發現每在結尾新增一個字母,等價於在圖上走一步。
如果圖上有環,說明存在無限長的滿足條件的串。
自動機上\(\rm DP\),\(f[i][j]\) 表示長度為 \(i\) 的串,匹配到自動機上的節點 \(j\) 的方案數。
同樣是自動機上跑 \(\rm DP\)。
一般採用\(\rm O(N\log N)\)的倍增方法。
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 1000005
using namespace std;
char s[N];int x[N],y[N],sa[N],c[N],n,m='z';
int main(){
scanf("%s",s+1);n=strlen(s+1);
rep(i,1,n)c[x[i]=s[i]]++;
rep(i,1,m)c[i]+=c[i-1];
rep(i,1,n)sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1){
int num = 0;
rep(i,n-k+1,n)y[++num]=i;
rep(i,1,n)if(sa[i]>k)y[++num]=sa[i]-k;
rep(i,1,m)c[i]=0;
rep(i,1,n)c[x[i]]++;
rep(i,1,m)c[i]+=c[i-1];
pre(i,n,1)sa[c[x[y[i]]]--]=y[i];
rep(i,1,n)swap(x[i],y[i]);
x[sa[1]]=num=1;
rep(i,2,n)x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?num:++num);
m=num;if(n==m)break;
}
rep(i,1,n)printf("%d ",sa[i]);
return 0;
}
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 2000005
using namespace std;
struct SAM{
int nxt[N][26],fa[N],len[N],pre,idx,sz[N];
SAM(){fa[0]=~0;}
void extend(int ch){
int cur=++idx,p=pre;len[cur]=len[p]+1;
while(~p&&!nxt[p][ch])nxt[p][ch]=cur,p=fa[p];
if(-1==p)fa[cur]=0;
else{
int q=nxt[p][ch];
if(len[p]+1==len[q])fa[cur]=q;
else{
int now=++idx;len[now]=len[p]+1;
rep(i,0,25)nxt[now][i]=nxt[q][i];
fa[now]=fa[q];fa[q]=fa[cur]=now;
while(~p&&nxt[p][ch]==q)nxt[p][ch]=now,p=fa[p];
}
}sz[pre=cur]++;
}
int b[N],c[N];
void solve(){
long long ans = 0;
rep(i,1,idx)c[len[i]]++;
rep(i,1,idx)c[i]+=c[i-1];
rep(i,1,idx)b[c[len[i]]--]=i;
pre(i,idx,1){
sz[fa[b[i]]]+=sz[b[i]];
if(sz[b[i]]>1)ans=max(ans,1LL*sz[b[i]]*len[b[i]]);
}
printf("%lld\n",ans);
}
}w;
int main(){
register char ch = getchar();
while(ch != '\n')w.extend(ch - 'a') , ch = getchar();
w.solve();
return 0;
}
正解迴文自動機。
\(\rm manacher+SAM\) 也可以。
由於\(\rm manacher\) 的對稱性,只有最右指標向右擴充套件的時候,才會產生新的有貢獻的子串。
我們再在\(\rm SAM\)中計算這個子串的存在值並更新答案。
然後是喜樂見聞的卡空間情節。
最小表示法,將串複製一倍然後字尾排序。
列舉起點,然後匹配。考慮到最多隻有三次失配,所以直接暴力求\(\rm LCP\)即可。
一個串的不同子串個數是對應字尾自動機的 \(\sum Len_i\) 。
加入一個節點的時候,發現除了新加的節點,其他節點的\(\sum Len_i\) 不變,所以我們只用在答案中加入新加節點的 \(Len_i -Len_{fa_i}\)。
還有一種動態維護\(\rm SA\)的做法,思維難度高一點,這裡不再贅述。
不同於\(\rm AC\)自動機,\(\rm SAM\)的轉移邊構成一個有向無環圖,一條從源點出發的路徑對應一個子串。
我們可以一遍拓撲求出從當前點開始的路徑條數,然後在圖上求出第\(k\)小的路徑。
注意不同位置的相同子串不算一次,我可以利用\(\rm endpos\)的\(\rm size\)計算。
求所有後綴之間的兩兩\(\rm LCP\)之和。
不難發現這就是字尾樹上兩兩\(\rm LCA\)的深度之和,反串跑\(\rm SAM\)即可。
也可以用\(\rm SA\)解決,我們先求出字尾陣列和 \(\rm Height\) 陣列,然後利用單調棧維護集合。
用一個分隔符,然後將兩個串連起來求\(\rm SA\)。
然後用單調棧分別計算 \(A\) 對 \(B\) 的貢獻和 \(B\) 對 \(A\) 的貢獻。
這道題的關鍵點操作非常值得一做。
我們列舉長度\(Len\),然後每隔\(Len\)設定一個關鍵點,如果存在\(AA\)使得\(A\)長度大於等於\(Len\),一定過兩個關鍵點。
我們只用在關鍵點之間求\(\rm LCP/LCS\)即可,字尾陣列即可。
如果我們將 \(\rm Height\ge r\) 的兩個字尾合併,那麼我們降序列舉 \(r\) ,聯通塊個數將會越來越少。
我們對每個聯通塊維護最大值最小值和大小即可,只需要並查集。
P4081 [USACO17DEC]Standing Out from the Herd P
廣義字尾自動機。
可以在插入一個串之後將 \(\rm pre\) 指標重置為 \(0\),這樣除了會多出一些空節點,對時間複雜度和正確性沒有影響。
不過最好的方式是先建\(\rm Trie\)樹,然後記錄\(\rm pre_i\),表示第\(i\)個字尾對應的節點,\(\rm BFS\)的時候以\(\rm Trie\)樹上父親的\(pre\)為前指標即可。
比較毒瘤的\(\rm SAM\)。
我們先對喵的姓和名一起建立\(\rm Parent\)樹,然一次點名對應一個子樹的數顏色。大力莫隊即可。
第二問一隻喵被點了多少次,我們可以在莫隊的時候將顏色彈出的時候將產生貢獻的區間加一下即可。
資料範圍較小,卡常還好。
非常簡單的一道題。
我們先將\(\rm Parent\)樹建立出來,那麼插入一個串就是路徑加,然後查詢樹上權值\(\ge k\)的點個數。
大力樹剖可以做到\(\rm O(N\log^2N)\)。
如果離線一下,考慮每個點的子樹中,第\(k\)次插入的時間戳,這就是經典問題子樹第\(k\)小,線段樹合併一下即可。
經典模型,維護\(\rm SAM\)的\(\rm endpos\)集合。
眾所周知集合大小之和是\(\rm O(N^2)\)的。
所以我們藉助線段樹合併,可以在\(\rm O(N\log N)\)的時間內求出每個節點的\(\rm endpos\)集合並解決問題。
這類模型比較肝是真的。
重工業。
思維難度不大,首先我們需要建立\(\rm Parent\)樹,然後對於給定串,拆出他對應的點,並建圖。
先跑一遍強連通分量判斷是否有解,然後一遍拓撲求出最長路。
同樣用線段樹合併維護\(\rm endpos\)集合,然後直接集合中查詢即可。
同理維護\(\rm endpos\),較前幾題簡單一些。