Min-25筛

发布时间 2023-12-20 10:50:17作者: cool_milo

Min-25筛学习笔记

现在写了。

拜谢oi-wiki。

波特好闪。

liuhangshin是我们的红太阳。

Chery写完OJ When?

启动!

问题

\[\sum_{i = 1} ^ {n} f(i) \]

其中 \(f\) 是积性函数,\(f(p)\) 是低阶多项式,\(f(p^c)\) 能快速求值。条件其实非常宽松。

约定和记号

  • \(p_k\) :第 \(k\) 小的质数。
  • \(mp(n)\)\(n\) 的最小质因数。如果\(n = 1\),定义为 \(1\)
  • \(F_p(n)\)\(\sum_{p \leq n}f(p)\)
  • \(F_k(n)\)\(\sum_{i = 2} ^ {n} [p_k \leq mp(i)]f(i)\) ,也就是满足最小质因数不小于 \(p_k\) 的数的 \(f\) 值和。

答案

\[ans = F_1(n) + 1 \]

最后加上的那个1是1的贡献,注意这个算法随时都要注意1的问题。

\(part 1\)

根据定义,大力递推。

\[\begin{aligned} \\& F_k(n) = \sum_{i = 2}^{n}[p_k \leq mp(i)]f(i)\\& = \sum_{i \geq k,p^2_i \leq n}\sum_{c \geq 1,p^c \leq n} (f(p_i^c)([c > 1] + F_{i + 1}(n / p_i^c))) + \sum_{i \geq k,p_i \leq n} f(p_i) \\& = \sum_{i \geq k,p^2_i \leq n}\sum_{c \geq 1,p^c \leq n} (f(p_i^c)([c > 1] + F_{i + 1}(n / p_i^c))) + F_p(n) - F_p(p_k - 1) \\& = \sum_{i \geq k,p^2_i \leq n}\sum_{c \geq 1,p^{c + 1} \leq n} (f(p_i^c)F_{i + 1}(n / p_i^c) + f(p_i^{c + 1})) + F_p(n) - F_p(p_k - 1) \end{aligned} \]

分别解释一下:
第一步转化是枚举每个数的最小质因子和它出现了几次,那么这部分的贡献根据积性函数的性质是可以分开算的。注意我们只需要枚举 \(\leq\sqrt{n}\) 的质因子。如果最小质因子都大于 \(\sqrt{n}\) 那么这个数肯定是个质数,所以右边我们需要质数处的前缀和。

左边枚举的是每个质数的幂和不是质数是幂的情况。一定注意 \(F_k\) 的求和下界是 \(2\)

第二步是定义。

第三步很有意思,它直接排除掉了最小质因子满足 \(p_i^c \leq n < p_i^{c + 1}\) 的情况。这是为什么呢?因为如果 \(i\) 乘上它的最小质因子都不能在 \(n\) 以内的话,乘以其它数就更加没有办法保持在 \(n\) 以内了。

这个式子的边界条件是 \(F_k(n)(p_k > n) = 0\)

这个看起来非常暴力啊!恭喜你答对了,这个做法的复杂度是 \(\Theta(n^{1-\epsilon})\) ,意思是在 \(n\) 增大的过程中这个玩意的效率越来越接近 \(\Theta(n)\)

但是有什么关系呢?反正这玩意跑的飞快。

\(\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\ell\)

