这是一篇豪夺洛谷最优解榜倒数 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=1∑ik=1∑iaj 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^51≤n≤2×105,1≤ai≤3×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=1∑ifj+j=1∑igj
关键在于求出每一个 fi,gif_i,g_ifi,gi。
Part 1: 求 f
根据 xmod y=x−⌊xy⌋yx \mod y=x-\lfloor \frac x y \rfloor yxmody=x−⌊yx⌋y,有
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(i−1)j<i∑⌊ajai⌋aj
考虑枚举 jjj。
对于每一个这样的 jjj,枚举值域区间 [0,aj−1][aj,2aj−1]⋯[0,a_j-1][a_j,2a_j-1]\cdots[0,aj−1][aj,2aj−1]⋯,那么 jjj 会向所有满足 ai∈[l,r]a_i \in [l,r]ai∈[l,r] 且 i>ji>ji>j 的 fif_ifi 产生贡献。
我们在 j+1j+1j+1 处打上标记 (r,1)(l−1,−1)(r,1)(l-1,-1)(r,1)(l−1,−1),其中在 xxx 处的标记 (p,△)(p,\triangle)(p,△) 表示,对于所有在 xxx 之后且权值不小于 ppp 的 fif_ifi 都要加上 △\triangle△。
从左往右扫描,用树状数组维护即可。时间复杂度 O(mlnmlogn)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<i∑aj−j<i∑⌊aiaj⌋ai
枚举 [0,ai−1][ai,2ai−1],⋯[0,a_i-1][a_i,2a_i-1],\cdots[0,ai−1][ai,2ai−1],⋯ 并查询 [1,i)[1,i)[1,i) 中有多少个在该区间中的数。也可以使用树状数组维护。
总复杂度 O(n+mlnmlogn)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(x≤B) 个。由于模运算需要两个数,所以还要再枚举一个桶,令这是第 yyy 个桶。显然,∀i∈Vx,j∈Vy\forall i \in V_x,j \in V_y∀i∈Vx,j∈Vy,它们都对 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_yVy 的 jjj,它作为 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,分类讨论 jjj 为 i,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}num 从 111 缓慢增加到 10510^5105 的过程中,模数会从 000 变化到 104−110^4-1104−1,然后又回到 000 再变成 104−110^4-1104−1,而这样变化的轮数的只有 O(mB)O(\frac m B)O(Bm)。
直接在原序列上扫描。
令当前看到的数是一个大数,且其值为 ccc。那么,我们需要查询 ∑i=0c−1i×cnti\sum_{i=0}^{c-1} i \times cnt_ii=0∑c−1i×cnti∑i=c2c−1(i−c)×cnti\sum_{i=c}^{2c-1} (i-c) \times cnt_ii=c∑2c−1(i−c)×cnti⋯⋯⋯⋯\cdots \cdots \cdots \cdots⋯⋯⋯⋯
其中 cnticnt_icnti 表示目前 iii 出现的次数。
于是,问题变为了一道纯粹的操作、查询类题目。
- 给定一个长度为 nnn 的序列 cntcntcnt。
- 你需要支持单点修改,查询区间 i×cntii \times cnt_ii×cnti 的和,查询区间 cnticnt_icnti 的和。
若直接采用树状数组维护,那么复杂度是 O(nmBlogm)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 yx−⌊yx⌋y。那么,等价于对于每个满足 ai>Ba_i>Bai>B 的 iii,求出
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=1∑i−1[aj>B])−j=1∑i−1[aj>B]⌊ajai⌋aj
令 cntjcnt_jcntj 表示,此前有多少个位置的 aaa 为 jjj。同时维护一个 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×ai−j=1∑mcntj⌊jai⌋
对于前半部分,直接计算。
对于后半部分,暴力整除分块,对于每一个区间(块)[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;
}

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

被折叠的 条评论
为什么被折叠?



