我决定开始从事Euler项目练习。我现在表现不错,但是第七个问题运行得很慢,我想不出任何办法可以使其更快地工作。完成大约需要1300-1500毫秒,这让我不满意,虽然我知道它可以在20-30毫秒内完成。

评论

您的问题是您没有使用筛子。

最简单的改进:测试除数j <= Math.Sqrt(i)而不是j
值得关注的是:您的素数列表包括1和2。这在您计算nth时会被抵消。

我也想知道这一点,但请看一下9。3 ^ 2 = 9基本上是数字的最小除数。 25 = 5 ^ 2同样,此处不小于5是25的除数。如果我们有12,我们将sqrt(12)取为3(〜3.4),我们仍然要达到12(3)的除数。在这里我们可以得出结论:除1以外,数字的最小除数是数字的平方。如果我错了请纠正我

@denis:您的陈述是完全错误的。大于1的12的最小除数是2,而不是3。要在此处查找的真实说法是,如果一个数字是一个复合数,则存在一个小于或等于平方根的质数这个数字。例如,取391。它的平方根略大于19.7,它是复合的,它的质数小于19.7,即17。

#1 楼


bool isPrime = true;
for (int j = 2; j < i / 2; j++)
{
    if (i%j != 0) continue;
    isPrime = false;
    break;
}
if (!isPrime) continue;



这是计算值是否为素数的最原始,效率最低的方法之一。

首先,我们应该使用一种方法来封装此逻辑,并且可以重复调用它: 。它还使我们能够非常轻松地准确地衡量整个算法的这一部分在时间上花费了我们多少。最后,它只是使我们的代码更具可读性,这对我来说通常是做任何事情的理由。

现在,对于这里的逻辑...我有一些
考虑质数97。您的算法将测试最多48个数,但是我们可以停在最接近97平方根的数10处。如果没有10或更小的数均匀地除以97的平方根为9.8,那么它是素数。


bool isPrime(int value)
{
    // return true/false
}



我们正在检查每个除数。我们可以及早消除偶数,然后看看我们的测试值是否可被奇数整除。


就我而言,牙套是一种严重的犯罪。您不应认为它们是可选的。但也许更重要的是,为什么要逆逻辑?您的循环体可以简单地写成: ..

让我们从以下逻辑开始处理一些特殊情况:

j < i / 2


我们还将继续进行过滤out 7,这是一个素数。

j++


现在,我们已经淘汰了许多素数。第一行减去所有有效数字的一半(所有负数,0和1)。 />
第三行通过消除3的所有奇数倍数(在上一行中消除了3的偶数倍数)来抽取剩余的三分之一。

第四行将剩余的百分之二十通过消除尚未被偶数或三的倍数淘汰的五的倍数(例如,已经淘汰了10、15、20,但在这里我们淘汰了25)。

最后一行处理7,以便在进入此模式之前处理所有特殊情况:

if (i%j != 0) continue;


此循环从7开始,每次迭代增加30。该循环的每一行都会测试不同的偏移量,以确保我们不会再次检查特殊情况。对于每一行,我们都在较小的可能值中进行测试,因为它上面的行消除了更多。

要明确,我们的特殊情况消除了... 剩余的一半剩余的1/5 << />
通过此循环的第一次迭代,我们消除了按行


剩余的1/7
剩余的1/11
剩余的1/13
剩余的1/17
剩余的1/19
剩余的1/23
剩余的1/29
剩余的1/31

注意那些分母吗?它们是素数。

循环每次迭代的效率降低(并且随着您需要循环越来越多的迭代,筛子开始变得更快,而浪费了内存)。

但是,在循环的第一次迭代结束时,我们已经针对11个素数测试了isPrime。这是确定数字是否为质数的最有效方法,方法是测试数字是否可以被质数整除(这是筛子允许您执行的操作)。


检查97是否可被4整除,因为如果它要被4整除,则可以被2整除,而我们已经检查了2。如果要被9整除,就可以被3整除,我们已经检查过了。
检查97是否能被25整除是没有意义的,因为如果它要被25整除,它将被5整除,这已经检查过了。

