C中素数分解的优化

计算科学 C
2021-11-29 21:05:38

我正在尝试通过解决项目 Euler 的问题来自学一些 C 语言编程。我试图找到数字 600851475143 的最大素数(欧拉问题 3)。我编写了一个代码,它成功地为不太大的数字做到了这一点:

#include <stdio.h>
int primearray(int,int);

main()
{
  int alpha, k, j, i=1, prime[200000], count = 0;
  prime[0]=2;

/* Define number which counts number of primes in interval [0,alpha] */
  int totprime=1;

  printf("Enter the number of which you want the prime factorization\n");
  scanf("%d",&alpha);

/* This procedure makes a list of all prime numbers up to alpha */
  for (j=3 ; j<alpha ; j++)
  {
    for (k=0 ; k<i ; k++)
    {
      if ((j/prime[k])*prime[k]==j)
        break;
      else
        count = count + 1;

    }
    if (count == i)
      {
      prime[i]=j;
      i = i + 1;
      count = 0;
      totprime = totprime + 1;
      }
    else
      count = 0;
  }

/* Print prime numbers */
  printf("The primes are:\n");
  for (j=0 ; j<totprime ; j++) printf("%d\n",prime[j]);

  printf("Total number of primes: %d\n",totprime);

  int primefac[totprime];

  for (j=0 ; j<totprime ; j++)
  {
    if ( (alpha/prime[j])*prime[j] == alpha)
      {
        primefac[count] = prime[j];
        count = count + 1;
      }
    else
      continue;
  }
printf("Primefactors are\n");
for (j=0 ; j<count ; j++) printf("%d\n",primefac[j]);
}

但它无法处理大量数据,可能是因为它的编程效率极低。我想知道您是否可以帮助我想出一种改进该程序的方法,使其能够在合理的时间内处理大量数据。欢迎所有提示!

4个回答

无论您的代码是否有效,它都不适用于编写的超过 32767 的任何数字。这是因为int数据类型是 16 位长度的有符号类型。一位用于符号,15 用于整数的值,使得可存储的最大整数2 15 -1=32767。如果您希望支持大于此的数字,则需要使用不同的整数类型,例如long. 假设您不需要存储负整数,您还应该使用无符号类型,例如unsigned long甚至unsigned long long可以分别存储高达 2 32 -1=14294967295 或 2 64 -1=18446744073709551615 的整数值。

编辑:
正如@Ahmed 在评论中指出的那样,C 标准没有指定整数类型的大小,因此可移植性可能是一个问题。为了确保你得到你期望的行为,我建议包括<inttypes.h>标题。<inttypes.h>包含<stdint.h>并包含额外的宏,尤其是与 I/O 相关的宏。如此处所述<stdint.h>定义类型:

int8_t    // 8 bit integer
int16_t   // 16 bit integer
int32_t   // 32 bit integer
uint8_t    // unsigned 8 bit integer
uint16_t   // unsigned 16 bit integer
uint32_t   // unsigned 32 bit integer

大多数实现还提供

int64_t   // 64 bit integer
uint64_t   // unsigned 64 bit integer

这些类型在架构和编译器之间是可移植的和可靠的。我认为在这种情况下使用外部库是多余的,但如果需要,您可以在评论中看到@Ahmed 的链接。

由于 Doug 已经指出需要更大的整数数据类型来解决您想要解决的问题,让我们来谈谈您的程序的逻辑和改进。

您的方法是 (A) 输入alpha作为要分解的数字,(B) 列出所有小于 的素数alpha,以及 (C) 查看其中哪些除法alpha并将其添加到数组primefac[ ]中。

这种因式分解技术是一个版本的试用部门这是一个很好的起点,至少当你知道它alpha本身不是素数时,你可能想从欧拉问题的上下文中假设。但是,在我们尝试分解它之前,有一些相对简单和快速的方法可以确保它alpha具有因子(是复合的),因此出于一般目的,您可能需要研究Miller-Rabin primality test但这可能是另一个问题。

在您的程序中修复的基本问题(在更长的数据类型之后)是检查重复的主要因素。一旦你确定一个素数除以alpha,你就继续下一个素数。这意味着您永远无法确定重复的素因数在 中出现多少次alpha,这对您的应用程序来说可能很好,但这也意味着您永远不会利用alpha任何将其分开的素因数来缩小它。正如我们在下一段中解释的那样,要检查的数字越小通常意味着要进行的检查越少。

您已经设定了检查alpha所有小于 的素数的可分性的目标alpha,但这太过分了。您真的只想检查小于或等于 的平方根的素数是否可整除alphaalpha在检查试除以小于或等于平方根的素数后留下的任何因子都alpha必须是素数。随着alpha下降的大小,它的平方根的大小也是如此,当试验除数变得大于这个下降目标(的平方根alpha)时,我们可以停止。

