整除分块讲解

引入

一般一个算法的引入都是为了解决一类问题,那么这个问题是什么呢?
求解 i = 1 i = n n / i \sum_{i=1}^{i=n}n/i
式子很简单,可以直接 O ( n ) O(n) 求但是如果n为1e9甚至1e14呢,这个时候就要引入整除分块的概念
其实我个人认为这个算法不能算是一种高深算法,不要掌握什么其他知识就能学,就类似于贪心算法
首先我们可以先写一个暴力算法(任何算法几乎都是暴力引申的)

n=20;
    for(int i=1;i<=n;i++){
        printf("%d ",n/i);
    }

得出的答案是20 10 6 5 4 3 2 2 2 2 1 1 1 1 1 1 1 1 1 1
你发现了相同数是连续一段的(显然)
那么其实你就可以把这些数划分成若干段,每一段中的n/i的值都相同
而n/i的个数是 n \sqrt{n} 级别的,那么时间复杂度就可以优化到 O ( n ) O(\sqrt{n})

如何确定每一段的左右边界呢,其实很简单,假设左边界是l,那么右边界显然就是 n n l \frac{n}{\frac{n}{l}} ,而下一个左边界显然是上一个右边界加1,最开始的左边界是1

代码如下

 for(int l=1,r;l<=n;l=r+1){
 	r=min(n,n/(n/l));//注意min
	ans+=(r-l+1)*(n/l);
 }

模板题

题目大意

题目定义f(n)为n的约数的个数,要求 i = 1 i = n f ( i ) \sum_{i=1}^{i=n}f(i) 显然可以直接等价于 i = 1 i = n n / i \sum_{i=1}^{i=n}n/i ,然后直接整除分块解决

代码

#include<set>
#include<map>
#include<queue>
#include<stack>
#include<cmath>
#include<cstdio>
#include<vector>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_map>
#define fi first
#define se second
#define debug printf(" I am here\n");
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int maxn=1e5+5,inf=0x3f3f3f3f,mod=19940417;
const double eps=1e-10;
int n;
ll ans=0;
signed main(){
    scanf("%d",&n);
    for(int l=1,r;l<=n;l=r+1){
        r=min(n,n/(n/l));//注意min
        ans+=1ll*(r-l+1)*(n/l);
    }
    printf("%lld\n",ans);
    return 0;
}

例题1链接

题目大意

要你求 G ( n , k ) = i = 1 i = n k m o d i G(n,k)=\sum_{i=1}^{i=n} k mod i

题目思路

这个题目和整除分块不同的就是从整除变成模了,这时你要想到转化为整除,a%b=a-a/b*b。
那么式子就可以转化为
i = 1 i = n k m o d i = = i = 1 i = n k k / i i = = n k i = 1 i = n i ( k / i ) \sum_{i=1}^{i=n} k mod i==\sum_{i=1}^{i=n} k-k/i*i==n*k-\sum_{i=1}^{i=n} i*(k/i)
这样不就是类似于整除分块嘛,然而里面有一个i怎么办,等差求和 ( l + r ) ( r l + 1 ) / 2 (l+r)*(r-l+1)/2

代码

#include<set>
#include<map>
#include<queue>
#include<stack>
#include<cmath>
#include<cstdio>
#include<vector>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_map>
#define fi first
#define se second
#define debug printf(" I am here\n");
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int maxn=1e5+5,inf=0x3f3f3f3f,mod=998244353;
const double eps=1e-10;
ll n,k;
ll ans=0;
signed main(){
    scanf("%lld%lld",&n,&k);
    ans=n*k;
    for(int l=1,r;l<=min(n,k);l=r+1){
        r=min(n,k/(k/l));
        ans-=1ll*(l+r)*(r-l+1)/2*(k/l);
    }
    printf("%lld\n",ans);
    return 0;
}

例题2链接

题目大意

在这里插入图片描述

题目思路

这个题目的关键点就是要明白f(x)的定义qwq,我太菜了没看出来,其实f(x)就是代表x的约数的个数,这个好像叫做因数个数定理,这个证明是很显然的qwq,那接下来不就简单了嘛,看到这类计算[l,r]的和的题目很容易想到容斥,计算[1,r]的和减去[1,l-1]的和
然后不就是整除分块的模板了嘛

代码

