Min 25筛入门

内容

好像是一种比较新的筛法,网上资料都是18年的
正式点好像就叫基于质因数分解的亚线性函数前缀和求法。
2020.4UPD:min25筛应用

用途

筛积性函数F的前缀和。(事实上不积性的也可以筛,只要是可以按质因子拆贡献的。)
要能构造一个完全积性函数 G G G,使得 G ( p ) = F ( p ) , p ∈ P r i m e G(p) = F(p),p \in Prime G(p)=F(p),pPrime

复杂度

据信,空间n^0.5,时间为 O ( n 0.75 / ln ⁡ n + n / p o l y ( ln ⁡ n ) ) O(n^{0.75} / \ln n+n/poly(\ln n)) O(n0.75/lnn+n/poly(lnn))
(但是实际上后面那一部分还跑的挺快的)
2s大概可以做两次1e10吧。

预处理 (模拟埃氏筛法)

下文中,记 R ( a ) R(a) R(a)表示a的最小质因子。
一个数x要么是质数,要么 R ( x ) < = x R(x)<=\sqrt x R(x)<=x ,这也是min25筛的精髓所在。他将所有数分成了两部分:质数和合数,而利用合数的上述性质,就可以快速统计答案。

首先,我们来求出所有质数的F和。
g ( i , j ) g(i,j) g(i,j)表示 ∑ F ( x ) , 满 足 x ∈ [ 2 , i ] , 且 x ∈ P r i m e 或 R ( x ) > P j \sum F(x), 满足x\in[2,i],且x\in Prime或R(x)>P_j F(x),x[2,i]xPrimeR(x)>Pj
Pj表示第j个质数。(即筛法进行到Pj时的状态)
直观地说,就是1…i做筛法筛去前j个质数后的状态。

其中j的取值只有 n \sqrt n n 个,按j分层,我们要求的是筛法进行完毕的状态 g ( i , M A X ) g(i,MAX) g(i,MAX)
初始状态 g [ i , 0 ] g[i,0] g[i,0]我们将所有数都视作质数来预处理,然后一个个质数去筛,将合数筛去,这样剩下的就是质数的F和。

容易推出g的递推式,用 g ( i , j − 1 ) g(i,j-1) g(i,j1)来推 g ( i , j ) g(i,j) g(i,j),即减去所有最小质因子为 P j Pj Pj的数的F和。
g ( i , j ) = g ( i , j − 1 ) − F ( P j ) ⋅ ( g ( i / P j , j − 1 ) − s u m f ( j − 1 ) ) g(i,j) = g(i,j-1) - F(Pj)\cdot (g(i/Pj,j-1) - sumf(j-1)) g(i,j)=g(i,j1)F(Pj)(g(i/Pj,j1)sumf(j1))
后面括号内sumf(j)是指 ∑ F ( P i ) , i ∈ [ 1 , j ] \sum F(Pi),i\in[1,j] F(Pi),i[1,j]

晒微解释一下式子的含义:
考虑任意一个需要被筛出的数 P j ⋅ A < = i P_j\cdot A<=i PjA<=i,即 A < = i / P j A<=i / P_j A<=i/Pj
注意需要同时满足 R ( A ) > = P j R(A)>=P_j R(A)>=Pj
这样的数的F和其实可以用g表示出来。但同时注意到g内有一段小于 P j Pj Pj的质数,需要减去。
*注意此处 i / P j i/Pj i/Pj可能小于Pj,此时式子不正确。但当 P j 2 > i Pj^2>i Pj2>i时显然 g ( i , j ) = g ( i , j − 1 ) g(i,j) = g(i,j-1) g(i,j)=g(i,j1),实现上由于省略了第二维,因此直接不更新即可。

根据常识,第一维大小只有 2 n 2\sqrt n 2n ,离散下来存好。具体可能需要看标。
下面在筛质数和。

int getid(ll x) {
	if (x <= P) return id[x]; else return id2[n/x];
}
	//p是质数数组,pre[x]表示p[2]+p[3]...+p[x]
	P = sqrt(n);
	for (ll l = 1; l <= n;) {
		ll v = n / l, r = n / v;
		if (v <= P) id[v] = ++m; else id2[n/v] = ++m; //想想为啥是单射
		w[m] = v;
		g[m] = (2+v)*(v-1)/2;
		l = r + 1;
	}
	//注意w是递减的。
	for (int j = 1; p[j] <= P; j++) {
		for (int i = 1; i <= m; i++) {
			if (p[j] * p[j] > w[i]) break; 
			g[i] -= p[j] * (g[getid(w[i] / p[j])] - pre[j-1]);
		}
	}

询问

求好了所有质数的和,就可以如何用质数和求所有和了。
其实是基于每个数的质因数分解。

有直接计算 / 递推两个方法。优缺点在最后。

直接计算版

类似g的定义,
f ( i , j ) f(i,j) f(i,j)表示 ∑ F ( x ) , 满 足 x ∈ [ 2 , i ] , 且 R ( x ) > = P j \sum F(x), 满足x\in[2,i],且R(x)>=P_j F(x),x[2,i]R(x)>=Pj
(注意是R(x)大于等于Pj而不是大于Pj)
分两个部分来计算:
1.质数
2.合数,通过枚举其最小质因子与次幂来统计。