另外,我们知道,如果将97除以25,就可以找到除以该数字的另一个数字。为了更清楚地说明这一点,让我们看一个类似的数字,它不是素数:98。 49将两次进入98。如此幼稚,我们可能要考虑以确定98是否为质数,我们需要一直检查直到value,对吗?错误。数字成对分解。对于每个对,我们只需要针对该值测试最小的对。

因此对于98的因数,我们有以下对:

br />对于任何数字,如果我们将其分成这样的对并查看对中较小的数字,则最大的较小数字将小于或等于我们要分解的数字的平方根。

考虑一个理想的平方,例如100。它的因子对如下所示:

。在这里,左列中的最大值是10。这是100的确切平方根,我们可以在这里停止检查。无需一直进行到一半。

总之,长话短说,最终的98 % 49函数看起来像这样:

if (i % j == 0)
{
    isPrime = false;
    break;
}


评论


\ $ \ begingroup \ $
哦,非常感谢您提供深入的答案,我将确保也进行深入检查并记录下来。在编写代码时,我没有检查哪个数字可以保证它们不是质数的倍数,以及其他一些与数学有关的东西,例如平方根。再次非常感谢您。
\ $ \ endgroup \ $
–丹尼斯
16年4月3日在19:10

\ $ \ begingroup \ $
@nhgrif:在一般算法中使用筛子是个好主意,但是现在我们列出了素数,为什么不循环遍历它们:for(int divisor = 1; primes [divisor] * primes [divisor] <= value;除数++)?那真的只会测试素数除数。
\ $ \ endgroup \ $
–马太·朱哈斯(MátéJuhász)
16-4-3在19:52

\ $ \ begingroup \ $
我不是.NET用户,也不是Windows上的用户,所以我无法真正进行测试,但是我认为在此注明@MátéJuhász重要的一点是,接受标准仅指定我们需要知道1000圣素。我们可以扔掉前10,000个。我们不需要花费执行时间来等待操作系统找到大量的内存块来分配我们的筛子。
\ $ \ endgroup \ $
– nhgrif
16-4-3在23:15

\ $ \ begingroup \ $
注意:描述的技术是车轮分解
\ $ \ endgroup \ $
– Nayuki
16年4月4日在2:38

\ $ \ begingroup \ $
@MátéJuhászsqrt在计算上非常昂贵,而乘法则便宜得多,因此该优化的价值取决于将使用多少。如果要测试较大的值是否为素数,则该优化可能有价值,但如果对较小的值进行大量测试,则可能导致性能下降。我建议对优化进行概要分析,以便在全局实施之前查看它是否具有任何价值,因为它可能无法带来您期望的改进-尤其是如果您计划在以后的Euler项目中将其重新用作主要生成器时,尤其如此。
\ $ \ endgroup \ $
– MT0
16-4-4在7:35

#2 楼

到目前为止,您对如何使IsPrime逻辑更快,更通用有很好的反应。我还要添加一些其他说明。

首先,现在移至longBigInteger。很快,Euler项目中的数字将超过int的限制,q4312079q只能代表最多20亿的数字。如果要继续使用整数,请打开选中的算法,以便在超出整数范围时知道。

考虑一下程序的抽象级别。问题是“第10001个素数是什么?”我们该怎么做才能解决这个问题?怎么样:生成一个所有质数的序列,然后使用该序列的第10001个成员?这是我编写程序的方式:

static bool IsPrime(long x) { /* You implement this */ }

static IEnumerable<long> PositiveIntegers()
{
  for (long i = 1; i < long.MaxValue; ++i)
    yield return i;
}

static IEnumerable<long> Primes()
{
  return PositiveIntegers().Where(IsPrime);
}

static long Problem7() 
{
  return Primes().Skip(10000).First();
}


看看该程序比您的程序好多少!看看每种方法的可读性,清晰性和明显的正确性!仅一两行长的方法很可能是正确的。您的程序是一堆乱七八糟的数组和循环,然后继续执行并中断,难怪它会有很多错误。并继续吗?当然。但谁在乎?这个程序清晰,易于理解,足够快,并且很容易从该程序中学习并用于将来解决更多问题。

