#include<stdio.h>
int main()
{
printf("|---------- PROGRAM FOR AMICABLE NUMBERS.----------|");
int num1,num2,sum=0;
for(num1=1; num1<=10000; num1++)
{
for(num2=1; num2<=10000; num2++)
{
if ((num1==sum_of_divisors(num2)) && (num2==sum_of_divisors(num1)) && num1!=num2)
{
printf("\n%d\t\t%d", num1,num2);
}
}
}
return 0;
}
int sum_of_divisors(int n)
{
int sum=0,i;
for(i=1; i<n; i++)
{
if(n%i==0)
{
sum=sum+i;
}
}
return sum;
}
我想提高性能-大约需要10分钟才能找到前4个对。
#1 楼
printf("|---------- PROGRAM FOR AMICABLE NUMBERS.----------|");
这种横幅消息使在管道中使用程序的输出变得更加困难。我建议删除它(用户已选择运行它;让他们知道他们在做什么!)。
int num1,num2,sum=0;
sum
的作用是什么?似乎没有使用。 for(num1=1; num1<=10000; num1++)
10000
的神奇价值从何而来?它应该是一个命名常量(或者更好,可以指定为命令行参数)。 for(num2=1; num2<=10000; num2++)
我们真的需要考虑双向的每一对吗?我们可以简单地迭代
num2 < num1
(这也节省了以后的比较)。但是请参阅下面的内容,为什么根本不需要此循环。 if ((num1==sum_of_divisors(num2))
sum_of_divisors()
没有可见的原型-启用相关的编译器警告并提供原型(例如,通过将定义移至前面) main()
)。 for(num2=1; num2<=10000; num2++)
{
if ((num1==sum_of_divisors(num2))
&& (num2==sum_of_divisors(num1))
&& num1!=num2)
在内部循环中,
sum_of_divisors(num1)
是一个常数,因此我们可以将其保存在外部循环中,并进行第一个比较(以便将&&
短路会然后仅评估一次sum_of_divisors(num2)
)。然后观察到我们有一个循环来测试其索引是否等于特定值-这意味着我们用一个测试替换了内部循环: for (num1 = 1; num1 <= 10000; num1++) {
num2 = sum_of_divisors(num1);
if (num1 < num2 && sum_of_divisors(num2) == num1) {
/* we found a pair */
}
}
那里的
num2 < num1
测试为我们节省了重新查找的时间对,我们已经见过。当然,num2 > num1
也可以使用-我选择此版本,以便我们按照惯例从低到低的顺序打印每对。 printf("\n%d\t\t%d", num1,num2);
最好始终结束输出以换行符开头,而不是以换行符开头。这在可能发出错误或其他诊断信息的程序中效果更好,并且与行缓冲输出(例如交互式终端(换行符会导致刷新))配合使用时效果很好。
for(i=1; i<n; i++) if(n%i==0) sum += i;
这是积累因子的一种非常低效的方法。您可以使用
sum += i + n/i
将其减小到范围(1,√n)(稍作调整以避免√n
为正方形时重复计数n
);最好是生成所有主要因子,然后使用它们来生成复合因子。进行了上述改进之后,我发现运行时间低于我的测量阈值。将限制增加到一百万可以使运行时间大约为2秒-这可能是可以接受的;如果不是这样,则问题可以很好地并行化(除了输出结果之外)。
修改版本
#include <stdio.h>
#if USE_LONG_TYPE
typedef unsigned long Number;
#define FMT "%lu"
#else
typedef unsigned int Number;
#define FMT "%u"
#endif
static const Number MAX_VALUE = 1000000;
Number sum_of_factors(Number n)
{
Number sum = 1;
Number i = 2;
for (; i < n/i; ++i) {
if (n/i*i == n) {
sum += i + n/i;
}
}
if (i*i == n)
sum += i; /* add square root only once */
return sum;
}
int main()
{
//#pragma omp parallel for
for (Number num1 = 1; num1 <= MAX_VALUE; ++num1) {
Number num2 = sum_of_factors(num1);
if (num2 > num1 && num1 == sum_of_factors(num2)) {
printf(FMT "\t\t" FMT "\n", num1, num2);
}
}
}
评论
\ $ \ begingroup \ $
是否可能由于测试num2> num1而跳过了对(n,n)? (我会写为num2> = num1或将相等作为特殊情况处理)
\ $ \ endgroup \ $
– SylvainD
18年1月9日在22:15
\ $ \ begingroup \ $
i * i
–chux-恢复莫妮卡
18-1-10的3:48
\ $ \ begingroup \ $
第二个i * i == n可以避免使用OF。我认为(n%i == 0)是否应该工作。
\ $ \ endgroup \ $
–chux-恢复莫妮卡
18年1月10日在4:18
\ $ \ begingroup \ $
@Josay:“是否有可能跳过一对(n,n)...?”这样的(n,n)不是一个“对”,而是一个数字!这样的数字n(其除数之和等于数字n本身)被称为“完美”。根据定义,完美数字不是友好数字,因此跳过它们是正确的。
\ $ \ endgroup \ $
– Quuxplusone
18年1月10日在8:03
\ $ \ begingroup \ $
@chux,我已经更新了我的版本,以避免循环约束中的溢出。作为奖励,它可以提高速度,因为我们可以在除数测试中重复使用n / i(令人惊讶的是,尽管n / i和n%i是同一除法的结果,但我得到了近2倍的加速-以为现代x86处理器产生了商和余数?也许GCC找不到连接?)。
\ $ \ endgroup \ $
– Toby Speight
18年1月10日在10:06
#2 楼
for(num1=1; num1<=10000; num1++)
{
for(num2=1; num2<=10000; num2++)
{
if (... && (num2==sum_of_divisors(num1)) && ...)
这里有一个明显的优化,它将使您的速度提高大约10000倍...
int sum_of_divisors(int n)
{
int sum=0,i;
for(i=1; i<n; i++)
{
if(n%i==0)
{
sum=sum+i;
这不是最快的计算方法。
优化1:仅循环至
i*i <= n
并使用以下事实:如果n / i = j
则n / j = i
。优化2:给定素数分解,除数之和的公式是什么?优化3:您实际上不想要一个数的除数之和,但所有的除数不超过
N
。如何有效地调整Eratosthenes的筛子,以有效计算N
以下所有数字的除数之和?评论
\ $ \ begingroup \ $
你是说我<= n / 2吗?因为i * i <= n给出错误的结果。假设n = 10,则循环将在i = 4处停止,但5等于10。
\ $ \ endgroup \ $
–拉胡尔
18年1月9日在16:02
\ $ \ begingroup \ $
该句子包含由和连接的两半。如果您实施的是前半部分而不是后半部分,那么事情将会出错。
\ $ \ endgroup \ $
– Peter Taylor
18年1月9日在16:07
\ $ \ begingroup \ $
@CrisLuengo,如果您认为只是暗示将sum_of_divisors(num1)从循环中拉出,则您已经严重地遗漏了这一点,即内部循环根本不需要成为循环。
\ $ \ endgroup \ $
– Peter Taylor
18年1月9日在21:24
\ $ \ begingroup \ $
@PeterTaylor,是的,我错过了。一旦看到它就很明显。 :)
\ $ \ endgroup \ $
–克里斯·伦戈(Cris Luengo)
18年1月9日在21:48
\ $ \ begingroup \ $
前几天,我看到了您的“优化3”提示,这让我挠头。我终于有了AH-HA的时刻。做得好!
\ $ \ endgroup \ $
– brian_o
18年1月11日在16:18
#3 楼
请尝试以下操作(速度更快):int num, num2, num3;
for (num1 = 1; num1<=10000; num1++)
{
num2 = sum_of_divisors(num1);
if (num1 < num2) /* at least one of any amicable pair is larger */
{
num3 = sum_of_divisors(num2);
if (num3 == num1) /* FOUND AN AMICABLE PAIR! */
... (record the pair) ...
}
}
评论
\ $ \ begingroup \ $
超级快。甚至可以在5秒内计算到第8对。
\ $ \ endgroup \ $
–拉胡尔
18年1月9日在18:27
\ $ \ begingroup \ $
致谢,因为您是第一个为此解决方案编写代码的人。它仅由Peter Taylor暗示,Toby Speight仅在以后的编辑中写过。
\ $ \ endgroup \ $
–Cœur
18年1月10日,下午2:12
\ $ \ begingroup \ $
欢迎使用代码审查!您提出了一种替代解决方案,但尚未查看代码。请对其进行编辑,以说明您的推理(解决方案的工作原理以及如何对原始解决方案进行改进),以便所有人都可以从您的思考过程中学习。
\ $ \ endgroup \ $
– Toby Speight
18年1月10日在10:08
#4 楼
for(num1=1; num1<=10000; num1++)
{
for(num2=1; num2<=10000; num2++)
{
if ((num1==sum_of_divisors(num2)) && (num2==sum_of_divisors(num1)) && num1!=num2)
要添加一个小注释,其他2个答案未提及:
您调用sum_of_divisors()的次数约为应有的20000倍。
您使用相同的输入值多次调用它,这是一个相当昂贵的函数。
如果您一次对数字1到10000计算sum_of_divisors(),就在程序开始时,将输出存储在数组中,并在需要该值时查看它,那么您就不会最终需要重复进行相同的工作〜10,000x次,而您可以只做一次。
评论
\ $ \ begingroup \ $
实际上,答案中的所有内容都只是提示。
\ $ \ endgroup \ $
– Peter Taylor
18年1月9日在14:51
\ $ \ begingroup \ $
是的,当彼得提到10000倍加速时,我明白了。感谢您的解释。对将来的观看者会有所帮助。
\ $ \ endgroup \ $
–拉胡尔
18年1月9日在15:44
\ $ \ begingroup \ $
@PeterTaylor的提示让我想到了一些替代方法,我不确定您是指预计算还是其他一些可能的优化,例如限制比较方向和/或将其中一个调用移入更高的循环或切掉永远无法友好的对。
\ $ \ endgroup \ $
–墨菲
18年1月9日在16:17
\ $ \ begingroup \ $
我的回答也是这样。诚然,它很容易被遗漏:“您可以将第二个循环减少到一个测试中”。
\ $ \ endgroup \ $
– Toby Speight
18年9月9日在18:03
#5 楼
要考虑的一件事是sum_of_divisors
函数。它是一个纯函数,意味着其返回值仅取决于输入参数
i
。此外,它被称为100,000,000次,但值只有1到1,000,000。
memoization
已经成熟。这基本上意味着,在调用函数时,将以输入参数为键来缓存返回值。随后具有相同输入参数的调用仅返回缓存的值。因此,
int sum_of_divisors(int n)
{
int sum=0,i;
for(i=1; i<n; i++)
{
if(n%i==0)
{
sum=sum+i;
}
}
return sum;
}
成为
int sums[1000000];
int sum_of_divisors(int n)
{
if(sums[n]) {
return sums[n];
}
int sum=0,i;
for(i=1; i<n; i++)
{
if(n%i==0)
{
sum=sum+i;
}
}
sums[n] = sum;
return sum;
}
评论
\ $ \ begingroup \ $
好主意。但是,使数组在函数内部静态。
\ $ \ endgroup \ $
–彼得-恢复莫妮卡
18年1月10日在8:34
\ $ \ begingroup \ $
进一步说明:if(sums [n]){适用于未知的ns,因为int [] array元素用0初始化,而0在这种情况下等于false。
\ $ \ endgroup \ $
– Minix
18年1月10日在13:07
\ $ \ begingroup \ $
记忆是一个好主意,但是sum_of_divisors()的值为O(n),应为O(sqrt(n))。 O()的变化具有更大的影响。
\ $ \ endgroup \ $
–chux-恢复莫妮卡
18年1月10日在18:59
#6 楼
我认为这是解决您问题的最佳方法之一。它的工作时间约为1秒钟,并找到1到2000万之间的所有对。首先,我们需要使用以下方法计算每个数字的最小除数:Eratosthenes筛。
然后我们使用sigma( n)-n的所有除数之和是乘法函数。对于每个数字,我们知道它的最小除数-p。找出我可被p ^ k整除的最大k。现在,sigma(i)是sigma(p ^ k)* sigma(i / p ^ k)
现在,我们可以计算每个数字的适当除数之和。 sum_proper [i] = sigma [i]-i。
最后,让我们遍历i并检查sigma [sigma [i]] = i并打印答案。
代码
#include <stdio.h>
const int N = 20000000;
const int CNT_PRIMES = N; // upper bound on number of primes within range from 1 to N
int main()
{
int smallest_divisor[N];
int sigma[N];
int sum_proper[N];
int primes[CNT_PRIMES];
int curcnt = 0;
for (int i = 2; i < N; ++i)
smallest_divisor[i] = i;
for (int i = 2; i < N; ++i)
{
if (smallest_divisor[i] == i)
primes[curcnt++] = i;
for (int j = 0; j < curcnt && primes[j] <= smallest_divisor[i] && primes[j] * i < N; ++j)
smallest_divisor[primes[j] * i] = primes[j];
}
sigma[1] = 1;
for (int i = 2; i < N; ++i)
{
int cur = i;
int p = smallest_divisor[i];
int k = 0;
while (cur % p == 0)
cur /= p, ++k;
int pk = i / cur;
sigma[i] = (pk * p - 1) / (p - 1); // sigma(p^k) = 1 + p + ... + p^k = (p^(k+1) - 1) / (p - 1)
sigma[i] *= sigma[cur];
sum_proper[i] = sigma[i] - i;
}
for (int i = 2; i < N; ++i)
{
int j = sum_proper[i];
if (j < N && j > i && sum_proper[j] == i)
printf("%d\t\t%d\n", i, j);
}
}
Idone演示
评论
\ $ \ begingroup \ $
我给了它一个快速检查,它抛出了太多的异常。通常在文件范围内进行各种修改的smallest_divisor。与在main外部声明的其他变量相同。
\ $ \ endgroup \ $
–拉胡尔
18年1月9日在21:24
\ $ \ begingroup \ $
@Rahul您的编译参数是什么?我用gcc a.cpp -o a -O2编译了它
\ $ \ endgroup \ $
–亚历山大·莫罗佐夫(Alexander Morozov)
18年1月9日在21:28
\ $ \ begingroup \ $
这对问题中的代码有什么见解?
\ $ \ endgroup \ $
–灰胡子
18年1月9日在21:49
\ $ \ begingroup \ $
@Rahul您正在使用用于其他语言的编译器来编译C代码。当然,它不能按预期工作。
\ $ \ endgroup \ $
–烟斗
18年1月10日在10:30
\ $ \ begingroup \ $
@pipe:Dev C ++可同时用于C和C ++。它具有MinGW编译器。
\ $ \ endgroup \ $
–拉胡尔
18年1月10日在16:55
#7 楼
这实际上是Euler项目的问题21。如果您想要纯粹的性能,就完全摆脱分歧。
我对C的了解不够,无法实际审查代码,但是几年前我写了一个针对Java的有效解决方案,并希望共享它。该算法非常简单,只需要对除数的思考方式进行一些更改即可。您会看到,我基本上只使用加法(一次除外,用于……加倍)。
// Note: this is Java, not C
public static int projectEuler21(final int limit) {
int[] sumOfDivisors = new int[limit];
int sum = 0;
// For each number below the limit
for (int number = 1; number < limit; number++) {
// Add it as divisor to each of its multiples
for (int multiple = number * 2; multiple < limit; multiple += number) {
sumOfDivisors[multiple] += number;
}
// sum both number and sumOfDivisors[number] if they're amicable, but only if number is the larger of the pair
if (sumOfDivisors[number] < number && number == sumOfDivisors[sumOfDivisors[number]]) {
sum += number + sumOfDivisors[number];
}
}
return sum;
}
上面的代码以大约2或3毫秒的速度运行,因此表现出色。如果将限制更改为20,000,000,则会在5到6秒之间运行。
评论
如果使用了C99功能(例如在for语句的init语句中声明循环索引变量),则可读性会有所改善。由于尚未有人提及它:请始终对源代码进行格式设置(缩进,运算符之间的间距等)。另外,不要浪费垂直空间:在前一行上放置开括号。此特定建议可能会引起争议,但我认为它客观地提高了可读性。
这个问题是关于欧拉计画的问题21。