如何高效地计算连续数字的数字乘积?

8
我正在尝试计算数字序列中每个数字的位数乘积,例如:

21、22、23……98、99……

应该是:

2、4、6……72、81……

为了简化复杂度,我只考虑有限位数内的[连续数字],例如从001999或从00019999
然而,当序列很大时,例如1000000000,对于每个数字重复地提取数字并相乘会很低效。
基本思路是跳过我们在计算中遇到的连续零,类似于:
using System.Collections.Generic;
using System.Linq;
using System;

// note the digit product is not given with the iteration
// we would need to provide a delegate for the calculation
public static partial class NumericExtensions {
    public static void NumberIteration(
            this int value, Action<int, int[]> delg, int radix=10) {
        var digits=DigitIterator(value, radix).ToArray();
        var last=digits.Length-1;
        var emptyArray=new int[] { };
        var pow=(Func<int, int, int>)((x, y) => (int)Math.Pow(x, 1+y));
        var weights=Enumerable.Repeat(radix, last-1).Select(pow).ToArray();

        for(int complement=radix-1, i=value, j=i; i>0; i-=1)
            if(i>j)
                delg(i, emptyArray);
            else if(0==digits[0]) {
                delg(i, emptyArray);

                var k=0;

                for(; k<last&&0==digits[k]; k+=1)
                    ;

                var y=(digits[k]-=1);

                if(last==k||0!=y) {
                    if(0==y) { // implied last==k
                        digits=new int[last];
                        last-=1;
                    }

                    for(; k-->0; digits[k]=complement)
                        ;
                }
                else {
                    j=i-weights[k-1];
                }
            }
            else {
                // receives digits of a number which doesn't contain zeros 
                delg(i, digits);

                digits[0]-=1;
            }

        delg(0, emptyArray);
    }

    static IEnumerable<int> DigitIterator(int value, int radix) {
        if(-2<radix&&radix<2)
            radix=radix<0?-2:2;

        for(int remainder; 0!=value; ) {
            value=Math.DivRem(value, radix, out remainder);
            yield return remainder;
        }
    }
}

这仅用于数字枚举,以避免包含零的数字在第一时间被计算,代码尚未给出数字乘积;但是通过提供一个委托来生成数字乘积仍将需要时间。
如何高效地计算连续数字的数字乘积?

亲爱的投反对票/关闭票的人,如果你投票的原因是因为我没有展示我的研究努力,我已经进行了修正;如果你认为这个问题对你来说不实用,我的个人观点是,有时候在密码学相关问题中,会使用各种数学方法来分析数字特征,但我认为简单而有效的方法可以让事情变得更容易。 - Ken Kin
7个回答

5

编辑: "从任何地方开始,扩展范围"版本...

这个版本的范围显著扩大,因此返回一个IEnumerable<long>而不是一个IEnumerable<int> - 将足够多的数字相乘,您将超过int.MaxValue。它还可以达到10,000,000,000,000,000 - 不完全是long的全部范围,但非常大:) 您可以从任何地方开始,并继续到结束。

class DigitProducts
{
    private static readonly int[] Prefilled = CreateFirst10000();

    private static int[] CreateFirst10000()
    {
        // Inefficient but simple, and only executed once.
        int[] values = new int[10000];
        for (int i = 0; i < 10000; i++)
        {
            int product = 1;
            foreach (var digit in i.ToString())
            {
                product *= digit -'0';
            }
            values[i] = product;
        }
        return values;
    }

    public static IEnumerable<long> GetProducts(long startingPoint)
    {
        if (startingPoint >= 10000000000000000L || startingPoint < 0)
        {
            throw new ArgumentOutOfRangeException();
        }
        int a = (int) (startingPoint / 1000000000000L);
        int b = (int) ((startingPoint % 1000000000000L) / 100000000);
        int c = (int) ((startingPoint % 100000000) / 10000);
        int d = (int) (startingPoint % 10000);

        for (; a < 10000; a++)
        {
            long aMultiplier = a == 0 ? 1 : Prefilled[a];
            for (; b < 10000; b++)
            {
                long bMultiplier = a == 0 && b == 0 ? 1
                                 : a != 0 && b < 1000 ? 0
                                 : Prefilled[b];
                for (; c < 10000; c++)
                {
                    long cMultiplier = a == 0 && b == 0 && c == 0 ? 1
                                     : (a != 0 || b != 0) && c < 1000 ? 0
                                     : Prefilled[c];

                    long abcMultiplier = aMultiplier * bMultiplier * cMultiplier;
                    for (; d < 10000; d++)
                    {
                        long dMultiplier = 
                            (a != 0 || b != 0 || c != 0) && d < 1000 ? 0
                            : Prefilled[d];
                        yield return abcMultiplier * dMultiplier;
                    }
                    d = 0;
                }
                c = 0;
            }
            b = 0;
        }
    }
}

