杜教筛

前置知识:莫比乌斯反演

杜教筛

杜教筛可以在非线性时间内求积性函数前缀和。

P4213 【模板】杜教筛(Sum)

我们假设要求前缀和的积性函数为f(n),设前缀和S(n)=i=1nf(i)

我们找一个合适的函数g(n),我们看看f,g两个函数的狄利克雷卷积的前缀和。

i=1n(fg)(i)

=i=1nd|if(id)g(d)

先枚举d再枚举i

d=1nd|i&inf(id)g(d)

i除以d

d=1ni=1ndf(i)g(d)

提出g(d)

d=1ng(d)i=1ndf(i)=d=1ng(d)S(nd)

也就是说i=1n(fg)(i)=i=1ng(i)S(ni)

再看g(1)S(n)的值:

g(1)S(n)=i=1ng(i)S(ni)i=2ng(i)S(ni)=i=1n(fg)(i)i=2ng(i)S(ni)

只要我们找到g使得可以O(1)算出fg的前缀和、g的前缀和,我们靠这个式子也就可以递归。

递归前,我们可以预处理n23内的前缀和,再用unordered_map记忆化,就能达到O(n23)的复杂度。

注:unordered_mapC++11的STL

本题,当fμ时,找的gI,则fg=ϵ,g(1)=1

fφ时,找的gI,则fg=id,g(1)=1

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int MAXN=5000005;
bool isp[MAXN+1];
int phi[MAXN+1],mu[MAXN+1],pcnt,n,prime[MAXN+1];
ll ans,smu[MAXN+1],sphi[MAXN+1];
unordered_map<int,ll>Sphi,Smu;
ll Get_smu(unsigned int n) {//mu
	if(n<=MAXN) return smu[n];
	if(Smu[n]) return Smu[n];
	ull ans=1ll;
	for(unsigned int l=2,r; l<=n; l=r+1) {//整除分块
		r=n/(n/l);
		ans-=(r-l+1ll)*Get_smu(n/l);//递归
	}
	return Smu[n]=ans;
} //mu*I=e
ll Get_sphi(unsigned int n) {//phi
	if(n<=MAXN)return sphi[n];
	if(Sphi[n]) return Sphi[n];
	ull ans=n*(n+1ll)>>1;
	for(unsigned int l=2,r; l<=n; l=r+1) {
		r=n/(n/l);
		ans-=(r-l+1ll)*Get_sphi(n/l);
	}
	return Sphi[n]=ans;
}//phi*I=id
int main() {
	sphi[1]=smu[1]=mu[1]=phi[1]=1;
	for(int i=2; i<=MAXN; ++i) {//筛出一些mu,phi并计算前缀和
		if(!isp[i]) {
			prime[++pcnt]=i;
			mu[i]=-1;
			phi[i]=i-1;
		}
		for(int j=1,x; j<=pcnt&&(x=i*prime[j])<=MAXN; ++j) {
			isp[x]=1;
			if(i%prime[j]) {
				mu[x]=-mu[i];
				phi[x]=phi[i]*(prime[j]-1);
			} else {
				mu[x]=0;
				phi[x]=phi[i]*prime[j];
				break;
			}
		}
		sphi[i]=sphi[i-1]+phi[i];
		smu[i]=smu[i-1]+mu[i];//预处理一些前缀和
	}
	int t,n;
	scanf("%d",&t);
	while(t--) {
		scanf("%d",&n);
		printf("%lld %lld\n",Get_sphi(n),Get_smu(n));
	}
	return 0;
}

一道题

P3768 简单的数学题

先用d枚举gcd(i,j)

d=1ni=1nj=1n[gcd(i,j)=d]ijd

i,j除以d

d=1ni=1ndj=1nd[gcd(i,j)=1](id)(jd)d

把三个d都提出来

d=1nd3i=1ndj=1ndij[gcd(i,j)=1]

再把ϵ=μI代入

d=1nd3i=1ndj=1ndijk|gcd(i,j)μ(k)

前面的题也做过,k|gcd(i,j)等价于k|i&k|j

d=1nd3i=1ndj=1ndijk|i&k|jμ(k)

k提前

d=1nd3k=1ndk|i&indk|j&jndijμ(k)

i,μ(k)提出

d=1nd3k=1ndμ(k)k|i&indik|j&jndj

后面的i,j都除以k

d=1nd3k=1ndμ(k)i=1ndkikj=1ndkjk

把两个k都提出

d=1nd3k=1ndμ(k)k2i=1ndkij=1ndkj

