专栏文章

题解:P9865 [POI 2021/2022 R2] lic

P9865题解参与者 1已保存评论 0

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
0 条
当前快照
1 份
快照标识符
@minmhuyi
此快照首次捕获于
2025/12/02 04:49
3 个月前
此快照最后确认于
2025/12/02 04:49
3 个月前
查看原文

P9865 [POI 2021/2022 R2] lic

题目大意

对于一个正整数 nn,将所有与 nn 互质的数从小到大排列,求此列表中的第 kk+c1k∼k+c−1 个数。

思路描述

本质上,想要完成题目的要求,求出在 kk 后的 cc 个与 nn 互质的自然数,其实只需要反过来,求出范围内不和 nn 互质的数(即和 nn 的最大公约数大于 11 )的集合,在该集合外的,就是我们要求的。

Part 1

因此第一步十分寻常,在 n\sqrt{n} 的时间复杂度下求出 nn 的质因数,存起来(后文统一为该数组命名为 PP,并将 PP 的长度命名为 lenlen)。
接下来,我们又惊喜地发现:长度 cc 的范围为: 1c1000001 ≤ c ≤ 100000 !想想就知道,与 nn 互质的数排列一定不会十分稀疏,因此只需要找到答案的开头(即序列中的第 kk 个),然后从它开始暴力枚举出接下来的 cc即可。
而如果只想找到开头,就不难想到:二分答案
那么重点就在于:如何写 check 函数。

Part 2

这一部分的写法可能有点抽象,需要用到位运算,如果对这方面的知识有疑问,详情可见这里
直接讲思路,简单说就是:我们需要对于每个自然数 xx,检查它是否包含了 kk 个及以上的与 nn 互质的数,如果是,则二分中的 rr 减小,否则 ll 增大。
进一步,要想得到答案,刚才处理好的 PP 数组就有用了,但因为该数组里的因数在范围内大概率有重复,因此整个问题就变成了容斥原理——即处理 nn 的质因数数组中的元素的倍数的交集,因此需要使用集合,如果对这方面不清楚,可以看这里
那么如何高效地对一个集合进行操作呢?此处提供一个十分具有启发性的方法,有点像状压:这里,此处就只讲一下和本题有关的关键点:
我们可以对 PP 数组枚举每个因数的组合,因此参考刚才链接中的方法,枚举所有的状态 plansplans (表示为整形变量,转化为二进制后对于从低往高的第 ii 位,该位为 11 则表示要选择因数 PiPi,否则不选,因此枚举 plansplans 的最大取值不到 2len2^{len},因为选择的 PP 数组长度仅有 lenlen,则 plansplans 最大时二进制下每一位都为 11,即 2len12^{len}-1)。而我们在每次枚举中要做的,就是读取 plansplans 二进制下的每一位,并记录一个临时乘积 nownow,表示为若干个 PiPi 相乘
那么, plansplans 有了,方案乘积 nownow 如何记录呢?或者从根本上说,怎么高效读取 plansplans 从低往高的第 ii 位呢?其实很简单,再暴力枚举一个需要查看的 ii(从 00len1len-1),考虑进行位运算:
读取 plansplans 从低往高的第 ii 位分两步:先砍掉 plansplans 的后 i1i-1 位(plansplans 右移 ii 位),再取剩下的数的最后一位(与 11 进行与运算)——总的来说:plans>>i&1就是我们求的。接下来,若取到的值为 11nownow 乘上 PiPi 即可。
而每次枚举 plansplans 的最后,我们则需要维护一个 cntcnt记录当前被检查的数 xx 包含了多少个与 nn 互质的数,由于当前枚举的数为 nownow,因此它在 xx 范围中出现了 xnow\lfloor\frac{x}{now}\rfloor,可是就像我们前面提到过的, xx 当中大概率有不止一个数为 nownowkk 倍,因此需要考虑容斥,算出当前的 nownow 在答案中的贡献为正或是为负即可。
那么具体考虑一下,不难想到,若我们需要计算所有 nownow 的并集,则当取了奇数个 PiPi 时,对于答案的贡献就一定为正,否则说明该集合为某两个集合的交集,贡献为负。而我们现在要求与 nn 互质的数的集合,因此该集合为刚才的集合的补集,所以应反过来:当取了奇数个 PiPi 时,对于答案的贡献为负,否则贡献为正。详如图解:
图解集合
(实现中可以在计算 nownow 时统计 cntcnt,也可以使用 GCC 自带的 __builtin_parity 函数统计 plansplans 二进制下 11 的个数的奇偶性
因此,cntcnt 每次应加上:xnow{1xO1xE(x,nowN+)\lfloor\frac{x}{now}\rfloor \cdot \begin{cases} -1 & x \in \mathbb O \\ 1 & x \in \mathbb E \\ \end{cases} (x,now \in \mathbb N^+)

Part 3

最后,按我们之前设想的,通过__gcd函数从二分的答案开始枚举出 cc,然后输出,代码记得long long

代码详解

CPP
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,k,c,t,l,r;
vector<int> p,ans;
bool check(int x){
	int cnt=0,now;
    //blk同刚才讲的plans
	for(int blk=0;blk<(1<<t);blk++){
		now=1;
        //计算被选中的Pi乘积now
		for(int i=0;i<t;i++)
			if(blk>>i&1) now*=p[i];
        //集合计算
		cnt+=(x/now)*(__builtin_parity(blk)?-1:1);
	}
    //check返回
	return cnt>=k;
}
signed main(){
	freopen("prime.in","r",stdin);
	freopen("prime.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>k>>c;
    //预处理质因数P
	int now=n;
	for(int i=2;i*i<=now;i++)
		if(now%i==0){
			p.push_back(i);
			while(!(now%i)) now/=i;
		}
	if(now!=1) p.push_back(now);
    //二分答案
	t=p.size(),l=0,r=1e18;
	while(l+1<r){
		int mid=(l+r)/2;
		if(check(mid)) r=mid;
		else l=mid;
	}
	now=r;
    //从r枚举出所有答案
	while(ans.size()<c){
		if(__gcd(now,n)==1) ans.push_back(now);
		now++;
	}
	for(int i=0;i<c;i++) cout<<ans[i]<<" ";
	return 0;
}

评论

0 条评论,欢迎与作者交流。

正在加载评论...