例如,考虑问题10,即“找出200万以下的所有素数之和”。修改问题7的解决方案以解决问题10相当容易!方法清楚地实现了所需的规范。你想要什么?第10001个素数。你怎么得到的?跳过前10000个素数,然后采用找到的第一个素数。代码是什么样的?就是这样。


更新:一位评论者指出,我可以在C#6.0中使用表达式主体函数。我认为可以澄清。这在C#6.0中是合法的:

static IEnumerable<long> Primes() => PositiveIntegers().Where(IsPrime);

static long Problem7() => Primes().Skip(10000).First();


此版本与原始版本没有区别; C#6.0仅允许更简短地编写这些简短方法。

评论


\ $ \ begingroup \ $
感谢您提供简洁的示例代码Eric!下次,我将确保更清晰地实现我的功能。
\ $ \ endgroup \ $
–丹尼斯
16年4月4日在18:31

\ $ \ begingroup \ $
不接受具有表达式功能的函数Eric吗?
\ $ \ endgroup \ $
– Corey
16年4月5日在10:46

\ $ \ begingroup \ $
@Corey:我爱他们,我还没养成习惯!
\ $ \ endgroup \ $
–埃里克·利珀特
16年4月5日在14:52

\ $ \ begingroup \ $
尽管字符串插值是一个强有力的竞争者,但我认为这是我使用最多的C#6功能。暂时担心您在使用它们,因为它们出了点问题...很高兴不是这样。
\ $ \ endgroup \ $
– Corey
16-4-5在23:23



#3 楼

尽管我的其他答案主要针对您的算法本身,但是我想解决的代码中还有一些其他问题:


primes.Add(i);



查看有关此Add方法的MSDN备注: />在添加新元素之前,将现有元素复制到新数组。

如果Count小于Capacity,则此方法是O(1)操作。如果需要增加容量以容纳新元素,则此方法将变为O(n)操作,其中n为Count。


重点是我的。

我不熟悉.NET的实现,但是通常来说,这些趋向于工作的方式是将这些集合初始化为一些默认容量,然后每次需要扩展,容量增加一倍。

因此,如果我们从容量10开始,当我们尝试添加第11个项目时,我们将移动所有内容,并调整为容量20。在尝试添加第21个项目时,我们将容量调整为40。这变得非常昂贵。每次调整大小时,都必须移动大量内存以腾出空间。

我们可以通过使用恰好需要大小的固定大小的数组或通过设置大小来减少内存我们的List的容量恰好满足了我们的需求(或更多)。可以衡量它,它是单个分配而不是多个分配,并且它不涉及移动任何内存,因此变得更加高效。

评论


\ $ \ begingroup \ $
通常是两倍,而不是平方。
\ $ \ endgroup \ $
–user253751
16年4月4日在2:16

\ $ \ begingroup \ $
事实证明,您甚至不需要存储此列表。您可以舍弃前10000个素数,找到后返回10001个素数。
\ $ \ endgroup \ $
– Nayuki
16-4-4的2:39

\ $ \ begingroup \ $
我从未听说过“装满后的正方形”列表。双倍完整列表的意义在于,对摊销成本的分析表明,无论列表增加一倍,每次添加的成本平均都保持不变。是的,少量昂贵的添加物的成本就成线性关系,但是平均而言,它们却可以保持恒定。
\ $ \ endgroup \ $
–埃里克·利珀特
16年4月4日在17:21

\ $ \ begingroup \ $
在此实现中,它不是“满时平方”。如果是这样,那么当需要调整大小时,复杂度将为O(n ^ 2)。
\ $ \ endgroup \ $
–Lacklub
16-4-4在19:40



#4 楼

性能


我想不出什么能让它更快地运行


您可以使用Eratosthenes筛子来使其更快-在这里以下是我用Java编写并适应C#的一些示例代码(仅筛除奇数),给出了这样一个筛子的示例:


