数论分块
什么是数论分块?
我们要给幼儿园的小朋友分苹果(平均分,多余的自己留着吃):
- 如果小朋友很少,每个人都能分到很多苹果。
- 但是如果小朋友很多,每个人都只能分到很少的苹果。
数论分块其实就是发现:
对于一些不同的人数,每个小朋友分到的苹果数是一样的!
它有什么用呢?
题目描述:求出
普通思路
直接暴力:
-
\frac{100}{1} = 100 -
\frac{100}{2} = 50 -
\cdots -
\frac{100}{100} = 1
总运行次数:
数论分块
我们可以找到规律(关键点在后面):
-
-
-
\cdots -
-
总运行次数:
乍一看是不是差距不大?但如果是计算
简单例题
题目描述:输入
我们可以以
| 1 | 10 |
| 2 | 5 |
| 3 | 3 |
| 4 | 2 |
| 5 | 2 |
| 6 | 1 |
| 7 | 1 |
| 8 | 1 |
| 9 | 1 |
| 10 | 1 |
我们可以注意到,一些相同的值会连续出现好几次,特别是
计算步骤
-
确定当前块的起点
l ,把l 初始化为1 。 -
计算这个块的值:
\lfloor \frac{n}{l} \rfloor ,记作num 。 -
找到这个块的终点:即最大的
r 使得\lfloor \frac{n}{r} \rfloor = num ,数学推导得r = \lfloor \frac{n}{num} \rfloor 。具体推导过程如下:- 我们要找到最大
r 使得num \le \frac{n}{r} \lt num + 1 - 变形得
\frac{n}{num+1} \lt r \le \frac{n}{num} 。 - 因为
r 是整数,所以r 就是\lfloor \frac{n}{num} \rfloor 。
- 我们要找到最大
-
计算这个块对答案的贡献:
(r - l + 1) \times num 。 -
跳到下一块:让
l 变成r + 1 。如果l \gt n ,那么结束循环,输出答案。代码
#include<bits/stdc++.h>
using namespace std;
int n,l,r,ans;
int main()
{
scanf("%d",&n);
for(l=1;l<=n;l=r+1)
{
int num=n/l;
r=n/num;
ans+=(r-l+1)*num;
}
printf("%d",ans);
return 0;
}
时间复杂度分析
-
- 对于每一个 $i \le \sqrt{n}$,$\lfloor \frac{n}{i} \rfloor$ 最多有 $\sqrt{n}$ 个取值,因为 $i$ 就有 $\sqrt{n}$ 个取值。 - 对于每一个 $i \gt \sqrt{n}$,$\lfloor \frac{n}{i} \rfloor$ 最多有 $\sqrt{n}$ 个取值,因为结果肯定 $\ge 1$ 并且 $\le \sqrt{n}$。
所以数论分块的时间复杂度是
余数求和
我们看一道题目:P2261。我们知道:
所以题目就变成了求:
把
其中:
可以用数论分块求出来,剩下的就简单了。
代码
#include<bits/stdc++.h>
#define int long long//开long long
using namespace std;
int n,k,l,r,ans;
signed main()
{
scanf("%lld%lld",&n,&k);
for(l=1;l<=n;l=r+1)
{
int num=k/l;
if(num==0)
break;
r=min(k/num,n);
ans+=((r-l+1)*(l+r)/2)*num;//这里不是区间长度了,而是l一直加到r,可以用等差数列求和公式
}
printf("%lld",n*k-ans);
return 0;
}
高级用法
向上取整
输入
我们知道:
然后我们就可以正常数论分块了,但是要注意,在
上代码
#include<bits/stdc++.h>
using namespace std;
int n,l,r,ans=0;
int main()
{
scanf("%d",&n);
for(l=1;l<=n;l=r+1)
{
int num=(n-1)/l;
if(l!=n)
r=min((n-1)/num,n);
else
r=n;
ans+=(num+1)*(r-l+1);
}
printf("%d",ans);
return 0;
}
例题
看看我们的例题,求约数和的题。我们可以设
仔细一想就会发现我们可以枚举每一个可能的因数
我们的最终答案就是
上代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int l,r,ans;
int calc(int x)
{
ans=0;
for(l=1;l<=x;l=r+1)
{
int num=x/l;
r=x/num;
ans+=(l+r)*(r-l+1)/2*num;
}
return ans;
}
int x,y;
signed main()
{
scanf("%lld%lld",&x,&y);
printf("%lld",calc(y)-calc(x-1));
return 0;
}
我们来看第二道题。乍一看很难,但其实跟上一题很类似。首先,根据因数个数定理,可以知道
那么我们同样的设
我们可以用前缀和的想法,我们的最终答案就是
上代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=998244353;
int ans;
int calc(int x)
{
ans=0;
for(int l=1,r;l<=x;l=r+1)
{
int num=x/l;
r=x/num;
ans+=(r-l+1)*num;
}
return ans;
}
int l,r;
signed main()
{
scanf("%lld%lld",&l,&r);
printf("%lld",(calc(r)-calc(l-1))%mod);
return 0;
}
给大家补充个 二维数论分块。
我们在二维数论分块中什么叫做一个块呢?我们在这里把
- 求解
num :这个很好改,就是从\lfloor \frac{n}{i} \rfloor 改成\lfloor \frac{n}{i} \rfloor \times \lfloor \frac{m}{i} \rfloor 。 -
求解块的终点
r :- 如果只需
\lfloor \frac{n}{i} \rfloor 一样,那么这个块的右端点就是\lfloor \frac{n}{\lfloor \frac{n}{i} \rfloor} \rfloor ,我们记作r_1 。 - 同理,如果只需
\lfloor \frac{m}{i} \rfloor 一样,那么这个块的右端点就是\lfloor \frac{m}{\lfloor \frac{m}{i} \rfloor} \rfloor ,我们记作r_2 。 - 由于我们需要两个值都一样,那么很好想到,右端点就是
\min(r_1,r_2) 。上代码
- 如果只需
#include<bits/stdc++.h>
#define int long long//不开 long long 见祖宗
using namespace std;
int n,m,l,r,t,ans;
signed main()
{
scanf("%d",&t);
while(t--)
{
ans=0;//不要忘记初始化
scanf("%lld%lld",&n,&m);
for(l=1;l<=min(n,m);l=r+1)
{
int num=(n/l)*(m/l);
int r1=n/(n/l);//只看n/l
int r2=m/(m/l);//只看m/l
r=min(r1,r2);
r=min(r,n);//防止超过n,不然后面更新答案会有问题
ans+=(r-l+1)*num;
}
printf("%lld\n",ans);
}
return 0;
}
数论分块虽然刚开始觉得难,但是做了几道例题后就能大概熟悉它的套路了。