寻找十进制数位和的最快方法

6

如何快速找到十进制数字的总和?以下是我写的代码,但对于范围在1到1000000000000000000之间的数字非常非常慢。

long long sum_of_digits(long long input) {
    long long total = 0;
    while (input != 0) {
        total += input % 10;
        input /= 10;
    }
    return total;
}

int main ( int argc, char** argv) {
    for ( long long i = 1L; i <= 1000000000000000000L; i++) {
        sum_of_digits(i);
    }
    return 0;
}

1
看起来不会很慢,你需要它有多快? - Vaughn Cato
2
如果在你的电脑上这个操作超过一秒钟,我想知道你是如何在不到100年的时间里编译它的。 - Benjamin Lindley
1
目前你的代码没有对sum_of_digits的返回值进行任何处理。你只是在主函数中循环来测试它的速度吗? - Vaughn Cato
8
一台计算机无法在合理的时间内重复执行十万亿次任务。也许你对需求有所误解。 - Vaughn Cato
5
@Avinash:既然你已经添加了主函数,我想完全修改我的陈述。即使你能编写一些魔法函数,在单个周期内执行,并且你有一个6核心的计算机,每个核心运行速度为4ghz,你的代码也不可能在一年之内执行完成。做个简单的计算:1000000000000000000个周期 /(4千兆赫 * 6核心) - Benjamin Lindley
显示剩余9条评论
14个回答

6
我假设你想要做的是与以下类似的事情:
#include <iostream>
const long long limit = 1000000000000000000LL;
int main () {
   long long grand_total = 0;
   for (long long ii = 1; ii <= limit; ++ii) {
      grand_total += sum_of_digits(i);
   }
   std::cout << "Grand total = " << grand_total << "\n";
   return 0;
}

这个方法不可行,有两个原因:

  • 它需要很长很长的时间。
  • 它会溢出。

为了解决溢出问题,你要么必须限制上限,要么使用一些大数包。我会把解决这个问题留给你。

为了解决计算负担问题,你需要创造性地思考。如果你知道上限被限制在10的幂次方上,那么这就相当容易了。如果上限可以是任意数字,你就需要更有创意。

首先看一下如何计算从0到10n-1(例如,从0到9(n=1),从0到99(n=2),等等)的所有整数的数字总和。将10n-1的所有整数的数字总和表示为Sn。对于n=1(0到9),这只是0+1+2+3+4+5+6+7+8+9=45(9*10/2)。因此S1=45。

对于n=2(0到99),你要将0-9加十次,再将0-9加十次。对于n=3(0到999),你要将0-99加十次,再将0-9加100次。对于n=4(0到9999),你要将0-999加十次,再将0-9加1000次。一般来说,Sn=10Sn-1+10n-1S1作为一个递归表达式。这简化为Sn=(9n10n)/2。

如果上限是10的幂次方形式,那么解决方法就是上述的Sn再加上数字1000...000。如果上限是任意数字,你需要再次创造性地思考。想一想开发Sn公式的思路。


2

阅读您的编辑:在1到1000000000000000000之间对该函数进行循环计算需要很长时间。这是一个显而易见的问题。

1000000000000000000是一百万亿。您的处理器最多只能执行数十亿次操作。即使使用不存在的4-5 GHz处理器,并且假设最佳情况下它编译成加法、模数、除法和比较跳转,您每秒只能执行10亿次迭代,这意味着需要大约10亿秒。


2
你可以递归地将其拆分。一个18位数的数字的数字之和是前9个数字的和加上后9个数字的和。同样,一个9位数的数字的数字之和将是前4或5个数字的和加上后5或4个数字的和。当然,如果值为0,你可以特殊处理。

1
很难看出这如何解决问题(将所有数字的数字求和达到n)。这涉及单个数字的数字总和,但对于那个提问者的代码来说,它已经很好了。- 对于这个问题,也许更有帮助的是不要把数字分成一半,而是将18位数字中的数字总和视为通过“魔法运算”与17位数字中的数字总和结合起来的1-9数字的“魔法数字”。 - visitor

2

你可能不想用暴力的方式来解决这个问题。这更像是一个逻辑思维问题。

注意 - 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 = N(N+1)/2 = 45。

---- 在David的评论后更改答案,使其更清晰

请参考David的答案 - 我错了。


-1:不想采用暴力方法是个好主意,但实现方式不佳。计算从1到10^n的数字总和的正确公式是1 +(9 * n * 10 ^ n)/ 2。您的公式给出了1到100和1到1000的数字总和分别为541和5041。正确的值应该是901和13501。 - David Hammen
你可能是对的..我没有跟进并完成计算。你有你的公式证明吗?在我看来,它非常像1 + 45(1 + 10 + .... + 10^n-1)= 1 + 45(10^n - 1)/ 9 = 1 + 5(10^n -1)。 - Chip
@David,我已经让我的证明更清晰了...如果你发现逻辑上的漏洞,请告诉我。 - Chip
还不正确。请看我的答案。正确的公式是1 +(9 * n * 10 ^ n)/ 2。 - David Hammen
你看了我的回答吗?目前为止它还没有得到任何投票。设S_n为0到10^n-1之间所有整数的数字和(例如,S_3是整数0到999的数字和)。我找到了一个关于S_n的递归公式,S_n = 10*S_{n-1} + 10^{n-1}*S_1。至于显式表达式,我只需将递归关系输入Mathematica并让它完成工作。 - David Hammen
显示剩余3条评论