此外,您正在使用试用除法来构建素数列表以检查是否可整除alpha这是矫枉过正。您确实将 2 视为一种特殊情况(已知素数),但随后您开始检查所有以 3 开头的连续整数的素数。正如戈德里克上面评论的那样,在创建素数候选者时,您可以轻松地增加 2 而不是 1,因为永远不会有另一个偶素数。

事实上,对这些候选素性进行测试浪费了太多时间。您可以快速生成一些包含所有奇质数的奇数,并将它们用作您的试除数。即使这些将包括一些奇数的合数,您也将节省时间,因为您跳过了测试试除数的素数并直接测试alpha这些试除数的可除性。

所以我推荐这些步骤:

(1) 找出 2 是否整除alpha,如果是,则设置alpha = alpha/2untilalpha不再能被 2 整除。

(2) 使用上一步可能修改的值alpha,找出它是否能被 3 整除,如果是,则设置alpha = alpha/3untilalpha不再能被 3 整除。

(3) 从 5 开始,一直持续到试除数大于当前 的平方根,alpha通过交替相加 2 和 4 生成试除数。即生成 5,7=5+2,11=7+4,13 =11+2,17=13+4 等。如果alpha能被当前试除数整除T,则设置alpha = alpha/T直到alpha不再能被 整除T

(4)T大于 current 的平方根的停止准则alpha不需要平方根运算。alpha/T相反,检查是否小于T并在这种情况下停止是有意义的。这是相当有效的,因为您alpha/T在检查(精确)可分性的过程中无论如何都会计算,T因为您的代码已经这样做了,询问是否alpha == (alpha/T)*T.

摘要:更大的数据类型,没有素数列表,只检查alpha. 如果 的最终值alpha大于 1,则那是原始输入 的最大素因子alpha否则最大的素因数是最后一次成功的试除数T

检查你的尝试

由于您正在尝试自学 C,因此让我们实际向您展示代码中存在哪些问题,并展示如何利用 C 的完整功能正确解决此问题。

#include <stdio.h>

以下声明无用

int primearray(int,int);

下面的声明应符合 ANSI

main()

主函数()

{

您不应该将这么大的数组用作堆栈变量,实际上要么将其声明为全局变量,要么动态分配它(更好的解决方案)

  int alpha, k, j, i=1, prime[200000], count = 0;
  prime[0]=2;

/* Define number which counts number of primes in interval [0,alpha] */
  int totprime=1;

您应该将代码分成单独的函数,以便您可以简单轻松地处理每个部分

例如一个函数,它接受一个数字并使用任何方法找到所有素数,这样你就可以从一个简单的函数开始,当你学习更好的方法时,你实际上可以使这个例程更快。

  printf("Enter the number of which you want the prime factorization\n");
  scanf("%d",&alpha);

/* This procedure makes a list of all prime numbers up to alpha */

下面的块有很多问题。

  1. 首先它是次优的,因为它测试了太多的素数。

  2. 对素数的测试是不正确的,并且不会导致值总是等于 j,而应该是。此外,它非常占用 CPU,您可以使用模运算符更有效地获得结果

  3. 存储素数的测试很奇怪。

  4. 这整个块应该在它自己的功能中。

  for (j=3 ; j<alpha ; j++)
  {


    for (k=0 ; k<i ; k++)
    {
      if ((j/prime[k])*prime[k]==j)
        break;
      else
        count = count + 1;

    }

    if (count == i)
      {
      prime[i]=j;
      i = i + 1;
      count = 0;
      totprime = totprime + 1;
      }
    else
      count = 0;
  }

不要评论明显的代码代码显示您将打印素数添加评论是轻浮的并且有损

/* Print prime numbers */
  printf("The primes are:\n");

  for (j=0 ; j<totprime ; j++) printf("%d\n",prime[j]);

像上面那样使用 for 循环是不好的形式,你不应该在同一行使用 printf 语句。

一般来说:当你编码时,你的标签和块样式不一致你应该使用一致的方式来标记你的块代码的阅读频率远高于使用一致的样式编写有助于遵循逻辑

  printf("Total number of primes: %d\n",totprime);

C 不支持动态数组。下面的代码不是标准的。如果您的平台/编译器支持它,您在移植代码时会得到不一致的结果。

  int primefac[totprime];

下面的块有很多问题。

  for (j=0 ; j<totprime ; j++)
  {

这个测试会失败,因为你假设整数除法是如何工作的(以及它是否会导致分数)更不用说这是一个非常昂贵的操作

    if ( (alpha/prime[j])*prime[j] == alpha)
      {
        primefac[count] = prime[j];
        count = count + 1;
      }

任何逻辑都完全不需要下面的 else

    else
      continue;
  }


printf("Primefactors are\n");

和上面一样的问题

for (j=0 ; j<count ; j++) printf("%d\n",primefac[j]);

这应该与主要功能一致}

/* 仍在编辑 */ 正确的方法即将出现。

好的,现在让我们弄清楚如何正确解决这个问题。

简单说明问题

您想找到 600851475143 的最大素数。

也就是说你要找到N的最大值

6008514751430(modN)|N0(modp)pZ+

