P5072 [Ynoi2015] 盼君勿忘

发布时间 2023-03-25 15:17:05作者: Multi-tree

第一道 Ynoi 也可能是最后一道了

题面的意思挺简洁,对于每一次询问的 \(l,r\) 求所有的子区间内的元素和,其中子区间内的元素要去重再进行求和。

首先我们可以想到,对于一个长度为 \(n\) 序列的子区间个数是 \(2^{n}\),如果要是里面全都是一个数 \(a^{i}\) 的话,那么对于 \(1,n\) 的询问的答案就是 \(2^{n}\times a_{i}\) 。换个角度思考一下,如果要是序列里面只有两种数,一个是 \(a_{i}\),另一个是和 \(a_{i}\) 不相等的数,那么设序列中 \(a_{i}\) 的出现次数为 \(b\),所以另一个数出现的次数就是 \(n-b\),那么不包含 \(a_{i}\) 的子序列一定只能由另一个数构成,也就是有 \(2^{n-b}\) 种方案,也就是说剩下 \(2^{n}-2^{n-b}\) 种方案里面都是有 \(a_{i}\) 的。这样我们可以得到对于一个询问的长度为 \(len\) 的区间,\(a_{i}\) 对答案的贡献为 \(a_{i}\times (2^{len}-2^{len-b})\)

题目只有询问操作,没有修改,\(1e5\) 的数据范围,我们想到用莫队来进行询问的操作。

当然这个是 Ynoi ,所以你需要更多的优化。

一开始我的思路是用个桶维护每一个数在当前询问的区间内出现的次数,然后用链表维护一下出现的数,然后直接遍历一遍链表用快速幂根据上面得出的结论计算输出答案,当然,A 是不可能 A 的,但是会 T。

image

我们知道快速幂的复杂度是 \(O(log n)\) 的,按理来说已经非常快了,但是在这里面高额的计算让他复杂度并不是那么优秀。
并且我们的链表在查询 \(len\) 个数都不相同的区间的时候复杂度就比较高了,所以我们用链表来维护在当前区间内出现次数相同的数的和,他们每一个数的贡献都是 \(a_{i}\times 2^{len}-2^{len-b}\),所以可以直接合并同类项最后直接用他们的和来和公式相乘得到对答案的贡献。我们可以计算一下,当一个长度为 \(len\) 的区间,设里面能产生的不同的出现次数的数是 \(x\) 个,那么 \(\frac{x\times (x+1)}{2}=len\),最后解出来也是小于 \(\sqrt{2\times len}\) 的,也就是说我们这个链表的长度是不会超过 \(sqrt{2\times n}\) 的,所以对于 \(n\) 的若干次幂,我们可以提前处理出来,然后直接 \(O(1)\) 查询,大大提高效率。

这个预处理的部分实际上也用到了一个叫光速幂的知识点,他可以 \(O(sqrt{n})\) 预处理,然后做到 \(O(1)\) 查询 \(a^{b}\bmod p\) 的值,前提条件是底数和模数是已知的。

