计算具有数字S计数的大A和B之间的整数

时间:2014-08-30 04:30:30

标签: algorithm dynamic-programming

我试图计算范围A到B之间的整数,其数字和S(假设S = 60)。

A和B的范围为1到10 ^ 18。

让X为数字,Upto Y我们必须计算整数。

X = x1 x2 ... xn - 1 xn和Y = y1 y2 ... yn - 1 yn,其中xi和yi是X和Y的十进制数字。

leftmost_lo是最小的i,xi<义。如果没有这样的i,我们将leftmost_lo定义为n + 1。类似地,我们将leftmost_hi定义为xi> 1的最小i。 yi,否则为n + 1。

函数计数返回整数X的数字f(Y),其属性X≤Y,X的数字总和为60.

根据上面的定义,令n为Y的数字,y [i]为Y的第i个十进制数。以下递归算法解决了这个问题:

    count(i, sum_so_far, leftmost_lo, leftmost_hi):
       if i == n + 1:
       # base case of the recursion, we have recursed beyond the last digit
       # now we check whether the number X we built is a valid solution
        if sum_so_far == 60 and leftmost_lo <= leftmost_hi:
          return 1
        else: 
          return 0
     result = 0
     # we need to decide which digit to use for x[i]
     for d := 0 to 9
        leftmost_lo' = leftmost_lo
        leftmost_hi' = leftmost_hi
        if d < y[i] and i < leftmost_lo': leftmost_lo' = i
        if d > y[i] and i < leftmost_hi': leftmost_hi' = i
       result += count(i + 1, sum_so_far + d, leftmost_lo', leftmost_hi')
    return result






Compute the number f(Y) of integers X with the property X ≤ Y and X has the digit sum 60

现在我们有f(Y)= count(1,0,n + 1,n + 1),我们已经解决了问题。运行时

对于这个特定的实现,

是O(n ^ 4)。

我理解这个概念来自这个链接。 How to count integers between large A and B with a certain property?

但无法理解如何优化这一点。

现在,我如何针对此特定问题优化此O(n)解决方案。

任何人都可以帮助我。

3 个答案:

答案 0 :(得分:2)

首先,你可以注意到,如果你有一个函数F,它返回的数量&lt; = A,数字和S,那么A和B之间的数字和S的整数是F(B)-F (A-1)。

然后定义一些符号:

  • n(A)表示由与A)具有相同位数的所有9个组成的数字。例如,n(123)= 999。
  • A [0]表示A
  • 的最左侧数字
  • A [1:]表示删除了最左边数字的A。

然后你有这些关系,一次做一个数字,并注意到可能性是你匹配A的第一个数字,或者你在那里放一个较低的数字(然后对于递归的情况,你可以用一个数字替换A所有9s。)

F(S, A) = 1 if S = 0
F(S, A) = 0 if S < 0 or A = 0
otherwise F(S, A) =
    F(S-A[0], A[1:])
    + F(S-0, n(A[1:])) + F(S-1, n(A[1:])) + ... + F(S-A[0]-1, n(A[1:]))

这为您提供了这段代码(使用缓存来避免多次计算相同的事情):

def count1(S, digits, nines, k, cache):
    if S <= 0 or k == len(digits): return S==0
    key = (S, nines, k)
    if key not in cache:
        dk = 9 if nines else digits[k]
        cache[key] = sum(count1(S-i, digits, nines or i<dk, k+1, cache)
                         for i in xrange(dk+1))
    return cache[key]

def count(S, A):
    return count1(S, map(int, str(A)), False, 0, {})

def count_between(S, A, B):
    return count(S, B) - count(S, A-1)

print count_between(88, 1, 10**10)

缓存最终大小为S * 2 * len(str(A))并且每个事物都计算一次,这给你复杂性:O(S * log_10(A))。

答案 1 :(得分:1)

对于A = 1且B = 10 ^ 18,生成S的所有整数分区,其中少于19个部分且每个部分小于10.答案是每个分区的不同排列数的总和数字与(18 - number_of_parts)零结合。