#include<set>
#include<map>
#include<queue>
#include<stack>
#include<cmath>
#include<cstdio>
#include<vector>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_map>
#define fi first
#define se second
#define debug printf(" I am here\n");
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int maxn=1e5+5,inf=0x3f3f3f3f,mod=998244353;
const double eps=1e-10;
ll l,r;
ll work(ll x){
    ll ans=0;
    for(ll l=1,r;l<=x;l=r+1){
        r=min(x,x/(x/l));
        ans=(ans+(r-l+1)*(x/l))%mod;
    }
    return ans;
}
signed main(){
    scanf("%lld%lld",&l,&r);
    ll ans=((work(r)-work(l-1))%mod+mod)%mod;
    printf("%lld\n",ans);
    return 0;
}

题目链接

题目大意

在这里插入图片描述

题目思路

要是没有i!=j,那不就马上变成了第一个例题的两次乘积就行了嘛,然而这样就要容斥一下
i = 1 i = n j = 1 j = m ( n m o d i ) ( m m o d j ) i = 1 i = min ( m , n ) ( n m o d i ) ( m m o d i ) \sum_{i=1}^{i=n}\sum_{j=1}^{j=m}(n mod i)*(m mod j)-\sum_{i=1}^{i=\min(m,n)}(n mod i)*(m mod i)
主要考虑第二个式子如何化简
i = 1 i = min ( m , n ) ( n m o d i ) ( m m o d i ) = i = 1 i = min ( m , n ) ( n i ( n / i ) ) ( m i ( m / i ) ) = i = 1 i = min ( m , n ) ( n m n i ( m / i ) m i ( n / i ) + i i ( n / i ) ( m / i ) ) \sum_{i=1}^{i=\min(m,n)}(n mod i)*(m mod i)=\sum_{i=1}^{i=\min(m,n)}(n-i*(n/i))*(m-i*(m/i))\\ =\sum_{i=1}^{i=\min(m,n)}(n*m-n*i*(m/i)-m*i*(n/i)+i*i*(n/i)*(m/i))
显然前面三个式子可以直接用简单的整除分块,而第四个式子怎么求呢首先明白一个公式
i = 1 i = n i 2 = n ( n + 1 ) ( 2 n + 1 ) / 6 \sum_{i=1}^{i=n}i^2=n(n+1)(2n+1)/6
证明如下
在这里插入图片描述

然而你会发现既有n/i,又有m/i,那么该怎么整除分块呢,其实很简单,你要明白整除分块的含义就是每一段的除数相等,那么令 r = min ( l i m , n / ( n / l ) , m / ( m / l ) ) r=\min(lim,n/(n/l),m/(m/l)) 这样不就行了嘛,那么就可以做了

易错警示

1:如果你要求abc/d在mod p的情况下,如果abc爆了ll,那么d就要用逆元表示(如果范围允许__int128也可以)这个cal函数显然就要用逆元表示了。
2:求逆元一定要注意模数是否为质数,如果不为质数就不能用快速幂,而要用扩欧来求,这个题目的模数就不是质数,所以你要以用扩偶来求

代码

#include<set>
#include<map>
#include<queue>
#include<stack>
#include<cmath>
#include<cstdio>
#include<vector>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_map>
#define fi first
#define se second
#define debug printf(" I am here\n");
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int maxn=1e5+5,inf=0x3f3f3f3f,mod=19940417;
const double eps=1e-10;
ll inv6=3323403,inv2=9970209;
ll n,m;
ll cal(ll x){
    return x*(x+1)%mod*(2*x+1)%mod*inv6%mod;
}
ll work1(ll x,ll lim){
    ll sum=lim*x%mod;
    for(ll l=1,r;l<=lim;l=r+1){
        r=min(lim,x/(x/l));
        sum=((sum-(r+l)*(r-l+1)%mod*inv2%mod*(x/l))%mod+mod)%mod;
    }
    return sum;
}
ll work2(ll x,ll y){
    ll lim=min(x,y),sum=0;
    for(ll l=1,r;l<=lim;l=r+1){
        r=min(lim,min(x/(x/l),y/(y/l)));
        sum=(sum+x*y%mod*(r-l+1))%mod;
        sum=(sum-x*(r-l+1)%mod*(l+r)%mod*inv2%mod*(y/l)-y*(r-l+1)%mod*(l+r)%mod*inv2%mod*(x/l))%mod;
        sum=((sum+(cal(r)-cal(l-1))%mod*(x/l)%mod*(y/l))%mod+mod)%mod;
    }
    return sum;
}
signed main(){
    scanf("%lld%lld",&n,&m);
    ll ans=work1(n,n)*work1(m,m)%mod;
    ans=((ans-work2(n,m))%mod+mod)%mod;
    printf("%lld\n",ans);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/m0_46209312/article/details/108012057
今日推荐