牛客——2020年西北工业大学“编程之星”程序设计挑战赛——I题小朋友你是否有很多问号——莫比乌斯反演

本人菜鸡,第一次写莫比乌斯的东西,如果有错误的话,还请各位大大指出,顺便轻喷···
首先介绍莫比乌斯反演:
观察图片里这个函数关系,我们现在有函数F与f
牛客——2020年西北工业大学“编程之星”程序设计挑战赛——I题小朋友你是否有很多问号——莫比乌斯反演_第1张图片

用公式来描述就是
在这里插入图片描述

其中d是x的所有因子
现在想一下,怎么用F来把f表示出来
不难得到
牛客——2020年西北工业大学“编程之星”程序设计挑战赛——I题小朋友你是否有很多问号——莫比乌斯反演_第2张图片
写成公式表示就是

那么问题来了,u是什么?答:大名鼎鼎的莫比乌斯函数
也就是求出莫比乌斯函数之后,我们就可以通过这个公式和F函数的值,求出f。主要意义就是这样做可以减少运算量。毕竟有的时候我们要求的f(x)可能很难算,或者复杂度太高。能通过求出来可以轻松得到的F,然后推导f(x)是十分划算的。

那么,我们先来了解一下莫比乌斯函数
牛客——2020年西北工业大学“编程之星”程序设计挑战赛——I题小朋友你是否有很多问号——莫比乌斯反演_第3张图片
求法也十分简单,莫比乌斯函数是积性函数,满足
在这里插入图片描述
而我们以前所学的线性欧拉筛,也满足这个,也就是说,我们可以直接用线性欧拉筛来预处理莫比乌斯函数,这个板子也很好找。相对的,所有积性函数,都可以用欧拉筛预处理。

那么,开始正题:反演
莫比乌斯反演一共有两种形式:
1.约数形式:
在这里插入图片描述

2.倍数形式:(做题更常用)
在这里插入图片描述

到这里为止都不难,莫比乌斯的题目的主要问题是构造,你得知道F设置成什么比较好,因为要快速求出f,就必须要求F能够快速求出。第二个问题才是公式的推导。数学真的容易杀人

在当前题目
n个数,求出其中任意m个数的所有m元公质数,如果我们能够求出在给定的n个数字中,每个数字x有多少数字与其互质,不妨记为res[x],那么,只有我们从res[x]中取出m个数字,x必然是这m个数字的m元公质数,其对答案的贡献就是result+=x;那么考虑对数,首先是x可能有多个然后是取出m个数字,是一个组合数,也就是我们的result可以写成C(res[x],m) * x * cnt[x]的形式,然后枚举每一个x就好。
现在的难点就是怎么求出rex[x],即,对于每一个数字x,有多少个数字与其互质。
转化为数学公式
在这里插入图片描述
其中x就是我们要当前枚举的数字,公式计算的过程中是一个常数。我们要求的就是f(1)
和平时不同的是,这里直接求f(1)我不会,我只会板子推法,从概括到一般。我们先求出f(k),然后带入k=1;
重点
在这里插入图片描述

我们令F(k)为,a[i]与x的最大公约数是k的倍数的个数,即gcd=k,2k,3k…,我们把这个gcd值设为d

在这里插入图片描述

要知道F可以在nlog(n)全部预处理结束
因为F(k)就等于所有数字里,大小是k的整数倍的数字的个数。
我们用桶的方式来记录每一个数字,那么直接for循环,从k到max,k+=k,然后F+=cnt[k]即可,再加上外层枚举k,二层循环,循环次数是max+max/2+max/3+max/4······,化成函数积分后,结果收敛为max*log(max),复杂度可以接受。(F的处理如下图所示)
牛客——2020年西北工业大学“编程之星”程序设计挑战赛——I题小朋友你是否有很多问号——莫比乌斯反演_第4张图片

然后就是一个标准的倍数形式的莫比乌斯反演:
枚举k的整数倍d,上限为能取到的最大值。

在这里插入图片描述

k=1带入
在这里插入图片描述

F(d)预处理过,可以直接带入

上代码:

#include 

using namespace std;
typedef long long LL;
const int mod=998244353;
const int N=1e5+10;
int mo[N],f[N],prime[N],num=0,a[N];
int cnt[N],sum[N];//cnt 桶 记录每个数字出现的次数   sum[i]记录i的整数倍出现的次数
int res[N];
LL fac[N],inv[N];
void get_mo(){
	mo[1]=1; f[1]=1;f[0]=1;
	for(int i=2;i<N;i++){
		if(!f[i]){prime[num++]=i;mo[i]=-1;  }
		for(int j=0;j<num&&prime[j]*i<N;j++){
			f[i*prime[j]]=1;
			if(i%prime[j]==0){
				mo[i*prime[j]]=0;
				break;
			}
			mo[i*prime[j]]=-mo[i];
		}
	}
}
LL quickpow(LL x,LL k){//快速幂
	LL res=1;
	while(k){
		if(k&1){
			res=res*x%mod;
		}
		k>>=1;
		x=x*x%mod;
	}
	return res;
}
void init(){//预处理阶乘 和 阶乘逆元
	fac[0]=1;
	for(int i=1;i<N;i++) fac[i]=fac[i-1]*i%mod;
	inv[N-1]=quickpow(fac[N-1],mod-2);
	for(int i=N-2;i>0;i--){
		inv[i]=inv[i+1]*(i+1)%mod;
	}
}
LL C(int n,int m){//求组合数 C(n,m)
	if(n<m) return 0;
	if(n==m) return 1;
	//cout<<"C("<return fac[n]*inv[m]%mod*inv[n-m]%mod;
}
int main()
{
	get_mo();
	init();
    int n,m; cin>>n>>m;
    for(int i=0;i<n;i++){
		cin>>a[i];
		cnt[ a[i] ]++;
    }

    for(int i=1;i<N;i++){//整个过程nlog(n)
		for(int j=i;j<N;j+=i){
			sum[i]+=cnt[j];
		}
    }

    for(int x=1;x<N;x++){
		res[x]=0;
		int d=1;
		for(d=1;d*d<x;d++){
			if(x%d==0)
			res[x]+=sum[d]*mo[d]+sum[x/d]*mo[x/d];
		}
		if(d*d==x){
			res[x]+=sum[d]*mo[d];
		}
    }
    LL result=0;
    for(int i=1;i<N;i++){
		if(cnt[i]){
			//cout<<"C("<
			result+=C(res[i],m)*i%mod*cnt[i]%mod;
			result%=mod;
		}
    }
    cout<<result<<endl;


    return 0;
}

吐槽:
嗯,比赛的时候其实压根没看这道题,赛后我的某队友兴致勃勃的告诉我,I题简单,概率+组合数。我,啊?出的人这么少,那大概概率很难吧,然后到了第二天,他问我,别人的代码里,函数里这个mu数组是干嘛的,我秒懂,莫比乌斯反演,好了,我死了,不会。但是队友觉得不妥,他觉得我是学数学的,必须要把这个题目给他讲明白,嗯,然后我推了两个小时公式,果然错了,强颜欢笑,最后看着大佬的代码,倒推出了做法,还有就是,大佬对不起,虽然我是重写的,但是我看你的代码太久了,写出来之后结构跟你的很想,只是最后一部分更新倍数个数的时候,我用了不同的枚举方法,然后我还找不到你的代码的网页了···贴不出来了···。十分抱歉。

你可能感兴趣的:(算法)