古典叙事 · 技术札记

序章

山河有卷
人间有声

写代码,也写长风、旧城与未熄的灯。
愿每一篇随笔,都有自己的山水与回声。
阅览随笔

莫比乌斯反演

莫比乌斯函数的定义

要学会莫比乌斯反演,首先我们得明白,莫比乌斯函数的定义是什么
莫比乌斯函数(Möbius 函数)定义为:
image
首先我们知道,任何一个数都可以用若干个素数相乘表示,对于莫比乌斯函数,如果存在一个素数的次幂大于1,那么莫比乌斯的函数值为0,否则的话就定义k为互异素因子的个数,莫比乌斯函数的值就是-1的k次幂

莫比乌斯函数的求法

对于单个数,我们可以对他分解质因数,然后计算他的莫比乌斯函数

int mu(int n) {
  int res = 1;
  for (int i = 2; i * i <= n; ++i) {
    if (n % i == 0) {
      n /= i;
      // 检查是否是2次幂
      if (n % i == 0) return 0;
      res = -res;
    }
  }
  // The remaining factor must be prime.
  if (n > 1) res = -res;
  return res;
}

对于多个数字,我们采取一线性筛来用O(N)的复杂度完成

std::vector<int> get_mu(int n) {
    // mu:存储莫比乌斯函数值,下标 0~n(0  unused,1~n 对应实际值)
    // primes:存储筛选过程中发现的素数(用于后续筛非素数)
    std::vector<int> mu(n + 1), primes;
    // not_prime:标记是否为非素数(true=非素数,false=素数),初始默认全为素数(false)
    std::vector<bool> not_prime(n + 1);
    // 预分配 primes 内存(避免动态扩容开销),预估素数个数约为 n / ln(n),此处简化用 n
    primes.reserve(n);
    
    // 初始化:根据莫比乌斯函数定义,μ(1) = 1
    mu[1] = 1;

    // 外层循环:遍历 2~n 的所有数,处理每个数 x
    for (int x = 2; x <= n; ++x) {
        // 若 x 未被标记为非素数,说明 x 是素数
        if (!not_prime[x]) {
            primes.push_back(x);  // 将素数 x 加入素数列表
            mu[x] = -1;           // 素数是「无平方因子且仅1个素因子」,故 μ(x) = (-1)^1 = -1
        }

        // 内层循环:用已发现的素数 p 筛 x*p(线性筛核心:每个数仅被最小素因子筛一次)
        for (int p : primes) {
            // 边界判断:x*p 超过 n 时,无需继续(避免数组越界)
            if (x * p > n) break;
            
            // 标记 x*p 为非素数(x*p 是素数 p 的倍数,必然非素数)
            not_prime[x * p] = true;

            // 关键判断:x 是否能被当前素数 p 整除
            if (x % p == 0) {
                // 情况1:x 能被 p 整除 → x 中已包含素因子 p
                // 则 x*p 中包含 p²(x 有 p,再乘 p),即含平方因子 → μ(x*p) = 0
                mu[x * p] = 0;
                break;  // 线性筛核心:p 是 x 的最小素因子,后续素数更大,无需继续(避免重复筛)
            } else {
                // 情况2:x 不能被 p 整除 → p 是 x*p 的「新素因子」
                // x 的素因子个数为 k,则 x*p 的素因子个数为 k+1 → μ(x*p) = (-1)^(k+1) = -(-1)^k = -μ(x)
                mu[x * p] = -mu[x];
            }
        }
    }

    // 返回 1~n 的莫比乌斯函数值数组
    return mu;
}

莫比乌斯反演

在我们了解了莫比乌斯函数后,现在就该用他的性质,莫比乌斯反演
image

然后这个有什么用呢?
我们接下来来写一个题

P2522 [HAOI2011] Problem b

题目描述

对于给出的 \(n\) 个询问,每次求有多少个数对 \((x,y)\),满足 \(a \le x \le b\)\(c \le y \le d\),且 \(\gcd(x,y) = k\)\(\gcd(x,y)\) 函数为 \(x\)\(y\) 的最大公约数。

输入格式

第一行一个整数 \(n\),接下来 \(n\) 行每行五个整数,分别表示 \(a,b,c,d,k\)

输出格式

\(n\) 行,每行一个整数表示满足要求的数对 \((x,y)\) 的个数。

输入输出样例 #1

输入 #1

2
2 5 1 5 1
1 5 1 5 2

输出 #1

14
3

说明/提示

对于 \(100\%\) 的数据满足:\(1 \le n,k \le 5 \times 10^4\)\(1 \le a \le b \le 5 \times 10^4\)\(1 \le c \le d \le 5 \times 10^4\)

思路

image
image
image
image
image
最后我们还要用到一个分块的思想,继续优化时间复杂的

#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
#include <queue>
#include <string>
#include <cstring>
using namespace std;

// 定义64位整数类型(此处实际为int,若数据范围大建议改为long long)
using i64 = int;
// 宏定义换行符,加速输出
#define endl '\n';

