得到n下面每个数字的总和

时间:2015-10-21 10:43:22

标签: c# algorithm optimization

这是我的代码,但速度慢,任何方式都可以更快地完成.. 我打的数字范围是123456789但我不能低于15秒,我需要它才能低于5秒..

long num = 0;

for (long i = 0; i <= n; i++)
{
    num = num + GetSumOfDigits(i);
}

static long GetSumOfDigits(long n)
{
    long num2 = 0;
    long num3 = n;
    long r = 0;
    while (num3 != 0)
    {
        r = num3 % 10;
        num3 = num3 / 10;
        num2 = num2 + r;
    }

    return num2;
}

sum =(n(n+1))/2没有给我我需要的结果,也没有正确计算..

对于N = 12,和为1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 +(1 + 0)+(1 + 1)+(1 + 2)= 51。 我需要用公式而不是循环来做这件事。

我在6秒钟内完成了大约15项测试。

并行我从15秒到4-8秒进行了一次测试..

只是为了向你展示我正在做的测试,这很难......

[Test]
public void When123456789_Then4366712385()
{
   Assert.AreEqual(4366712385, TwistedSum.Solution(123456789));
}

在我的电脑上,我可以在5秒内完成所有测试。 看看照片..

enter image description here

使用DineMartine答案我得到了这些结果:

enter image description here

6 个答案:

答案 0 :(得分:5)

有点令人费解,但却把时间缩短到实际为零:

    private static long getSumOfSumOfDigitsBelow(long num)
    {
        if (num == 0)
            return 0;
        // 1 -> 1 ; 12 -> 10; 123 -> 100; 321 -> 100, ...
        int pow10 = (int)Math.Pow(10, Math.Floor(Math.Log10(num)));
        long firstDigit = num / pow10;
        long sum = 0;
        var sum999 = getSumOfSumOfDigitsBelow(pow10 - 1);
        var sumRest = getSumOfSumOfDigitsBelow(num % pow10);
        sum += (firstDigit - 1)*(firstDigit - 0)/2*pow10 + firstDigit*sum999;
        sum += firstDigit*(num%pow10 + 1) + sumRest;
        return sum;
    }


    getSumOfSumOfDigitsBelow(123456789) -> 4366712385 (80us)
    getSumOfSumOfDigitsBelow(9223372036854775807) -> 6885105964130132360 (500us - unverified)

诀窍是避免一次又一次地计算相同的答案。例如33:

你的方法:

sum = 1+2+3+4+5+6+7+8+9+(1+0)+(1+1)+(1+2)+ ... +(3+2)+(3+3)

我的方法:

sum = 10*(0 + (1+2+3+4+5+6+7+8+9)) + 
   10*(1 + (1+2+3+4+5+6+7+8+9)) + 
   10*(2 + (1+2+3+4+5+6+7+8+9)) + 
   3*(3 + (1 + 2 + 3))

(1+2+3+4+5+6+7+8+9) - 部分只需计算一次。 0..firstDigit-1 - 技巧可以避免n(n-1)/2的循环。我希望这是有道理的。

复杂度为O(2^N),N计算位数。这看起来非常糟糕,但即使是长期最大,也足够快到5秒的阈值。通过仅调用O(n)一次,可以将此算法转换为getSumOfSumOfDigitsBelow()中运行的内容,但看起来会更复杂。

优化的第一步:查看您的算法;)

在DineMartine回答后回到这个问题:

为了进一步优化算法,可以用显式公式替换sum999 - 部分。让我们在代码中加入一些9999...9=10^k-1并相应地替换:

sum(10^k-1) = (9 - 1)*(9 - 0)/2*pow10 + 9*sum999 + 9*(num%pow10 + 1) + sumRest
sum(10^k-1) = 36*pow10 + 9*sum999 + 9*(num%pow10 + 1) + sumRest
对于sum999类型的数字,

sumRest10^k相同:

