CF1553(Div.1+Div.2) F 题解

这篇博客详细解析了一道洛谷高分题解,涉及计算序列中元素模运算的累积贡献。作者提出了两种算法,第一种利用树状数组优化,复杂度为O(n+mln⁡mlog⁡n)O(n+mlnmlogn)O(n+mlnmlogn),第二种为加强版,利用根号分治,复杂度为O(nn)O(nsqrtn)O(nn​)。文章深入探讨了模运算的转换和处理大数与小数的不同策略,并给出了完整的C++代码实现。

这是一篇豪夺洛谷最优解榜倒数 rnk 1 的题解。所以我为啥还有脸写

Description

给定序列 aaa

ti=∑j=1i∑k=1iaj mod akt_i=\sum_{j=1}^i \sum_{k=1}^i a_j \ \text{mod} \ a_kti=j=1ik=1iaj mod ak

你需要分别求出 t1,t2,⋯ ,tnt_1,t_2,\cdots,t_nt1,t2,,tn

1≤n≤2×105,1≤ai≤3×1051 \le n \le 2 \times 10^5,1 \le a_i \le 3 \times 10^51n2×105,1ai3×105

Solution

算法一


fi=∑j<i(aimod  aj)f_i=\sum_{j<i} (a_i \mod a_j)fi=j<i(aimodaj)

gi=∑j<i(ajmod  ai)g_i=\sum_{j<i} (a_j \mod a_i)gi=j<i(ajmodai)

则第 iii 个答案为

∑j=1ifj+∑j=1igj\sum_{j=1}^i f_j+\sum_{j=1}^i g_jj=1ifj+j=1igj

关键在于求出每一个 fi,gif_i,g_ifi,gi

Part 1: 求 f

根据 xmod  y=x−⌊xy⌋yx \mod y=x-\lfloor \frac x y \rfloor yxmody=xyxy,有

fi=ai(i−1)∑j<i⌊aiaj⌋ajf_i=a_i(i-1)\sum_{j<i} \left \lfloor \frac {a_i} {a_j} \right \rfloor a_jfi=ai(i1)j<iajaiaj

考虑枚举 jjj

对于每一个这样的 jjj,枚举值域区间 [0,aj−1][aj,2aj−1]⋯[0,a_j-1][a_j,2a_j-1]\cdots[0,aj1][aj,2aj1],那么 jjj 会向所有满足 ai∈[l,r]a_i \in [l,r]ai[l,r]i>ji>ji>jfif_ifi 产生贡献。

我们在 j+1j+1j+1 处打上标记 (r,1)(l−1,−1)(r,1)(l-1,-1)(r,1)(l1,1),其中在 xxx 处的标记 (p,△)(p,\triangle)(p,) 表示,对于所有 xxx 之后且权值不小于 pppfif_ifi 都要加上 △\triangle

从左往右扫描,用树状数组维护即可。时间复杂度 O(mln⁡mlog⁡n)O(m \ln m \log n)O(mlnmlogn)。其中 mmm 表示所有 aia_iai 的最大值。

Part 2: 求 g

同理,有

gi=∑j<iaj−∑j<i⌊ajai⌋aig_i=\sum_{j<i} a_j-\sum_{j<i} \left \lfloor \frac {a_j} {a_i} \right \rfloor a_igi=j<iajj<iaiajai

枚举 [0,ai−1][ai,2ai−1],⋯[0,a_i-1][a_i,2a_i-1],\cdots[0,ai1][ai,2ai1], 并查询 [1,i)[1,i)[1,i) 中有多少个在该区间中的数。也可以使用树状数组维护。


总复杂度 O(n+mln⁡mlog⁡n)O(n+m \ln m \log n)O(n+mlnmlogn),本题被解决。

算法二(加强版: aia_iai 可能相同)

考虑根号分治。

我们将数分为两类——第一类数不超过 BBB,称它们为 ⌈\lceil 小数 ⌋\rfloor;第二类数超过 BBB,称它们为 ⌈\lceil 大数 ⌋\rfloor。小数的性质是值域较小,大数的性质是倍数较少。

Part 1

先考虑小数。为了利用它们值域小的性质,我们开一个桶,其中第 iii 个桶 ViV_iVi 里面装了所有值为 iii 的数的位置。枚举编号在 [1,B][1,B][1,B] 中的桶,并考虑计算出它们对序列 ttt 的贡献。

令当前需要计算贡献的桶是第 x(x≤B)x(x \le B)x(xB) 个。由于模运算需要两个数,所以还要再枚举一个桶,令这是第 yyy 个桶。显然,∀i∈Vx,j∈Vy\forall i \in V_x,j \in V_yiVx,jVy,它们都对 t[max⁡(i,j),n]t[\max(i,j),n]t[max(i,j),n]x mod yx \ \text{mod} \ yx mod y 的贡献。

