计算2个数字之间的小数位数


16

假设我们有一个非负整数,如果其平均数字值大于7,则该整数为“重”(即“重”)。

数字6959是“沉重的”,因为:

(6 + 9 + 5 + 9)/ 4 = 7.5

数字1234不是,因为:

(1 + 2 + 3 + 4)/ 4 = 2.5

用任何语言编写一个函数,

HeftyDecimalCount(a, b)

当提供两个正整数a和b时,它返回一个整数,该整数指示在[a..b](含)区间内有多少个“重”整数。

例如,给定a = 9480和b = 9489:

9480   (9+4+8+0)/4 21/4 = 5.25 
9481   (9+4+8+1)/4 22/4 = 5.5
9482   (9+4+8+2)/4 23/4 = 5.75  
9483   (9+4+8+3)/4 24/4 = 6    
9484   (9+4+8+4)/4 25/4 = 6.25     
9485   (9+4+8+5)/4 26/4 = 6.5 
9486   (9+4+8+6)/4 27/4 = 6.75  
9487   (9+4+8+7)/4 28/4 = 7
9488   (9+4+8+8)/4 29/4 = 7.25   hefty 
9489   (9+4+8+9)/4 30/4 = 7.5    hefty

此范围内的两个数字为“ hefty”,因此该函数应返回2。

一些准则:

  • 假设a或b都不超过200,000,000。
  • 一个n平方的解决方案会起作用,但会很慢-我们能解决这个问题的最快方法是什么?

2
是什么导致了超时?

Answers:


11

这个问题可以在O(polylog(b))中解决。

我们定义f(d, n)为不超过d个十进制数字的整数,其数字总和小于或等于n。可以看出,该函数由公式给出

f(d,n)

让我们从更简单的内容开始派生此功能。

h(n,d)= \ binom {n + d-1} {d-1} = \ binom {(n + 1)+(d-1)-1} {d-1}

函数h计算从包含n + 1个不同元素的多集中选择d-1个元素的方式的数量。这也是将n划分为d个容器的方法的数量,可以通过在n个栅栏周围构建d-1栅栏并对每个分离的部分求和来很容易地看出。n = 2,d = 3'的示例:

3-choose-2     fences        number
-----------------------------------
11             ||11          002
12             |1|1          011
13             |11|          020
22             1||1          101
23             1|1|          110
33             11||          200

因此,h计算具有n位和d位数字总和的所有数字。除非它仅对n小于10有效,因为数字限制为0-9。为了将其固定为值10-19,我们需要减去一个分区的分区数大于9,从现在开始,我将其称为溢出分区。

可以通过以下方式重用h来计算该项。我们计算分割n-10的方法的数量,然后选择将10放入其中的垃圾箱之一,这将导致具有一个溢出垃圾箱的分区数。结果是以下初步功能。

g(n,d)= \ binom {n + d-1} {d-1}-\ binom {d} {1} \ binom {n + d-1-10} {d-1}

我们通过计算对n-20进行分区的所有方式,然后选择2个bin(将10放入其中),从而对n小于或等于29的方式继续进行此操作,从而计算包含2个溢出bin的分区数。

但是在这一点上,我们必须要小心,因为在上一项中我们已经计算了具有2个溢出仓的分区。不仅如此,而且实际上我们对它们进行了两次计数。让我们使用一个示例,查看总和为21的分区(10,0,11)。在上一项中,我们减去了10,计算了其余11个分区的所有分区,然后将10放入3个bin中的一个。但是可以通过以下两种方法之一来达到此特定分区:

(10, 0, 1) => (10, 0, 11)
(0, 0, 11) => (10, 0, 11)

由于我们在第一项中也对这些分区进行了一次计数,因此具有2个溢出箱的分区的总数为1-2 = -1,因此我们需要通过添加下一项来对其进行计数。

g(n,d)= \ binom {n + d-1} {d-1}-\ binom {d} {1} \ binom {n + d-1-10} {d-1} + \ binom { d} {2} \ binom {n + d-1-20} {d-1}

再仔细考虑一下,我们很快就会发现,可以用下表来表示在特定术语中具有特定数量的溢出垃圾箱的分区的计数次数(第i列是术语i,第j行是j溢出的分区垃圾箱)。

1 0 0 0 0 0 . .
1 1 0 0 0 0 . .
1 2 1 0 0 0 . .
1 4 6 4 1 0 . .
. . . . . . 
. . . . . . 

是的,它是帕斯卡三角形。我们唯一感兴趣的计数是第一行/列中的计数,即具有零溢出箱的分区数。而且由于每行的交替总和,但第一行等于0(例如1-4 + 6-4 + 1 = 0),所以我们摆脱了它们,得出倒数第二个公式。

g(n,d)= \ sum_ {i = 0} ^ {d}(-1)^ i \ binom {d} {i} \ binom {n + d-1-10i} {d-1}

此函数计算d位数字总和为n的所有数字。