对于其他A和B,在边缘处涉及的数学略多一些:)

对于范围1到任意B,我们可以使用类似的过程,但有更多的枚举:

让我们说B有数字b1 b2 ... bn - 1 bn。我们递减b1并枚举数字S - (b1 - 1)的分区(少于n个部分,每个部分低于10),以及与(n - 1 - number_of_parts)零结合时它们的不同排列的基数。我们重复这个过程直到并包括b1 = 0(这里最大的零件数和前导零将减1)。然后我们对b2重复类似的过程,但这次S首先减少b1。等等,总结结果。

对于任意A和B,我们从f(B)中减去f(A)。

JavaScript代码:

function choose(n,k){
  if (k == 0 || n == k){
    return 1;
  }
  var product = n;
  for (var i=2; i<=k; i++){
    product *= (n + 1 - i) / i
  }
  return product;
}

function digits(n){
  var ds = [];
  while (n){
    ds.push(n % 10);
    n = Math.floor(n/10);
  }
  return ds.reverse()
}

function ps(n,maxParts){
  if (maxParts <= 0){
    return 0;
  }
  var result = 0;
  for (var i=9; i>=Math.floor(n/maxParts); i--){
    var r = [0,0,0,0,0,0,0,0,0,0,1]; // 11th place is number of parts
    r[i]++;
    result += _ps(n-i,r,i,1,maxParts);
  }
  return result;
}

function _ps(n,r,i,c,maxParts){
  if (n==0){
    return numPs(r,maxParts);
  } else if (c==maxParts || n<0){
    return 0;
  } else{
    var result = 0;
    for (var j=i; j>0; j--){
      var r0 = r.slice();
      r0[j]++;
      r0[10]++;
      result += _ps(n-j,r0,j,c+1,maxParts);
    }
    return result;
  }
}

function numPs(partition,n){
  var l = choose(n,n - partition[10]);
  n = partition[10];
  for (var i=0; i<10;i++){
    if (partition[i] != 0){
      l *= choose(n,partition[i]);
      n -= partition[i];
    }
  }
  return l;
}

function f(n,s){
  var ds = digits(n),
      n = ds.length,
      answer = 0;
  for (var i=0; i<n - 1; i++){
    if (ds[i] != 0){
      var d = ds[i] - 1;
      while (d >= 0){
        answer += ps(s - d,n - i - 1);
        d--;
      }
      s -= ds[i];
    }
  }
  if (s <= ds[n - 1]){
    answer++;
  }

  return answer;
}

输出:

console.log(f(1,1));
1

console.log(f(1000,3));
10

console.log(f(1001,3));
10

console.log(f(1002,3));
11

console.log(f(1003,3));
11

console.log(f(1010,3));
11

答案 2 :(得分:0)

编辑哦亲爱的!就在我承认我的答案错过了重点之后,它就被接受了。我已经离开了原来的答案,并解释了我的算法背后的想法,以及它在折叠后如何不如原始解决方案。


这个特殊问题应该被视为“所有18位数的整数”而不是“1到10 ^ 18之间的所有整数”。 (对于数字总和,少于18位的数字可以被视为带有前导零的18位数字。)

然后你可以使用从下往上传播的算法,就像Erathostenes的筛子遍布所有非素数一样。