sum(10^k-1) = 36*pow10 + 10*sum(10^(k-1)-1) + 9*(num%pow10 + 1)
sum(10^k-1) = 36*pow10 + 10*sum(10^(k-1)-1) + 9*((10^k-1)%pow10 + 1)
sum(10^k-1) = 36*pow10 + 10*sum(10^(k-1)-1) + 9*pow10
sum(10^k-1) = 45*pow10 + 10*sum(10^(k-1)-1)

我们有sum(10^k-1)的定义,知道sum(9)=45。我们得到:

sum(10^k-1) = 45*k*10^k

更新的代码:

    private static long getSumOfSumOfDigitsBelow(long num)
    {
        if (num == 0)
            return 0;
        long N = (int) Math.Floor(Math.Log10(num));
        int pow10 = (int)Math.Pow(10, N);
        long firstDigit = num / pow10;
        long sum = (firstDigit - 1)*firstDigit/2*pow10 
            + firstDigit* 45 * N * pow10 / 10 
            + firstDigit*(num%pow10 + 1) 
            + getSumOfSumOfDigitsBelow(num % pow10);
        return sum;
    }

这是与DineMartine相同的算法,但是以递归方式表示(我比较了两种实现,是的,我确定它是;))。运行时间几乎为零,时间复杂度为O(N)计算数字或O(long(N))计算值。

答案 1 :(得分:5)

您的算法复杂度为N log(N)。我找到了一个复杂log(N)的更好的算法。我们的想法是迭代数字的位数:

log10(n) = ln(n)/ln(10) = O(log(n)).

该算法的演示涉及大量的组合演算。所以我选择不在这里写。

以下是代码:

public static long Resolve(long input)
{
    var n = (long)Math.Log10(input);
    var tenPow = (long)Math.Pow(10, n);
    var rest = input;
    var result = 0L;
    for (; n > 0; n--)
    {
        var dn = rest / tenPow;
        rest = rest - dn * tenPow;
        tenPow = tenPow / 10;
        result += dn * (rest + 1) + dn * 45 * n * tenPow + dn * (dn-1) * tenPow * 5 ;
    }

    result += rest * (rest + 1) / 2;

    return result;
}

现在你可以在几分之一秒内解决问题。

想法是将输入写为数字列表:

假设解决方案由函数f给出,我们正在寻找g在n上的递归表达式:

实际上g可以写成如下:

如果你找到h,问题就会得到解决。

答案 2 :(得分:3)

为了提高性能,您可以从最高数字开始计算总和。

r=n%10 + 1。计算最后r个数的总和。

然后我们注意到,如果n9结尾,则总和可以计算为10 * sum(n/10) + (n+1)/10 * 45。第一项是所有数字的总和,但是最后一项,第二项是最后一位数的总和。

计算总和的函数变为:

static long GetSumDigitFrom1toN(long n)
{
    long num2 = 0;
    long i;
    long r = n%10 + 1;

    if (n <= 0)
    {
        return 0;
    }
    for (i = 0; i < r; i++)
    {
        num2 += GetSumOfDigits(n - i);
    }
    // The magic number 45 is the sum of 1 to 9.
    return num2 + 10 * GetSumDigitFrom1toN(n/10 - 1) + (n/10) * 45;
}

试运行:

GetSumDigitFrom1toN(12L): 51
GetSumDigitFrom1toN(123456789L): 4366712385

时间复杂度为O(log n)

答案 3 :(得分:3)

如果您的系统中有多个处理器(或核心),则可以通过并行计算来加快速度。

以下代码演示了(这是一个可编辑的控制台应用程序)。

我在我的系统上尝试的输出(4个超线程核心)对于发布版本如下:

x86 version:

Serial took: 00:00:14.6890714
Parallel took: 00:00:03.5324480
Linq took: 00:00:04.4480217
Fast Parallel took: 00:00:01.6371894

x64 version:

Serial took: 00:00:05.1424354
Parallel took: 00:00:00.9860272
Linq took: 00:00:02.6912356
Fast Parallel took: 00:00:00.4154711

