题目大意
loj6517
有
N
N
N 个字符串,每个字符串有一个权值
v
i
v_i
vi。随后给出
M
M
M 次询问,每次对一个区间进行检测。令最长的字符串长度为
L
L
L,那么会给出
g
i
gi
gi 表示每个长度的字符串的「识别值」。
对若干个字符串构成的集合 P P P 进行测试的过程如下:
对字符串 S S S 定义 f ( S ) f(S) f(S) 表示 S S S 在 P P P 中以其为前缀出现的串的权值和。 那么如果 S S S 在 P P P 中作为前缀出现过,并且 B f ( S ) + A × l e n ( S ) ≥ C Bf(S)+A\times len(S) \ge C Bf(S)+A×len(S)≥C,那么则将 g l e n ( s ) g_{len(s)} glen(s) 加入集合 G G G。
最后随机选择一个区间 [ x , y ] ( 1 ≤ x ≤ y ≤ L ) [x,y](1\le x\le y\le L) [x,y](1≤x≤y≤L),如果 [ x , y ] ∩ G ̸ = ∅ [x,y] \cap G\not=\varnothing [x,y]∩G̸=∅,那么测试成功,否则测试失败。输出测试成功的概率并用最简分数表示。
特别地,整数 k k k 表示为 k / 1 k/1 k/1 。
思路
先考虑暴力做法,对于每一个 [ x , y ] [x,y] [x,y]建立一棵 T r i e Trie Trie,则复杂度为 O ( T r i e 大 小 × m ) O(Trie大小\times m) O(Trie大小×m)
在想到如果一个询问是 [ 2 , 3 ] [2,3] [2,3],另一个询问是 [ 2 , 4 ] [2,4] [2,4],那么我们可以先建出包含 S [ 2 ] , S [ 3 ] S[2],S[3] S[2],S[3]的 T r i e Trie Trie,再加入 S [ 4 ] S[4] S[4]后稍作修改即可。
所以我们离线询问,先按左端点排序,再按右端点从大到小排序,然后稍微分一个块。
然后我们就会发现这其实是一个莫队算法。
用树状数组可以做到插入删除 O ( l o g n ) O(logn) O(logn),但这不是最优秀的复杂度。
我们考虑链表,不支持插入,但支持删除与撤回 O ( 1 ) O(1) O(1)
然后我们不使用普通莫队,换成一种叫回滚莫队的高级莫队,不需要插入
具体细节代码内有讲解,时间复杂度 O ( n l o g n n ) − > O ( n n ) O(nlogn\sqrt n)->O(n\sqrt n) O(nlognn)−>O(nn)
Code
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define inf 1000000000
#define infll 1000000000000000000ll
#define pii pair<int,int>
#define pll pair<ll,ll>
#define mpr make_pair
#define fi first
#define se second
#define vi vector<int>
#define pb push_back
#define pq priority_queue
#define up(x,y) ((x<(y))?x=(y):0)
#define dn(x,y) ((x>(y))?x=(y):0)
#define ad(x,y) (x=(x+y)%mod)
#define N 300009
using namespace std;
int n,m,tot=1,cnt,tp,bg[N],ed[N],val[N],p[N],blk[N],num[N],ch[N][26],d[N],lf[N],rg[N];
ll A,B,C,ans,sum[N]; pii h[N]; pll b[N]; pair<int,pii > q[N]; char s[N];
ll calc(int x){ return (ll)x*(x+1)>>1; } //某区间非空子集个数
int counter;
struct node{ int x,y,id; }a[N];
bool cmp(node u,node v){
return blk[u.x]<blk[v.x] || blk[u.x]==blk[v.x] && u.y>v.y;
}
//h在这里是一个链表
//由于普通链表插入很困难,但删除和撤回删除操作只需O(1)
//所以我们用链表记录所有在集合G中出现的g(i)
//链表中一个数左右的数为其前驱与后继
//所以如果[l,r]与G交集为空,则[l,r]必为链表中某两个相邻元素p,q之间的区间[p+1,q-1]的子集
//利用上面的calc函数及容斥即可得到不合法情况的个数
//最终答案为(总数-不合法个数)/总数
void dlt(int x){
int l=h[x].fi,r=h[x].se; //first指前驱,second指后继
ans+=calc(r-l-1)-calc(x-l-1)-calc(r-x-1);
//删除值x后不合法情况数量
//原本只有区间[l+1,x-1]与[x+1,r-1]的非空子集为不合法
//现在整个区间[l+1,r-1]均不合法,故ans需要加上多的那部分
q[++tp]=mpr(l,h[l]); q[++tp]=mpr(r,h[r]); //记录当前删除的链表的信息以便于以后还原
h[l].se=r; h[r].fi=l; //将x的前驱与后继连边,即删除x
}
void ins(int k,int x){
x*=val[k]; //加入或删除该点后产生的贡献
int i,j,tmp,c,now=1;
for (i=bg[k],j=1; i<=ed[k]; i++,j++){ //建Trie
c=s[i]-'a';
if (!ch[now][c]) d[ch[now][c]=++tot]=d[now]+1; now=ch[now][c];
//d数组记录每个点的深度
tmp=-(sum[now]>0 && A*j+B*sum[now]>=C);
sum[now]+=x;
//根据x的正负来减去以前产生的权值或加上现在新获得的权值
tmp+=(sum[now]>0 && A*j+B*sum[now]>=C);
//前面3行是对4种情况分别讨论
//1.以前这个点就满足条件,现在还是满足条件,tmp=0,相当于无任何贡献
//2.以前这个点满足条件, 现在不满足条件,tmp=-1,要减去之前的贡献
//3.以前这个点不满足条件,现在满足条件,tmp=1,要加上一个贡献
//4.以前不满足条件,现在也不满足条件,tmp=0,相当于无任何贡献
if (tmp<0)
if (!(--num[p[j]])){ dlt(p[j]); }
//num是指链表中是否包含p[j],当j原本的贡献被减去时,j所代表的数p[j]也没有了,在链表中把p[j]删去
if (tmp>0){ num[p[j]]++; }
//如果j有新的贡献,就代表我们的答案里有了数p[j],所以链表中将出现p[j]
}
}
ll gcd(ll x,ll y){ return y?gcd(y,x%y):x; }
void opt(ll x,ll y){
ll z=gcd(x,y);
printf("%lld/%lld\n",x/z,y/z);
}
int main(){
//freopen("string.in","r",stdin);
//freopen("string.out","w",stdout);
scanf("%d%lld%lld%lld",&n,&A,&B,&C);
int i,j,k,l,mx=0;
for (i=1; i<=n; i++) scanf("%d",&val[i]);
for (i=1; i<=n; i++){
bg[i]=ed[i-1]+1; //将字符串拼接在一起,为莫队做铺垫
scanf("%s",s+bg[i]); ed[i]=bg[i]+strlen(s+bg[i])-1;
mx=max(mx,ed[i]-bg[i]+1);
}
for (i=1; i<=mx; i++) scanf("%d",&p[i]);
scanf("%d",&m);
for (i=1; i<=m; i++){
scanf("%d%d",&a[i].x,&a[i].y); a[i].id=i;
}
blk[1]=1;
int sz=ceil(1.*ed[n]/sqrt(m)); //分块的大小
for (i=2,j=1; i<=n; i++){ //分块操作
blk[i]=blk[i-1];
if (ed[i]-bg[j]+1>sz){
blk[i]++; j=i;
}
}
sort(a+1,a+m+1,cmp);
//将询问排序,先按左端点排,左端点相同按右端点排序
cnt=blk[n];
for (i=1; i<=n; i++){
k=blk[i];
if (!lf[k]) lf[k]=i; rg[k]=i; //记录块的左右端点
}
num[mx+1]=1;
int last; ll now,all=calc(mx); //all为情况的总数,ans为不合法情况的总数
for (i=k=1; i<=cnt; i++){
for (j=lf[i]; j<=n; j++) ins(j,1);
//先将所有边加入
//由于所有字符串已按照左端点排好,所以j之前的边在后面肯定不会出现
ans=0;
for (j=1,last=0; j<=mx+1; j++)
if (num[j]){
//如果j这个值对答案有贡献, 就将j加入链表
h[j].fi=last; h[last].se=j;
ans+=calc(j-last-1); last=j; //统计不合法情况为区间[last+1,j-1]的所有子集
}
for (j=n; k<=m && blk[a[k].x]==i; k++){ //莫队操作
while (j>a[k].y) ins(j--,-1); //将不在要找的区间内的情况删去
tp=0; now=ans; //记录ans初值,方便撤回
for (l=lf[i]; l<a[k].x; l++) ins(l,-1); //将不在要找的区间内的情况删去
b[a[k].id]=mpr(all-ans,all); //记录答案
for (l=lf[i]; l<a[k].x; l++) ins(l,1); //撤回
for (; tp; tp--) h[q[tp].fi]=q[tp].se; //撤回
ans=now; //撤回
}
while (j>=lf[i]) ins(j--,-1); //将所有边删除,相当于初始化
}
for (i=1; i<=m; i++) opt(b[i].fi,b[i].se);//输出
return 0;
}