编辑:性能分析

我还没有详细研究过性能,但我认为目前的主要工作只是对十亿个值进行简单的迭代。在我的笔记本电脑上,一个简单的for循环仅返回值本身就需要超过5秒的时间,而仅迭代数字乘积只需略微超过6秒,因此我认为优化空间不多——如果你想从头开始的话。如果你想要(高效地)从不同位置开始,就需要进行更多的调整。


好的,这里有一种使用迭代器块来生成结果并预先计算前一千个结果以加快速度的尝试。

我已经测试了大约1.5亿的结果,到目前为止都是正确的。它仅返回前十亿个结果——如果你需要更多,可以在最后添加另一个块...

static IEnumerable<int> GetProductDigitsFast()
{
    // First generate the first 1000 values to cache them.
    int[] productPerThousand = new int[1000];

    // Up to 9
    for (int x = 0; x < 10; x++)
    {
        productPerThousand[x] = x;
        yield return x;
    }
    // Up to 99
    for (int y = 1; y < 10; y++)
    {
        for (int x = 0; x < 10; x++)
        {
            productPerThousand[y * 10 + x] = x * y;
            yield return x * y;
        }
    }
    // Up to 999
    for (int x = 1; x < 10; x++)
    {
        for (int y = 0; y < 10; y++)
        {
            for (int z = 0; z < 10; z++)
            {
                int result = x * y * z;
                productPerThousand[x * 100 + y * 10 + z] = x * y * z;
                yield return result;
            }
        }
    }

    // Now use the cached values for the rest
    for (int x = 0; x < 1000; x++)
    {
        int xMultiplier = x == 0 ? 1 : productPerThousand[x];
        for (int y = 0; y < 1000; y++)
        {
            // We've already yielded the first thousand
            if (x == 0 && y == 0)
            {
                continue;
            }
            // If x is non-zero and y is less than 100, we've
            // definitely got a 0, so the result is 0. Otherwise,
            // we just use the productPerThousand.
            int yMultiplier = x == 0 || y >= 100 ? productPerThousand[y]
                                                 : 0;
            int xy = xMultiplier * yMultiplier;
            for (int z = 0; z < 1000; z++)
            {
                if (z < 100)
                {
                    yield return 0;
                }
                else
                {
                    yield return xy * productPerThousand[z];
                }
            }
        }
    }
}

我已经进行了测试,通过与极其幼稚的版本的结果进行比较:

static IEnumerable<int> GetProductDigitsSlow()
{
    for (int i = 0; i < 1000000000; i++)
    {
        int product = 1;
        foreach (var digit in i.ToString())
        {
            product *= digit -'0';
        }
        yield return product;
    }
}

希望这个想法对你有所帮助……我不知道它在性能方面与其他展示的方法相比如何。

编辑:稍微扩展一下,使用简单循环的地方我们知道结果将为0,我们需要担心的条件更少,但由于某种原因它实际上略微慢了。(这真的让我很惊讶。)这段代码更长,但可能更容易理解。

