桃子的算法笔记——反素数详解(acm/OI)

桃子的算法笔记——反素数详解(acm/OI)

文章已更新。

感谢 @stratoes 指出此文的错误,另外hdu2521数据存在一些问题。详见题型第三类。

再次感谢!

——————————————————————


做了POJ2286之后,发现题目要求求出1-n因数最多的那个数。一时做题做傻了(orz)没清晰的想到合适的方法,后来想到了是反素数。

顺便总结了常见的和反素数有关的题型(个人觉得反素数只是作为题目的某一部分出现),还请各位dalao轻拍。

其实顾名思义,素数就是因子只有两个的数,那么反素数,就是因子最多的数(并且因子个数相同的时候值最小),所以反素数是相对于一个集合来说的。

我所理解的反素数定义就是,在一个集合中,因素最多并且值最小的数,就是反素数。

那么,如何来求解反素数呢?

首先,既然要求因子数,我首先想到的就是素因子分解。把n分解成

的形式,其中p是素数,k为他的指数。这样的话

就是总因子个数。

但是显然质因子分解的复杂度是很高的,并且前一个数的结果不能被后面利用。所以要换个方法。

我们来观察一下反素数的特点。

  1. 反素数肯定是从2开始的连续素数的幂次形式的乘积。
  2. 数值小的素数的幂次大于等于数值大的素数,即

中,有


解释:第一条:如果不是从2开始的连续素数,那么如果幂次不变,把素数变成数值更小的素数,那么此时因子个数不变,但是n的

数值变小了。交换到从2开始的连续素数的时候n值最小。

第二条:如果数值小的素数的幂次小于数值大的素数的幂,那么如果把这两个素数交换位置(幂次不变),那么所得的n因子数量不变,但是n的值变小,直到符合

条件。

另外还有两个问题,

1.对于给定的n,要枚举到哪一个素数呢?

最极端的情况大不了就是

,所以只要连续素数连乘到刚好小于等于n就可以的呢。再大了,连全都一次幂,都用不了,当然就是用不到的啦!

2.我们要枚举到多少次幂呢?

我们考虑一个极端情况,当我们最小的素数的某个幂次已经比所给的n(的最大值)大的话,那么展开成其他的形式,最大幂次一定小于这个幂次。unsigned long long 的最大值是2的64次方,所以我这边习惯展开成2的64次方。

细节有了,那么我们具体如何具体实现呢?

我们可以把当前走到每一个素数前面的时候列举成一棵树的根节点,然后一层层的去找。找到什么时候停止呢?

1.当前走到的数字已经大于我们想要的数字了

2.当前枚举的因子已经用不到了(和1重复了嘻嘻嘻)

3.当前因子大于我们想要的因子了

4.当前因子正好是我们想要的因子(此时判断是否需要更新最小ans)

然后dfs里面不断一层一层枚举次数继续往下迭代就好啦~~

常见题型有下面这两种。

1.给定因子数,求满足因子数恰好等于这个数的最小数。

题目链接:codeforces.com/problems

对于这种题,我么只要以因子数为dfs的返回条件基准,不断更新找到的最小值就可以了

上代码:

#include<stdio.h>
#define ULL unsigned long long
#define INF ~0ULL
int p[16] = {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53};
 
ULL ans;
int n;
 
//depth:当前在枚举第几个素数。num:当前因子数。
//temp:当前因子数量为num的时候的数值。up:上一个素数的幂,这次应该小于等于这个幂次嘛
void dfs(int depth,int temp,int num,int up){
    if(num > n||depth >= 16) return;
    if(num == n&&ans > temp){
        ans = temp;
        return;
    }
    for(int i = 1;i <= up;i++){
        if(temp/p[depth] > ans) break;
        dfs(depth+1,temp = temp*p[depth],num*(i+1),i);
    }
}
 
 
int main(){
    while(scanf("%d",&n) != EOF){
        ans = INF;
        dfs(0,1,1,64);
        printf("%d\n",ans);
    }
    return 0;
}
 

如有问题请留言。

2.给定一个n,求n以内因子数最多的数。

acm.zju.edu.cn/onlineju

思路同上,只不过要改改dfs的返回条件。注意这样的题目的数据范围,我一开始用了int,应该是溢出了,在循环里可能就出不来了就超时了。上代码,0ms过。注释就没必要写了上面写的很清楚了。

#include<cstdio>
#include<iostream>
#define ULL unsigned long long
 
int p[16] = {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53};
ULL n;
ULL ans,ans_num;//ans为n以内的最大反素数(会持续更新),ans_sum为ans的因子数。
 
void dfs(int depth,ULL temp,ULL num,int up){
    if(depth >= 16||temp > n)return;
    if(num > ans_num){
        ans = temp;
        ans_num = num;
    }
    if(num == ans_num&&ans > temp)
        ans = temp;
    for(int i = 1;i <= up;i++){
        if(temp*p[depth] > n)break;
        dfs(depth+1,temp *= p[depth],num*(i+1),i);
    }
    return;
}
 
 
int main(){
    while(scanf("%lld",&n) != EOF){
        ans_num = 0;
        dfs(0,1,1,60);
        printf("%lld\n",ans);
    }
    return 0;
}

3.如果要求的是一个区间

的反素数,并不是一个连续区间

那么不能用上面的代码了。

Problem - 2521acm.hdu.edu.cn



此时选出的数字并不是严格意义上的反素数。不满足性质1,2.比如区间【10,11】感谢大佬!!!但是由于区间特别特别小,所以可以暴力求解。

其余方法暂时还没想出来。




斜体为错误分析

这时有两个地方需要改动。

1.要在更新的时候加一个判断条件,即更新的数字是否在区间

内。

2.由于这时的数字已经不是严格意义上的反素数,所以不再符合性质2。因为遵循这个条件不一定能枚举到区间所有的数字。

对于性质一,其实是可以的,因为没有两个连续不能分解的数字,一旦给定区间,肯定有能够分解的数字。

错误的AC代码:

#include<cstdio>
#include<iostream>
#define ULL unsigned long long
#define INF ~0
 
int p[16] = {2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53};
ULL n,m;
ULL ans,ans_num;//ans为m,n内的最大反素数(会持续更新),ans_num为ans的因子数。
 
 
void dfs(int depth,ULL temp,ULL num){
    if(depth >= 7||temp > n)return;
    if(temp >= m&&num > ans_num){
        ans = temp;
        ans_num = num;
    }
    if(num == ans_num&&ans > temp&&temp >= m)
        ans = temp;
    for(int i = 1;;i++){
        if(temp > n/p[depth])break;
        dfs(depth+1,temp *= p[depth],num*(i+1));
    }
    return;
}
 
 
int main(){
    int T;
    scanf("%d",&T);
    while(T--){
        scanf("%lld%lld",&m,&n);
        if(m == n){
            printf("%lld\n",m);
            continue;
        }
       ans = INF;
        ans_num = 0;
        dfs(0,1,1);
        printf("%lld\n",ans);
    }
    return 0;
}

QAQ

编辑于 2018-08-14

文章被以下专栏收录