\[对于a,m\in \mathbb{Z},有a^{b} \equiv \left\{\begin{matrix} a^{b} (b<\varphi (m)) \\ a^{(b \bmod \varphi (m))+\varphi (m)}(b>\varphi (m)) \end{matrix}\right. \pmod{p} \]

令 $k=\left \lceil \sqrt{b} \right \rceil $,对于 \(b\) 显然有 \(b=\left \lfloor \frac{b}{k} \right \rfloor \times k+b\bmod k\) 。所以就将 \(a^{b}\) 转化为:\(a^{b}=a^{\left \lfloor \frac{b}{k} \right \rfloor \times k+b\bmod k}\)
这时我们就可以进行 \(O(n)\) 的处理了:定义两个数组 \(x,y\)
\(x_{i}=a^{i},i\in [1,k]\),显然可以通过 \(x_{i}=x_{i-1}\times a\) 得到数组 \(x\),其中要初始化数组为 \(x_{0}=1,x_{1}=a\)
\(y_{i}=(a^{k})^{i}\),显然可以通过 \(y_{i}=y_{i-1}\times a^{k}\) 也就是 \(x_{k}\) 得到数组 \(y\),其中初始化数组为 \(y_{0}=1,y_{1}=x_{k}\)
所以 \(a^{b}\bmod p=x_{\left \lfloor \frac{b}{k} \right \rfloor }\times y_{b}\bmod k\)

对于这道题目每一次的模数都是变化的,所以每一次询问都要重新预处理一下,然后就是愉快的莫队了。

code:

#include<bits/stdc++.h>
#define bug cout<<"cao"<<endl
#define int long long
#define N 100010
using namespace std;
int n,m,l,r,a[N],kc,ans[N],lx,bl[N];//bl标记当前点属于哪一块,lx标记链表最后一个元素的标号 
int p1[N],p2[N],p,cnt[N],sum[N];//sum存放当前出现次数的数的和,cnt是当前数的出现次数,p1p2是光速幂预处理的数组,p是模数 
struct sb{int l,r,p,bh;}e[N];//询问操作 
struct lb{int next,pre;}e1[N];//链表 
inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}
inline void print(int x){if(x>=10)print(x/10);putchar(x%10+48);}
inline int cmp(sb a,sb b)//询问排序函数 
{
	if(bl[a.l]==bl[b.l])return a.r<b.r;
	else return a.l<b.l;
}
inline void _insert(int x)//从链表中加入某点 
{
	e1[x].pre=lx;//当前点的前驱是上一个节点 
	e1[lx].next=x;//上一个节点的后继是当前点 
	lx=x;//替换当前点为链表最后的节点 
	return ;
}
inline void _delete(int x)//删除一个节点 
{
	if(lx==x)//如果是最后一个节点的话 
	{
		e1[e1[x].pre].next=0;//当前点的前一个节点的后继标记为没有 
		lx=e1[x].pre;//最后一个节点的编号改为当前点的前驱 
		e1[x].next=e1[x].pre=0;//清空当前点存的信息 
		return ;//返回 
	}
	e1[e1[x].pre].next=e1[x].next;//当前节点的前驱的后继为当前节点的后继 
	e1[e1[x].next].pre=e1[x].pre;//当前节点后继的前驱为当前节点的前驱 
	e1[x].next=e1[x].pre=0;//清空信息 
	return ;//返回 
}
inline void add(int x)//加入当前点的贡献 
{
	if(cnt[x])//如果要是当前点有出现次数 
	{
		sum[cnt[x]]-=x;//先减去之前的 
		if(!sum[cnt[x]])//如果要是当前出现次数的点没了 
		  _delete(cnt[x]);//从链表中删除当前节点 
	}
	cnt[x]++;//当前点的出现次数加一 
	if(!sum[cnt[x]])//如果要是当前出现次数的点之前没有 
	  _insert(cnt[x]);//插入当前点 
	sum[cnt[x]]+=x;//累加 
}
inline void del(int x)//删除当前点的贡献 
{
	sum[cnt[x]]-=x;//首先减去当前点之前的贡献 
	if(!sum[cnt[x]])//如果要是sum是0了,说明没有当前出现次数的数 
	  _delete(cnt[x]);//从链表中删除 
	cnt[x]--;//出现次数减一 
	if(cnt[x])//如果当前的数在序列中出现 
	{
		if(!sum[cnt[x]])//如果要是没有当前出现次数的元素 
		  _insert(cnt[x]);//插入链表中 
		sum[cnt[x]]+=x;//直接累加到当前sum数组上 
	}
}
inline int pw(int x){return p1[x%kc]*p2[x/kc]%p;}//利用预处理出的两个数组来o(1)查询 
signed main()
{
	n=read();m=read();
	kc=sqrt(n);//计算块长 
	for(int i=1;i<=n;i++)
		a[i]=read(),bl[i]=(i-1)/kc+1;//输入数据的同时处理出每一个数所属的块 
	for(int i=1;i<=m;i++)
		e[i].l=read(),e[i].r=read(),e[i].p=read(),e[i].bh=i;//输入查询的信息并存编号 
	sort(e+1,e+m+1,cmp);//对询问进行排序 
	for(int nl=1,nr=0,i=1;i<=m;i++)
	{
		while(nr<e[i].r)add(a[++nr]);//正常莫队板子 
		while(nl>e[i].l)add(a[--nl]);
		while(nr>e[i].r)del(a[nr--]); 
		while(nl<e[i].l)del(a[nl++]);
		p=e[i].p;//更新模数 
//		cout<<nl<<" "<<nr<<endl;
		p1[0]=p2[0]=1;//光速幂预处理 
		for(int j=1;j<=kc;j++)
			p1[j]=p1[j-1]*2%p;
		p2[1]=p1[kc];
		for(int j=2;j<=kc;j++)
		    p2[j]=p2[j-1]*p2[1]%p;
		for(int j=e1[0].next;j;j=e1[j].next)//利用链表来遍历每一个出现次数的数的和 
		{
			ans[e[i].bh]+=sum[j]*(pw(e[i].r-e[i].l+1)-pw(e[i].r-e[i].l+1-j))%p;//计算答案,调用光速幂 
			ans[e[i].bh]=(ans[e[i].bh]+p)%p;//取模,保证是个非负数 
		}
	}
	for(int i=1;i<=m;i++)//遍历一遍询问输出答案 
		print(ans[i]),cout<<"\n";
	return 0;
}

image

这个地方把排序的里面的 bl 数组预处理出来忘调用了。。。然后块长设的 400 T 飞了。。。