在 C 中,模运算符 (%)(实际上是余数运算符)提供了必要的功能。

那么我们如何做到这一点,首先让我们创建一个函数来做到这一点。我们知道如果 $ a \times b \equiv N \; \forall {a, b} \in \mathbb{Z}^+ $ 然后 $ a \leq \sqrt{N} $ 或 $ b \leq \sqrt{N} $a×bNa,bZ+ then either aN or bN

因此,一个简单(粗略)的算法将是查看是否存在一个素数 $p$,使得:$\left。p \leq \sqrt{600851475143} \; \大| \; p \, \vert \, 600851475143 \对。$p such that: p600851475143|p|600851475143

正确的做法

我不会为您提供解决方案,但会引导您了解一些基本概念。

首先,将事物分成更小的块并将每个块视为一个函数是一个好主意。如果你一步一步定义你的方法,那么一个好的经验法则是每一步创建一个函数。如果这是一个复杂的步骤,具有更简单的子步骤,那么该功能应该分为多个子功能。你可能第一次就做错了,没关系,随着你对事情应该如何划分的进行,你会发展出更好的判断力。

好的,让我们使用我们使用流程图的简单算法逐步查看问题

计算除以合数的最大素数的粗略算法流程图

现在基本上上面流程图中的每个框都指向您的主要功能的语句:

/** 
 * name: primes_upto
 * parameters:
 *    M: finds all primes until M and stores them in zero-terminated list
 *    list: the pointer to an array of ints
 *          if list is NULL then it will allocate enough memory 
 *          to store the primes + 1 
 *          if list is not NULL it will truncate insert a 0 at 
 *          the index where value of prime is > M
 *
 * return value: returns number of primes on success or 0 on failure
 */
int primes_upto(int k, int **list); 

int main() {

      const int original_N = 600851475143;
      int N, a, S;
      int *L = NULL;    /* list of primes we need to fill */
      int *ap = NULL; /* a memory pointer to integers that will point to the next prime */

      N = original_N;

      do {
          S = sqrt(N); /* we have to provide this */

          primes_upto(S, &L); /* we have to provide this function */

         /* we will make sure that the primes_upto function 
            injects a zero at the end of the list. Since zero is not a prime
            we can use it as a marker to indicate the end of the list 
            and that way we don't have to worry about how long the list is. */


         for (ap = L; ap && *ap != 0; ap++) {
              int a = *ap;
              if (N % a == 0) {
                  N = N / a;
                  break;
              }
         }

      } while (ap && *ap != 0); 

      printf ("The largest number that divides %d is %u\n", original_N, N);

}

您应该考虑使用模数 (%) 运算符而不是 (j/prime[k]*prime[k] == j) 说 (j%prime[k] == 0)。我知道这在 C++ 中受支持,如果 C 中不存在,请纠正我。

此外,您只需要检查直到目标数平方根的素数。因此,假设您想分解一个数字 N,您只需要不超过 sqrt(N) 的素数,因为它保证如果该数字不是复合数,则该区间上将存在一个因子。

这是一个例子:

假设您想找到所有小于 100 的素数。我们只需要通过您选择的过程生成素数 2、3、5、7。然后如果你写出从 8 到 100 的所有数字,并且只是划掉 2、3、5 和 7 的倍数(不考虑任何额外的素数),唯一不会被划掉的数字将是剩下的素数数字。

因此,为了简化您的工作:

列出每个小于 775146 的素数

并在生成它们时根据 600851475143 检查每一个。

为了处理大型数值数据类型,我建议要么从外部导入,要么:

看一下向量类(那是c++),如果那是限制,那么您将要创建一个数值类型函数,该函数本质上将字符串('101101343433')转换为数字链接列表,然后创建functinos用于在这个链表设备上进行加、减、除和乘。

最后,如果您真的需要高速。

当您生成素数列表时,将每 k 个连续素数相乘(选择 k 以使产生的数字的平均大小与您要考虑的数字具有相同的位数)。然后找到这个目标数的 GCF 和你的 k 个素数组相乘。这里的妙语是,不必进行 k 个单独的模数检查(是 Num % prime == 0),或者在您的情况下 k 个单独的除法和乘法,您可以进行 k 个较小的乘法(因为它们是较小的数字),从而节省一些时间。应该使用欧几里得算法来寻找 GCF。

这是:

给定 A, B 找到 GCF(a,b)

while(true): if( A >= B)
A = A %B;

if A == 0 
   return B

如果( B >= A) B = B%A;

如果 B == 0 返回 A

因此这里是一个检查数字 143 的因数的例子。

列表

2,3,5,7,11(这些都是小于 sqrt(143) 的素数,使用与代码中相同的 eratosthenes 协议生成(eratosthenes 的筛子是您实现的试验除法算法的正式名称) )

团体:

2x3x5 = 30, 5x7x1​​1 = 385

GCF(143,30) = 1 GCF(143,385) = 11

11 = 系数

143/11 = 13

13 是另一个因素。

13 > 11

13 是最大的因素。