Loading

2.20模拟赛T3解析

零.送给未来复习的自己:

如果一段时间后看不懂自己的题解了,可以尝试拿\(n=12\)这一样例模拟,因为\(n=2^2\times 3^1\)只有两个不同的质因子,并且一个是指数只有\(2\),一个指数只有\(1\),所以可以画一个大小为\((2+1)\times(1+1)\)的二维表格来辅助理解容斥部分

一.题意:

对于给定的\(n\),求有多少集合\(\{1,2,3...,n\}\)的子集满足最大公约数\((gcd)\)\(1\),而最小公倍数\((lcm)\)\(n\)。答案对\(998244353\)
取模。

二.思路:

首先看到\(n\leq 10^{18}\)这一逆天范围,又结合题目中的数论元素,我们考虑对\(n\)质因数分解:

\[n=\prod_{i=1}^{r}p_i^{a_i} \]

现在转化题目,考虑什么时候满足题意,通过gcd和lcm的形式化定义我们发现,\(n\)的每一个指数\(a_i\)都至少在这个子集中出现一次,换句话说也就是,对于每一个\(i\),子集中至少存在一个数包含指数\(a_i\)。同理,对于每一个\(i\),子集中一定至少包含一个数使得\(p_i\)的指数为\(0\)

现在我们考虑\(n\)只包含\(1\)个质因子的特殊情况,你发现我们如果考虑总情况数,他就包含\(2^{a+1}\)种方案,而我们应当减去不包含\(0\)和不包含\(a\)的方案,方案数分别都是是\(2^a\),而根据容斥原理,我们应当把两个都不包含的方案加回来,即:\(2^{a-1}\)

我们思考,上述过程是否可以拓展呢?

显然是可以的,我们对于每一个\(i\in[1,r]\)都去考虑上述四种情况,枚举每个质因数每次是哪种选取方式,再套用容斥原理即可,发现上面有四种方案,所以复杂度大概是在 \(O(\omega(n)\cdot 4^{\omega(n)})\) 的,(\(\omega(n)\)指的是\(n\)中包含的不同的质因子数量),可以通过前\(60\)分的测试点。

考虑优化,我们发现不包含\(0\)和不包含\(a_i\)其实方案数是一样的且对于每一次计算都是对称出现的,所以我们可以把他们考虑成一种情况,最终答案乘以二就好了,这样就优化成了 \(O(\omega(n)\cdot 3^{\omega(n)})\) 的复杂度,那这样你就可以拿到\(80\)分。具体怎么在实现容斥的过程中体现某个质因子的选取状态呢?其实我们可以类比二进制状态压缩的思想生成一个三进制数去存储当前的状态,\(0\)表示这一个质因子没有限制,\(1\)表示我们不选\(0\)或者不选\(a_{i}\)的方案数,\(2\)表示我们两个都不选。
这样你会发现非常方便,我们对于每一个质因子,他的选取方案就是:

\[2^{a_i+1-numbit_i} \]

其中\(numbit_i\)表示这一位的三进制数是多少,所以对于最后所有的质因子我们就可以列出方案:

\[2^{\prod_{i=1}^r(a_i+1-numbit_i)+cnt_1} \]

其中\(cnt_1\)表示在三进制状态集合下总共有多少质因子状态为\(1\),最后输出方案即可。

code for 80pts:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define int long long
inline int read() {
	int x=0,f=1;char ch=getchar();
	while(ch > '9' || ch < '0'){if(ch == '-'){f = -1;}ch = getchar();}
	while(ch >= '0'&&ch <= '9'){x = x * 10 + ch - 48; ch = getchar();}
	return x * f;
} 

const int MOD = 998244353;
const int PRIME_NUM = 20;
int n,a[PRIME_NUM],cnt,three_shift[PRIME_NUM],ans,bit_3;

void divide(int x){
	int sq = std::sqrt(x);
	for(int i = 2;i <= sq;++i)
	{
		if(x % i == 0)
		{
			int res = 0;
			while(x % i == 0) x /= i,++res;
			a[++cnt] = res;
		}
	}
	if(x == 1) return ;
	a[++cnt] = 1;
}

int qpow(int a,int b)
{
	int res = 1;
	while(b)
	{
		if(b & 1) res = (res * a) % MOD;
		a = (a * a) % MOD;
		b >>= 1;
	}
	return res;
}

void change(int x)
{
	for(int i = 1;i <= bit_3;++i) three_shift[i] = 0;
	bit_3 = 0;
	while(x)
	{
		three_shift[++bit_3] = x % 3;
		x /= 3;
	}
}

