九省联考2018 制胡窜
制胡窜
对于一个字符串 \(S\),我们定义 \(|S|\) 表示 \(S\) 的长度。
接着,我们定义 \(S_i\) 表示 \(S\) 中第 \(i\) 个字符,\(S_{L,R}\) 表示由 \(S\) 中从左往右数,第 \(L\) 个字符到第 \(R\) 个字符依次连接形成的字符串。特别的,如果 \(L > R\) ,或者 \(L < [1, |S|]\), 或者 \(R < [1, |S|]\) 我们可以认为 \(S_{L,R}\) 为空串。
给定一个长度为 \(n\) 的仅由数字构成的字符串 \(S\),现在有 \(q\) 次询问,第 \(k\) 次询问会给出 \(S\) 的一个字符串 \(S_{l,r}\) ,请你求出有多少对 \((i, j)\),满足 \(1 \le i < j \le n\),\(i + 1 \lt j\),且 \(S_{l,r}\) 出现在 \(S_{1,i}\) 中或 \(S_{i+1, j−1}\) 中或 \(S_{j,n}\) 中。
对于所有测试数据,\(1 \le n \le 10^5\),\(1 \le q \le 3 · 10^5\),\(1 \le l \le r \le n\)。
题解
参照cz_xuyixuan和TS_Hugh的题解。
问题跟所有子串有关,考虑后缀自动机。
问题转化
题目中两个“或”已经说明了正面求很难做。正难则反,计算\(i,j\)分布使得三段都不包含所有出现的串的方案数。显然\((i,i+1),(j-1,j)\)这两个空隙要切断所有的串。所以问题转化到了空隙上面,把方案数求出来用\(\binom{n-1}2\)减去它就是答案了。
下面的论述为了方便,以右端点代替空隙,即用\(i\)来代替\((i-1,i)\)这个空隙,显然\(i,j\in[2,n],i<j\)。
只询问一个串
首先,一个询问的答案只和询问串的在主串中所有出现的位置有关。因此要找出询问串在后缀自动机上的位置。
定位一个询问的串可以在后缀自动机parent树上倍增在\(O(\log n)\)的时间内完成。如果能预处理出right集合那么所有出现的位置就找到了。
考虑用线段树合并来做这件事。现在我们有了一棵维护着所有询问串出现位置的右端点的线段树,考虑如何得到答案。
考虑较靠前的断点切断了哪些字符串。我们需要求出的即是:
其中\([ql,qr]\)为\(k\)可能的取值范围。\(ql\)即第\(k+1\)个串与后面的串有交集时,\(k\)的可取最小值;类似的,\(qr\)即第\(k\)和前面的串有交集时,\(k\)的可取最大值。它们可以在线段树上二分得到。
记询问串第\(k\)次出现的左端点和右端点分别为\(L_k\)和\(R_k\)
对于询问串第一次出现和第\(k\)次出现的交,它在大部分的情况下为\(L_{k+1}-L_k\),也即\(R_{k+1}-R_k\),只有在\(k=qr\)时,它有可能为\(R_1−L_i+1\)。
对于询问串最后一次出现和第\(k+1\)次出现的交,它在大部分情况下为\(R_{k+1}−L_{last}+1\)。
还需要考虑一些特殊情况,可能可以用\(i,j\)中的一个切断所有串。为了不重不漏,对\(i\)来讨论,分为两种情况。
- \(i\)不切断任意的字符串,那么它一定在询问串第一次出现之前,并且\(j\)要恰好切断所有字符串,也即\(j\)的取值范围是询问串所有出现位置的并。
只有在\(k=ql\)时,有可能对应这种情况,此时\(ql=0\)。 - \(i\)切断了所有的字符串,那么\(j\)只需要满足在较靠前的断点之后即可,因此在这种情况下可能的方案数是一个等差数列的各项之和。
只有在\(k=qr\)时有可能出现这种情况,此时\(qr=last\)
那么,我们可以对\(k=ql\),和\(k=qr\)时特殊处理。对于剩下的情况,也即\(k\in(ql,qr)\)时,我们需要求出
对于求和部分,是关于两个相邻位置的信息,我们可以在线段树上额外维护这个信息。那么一个串就做完了。
询问多个串
由于使用线段树合并要求在线的话,合并的时候必须新建节点,所以空间复杂度就错了。所以把询问离线挂在parent树上,询问的时候在parent按从叶子到根的拓扑序来做。这样线段树合并的时候直接用原来的节点就行了。合并时线段树节点按访问量满来计算,最坏复杂度\(O(n\log n)\)。
然后这道题就做完了。时间复杂度\(O(n\log n+q\log n)\)。
代码
这题给的数字串……写的字母串调了好久。
co int N=2e5;
int n,m;
char s[N];
ll ans[300001];
// Interval Tree
struct node{int min,max;ll sum;};
node operator+(co node&a,co node&b){
node c=(node){a.min,b.max,a.sum+b.sum};
if(!c.min) c.min=b.min;
if(!c.max) c.max=a.max;
if(a.max&&b.min) c.sum+=(ll)b.min*(b.min-a.max);
return c;
}
namespace T{
node t[N*17];
int tot,lc[N*17],rc[N*17];
void insert(int&x,int l,int r,int p){
if(!x) x=++tot;
if(l==r) return t[x]=(node){l,l,0},void();
int mid=l+r>>1;
if(p<=mid) insert(lc[x],l,mid,p);
else insert(rc[x],mid+1,r,p);
t[x]=t[lc[x]]+t[rc[x]];
}
int merge(int x,int y){
if(!x||!y) return x+y;
lc[x]=merge(lc[x],lc[y]),rc[x]=merge(rc[x],rc[y]);
t[x]=t[lc[x]]+t[rc[x]];
return x;
}
int lower(int x,int l,int r,int p){ // first >=
if(t[x].min>=p) return t[x].min;
int mid=l+r>>1;
if(t[lc[x]].max&&t[lc[x]].max>=p) return lower(lc[x],l,mid,p);
else return lower(rc[x],mid+1,r,p);
}
int upper(int x,int l,int r,int p){ // last <=
if(t[x].max<=p) return t[x].max;
int mid=l+r>>1;
if(t[rc[x]].min&&t[rc[x]].min<=p) return upper(rc[x],mid+1,r,p);
else return upper(lc[x],l,mid,p);
}
node query(int x,int l,int r,int ql,int qr){
if(ql>qr) return (node){0,0,0};
if(ql<=l&&r<=qr) return t[x];
int mid=l+r>>1;
if(qr<=mid) return query(lc[x],l,mid,ql,qr);
if(ql>mid) return query(rc[x],mid+1,r,ql,qr);
return query(lc[x],l,mid,ql,qr)+query(rc[x],mid+1,r,ql,qr);
}
ll query(int x,int len){ // represent gap with right vertice
if(len<=0) return 0;
int r1=t[x].min,l1=r1-len+1; // len=r-l+1-1
int rn=t[x].max,ln=rn-len+1;
int ql=lower(x,1,n,ln);
if(ql!=r1) ql=upper(x,1,n,ql-1);
else ql=0;
int qr=upper(x,1,n,r1+len-1);
if(ql>qr) return 0;
ll ans=0;
if(ql==qr){
assert(0<ql&&ql<rn);
ans+=(ll) (min(r1,lower(x,1,n,ql+1)-len)-(ql-len+1)+1) * (lower(x,1,n,ql+1)-ln+1);
}
else{
node tmp=query(x,1,n,ql+1,qr);
ans+=tmp.sum-(ll) (ln-1) * (tmp.max-tmp.min);
if(ql==0) ans+=(ll) (l1-2) * (r1-ln+1);
else ans+=(ll) (min(r1,lower(x,1,n,ql+1)-len)-(ql-len+1)+1) * (lower(x,1,n,ql+1)-ln+1);
if(qr==rn) ans+=(ll) (n-r1+n-ln) * (r1-ln+1) / 2;
else ans+=(ll) (min(r1,lower(x,1,n,qr+1)-len)-(qr-len+1)+1) * (lower(x,1,n,qr+1)-ln+1);
}
return ans;
}
}
// Suffix Automaton
namespace SAM{
int last=1,tot=1;
int ch[N][10],fa[N],len[N],pos[N]; // pos:out->in
int root[N]; // for Interval Tree
void extend(int c,int po){
int p=last,cur=last=++tot;
len[cur]=len[p]+1,pos[po]=cur;
T::insert(root[cur],1,n,po);
for(;p&&!ch[p][c];p=fa[p]) ch[p][c]=cur;
if(!p) fa[cur]=1;
else{
int q=ch[p][c];
if(len[q]==len[p]+1) fa[cur]=q;
else{
int clone=++tot;
memcpy(ch[clone],ch[q],sizeof ch[q]);
fa[clone]=fa[q],len[clone]=len[p]+1;
fa[cur]=fa[q]=clone;
for(;ch[p][c]==q;p=fa[p]) ch[p][c]=clone;
}
}
}
int anc[N][19];
vector<int> e[N];
vector<pair<int,int> > q[N];
void init(){
for(int i=1;i<=n;++i) extend(s[i]-'0',i);
for(int i=1;i<=tot;++i) anc[i][0]=fa[i],e[fa[i]].push_back(i);
for(int k=1;k<=18;++k)
for(int i=1;i<=tot;++i) anc[i][k]=anc[anc[i][k-1]][k-1];
}
void storequery(int l,int r,int id){
int len=r-l+1,p=pos[r];
for(int i=18;i>=0;--i)
if(SAM::len[anc[p][i]]>=len) p=anc[p][i];
q[p].push_back(make_pair(len,id));
}
void work(int p){
for(int i=0;i<e[p].size();++i)
work(e[p][i]),root[p]=T::merge(root[p],root[e[p][i]]);
for(int i=0;i<q[p].size();++i)
ans[q[p][i].second]=(ll)(n-1)*(n-2)/2-T::query(root[p],q[p][i].first-1); // available len for cutting
}
}
int main(){
read(n),read(m),scanf("%s",s+1);
SAM::init();
for(int l,r,i=1;i<=m;++i){
read(l),read(r);
SAM::storequery(l,r,i);
}
SAM::work(1);
for(int i=1;i<=m;++i) printf("%lld\n",ans[i]);
return 0;
}
强烈推荐cz_xuyixuan的代码,他竟然把码风维护到这种题上。当初我早就放弃封装了。