现在,数字总和小于n的数字呢?我们可以对二项式加上归纳参数使用标准递归来表明

\ bar {h}(n,d)= \ binom {n + d} {d} = \ binom {n + d-1} {d-1} + \ binom {n + d-1} {d} = h(n,d)+ \ bar {h}(n-1,d)

计算最多为n的数字总和的分区数。从中可以使用与g相同的参数得出f。

使用此公式,我们可以找到例如8000到8999之间的重数个数,例如 1000 - f(3, 20),因为此区间内有数千个数,并且我们必须减去数字总和小于或等于28的数个数同时考虑到第一个数字已经为数字总和贡献了8。

作为更复杂的示例,让我们看一下间隔1234..5678中的大量数字。我们首先可以以1的步长从1234到1240。然后,以10的步长从1240到1300。上面的公式为我们提供了每个这样的间隔中的重数数量:

1240..1249:  10 - f(1, 28 - (1+2+4))
1250..1259:  10 - f(1, 28 - (1+2+5))
1260..1269:  10 - f(1, 28 - (1+2+6))
1270..1279:  10 - f(1, 28 - (1+2+7))
1280..1289:  10 - f(1, 28 - (1+2+8))
1290..1299:  10 - f(1, 28 - (1+2+9))

现在我们从1300到2000,以100为步长:

1300..1399:  100 - f(2, 28 - (1+3))
1400..1499:  100 - f(2, 28 - (1+4))
1500..1599:  100 - f(2, 28 - (1+5))
1600..1699:  100 - f(2, 28 - (1+6))
1700..1799:  100 - f(2, 28 - (1+7))
1800..1899:  100 - f(2, 28 - (1+8))
1900..1999:  100 - f(2, 28 - (1+9))

从2000到5000,以1000为步长:

2000..2999:  1000 - f(3, 28 - 2)
3000..3999:  1000 - f(3, 28 - 3)
4000..4999:  1000 - f(3, 28 - 4)

现在我们必须再次减小步长,以100步从5000减小到5600,以10步从5600减小到5670,最后以1步从5670减小到5678。

一个示例Python实现(同时进行了一些优化和测试):

def binomial(n, k):
    if k < 0 or k > n:
        return 0
    result = 1
    for i in range(k):
        result *= n - i
        result //= i + 1
    return result

binomial_lut = [
    [1],
    [1, -1],
    [1, -2, 1],
    [1, -3, 3, -1],
    [1, -4, 6, -4, 1],
    [1, -5, 10, -10, 5, -1],
    [1, -6, 15, -20, 15, -6, 1],
    [1, -7, 21, -35, 35, -21, 7, -1],
    [1, -8, 28, -56, 70, -56, 28, -8, 1],
    [1, -9, 36, -84, 126, -126, 84, -36, 9, -1]]

def f(d, n):
    return sum(binomial_lut[d][i] * binomial(n + d - 10*i, d)
               for i in range(d + 1))

def digits(i):
    d = map(int, str(i))
    d.reverse()
    return d

def heavy(a, b):
    b += 1
    a_digits = digits(a)
    b_digits = digits(b)
    a_digits = a_digits + [0] * (len(b_digits) - len(a_digits))
    max_digits = next(i for i in range(len(a_digits) - 1, -1, -1)
                      if a_digits[i] != b_digits[i])
    a_digits = digits(a)
    count = 0
    digit = 0
    while digit < max_digits:
        while a_digits[digit] == 0:
            digit += 1
        inc = 10 ** digit
        for i in range(10 - a_digits[digit]):
            if a + inc > b:
                break
            count += inc - f(digit, 7 * len(a_digits) - sum(a_digits))
            a += inc
            a_digits = digits(a)
    while a < b:
        while digit and a_digits[digit] == b_digits[digit]:
            digit -= 1
        inc = 10 ** digit
        for i in range(b_digits[digit] - a_digits[digit]):
            count += inc - f(digit, 7 * len(a_digits) - sum(a_digits))
            a += inc
            a_digits = digits(a)
    return count

编辑:用优化的版本替换了代码(看起来比原始代码还要难看)。还修复了一些我遇到的棘手问题。 heavy(1234, 100000000)我的机器大约需要一毫秒的时间。


嗨,此解决方案有效且计算正确,但是小数的时间限制仅为0.10秒,大数的时间限制为0.35秒。您发布的上述代码花费了大约1秒钟的时间。您是否认为有更好的方法来处理某些数字,因为我们已经知道特定数字的数字总和小于7,所以可以跳过一些数字吗?或者,也许有一种更聪明的方式来解决这个问题?供您参考,该问题也被标记为难题。

1
@Bob:代码是用Python编写的,根本没有优化。如果您希望它快一些,请用C编写。但是在纯Python中,还有很多改进的余地。需要优化的第一件事是binomial()功能。还有其他一些可以轻松改进的地方。我将在几分钟后发布更新。
Sven Marnach 2011年

或者,我们可以仅使用具有预先计算的f(m,n)的查找表。鉴于限制为200,000,000,因此内存使用量应该最少。(您已经有我的+1)。

