53

想象一下,您出售用于对房屋、储物柜门、酒店房间等进行编号的金属数字。当您的客户需要对门/房屋进行编号时,您需要找出每个数字中要运送的数量:

  • 1 到 100
  • 51 至 300
  • 1 到 2,000,左边有零

显而易见的解决方案是从第一个数字到最后一个数字进行循环,将计数器转换为左侧有或没有零的字符串,提取每个数字并将其用作索引以增加 10 个整数的数组。

我想知道是否有更好的方法来解决这个问题,而不必遍历整个整数范围。

欢迎使用任何语言或伪代码的解决方案。


编辑:

答案
是 CashCommons 的 JohnWayne Conrad评论说我目前的方法足够好且足够快。让我用一个愚蠢的类比:如果你被要求在不到 1 分钟的时间内计算棋盘中的方格,你可以通过一个一个地计算方格来完成任务,但更好的解决方案是数边数和做乘法,因为稍后可能会要求您计算建筑物中的瓷砖。
Alex Reisner指出了一个非常有趣的数学定律,不幸的是,它似乎与这个问题无关。
Andres建议使用我正在使用的相同算法,但使用 %10 操作而不是子字符串来提取数字。
约翰在 CashCommonsphord建议预先计算所需的数字并将它们存储在查找表中,或者为了原始速度,存储在一个数组中。如果我们有一个绝对的、不可移动的、一成不变的最大整数值,这可能是一个很好的解决方案。我从未见过其中之一。
高性能标记过滤器计算了各种范围所需的数字。一百万的结果似乎表明有一个比例,但其他数字的结果显示出不同的比例。
过滤器发现了一些可用于计算数字的数字的公式,这些数字是十的幂。 Robert Harvey在 MathOverflow 上发布问题时有过一次非常有趣的经历。一个数学家用数学符号写了一个解决方案。
阿罗诺特使用数学开发和测试了一个解决方案。发布后,他查看了源自 Math Overflow 的公式,发现其中存在缺陷(指向 Stackoverflow :)。
noahlavine开发了一种算法并以伪代码形式呈现。

一个新的解决方案
在阅读了所有答案并做了一些实验后,我发现对于从 1 到 10 n -1 的整数范围:

  • 对于数字 1 到 9,需要 n*10 (n-1)
  • 对于数字 0,如果不使用前导零,则需要 n*10 n-1 - ((10 n -1) / 9)
  • 对于数字 0,如果使用前导零,则需要 n*10 n-1 - n

第一个公式是由过滤器(可能是其他人)找到的,我通过反复试验找到了另外两个(但它们可能包含在其他答案中)。

例如,如果 n = 6,则范围是 1 到 999,999:

  • 对于数字 1 到 9,我们需要 6*10 5 = 每个数字 600,000
  • 对于数字 0,没有前导零,我们需要 6*10 5 – (10 6 -1)/9 = 600,000 - 111,111 = 488,889
  • 对于数字 0,带前导零,我们需要 6*10 5 – 6 = 599,994

可以使用High-Performance Mark结果检查这些数字。

使用这些公式,我改进了原始算法。它仍然从整数范围内的第一个数字循环到最后一个数字,但是,如果它找到一个 10 的幂的数字,它会使用公式将数字添加到数字上,计算 1 到 9 的整个范围内的数量或 1 到 99 或 1 到 999 等。这是伪代码中的算法:

integer First,Last //范围内的第一个和最后一个数字
integer Number //当前循环中的数字
integer Power //Power 是公式中 10^n 中的 n
integer Nines //Nines 是 10^n - 1, 10^5 - 1 = 99999 的结果
integer Prefix //数字中的第一个数字。对于 14,200,前缀是 142
array 0..9 Digits //将保存所有数字的计数