为方便进一步的推导,考虑将 max⁡(i,j)\max(i,j)max(i,j)max⁡\maxmax 去掉,即分类地钦定 i,ji,ji,j 之间的大小关系。我们先预处理出一个序列 CCC,其中第 kkk 位上的数是 ViV_iVi 中不超过 kkk 的数的个数,那么对于某个属于 VyV_yVyjjj,它作为 max⁡(i,j)\max(i,j)max(i,j) 的次数为 CjC_jCj 次,其中 mmm 表示所有 aia_iai 的最大值,可以直接在 ttt 上差分。但是,若 j=min⁡(i,j)j=\min(i,j)j=min(i,j),那么应该在 iii 处差分地修改,但 iii 我们并不清楚。这该怎么办呢?

考虑维护另一个数组 DDD,其中 DiD_iDi 表示,当 iii 作为 max⁡(i,j)\max(i,j)max(i,j)(即 jjj 作为 min⁡(i,j)\min(i,j)min(i,j))时的总贡献。这个数组可以采用下面的方法求出——对于每个属于其他桶的 jjj,在 DjD_jDj 处差分地加上 i mod ji \ \text{mod} \ ji mod j 以及 j mod ij \ \text{mod} \ ij mod i。在枚举完毕后,将 DDD 求一遍前缀和得到真实的 DDD,再去逐一向 ttt 差分地贡献。

总结一下 ⌈\lceil 小数 ⌋\rfloor 部分的处理方式:

  • 建立一个桶;
  • 扫描桶 [1,B][1,B][1,B]
    • 通过前缀和技巧求出 CCC 数组;
    • 枚举桶 [1,m][1,m][1,m] 以及其中的数 jjj,分类讨论 jjji,ji,ji,j 中的较大者以及较小者两种情况:
      • 作为较大者: 直接在 ttt 上差分地修改。
      • 作为较小者: 在 DDD 上差分地修改。
    • 做前缀和得到真实的 DDD,并在 ttt 上差分地修改,解决 i=max⁡(i,j)i=\max(i,j)i=max(i,j) 的情况。

此部分复杂度为 O(B(n+m))O(B(n+m))O(B(n+m))

Part 2

现在,我们已经处理完了,所有与小数有关的取模式,现在只需要考虑大数与大数之间产生的贡献。枚举一个大数,它有两种情况,一是向别人取模,一是被别人取模。这两种情况联系不大,需要进行分讨。

Part 2.1: 被别人取模

对于大数而言,其倍数较少;也就是说,当某个数 num\text{num}num 逐渐增大的时候,其对大数取模的值的极大连续段的数量较少。例如,这个大数是 10410^4104,那么当 num\text{num}num111 缓慢增加到 10510^5105 的过程中,模数会从 000 变化到 104−110^4-11041,然后又回到 000 再变成 104−110^4-11041,而这样变化的轮数的只有 O(mB)O(\frac m B)O(Bm)

直接在原序列上扫描。

令当前看到的数是一个大数,且其值为 ccc。那么,我们需要查询 ∑i=0c−1i×cnti\sum_{i=0}^{c-1} i \times cnt_ii=0c1i×cnti∑i=c2c−1(i−c)×cnti\sum_{i=c}^{2c-1} (i-c) \times cnt_ii=c2c1(ic)×cnti⋯⋯⋯⋯\cdots \cdots \cdots \cdots

其中 cnticnt_icnti 表示目前 iii 出现的次数。

于是,问题变为了一道纯粹的操作、查询类题目。

  • 给定一个长度为 nnn 的序列 cntcntcnt
  • 你需要支持单点修改,查询区间 i×cntii \times cnt_ii×cnti 的和,查询区间 cnticnt_icnti 的和。

若直接采用树状数组维护,那么复杂度是 O(nmBlog⁡m)O(n \frac {m} {B} \log m)O(nBmlogm),难以接受。

注意到,查询次数为 n×mBn \times \frac m {B}n×Bm 次,操作次数只有 nnn 次,进行根号平衡即可。复杂度为 O(nmB)O(n \frac m B)O(nBm)

Part 2.2: 向别人取模

先将 xmod  yx \mod yxmody 变为 x−⌊xy⌋yx-\lfloor \frac x y \rfloor yxyxy。那么,等价于对于每个满足 ai>Ba_i>Bai>Biii,求出

ai×(∑j=1i−1[aj>B])−∑j=1i−1[aj>B]⌊aiaj⌋aja_i \times \left(\sum_{j=1}^{i-1} [a_j>B]\right)-\sum_{j=1}^{i-1} [a_j>B] \left \lfloor \frac {a_i} {a_j} \right \rfloor a_jai×(j=1i1[aj>B])j=1i1[aj>B]ajaiaj

cntjcnt_jcntj 表示,此前有多少个位置的 aaajjj。同时维护一个 cnt_big\text{cnt\_big}cnt_big,表示此前大数的数量。那么,式子可以被化为

cnt_big×ai−∑j=1mcntj⌊aij⌋\text{cnt\_big} \times a_i-\sum_{j=1}^m cnt_j \left \lfloor \frac {a_i} {j} \right \rfloorcnt_big×aij=1mcntjjai