手动分割(被打

\(part 2\)

那么我们要求出 \(F_p(n)\)

这个怎么做呢?当然是Meissel-Lehmer 不可能的。

注意到我们求值的所有地方全都是 \(\frac{n}{x}\) 和质数。这个质数的值一定比较小,因为如果质数超过 \(\sqrt{n}\),在递推进入下一层搜索的 \(\frac{n}{p_i}\) 就小于 \(\sqrt{n}\) ,就会直接返回。所以这个我们直接暴力做前缀和就可以了。

那么我们又知道,所有 \(\frac{n}{x}\) 的取值只有 \(\sqrt{n}\) 个,我们先通过一次整出分块找到所有这样的值并编号,然后把 \(<\sqrt{n}\) 的那部分直接用一个数组保存它的编号,把 \(\geq\sqrt{n}\) 的那部分用另一个数组在 \(\frac{n}{\frac{n}{x}}\) 处保存它的编号。这部分的预处理就做完了。

根据我们的 \(f(p)\) 是低阶多项式,我们拆每一项计算它的贡献。那么我们只需要算 \(\sum_{i = 2}^{n}[isprime(i)]i^x\),记 \(g(i) = i^x\) 。注意 \(g_i\)完全积性函数

剩下的部分我们 模拟埃筛,记 \(G_k(n) = \sum_{i = 1}^n[isprime(i) \lor mp(i) > p_k]g(i)\) ,也就是埃筛用第 \(k\) 个质数筛完之后剩下函数值的和。

我们写出递推式:

\[G_k(n) = G_{k - 1}(n) - [p_k^2 \leq n]g(p_k)(G_{k - 1}(n / p_k) - G_{k - 1}(p_{k} - 1)) \]

怎么理解?这个式子其实是模拟埃筛用最小的质因子筛掉一个合数的过程。所以首先 \(p_k^2 > n\) 的部分一定不是最小质因子,可以排除。
那么我们先拿所有含有质因子 \(p_k\) 的数出来,那么这些数是 \(p_k,2p_k,3p_k\cdots\) ,在这些数 \(x\) 中,\(\frac{x}{p_k}\) 没有被前 \(k - 1\) 个质数筛掉的部分才具有保留的意义。所以减去的部分是 \(G_{k - 1}(n / p_k)\)

但是如果 \(\frac{x}{p_k}\) 就是前 \(k - 1\) 个质数之一呢?这里也会被减掉。所以我们需要加上 \(G_{k - 1}(p_{k} - 1)\) ,注意这也就是前 \(k - 1\) 个质数处的 \(g\) 值。因为 \(p_k^2 \leq n\) ,所以这个也是可以预处理的。

发现这里访问到的 \(G\) 的取值也只会是 \(\frac{n}{x}\)直接按照这个dp,复杂度 \(\Theta(\frac{n^\frac{3}{4}}{\log(n)})\) 。显然可以滚动数组。

复杂度证明先咕了,反正 oi-wiki 上也有。

这下应该是真没了。

上代码。

code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int P = 1e9+7, inv2 = (P + 1) >> 1;
const int N = 1e6+5;
template<typename T>inline void read(T &a)
{
	a = 0;
	int f = 1;
	char c = getchar();
	while(!isdigit(c))
	{
		if(c == '-')	f = -1;
		c = getchar();
	}
	while(isdigit(c))
		a = a * 10 + c - 48,c = getchar();
	a *= f;
}

template<typename T,typename ...L> inline void read(T &a,L &...l)
{
	read(a),read(l...);
}

inline int Mul(ll a, ll b) {
	return (a % P) * (b % P) % P;
}

inline int Add(int a, int b) {
	return a + b >= P ? a + b - P : a + b;
}

inline int Sub(int a, int b) {
	return a - b < 0 ? a - b + P : a - b;
}

inline void Inc(int &a, int b) {
	a = Add(a, b);
} 

const int inv6 = Mul((P + 1) >> 1, (P + 1) / 3);

vector<int> primes;
bool isprime[N];
ll n, val[N];
int le[N], ge[N], ptr, g1[N], g2[N];
int h1[N], h2[N];//质数的值的前缀和 

ll fs(ll x) {
	return Mul(x % P, x % P - 1);
}

void xxs() {
	for(int i = 2; i < N; i++) {
		h1[i] = h1[i - 1], h2[i] = h2[i - 1];
		if(isprime[i]) {
			primes.emplace_back(i);
			Inc(h1[i], i);
			Inc(h2[i], Mul(i, i));
		}
		for(auto it:primes) {
			if(i * it > N) break;
			isprime[i * it] = 0; 
		} 
	}
}

int F(int k, ll n) {
	if(primes[k] > n) return 0;
	int res = 0;
	for(int i = k; i < int(primes.size()); i++) {
		if(primes[i] > n / primes[i]) break;
		ll d = primes[i];
		while(d <= n / primes[i]) {
			Inc(res, Add(Mul(fs(d), F(i + 1, n / d)), fs(d * primes[i]))), d *= primes[i]; 
		}
	}
	int id = 0;
	if(n > N) {
		id = ge[::n / n];
	} 
	else {
		id = le[n];
	}
	return Add(res, Sub(Sub(g2[id], g1[id]), Sub(h2[primes[k] - 1], h1[primes[k] - 1])));
}

int main() {
	read(n);
	memset(isprime, 1, sizeof isprime);
	isprime[1] = 0;
	xxs();
	for(ll l = 1; l <= n; l = (n / (n / l)) + 1) {
		ll d = (n / l);
		val[++ptr] = d;
		if(d < N) {
			le[d] = ptr;
		}
		else {
			ge[n / d] = ptr;
		}
		g1[ptr] = Mul(d - 1, Mul(d + 2, inv2));
		g2[ptr] = Sub(Mul(Mul(Mul(d, d + 1), 2 * d + 1), inv6), 1);//减掉1的值 
	}
	for(int i = 1; i < N; i++) {
		h1[i] = h1[i - 1], h2[i] = h2[i - 1];
		if(isprime[i]) {
			Inc(h1[i], i);
			Inc(h2[i], Mul(i, i)); 
		}
	} 
	for(auto it:primes) {
		for(int i = 1; i <= ptr; i++) {
			if(it * ll(it) > val[i]) break;
			ll c = val[i] / it;
			int pos = 0;
			if(c >= N) {
				pos = ge[n / c];
			} 
			else {
				pos = le[c];
			} 
			g1[i] = Sub(g1[i], Mul(it, g1[pos]));
			g1[i] = Add(g1[i], Mul(it, h1[it - 1]));
			g2[i] = Sub(g2[i], Mul(Mul(it, it), g2[pos]));
			g2[i] = Add(g2[i], Mul(Mul(it, it), h2[it - 1]));
		}
	}
	printf("%d\n", Add(F(0, n), 1));
}

/*
	start coding at:2023/12/18 22:09
	finish debugging at:2023/12/18 19:36
	stubid mistakes:d没有累乘,算G时fs的参数是it,元素在表中是倒序的,预处理g和h的时候使用定义式,少开ll 
*/

/*
  吾日三省吾身:
  你排序了吗?
  你取模了吗?
  你用%lld输出long long 了吗?
  1LL<<x写对了吗?
  判断=0用了abs()吗?
  算组合数判了a<b吗?
  线段树build()了吗?
  .size()加了(signed)吗?
  树链剖分的DFS序是在第二次更新的吗?
  修改在询问前面吗?
  线段树合并到叶子结点return了吗?
  __builtin_ctz后面需要加ll吗?
*/