输出:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

public class PrimeList
{
    // A list of prime values.
    private static readonly List<int> Primes        = new List<int>( 10001 );

    // An array of bits indicating whether the odd numbers (from 3 upwards)
    // are prime (true) or non-prime (false).
    private static readonly BitArray  OddPrimeSieve = new BitArray( 100000,
                                                                    true );

    // The maximum prime number that has currently been found from the sieve.
    private static          int       MaxSieved     = 3;

    static PrimeList()
    {
        Primes.Add( 2 );
        Primes.Add( 3 );
    }

    public static int GetNthPrime( int index )
    {
        // Caclulate the sieve index of the currently maximum sieved prime.
        int sieveIndex = ( MaxSieved - 3 ) / 2;
        int multiple;

        while( Primes.Count < index )
        {
            // Set all higher multiples of the current prime to be non-prime.
            for ( multiple = sieveIndex + MaxSieved;
                    multiple < OddPrimeSieve.Count; 
                    multiple += MaxSieved )
            {
                OddPrimeSieve.Set( multiple, false );
            }

            // Find the sieve index of the next prime number.
            do
            {
                ++sieveIndex;
            }
            while ( sieveIndex < OddPrimeSieve.Count
                    && !OddPrimeSieve[sieveIndex] );

            // Reverse the calculation of the sieve index to get the next prime.
            MaxSieved = 2*sieveIndex + 3;

            // Add the prime to the list.
            Primes.Add( MaxSieved );
        }
        return Primes[index-1];
    }

    public static void Main()
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        int p = GetNthPrime(10001);
        sw.Stop();
        Console.WriteLine( p );
        Console.WriteLine( "Time to calculate in milliseconds : {0}",
                            sw.ElapsedMilliseconds );
    }
}


解释:

OddPrimeSieve BitSet是否存储奇数(从3开始)是否素数。最初,所有奇数值都设置为素数,并且每当找到质数时,所有更高的倍数都将设置为非素数(for循环)。然后通过遍历筛子直到找到一个仍将其设置为true的值来找到下一个素数。

注意:

我制作了该类,以便它维护可在重复请求中使用的素数列表。这对于解决Project Euler的问题不是必需的-因此,如果您要对此进行优化,则可以删除该部分代码,而只需使用一个简单的计数器即可获取10001st值。

评论


\ $ \ begingroup \ $
我必须说,我根本不喜欢这种间隔。
\ $ \ endgroup \ $
– nhgrif
16年4月3日在23:12

\ $ \ begingroup \ $
如何以任何方式查看OP的代码?
\ $ \ endgroup \ $
–塔莫格纳·乔杜里(Tamoghna Chowdhury)
16-4-4在2:14

\ $ \ begingroup \ $
@TamoghnaChowdhury OP在问题“我想不出什么可以使它更快地工作”中表示,这满足了该特定请求。
\ $ \ endgroup \ $
– MT0
16年4月4日在2:36

\ $ \ begingroup \ $
还有BitArray注释-为什么不使用多行注释?
\ $ \ endgroup \ $
–塔莫格纳·乔杜里(Tamoghna Chowdhury)
16年4月4日在2:41

#5 楼