FOR 编号 = 从第一个到最后一个
  CALL TallyDigitsForOneNumber WITH Number,1 //统计每个数字的计数
                                              //在数字中,加1
  //开始优化。注释适用于 Number = 1,000 和 Last = 8,000。
  幂 = 数字末尾的零 //对于 1,000,幂 = 3
  IF Power > 0 //数字以0 00 000等结尾
    九号 = 10^Power-1 //九号 = 10^3 - 1 = 1000 - 1 = 999
    IF Number+Nines <= Last //如果 1,000+999 < 8,000,添加一个完整的集合
      Digits[0-9] += Power*10^(Power-1) //将 3*10^(3-1) = 300 添加到数字 0 到 9
      Digits[0] -= -Power //调整数字0(前导零公式)
      Prefix = Number 的前几位 //对于 1000,前缀为 1
      CALL TallyDigitsForOneNumber WITH Prefix,Nines //统计每个的计数
                                                     //前缀中的数字,
                                                     //递增 999
      Number += Nines //增加循环计数器 999 个周期
    万一
  万一
  //优化结束
ENDFOR  

SUBROUTINE TallyDigitsForOneNumber PARAMS Number,Count
  重复
    数字 [ 数字 % 10 ] += 计数
    数字 = 数字 / 10
  直到编号 = 0

例如,对于 786 到 3,021 的范围,计数器将递增:

  • 由 1 从 786 到 790(5 个周期)
  • 由 9 从 790 到 799(1 个周期)
  • 由 1 从 799 到 800
  • 由 99 从 800 到 899
  • 由 1 从 899 到 900
  • 由 99 从 900 到 999
  • 由 1 从 999 到 1000
  • 999 从 1000 到 1999
  • 1999 年至 2000 年
  • 999 从 2000 年到 2999
  • 由 1 从 2999 到 3000
  • 由 1 从 3000 到 3010(10 个周期)
  • 由 9 从 3010 到 3019(1 个周期)
  • 由 1 从 3019 到 3021(2 个周期)

总计:28 个周期 未优化:2,235 个周期

请注意,此算法在没有前导零的情况下解决了该问题。要将它与前导零一起使用,我使用了一个技巧:

如果需要带前导零的 700 到 1,000 范围,请使用 10,700 到 11,000 的算法,然后从数字 1 的计数中减去 1,000 - 700 = 300。

基准和源代码

我测试了原始方法,使用 %10 的相同方法和一些大范围的新解决方案,结果如下:

原始 104.78 秒
含 %10 83.66
十次方 0.07