对于f(i,j),显然质数部分就是 g ( i , M A X ) − s u m f ( j − 1 ) g(i,MAX) - sumf(j-1) g(i,MAX)sumf(j1)
合数部分,先枚举最小质因子与次幂 p k p^k pk,当然要满足 p > = P j p>=P_j p>=Pj
我们要将符合这一部分的F算进来,这样的数 p k A < = n 且 R ( A ) > p p^kA<=n且R(A)>p pkA<=nR(A)>p
由于F是积性函数,其实就是 F ( p k ) ⋅ f ( i / p k , p 的 序 号 + 1 ) F(p^k)\cdot f(i/p^k,p的序号+1) F(pk)f(i/pk,p+1)。递归下去求就行。出于某种神奇的原因不需要记忆化。

另外,因为f函数是不包括1的,所以质数的次幂会漏算。加上就行。

#define F(x,y) (...)
ll ask(ll x,ll i) {
	if (x <= 1 || prime[i] > x) return 0;
	ll ans = g[getid(x)] - pre[i-1];
	
	for (ll j = i; j <= prime[0] && prime[j] * prime[j] <= x; j ++) {
		ll r = prime[j];
		for (ll e = 1; r * prime[j] <= x; e++, r *= prime[j]) {
			ans = (ans + F(prime[j], e) * ask( x / r, j + 1 ) + F(prime[j], e+1)) % mo;
		}
	}
	return ans;
}

递推版

这个没有那么灵活,但是也有其优点。
使用了求g的思想。(反向模拟埃氏筛)
新设 f ( i , j ) f(i,j) f(i,j)表示 ∑ F ( x ) , 满 足 x ∈ [ 2 , i ] , 且 x ∈ P r i m e 或 R ( x ) > = P j \sum F(x), 满足x\in[2,i],且x\in Prime或R(x)>=P_j F(x),x[2,i]xPrimeR(x)>=Pj

初值 f ( i , M A X ) f(i,MAX) f(i,MAX)即质数和g.

f ( i , j ) f(i,j) f(i,j)相比于 f ( i , j + 1 ) f(i,j+1) f(i,j+1),需要加上
∑ F ( p j k A ) , 满 足 R ( A ) > P j \sum F({p_j}^kA),满足R(A)>P_j F(pjkA)R(A)>Pj
同样,这样的F实际上就是 f ( i / p k , j + 1 ) − s u m f ( j ) f(i/p^k,j+1) - sumf(j) f(i/pk,j+1)sumf(j)再整体乘上 F ( P j k ) F({P_j}^k) F(Pjk),此处也漏了质数幂,需要补上。
同样在筛数的和(虽然没有什么意义…)。

	for (int j = p[0]; j; j--) {
		if (p[j] * p[j] > n) continue;
		
		for (int i = 1; i <= m; i++) {
			if (p[j] * p[j] > w[i]) break;
			ll l = p[j];
			for (int e = 1; l * p[j] <= w[i]; e++,l*=p[j]) {
				g[i] += l * (g[getid(w[i]/l)] - pre[j]) + l * p[j];
			}
		}
	}

方法比较

若只需要求一个f,那么直接计算会有比较小的常数(相比后者1/2到1/3)。
但后者可以一次处理出对于所有f(n),f(n/2),f(n/3)…f(1)的值。

步骤总结

  1. 处理出n/x的所有取值并标号, 并构造一个函数G,求出对应的前缀和。
  2. 对这个G的前缀和模拟埃氏筛,求出质数的G的前缀和(事实上就是所求函数F的质数前缀和)
  3. 使用递归求法计算某个点的前缀和 / 反向模拟埃氏筛求所有n/x位置的前缀和。

例题

LOJ简单的函数
直接计算法,套式子就行。


#include 
#include 
#include 
#include 
#include 
using namespace std;
const int N = 1e5 + 1000, mo = 1e9 + 7, inv2 = 500000004;
typedef long long ll;
ll n,P;
ll id1[N],id2[N],w[2*N],m,g[2*N],h[2*N],f[2*N];
ll prime[N],pre[N];
bool is[N];

void init() {
	for (ll i = 2; i <= 1e5; i ++) {
		if (!is[i]) prime[++prime[0]] = i, pre[prime[0]] = (pre[prime[0] - 1] + i) % mo;
		for (ll j = 1; j <= prime[0] && prime[j] * i <= 100000; j ++) {
			is[prime[j] * i] = 1;
			if (i % prime[j] == 0) break;
		}
	}
}

int getid(ll x) {
	if (x <= P) return id1[x]; else return id2[n / x];
}

