整除分块入门(更新中)
【正文】
整除分块是个什么东西,他是一种用来高效求
\(\sum_{i=1}^n \lfloor \dfrac{n}{i} \rfloor\) 的方法。这个东西在莫比乌斯反演中会经常遇到,所以还是比较重要。学了这个东西你可以马上去学莫比乌斯反演。
运用整除分块我们可以高效的解决这个问题,时间复杂度为 \(\sqrt{n}\) 。
先举个例子吧,假设 \(n =25\) 。
那么式子所有的情况可以列出来如下: (下面表来源于 \(\text{cmd}\) 哥哥)
################################################## 25/1=25 *
######################## 25/2=12 *
################ 25/3=8 *
############ 25/4=6 *
########## 25/5=5 *
######## 25/6=4 *
###### 25/7=3 *
###### 25/8=3
#### 25/9=2 *
#### 25/10=2
#### 25/11=2
#### 25/12=2
## 25/13=1 *
## 25/14=1
## 25/15=1
## 25/16=1
## 25/17=1
## 25/18=1
## 25/19=1
## 25/20=1
## 25/21=1
## 25/22=1
## 25/23=1
## 25/24=1
## 25/25=1
按照取值分类发现有九种取值,把每一种取值看成一块,那么就有九块。
################################################## 25
----------
######################## 12
----------
################ 8
----------
############ 6
----------
########## 5
----------
######## 4
----------
###### 3*2
######
----------
#### 2*4
...
####
----------
## 1*12
...
##
----------
那么这么看起来,如果我们知道了可能的取值有几块,并且知道了每一块的块长,那么我们就可以很快的求得最终式子的答案。
假设,我们现在已经知道了一个块的左端点为 \(l\) ,且该块的值为 \(k\) 。如果我们求出了右端点 \(r\)。
那么这一块的贡献就为 \((r-l+1) \times k\)
考虑怎么求右端点 \(r\)
可以发现对于这个块中的元素,也即是 \(l \sim r\)间的元素。因为他们的值都为 \(k\) ,所以我们也就是说下面这个式子:
那么我们知道我们所求的 \(r\) 就是最大的 $ i$ 满足 \(ik\leq n\) ,此时的 \(i\) 也就是我们所求的 \(r\)
那么就是如下式子:
也就是说:
\(r= \lfloor \dfrac{n}{k} \rfloor = \lfloor \dfrac{n}{ \dfrac{n} {l}} \rfloor\)
然后你求完 \(r\) ,那么 \(r+1\) 就是新的左端点,然后就可以继续往下求。
根据他人博客知道总共的块值 只有 \(\sqrt{n}\) 种取值,又因为我们的时间复杂度是在依赖块值的。所以,这个东西的复杂度就为 \(\sqrt{n}\) 。
然后就可以做例题1。
例题题解:
首先对于式子 $ k \mod i$ 可以将其变为 $ k- i \dfrac{k}{i} $
那么我们要求的式子就变为了 $ \sum_{i=1}^n k- i \lfloor \dfrac{k}{i} \rfloor $ ,发现可以将一些 $ k$ 提取出来。
所以式子就变成了:
我们发现,里面的 \(\sum_{i=1}^n \lfloor \dfrac{k}{i} \rfloor\) 其实就是我们所讲的整除分块。然后就可以很快的完成计算,但是有个问题是,我们要求的是 \(\sum_{i=1}^n i \lfloor \dfrac{k}{i} \rfloor\) ,答案还需要乘上一个系数 \(i\) 。
容易发现,我们的 \(\lfloor \dfrac{k}{i} \rfloor\) 中在不同的 \(i\) 时的取值是有重复的(上面讲了)。
考虑求 \(\lfloor \dfrac{k}{i} \rfloor\) 相等的一块 \([l,r]\) 的贡献,也就是 \(\sum_{i=l}^r i \lfloor \dfrac{k}{i} \rfloor\) 。这个式子有一个更好看的形式,因为它的 \(\lfloor \dfrac{k}{i} \rfloor\) 相等。
所以,这一块的贡献可以写为:
容易发现前面是一坨等差数列,然后就可以用等差数列求和公式,或者说预处理前缀。但因为这里 \(n\) 较大,所以需要用公式。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,k;
int sum(int l,int r){
return (r-l+1)*(l+r)/2;
}
int calc(int n){
int ans=0;
for(int l=1,r;l<=n&&k/l;l=r+1){//k/l有可能为0,这样答案贡献为0,所以不用管
r=min(k/(k/l),n);
ans+=(k/l)*sum(l,r);
}
return ans;
}
signed main(){
cin>>n>>k;
cout<<n*k-calc(n);
return 0;
}
例题2
例题题解
发现,例题其实就是要我们求 \(\sum_{i=1}^bf(i)-\sum_{i=1}^{a-1}f(i)\)
显然的结论是 \(\sum_{i=1}^n f(i)\) 就是 \(\sum_{i=1}^n i \lfloor \dfrac{n}{i} \rfloor\)
这东西太熟悉了,就是我们上面的那道题的一个子问题,然后就直接整除分块过。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,k;
int sum(int l,int r){
return (r-l+1)*(l+r)/2;
}
int calc(int n){
int ans=0;
for(int l=1,r;l<=n&&k/l;l=r+1){//k/l有可能为0,这样答案贡献为0,所以不用管
r=min(k/(k/l),n);
ans+=(k/l)*sum(l,r);
}
return ans;
}
int x,y;
signed main(){
cin>>x>>y;
n=y;k=y;
int ans1=calc(n);
n=x-1;k=x-1;
int ans2=calc(n);
cout<<ans1-ans2;
return 0;
}
然后接下来,我们在来思考一些可能题目中会有的情况。
比如说,它如果不是向下取整,而是向上取整,那么我们该怎么办呢?
想一下,如果他问的是:
那么我们有没有什么办法去快速的求解呢?
我们可以改变一下成:
然后你就可以继续进行整除分块了。