一个大问题是您的算法将4识别为素数:-(

问题是“找到10,001st素数”。您可以通过检查数字是否为素数并将其添加到素数来解决此问题列表中,直到列表中有10,001个素数(实际上,列表中包含数字1和10,000素数,但没有素数2)。请注意,根本不需要创建列表,但是如果创建该列表,则可以很好地使用它,只检查潜在质数p是否可以被另一个质数除以,而不是每个数都可以。

通过选中“ p没有除数> = 2”来解决“ is pa prime”问题和


但是,对于此类问题,通常需要从整体上解决问题,而不是通过检查单个数字,而是通过检查大量的麻木表er,例如同时从3到199,999的奇数。 (对于Google,它是“ Eratosthenes的筛子”,或者只是Eratosthenes的谷歌。但是,总的来说,许多Euler项目问题​​都要求您从整体上解决问题)。

评论


\ $ \ begingroup \ $
感谢您的来信,我将如何同时查看庞大的数字列表,例如从3到199,999的奇数。而且它只会错过2,不会将4标识为素数。
\ $ \ endgroup \ $
–丹尼斯
16-4-3在22:21



\ $ \ begingroup \ $
在您的最后一段中,您是否正在尝试推荐某种多线程解决方案?我很想听听关于确定要产生多少线程以及如何划分工作然后将其重新组合在一起的攻击计划。
\ $ \ endgroup \ $
– nhgrif
16年4月3日在23:06

#6 楼

只是说明如何解决问题:这里的问题很容易。无论您使用快速算法还是慢速算法,它都应在合理的时间内找到第10,001个素数。现在想象一下,他们曾要求提供1,000,000,000,000,000,001st的本金。如果您检查哪些数字是质数,那么这是完全无法企及的,如果您使用高度优化的筛子,那将仍然遥不可及。

如果您再次阅读该问题,您会发现没有人真正想要所有素数,他们只是在索要一个特定素数!所以你会问自己:我能找到(十亿亿美元以上)的素数而不找到其他所有素数吗?

答案是肯定的:有一种算法可以计算O中的素数<= n(n ^(2/3))。有一些公式可以告诉您该素数大约是多少。您使用这样的公式来查找“我们正在寻找的素数是关于X的”。然后,您可以使用素数计数功能来查找<= X的素数。如果该数接近10 ^ 18 + 1,则会为X周围的数字创建一个筛子。 10 ^ 18 + 1素数和重复的近似值。

这仍然会花费您的计算机一些时间,但是它将问题从无法解决变为辛苦工作。

#7 楼

我今天正在C#中处理Euler项目问题​​。这是我的代码。 nhgrif的技术非常出色,但是无需任何高级技术,您就可以将代码执行时间缩短到50ms。我唯一优化代码的方法是从2 to Math.sqrt(num)而不是2 to (num-1)进行迭代。我什至没有实现跳过偶数的代码,这可能使它甚至更快。您的技术是将每个素数存储在List高级数据类型中。相反,您可以仅将当前素数和当前素数计数存储在intlong基本数据类型中。



private void button7_Click(object sender, EventArgs e)
{
    Stopwatch sw = StartProblem("By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13.\r\n\r\nWhat is the 10 001st prime number?");

    FinishProblem(FindNthPrime(10001), sw);
}

private long FindNthPrime(long n)
{
    if (n < 1)
    {
        throw new ArgumentException("The number of the nth prime to find must be positive!");
    }

    long numToTry = 2;
    long currentPrime = 0;
    long currentPrimeCount = 0;

    // TODO: no need to check even numbers, they are not prime
    while (currentPrimeCount < n)
    {
        if (IsPrime(numToTry))
        {
            currentPrime = numToTry;
            currentPrimeCount++;
        }

        numToTry++;
    }

    return currentPrime;
}

private bool IsPrime(long NumToCheck)
{
    // A prime number (or a prime) is a natural number greater than 1 that has no positive divisors
    // other than 1 and itself.

    // By definition, numbers 1 or less are not primes.
    if (NumToCheck <= 1)
    {
        return false;
    }
    // 2 is prime, but the loop below won't think so. We need to define it up here.
    else if (NumToCheck == 2)
    {
        return true;
    }

    long SquareRoot = SquareRootRoundedUp(NumToCheck);

    for (long i = 2; i <= SquareRoot; i++)
    {
        if ( NumToCheck % i == 0 )
        {
            return false;
        }
    }

    return true;
}

private long SquareRootRoundedUp(long NumToSquareRoot)
{
    return Convert.ToInt64(Math.Ceiling(Convert.ToDecimal(Math.Sqrt(NumToSquareRoot))));
}


评论


\ $ \ begingroup \ $
列表很快。添加不在内部循环中。
\ $ \ endgroup \ $
–亚历山大·杜宾斯基(Aleksandr Dubinsky)
16年4月5日在20:36