Как эффективно рассчитать цифровые произведения последовательных чисел?

Я пытаюсь вычислить произведение цифр каждого числа последовательности цифр, например:

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

будет:

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

чтобы уменьшить сложность, я бы рассматривал только [последовательными номерами] в ограниченной длине цифр, таких как от 001 to 999 или 0001 to 9999.

, когда последовательность велика, например,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;
        }
    }
}

это только для перечисления чисел, чтобы избежать чисел, которые содержат нули, которые должны быть рассчитаны в первую очередь, цифровые продукты еще не заданы кодом; но генерировать цифровые продукты, предоставляя делегат для выполнения вычисления, все равно потребуют времени.

как эффективно рассчитать цифровые продукты последовательных чисел?

7 ответов


EDIT: версия" start from anywhere, extended range"...

эта версия имеет значительно расширенный диапазон, и поэтому возвращает 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;
        }
    }
}

EDIT: производительность анализ

Я не смотрел на исполнении в деталь, но я считаю, что на данный момент основная часть работы просто повторяет более миллиарда значений. Простой for цикл, который просто возвращает само значение, занимает более 5 секунд на моем ноутбуке, а итерация по цифровым продуктам занимает немного больше 6 секунд, поэтому я не думаю, что есть намного больше места для оптимизации - если вы хотите начать с самого начала. Если вы хотите (эффективно) начать с другого позиция, требуется больше настроек.


хорошо, вот попытка, которая использует блок итератора для получения результатов и предварительно вычисляет первую тысячу результатов, чтобы сделать все немного быстрее.

я протестировал его примерно до 150 миллионов, и это правильно до сих пор. Он возвращает только первый миллиард результатов-если вам нужно больше, вы можете добавить еще один блок в конце...

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

Надеюсь, эта идея будет полезна... Я не знаю, как он сравнивается с другими, показанными здесь с точки зрения производительности.

EDIT: немного расширяя это, чтобы использовать простые циклы, где мы знаем, что результаты будут 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];
            }
        }
    }
}

вы можете сделать это в 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))

вы можете избавиться от дорогое умножение, просто добавив doing a[n - 1] + a[n / 10] для чисел, где последняя цифра не 0.


ключ к эффективности-не перечислять числа и извлекать цифры, а перечислять цифры и генерировать числа.

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

вычисление продукта из предыдущего

поскольку номера последовательны, в большинстве случаев вы можете создать один продукт из предыдущего, проверив только место единиц.

например:

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). Кроме того, поскольку это разовая стоимость, эффективность алгоритма, который вы используете для их генерации, может быть менее важна в этом случае.


я получаю очень простой код следующим образом:

  • код:

    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 - это самая значительная цифра это как самый значительный бит в двоичной системе.

причина, по которой я не решил использовать шаблон итератора, заключается в том, что он занимает больше времени, чем вызов метода. Полный код (с тестом) помещается в задней части этого ответа.

обратите внимание, что линии 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. рекурсия

    делегат(Action) R является рекурсивным определением делегата, которое используется как хвост вызов рекурсия, как для алгоритма, так и для делегата, который получает результат цифрового продукта.

    и другие идеи ниже объясняют, почему рекурсия.

  2. дивизии

    для последовательных номеров используйте разделение для извлечения каждой цифры считается низкой эффективностью,поэтому я решил работать с цифрами напрямую с рекурсией в обратном порядке.

    например, с 3-мя цифрами числа 123, это один из 3-х цифр, начиная с 999:

    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. не кэш

    как мы видим, это ответ

    как эффективно умножить каждую цифру в числе

    предложил использовать механизм кэширования, но для последовательных чисел мы этого не делаем, так как это is кэш.

    цифры 123, 132, 213, 231, 312, 321, продукты числа идентичны. Таким образом, для кэша, мы можем уменьшить элементы для хранения, которые являются только те же цифры с различным порядком (перестановки), и мы можем рассматривать их как один и тот же ключ.

    однако сортировка цифр также требует времени. С HashSet реализованная коллекция ключей, мы платим больше хранения с большим количеством элементов; даже мы сократили элементы, мы по-прежнему тратим время на сравнение равенства. Кажется, что хэш-функция не лучше, чем используйте его значение для сравнения равенства, а что просто результат мы вычисление. Например, за исключением 0 и 1, в таблице умножения двух цифр есть только 36 комбинаций.

    таким образом, пока расчет достаточно эффективен, мы можем считать, что сам алгоритм является виртуальный кэш без стоимости хранения.

  4. сократить время на расчет чисел, содержащих ноль (ы)

    для продуктов числа последовательных номеров, мы столкнемся:

    1 ноль на 10

    10 последовательных нулей на 100

    100 последовательных нулей на 1000

    и так далее. Отметим, что еще 9 нулей мы столкнемся с per 10 на per 100. Количество нулей можно вычислить с помощью следующего кода:

    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 - основание системы счисления. Число будет ряд, которая рассчитывается из геометрическая серия и плюс 1 к 0.

    например, числа из 4 цифр, нули, с которыми мы столкнемся:

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

    для этой реализации, мы делаем не действительно пропустить расчет чисел содержат ноль. Причина является результатом мелкий уровень рекурсии используется с рекурсивной реализации которые мы можем рассматривать как кэшированные. Попытка умножения с одним нулем может быть обнаружена и предотвращена до его выполнения, и мы можем напрямую передать ноль на следующий уровень рекурсии. Тем не менее, просто умножить не вызовет большого влияния на производительность.


полный код:

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

в коде doNothing, countOnly, printProd для чего сделать когда мы получим результат продукта числа, мы может передать любой из них digitProd который реализовал полный алгоритм. Например, digitProd(countOnly, power) будет только увеличиваться count, и конечный результат будет таким же, как CountOfZeros возвращает.


Я бы создал массив, который представляет десятичные цифры числа, а затем увеличить это число так же, как в реальной жизни (т. е. при переполнении увеличить более значительную цифру).

оттуда я бы использовал массив продуктов, которые можно использовать в качестве крошечной таблицы поиска.

Е. Г. число 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.

кстати, мне очень любопытно, для чего вы пытаетесь использовать все это?


в зависимости от длины ваших чисел и длины последовательности, если бы для некоторой оптимизации.

поскольку вы можете ограничить максимальный размер числа, вы можете перебирать само число с помощью увеличивающегося модуля.

допустим, у вас есть число 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;

вы можете упаковать эту операцию в цикл, который достаточно мал, чтобы поместиться в кэш процессора и перебрать весь ряд. Как вы избегаете любой функции звонки это, вероятно, тоже довольно быстро.

Если вы хотите касаться нулей в качестве критериев прерывания, реализация для этого, очевидно, довольно проста.

As : максимальная производительность и эффективность будут получены с помощью таблицы поиска.

чем меньше диапазон ваших порядковых номеров, тем быстрее таблица поиска; потому что она будет извлечена из кэша, а не из медленной памяти.