2
相当晚才参加派对,但无论如何,这是我的解决方案。很抱歉它是用 Python 而不是 C++ 写的,但应该相对容易翻译。因为这主要是一个算法问题,希望这样可以。至于溢出问题,我能想到的唯一办法就是使用数字数组而不是实际数字。考虑到这个算法,我希望它不会影响性能太多。

https://gist.github.com/frnhr/7608873

它使用了我通过观察和探索问题找到的这三种递归方式。与其试图想出一些晦涩难懂的通用方程式,这里提供了三个例子。从中很容易看出一般情况。

关系1

将具有任意参数的函数调用减少为几个具有更可预测参数的递归调用,以便在关系2和3中使用。

foo(3456) == foo(3000)
           + foo(400) + 400 * (3)
           + foo(50) + 50 * (3 + 4)
           + foo(6) + 6 * (3 + 4 + 5)

关系 2

将形式为L*10^M(例如:30、7000、900000)的带参数调用简化为可用于关系 3 的递归调用。这些 三角形数 出现得很突然(但是很受欢迎):)

triangular_numbers = [0, 1, 3, 6, 10, 15, 21, 28, 36]  # 0 not used
foo(3000) == 3 * foo(1000) + triangular_numbers[3 - 1] * 1000

只有当 L > 1 时才有用。对于 L = 1,它是显然的。在这种情况下,直接转到关系3。
关系3:递归地将参数格式为 1*10^M 的调用缩小到参数被除以10的调用。
foo(1000) == foo(100) * 10 + 44 * 100 + 100 - 9  # 44 and 9 are constants

最终你只需要真正计算0到10的数字的和或数字,结果只需要进行3次这样的计算。其他所有事情都可以通过递归来处理。我相当确定它在O(logN)时间内运行。那真是太快了!!!在我的笔记本电脑上,它可以在不到7秒的时间内计算出给定数字的超过1300位数的数字总和!你的测试(1000000000000000000)在0.000112057秒内被计算出来!

GitHub的链接已经失效了,如果你在其他地方备份了那个脚本,请更新链接,谢谢。 - undefined

1

我认为你不能找到比 O(N) 更好的算法,其中 N 是给定数字中数字的数量(这不是计算上昂贵的)。

然而,如果我正确理解了你的问题(范围),你想要输出一系列数字的数字总和。在这种情况下,当你从 number0 到 number9 时可以递增一次,然后减少8次。


那个最后的段落对我来说没有意义。此外,虽然他无法做得比O(N)更好,但他肯定可以使它变得更快或更慢。 - Arafangion

1

你需要作弊——寻找数学模式,以便快速计算。

  • 例如,你真的需要每次测试输入是否不等于0吗?如果你多次添加0/10会有影响吗?既然没有影响,考虑展开循环。
  • 你能否在更大的基础上进行计算,例如10^2、10^3等,这可能会使你减少数字的数量,然后再转换回10进制?如果这样做有效,你将能够更轻松地实现缓存。
  • 考虑查看编译器内置函数,让你为分支预测提供提示。
  • 鉴于这是C++,考虑使用模板元编程来实现。
  • 鉴于sum_of_digits是纯函数式的,请考虑缓存结果。

现在,大多数建议都会失败——但我想说的是,如果你已经达到了计算机在给定算法中所能承受的极限,你确实需要找到一个不同的解决方案。

如果您想要详细了解这个问题,以下链接可能是一个很好的起点:http://mathworld.wolfram.com/DigitSum.html

在纠结于微小的优化之前,或许可以先测量一下将数字相加直到100,000,000所需的时间,然后乘以1,000,000,000来大致估算这个算法需要多长时间?如果某个操作需要一百万年,那么微小的优化也只能将其缩短到最多10万年。 - visitor
@visitor:他不早就这样做了吗?难道他在问题中的main()函数不是为了演示他想要的性能吗? - Arafangion
我不明白。您是在暗示微小的优化可以让OP在人类寿命内以有意义的时间执行1000000000000000000L次操作吗?- 无论您如何优化,都不能用sum_of_digits(n)函数解决此问题。相反,您需要像sum_of_digits_in_n_digit_numbers(d)这样的函数。 - UncleBens
@UncleBens:除非他采用某种形式的惰性求值,使得这些优化在概念上完成,但直到必须这样做才实际计算。 他需要欺骗 - Arafangion

1

可能性1:

你可以通过将循环的一次迭代结果输入到下一次迭代中来加快速度。

例如,如果 i == 365,那么结果是 14。在下一个循环中,i == 366 -- 比上一个结果多了1。总和也多了1:3 + 6 + 6 = 15

当有进位数时会出现问题。如果 i == 99(即结果为18),下一个循环的结果不是19,而是1。您需要额外的代码来检测这种情况。

可能性2:

在思考以上问题时,我想到了 sum_of_digits 的结果序列在图形上看起来像是一个锯齿形。通过对结果图形的一些分析(留给读者作为练习),可能可以确定一种允许直接计算总和结果的方法。

然而,正如其他一些人指出的那样:即使使用最快的sum_of_digits实现和最优化的循环代码,您也不可能在任何有用的时间范围内计算1000000000000000000个结果,肯定不可能在不到一秒的时间内完成。

0

不是最好的,但很简单:

int DigitSumRange(int a, int b) {
    int s = 0;
    for (; a <= b; a++)
        for(c : to_string(a)) 
            s += c-48;

    return s;
}

0
下面给出了一个Python函数,它将数字转换为字符串,然后转换为数字列表,最后找到这些数字的总和。
def SumDigits(n):
    ns=list(str(n))
    z=[int(d) for d in ns]
    return(sum(z))

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