从数字计数dig开始,数字1到9表示0,即全零。 (零的数量可以计算为18 - sum(dig)。然后你可以像这样递归:

recurse(dig[], pos) {
    if (digitsum(dig) > 60) return;

    if (digitsum(dig) == 60) {
        count += poss(dig)
    } else {
        if (pos < 9) recurse(dig, pos + 1);
        if (sum(dig) < 18) {
            dig[pos]++;
            recurse(dig, pos);
            dig[pos]--;
        }  
    }
}

这样您就可以处理所有数字计数组合并在超过60时返回。当您准确命中60时,您必须计算与该数字计数对应的可能数字的数量:

poss(dig) = 18! / prod(dig[i]!)

阶乘prod(dig[i]!)的乘积必须包含零阶乘。 (当然,0! == 1。)

如果你到目前为止跟踪总和并预先计算阶乘,那么这种方法应该足够快。如果你想计算50到5,000,000,000之间数字总和为60的所有数字,它就不起作用了。


附录您链接到的框架可以处理从A到B的任何范围。这里,让我们关注范围从0到10 ^ n,即n位数字,其中数字较少数字被认为具有前导零。

我的算法的想法不是枚举所有数字,而是考虑数字的计数。例如,如果我的号码有5倍数字9和3倍数字5,则数字总和为60.现在我们必须找到满足该条件的18位数。 590,050,005,090,900,099就是这样一个数字,但这个数字的所有唯一排列也是有效的。该数字具有18 - (5 + 3)= 10个零,因此该组合具有

N(5x9, 3x5) = 18! / (5! * 3! * 10!)

排列。

算法必须枚举所有排列。它跟踪数组中的枚举dig

     ^ count
     |
2x   ...  ...  ...  ...  ...  ...  ...  ...  ... 

1x   ...  ...  ...  ...  ...  ...  ...  ...  ...  

0x   ...  ...  ...  ...  ...  ...  ...  ...  ...

     ---------------------------------------------> pos
      1    2    3    4    5    6    7    8    9

以上案例将是

dig == [0, 0, 0, 0, 3, 0, 0, 0, 5]

为实现这一目标,它以锯齿形图案传播。当前数字称为pos。它可以通过将当前数字的计数递增1来垂直移动,也可以通过考虑下一个数字来水平移动。如果数字总和达到或超过S或者如果pos超过9,则递归停止。每当我们达到S时,我们进行如上所示的置换计算。

因为数组是通过引用传递的,并且实际上是相同的数组,所以我们必须在递增后递减它:我们必须在追溯后清理它。

此算法有效,并且会找到所有18位数字的数字的答案,其数字总和为60秒。

但它没有扩展,因为它的运行时间呈指数级增长。还因为你可以计算18的阶乘! 64位整数,但20后!你需要大整数算术。 (然而,通过简化分数N! / prod(dig[i]!),巧妙的实现将能够进一步发展。)

现在考虑您发布的代码。我删除了计算范围的所有内容。简陋的版本是:

ds_count(i, sum)
{
    if (sum > 60) return 0;

    if (i == 18) {
        if (sum == 60) return 1;
        return 0;
    }

    result = 0;
    for (d = 0; d < 10; d++) {
        result += ds_count(i + 1, sum + d);
    }

    return result;
}

这列举了所有18位数值。当总和超过60时,它会停止,但这就是它。这并不比蛮力解决方案好。

但是这个解决方案有助于记忆。它经常会被调用相同的值,并且很容易理解为什么。例如,将从ds_count(2, 5)05...14...23...32...41...调用来电50...。 (这让我想起了Settlers of Catan的棋盘游戏中不同大小的数字筹码,它占了两个骰子的总和。)

如果您可以确定其中一个值,则可以保存它并有效地将5个调用切换为16个数字的尾部。所以:

ds_count(i, sum)
{
    if (sum > 60) return 0;

    if (i == 18) {
        if (sum == 60) return 1;
        return 0;
    }

    if (defined(memo[i, sum])) return memo[i, sum];

    result = 0;
    for (d = 0; d < 10; d++) {
        result += ds_count(i + 1, sum + d);
    }

    memo[i, sum] = result;
    return result;
}

这非常快,并且没有像阶乘解决方案那样的硬性限制。它也更容易被推动,因为它是一种递归的枚举。

值得注意的是,我的解决方案不适合记忆。 (除了记住阶乘,但这不是限制因素。)之字形计数集生成的要点是只进行唯一的递归调用。还有一个状态,即数字集,这使得记忆更难。