如何有效地计算连续数字的数字产品?

时间:2013-07-11 07:31:40

标签: c# math language-agnostic

我正在尝试计算一系列数字的数字的乘积,例如:

  

21,22,23 ... 98,99 ..

将是:

  

2,4,6 ... 72,81 ..

为了降低复杂性,我只考虑有限长度的数字[consecutive numbers],例如从001999或从0001到{ {1}}。

但是,当序列很大时,例如9999,重复提取数字,然后对每个数字乘以将是低效的。

基本思路是跳过我们在计算过程中会遇到的连续零点,如:

1000000000

这仅用于数字的枚举,以避免首先计算包含零的数字,数字产品尚未由代码给出;但通过提供代表执行计算生成数字产品仍需要时间。

如何有效地计算连续数字的数字产品?

7 个答案:

答案 0 :(得分: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;
        }
    }
}

编辑:绩效分析

我没有看过 detail 中的表现,但我相信在这一点上,大部分工作只是迭代超过十亿的价值。一个简单的for循环只返回值本身在我的笔记本电脑上需要5秒以上,迭代数字产品只需要6秒多一点,所以我认为没有更多的优化空间 - 如果你想从头开始。如果您想(有效地)从不同的位置开始,则需要进行更多调整。


好的,这是一个尝试,它使用迭代器块来产生结果,并预先计算前1000个结果以使事情变得更快。

我已经测试了大约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];
            }
        }
    }
}

答案 1 :(得分:4)

您可以使用以下递归公式以类似dp的方式执行此操作:

n                   n <= 9
a[n/10] * (n % 10)  n >= 10

其中a[n]n数字相乘的结果。

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

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

您可以通过为最后一位数字不是a[n - 1] + a[n / 10]的数字添加0来摆脱昂贵的乘法。

答案 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);
}

答案 3 :(得分:2)

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

  • 代码:

    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最重要的数字,就像二进制文件中的most significant bit一样。

我没有选择使用迭代器模式的原因是它比方法调用花费的时间更多。完整的代码(带有测试)放在这个答案的后面。

请注意,行default(R)!=(digitProd=default(R))?default(R): ...仅用于digitProd的分配,因为代理在分配之前无法使用。我们实际上可以把它写成:

  • 替代语法:

    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. 递归

    委托(ActionR是一个递归委托定义,用作tail call递归,用于接收数字产品结果的算法和委托。

    下面的其他想法解释了为什么递归。

  2. 无分区

    对于连续的数字,使用除法来提取每个数字被认为效率很低,因此我选择以递减计数的方式直接对数字进行操作。

    例如,使用数字123的3位数字,它是从999开始的3位数之一:

      

    9 8 7 6 5 4 3 2 [1] 0 - 第一级递归

         

    9 8 7 6 5 4 3 [2] 1 0 - 第二级递归

         

    9 8 7 6 5 4 [3] 2 1 0 - 第三级递归

  3. 不要缓存

    我们可以看到这个答案

    How to multiply each digit in a number efficiently

    建议使用缓存机制,但对于连续数字,我们不这样做,因为它 缓存。

    对于数字123, 132, 213, 231, 312, 321,数字产品是相同的。因此,对于缓存,我们可以减少要存储的项目,这些项目只是具有不同顺序(排列)的相同数字,并且我们可以将它们视为相同的密钥。

    但是,排序数字也需要时间。通过HashSet实施的密钥集合,我们可以使用更多项目支付更多存储空间;即使我们减少了项目,我们仍然花时间进行平等比较。似乎没有哈希函数优于使用其值进行相等性比较,这只是我们计算的结果。例如,除了0和1之外,在两位数的乘法表中只有36种组合。

    因此,只要计算足够有效,我们就可以认为算法本身是一个虚拟缓存而无需花费存储空间。

  4. 减少计算包含零的数字的时间

    对于连续数字的数字产品,我们将遇到:

      

    每10个零点

         

    每100个连续10个零

         

    每1000个连续100个零

    等等。请注意,per 10per 100我们将遇到9个零。可以使用以下代码计算零的数量:

    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;
    }
    

    对于n是数字位数,r是基数。该数字为power series,根据geometric series计算,0加1。

    例如,4位数字,我们将遇到的零是:

      

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

    对于这个实现,我们真的跳过计算数字包含零。原因是递归实现的浅层递归的结果与递归实现一起重用,我们可以将其视为 cached 。在执行之前可以检测并避免尝试乘以单个零,并且我们可以直接将零传递到下一级递归。但是,只是乘法不会对性能产生太大影响。


  5. 完整的代码:

    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返回相同。

答案 4 :(得分:2)

计算前一个产品

由于这些数字是连续的,因此在大多数情况下,您只需检查单位位置即可生成上一个产品。

例如:

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

12346 = 1 * 2 * 3 * 4 * 6 = 144

但是,一旦计算出12345的值,就可以将12346计算为(120/5)* 6

显然,如果之前的产品为零,这将不起作用。当从9到10包裹时它确实有效,因为新的最后一位数为零,但你仍然可以优化那个情况(见下文)。

如果你正在处理大量数字,这种方法即使涉及分割,也会相当节省。

处理零

当您循环使用值来生成产品时,只要您遇到零,就会知道产品将为零。

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

最终效率

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

答案 5 :(得分: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 said already:使用查找表可以获得最终的性能和效率。

序列号的范围越小,查找表越快;因为它将从缓存中检索而不是从慢速内存中检索。

答案 6 :(得分:1)

我创建一个表示数字的十进制数字的数组,然后像在现实生活中那样增加该数字(即在溢出时增加更高有效数字)。

从那里开始,我会使用一系列可用作微型查找表的产品。

E.g。 数字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的数量一样快。

顺便说一下,我很好奇你想要用这一切来做什么?