// 全局变量:k为目标gcd值,t为测试用例数量
i64 k,t;
// mu数组存储莫比乌斯函数的前缀和(预处理后)
i64 mu[100005];
// is_prime数组标记是否为非素数(1表示非素数,0表示素数)
i64 is_prime[100005];
// prime向量存储筛选出的素数
vector<i64> prime;

/**
 * @brief 预处理莫比乌斯函数值,并计算其前缀和
 * 采用线性筛(欧拉筛)算法,时间复杂度O(n),n=50000
 * 最终mu数组存储的是莫比乌斯函数的前缀和(便于后续区间和计算)
 */
void get_mu()
{
	// 初始化mu[1] = 1(莫比乌斯函数定义:μ(1)=1)
	mu[1] = 1;
	// 线性筛核心:遍历2到50000的所有数
	for (i64 i = 2; i <= 50000; i++)
	{
		// 若i未被标记为非素数,则i是素数
		if(!is_prime[i]){
			prime.push_back(i);  // 将素数i加入素数列表
			mu[i] = -1;          // 素数是单个不同素因子,μ值为-1((-1)^1)
		}
		// 用已发现的素数prime[j]筛去i*prime[j]
		for (i64 j = 0; j < prime.size(); j++)
		{
			// 若i*prime[j]超过50000,跳出循环(避免越界)
			if(i * prime[j] > 50000)break;
			// 标记i*prime[j]为非素数
            is_prime[i * prime[j]] = 1;
			// 若i能被prime[j]整除(即prime[j]是i的最小素因子)
			if (i % prime[j] == 0)
			{
                mu[i * prime[j]] = 0;  // i*prime[j]含平方因子(prime[j]^2),μ值为0
				break;  // 线性筛关键:每个数仅被最小素因子筛一次,跳出避免重复
			}
			// 若i不能被prime[j]整除,说明prime[j]是新素因子,μ值取反
			mu[i * prime[j]] = -mu[i];
		}
	}
	// 计算莫比乌斯函数的前缀和:mu[i] = μ(1) + μ(2) + ... + μ(i)
	// 目的是快速计算区间[d, nextd]的μ值和(mu[nextd] - mu[d-1])
	for (i64 i = 1; i <= 50000; i++)
	{
		mu[i] += mu[i - 1];
	}
}

/**
 * @brief 计算区间[1,n]×[1,m]中,gcd(i,j)=k的数对(i,j)的个数
 * 基于数论推导:转化为互质计数问题,并用莫比乌斯函数和数论分块优化
 * @param n 第一个维度的上限
 * @param m 第二个维度的上限
 * @return 满足条件的数对个数
 */
i64 solve(i64 n,i64 m)
{
	i64 ans = 0;  // 存储结果
	// 数论分块:枚举d,每次处理一个区间[d, nextd],优化求和效率
	// d的范围:d*k <= min(n,m)(由推导中的变量范围限制)
	for (int d = 1, nextd; d * k <= min(n, m); d = nextd + 1)
	{
		// 计算当前分块的右边界nextd:取n/(n/d)和m/(m/d)的最小值
		// 原理:对于d,使得n/d和m/d的值不变的最大d为nextd
		nextd = min(n / (n / d), m / (m / d));
		// 确保nextd不超过上限(d*k <= min(n,m))
		nextd = min(nextd, min(n, m) / k);
		// 累加当前分块的贡献:
		// (n/(k*d)) * (m/(k*d)) 是满足d'=d时的计数项
		// (mu[nextd] - mu[d-1]) 是区间[d, nextd]的莫比乌斯函数和(前缀和相减)
		ans += (n / k / d) * (m / k / d) * (mu[nextd] - mu[d - 1]);
	}
	return ans;
}

int main()
{
	// 关闭同步流,加速输入输出
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	
	// 预处理莫比乌斯函数及其前缀和
	get_mu();
	
	// 读入测试用例数量
	cin >> t;
	i64 a, b, c, d;  // 区间参数:i∈[a,b],j∈[c,d]
	while (t--)
	{
		// 读入区间参数和目标gcd值k
		cin >> a >> b >> c >> d >> k;
		// 二维前缀和容斥原理:计算[a,b]×[c,d]的结果
		// 公式:f(b,d) - f(b,c-1) - f(a-1,d) + f(a-1,c-1)
		// 其中f(n,m)是[1,n]×[1,m]中满足条件的数对个数
		cout << solve(b, d) - solve(b, c - 1) - solve(a - 1, d) + solve(a - 1, c - 1) << endl;
	}
	return 0;
}

posted @ 2025-10-13 20:50  Morphis‘  阅读(20)  评论(0)    收藏  举报

特别策划 · CINEMATIC NOTES

风沙与孤骑

风起塞外,
胜负在刀剑之前。

“把复杂拆成秩序,把未知写成答案。”
Morphis · 山河一卷 愿你从这里出发,仍能听见山风。 影像:farfarSébastien Goldberg / Unsplash