【问题标题】:How would you write this algorithm for large combinations in the most compact way?您将如何以最紧凑的方式为大型组合编写此算法?
【发布时间】:2009-06-05 15:33:04
【问题描述】:

Nitems 中可以检索到的kitems 的组合数由以下公式描述。

             N! 
c =  ___________________ 
       (k! * (N - k)!)

一个例子是在抽奖中可以从48 Balls 的鼓中抽取多少个6 Balls 组合。

优化这个公式以最小的 O 时间复杂度运行

这个问题的灵感来自于新的 WolframAlpha 数学引擎,它可以非常快速地计算出极大的组合。例如以及随后在另一个论坛上对该主题的讨论。

http://www97.wolframalpha.com/input/?i=20000000+Choose+15000000

在有人尝试解决方案后,我将发布该讨论中的一些信息/链接。

任何语言都可以接受。

【问题讨论】:

  • 这是一个关于字符数还是 O 时间复杂度的代码高尔夫挑战?
  • 我同意这不是真正的代码高尔夫。但这很有趣。

标签: language-agnostic performance combinations binomial-coefficients


【解决方案1】:

Python: O(min[k,n-k] 2)

def choose(n,k):
    k = min(k,n-k)
    p = q = 1
    for i in xrange(k):
        p *= n - i
        q *= 1 + i
    return p/q

分析:

  • pq 的大小将在循环内线性增加,如果 n-i1+i 可以被认为具有恒定大小。
  • 每次乘法的成本也会线性增加。
  • 所有迭代的总和成为k 上的算术级数。

我的结论:O(k2)

如果重写为使用浮点数,乘法将是原子操作,但我们会损失很多精度。它甚至会溢出choose(20000000, 15000000)。 (不足为奇,因为结果大约为 0.2119620413×104884378。)

def choose(n,k):
    k = min(k,n-k)
    result = 1.0
    for i in xrange(k):
        result *= 1.0 * (n - i) / (1 + i)
    return result

【讨论】:

  • 非常pythonic,我正要发布或多或少相同的东西:P
  • 这将在 20000000 时窒息而死 选择 15000000。此外,如果您沉迷于可以在恒定时间内执行大整数运算的虚构,那么它只有 O(min[k,nk]) .
  • 我之前的评论在重新阅读时听起来不屑一顾。如有冒犯,请见谅。但是,是的,20000000 选择 15000000 的结果几乎有 500 万位。后期的被乘数甚至不适合大多数处理器的二级缓存,所以事情肯定会很慢。 python 对非常大的浮点数有什么设施?有人知道吗?
  • 如果乘法的两个操作数都大于 1050 位(70 * 15 位/位),则 python 将使用 Karatsuba 的乘法算法。与“小学”算法 O(n^2) 相比,它的时间复杂度仅为 O(n^1.584)。在其他操作上,也有类似的优化。
【解决方案2】:

注意 WolframAlpha 返回一个“十进制近似值”。如果您不需要绝对精度,您可以通过使用Stirling's Approximation 计算阶乘来做同样的事情。

现在,Stirling 的近似需要计算 (n/e)^n,其中 e 是自然对数的底,这将是迄今为止最慢的运算。但这可以使用another stackoverflow post 中概述的技术来完成。

如果你使用双精度和重复平方来完成求幂,操作将是:

  • 对斯特林近似的 3 次评估,每次都需要 O(log n) 次乘法和一次平方根评估。
  • 2 次乘法
  • 1 个部门

通过一些巧妙的方法可能会减少操作的数量,但使用这种方法的总时间复杂度将是 O(log n)。很容易管理。

编辑:考虑到这种计算的普遍性,也必然会有很多关于这个主题的学术文献。一个好的大学图书馆可以帮助你找到它。

EDIT2:另外,正如另一个响应中所指出的,这些值很容易溢出双精度值,因此即使是中等大的 k 和 n 值,也需要使用精度非常高的浮点类型。

【讨论】:

    【解决方案3】:

    我会在Mathematica解决它:

    Binomial[n, k]
    

    伙计,这很容易......

    【讨论】:

    • Mathematica 如何计算阶乘?
    • @Matt Kane:可能与 WolframAlpha 的做法完全相同。它也是 Wolfram 产品。
    • @Rich B:我认为 WinRAR 没有这种功能。
    • 那么,时间复杂度是多少?这就是问题所要寻找的。​​span>
    • 你不够认真。这就是互联网。网上没有开玩笑的。
    【解决方案4】:

    Python: O(1) 中的近似?

    使用python十进制实现来计算近似值。由于它不使用任何外部循环,并且数量有限,我认为它会在O(1)中执行。

    from decimal import Decimal
    
    ln = lambda z: z.ln()
    exp = lambda z: z.exp()
    sinh = lambda z: (exp(z) - exp(-z))/2
    sqrt = lambda z: z.sqrt()
    
    pi = Decimal('3.1415926535897932384626433832795')
    e = Decimal('2.7182818284590452353602874713527')
    
    # Stirling's approximation of the gamma-funciton.
    # Simplification by Robert H. Windschitl.
    # Source: http://en.wikipedia.org/wiki/Stirling%27s_approximation
    gamma = lambda z: sqrt(2*pi/z) * (z/e*sqrt(z*sinh(1/z)+1/(810*z**6)))**z
    
    def choose(n, k):
      n = Decimal(str(n))
      k = Decimal(str(k))
      return gamma(n+1)/gamma(k+1)/gamma(n-k+1)
    

    例子:

    >>> choose(20000000,15000000)
    Decimal('2.087655025913799812289651991E+4884377')
    >>> choose(130202807,65101404)
    Decimal('1.867575060806365854276707374E+39194946')
    

    再高一点,就会溢出。指数似乎限制在 40000000。

    【讨论】:

      【解决方案5】:

      给定合理数量的 n 和 K 值,提前计算它们并使用查找表。

      它以某种方式回避问题(您正在卸载计算),但如果您必须确定大量值,它是一种有用的技术。

      【讨论】:

      • 我假设您的意思是在查找中计算阶乘并将第 n 个条目除以第 k 个和第 (n-k) 个条目。否则,您将使用二维查找表(并且仍需要指定算法以避免一维方法中所需的可怕划分)。
      • 除了对上述部分/全部使用查找表之外,我根本没有考虑太多。显然,您在内存和初始计算方面会受到惩罚(您需要 all 排列吗?),因此我在某种程度上通过说“合理”数量的值来对冲自己,并且故意不定义那个!
      • 我认为如果您需要对大量数据进行记忆阶乘会更好。
      【解决方案6】:

      MATLAB:

      • 作弊者的方式(使用内置函数NCHOOSEK):13个字符,O(?)

        nchoosek(N,k)
        
      • 我的解决方案:36 个字符,O(min(k,N-k))

        a=min(k,N-k);
        prod(N-a+1:N)/prod(1:a)
        

      【讨论】:

        【解决方案7】:

        我知道这是一个非常老的问题,但我一直在努力解决这个问题,直到我找到了一个用 VB 6 编写的非常简单的问题,并将其移植到 C# 后,结果如下:

        public int NChooseK(int n, int k)
        {
            var result = 1;
            for (var i = 1; i <= k; i++)
            {
                result *= n - (k - i);
                result /= i;
            }
        
            return result;
        }
        

        最终的代码非常简单,除非你运行它,否则你不会相信它会工作。

        另外,original article 对他如何达到最终算法给出了一些很好的解释。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-09-30
          • 2013-01-19
          • 2016-06-29
          • 2022-12-01
          • 2021-07-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多