d乘进去,设S(n)=i=1ni

d=1nk=1ndd3k2μ(k)S2(ndk)

先枚举T=dk,再枚举d|Tk就是nd

T=1nd|TdT2μ(nd)S2(nT)

提出T2S2(nT)

T=1nT2S2(nT)d|Tdμ(nd)

后面的d|Tdμ(nd)就是idμ

Iφ=id莫比乌斯反演得idμ=φ

T=1nT2S2(nT)φ(T)=T=1nS2(nT)T2φ(T)

f(n)=T2φ(T),若能求出它的前缀和,就可以整出分块了。

考虑用杜教筛。

g(n)=n2,则fg=d|nd2φ(d)×(nd)2=n2d|nφ(d)

后面的部分就是φI,是等于id

fg=id3,前缀和为(n(n+1)2)2

而且g的前缀和为n(n+1)(2n+1)6,然后就可以杜教筛了。

注:代码中,我手写了哈希表。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN=5000005;
namespace Hash {//图版哈希表
	const int mo=233333;
	struct hash_table {
		int head[mo],htcnt;
		struct hashtable {
			ll id,val;
			int nxt;
		} ht[mo];
		void add(int u,ll v,ll w) {
			++htcnt;
			ht[htcnt].id=v;
			ht[htcnt].val=w;
			ht[htcnt].nxt=head[u];
			head[u]=htcnt;
		}
		void clr() {
			memset(head,0,sizeof(head));
			memset(&ht,0,sizeof(&ht));
			htcnt=0;
		}
		void insert(ll x,ll i) {
			int k=x%mo;
			add(k,x,i);
		}
		int query(ll x) {
			for(int i=head[x%mo]; i; i=ht[i].nxt)
				if(ht[i].id==x)
					return ht[i].val;
			return -1;
		}
	};
} using namespace Hash;
int prime[MAXN+1],pcnt,phi[MAXN+1],mod;
int sphi[MAXN+1],inv6;//inv6为6的逆元
hash_table Sphi;
ll n;
int Getsphi(ll m) {//查询
	if(m<=MAXN) return sphi[m];
	return Sphi.query(m);
}
int S(int n) {//1~n和
	return (n*(n+1ll)>>1)%mod;
}
int Sp(int n) {//g的前缀和
	return n*(n+1ll)%mod*(n<<1|1ll)%mod*inv6%mod;
}
void Get(ll m,ll d) {//杜教筛
	if(m<=MAXN||~Sphi.query(m))return ;//查询过就不管
	ll ans=S(m%mod);
	ans=ans*ans%mod;//f*g前缀和
	for(ll l=2,r; l<=m; l=r+1) {
		r=m/(m/l);
		Get(m/l,d*l);//先递归
		ans=(ans+mod-(Sp(r%mod)+0ll+mod-Sp((l-1)%mod))%mod*Getsphi(m/l)%mod)%mod;
	}
	Sphi.insert(m,ans);//记忆化
}
int Get_sphi(ll m) {
	Get(m,1);
	return Getsphi(m);//查询
}
int fastpow(int a,int k=mod-2) {//快速幂求逆元
	int base=1;
	while(k) {
		if(k&1)base=base*1ll*a%mod;
		a=a*1ll*a%mod;
		k>>=1;
	}
	return base;
}
int main() {
	scanf("%d%lld",&mod,&n);
	inv6=fastpow(6);
	sphi[1]=phi[1]=1;
	for(int i=2; i<=MAXN; ++i) {//线性筛前缀和
		if(!phi[i]) {
			prime[++pcnt]=i;
			phi[i]=i-1;
		}
		for(int j=1,x; j<=pcnt&&(x=i*prime[j])<=MAXN; ++j) {
			if(i%prime[j]) {
				phi[x]=phi[i]*(prime[j]-1);
			} else {
				phi[x]=phi[i]*prime[j];
				break;
			}
		}
		sphi[i]=(sphi[i-1]+phi[i]*1ll*i%mod*i)%mod;
	}
	ll tmp1,ans=0;
	for(ll l=1,r; l<=n; l=r+1) {//整除分块求答案
		r=n/(n/l);
		tmp1=S((n/l)%mod);
		tmp1=tmp1*tmp1%mod;
		ans=(ans+tmp1*(Get_sphi(r)+0ll+mod-Get_sphi(l-1))%mod)%mod;
	}
	printf("%lld\n",ans);
	return 0;
}
posted @   mod998244353  阅读(170)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
Live2D
欢迎阅读『杜教筛』
点击右上角即可分享
微信分享提示