【问题标题】:count integers between large A and B having count of digit S计算大 A 和 B 之间具有数字 S 的整数
【发布时间】:2014-10-24 02:24:06
【问题描述】:

我正在尝试计算范围 A 到 B 之间的整数和 S(假设 S=60)。

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

让 X 是一个数字,直到 Y 我们必须计算整数。

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

leftmost_lo 作为 xi yi,否则为 n + 1。

函数计数返回具有属性 X ≤ Y 且 X 的数字总和为 60 的整数 X 的数量 f(Y)。

根据上述定义,设 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) 解决方案。

谁能帮帮我。

【问题讨论】:

  • 这些类型的计数问题通常通过应用数学和组合公式来解决,而不是实际评估每个可能的候选者。
  • 为什么用C、C++标记?代码不是 C。
  • @user1990169,是的,你可以通过计算数学公式来解决这个问题,但我正在考虑解决这个问题的一般方法。
  • 仅供参考 - 我为任意 A 和 B 添加了一个大纲想法,它可以适用于我的或 M Oehm 的方法。

标签: algorithm dynamic-programming


【解决方案1】:

首先,你可以注意到,如果你有一个函数 F,它返回整数

然后定义一些符号:

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

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

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))。

【讨论】:

    【解决方案2】:

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

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

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

    假设 B 有数字 b1 b2 ... bn - 1 0 亿。我们递减 b1 并枚举数字 S - (b1 - 1) 的分区(少于 n 个部分,每个部分低于 10),以及与 (n - 1 - number_of_parts) 个零组合时它们不同排列的基数。我们重复这个过程,直到 b1 = 0(这里的最大部分数和前导零将减一)。然后我们对 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
    

    【讨论】:

    • 感谢您的回复,但我没有正确理解您的方法,_ps , numPS , ps 在做什么,没明白。请您再解释一下。
    • @Assians _ps 是由ps 发起的递归函数——他们一起为s 生成分区,谁的parts 数量受maxParts 约束,然后返回累积的数量这些分区的不同排列,由函数numPS 计算得出。例如,如果 s 为 3 且 n 1000,则分区将为 [1,1,1](1 个排列)、[1,2,0](6 个排列)、[3,0,0](3 个排列)。总数为 10,就像我的答案中的示例一样。有意义吗?
    【解决方案3】:

    编辑哦,天哪!就在我承认我的回答没有抓住重点之后,它就被接受了。我保留了原来的答案,并解释了我的算法背后的想法以及折叠后它如何不如原来的解决方案。


    这个特殊问题应该被视为“所有 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 位数字,其中数字较少数字被认为有前导零。

    我的算法的想法不是枚举所有数字,而是考虑数字的计数。例如,如果我的数字是数字 9 的 5 倍,数字 5 的 3 倍,则数字总和为 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。它可以通过将当前数字的计数增加一个来垂直移动,也可以通过考虑下一个数字来水平移动。如果数字总和达到或超过 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;
    }
    

    这非常快,并且没有像阶乘解决方案那样的硬限制。它也更容易实现,因为它本质上是一个递归枚举。

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

    【讨论】:

    • 这实际上比 OP 已经拥有的解决方案效率低得多......据我所知,这在 n 中具有运行时指数,而不是多项式
    • 顺便说一句,对于给定的 X,问题要求具有该属性的数字
    • @NiklasB.:触摸。我没有看到该解决方案使用了记忆,这仅在您对 OP 链接到的您自己的问题的(长)回答中提到。至于您的第二条评论:这个特定问题要求范围为 1 到 10^18。我认为将您更通用的解决方案应用于这种特殊情况会很浪费。 (不过,我已经在帖子中承认了我的算法的这种限制。)幸好我不是靠数字计数行业谋生的。
    • @NiklasB.:我也没有回答这个问题,不是为了找到A=1,B=10^18,S=60的解决方案,而是改进算法。 (因为我一开始并不了解该算法。有趣的是,当它适合于 meoisation 时,如何优化核心访问范围内所有数字的蛮力解决方案。所以至少 从这个问题中学到了一些东西,如果其他人似乎一直都知道的话。)
    • 我稍后会删除这个答案。 (在不解决 cmets 提出的问题的情况下立即删除,感觉有点像偷偷溜出后门。)
    猜你喜欢
    • 2014-04-19
    • 1970-01-01
    • 2022-02-10
    • 1970-01-01
    • 2018-01-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多