「雅礼集训 2018 Day11」字符串 题解

题目大意

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](1xyL),如果 [ 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值