对于前半部分,直接计算。
对于后半部分,暴力整除分块,对于每一个区间(块)[l,r][l,r][l,r],查询区间内 cntcntcnt 和。

注意到,这是一个 nnn 次单点修改,nmn \sqrt mnm 次区间查询的问题,也可以通过根号平衡做到 O(nm)O(n \sqrt m)O(nm)


不妨设 n,mn,mn,m 同级,则总复杂度 O(Bn+n2B)O(Bn+\frac {n^2} {B})O(Bn+Bn2),取 B=nB=\sqrt nB=n 时最优,O(nn)O(n \sqrt n)O(nn) 可以通过加强版。

Code

//根号分治做法
#include <bits/stdc++.h>
#pragma GCC optimize(2)
#define rg register
#define ll long long
using namespace std;
const int maxl=300005;

int read(){
	int s=0,w=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-')  w=-w;ch=getchar();}
	while (ch>='0'&&ch<='9'){s=s*10+(ch^'0');ch=getchar();}
	return s*w;
}
int m,n,B,blo,pre_big;
int a[maxl],cnt[maxl],bl[maxl];
ll ans[maxl],cf[maxl],sum0[maxl],sum1[maxl],sum0_bl[maxl],sum1_bl[maxl];
vector<int> ve[maxl];

void change0(int rt,ll num){	
	int le=(bl[rt]-1)*blo+1,ri=rt;
	for (int i=le;i<=ri;++i)  sum0[i]+=num;
	
	le=1,ri=bl[rt]-1;
	for (int i=le;i<=ri;++i)  sum0_bl[i]+=num;
}

void change1(int rt,ll num){
	int le=(bl[rt]-1)*blo+1,ri=rt;
	for (int i=le;i<=ri;++i)  sum1[i]+=num;
	
	le=1,ri=bl[rt]-1;
	for (int i=le;i<=ri;++i)  sum1_bl[i]+=num;
}
ll Query0(int l,int r){return sum0[l]+sum0_bl[bl[l]]-sum0[r+1]-sum0_bl[bl[r+1]];}
ll Query1(int l,int r){return sum1[l]+sum1_bl[bl[l]]-sum1[r+1]-sum1_bl[bl[r+1]];} 

signed main(){
	n=read();
	for (int i=1;i<=n;i++){
		a[i]=read();
		m=max(m,a[i]);
		ve[a[i]].push_back(i);
	}
	B=sqrt(m)/4,blo=sqrt(m);
	for (int i=1;i<=m+1;i++)  bl[i]=(i-1)/blo+1;
	for (int i=1;i<=B;i++){
		if (!ve[i].size())  continue;
		
		for (int j=0;j<(int)ve[i].size();j++)  cnt[ve[i][j]]++;
		for (int j=1;j<=m;j++)  cnt[j]=cnt[j-1]+cnt[j];
		for (rg int j=1;j<=B;++j){
			if (i==j)  continue;
			
			int tmp=(i%j)+(j%i);
			for (int k=0;k<(int)ve[j].size();++k){
				int now=ve[j][k];
				cf[now]+=tmp;
			}
		}
		for (rg int j=B+1;j<=m;++j){
			int tmp=(i%j)+(j%i);
			for (int k=0;k<(int)ve[j].size();++k){
				int now=ve[j][k];
				ans[now]+=(ll)cnt[now]*tmp,cf[now]+=tmp;
			}
		}
		for (int j=1;j<=m;++j)  cf[j]=cf[j-1]+cf[j];
		for (rg int j=0;j<(int)ve[i].size();++j)  ans[ve[i][j]]+=cf[ve[i][j]];
		memset(cf,0,sizeof(cf));
		memset(cnt,0,sizeof(cnt));
	}
	for (int i=1;i<=n;i++){
		if (a[i]<=B)  continue;
		
		ll cur=0;
		for (int j=0;j<=m;j+=a[i]){
			int l=max(j,1),r=min(j+a[i]-1,m);
			
			cur+=Query1(l,r);
			if (j)  cur-=(ll)Query0(l,r)*j;
		}
		change0(a[i],1),change1(a[i],a[i]),ans[i]+=cur;
	}
	for (int i=1;i<=m+1;i++)  sum0[i]=sum1[i]=sum0_bl[i]=sum1_bl[i]=0;
	for (int i=1;i<=n;i++){
		if (a[i]<=B)  continue;
		
		ll now=(ll)a[i]*pre_big,res=0;
		for (int l=1,r;l<=a[i];l++){
			r=a[i]/(a[i]/l);
			res+=(ll)Query0(l,r)*(a[i]/l);
			l=r;
		}
		ans[i]+=now-res,pre_big++,change0(a[i],a[i]);
	}
	for (int i=1;i<=n;i++)  ans[i]+=ans[i-1];
	for (int i=1;i<=n;i++)  printf("%lld ",ans[i]);
	puts("");
	
	return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值