static IEnumerable<int> GetProductDigitsFast()
{
    // First generate the first 1000 values to cache them.
    int[] productPerThousand = new int[1000];

    // Up to 9
    for (int x = 0; x < 10; x++)
    {
        productPerThousand[x] = x;
        yield return x;
    }
    // Up to 99
    for (int y = 1; y < 10; y++)
    {
        for (int x = 0; x < 10; x++)
        {
            productPerThousand[y * 10 + x] = x * y;
            yield return x * y;
        }
    }
    // Up to 999
    for (int x = 1; x < 10; x++)
    {
        for (int y = 0; y < 10; y++)
        {
            for (int z = 0; z < 10; z++)
            {
                int result = x * y * z;
                productPerThousand[x * 100 + y * 10 + z] = x * y * z;
                yield return result;
            }
        }
    }

    // Use the cached values up to 999,999
    for (int x = 1; x < 1000; x++)
    {
        int xMultiplier = productPerThousand[x];
        for (int y = 0; y < 100; y++)
        {
            yield return 0;
        }
        for (int y = 100; y < 1000; y++)
        {
            yield return xMultiplier * y;
        }
    }

    // Now use the cached values for the rest
    for (int x = 1; x < 1000; x++)
    {
        int xMultiplier = productPerThousand[x];
        // Within each billion, the first 100,000 values will all have
        // a second digit of 0, so we can just yield 0.
        for (int y = 0; y < 100 * 1000; y++)
        {
            yield return 0;
        }
        for (int y = 100; y < 1000; y++)
        {
            int yMultiplier = productPerThousand[y];
            int xy = xMultiplier * yMultiplier;
            // Within each thousand, the first 100 values will all have
            // an anti-penulimate digit of 0, so we can just yield 0.
            for (int z = 0; z < 100; z++)
            {
                yield return 0;
            }
            for (int z = 100; z < 1000; z++)
            {
                yield return xy * productPerThousand[z];
            }
        }
    }
}

@KenKin:我认为迭代器块并没有太大的区别。我刚刚写了一个接受Action<int>的替代版本,两个版本都需要大约6秒钟的时间在我的笔记本电脑上处理它所产生的10亿个数据。 - Jon Skeet
你说得对,区别不是很大。也许我测试中出现的微小差异是由于指令生成或优化引起的。但是迭代器模式和尾递归之间的效率是棘手的。 - Ken Kin
@KenKin:谢谢 - 我确实觉得它比较容易理解,但承认有些东西我不太懂。现在我正在想办法让它更简单但更长(并希望更快)... - Jon Skeet
@KenKin:抱歉,我稍后会处理这件事 - 但是我目前在外地工作,可能要等到周末才能处理。 - Jon Skeet
1
@KenKin:我真的不知道,恐怕可能是处理器缓存不够大,无法将数组保存在超快速内存中? - Jon Skeet
显示剩余20条评论

4
您可以使用以下递归公式以dp方式完成此操作:
n                   n <= 9
a[n/10] * (n % 10)  n >= 10

其中a[n]n的各位数字乘积的结果。

这导致了一个简单的O(n)算法:在计算f(n)时,假设您已经计算出较小nf(·),则可以使用除最后一位数字以外的所有数字的结果与最后一位数字相乘的结果。

a = range(10)
for i in range(10, 100):
    a.append(a[i / 10] * (i % 10))

如果末位数字不是 0,你可以通过 a[n - 1] + a[n / 10] 的方式避免昂贵的乘法。


谢谢你的回答。顺便说一下,相比除法,乘法似乎并不那么昂贵。 - Ken Kin
@KenKin 这是你在寻找的吗?我已经粗略地查看了你的代码,但似乎你还没有那个功能。 - phant0m
是的,你说得对,但你也可以轻松摆脱它。我认为这会使代码变得不必要地复杂,因为它并没有改变所使用的基本思想。 - phant0m
1
@KenKin,我并不是想表达你的代码有问题之类的意思。至于更复杂的部分,请看看Ben的回答。它也基于完全相同的想法,但它更加优化。这就是我所说的更复杂的意思;) 你可以用重复加法来替换他的 prior * j,这样会使它更快,但也会使它更难读懂。 - phant0m
代码看起来没问题(Ben的),但就我个人理解,递归的尾调用应该在概念上与非递归实现一样高效。 - Ken Kin
显示剩余2条评论

4
关键在于提取数字并生成数字,而不是列举数字并提取数字。
int[] GenerateDigitProducts( int max )
{
    int sweep = 1;
    var results = new int[max+1];
    for( int i = 1; i <= 9; ++i ) results[i] = i;
    // loop invariant: all values up to sweep * 10 are filled in
    while (true) {
        int prior = results[sweep];
        if (prior > 0) {
            for( int j = 1; j <= 9; ++j ) {
                int k = sweep * 10 + j; // <-- the key, generating number from digits is much faster than decomposing number into digits
                if (k > max) return results;
                results[k] = prior * j;
                // loop invariant: all values up to k are filled in
            }
        }
        ++sweep;
    }
}