基准应用程序的屏幕截图:(来源:clarion.sca.mx
替代文字

如果您想查看完整的源代码或运行基准测试,请使用以下链接:

接受的答案

noahlavine解决方案可能是正确的,但我只是无法遵循伪代码,我认为有一些细节缺失或没有完全解释。

Aaronaught解决方案似乎是正确的,但代码对我来说太复杂了。

我接受了过滤器的回答,因为他的思路指导我开发了这个新的解决方案。

4

11 回答 11

10

像这样的问题有一个明确的数学解决方案。让我们假设该值被零填充到最大位数(不是,但我们稍后会对此进行补偿),并通过它进行推理:

  • 从0-9,每个数字出现一次
  • 从 0 到 99,每个数字出现 20 次(位置 1 为 10x,位置 2 为 10x)
  • 从 0 到 999,每个数字出现 300 次(P1 中 100x,P2 中 100x,P3 中 100x)

如果范围是从 0 到 10 的幂,则任何给定数字的明显模式是N * 10 N-1,其中N是 10 的幂。

如果范围不是 10 的幂怎么办?从最低的 10 次幂开始,然后逐步增加。最容易处理的情况是像 399 这样的最大值。我们知道,对于 100 的每个倍数,每个数字至少出现20 次,但我们必须补偿它出现在最高有效数字位置的次数,对于数字 0-3,这将是 100,对于所有其他数字,这将是完全零。具体来说,要添加的额外数量是相关数字的 10 N。

将其放入公式中,对于比 10 的某个倍数(即 399、6999 等)小 1 的上界,它变为: M * N * 10 N-1 + iif(d <= M, 10 N , 0)

现在您只需要处理其余部分(我们将其称为R)。以445为例。这就是 399 加上范围 400-445 的结果。在此范围内,MSD 出现R次更多,并且所有数字(包括 MSD)也以与范围 [0 - R ] 相同的频率出现。

现在我们只需要补偿前导零。这种模式很简单——它只是:

10 N + 10 N-1 + 10 N-2 + ... + **10 0

更新: 这个版本正确地考虑了“填充零”,即在处理余数时中间位置的零([4 0 0, 4 0 1, 4 0 2, ...])。找出填充零有点难看,但修改后的代码(C 风格的伪代码)可以处理它:

function countdigits(int d, int low, int high) {
    return countdigits(d, low, high, false);
}

function countdigits(int d, int low, int high, bool inner) {
    if (high == 0)
        return (d == 0) ? 1 : 0;

    if (low > 0)
        return countdigits(d, 0, high) - countdigits(d, 0, low);

    int n = floor(log10(high));
    int m = floor((high + 1) / pow(10, n));
    int r = high - m * pow(10, n);
    return
        (max(m, 1) * n * pow(10, n-1)) +                             // (1)
        ((d < m) ? pow(10, n) : 0) +                                 // (2)
        (((r >= 0) && (n > 0)) ? countdigits(d, 0, r, true) : 0) +   // (3)
        (((r >= 0) && (d == m)) ? (r + 1) : 0) +                     // (4)
        (((r >= 0) && (d == 0)) ? countpaddingzeros(n, r) : 0) -     // (5)
        (((d == 0) && !inner) ? countleadingzeros(n) : 0);           // (6)
}

function countleadingzeros(int n) {
      int tmp= 0;
      do{
         tmp= pow(10, n)+tmp;
         --n;
         }while(n>0);
         return tmp;
         }

function countpaddingzeros(int n, int r) {
    return (r + 1) * max(0, n - max(0, floor(log10(r))) - 1);
}

正如你所看到的,它变得有点难看,但它仍然在 O(log n) 时间内运行,所以如果你需要处理数十亿的数字,这仍然会给你即时的结果。:-) 如果你在 [0 - 1000000] 范围内运行它,你会得到与 High-Performance Mark 发布的完全相同的分布,所以我几乎肯定它是正确的。

仅供参考,变量的原因inner是前导零函数已经是递归的,所以它只能在第一次执行时被计算在内countdigits

更新 2:如果代码难以阅读,这里是countdigitsreturn 语句每一行含义的参考(我尝试了内联注释,但它们使代码更难阅读):

  1. 任何数字的频率高达 10 的最高幂(0-99 等)
  2. MSD 频率高于 10 (100-399) 的最高功率的任意倍数
  3. 余数中任何数字的频率(400-445,R = 45)
  4. 其余 MSD 的额外频率
  5. 在余数范围的中间位置计数零(404、405...)
  6. 仅减去前导零一次(在最外层循环上)
于 2010-01-14T03:52:52.673 回答
8

我假设你想要一个数字在一个范围内的解决方案,并且你有开始和结束的数字。想象一下,从起始编号开始计数,直到到达结束编号 - 它会起作用,但会很慢。我认为快速算法的诀窍是要意识到,为了在 10^x 的位置上增加一个数字并保持其他所有内容相同,您需要使用它之前的所有数字 10^x 次加上所有数字 0 -9 10^(x-1) 次。(除了您的计数可能涉及到第 x 位的进位 - 我在下面对此进行了更正。)

这是一个例子。假设您从 523 数到 1004。

  • 首先,您从 523 数到 524。这使用数字 5、2 和 4 各一次。
  • 其次,从 524 数到 604。最右边的数字在所有数字中循环 6 次,因此每个数字需要 6 个副本。第二个数字经过数字 2 到 0,每个数字 10 次。第三位数字是 6 5 次和 5 100-24 次。
  • 第三,从 604 数到 1004。最右边的数字做了 40 个循环,所以每个数字添加 40 个副本。右数第二个执行 4 个循环,因此每个数字添加 4 个副本。最左边的数字是 7、8 和 9 中的每一个,加上 0 中的 5 和 6 中的 100 - 5。最后一个数字是 1 5 次。

