解题报告之最大约数个数

 |   

本题的解法是在 基本算法之约数个数原理 的基础上对问题进行优化,从而大大降低算法的时间复杂度。而解题的思路是从结果去想它满足什么样的条件,从而获得优化问题的方法。

题目

题目来自微软2016年校招探星夏令营第二题 Divisor,并在 HiHoCoder上进行编程笔试,题目大意是:

给一个数n,$n < 10^{16}$,输出在$1 \sim n$之间约数最多的数,如果有多个相同最大约数个数的数,就输出最小的数

思路

  1. 暴力求解
    根据 基本算法之约数个数原理,我们可以很快的计算出给定的数 n 的约数个数。不过显然其时间复杂度是很大的。提交的结果是超时。

  2. 枚举优化
    假定 n 以内的所有质数为 $ p _1, \cdots , p _k $,那么对于 n 有 $$ n = \prod _{i=1}^k p _i^{a _i} = p _1^{a _1} * \cdots * p _k^{a _k}
    $$ , 其中 $ a _i \in [ 0, \lfloor \log _{p _i} n \rfloor ] $ ,这个就是约数个数原理。通过对这 k 个 $ a _i $ 的自由组合我们能够枚举 n 以内所有的数。 而本题要求的是正整数n的约数个数最多并且 n 尽量小。该目的就是我们优化剪枝 $ a _i $ 组合的出发点。

    • 约数个数最多
      对于正整数n的约数个数 $ f(n)= \prod _{i=1}^k (a _i+1) $ ,而要使 $ f(n)$ 更大,那么 $a _i$ 值要大并且 $ a _i == 0 $ 的数目要少,这样乘积才会更大,当然此时 n 也会更大,这里 $ a _i $ 是质数 $ p _i $ 的指数。这里的冲突点在于当要给某个 $ a _i $ 增加值时,是给小的 $p _i$ 增加个大值还是给大的 $p _i$ 增加个小值。简单的例子如下:对于4这个整数,乘上 19 和 2^4 得到的 $f(76)=6,f(64)=7$ ,而对于 8 这个整数,乘上 19 和 2^4 得到的 $f(152)=8,f(128)=8$ ,再对于 16 这个整数,乘上 19 和 2^4 得到的 $f(304)=10,f(256)=9$。该冲突让我们考虑到底要用哪些 $p _i$ 来构成这个约数个数最多的数。经过计算质数 $[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43]$ 依次相乘为 $1.3 \times 10^{16}$。显然对于正整数 $N < 10^{16}$ 以内约数个数最多的数 n 一定是由上面的 14 个质数组成。

      剪枝条件1:
      对于正整数 $N < 10^{16}$ 以内约数个数最多的数 n 一定是由这 [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43] 14个质数组成。

      简要证明
      假设最终的 n 中有约数大于 43 的质数 $p _k$,其中 $n<N$ ,那么上面的质数组里必定有一个质数 $p _j$ 的指数为 0,不然n一定会超出 N。那么此时用 $p _j$ 代替 $p _k$ 一定会获得比 n 更小的值 $n'$,那么n并不是最终的 n,这和假设矛盾。

    • n 尽量小
      当约数个数相同的时候,会取 n 最小的那个正整数。这也就意味着此时 k 个 $a _i$ 的取值已经确定了,但是每个 $a _i$ 对应的质数 $p _j$ 还没有确定( $i,j \in [0,k]$ ),不同的组合会构成不同的数,但是最小的正整数只能有一种组合,那就是最大的 $a _i$ 对应最小的 $p _j$。所以我们获得了另一个剪枝的条件。

      剪枝条件2:
      最终求解的拥有最大约数个数的数 n 一定满足:
      若 $p _1 < p _2 < \cdots < p _k$,则$a _1 > a _2 > \cdots > a _k$。

算法说明

  1. 剪枝条件 1 的应用
    定义静态常量数组,用来存放这 14 个质数

    1static const int little_prime[] = {
    22,3,5,7,11,13,17,19,23,29,
    331,37,41,43};
    
  2. 递归函数的声明
    函数功能是当第 len 位质数加入后,寻找约数个数最多的最小正整数 ,其参数如下:

    • N – 输入要寻找的 N 范围内
    • n – 求解的 n
    • counts – 约数的个数
    • len – 第 len 个质数
    • maxTimes – 第 len 个质数的最大指数
    1void FindMinN(long long N, long long n, int counts, int len, int maxTimes);
    
  3. 函数内部解析

    • numOfMinN – 全局变量,用来存储当前所找数中约数最多的值
    • minN – 全局变量,用来存储当前所有数中最小的n
     1// 当第len位质数加入后,寻找约数个数最多的最小正整数
     2void FindMinN(long long N, long long n, int counts, int len, int maxTimes){
     3    // 当找到约数个数更多或者约数个数相同但数更小的直接更新
     4    // update
     5    if(counts > numOfMinN || (counts == numOfMinN && minN > N)){
     6        numOfMinN = counts;
     7        minN = n;
     8    }
     9
    10    // find
    11    int i;
    12    // 遍历第len位质数,其指数为1,2,。。。,maxTimes
    13    for(i=1; i<=maxTimes; i++){
    14        n = n * little_prime[len];
    15        if(ans > N)
    16            break;
    17        // 遍历第len+1位质数,其指数的最大值为i -- 剪枝条件2
    18        FindMinN(N, n, counts*(i+1), len+1, i);
    19    }
    20}
    
  4. hiho_divisors完整代码

举一反三

  1. k维数组的遍历
    对本题的另一种解读是n维数组的遍历。这里每个$p _i$算一个维度,在每个维度上有$a _i+1$个可能的值。对这k个维度的遍历就是对比N小的正整数的依次判别。

    • 代码说明

       1// 每个维度取值个数
       2int varMax[]={
       310,9,8,7,6,
       45,4,3,2,1};
       5// 第ind维度取值的情况
       6void TraversalByRecu(int dim, int ind, int max, int var[]){
       7    // 取完dim个维度打印输出
       8    if(ind>=dim){
       9        for(int j=0; j<dim; j++){
      10            printf("%d\t",var[j]);
      11        }
      12        printf("\n");
      13        return;
      14    }   
      15    int i;
      16    // 第ind维度取完所有可能的取值
      17    for(i=1;i<=max;i++){
      18        var[ind]=i;
      19        // 取第ind+1维度,并设定第ind+1维度取值个数
      20        TraversalByRecu(dim,ind+1,varMax[ind+1],var);
      21    }   
      22}
      

      `

    • hiho_divisors_travel完整代码

  2. 区间里约数个数最大的尽量小的数 对于这个问题,其实和N以内很类似,唯一的区别可能就是在更新的时候要判断当前的 n 在不在区间范围内。

    • 代码说明

       1void FindMinN(long long low, long long high, long long n, int counts, int len, int maxTimes){
       2    // update
       3    if(n<=high && n>=low){
       4        if(counts > numOfMinN || (counts == numOfMinN && minN > n)){
       5            numOfMinN = counts;
       6            minN = n;
       7        }
       8    }   
       9    // find
      10    int i;
      11    for(i=1; i<=maxTimes; i++){
      12        n = n * little_prime[len];
      13        if(n > high)
      14            break;
      15            FindMinN(low, high, n, counts*(i+1), len+1, i);
      16    }   
      17}
      

      `

    • 源码

技术茶话会
< 前一篇 后一篇 >