调用者可以忽略比min小的结果。


这里是使用分支限界剪枝技术的低空间版本:

static void VisitDigitProductsImpl(int min, int max, System.Action<int, int> visitor, int build_n, int build_ndp)
{
    if (build_n >= min && build_n <= max) visitor(build_n, build_ndp);

    // bound
    int build_n_min = build_n;
    int build_n_max = build_n;

    do {
        build_n_min *= 10;
        build_n_max *= 10;
        build_n_max +=  9;

        // prune
        if (build_n_min > max) return;
    } while (build_n_max < min);

    int next_n = build_n * 10;
    int next_ndp = 0;
    // branch
    // if you need to visit zeros as well: VisitDigitProductsImpl(min, max, visitor, next_n, next_ndp);
    for( int i = 1; i <= 9; ++i ) {
        next_n++;
        next_ndp += build_ndp;
        VisitDigitProductsImpl(min, max, visitor, next_n, next_ndp);
    }

}

static void VisitDigitProducts(int min, int max, System.Action<int, int> visitor)
{
    for( int i = 1; i <= 9; ++i )
        VisitDigitProductsImpl(min, max, visitor, i, i);
}

@KenKin:这是从数字和归纳中生成数字。sweep包含最左边数字的串联,j是下一个数字,k是串联。它使用phantom提到的递归公式,但是它一次一个数字地构建数字,而不是拉出数字。 - Ben Voigt
我已经测试了这段代码。我认为这是一个非常高效的解决方案,唯一遇到的问题是当我尝试将999999999作为max使用时,我得到了一个OutOfMemoryException异常。有没有可能实现一个不需要分配大型int数组的解决方案? - Ken Kin
@KenKin:你有没有想到一个特定的例子,其中结果适合内存,但从0开始的整个数组不适合? - Ben Voigt
很遗憾,我无法制作一个没有存储的代码版本来实现这个功能,但我认为这是目前赏金最有希望的答案。 - Ken Kin
1
@KenKin:添加了流式版本,只需要“O(log(max))”的空间。它可能比其他版本稍微慢一点。 - Ben Voigt
显示剩余3条评论

2
我最终得到的代码非常简单,如下所示:
  • Code:

    public delegate void R(
        R delg, int pow, int rdx=10, int prod=1, int msd=0);
    
    R digitProd=
        default(R)!=(digitProd=default(R))?default(R):
        (delg, pow, rdx, prod, msd) => {
            var x=pow>0?rdx:1;
    
            for(var call=(pow>1?digitProd:delg); x-->0; )
                if(msd>0)
                    call(delg, pow-1, rdx, prod*x, msd);
                else
                    call(delg, pow-1, rdx, x, x);
        };
    

    msd is the most significant digit, it's like most significant bit in binary.

我没有选择使用迭代器模式的原因是它比方法调用需要更多时间。带有测试的完整代码放在本回答的末尾。
请注意,行 default(R)!=(digitProd=default(R))?default(R): ... 只是用于分配digitProd,因为委托在赋值之前不能使用。实际上,我们可以将其写成:
  • Alternative syntax:

    var digitProd=default(R);
    
    digitProd=
        (delg, pow, rdx, prod, msd) => {
            var x=pow>0?rdx:1;
    
            for(var call=(pow>1?digitProd:delg); x-->0; )
                if(msd>0)
                    call(delg, pow-1, rdx, prod*x, msd);
                else
                    call(delg, pow-1, rdx, x, x);
        };
    