要加快最后一点,请查看最右边两个位置的部分。它使用每个数字 10 + 1 次。一般来说,1 + 10 + ... + 10^n = (10^(n+1) - 1)/9,我们可以使用它来加快计数速度。

我的算法是从开始数到结束数(使用 base-10 计数),但使用上面的事实可以快速完成。您从最低有效位到最高有效位遍历起始数字的数字,并在每个位置进行计数,以使该数字与结束数字中的数字相同。在每一点上,n 是在进位前你需要做的向上计数的次数,m 是你之后需要做的次数。

现在让我们假设伪代码算作一种语言。那么,这就是我要做的:

将开始和结束数字转换为数字数组 start[] 和 end[]
创建一个包含 10 个元素的数组 counts[],用于存储
     您需要的每个数字

从右到左遍历开始编号。在第 i 位,
    让 d 是您必须从该数字中获得的位数
        到结束号码中的第 i 个数字。(即减去等价的
        数字 mod 10)
    将 d * (10^i - 1)/9 添加到计数中的每个条目。
    令 m 为该数字右边所有数字的数值,
        n 为 10^i - m。
    对于每个数字 e 从起始数字的左侧直到并包括
        第 i 个数字,将 n 添加到该数字的计数中。
    对于 j 在 1 到 d
        将第 i 个数字加一,包括进行任何进位
        对于从起始数字左侧到包括在内的每个数字 e
            第 i 个数字,将 10^i 添加到该数字的计数
    对于每个数字 e 从起始数字的左侧直到并包括
        第 i 个数字,将 m 添加到该数字的计数中。
    将起始数字的第 i 位设置为结尾的第 i 位
        数字。

哦,由于 i 的值每次都会增加 1,因此请跟踪您的旧 10^i 并将其乘以 10 以获得新的,而不是每次取幂。

于 2010-01-13T20:01:37.120 回答
7

要从一个数字中提取数字,如果我们不能做一个 mod,我们只需要进行昂贵的字符串转换,数字可以很快地从一个数字中推入,如下所示:

feed=number;
do
{ digit=feed%10;
  feed/=10; 
  //use digit... eg. digitTally[digit]++;
  }
while(feed>0)

该循环应该非常快,并且可以放在从开始到结束数字的循环中,以便以最简单的方式计算数字。

为了更快,对于更大范围的数字,我正在寻找一种优化的方法来计算从 0 到 number*10^significance 的所有数字(从头到尾让我感到困惑)

这是一个表格,显示了一些单个有效数字的数字计数。这些包括 0,但不包括最高值本身, - 这是一个疏忽,但它可能更容易看到模式(这里没有最高值数字)这些计数不包括尾随零,

  1 10 100 1000 10000 2 20 30 40 60 90 200 600 2000  6000

0 1 1  10  190  2890  1  2  3  4  6  9  30 110  490  1690
1 0 1  20  300  4000  1 12 13 14 16 19 140 220 1600  2800
2 0 1  20  300  4000  0  2 13 14 16 19  40 220  600  2800
3 0 1  20  300  4000  0  2  3 14 16 19  40 220  600  2800
4 0 1  20  300  4000  0  2  3  4 16 19  40 220  600  2800
5 0 1  20  300  4000  0  2  3  4 16 19  40 220  600  2800
6 0 1  20  300  4000  0  2  3  4  6 19  40 120  600  1800
7 0 1  20  300  4000  0  2  3  4  6 19  40 120  600  1800
8 0 1  20  300  4000  0  2  3  4  6 19  40 120  600  1800
9 0 1  20  300  4000  0  2  3  4  6  9  40 120  600  1800

