AC自动机
CF710F String Set Queries
洛谷题目传送门
询问出现次数是AC自动机的拿手好戏,一个节点的出现次数是这个节点在失陪树上到根节点的路径上的出现次数总和
但是AC自动机这种东西不支持凭空添加一个字符串
如果可以离线就可以用CDQ分治搞定
但是强制在线就有点困难
有一个可以解决强制在线问题的分治算法,就是二进制分组
我们分别维护大小为
1
,
2
,
4
,
8
,
16
…
…
1,2,4,8,16……
1,2,4,8,16……的
A
C
AC
AC自动机
加入一个串的时候,我们就先将它和大小为1的合并,重构AC自动机,发现大小和下一组的大小相等
那么就和下一组合并,已知持续下去知道不能继续合并
答案即为在所有AC自动机的答案之和
分析复杂度
查询复杂度
O
(
q
l
o
g
q
)
O(qlogq)
O(qlogq)
合并次数复杂度
O
(
q
l
o
g
q
)
O(qlogq)
O(qlogq)
重构复杂度,因为每个串只会被重构
l
o
g
q
logq
logq次,复杂度
O
(
∑
∣
s
∣
l
o
g
q
)
O(\sum|s|logq)
O(∑∣s∣logq)
默认
∑
∣
s
∣
\sum|s|
∑∣s∣和
q
q
q同阶,复杂度
O
(
q
l
o
g
q
)
O(qlogq)
O(qlogq)
但是还有删除,观察到匹配次数具有可减性,因此我们考虑再开一个删除的串的二进制分组
我们用答案在所有串的出现次数减去删除中的出现次数
就是答案了
常数有点大,可以用垃圾回收减少空间复杂度
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 6e5+7;
vector<int> str[N];
int len[N];
int n;
char s[N];
struct node
{
int tr[N][28];
int Next[N];
LL cnt[N];
int rot[N];
int top;
int siz[N],seq[30][N];
int bin[N],pos;
bool ins[N];
void Insert(int r,int x)
{
int p=rot[r];
for(int i=1;i<=len[x];i++)
{
int c=str[x][i];
if(tr[p][c]==rot[r])
{
ins[bin[pos]]=0;
tr[p][c]=bin[pos--];
for(int ch=0;ch<26;ch++)
tr[tr[p][c]][ch]=rot[r];
}
p=tr[p][c];
}
cnt[p]++;
}
void init()
{
memset(tr,0,sizeof(tr));
memset(Next,0,sizeof(Next));
memset(cnt,0,sizeof(cnt));
memset(rot,0,sizeof(rot));
memset(seq,0,sizeof(seq));
top=pos=0;
for(int i=(N-10);i>=1;i--)
{
bin[++pos]=i;
ins[i]=1;
}
}
queue<int> q;
void Build(int r)
{
Next[rot[r]]=rot[r];
for(int c=0;c<26;c++)
if(tr[rot[r]][c]!=rot[r])
{
q.push(tr[rot[r]][c]);
Next[tr[rot[r]][c]]=rot[r];
}
while(!q.empty())
{
int x=q.front();
q.pop();
for(int c=0;c<26;c++)
{
int y=tr[x][c];
if(y==rot[r]) tr[x][c]=tr[Next[x]][c];
else
{
Next[y]=tr[Next[x]][c];
cnt[y]+=cnt[Next[y]];
q.push(y);
}
}
}
}
void clear(int r)
{
Pick(rot[r],rot[r]);
for(int c=0;c<26;c++)
tr[rot[r]][c]=rot[r];
}
void Exclear(int r)
{
clear(r);
ins[rot[r]]=1;
bin[++pos]=rot[r];
cnt[rot[r]]=Next[rot[r]]=0;
siz[r]=0;
for(int c=0;c<26;c++)
tr[rot[r]][c]=0;
rot[r]=0;
top--;
}
void Pick(int x,int r)
{
for(int c=0;c<26;c++)
{
int y=tr[x][c];
if(ins[y]||y==r) continue;
cnt[y]=Next[y]=0;
ins[y]=1;
bin[++pos]=y;
Pick(y,r);
tr[x][c]=r;
}
}
void Merge(int A,int B)
{
clear(A);
for(int i=1;i<=siz[B];i++)
seq[A][++siz[A]]=seq[B][i];
for(int i=1;i<=siz[A];i++)
Insert(A,seq[A][i]);
Build(A);
Exclear(B);
}
void Addin(int x)
{
int r=++top;
siz[r]=1;
seq[r][1]=x;
ins[bin[pos]]=0;
rot[r]=bin[pos--];
for(int c=0;c<26;c++)
tr[rot[r]][c]=rot[r];
Insert(r,x);
Build(r);
while(siz[r]==siz[r-1])
{
Merge(r-1,r);
r--;
}
}
LL Ask()
{
int m=strlen(s+1);
LL res=0;
for(int r=1;r<=top;r++)
{
int p=rot[r];
for(int i=1;i<=m;i++)
{
int c=s[i]-'a';
p=tr[p][c];
res+=cnt[p];
}
}
return res;
}
}Add,Del;
int idx=0;
int main()
{
// freopen("b.in","r",stdin);
// freopen("b.out","w",stdout);
cin>>n;
Add.init();
Del.init();
int cnt=0;
for(int p=1;p<=n;p++)
{
int t;
scanf("%d",&t);
if(t==1)
{
scanf("%s",s+1);
++idx;
len[idx]=strlen(s+1);
str[idx].push_back(0);
int m=strlen(s+1);
for(int i=1;i<=m;i++)
str[idx].push_back(s[i]-'a');
Add.Addin(idx);
cnt++;
}
else if(t==2)
{
scanf("%s",s+1);
++idx;
str[idx].push_back(0);
len[idx]=strlen(s+1);
int m=strlen(s+1);
for(int i=1;i<=m;i++)
str[idx].push_back(s[i]-'a');
Del.Addin(idx);
}
else
{
scanf("%s",s+1);
printf("%lld\n",Add.Ask()-Del.Ask());
}
fflush(stdout);
}
return 0;
}
CF587F Duff is Mad
洛谷题目传送门
考虑根号分治
设
m
=
∑
∣
s
∣
m=\sum|s|
m=∑∣s∣
如果
∣
s
k
∣
≤
m
|s_k|\leq\sqrt m
∣sk∣≤m,那么这样的长度很少,离线之后可以暴力做
具体的做法就是把询问离线,把
l
,
r
l,r
l,r换成前缀相减的样子
然后把
k
k
k挂在
s
l
−
1
s_{l-1}
sl−1和
s
r
s_r
sr下面
问题变成求
s
1
−
s
i
s_1-s_i
s1−si在
s
k
s_k
sk中出现的次数
然后建出AC自动机
遍历所有的字符串,在这个字符串对应的节点的子树的所有权值就要加一
然后遍历属于这个字符串的询问,查询这个询问在
A
C
AC
AC自动机上的答案
也就是在AC自动机中跑一遍,当前节点的权值就是出现次数
我们需要一个区间加,单点求值的数据结构
如果用树状数组,会加上一个log,因此,我们用分块
复杂度
O
(
n
m
+
q
m
)
O(n\sqrt m+q\sqrt m)
O(nm+qm)
如果
∣
s
k
∣
>
m
|s_k|>\sqrt m
∣sk∣>m,那么串的个数很少,我们可以对于每个这样的串分别处理
首先把
s
k
s_k
sk在AC自动机上跑一边,把遍历的节点的权值加一
然后类似的转化为前缀和相减的形式
把线性扫过
n
n
n个串,把这个串在AC自动机上跑一遍,那么这个某个节点在
s
k
s_k
sk的出现次数就是他在失配树上子树的权值之和,可以直接在AC自动机上做一遍子树和就行了
复杂度
O
(
m
m
)
O(m\sqrt m)
O(mm)
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+7;
const int S = 400;
typedef long long LL;
int tr[N][27],Next[N];
vector<int> str[N];
int len[N],End[N];
int tot=0;
void Insert(int x)
{
int p=0;
for(int i=1;i<=len[x];i++)
{
int c=str[x][i];
if(!tr[p][c]) tr[p][c]=++tot;
p=tr[p][c];
}
End[x]=p;
}
queue<int> q;
void Build()
{
for(int c=0;c<26;c++)
if(tr[0][c]) q.push(tr[0][c]);
while(!q.empty())
{
int x=q.front();
q.pop();
for(int c=0;c<26;c++)
{
int y=tr[x][c];
if(!y) tr[x][c]=tr[Next[x]][c];
else
{
Next[y]=tr[Next[x]][c];
q.push(y);
}
}
}
}
int n,m;
char s[N];
int B;
struct Query
{
int x,tp,id;
};
vector<Query> Q1[N],Q2[N];
struct edge
{
int y,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y)
{
e[++t].y=y;
e[t].next=link[x];
link[x]=t;
}
int st[N],ed[N],siz[N];
int top=0;
int seq[N];
void dfs(int x)
{
siz[x]=1;
st[x]=++top;
seq[top]=x;
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
dfs(y);
siz[x]+=siz[y];
}
ed[x]=top;
}
int L[N],R[N],cnt=0;
int bel[N];
LL a[N],tag[N];
void Blocks()
{
for(int i=1;i<=tot+1;i++)
{
bel[i]=(i-1)/B+1;
R[bel[i]]=i;
if(!L[bel[i]]) L[bel[i]]=i;
cnt=max(cnt,bel[i]);
}
}
void Add(int l,int r,int v)
{
if(bel[r]-bel[l]<=1)
{
for(int i=l;i<=r;i++)
a[i]+=v;
return;
}
else
{
for(int i=l;i<=R[bel[l]];i++) a[i]+=v;
for(int i=L[bel[r]];i<=r;i++) a[i]+=v;
for(int i=bel[l]+1;i<bel[r];i++) tag[i]+=v;
return;
}
}
LL Ans[N];
bool cmp(Query a,Query b)
{
return a.x<b.x;
}
int main()
{
cin>>n>>m;
for(int x=1;x<=n;x++)
{
scanf("%s",s+1);
str[x].push_back(0);
len[x]=strlen(s+1);
for(int i=1;i<=len[x];i++)
str[x].push_back(s[i]-'a');
Insert(x);
}
Build();
B=sqrt(tot*1.0+1);
for(int i=1;i<=m;i++)
{
int l,r,x;
scanf("%d %d %d",&l,&r,&x);
if(len[x]>B)
{
if(l!=1) Q2[x].push_back((Query){l-1,-1,i});
Q2[x].push_back((Query){r,1,i});
}
else
{
if(l!=1) Q1[l-1].push_back((Query){x,-1,i});
Q1[r].push_back((Query){x,1,i});
}
}
for(int i=1;i<=tot;i++)
add(Next[i],i);
dfs(0);
Blocks();
for(int i=1;i<=n;i++)
{
Add(st[End[i]],ed[End[i]],1);
for(int p=0;p<(int)Q1[i].size();p++)
{
int x=Q1[i][p].x,tp=Q1[i][p].tp,id=Q1[i][p].id;
int r=0;
for(int j=1;j<=len[x];j++)
{
int c=str[x][j];
r=tr[r][c];
Ans[id]+=1ll*(a[st[r]]+tag[bel[st[r]]])*tp;
}
}
}
for(int x=1;x<=n;x++)
{
if(Q2[x].size())
{
memset(a,0,sizeof(a));
int p=0;
for(int i=1;i<=len[x];i++)
{
int c=str[x][i];
p=tr[p][c];
a[p]++;
}
sort(Q2[x].begin(),Q2[x].end(),cmp);
for(int i=top;i>=2;i--)
a[Next[seq[i]]]+=a[seq[i]];
int j=1;
LL sum=0;
for(p=0;p<(int)Q2[x].size();p++)
{
int y=Q2[x][p].x;
while(j<=y) sum+=a[End[j++]];
Ans[Q2[x][p].id]+=1ll*sum*Q2[x][p].tp;
}
}
}
for(int i=1;i<=m;i++)
printf("%lld\n",Ans[i]);
return 0;
}
后缀自动机(SAM)
CF666E Forensic Examination
洛谷题目传送门
考虑把
S
S
S和所有的
T
T
T一起建成广义后缀自动机
然后再每个节点维护一个线段树,表示这个节点代表的字串在每个
T
T
T中出现的次数
可以在
p
a
r
e
n
t
parent
parent树上线段树合并就可以了
查询的时候先树上倍增找到这个区间对应的节点然后在线段树上做就可以了
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define mk(x,y) make_pair(x,y)
#define X(x) x.first
#define Y(x) x.second
const int S = 6e5+7;
const int N = 2*S;
int tr[N][26],len[N],fa[N];
int tot=0;
int n,m;
int sum=0;
void copy(int x,int y)
{
len[x]=len[y];
fa[x]=fa[y];
for(int c=0;c<26;c++)
tr[x][c]=tr[y][c];
}
int nodes[S];
int Extend(int c,int last)
{
int p=last,np=++tot;
if(tot>=2e6)
{
cout<<sum;
exit(0);
}
len[np]=len[p]+1;
while(p&&!tr[p][c])
{
tr[p][c]=np;
p=fa[p];
}
if(!p) fa[np]=1;
else
{
int q=tr[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else
{
int nq=++tot;
copy(nq,q);
len[nq]=len[p]+1;
fa[np]=fa[q]=nq;
while(p&&tr[p][c]==q)
{
tr[p][c]=nq;
p=fa[p];
}
}
}
return np;
}
int trie[S][26];
char s[S];
vector<int> appear[S];
bool ins[S];
int length;
void Insert(int x)
{
int p=0;
ins[p]=1;
length=strlen(s+1);
for(int i=1;i<=length;i++)
{
int c=s[i]-'a';
if(!trie[p][c]) trie[p][c]=++tot;
p=trie[p][c];
if(x!=0) appear[p].push_back(x);
else ins[p]=1;
}
}
int num=0;
int lson[N*20],rson[N*20],rot[N];
int Max[N*20],Pos[N*20];
void pushup(int k)
{
if(Max[lson[k]]>Max[rson[k]])
{
Max[k]=Max[lson[k]];
Pos[k]=Pos[lson[k]];
}
else if(Max[lson[k]]<Max[rson[k]])
{
Max[k]=Max[rson[k]];
Pos[k]=Pos[rson[k]];
}
else
{
Max[k]=Max[lson[k]];
Pos[k]=Pos[lson[k]];
}
}
void Modify(int &k,int l,int r,int pos)
{
if(!k) k=++num;
if(l==r)
{
Max[k]++;
Pos[k]=l;
return;
}
int mid=(l+r)>>1;
if(pos<=mid) Modify(lson[k],l,mid,pos);
else Modify(rson[k],mid+1,r,pos);
pushup(k);
}
int Merge(int x,int y,int l,int r)
{
if(!x||!y) return x+y;
int k=++num;
if(l==r)
{
Max[k]=Max[x]+Max[y];
Pos[k]=l;
return k;
}
int mid=(l+r)>>1;
lson[k]=Merge(lson[x],lson[y],l,mid);
rson[k]=Merge(rson[x],rson[y],mid+1,r);
pushup(k);
return k;
}
void Better(PII &A,PII B)
{
if(Y(A)<Y(B)||(Y(A)==Y(B)&&X(A)>X(B))) A=B;
}
PII Ask(int k,int l,int r,int L,int R)
{
if(!k) return mk(0,0);
if(L<=l&&r<=R) return mk(Pos[k],Max[k]);
int mid=(l+r)>>1;
PII res=mk(0,0);
if(L<=mid) Better(res,Ask(lson[k],l,mid,L,R));
if(R>mid) Better(res,Ask(rson[k],mid+1,r,L,R));
return res;
}
struct edge
{
int y,next;
}e[N];
int link[N],t=0;
int pos[S],pre[S];
void add(int x,int y)
{
e[++t].y=y;
e[t].next=link[x];
link[x]=t;
}
queue<int> q;
int f[N][30];
int tt=0;
void dfs(int x)
{
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
f[y][0]=x;
for(int k=1;k<=tt;k++)
f[y][k]=f[f[y][k-1]][k-1];
dfs(y);
rot[x]=Merge(rot[x],rot[y],1,m);
}
}
void Build()
{
sum=tot;
tot=0;
nodes[0]=++tot;
q.push(0);
while(!q.empty())
{
int x=q.front();
q.pop();
for(int c=0;c<26;c++)
{
if(!trie[x][c]) continue;
int y=trie[x][c];
nodes[y]=Extend(c,nodes[x]);
for(int p=0;p<(int)appear[y].size();p++)
Modify(rot[nodes[y]],1,m,appear[y][p]);
if(ins[x]&&ins[y])
{
pre[y]=pre[x]+1;
pos[pre[y]]=nodes[y];
}
q.push(y);
}
}
for(int i=2;i<=tot;i++)
add(fa[i],i);
tt=log2(tot+1);
dfs(1);
}
inline int read()
{
int X=0;bool flag=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')flag=0;ch=getchar();}
while(ch>='0'&&ch<='9'){X=(X<<1)+(X<<3)+ch-'0';ch=getchar();}
if(flag) return X;
return ~(X-1);
}
int Find(int x,int l)
{
int r=x;
for(int k=tt;k>=0;k--)
{
if(f[x][k]&&len[r]-len[f[x][k]]<=l)
{
x=f[x][k];
}
}
return x;
}
int main()
{
scanf("%s",s+1);
Insert(0);
m=read();
for(int i=1;i<=m;i++)
{
scanf("%s",s+1);
Insert(i);
}
Build();
int q=read();
while(q--)
{
int l=read(),r=read(),ql=read(),qr=read();
ql--;
int x=Find(pos[qr],ql);
PII ans=Ask(rot[x],1,m,l,r);
if(X(ans)==0) X(ans)=l;
printf("%d %d\n",X(ans),Y(ans));
}
return 0;
}
CF235C Cyclical Quest
洛谷题目传送门
如果暴力把循环同构字符串建成广义SAM,显然空间时间都炸裂
因此考虑换一种思路
对原串S建出SAM,然后把询问串倍长,扔进SAM里跑
求出对于某一个前缀,可以和S匹配的最长后缀长度,设为
l
e
n
len
len
那么如果询问串长度为
m
m
m
只有
l
e
n
≥
m
len\geq m
len≥m才是合法的
然后用树上倍增找到对应的区间
然后把答案加上这个区间在
S
S
S中的出现次数(子树和)就行了
因为循环同构字符串可能会相同,产生了重复贡献,所以我们强制一个节点只能产生一次贡献
贡献过了标记就可以了
别忘了询问过后撤销标记
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e6+7;
int tr[N][26],fa[N],len[N];
int last=1,tot=1;
void copy(int x,int y)
{
len[x]=len[y];
fa[x]=fa[y];
for(int c=0;c<26;c++)
tr[x][c]=tr[y][c];
}
LL f[N];
void Extend(int c)
{
int p=last,np=last=++tot;
len[np]=len[p]+1;
f[np]=1;
while(p&&!tr[p][c])
{
tr[p][c]=np;
p=fa[p];
}
if(!p) fa[np]=1;
else
{
int q=tr[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else
{
int nq=++tot;
copy(nq,q);
len[nq]=len[p]+1;
fa[np]=fa[q]=nq;
while(p&&tr[p][c]==q)
{
tr[p][c]=nq;
p=fa[p];
}
}
}
}
struct edge
{
int y,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y)
{
e[++t].y=y;
e[t].next=link[x];
link[x]=t;
}
void dfs(int x)
{
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
dfs(y);
f[x]+=f[y];
}
}
char s[N];
int used[N],cnt=0;
bool tag[N];
int Find(int x,int m)
{
while(fa[x]&&len[fa[x]]>=m)
x=fa[x];
return x;
}
void solve()
{
scanf("%s",s+1);
int m=strlen(s+1);
for(int i=m+1;i<=m+m;i++)
s[i]=s[i-m];
int p=1;
LL ans=0;
int t=0;
for(int i=1;i<=m+m;i++)
{
int c=s[i]-'a';
if(tr[p][c]) p=tr[p][c],t++;
else
{
while(p&&!tr[p][c]) p=fa[p];
if(!p) p=1,t=0;
else
{
t=len[p]+1;
p=tr[p][c];
}
}
if(t<m) continue;
int q=Find(p,m);
if(tag[q]) continue;
ans+=f[q];
tag[q]=1;
used[++cnt]=q;
}
printf("%lld\n",ans);
for(int i=1;i<=cnt;i++)
tag[used[i]]=0;
cnt=0;
}
int main()
{
scanf("%s",s+1);
int m=strlen(s+1);
for(int i=1;i<=m;i++)
Extend(s[i]-'a');
for(int i=2;i<=tot;i++)
add(fa[i],i);
dfs(1);
int n;
scanf("%d",&n);
while(n--)
{
solve();
}
return 0;
}
[HEOI2016/TJOI2016]字符串
洛谷题目传送门
不理解为什么是黑色的
对S建出后缀自动机
询问的时候二分答案,找到
s
[
c
…
…
c
+
m
i
d
−
1
]
s[c……c+mid-1]
s[c……c+mid−1]对应的字串的节点
然后查询这个节点在
s
[
a
…
…
b
]
s[a……b]
s[a……b]中出现了没
可以用线段树合并维护出现的位置的集合
复杂度
O
(
n
log
2
n
)
O(n\log ^2n)
O(nlog2n)
代码也很好写
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+7;
char s[N];
int n;
int tr[N][26],len[N],fa[N];
void copy(int a,int b)
{
len[a]=len[b];
fa[a]=fa[b];
for(int c=0;c<26;c++)
tr[a][c]=tr[b][c];
}
int last=1,tot=1;
int tree[N*20],lson[N*20],rson[N*20];
int cnt=0;
int rot[N];
void pushup(int k)
{
tree[k]=tree[lson[k]]+tree[rson[k]];
}
void Insert(int &k,int l,int r,int x)
{
if(!k) k=++cnt;
if(l==r)
{
tree[k]++;
return;
}
int mid=(l+r)>>1;
if(x<=mid) Insert(lson[k],l,mid,x);
else Insert(rson[k],mid+1,r,x);
pushup(k);
}
int Merge(int x,int y,int l,int r)
{
if(!x||!y) return x+y;
int k=++cnt;
tree[k]=tree[x]+tree[y];
if(l==r) return k;
int mid=(l+r)>>1;
lson[k]=Merge(lson[x],lson[y],l,mid);
rson[k]=Merge(rson[x],rson[y],mid+1,r);
return k;
}
struct edge
{
int y,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y)
{
e[++t].y=y;
e[t].next=link[x];
link[x]=t;
}
int pos[N];
void Extend(int c,int x)
{
int p=last,np=last=++tot;
len[np]=len[p]+1;
pos[x]=np;
Insert(rot[np],1,n,x);
while(p&&!tr[p][c])
{
tr[p][c]=np;
p=fa[p];
}
if(!p) fa[np]=1;
else
{
int q=tr[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else
{
int nq=++tot;
copy(nq,q);
fa[np]=fa[q]=nq;
len[nq]=len[p]+1;
while(p&&tr[p][c]==q)
{
tr[p][c]=nq;
p=fa[p];
}
}
}
}
int f[N][20];
void dfs(int x)
{
for(int i=link[x];i;i=e[i].next)
{
int y=e[i].y;
f[y][0]=x;
for(int k=1;k<=18&&f[y][k-1];k++)
f[y][k]=f[f[y][k-1]][k-1];
dfs(y);
rot[x]=Merge(rot[x],rot[y],1,n);
}
}
int Find(int x,int lenth)
{
for(int i=18;i>=0;i--)
if(f[x][i]&&len[f[x][i]]>=lenth) x=f[x][i];
return x;
}
int Pick(int k,int l,int r,int L,int R)
{
if(!k) return 0;
if(L<=l&&r<=R) return tree[k];
int mid=(l+r)>>1;
int res=0;
if(L<=mid) res=res+Pick(lson[k],l,mid,L,R);
if(R>mid) res=res+Pick(rson[k],mid+1,r,L,R);
return res;
}
bool check(int a,int b,int c,int lenth)
{
if(lenth==0) return 1;
int x=Find(pos[c+lenth-1],lenth);
// cout<<pos[c+lenth-1]<<' '<<x<<endl;
if(Pick(rot[x],1,n,a+lenth-1,b)>0) return 1;
return 0;
}
int calc(int a,int b,int c,int d)
{
int l=0,r=min(b-a+1,d-c+1),mid,ans=0;
while(l<=r)
{
mid=(l+r)>>1;
if(check(a,b,c,mid))
{
ans=mid;
l=mid+1;
}
else r=mid-1;
}
return ans;
}
int m;
void Put()
{
for(int i=1;i<=tot;i++)
for(int c=0;c<26;c++)
if(tr[i][c])
cout<<i<<' '<<tr[i][c]<<' '<<c<<endl;
}
void Pl()
{
for(int i=2;i<=tot;i++)
cout<<fa[i]<<' '<<i<<endl;
}
void Build()
{
cin>>n>>m;
scanf("%s",s+1);
for(int i=1;i<=n;i++)
Extend(s[i]-'a',i);
for(int i=2;i<=tot;i++)
add(fa[i],i);
// Put();
// Pl();
dfs(1);
}
int main()
{
Build();
while(m--)
{
int a,b,c,d;
scanf("%d %d %d %d",&a,&b,&c,&d);
printf("%d\n",calc(a,b,c,d));
}
return 0;
}