这种实现方式的缺点是无法从某个特定数字开始,而只能从最大位数开始。
我提供几个简单的解决方案:
  1. Recursion

    The delegate(Action) R is a recursive delegate definition which is used as tail call recursion, for both the algorithm and the delegate which receives the result of digit product.

    And the other ideas below explain for why recursion.

  2. No division

    For consecutive numbers, use of the division to extract each digit is considered low efficiency, thus I chose to operate on the digits directly with recursion in a down-count way.

    For example, with 3 digits of the number 123, it's one of the 3 digits numbers started from 999:

    9 8 7 6 5 4 3 2 [1] 0 -- the first level of recursion

    9 8 7 6 5 4 3 [2] 1 0 -- the second level of recursion

    9 8 7 6 5 4 [3] 2 1 0 -- the third level of recursion

  3. Don't cache

    As we can see that this answer

    How to multiply each digit in a number efficiently

    suggested to use the mechanism of caching, but for the consecutive numbers, we don't, since it is the cache.

    For the numbers 123, 132, 213, 231, 312, 321, the digit products are identical. Thus for a cache, we can reduce the items to store which are only the same digits with different order(permutations), and we can regard them as the same key.

    However, sorting the digits also takes time. With a HashSet implemented collection of keys, we pay more storage with more items; even we've reduced the items, we still spend time on equality comparing. There does not seem to be a hash function better than use its value for equality comparing, and which is just the result we are calculating. For example, excepting 0 and 1, there're only 36 combinations in the multiplication table of two digits.

    Thus, as long as the calculation is efficient enough, we can consider the algorithm itself is a virtual cache without costing a storage.

  4. Reduce the time on calculation of numbers contain zero(s)

    For the digit products of consecutive numbers, we will encounter:

    1 zero per 10

    10 consecutive zeros per 100

    100 consecutive zeros per 1000

    and so on. Note that there are still 9 zeros we will encounter with per 10 in per 100. The count of zeros can be calculated with the following code:

    static int CountOfZeros(int n, int r=10) {
        var powerSeries=n>0?1:0;
    
        for(var i=0; n-->0; ++i) {
            var geometricSeries=(1-Pow(r, 1+n))/(1-r);
            powerSeries+=geometricSeries*Pow(r-1, 1+i);
        }
    
        return powerSeries;
    }
    

    For n is the count of digits, r is the radix. The number would be a power series which calculated from a geometric series and plus 1 for the 0.

    For example, the numbers of 4 digits, the zeros we will encounter are:

    (1)+(((1*9)+11)*9+111)*9 = (1)+(1*9*9*9)+(11*9*9)+(111*9) = 2620

    For this implementation, we do not really skip the calculation of numbers contain zero. The reason is the result of a shallow level of recursion is reused with the recursive implementation which are what we can regard as cached. The attempting of multiplication with a single zero can be detected and avoided before it performs, and we can pass a zero to the next level of recursion directly. However, just multiply will not cause much of performance impact.

完整的代码如下:
public static partial class TestClass {
    public delegate void R(
        R delg, int pow, int rdx=10, int prod=1, int msd=0);

    public static void TestMethod() {
        var power=9;
        var radix=10;
        var total=Pow(radix, power);

        var value=total;
        var count=0;

        R doNothing=
            (delg, pow, rdx, prod, msd) => {
            };

        R countOnly=
            (delg, pow, rdx, prod, msd) => {
                if(prod>0)
                    count+=1;
            };

        R printProd=
            (delg, pow, rdx, prod, msd) => {
                value-=1;
                countOnly(delg, pow, rdx, prod, msd);
                Console.WriteLine("{0} = {1}", value.ToExpression(), prod);
            };

        R digitProd=
            default(R)!=(digitProd=default(R))?default(R):
            (delg, pow, rdx, prod, msd) => {
                var x=pow>0?rdx:1;

                for(var call=(pow>1?digitProd:delg); x-->0; )
                    if(msd>0)
                        call(delg, pow-1, rdx, prod*x, msd);
                    else
                        call(delg, pow-1, rdx, x, x);
            };

        Console.WriteLine("--- start --- ");

        var watch=Stopwatch.StartNew();
        digitProd(printProd, power);
        watch.Stop();

        Console.WriteLine("  total numbers: {0}", total);
        Console.WriteLine("          zeros: {0}", CountOfZeros(power-1));

        if(count>0)
            Console.WriteLine("      non-zeros: {0}", count);

        var seconds=(decimal)watch.ElapsedMilliseconds/1000;
        Console.WriteLine("elapsed seconds: {0}", seconds);
        Console.WriteLine("--- end --- ");
    }

    static int Pow(int x, int y) {
        return (int)Math.Pow(x, y);
    }

    static int CountOfZeros(int n, int r=10) {
        var powerSeries=n>0?1:0;

        for(var i=0; n-->0; ++i) {
            var geometricSeries=(1-Pow(r, 1+n))/(1-r);
            powerSeries+=geometricSeries*Pow(r-1, 1+i);
        }

        return powerSeries;
    }