编辑:清理我的原始想法:

从显示从 0(包括)到 poweroTen(notinc)的计数的蛮力表中可以看出,十幂的一个主要数字:

increments tally[0 to 9] by md*tp*10^(tp-1)
increments tally[1 to md-1] by 10^tp
decrements tally[0] by (10^tp - 10) 
(to remove leading 0s if tp>leadingzeros)
can increment tally[moresignificantdigits] by self(md*10^tp) 
(to complete an effect)

如果对每个有效数字应用这些计数调整,则应修改计数,就好像从 0 计数到 end-1

可以反转调整以删除前面的范围(起始编号)

感谢 Aaronaught 提供完整且经过测试的答案。

于 2010-01-14T02:35:05.990 回答
6

这是一个非常糟糕的答案,我很惭愧发布它。我让 Mathematica 计算从 1 到 1,000,000 的所有数字中使用的数字,没有前导 0。这是我得到的:

0   488895
1   600001
2   600000
3   600000
4   600000
5   600000
6   600000
7   600000
8   600000
9   600000

下次您在五金店订购粘性数字时,按照这些比例订购,您不会错太多。

于 2010-01-14T03:00:51.287 回答
5

在 Math Overflow 上问了这个问题,因为问了这么简单的问题而被打屁股。其中一位用户同情我,说如果我将它发布到解决问题的艺术,他会回答;所以我做了。

这是他发布的答案:
http ://www.artofproblemsolving.com/Forum/viewtopic.php?p=1741600#1741600

令人尴尬的是,我的数学不足以理解他发布的内容(这个人 19 岁……这太令人沮丧了)。我真的需要上一些数学课。

从好的方面来说,这个方程是递归的,所以只要懂数学的人用几行代码把它变成一个递归函数应该是一件简单的事情。

于 2010-01-14T00:34:12.937 回答
3

你的方法很好。我不确定为什么您需要比您所描述的更快的东西。

或者,这会给你一个即时的解决方案:在你真正需要它之前,从 1 到某个最大数计算你需要什么。您可以存储每个步骤所需的数字。如果您有像第二个示例这样的范围,则它将是 1 到 300 所需的范围,减去 1 到 50 所需的范围。

现在您有一个可以随意调用的查找表。最多计算 10,000 只需要几 MB,而且,计算一次只需几分钟?

于 2010-01-13T20:01:49.667 回答
3

我知道这个问题有一个可以接受的答案,但我的任务是为面试编写这段代码,我想我想出了一个快速的替代解决方案,不需要循环,并且可以根据需要使用或丢弃前导零。

这实际上很简单,但不容易解释。

如果你列出前 n 个数字

     1
     2
     3

     .
     .
     .


     9
    10
    11

通常以从左到右的方式开始计算从开始房间号到结束房间号所需的数字,所以对于上面我们有一个 1,一个 2,一个 3 ...一个 9,两个 1 的一个零,四个 1 等。我见过的大多数解决方案都使用这种方法并进行了一些优化以加快速度。

我所做的是在列中垂直计数,如以百、十和单位为单位。您知道最高的房间号,因此我们可以通过一次除法计算百列中每个数字的数量,然后递归并计算十列中的数量等。然后我们可以根据需要减去前导零。

如果您使用 Excel 写出数字但对数字的每个数字使用单独的列,则更容易可视化

     A    B    C
     -    -    -
     0    0    1  (assuming room numbers do not start at zero)
     0    0    2
     0    0    3
     .
     .
     .
     3    6    4
     3    6    5
     .
     .
     .

     6    6    9
     6    7    0
     6    7    1

     ^
     sum in columns not rows

因此,如果最高房间号是 671,则数百列将有 100 个垂直零,然后是 100 个 1,依此类推,直到 71 个 6,如果需要,请忽略 100 个零,因为我们知道这些都是领先的。

