【问题标题】:Get sum of each digit below n as long获取n以下每个数字的总和
【发布时间】:2015-10-23 20:59:56
【问题描述】:

这是我拥有的代码,但它很慢,任何方法都可以更快地做到这一点.. 我打的数字范围是 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。 我需要用公式而不是循环来做到这一点..

我有大约 15 个测试要在每个 6 秒内完成..

并行我得到了一个从 15 秒到 4-8 秒的测试..

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

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

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

DineMartine 回答我得到了这些结果:

【问题讨论】:

  • 你需要输出为 1+2+3+4+...+9 吗?
  • for 循环的意义何在? GetSumOfDigits 将给出数字中所有数字的总和 .. 不需要 for 循环 ...
  • 如果你想要 1 到 n 的总和,那么你可以使用公式sum =(n(n+1))/2
  • 喜欢 1+...+9 + (1 + 0)
  • 1.每个大于 10 的数字一次又一次地排在第一位。尽可能地尝试乘法。 2. 使用数学公式。总和(1,..,9)=45。这可以用很多。 3. 如果您确实需要一些数字 - 没有数学/乘法,请使用基数(10、100 等)并遍历所有具有相同基数的数字,直到达到极限。这将节省大量% 操作。

标签: c# algorithm optimization


【解决方案1】:

您的算法复杂度为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 一个 f 在 n 上的递归表达式:

其实g可以写成这样:

如果你找到h,问题实际上就解决了。

【讨论】:

  • 我不确定你是怎么做到的,但你的代码将所有测试的时间更改为低于 5 毫秒...
  • 我编辑了我的原始帖子以帮助您找到公式。
  • The demonstration of this algorithm involves a lot of combinatorial calculus - 推导?
  • +1 @DineMartine:我不确定,但它看起来像我的解决方案。你“只是”把它放到下一步。但我觉得@greybeard ......你知道你是怎么得到h(D_n, n)的吗?
  • @greybeard 这不是太多的微积分。我已经更新了我的答案,以从我的 DineMartines 解决方案中获得答案。
【解决方案2】:

有点令人费解,但时间几乎为零:

    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)-part 只需计算一次。 0..firstDigit-1 的循环可以通过n(n-1)/2-trick 来避免。我希望这是有道理的。

复杂度为O(2^N),其中 N 计算位数。这看起来很糟糕,但对于您的 5 秒阈值来说已经足够快了,即使对于 long-max 也是如此。可以通过只调用一次getSumOfSumOfDigitsBelow() 将这个算法转换为在O(n) 中运行的东西,但它看起来要复杂得多。

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


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

为了进一步优化算法,sum999-part 可以替换为显式公式。让我们在代码中取一些数字 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

sum999sumRest对于10^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)) 取值。

【讨论】:

    【解决方案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() 是我电脑上之前最快速度的两倍多。您可以将 SUMS_SIZE 的值增加到 10 的更大倍数,以进一步提高速度,但会占用空间。在我的 PC 上将其增加到 10000 会将时间减少到 ~0.3 秒

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

    【讨论】:

    • 或单行:long sum = Enumerable.Range(1, 123456789).AsParallel().Select(i =&gt; GetSumOfDigits(i)).Sum();
    • @LasseV.Karlsen 该版本需要约 3 秒,而后者需要约 1 秒。
    • 我有 7.5 秒的并行时间。连续 18 秒
    • @FarhanAnam 实际上,时间会因您的系统而有很大差异,但如果至少有两个处理器内核,并行版本应该总是更快。
    • 我没有说它更快,但它低于 OP 在 5 秒时设置的阈值。
    【解决方案4】:

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

    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)

    【讨论】:

    • 好了,这次我真的跑了代码。必须修复几个错误(包括设计中的“一个”错误)。似乎现在可以工作了,至少对于12123456789
    【解决方案5】:

    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。

    你看到模式了吗?

    【讨论】:

    • 所以你认为你的公式可以处理我正在使用的数字范围??对于 N = 12,总和为 1+2+3+4+5+6+7+8+9+(1+0)+(1+1)+(1+2)= 51。N 是长整数。
    • 看到模式不是问题,而是 if 语句计数可能会使您的公式变慢..
    • 看到这种模式是提出O(log n) 解决方案的良好开端。如果您再采取一些步骤,您可以使用@DineMartine(最快的解决方案)或我(稍慢但可能更容易理解)这样的解决方案。
    【解决方案6】:

    您可以尝试另一种方法:

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

    示例代码:

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

    【讨论】:

    • 您认为将数字转换为字符串会比他得到的更快吗?
    • 我不知道,但值得一试。
    猜你喜欢
    • 2021-10-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-15
    • 2019-04-05
    • 2023-03-09
    • 1970-01-01
    相关资源
    最近更新 更多