    static String ToExpression(this int value) {
        return (""+value).Select(x => ""+x).Aggregate((x, y) => x+"*"+y);
    }
}

在代码中,doNothingcountOnlyprintProd用于在获取数字乘积的结果时要执行什么操作。我们可以将它们中的任何一个传递给实现完整算法的digitProd。例如,digitProd(countOnly, power)只会增加count,最终结果将与CountOfZeros返回的结果相同。请注意保留HTML标记。

请注意,语句 digitProd(printProd, power); 实际上很慢,因为它需要在控制台输出。将其替换为 digitProd(doNothing, power);,这样计算就可以在不打印任何内容的情况下进行,这才是实际的计算时间。 - Ken Kin

2

计算前一个数的乘积

由于数字是连续的,大多数情况下您可以通过仅检查个位数来从前一个数生成一个产品。

例如:

12345 = 1 * 2 * 3 * 4 * 5 = 120

12346 = (120 / 5)* 6 = 144,一旦您计算出12345的值,您可以计算12346。

显然,如果前一个产品为零,则此方法将无效。当从9到10换行时,它可以正常工作,因为新的最后一位数字是零,但是您仍可以优化该情况(如下所示)。

如果您正在处理许多数字,则即使涉及除法,此方法也会节省不少时间。

处理零

当循环遍历值以生成产品时,一旦遇到零,您就知道该产品将为零。

例如,对于四位数字,一旦达到1000,您就知道1111之前的所有产品都将为零,因此无需计算这些产品。

最终效率

当然,如果您愿意或能够提前生成和缓存所有值,那么您可以在O(1)中检索它们。此外,由于这是一次性成本,因此您用于生成它们的算法的效率在这种情况下可能不太重要。


谢谢你的回答。你可以提供一些代码(或伪代码)来详细说明答案中描述的缓存机制吗? - Ken Kin
@KenKin 我的意思是预先计算这些值,然后将它们放入代码中的数组或哈希表中(如果数量太多而无法放入内存,则可能需要使用数据库,尽管这显然会有其自身的低效性)。例如,如果您已经有一个包含这些值的数组,那么获取arr[12345]的时间复杂度将为O(1)。 - Matthew Strawbridge
嗯...虽然实际上可能会有一些性能影响,但我认为从概念上来说是正确的。谢谢。 - Ken Kin

1

根据您的数字长度和序列长度,可以进行一些优化。

由于您可以限制数字的最大值,因此可以通过递增模数来迭代数字本身。

假设您有数字42:

var Input = 42;
var Product = 1;
var Result = 0;

// Iteration - step 1: 
Result = Input % 10; // = 2
Input -= Result;
Product *= Result;

// Iteration - step 2:
Result = Input % 100 / 10; // = 4
Input -= Result;
Product *= Result;

你可以将此操作打包到一个漂亮的循环中,这循环可能足够小,适合放入处理器缓存并迭代整个数字。由于避免了任何函数调用,因此这可能也相当快。
如果您想将零视为终止条件,则实现这一点显然非常容易。
正如Matthew已经说过:使用查找表可以获得最终的性能和效率。
序列号范围越小,查找表就越快;因为它将从缓存而不是慢速内存中检索。

1
我会创建一个数组,表示数字的十进制位数,然后按照实际情况递增该数字(即在溢出时增加更重要的数字)。
从那里开始,我将使用可以用作小查找表的产品数组。
例如,数字314将导致产品数组:3、3、12 数字345将导致产品数组:3、12、60
现在,如果您增加十进制数,您只需要通过将其乘以左侧的产品来重新计算最右边的产品。当第二个数字被修改时,您只需要重新计算两个产品(从右边第二个和最外层右边的产品)。这样,您永远不会计算比绝对必要更多的内容,而且您有一个非常小的查找表。
所以,如果您从数字321开始递增,则:
digits = 3, 2, 1      products = 3, 6, 6
incrementing then changes the outer right digit and therefore only the outer right product is recalculated
digits = 3, 2, 2      products = 3, 6, 12
This goes up until the second digit is incremented:
digits = 3, 3, 0      products = 3, 9, 0 (two products recalculated)

这里有个例子来说明这个思路(代码不是很好,只是一个例子):
using System;
using System.Diagnostics;