请注意,并行版本的速度提高了约4倍。另请注意,x64版本更快(由于在计算中使用long)。

代码使用Parallel.ForEachPartitioner将数字范围划分为可用处理器数量的合理区域。它还使用Interlocked.Add()通过有效锁定快速添加数字。

我还添加了另一种方法,您需要预先计算0到1000之间数字的总和。您应该只需要为程序的每次运行预先计算一次总和。请参阅FastGetSumOfDigits()

使用FastGetSumOfDigits()可将PC上一次最快的时间翻倍。您可以将SUMS_SIZE的值增加到10的较大倍数,以进一步提高速度,但会占用空间。在我的PC上将它增加到10000会将时间减少到~0.3s

sums数组只需要是short数组,以节省空间。它不需要更大的类型。)

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    internal class Program
    {
        public static void Main()
        {
            long n = 123456789;

            Stopwatch sw = Stopwatch.StartNew();
            long num = 0;

            for (long i = 0; i <= n; i++)
                num = num + GetSumOfDigits(i);

            Console.WriteLine("Serial took: " + sw.Elapsed);
            Console.WriteLine(num);

            sw.Restart();
            num = 0;
            var rangePartitioner = Partitioner.Create(0, n + 1);

            Parallel.ForEach(rangePartitioner, (range, loopState) =>
            {
                long subtotal = 0;

                for (long i = range.Item1; i < range.Item2; i++)
                    subtotal += GetSumOfDigits(i);

                Interlocked.Add(ref num, subtotal);
            });

            Console.WriteLine("Parallel took: " + sw.Elapsed);
            Console.WriteLine(num);

            sw.Restart();
            num = Enumerable.Range(1, 123456789).AsParallel().Select(i => GetSumOfDigits(i)).Sum();
            Console.WriteLine("Linq took: " + sw.Elapsed);
            Console.WriteLine(num);

            sw.Restart();
            initSums();
            num = 0;

            Parallel.ForEach(rangePartitioner, (range, loopState) =>
            {
                long subtotal = 0;

                for (long i = range.Item1; i < range.Item2; i++)
                    subtotal += FastGetSumOfDigits(i);

                Interlocked.Add(ref num, subtotal);
            });

            Console.WriteLine("Fast Parallel took: " + sw.Elapsed);
            Console.WriteLine(num);
        }

        private static void initSums()
        {
            for (int i = 0; i < SUMS_SIZE; ++i)
                sums[i] = (short)GetSumOfDigits(i);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static long GetSumOfDigits(long n)
        {
            long sum = 0;

            while (n != 0)
            {
                sum += n%10;
                n /= 10;
            }

            return sum;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static long FastGetSumOfDigits(long n)
        {
            long sum = 0;

            while (n != 0)
            {
                sum += sums[n % SUMS_SIZE];
                n /= SUMS_SIZE;
            }

            return sum;
        }

        static short[] sums = new short[SUMS_SIZE];

        private const int SUMS_SIZE = 1000;
    }
}

答案 4 :(得分:2)

0..99999999的位数总和为10000000 * 8 *(0 + 1 + 2 + ... + 9)。 然后使用循环计算剩余部分(100000000..123456789)可能足够快。

对于N = 12: 0..9的数字总和为1 * 1 * 45.然后使用你的循环10,11,12。

对于N = 123: 0..99的数字总和为10 * 2 * 45.然后使用你的循环为100..123。

你看到了模式吗?

答案 5 :(得分:0)

您可以尝试不同的方法:

  1. 将数字转换为字符串,然后转换为Char数组
  2. 汇总所有字符的ASCII码,减去0
  3. 的代码

    示例代码:

    long num = 123456789;
    var numChars = num.ToString().ToCharArray();
    var zeroCode = Convert.ToByte('0');
    var sum = numChars.Sum(ch => Convert.ToByte(ch) - zeroCode);