void calc_gh() {
	for (ll l = 1; l <= n; ) {
		ll v = n / l, r = n / v;
		if (v <= P) id1[v] = ++m; else id2[r] = ++m;
		w[m] = v;
		ll z = v % mo;
		//sum 2 ~ v
		g[m] = (2 + z) * (z - 2 + 1) % mo * inv2 % mo;
		h[m] = z - 1;
		l = r + 1;
	}
	
	for (ll i = 1; i <= prime[0]; i++) {
		ll z = prime[i];
		for (ll j = 1; j <= m && z * z <= w[j]; j++) {
			int op = getid(w[j]/z);
			g[j] = (g[j] - prime[i] * (g[op] - pre[i-1]) % mo) % mo;
			h[j] = (h[j] - (h[op] - (i - 1))) % mo;
		}
	}
	for (int i = 1; i <= m; i++) {
		g[i] = (g[i] + mo) % mo;
		f[i] = (f[i] + mo) % mo;
//		printf("%lld %lld\n",g[i],h[i]);
		f[i] = (g[i] - h[i]) % mo;
	}
}

#define F(x,y) ((x)^(y))
ll ask(ll x,ll i) {
	if (x <= 1 || prime[i] > x) return 0;
	ll ans = f[getid(x)] - (pre[i-1] - (i - 1)); if (i == 1) ans+=2;
	
	for (ll j = i; j <= prime[0] && prime[j] * prime[j] <= x; j ++) {
		ll r = prime[j];
		for (ll e = 1; r * prime[j] <= x; e++, r *= prime[j]) {
			ans = (ans + F(prime[j], e) * ask( x / r, j + 1 ) + F(prime[j], e+1)) % mo;
		}
	}
//    printf("%lld %lld %lld\n",x,i,ans);
	return ans;
}

int main() {
	cin>>n; P = sqrt(n);
	init();
	calc_gh();
	printf("%lld\n",((ask(n,1) + 1)%mo+mo)%mo);
}

jzoj5594 最大真因数
使用了递推法,方便在途中统计答案。(我是枚举最小质因子来统计的。)
(事实上只需要part1就可以完成这个统计,我是在part2统计的。)

#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
const int N = 2 * 750000;
ll L,R;
ll g[N+10],f[N+10],id[N+10],id2[N+10],m,w[N+10],pre[N+10];
ll p[N+10],P;
bool bz[N+10];
void init() {
	for (int i = 2; i <= N; i++) {
		if (!bz[i]) p[++p[0]] = i,pre[p[0]] = pre[p[0]-1] + i;
		for (int j = 1; j <= p[0] && i * p[j] <= N; j++) {
			bz[i * p[j]] = 1;
		}
	}
}

ll zz;
int getid(ll x) {
	if (x <= P) return id[x]; else return id2[zz/x];
}

ll calc(ll n) {
	if (n <= 1) return 1;
	zz = n;
	P = sqrt(n);
	m = 0;
	memset(id,0,sizeof id); memset(id2,0,sizeof id2);
	memset(g,0,sizeof g); memset(f,0,sizeof f);
	for (ll l = 1; l <= n;) {
		ll v = n / l, r = n / v;
		if (v <= P) id[v] = ++m; else id2[n/v] = ++m;
		w[m] = v;
		g[m] = (2+v)*(v-1)/2;
		l = r + 1;
	}
	
	for (int j = 1; p[j] <= P; j++) {
		for (int i = 1; i <= m; i++) {
			if (p[j] * p[j] > w[i]) break; //不加这句会TLE!!! 
			g[i] -= p[j] * (g[getid(w[i] / p[j])] - pre[j-1]);
		}
	}
	
	ll ret = 0, sup = 0;
	for (int j = p[0]; j; j--) {
		if (p[j] * p[j] > n) continue;
		for (ll u = p[j]; u*p[j] <= n; u*=p[j]) {
			ret += u/p[j] * (g[getid(n/u)] - pre[j]) + u;
		}
		
		for (int i = 1; i <= m; i++) {
			if (p[j] * p[j] > w[i]) break; //不加这句会TLE!!! 
			ll l = p[j];
			for (int e = 1; l * p[j] <= w[i]; e++,l*=p[j]) {
				g[i] += l * (g[getid(w[i]/l)] - pre[j]) + l * p[j];
			}
		}
	}
	return ret + 1;
}

int main() {
	freopen("factor.in","r",stdin);
//	freopen("factor.out","w",stdout);
	cin>>L>>R;
	init();
	cout<<calc(R) - calc(L-1)<<endl;
}

复杂度口胡

大概是 ∑ n / i , 并 满 足 i < = n \sum \sqrt {n/i},并满足i<=\sqrt n n/i i<=n
事实上并不是每一个i都是质数,所以这是个非常宽的上界。
这个复杂度是 n ⋅ ∑ 1 / i \sqrt n \cdot \sum \sqrt {1/i} n 1/i ,后面那一部分是 n 1 / 4 n^{1/4} n1/4
证法是找一个可以求导之后等于 i − 0.5 i^{-0.5} i0.5的函数,转换为两个端点函数值相减。
不难发现这个函数就是 2 i 0.5 2i^{0.5} 2i0.5。因此等于 2 ( n 0.5 ) 2(\sqrt n ^ {0.5}) 2(n 0.5),也就是 O ( n 1 / 4 ) O(n^{1/4}) O(n1/4)
*为什么我觉得这个证法的界也超松的…
至于那个log怎么来的,大概是素数的缘故吧…

你可能感兴趣的:(新内容,数论)