这是一篇豪夺洛谷最优解榜倒数 rnk 1 的题解。所以我为啥还有脸写
Description
给定序列 a a a。
令
t i = ∑ j = 1 i ∑ k = 1 i a j mod a k t_i=\sum_{j=1}^i \sum_{k=1}^i a_j \ \text{mod} \ a_k ti=j=1∑ik=1∑iaj mod ak
你需要分别求出 t 1 , t 2 , ⋯ , t n t_1,t_2,\cdots,t_n t1,t2,⋯,tn。
1 ≤ n ≤ 2 × 1 0 5 , 1 ≤ a i ≤ 3 × 1 0 5 1 \le n \le 2 \times 10^5,1 \le a_i \le 3 \times 10^5 1≤n≤2×105,1≤ai≤3×105
Solution
算法一
令
f
i
=
∑
j
<
i
(
a
i
m
o
d
a
j
)
f_i=\sum_{j<i} (a_i \mod a_j)
fi=j<i∑(aimodaj)
g i = ∑ j < i ( a j m o d a i ) g_i=\sum_{j<i} (a_j \mod a_i) gi=j<i∑(ajmodai)
则第 i i i 个答案为
∑ j = 1 i f j + ∑ j = 1 i g j \sum_{j=1}^i f_j+\sum_{j=1}^i g_j j=1∑ifj+j=1∑igj
关键在于求出每一个 f i , g i f_i,g_i fi,gi。
Part 1: 求 f
根据 x m o d y = x − ⌊ x y ⌋ y x \mod y=x-\lfloor \frac x y \rfloor y xmody=x−⌊yx⌋y,有
f i = a i ( i − 1 ) ∑ j < i ⌊ a i a j ⌋ a j f_i=a_i(i-1)\sum_{j<i} \left \lfloor \frac {a_i} {a_j} \right \rfloor a_j fi=ai(i−1)j<i∑⌊ajai⌋aj
考虑枚举 j j j。
对于每一个这样的 j j j,枚举值域区间 [ 0 , a j − 1 ] [ a j , 2 a j − 1 ] ⋯ [0,a_j-1][a_j,2a_j-1]\cdots [0,aj−1][aj,2aj−1]⋯,那么 j j j 会向所有满足 a i ∈ [ l , r ] a_i \in [l,r] ai∈[l,r] 且 i > j i>j i>j 的 f i f_i fi 产生贡献。
我们在 j + 1 j+1 j+1 处打上标记 ( r , 1 ) ( l − 1 , − 1 ) (r,1)(l-1,-1) (r,1)(l−1,−1),其中在 x x x 处的标记 ( p , △ ) (p,\triangle) (p,△) 表示,对于所有在 x x x 之后且权值不小于 p p p 的 f i f_i fi 都要加上 △ \triangle △。
从左往右扫描,用树状数组维护即可。时间复杂度 O ( m ln m log n ) O(m \ln m \log n) O(mlnmlogn)。其中 m m m 表示所有 a i a_i ai 的最大值。
Part 2: 求 g
同理,有
g i = ∑ j < i a j − ∑ j < i ⌊ a j a i ⌋ a i g_i=\sum_{j<i} a_j-\sum_{j<i} \left \lfloor \frac {a_j} {a_i} \right \rfloor a_i gi=j<i∑aj−j<i∑⌊aiaj⌋ai
枚举 [ 0 , a i − 1 ] [ a i , 2 a i − 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 + m ln m log n ) O(n+m \ln m \log n) O(n+mlnmlogn),本题被解决。
算法二(加强版: a i a_i ai 可能相同)
考虑根号分治。
我们将数分为两类——第一类数不超过 B B B,称它们为 ⌈ \lceil ⌈ 小数 ⌋ \rfloor ⌋;第二类数超过 B B B,称它们为 ⌈ \lceil ⌈ 大数 ⌋ \rfloor ⌋。小数的性质是值域较小,大数的性质是倍数较少。
Part 1
先考虑小数。为了利用它们值域小的性质,我们开一个桶,其中第 i i i 个桶 V i V_i Vi 里面装了所有值为 i i i 的数的位置。枚举编号在 [ 1 , B ] [1,B] [1,B] 中的桶,并考虑计算出它们对序列 t t t 的贡献。
令当前需要计算贡献的桶是第 x ( x ≤ B ) x(x \le B) x(x≤B) 个。由于模运算需要两个数,所以还要再枚举一个桶,令这是第 y y y 个桶。显然, ∀ i ∈ V x , j ∈ V y \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 y x \ \text{mod} \ y x mod y 的贡献。
为方便进一步的推导,考虑将 max ( i , j ) \max(i,j) max(i,j) 的 max \max max 去掉,即分类地钦定 i , j i,j i,j 之间的大小关系。我们先预处理出一个序列 C C C,其中第 k k k 位上的数是 V i V_i Vi 中不超过 k k k 的数的个数,那么对于某个属于 V y V_y Vy 的 j j j,它作为 max ( i , j ) \max(i,j) max(i,j) 的次数为 C j C_j Cj 次,其中 m m m 表示所有 a i a_i ai 的最大值,可以直接在 t t t 上差分。但是,若 j = min ( i , j ) j=\min(i,j) j=min(i,j),那么应该在 i i i 处差分地修改,但 i i i 我们并不清楚。这该怎么办呢?
考虑维护另一个数组 D D D,其中 D i D_i Di 表示,当 i i i 作为 max ( i , j ) \max(i,j) max(i,j)(即 j j j 作为 min ( i , j ) \min(i,j) min(i,j))时的总贡献。这个数组可以采用下面的方法求出——对于每个属于其他桶的 j j j,在 D j D_j Dj 处差分地加上 i mod j i \ \text{mod} \ j i mod j 以及 j mod i j \ \text{mod} \ i j mod i。在枚举完毕后,将 D D D 求一遍前缀和得到真实的 D D D,再去逐一向 t t t 差分地贡献。
总结一下 ⌈ \lceil ⌈ 小数 ⌋ \rfloor ⌋ 部分的处理方式:
- 建立一个桶;
- 扫描桶
[
1
,
B
]
[1,B]
[1,B]:
- 通过前缀和技巧求出 C C C 数组;
- 枚举桶
[
1
,
m
]
[1,m]
[1,m] 以及其中的数
j
j
j,分类讨论
j
j
j 为
i
,
j
i,j
i,j 中的较大者以及较小者两种情况:
- 作为较大者: 直接在 t t t 上差分地修改。
- 作为较小者: 在 D D D 上差分地修改。
- 做前缀和得到真实的 D D D,并在 t t t 上差分地修改,解决 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 逐渐增大的时候,其对大数取模的值的极大连续段的数量较少。例如,这个大数是 1 0 4 10^4 104,那么当 num \text{num} num 从 1 1 1 缓慢增加到 1 0 5 10^5 105 的过程中,模数会从 0 0 0 变化到 1 0 4 − 1 10^4-1 104−1,然后又回到 0 0 0 再变成 1 0 4 − 1 10^4-1 104−1,而这样变化的轮数的只有 O ( m B ) O(\frac m B) O(Bm)。
直接在原序列上扫描。
令当前看到的数是一个大数,且其值为 c c c。那么,我们需要查询 ∑ i = 0 c − 1 i × c n t i \sum_{i=0}^{c-1} i \times cnt_i i=0∑c−1i×cnti ∑ i = c 2 c − 1 ( i − c ) × c n t i \sum_{i=c}^{2c-1} (i-c) \times cnt_i i=c∑2c−1(i−c)×cnti ⋯ ⋯ ⋯ ⋯ \cdots \cdots \cdots \cdots ⋯⋯⋯⋯
其中 c n t i cnt_i cnti 表示目前 i i i 出现的次数。
于是,问题变为了一道纯粹的操作、查询类题目。
- 给定一个长度为 n n n 的序列 c n t cnt cnt。
- 你需要支持单点修改,查询区间 i × c n t i i \times cnt_i i×cnti 的和,查询区间 c n t i cnt_i cnti 的和。
若直接采用树状数组维护,那么复杂度是 O ( n m B log m ) O(n \frac {m} {B} \log m) O(nBmlogm),难以接受。
注意到,查询次数为 n × m B n \times \frac m {B} n×Bm 次,操作次数只有 n n n 次,进行根号平衡即可。复杂度为 O ( n m B ) O(n \frac m B) O(nBm)。
Part 2.2: 向别人取模
先将 x m o d y x \mod y xmody 变为 x − ⌊ x y ⌋ y x-\lfloor \frac x y \rfloor y x−⌊yx⌋y。那么,等价于对于每个满足 a i > B a_i>B ai>B 的 i i i,求出
a i × ( ∑ j = 1 i − 1 [ a j > B ] ) − ∑ j = 1 i − 1 [ a j > B ] ⌊ a i a j ⌋ a j a_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_j ai×(j=1∑i−1[aj>B])−j=1∑i−1[aj>B]⌊ajai⌋aj
令 c n t j cnt_j cntj 表示,此前有多少个位置的 a a a 为 j j j。同时维护一个 cnt_big \text{cnt\_big} cnt_big,表示此前大数的数量。那么,式子可以被化为
cnt_big × a i − ∑ j = 1 m c n t j ⌊ a i j ⌋ \text{cnt\_big} \times a_i-\sum_{j=1}^m cnt_j \left \lfloor \frac {a_i} {j} \right \rfloor cnt_big×ai−j=1∑mcntj⌊jai⌋
对于前半部分,直接计算。
对于后半部分,暴力整除分块,对于每一个区间(块)
[
l
,
r
]
[l,r]
[l,r],查询区间内
c
n
t
cnt
cnt 和。
注意到,这是一个 n n n 次单点修改, n m n \sqrt m nm 次区间查询的问题,也可以通过根号平衡做到 O ( n m ) O(n \sqrt m) O(nm)。
不妨设 n , m n,m n,m 同级,则总复杂度 O ( B n + n 2 B ) O(Bn+\frac {n^2} {B}) O(Bn+Bn2),取 B = n B=\sqrt n B=n 时最优, O ( n n ) 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;
}