然后递归到十位并执行相同的操作,我们知道将有 10 个零,然后是 10 个 1,依此类推,重复六次,最后一次下降到 2 个七。同样可以忽略前 10 个零,因为我们知道它们领先。最后当然是做单位,根据需要忽略第一个零。

所以没有循环,一切都是用除法计算的。我使用递归来“向上”移动列,直到达到最大值(在本例中为数百个),然后随着它的推移返回总计。

我用 C# 编写了这个,如果有人感兴趣,可以发布代码,还没有做任何基准计时,但对于高达 10^18 个房间的值来说,它基本上是即时的。

找不到此处或其他地方提到的这种方法,因此认为它可能对某人有用。

于 2015-05-28T14:59:33.183 回答
1

这并不能回答您的确切问题,但有趣的是要注意根据本福德定律的第一个数字的分布。例如,如果你随机选择一组数字,其中 30% 会以“1”开头,这有点违反直觉。

我不知道任何描述后续数字的分布,但您可能能够根据经验确定这一点,并提出一个简单的公式来计算任何数字范围所需的近似位数。

于 2010-01-13T20:15:19.827 回答
1

如果“更好”意味着“更清晰”,那么我对此表示怀疑。如果它的意思是“更快”,那么是的,但如果没有迫切的需要,我不会使用更快的算法来代替更清晰的算法。

#!/usr/bin/ruby1.8

def digits_for_range(min, max, leading_zeros)
  bins = [0] * 10
  format = [
    '%',
    ('0' if leading_zeros),
    max.to_s.size,
    'd',
  ].compact.join
  (min..max).each do |i|
    s = format % i
    for digit in s.scan(/./)
      bins[digit.to_i] +=1  unless digit == ' '
    end
  end
  bins
end

p digits_for_range(1, 49, false) 
# => [4, 15, 15, 15, 15, 5, 5, 5, 5, 5]

p digits_for_range(1, 49, true)
# => [13, 15, 15, 15, 15, 5, 5, 5, 5, 5]

p digits_for_range(1, 10000, false)
# => [2893, 4001, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000]

Ruby 1.8,一种被称为“狗慢”的语言,在 0.135 秒内运行上述代码。这包括加载解释器。除非您需要更快的速度,否则不要放弃明显的算法。

于 2010-01-13T21:39:13.103 回答
1

如果您需要多次迭代的原始速度,请尝试查找表:

  1. 构建一个二维数组:10 x max-house-number

    int nDigits[10000][10] ;   // Don't try this on the stack, kids!
  1. 用从零到该数字所需的位数填充每一行。
    提示:使用上一行作为开始:

    n=0..9999:
       if (n>0) nDigits[n] = nDigits[n-1]
       d=0..9:
           nDigits[n][d] += countOccurrencesOf(n,d)   // 
  1. Number of digits "between" two numbers becomes simple subtraction.
       For range=51 to 300, take the counts for 300 and subtract the counts for 50.
       0's = nDigits[300][0] - nDigits[50][0]
       1's = nDigits[300][1] - nDigits[50][1]
       2's = nDigits[300][2] - nDigits[50][2]
       3's = nDigits[300][3] - nDigits[50][3]
       etc.
于 2010-01-15T18:01:59.343 回答
0

您可以分隔每个数字(查看此处的示例),创建一个包含 0..9 条目的直方图(它将计算一个数字中出现了多少个数字)并乘以所询问的“数字”的数量。

但如果不是你要找的,你能举一个更好的例子吗?

编辑:

现在我想我有问题了。我想你可以认为这个(伪C):

int histogram[10];
memset(histogram, 0, sizeof(histogram));

for(i = startNumber; i <= endNumber; ++i)
{
    array = separateDigits(i);
    for(j = 0; k < array.length; ++j)
    {
        histogram[k]++;
    }
}

单独的数字实现链接中的功能。

直方图的每个位置都会有每个数字的数量。例如

histogram[0] == total of zeros
histogram[1] == total of ones

...

问候

于 2010-01-13T19:47:05.387 回答