亂搞過【模板】Runs 的嘗試
前言
眾所周知,字串有極多的 Theory,根本學不完(就算學完了過兩年 WC 上又會出來一個)。所以乾脆不學,正所謂高階的食材往往只需要簡單的烹飪。
P6656 【模板】Runs
Lyndon Theory 實在太噁心,所以嘗試不學 Runs 做這題。
考慮模仿這題的做法,對於一組 Run \((i,j,p)\),我們先列舉 \(p\),並且每隔 \(p\) 放一個點,用一個字尾資料結構快速查詢 LCP 和 LCS,因為週期為 \(p\) 的 Run 長度至少為 \(2p\),所以一定經過兩個點。
有了上面的東西,再手玩一下,很容易就可以求出一些可能成為 Run 的極長區間 \((i,j,p)\)
std::map
實現。
這樣就得到了一個 \(\mathcal{O}(n\log^2 n)\) 的做法,嘗試優化,首先可以把一對 \((i,j)\) 壓起來用 std::unordered_map
,然後如果字尾資料結構那裡用的是 sam 的話,查詢 LCA 的時候可以使用尤拉序來搞。這裡我使用的是 sam,時間 \(\mathcal{O}(n\log n)\)。
後來才發現 std::unordered_map
也很慢,還不如一開始就存下來,之後再 sort
#include<iostream> #include<stdio.h> #include<ctype.h> #include<vector> #include<string.h> #include<unordered_map> #include<algorithm> #define N 1000005 #define fi first #define se second using namespace std; inline int read(){ int x=0,f=0; char ch=getchar(); while(!isdigit(ch)) f|=(ch==45),ch=getchar(); while(isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=getchar(); return f?-x:x; } struct SuffixAutoMation{ int tail,pool,nxt[2*N][26],fail[2*N],len[2*N],dep[2*N],euler[4*N][20],in[2*N],euler0; vector<int> G[2*N]; inline void init(){tail=pool=1;fail[1]=0;} inline int insert(int c){ len[++pool]=len[tail]+1; int p=tail;tail=pool; for(;p && !nxt[p][c];p=fail[p]) nxt[p][c]=tail; if(!p){fail[tail]=1;return tail;} int q=nxt[p][c]; if(len[p]+1==len[q]){fail[tail]=q;return tail;} len[++pool]=len[p]+1,fail[pool]=fail[q]; memcpy(nxt[pool],nxt[q],sizeof(nxt[q])); fail[tail]=fail[q]=pool; for(;nxt[p][c]==q;p=fail[p]) nxt[p][c]=pool; return tail; } void dfs(int u){ euler[++euler0][0]=u;in[u]=euler0; for(int v:G[u]){ dep[v]=dep[u]+1,dfs(v); euler[++euler0][0]=u; } } inline void build(){ for(int i=2;i<=pool;++i) G[fail[i]].push_back(i); dep[1]=1,dfs(1); for(int j=1;j<=19;++j){ for(int i=1;i+(1<<j)-1<=euler0;++i){ euler[i][j]=dep[euler[i][j-1]]<dep[euler[i+(1<<j-1)][j-1]]?euler[i][j-1]:euler[i+(1<<j-1)][j-1]; } } } inline int LCA(int x,int y){ if(in[x]>in[y]) swap(x,y); int k=__lg(in[y]-in[x]+1); return len[dep[euler[in[x]][k]]<dep[euler[in[y]-(1<<k)+1][k]]?euler[in[x]][k]:euler[in[y]-(1<<k)+1][k]]; } }sam1,sam2; int n,id1[N],id2[N]; char s[N]; inline int LCS(int x,int y){ if(x<1 || y<1) return 0; return sam1.LCA(id1[x],id1[y]); } inline int LCP(int x,int y){ if(x>n || y>n) return 0; return sam2.LCA(id2[x],id2[y]); } unordered_map<long long,int> mp; vector<pair<pair<int,int>,int> > ans; inline void work(int len){ for(int l=1,r;l+len<=n;l=r){ r=l-len; while(r+2*len<=n && LCP(r+len,r+2*len)==len) r+=len; int L,R;r+=2*len; L=LCS(l-1,l+len-1); R=LCP(r,r-len); long long ha=1LL*(l-L)*1000000+1LL*(r+R-1); if(r==l+len){ if(L+R>=len){ if(!mp[ha]){ mp[ha]=1; ans.push_back({{l-L,r+R-1},len}); } } } else{ if(!mp[ha]){ mp[ha]=1; ans.push_back({{l-L,r+R-1},len}); } } } } int main(){ sam1.init(),sam2.init(); scanf("%s",s+1); n=strlen(s+1); for(int i=1;i<=n;++i) id1[i]=sam1.insert(s[i]-'a'); for(int i=n;i>=1;--i) id2[i]=sam2.insert(s[i]-'a'); sam1.build(),sam2.build(); for(int i=1;i<=n;++i) work(i); sort(ans.begin(),ans.end()); printf("%d\n",ans.size()); for(auto v:ans) printf("%d %d %d\n",v.fi.fi,v.fi.se,v.se); return 0; }
這時候我們就就得到了 60 分,這個無*的出題人居然把空間開的只剩 256MB!
不過這可難不倒我們,只需要把 sam 換成 SA 就好了。
#include<iostream>
#include<stdio.h>
#include<ctype.h>
#include<vector>
#include<string.h>
#include<unordered_map>
#include<algorithm>
#define N 1000005
#define fi first
#define se second
using namespace std;
inline int read(){
int x=0,f=0; char ch=getchar();
while(!isdigit(ch)) f|=(ch==45),ch=getchar();
while(isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return f?-x:x;
}
int n;
char s[N];
int rk[N*2],sa[N],ork[N*2],cnt[N],id[N],lcp[N][20],lcs[N][20],height[N],P[N],S[N];
inline void suffixArray(int flag){
int z=300,p=0;
memset(cnt,0,sizeof(cnt));
memset(rk,0,sizeof(rk));
for(int i=1;i<=n;++i) cnt[rk[i]=s[i]]++;
for(int i=1;i<=z;++i) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;--i) sa[cnt[rk[i]]--]=i;
for(int w=1;p^n;w<<=1,z=p){
memset(cnt,0,sizeof(cnt)),p=0;
for(int i=n-w+1;i<=n;++i) id[++p]=i;
for(int i=1;i<=n;++i) if(sa[i]>w) id[++p]=sa[i]-w;
for(int i=1;i<=n;++i) cnt[rk[i]]++;
for(int i=1;i<=z;++i) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;--i) sa[cnt[rk[id[i]]]--]=id[i];
memcpy(ork,rk,sizeof(ork)),p=0;
for(int i=1;i<=n;++i)
rk[sa[i]]=(ork[sa[i]]==ork[sa[i-1]]&&ork[sa[i]+w]==ork[sa[i-1]+w]?p:++p);
}
if(!flag) for(int i=1;i<=n;++i) P[i]=rk[i];
else for(int i=1;i<=n;++i) S[i]=rk[i];
}
inline void getHeight(){
for(int i=1,k=0;i<=n;++i){
if(k) --k;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
height[rk[i]]=k;
}
}
inline int LCP(int x,int y){
if(x>n || y>n) return 0;
x=P[x],y=P[y];
if(x>y) swap(x,y);
x++;
int k=__lg(y-x+1);
return min(lcp[x][k],lcp[y-(1<<k)+1][k]);
}
inline int LCS(int x,int y){
if(x<1 || y<1) return 0;
y=S[n-y+1],x=S[n-x+1];
if(x>y) swap(x,y);
x++;
int k=__lg(y-x+1);
return min(lcs[x][k],lcs[y-(1<<k)+1][k]);
}
unordered_map<long long,int> mp;
vector<pair<pair<int,int>,int> > ans;
inline void work(int len){
for(int l=1,r;l+len<=n;l=r){
r=l-len;
while(r+2*len<=n && LCP(r+len,r+2*len)==len) r+=len;
int L,R;r+=2*len;
L=LCS(l-1,l+len-1);
R=LCP(r,r-len);
long long ha=1LL*(l-L)*10000000+1LL*(r+R-1);
if(r==l+len){
if(L+R>=len){
if(!mp[ha]){
mp[ha]=1;
ans.push_back({{l-L,r+R-1},len});
}
}
}
else{
if(!mp[ha]){
mp[ha]=1;
ans.push_back({{l-L,r+R-1},len});
}
}
}
}
int main(){
scanf("%s",s+1);
n=strlen(s+1);
suffixArray(0),getHeight();
for(int i=1;i<=n;++i) lcp[i][0]=height[i];
reverse(s+1,s+n+1);
suffixArray(1),getHeight();
for(int i=1;i<=n;++i) lcs[i][0]=height[i];
for(int j=1;j<20;++j){
for(int i=1;i+(1<<j)-1<=n;++i){
lcp[i][j]=min(lcp[i][j-1],lcp[i+(1<<j-1)][j-1]);
lcs[i][j]=min(lcs[i][j-1],lcs[i+(1<<j-1)][j-1]);
}
}
for(int i=1;i<=n;++i) work(i);
sort(ans.begin(),ans.end());
printf("%d\n",ans.size());
for(auto v:ans) printf("%d %d %d\n",v.fi.fi,v.fi.se,v.se);
return 0;
}
然後就有了 70 分,TLE 了,這個無*的出題人居然把時間開到線性做法才能過!
然後我就沒辦法了,只能在心中默默問候出題人,嘗試以失敗告終。
不過正常的字串題一般都不會開到 \(10^6\) 的,也就是說上面的做法一般是可以用的,我們不用學 Lyndon Theory 了!!!
下面就是一個例子。
uoj429【集訓隊作業2018】串串劃分
這題前面的轉化相當的巧妙。
看到集訓隊作業先轉化模型:兩個限制看起來是有關聯的,因為兩個相鄰的串相等的話,如果把他們合到一起就變成了一個迴圈串。
相鄰串不能相等,如果有這個限制的話 DP 必不能優化到 \(\mathcal{O}(n)\),因為要處理這個限制需要記錄上一次選了哪。所以考慮容斥,現在相鄰串可以相等了,但好像還是沒什麼用,需要把它和第二個限制聯絡起來。如果現在又 \(k\) 個串相鄰相等,把它們看作一個整體,因為每個串都不迴圈,所以整個大串就是一個最多有 \(k\) 個迴圈節的串,這對這個方案的貢獻是 \((-1)^{k-1}\),所以我們 DP 的時候可以把這 \(k\) 個串一起考慮,也就是說我們加入一個串時,它的貢獻是負一的它的最大迴圈節數減一次方。
但這個還是不好做,於是繼續轉化,發現我們只關心 \(k\) 的奇偶,\(k\) 是偶數的時候才會有 \(-1\)。而當 \(k\) 是偶數,這個串 \(S\) 肯定能表示為 \(AA\) 的形式,這個非常好 check。所以我們就有了一個 \(\mathcal{O}(n^2)\) 的做法。
繼續優化,發現一個結論,我們稱能表示為 \(AA\) 的串為平方串,\(S\) 為平方串,則 \(S^i\) 也是平方串,這個非常顯然。我們稱不能表示為 \(BB\) 的平方串 \(S\) 為基本平方串,其中 \(B\) 為平方串,說人話就是不能拆成兩個相同的平方串的平方串。還有一個結論,在一個串 \(S\) 中,\(A,B,C\) 是 \(S\) 的子串,左端點相同且都是基本平方串,設 \(|A|<|B|<|C|\),則有 \(|C|\ge|A|+|B|\),這說明以一個位置為左端點的基本平方串數量是 \(\log\) 的,那整個串就是 \(n\log n\) 的。這個證明過(wo)於(bu)繁(hui)雜(zheng),所以這裡不寫了。
所以所有的基本平方串理論上是可以全部找出的,因為其他的平方串都可以用基本平方串自己接自己得到,所以 DP 就很容易優化成一個 \(\log\),至於怎麼優化,亂搞即可。
現在問題來到怎麼求所有的基本平方串,發現如果我們知道所有的 Run \((i,j,p)\),基本平方串就是 \(S[k,k+2p-1],k\in[i,j-2p+1]\)。所以套上一個求 Runs 就行啦!這裡 \(n\) 是 \(2\cdot 10^5\),上面的亂搞做法是可以過的。
然後我就喜提 uoj 最快解倒數第一了!
#include<iostream>
#include<stdio.h>
#include<ctype.h>
#include<vector>
#include<string.h>
#include<unordered_map>
#include<algorithm>
#include<set>
#define N 200005
#define fi first
#define se second
using namespace std;
inline int read(){
int x=0,f=0; char ch=getchar();
while(!isdigit(ch)) f|=(ch==45),ch=getchar();
while(isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return f?-x:x;
}
struct SuffixAutoMation{
int tail,pool,nxt[2*N][26],fail[2*N],len[2*N],dep[2*N],euler[4*N][20],in[2*N],euler0;
vector<int> G[2*N];
inline void init(){tail=pool=1;fail[1]=0;}
inline int insert(int c){
len[++pool]=len[tail]+1;
int p=tail;tail=pool;
for(;p && !nxt[p][c];p=fail[p]) nxt[p][c]=tail;
if(!p){fail[tail]=1;return tail;}
int q=nxt[p][c];
if(len[p]+1==len[q]){fail[tail]=q;return tail;}
len[++pool]=len[p]+1,fail[pool]=fail[q];
memcpy(nxt[pool],nxt[q],sizeof(nxt[q]));
fail[tail]=fail[q]=pool;
for(;nxt[p][c]==q;p=fail[p]) nxt[p][c]=pool;
return tail;
}
void dfs(int u){
euler[++euler0][0]=u;in[u]=euler0;
for(int v:G[u]){
dep[v]=dep[u]+1,dfs(v);
euler[++euler0][0]=u;
}
}
inline void build(){
for(int i=2;i<=pool;++i) G[fail[i]].push_back(i);
dep[1]=1,dfs(1);
for(int j=1;j<=19;++j){
for(int i=1;i+(1<<j)-1<=euler0;++i){
euler[i][j]=dep[euler[i][j-1]]<dep[euler[i+(1<<j-1)][j-1]]?euler[i][j-1]:euler[i+(1<<j-1)][j-1];
}
}
}
inline int LCA(int x,int y){
if(in[x]>in[y]) swap(x,y);
int k=__lg(in[y]-in[x]+1);
return len[dep[euler[in[x]][k]]<dep[euler[in[y]-(1<<k)+1][k]]?euler[in[x]][k]:euler[in[y]-(1<<k)+1][k]];
}
}sam1,sam2;
int n,id1[N],id2[N];
char s[N];
inline int LCS(int x,int y){
if(x<1 || y<1) return 0;
if(x>n || y>n) return 0;
return sam1.LCA(id1[x],id1[y]);
}
inline int LCP(int x,int y){
if(x<1 || y<1) return 0;
if(x>n || y>n) return 0;
return sam2.LCA(id2[x],id2[y]);
}
unordered_map<long long,int> mp;
vector<pair<pair<int,int>,int> > run;
inline void work(int len){
for(int l=1,r;l+len<=n;l=r){
r=l-len;
while(r+2*len<=n && LCP(r+len,r+2*len)==len) r+=len;
int L,R;r+=2*len;
L=LCS(l-1,l+len-1);
R=LCP(r,r-len);
long long ha=1LL*(l-L)*1000000+1LL*(r+R-1);
if(r==l+len){
if(L+R>=len){
if(!mp[ha]){
mp[ha]=1;
run.push_back({{l-L,r+R-1},len});
}
}
}
else{
if(!mp[ha]){
mp[ha]=1;
run.push_back({{l-L,r+R-1},len});
}
}
}
}
inline bool cmp(pair<pair<int,int>,int> x,pair<pair<int,int>,int> y){
return x.se>y.se;
}
#define mo 998244353
vector<int> sq[N];
vector<pair<int,int> > add[N];
int f[N],sum[N];
inline void red(int &x){x>=mo?x-=mo:0;}
int main(){
sam1.init(),sam2.init();
scanf("%s",s+1);
n=strlen(s+1);
for(int i=1;i<=n;++i) id1[i]=sam1.insert(s[i]-'a');
for(int i=n;i>=1;--i) id2[i]=sam2.insert(s[i]-'a');
sam1.build(),sam2.build();
for(int i=1;i<=n;++i) work(i);
sort(run.begin(),run.end(),cmp);
for(auto v:run){
for(int i=v.fi.fi+2*v.se-1;i<=v.fi.se;++i){
sq[i].push_back(i-2*v.se+1);
}
}
f[0]=sum[0]=1;
for(int i=1;i<=n;++i){
f[i]=sum[i-1];
int p=0;
for(int j:sq[i]){
int tmp=f[j-1];
while(p<add[i].size() && add[i][p].fi==j) red(tmp+=add[i][p].se),p++;
int k=i+(i-j+1);
if(LCP(j,i+1)>=i-j+1) add[k].push_back({i+1,tmp});
red(f[i]+=mo-2*tmp%mo);
}
red(sum[i]=sum[i-1]+f[i]);
}
cout<<f[n];
return 0;
}