@Moron:那肯定是最好的选择-我会尝试的。
Sven Marnach 2011年

@Moron:我需要在源代码中包含查找表。通常f(d, n),在一次程序运行期间,不会使用相同的参数调用两次。
Sven Marnach 2011年

5

递归并使用排列。

假设我们定义了一个通用函数,该函数查找重于x的a和b之间的值:

heavy_decimal_count(a,b,x)

以a = 8675到b = 8689的示例为例,第一个数字为8,因此将其扔掉-答案将与675到689相同,再次从75到89。

前两位数字86的平均权重为7,因此剩余的数字需要大于7的平均权重才能合格。因此,电话

heavy_decimal_count(8675,8689,7)

相当于

heavy_decimal_count(75,89,7)

因此,我们的(新)第一位数范围是7到8,这些可能性如下:

7: 5-9
8: 0-9

对于7,我们仍然需要大于7的平均值,这只能来自最后8或9的数字,这给了我们2个可能的值。

对于8,我们需要大于6的平均值,只能取7-9的最后一位数字,这给了我们3个可能的值。

因此,2 + 3产生5个可能的值。

发生的事情是该算法从4位数字开始并将其划分为较小的问题。该函数将以问题的更简单版本反复调用自身,直到可以处理为止。


2
那么您是否声称Heavy(886,887)= Heavy(6,7)?

@Moron:不,因为前两个8更改了重量阈值。在示例中,前两个是86,平均为7,因此不会更改阈值。如果(8 + 8 + x)/ 3> 7,则x> 5。因此,重型(886,887,7.0)==重型(6,7,5.0)。

@Phil H,我认为这种想法不会奏效:如果您采用9900和9999,则会将其更改为可能介于0和99之间的较重值,例如考虑到8而9908并不是一个很大的数字( @Aryabhatta)。
汉斯·罗格曼

3

也许您可以通过累积“重”来跳过从a到b的间隔中的许多候选项。

如果您知道数字的长度,那么您就会知道每个数字只能改变1 /长度的重量。

因此,如果您从一个不重的数字开始,如果将其加一,则应该能够计算下一个重的数字。

在上面的示例中,从8680 avg = 5.5(距您的宽边边界7-5.5 = 1.5点)开始,您会知道之间有1.5 /(1/4)= 6个数字,这些数字并不繁重。

那应该是把戏!


一行“大量”数字也是如此。您只需计算出数字即可跳过!

1
只需将所有数字乘以位数,您将摆脱那些讨厌的/lengths。

1

一个简单的递归函数呢?为简单起见,它会计算所有带digits数字的重数字,以及一个最小数字之和min_sum

int count_heavy(int digits,int min_sum) {
  if (digits * 9 < min_sum)//impossible (ie, 2 digits and min_sum=19)
    return 0; //this pruning is what makes it fast

  if (min_sum <= 0)
      return pow(10,digits);//any digit will do,
      // (ie, 2 digits gives 10*10 possibilities)

  if (digits == 1)
  //recursion base
    return 10-min_sum;//only the highest digits

  //recursion step
  int count = 0;
  for (i = 0; i <= 9; i++)
  {
     //let the first digit be i, then
     count += count_heavy(digits - 1, min_sum - i);
  }
  return count;
}

count_heavy(9,7*9+1); //average of 7,thus sum is 7*9, the +1 is 'exceeds'.

在python中实现此功能,它在约2秒内发现了所有9位重数字。进行一些动态编程可以改善这一点。


0

这是一种可能的解决方案。

public int heavy_decimal_count(int A, int B)
{
    int count = 0;                       
    for (int i = A; i <= B; i++)
    {
        char[] chrArray = i.ToString().ToCharArray();
        float sum = 0f;
        double average = 0.0f;
        for (int j = 0; j < chrArray.Length; j++)
        {
            sum = sum + (chrArray[j] - '0');                   
        }
        average = sum / chrArray.Length;                
        if (average > 7)
            count++;
    }
    return count;
}

1
欢迎来到Code Golf。当一个问题已经被回答时,如果比一个获胜标准中的问题要好得多,或者它们显示出一种新颖有趣的回答方式,就会欢迎更多答案。我也看不到你的答案。
ugoren 2012年

0

C,间隔[a,b]为O(ba)

c(n,s,j){return n?c(n/10,s+n%10,j+1):s>7*j;}

HeftyDecimalCount(a,b){int r; for(r=0;a<=b;++a)r+=c(a,0,0); return r;}

//练习

main()
{
 printf("[9480,9489]=%d\n", HeftyDecimalCount(9480,9489));
 printf("[0,9489000]=%d\n", HeftyDecimalCount(9480,9489000));
 return 0;
}

//结果

//[9480,9489]=2
//[0,9489000]=66575

什么是“标准漏洞”?
RosLuP

1
@Riker这里的标签不是<codegolf>,而是<fast algorithm>
RosLuP
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.