namespace Numbers2
{
    class Program
    {
        /// <summary>
        /// Maximum of supported digits. 
        /// </summary>
        const int MAXLENGTH = 20;
        /// <summary>
        /// Contains the number in a decimal format. Index 0 is the righter number. 
        /// </summary>
        private static byte[] digits = new byte[MAXLENGTH];
        /// <summary>
        /// Contains the products of the numbers. Index 0 is the righther number. The left product is equal to the digit on that position. 
        /// All products to the right (i.e. with lower index) are the product of the digit at that position multiplied by the product to the left.
        /// E.g.
        /// 234 will result in the product 2 (=first digit), 6 (=second digit * 2), 24 (=third digit * 6)
        /// </summary>
        private static long[] products = new long[MAXLENGTH];
        /// <summary>
        /// The length of the decimal number. Used for optimisation. 
        /// </summary>
        private static int currentLength = 1;
        /// <summary>
        /// The start value for the calculations. This number will be used to start generated products. 
        /// </summary>
        const long INITIALVALUE = 637926372435;
        /// <summary>
        /// The number of values to calculate. 
        /// </summary>
        const int NROFVALUES = 10000;

        static void Main(string[] args)
        {
            Console.WriteLine("Started at " + DateTime.Now.ToString("HH:mm:ss.fff"));

            // set value and calculate all products
            SetValue(INITIALVALUE);
            UpdateProducts(currentLength - 1);

            for (long i = INITIALVALUE + 1; i <= INITIALVALUE + NROFVALUES; i++)
            {
                int changedByte = Increase();

                Debug.Assert(changedByte >= 0);

                // update the current length (only increase because we're incrementing)
                if (changedByte >= currentLength) currentLength = changedByte + 1;

                // recalculate products that need to be updated
                UpdateProducts(changedByte);

                //Console.WriteLine(i.ToString() + " = " + products[0].ToString());
            }
            Console.WriteLine("Done at " + DateTime.Now.ToString("HH:mm:ss.fff"));
            Console.ReadLine();
        }

        /// <summary>
        /// Sets the value in the digits array (pretty blunt way but just for testing)
        /// </summary>
        /// <param name="value"></param>
        private static void SetValue(long value)
        {
            var chars = value.ToString().ToCharArray();

            for (int i = 0; i < MAXLENGTH; i++)
            {
                int charIndex = (chars.Length - 1) - i;
                if (charIndex >= 0)
                {
                    digits[i] = Byte.Parse(chars[charIndex].ToString());
                    currentLength = i + 1;
                }
                else
                {
                    digits[i] = 0;
                }
            }
        }

        /// <summary>
        /// Recalculate the products and store in products array
        /// </summary>
        /// <param name="changedByte">The index of the digit that was changed. All products up to this index will be recalculated. </param>
        private static void UpdateProducts(int changedByte)
        {
            // calculate other products by multiplying the digit with the left product
            bool previousProductWasZero = false;
            for (int i = changedByte; i >= 0; i--)
            {
                if (previousProductWasZero)
                {
                    products[i] = 0;
                }
                else
                {
                    if (i < currentLength - 1)
                    {
                        products[i] = (int)digits[i] * products[i + 1];
                    }
                    else
                    {
                        products[i] = (int)digits[i];
                    }
                    if (products[i] == 0)
                    {
                        // apply 'zero optimisation'
                        previousProductWasZero = true;
                    }
                }
            }
        }

        /// <summary>
        /// Increases the number and returns the index of the most significant byte that changed. 
        /// </summary>
        /// <returns></returns>
        private static int Increase()
        {
            digits[0]++;
            for (int i = 0; i < MAXLENGTH - 1; i++)
            {
                if (digits[i] == 10)
                {
                    digits[i] = 0;
                    digits[i + 1]++;
                }
                else
                {
                    return i;
                }
            }
            if (digits[MAXLENGTH - 1] == 10)
            {
                digits[MAXLENGTH - 1] = 0;
            }
            return MAXLENGTH - 1;
        }
    }
}

这种方式计算十亿范围内的1000个数字的乘积,几乎和计算1到1000的数字的乘积一样快。
顺便问一下,我非常好奇你打算用这些做什么?

你想用这个做什么?我在问题中已经评论了。 - Ken Kin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接