signed main()
{
	n = read();
	divide(n);
	
	int up_limit = qpow(3,cnt);
	for(int i = 0;i < up_limit;++i)
	{
		change(i);
		int res = 1,cnt_parity = 0,cnt_1 = 0;//cnt_parity判断最终容斥系数奇偶 
		for(int j = 1;j <= cnt;++j)
		{
			res = res * (a[j] - three_shift[j] + 1) % MOD;
			if(three_shift[j] == 1) ++cnt_1;
			cnt_parity += three_shift[j];
		}
		int res1 = qpow(2,res + cnt_1);
		if(cnt_parity % 2) ans = (ans - res1 + MOD) % MOD;
		else ans = (ans + res1) % MOD; 
	}
	std::cout << ans;
	return 0;
}

所以问题来了,为什么只有\(80\)分呢,看着上面的复杂度很可以过啊。其实真正的瓶颈在于找质因子这一部分,他的复杂度是根号级别的,而\(n\)的上界达到了\(10^{18}\)这显然是无法接受的,所以需要新的,更快的算法工具辅助我们获取质因子。这里考虑使用miller-rabin算法,你可以永远相信随机数的速度。

tips:在最终算答案的时候,我们最好使用欧拉定理来降幂,否则时间可能会超过\(2s\),(反正我是第一次没用没有过最后两个点)。

code for 100pts

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define int long long
inline int read() {
	int x=0,f=1;char ch=getchar();
	while(ch > '9' || ch < '0'){if(ch == '-'){f = -1;}ch = getchar();}
	while(ch >= '0'&&ch <= '9'){x = x * 10 + ch - 48; ch = getchar();}
	return x * f;
} 

const int MOD = 998244353;
const int PRIME_NUM = 20;
int n,a[PRIME_NUM],cnt,ans,Cnt,pri[1000010];

int qpow(__int128 a,int b,int P)
{
	int res = 1;
	while(b)
	{
		if(b & 1) res = (res * a) % P;
		a = (a * a) % P;
		b >>= 1;
	}
	return res;
}

bool check(int a,int b)
{
	int tmp = b - 1;
	if(qpow(a,tmp,b) != 1) return 0;
	while(tmp % 2 == 0)
	{
		tmp >>= 1;
		if(qpow(a,tmp,b) == b - 1) return true;
		if(qpow(a,tmp,b) != 1) return false;
	}
	return true;
}

bool check_prime(int x)
{
	for(int i = 1;i <= 12;++i)
	{
		if(!check(pri[i],x)) return false;
	}
	return true;
}

bool vis[1000010];
void divide(int x)
{
	for(int i = 2;i <= 1e6;++i)
	{
		if(!vis[i]) pri[++Cnt] = i;
		for(int j = 1;j <= Cnt && pri[j] * i <= 1e6;++j)
		{
			vis[i * pri[j]] = true;
			if(i % pri[j] == 0) break;
		}
	}
	
	for(int i = 1;i <= Cnt;++i)
	{
		if(x % pri[i] == 0)
		{
			++cnt;
			while(x % pri[i] == 0) x /= pri[i],++a[cnt];
		}
	}
	if(x > 1)
	{
		int t = (int)sqrt(x);
		if(t * t == x)
		{
			a[++cnt] = 2;
		}
		else
		{
			a[++cnt] = 1;
			if(!check_prime(x)) a[++cnt] = 1;
		}
	}
}

signed main()
{
	n = read();
	divide(n);
	
	int up_limit = qpow(3,cnt,MOD);
	for(int i = 0;i < up_limit;++i)
	{
		int res = 1,cnt_parity = 0,cnt_1 = 0,tmp = i;//cnt_parity判断最终容斥系数奇偶 
		for(int j = 1;j <= cnt;++j)
		{
			int now = tmp % 3;
			if(now == 1) ++cnt_1;
			cnt_parity += now;
			res = res * (a[j] + 1 - now);
			tmp /= 3; 
		}
		int res1 = qpow(2,(res + cnt_1) % (MOD - 1),MOD);//数论欧拉定理 
		if(cnt_parity % 2) ans = (ans - res1 + MOD) % MOD;
		else ans = (ans + res1) % MOD; 
	}
	std::cout << ans;
	return 0;
}
posted @ 2025-03-05 08:25  AxB_Thomas  阅读(15)  评论(0)    收藏  举报
Title