Luogu4384 [八省联考2018]制胡窜
Luogu4384 [八省联考2018]制胡窜
参考blog
这篇博客的分类讨论情况、式子和参考blog中是基本一致,因为蒟蒻切不了,只能够学习\(shadowice1984\)大佬,解析都是自己写的。
\(SAM+\)线段树合并+分类讨论
字符串太可怕了!!!
原问题即给定一个字符串\(S\),每次询问的字符串为原串的一个区间\(S(l,r)\),求有多少种方案把原字符串切成三段,使得\(S(l,r)\)是其中一段的子串。
转化一下,我们可以用总数减去不满足条件的切法,即三段中任何一段都不拥有子串\(S(l,r)\),总数很容易得出是\({n-1 \choose 2}\)。
对于\(S\)中每一个\(T\)出现的区间\([l_i,r_i]\)(后面简称\(C_i\),总区间数为\(m\)),我们需要用两刀把所有区间都切断,这些区间的性质为长度相等。
设询问串为\(T\),在原字符串中第一次出现的位置和最后一次出现的区间为\(A,B\),\(A\)的右端点为\(first\),\(B\)的左端点为\(last\)。
分类讨论(一定要画图):
\(1.\)\(S\)中含有三段互不相交的询问串,那么显然问题无解。
\(2.\)\(A,B\)相交
\(2.1\)第一刀没有切完所有区间(保证第一刀在第二刀左侧,为了方便,会考虑部分切完所有区间的情况)
考虑在区间\([l_i,l_{i+1})\)切第一刀,它们必然是等价的,因为左端点必然小于等于每一个右端点。
为了满足切完所有区间,我们考虑下一刀的位置。首先,我们在区间\([l_i,l_{i+1})\)上切一刀,区间\(1 \sim i\)已经被切断了。那么我们需要切断后面的所有区间,显然我们至少要切在\(last\)位置才能切断\(B\),所以\(last\)为第二刀的左端点。那么右端点呢?我们需要避免两刀之间仍然含有区间的情况。我们暂时还没有切断区间\(C_{i+1}\),我们只需要使得第二刀在每一个\(C_j(j>i)\)右端点之前即可完全切断所有区间,\(r_{i+1}=\min_{i+1 \le j \le m} r_j\),因此右端点为\(r_{i+1}-1\)。
所以满足题意的第二刀位置为\([last,r_{i+1})\),这个条件下已经满足了\(last \le r_{i+1}\),同时空区间的计算并不影响答案。
我们整理一下总贡献(在贡献统计中我们都使用\(r_i-len+1\)代替\(l_i\),因为\(SAM\)统计的是后缀):
\(2.2\)有一刀切完了所有区间(这里的统计并不注重顺序关系)
首先考虑满足条件的一刀位置,容易得出是\([last,first)\)。
继续考虑,两刀都在\([last,first)\)区间内,因为这个区间在\(B\)的左端点之后,在\(2.1\)中没有被统计,因此贡献为\({first-last \choose 2}\)。
另一刀不在该区间内,在\(2.1\)的统计中,这一段被统计过一次,那时我们统计的是\([last-len+1,last)\)为第一刀,那么\([last,first)\)显然是满足条件的第二刀,我们不能再统计这里的贡献。
因此另一刀可以在\([1,last-len+1)\)和\([last,n)\)中,贡献为\((first-last)(n-len)\)。
整理贡献:
综合\(2.1\)和\(2.2\),得到贡献为:
\(3.\)\(A,B\)不相交
那么有一刀切完了所有区间是不可能出现的。
我们可以先把\(2.1\)的式子\(Copy\)过来,看看哪里出了问题。
在\(2.1\)中,第二刀位置为\([last,r_{i+1})\)。
第一个问题,存在\(r_{i+1} \le last\)的问题,而这种情况显然是无解的,我们不能把负数加入答案。
第二个问题,由于\(first\)不再位于所有串左端点的右侧,也就是说区间\(1 \sim i\)已经被切断并不成立,那么如果第一刀位于\([l_i,l_{i+1})\),我们必须让区间\(1 \sim i\)已经被切断,也就是这一刀位置\(<first\)。
对于问题一,我们有了限制\(r_{i+1}>last\),即线段树中\(last\)的后继开始的节点。
对于问题二,发现这个限制是对于每一个位置而言的,因此\([l_i,l_{i+1})\)可能不再等价,有一部分需要舍去。
但是不等价区间只有一个,因此我们可以直接找到该区间\(C_k\)(线段树中\(first+len-1\)的前驱),那一段区间的第一刀为\([l_k,first)\),第二刀为\([last,r_{k+1})\)
那一段区间的贡献为:
统计贡献:
来总的式子吧!
观察式子,可以发现\(first,last,k\)等量可以在权值线段树上查找最值、前驱后继得到,我们需要计算的是两个量。
由于权值线段树并不是维护区间的,因此我们合并两个区间必须将左区间的\(\max\)和右区间的\(\min\)合并,也就是需要额外维护两个标记。
\(AC\)代码(伟大的成就):
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define N 100005
#define ll long long
using namespace std;
const int INF=1000000007;
int n,q,l,r,k,d,first,last,Len;
char str[N];
int tot=1,lst=1,tr[N << 1][10],pre[N << 1],len[N << 1];
int ed[N];
int f[N << 1][22];
ll ans=0;
void ins(int c)
{
int p=lst,np;
lst=np=++tot;
len[np]=len[p]+1;
for (;p && !tr[p][c];p=pre[p])
tr[p][c]=np;
if (!p)
pre[np]=1; else
{
int q=tr[p][c];
if (len[p]+1==len[q])
pre[np]=q; else
{
int g=++tot;
memcpy(tr[g],tr[q],sizeof(tr[q]));
len[g]=len[p]+1,pre[g]=pre[q];
for (;p && tr[p][c]==q;p=pre[p])
tr[p][c]=g;
pre[np]=pre[q]=g;
}
}
}
struct edge
{
int nxt,v;
edge (int Nxt=0,int V=0)
{
nxt=Nxt,v=V;
}
}e[N << 1];
int tt,fr[N << 1];
void add(int x,int y)
{
++tt;
e[tt]=edge(fr[x],y),fr[x]=tt;
}
int Cnt=0,rt[N << 1];
#define ls(p) t[p].lc
#define rs(p) t[p].rc
#define s(p) t[p].S
#define muls(p) t[p].Muls
#define cnt(p) t[p].ct
#define mx(p) t[p].Mx
#define mn(p) t[p].Mn
struct node
{
int lc,rc;
int ct,Mx=0,Mn=INF;
ll S=0,Muls=0;
}t[N << 6];
void update(int p)
{
cnt(p)=cnt(ls(p))+cnt(rs(p));
mx(p)=max(mx(ls(p)),mx(rs(p)));
mn(p)=min(mn(ls(p)),mn(rs(p)));
s(p)=s(ls(p))+s(rs(p));
muls(p)=muls(ls(p))+muls(rs(p));
if (!mx(ls(p)) || mn(rs(p))==INF)
return;
s(p)+=mn(rs(p))-mx(ls(p));
muls(p)+=(ll)(mn(rs(p))-mx(ls(p)))*mn(rs(p));
}
void modify(int &p,int l,int r,int x)
{
if (!p)
p=++Cnt;
if (l==r)
{
++cnt(p);
mx(p)=mn(p)=x;
return;
}
int mid=(l+r) >> 1;
if (x<=mid)
modify(ls(p),l,mid,x); else
modify(rs(p),mid+1,r,x);
update(p);
}
int combine(int x,int y,int l,int r)
{
if (!x || !y)
return x|y;
int p=++Cnt;
if (l==r)
{
cnt(p)=cnt(x)+cnt(y);
mx(p)=max(mx(x),mx(y));
mn(p)=min(mn(x),mn(y));
return p;
}
int mid=(l+r) >> 1;
ls(p)=combine(ls(x),ls(y),l,mid);
rs(p)=combine(rs(x),rs(y),mid+1,r);
update(p);
return p;
}
struct cnode
{
int Mx=0,Mn=INF;
ll S=0,Muls=0;
cnode (int MX=0,int MN=INF,ll St=0,ll MULS=0)
{
Mx=MX,Mn=MN,S=St,Muls=MULS;
}
};
cnode combine(cnode x,cnode y)
{
cnode z;
z.Mx=max(x.Mx,y.Mx);
z.Mn=min(x.Mn,y.Mn);
z.S=x.S+y.S;
z.Muls=x.Muls+y.Muls;
if (!x.Mx || y.Mn==INF)
return z;
z.S+=y.Mn-x.Mx;
z.Muls+=(ll)(y.Mn-x.Mx)*y.Mn;
return z;
}
cnode calc(int p,int l,int r,int x,int y)
{
if (!p)
return cnode();
if (l==x && r==y)
return cnode(mx(p),mn(p),s(p),muls(p));
int mid=(l+r) >> 1;
if (y<=mid)
return calc(ls(p),l,mid,x,y); else
if (x>mid)
return calc(rs(p),mid+1,r,x,y); else
return combine(calc(ls(p),l,mid,x,mid),calc(rs(p),mid+1,r,mid+1,y));
}
int findmn(int p,int l,int r)
{
if (!cnt(p))
return -1;
if (l==r)
return l;
int mid=(l+r) >> 1;
if (cnt(ls(p)))
return findmn(ls(p),l,mid); else
return findmn(rs(p),mid+1,r);
}
int findmx(int p,int l,int r)
{
if (!cnt(p))
return -1;
if (l==r)
return l;
int mid=(l+r) >> 1;
if (cnt(rs(p)))
return findmx(rs(p),mid+1,r); else
return findmx(ls(p),l,mid);
}
int findpre(int p,int l,int r,int x)
{
if (!cnt(p))
return -1;
if (l==r)
return l;
int mid=(l+r) >> 1;
if (x-1<=mid)
return findpre(ls(p),l,mid,x); else
{
int o=findpre(rs(p),mid+1,r,x);
if (~o)
return o; else
return findmx(ls(p),l,mid);
}
}
int findsucc(int p,int l,int r,int x)
{
if (!cnt(p))
return -1;
if (l==r)
return l;
int mid=(l+r) >> 1;
if (x+1>mid)
return findsucc(rs(p),mid+1,r,x); else
{
int o=findsucc(ls(p),l,mid,x);
if (~o)
return o; else
return findmn(rs(p),mid+1,r);
}
}
void work(int u)
{
for (int i=fr[u];i;i=e[i].nxt)
{
int v=e[i].v;
f[v][0]=u;
work(v);
rt[u]=combine(rt[u],rt[v],1,n);
}
}
ll C2(int x)
{
return (ll)x*(x-1)/2;
}
void Task1()
{
printf("%lld\n",ans);
}
void Task2()
{
cnode res=calc(rt[d],1,n,1,n);
ans-=res.Muls-(ll)last*res.S+C2(first-last)+(ll)(first-last)*(n-Len);
printf("%lld\n",ans);
}
void Task3()
{
cnode res;
int sti=findsucc(rt[d],1,n,last);
int edi=findpre(rt[d],1,n,first+Len-1);
k=edi;
if (sti!=-1 && edi!=-1 && sti<=edi)
{
sti=findpre(rt[d],1,n,sti);
res=calc(rt[d],1,n,sti,edi);
ans-=res.Muls-(ll)last*res.S;
}
if (k!=-1)
{
int succ=findsucc(rt[d],1,n,k);
if (succ!=-1 && succ>last)
ans-=(ll)(first-k+Len-1)*(succ-last);
}
printf("%lld\n",ans);
}
int main()
{
scanf("%d%d",&n,&q);
scanf("%s",str+1);
for (int i=1;i<=n;++i)
ins(str[i]-'0');
for (int i=2;i<=tot;++i)
add(pre[i],i);
int st=1;
for (int i=1;i<=n;++i)
{
st=tr[st][str[i]-'0'];
ed[i]=st;
modify(rt[st],1,n,i);
}
work(1);
for (int j=1;j<=20;++j)
for (int i=1;i<=tot;++i)
f[i][j]=f[f[i][j-1]][j-1];
while (q--)
{
scanf("%d%d",&l,&r);
d=ed[r];
for (int i=20;i>=0;--i)
if (f[d][i] && r-len[pre[f[d][i]]]<l)
d=f[d][i];
if (r-len[pre[d]]<l)
d=pre[d];
Len=r-l+1;
first=findmn(rt[d],1,n);
last=findmx(rt[d],1,n)-Len+1;
ans=C2(n-1);
if (first==-1)
Task1(); else
if (first>=last)
Task2(); else
Task3();
}
return 0;
}
\(update:\)在该代码\(findpre,findsucc\)两个查找前驱后继的函数中调用了\(findmx,findmn\),其实不调用复杂度也是对的。
也就是下面的代码同样可行。
int findpre(int p,int l,int r,int x)
{
if (!cnt(p))
return -1;
if (l==r)
return l;
int mid=(l+r) >> 1;
if (x-1<=mid)
return findpre(ls(p),l,mid,x); else
{
int o=findpre(rs(p),mid+1,r,x);
if (~o)
return o; else
return findpre(ls(p),l,mid,x);
}
}
int findsucc(int p,int l,int r,int x)
{
if (!cnt(p))
return -1;
if (l==r)
return l;
int mid=(l+r) >> 1;
if (x+1>mid)
return findsucc(rs(p),mid+1,r,x); else
{
int o=findsucc(ls(p),l,mid,x);
if (~o)
return o; else
return findsucc(rs(p),mid+1,r,x);
}
}