【问题标题】:Most efficient algorithm for determining if X out of N inputs are true确定 N 个输入中的 X 个是否为真的最有效算法
【发布时间】:2011-10-05 10:30:06
【问题描述】:

这个问题的灵感来自我昨天正在研究的一个答案。

假设我们有 N 个输入,其评估结果为真或假, 确定这些输入中的 X 是否是最有效的方法是什么 真的?

注意事项:

  1. 输入不在数组中,因此如果将它们转换为数组,请考虑任何间接费用。
  2. “最有效”是指最佳平均情况(尽管我也希望看到最佳和最差情况的统计数据)。

这是我昨天遇到的两种方法。

1) 将变量视为电路的布尔输入,并使用 K-map 减少它们

起初我认为这将是最有效的方法,因为它遵循电路逻辑,但我肯定有其他想法。随着输入数量的增加,比较的数量呈指数增长

2 inputs:
   1 of 2: if(1 OR 2)
   2 of 2: if(1 AND 2)

3 inputs:
   1 of 3: if(1 OR 2 OR 3)
   2 of 3: if((1 AND 2) OR (1 AND 3) OR (2 AND 3))
   3 of 3: if(1 AND 2 AND 3)

4 inputs:
   1 of 4: if(1 OR 2 OR 3 OR 4)
   2 of 4: if((1 AND 2) OR (1 AND 3) OR (1 AND 4) OR (2 AND 3) OR (2 AND 4) OR (3 AND 4))
   3 of 4: if((1 AND 2 AND 3) OR (1 AND 2 AND 4) OR (1 AND 3 AND 4) OR (2 AND 3 AND 4))
   4 of 4: if(1 AND 2 AND 3 AND 4)

... etc. ...

最好的情况很好(O(1)),但最坏的情况远比......

2) 计数器和顺序 if 语句

这总是在O(n) 时间执行,这没关系,但我希望有更好的最佳情况。

counter = 0

if(input 1)
   counter++

if(input 2)
   counter++

if(input 3)
   counter++

... etc. ...

if(counter >= X)
   // true

还有什么比这两种方法更有效的解决方案?

【问题讨论】:

  • 如果有人感兴趣的话,启发这个问题的问题:stackoverflow.com/q/6687557/385913
  • “输入不在数组中,因此如果将它们转换为数组,请考虑任何间接成本。” 那么......它们是什么形式的?当然,它们并不都是命名变量。如果它们是只能一个一个访问的I/O端口,那可能比其他任何东西都更昂贵,并且计数是唯一的方法。如果它们被打包在一个变量中,就会出现一个选项世界。
  • 在启发我发帖的问题中,它们都被命名为变量,但我希望看到使用包含位的变量的答案。
  • [计算 32 位整数中设置位数的最佳算法?][1]。 [1]:stackoverflow.com/questions/109023/…
  • 问题是最好的情况omega(n)。证明:每个解决方案都是omega(X),因为X 是O(n),所以没有解决方案比最佳情况omega(n) 更快。因此平均运行时间为omega(n)。我们得出结论,简单的计数器解决方案是最优的。

标签: algorithm language-agnostic performance


【解决方案1】:

关于问题的复杂性
由于要求准确计数(而不是询问是否至少 x 输入打开),问题很明显O(n):

  • 需要访问每个输入,并且,
  • 每个输入的功与 n 无关(虽然给定输入的功可能会因输入的特定值而异,但如果输入的数量为已更改。)

我们当然可以实现次优算法,例如,在处理每个输入时会[不必要地]访问所有其他输入,使其成为 O(n^2) 实现,但这当然很愚蠢。

据此断言,因此问题可能是关于...
可以加快实施速度的技巧
应该注意的是,虽然可能存在这样的技巧,但算法/问题的复杂性仍然顽固地保持在 O(n)。

技巧 1:更好地存储输入
不幸的是,该问题表明输入来自命名变量,并且在评估算法的整体性能时必须考虑任何输入转换的成本[为了允许更快的计数]。尽管这最终取决于底层语言、运行时等,但考虑到转换成本的需要很可能会导致任何基于备用存储的算法都比保持输入不变的解决方案慢。

技巧 2:评估短路
我们的想法是尽快(或不久之后)返回false

  • 打开的输入的运行计数大于 X 的数量(或者,如果我们计算关闭的输入的数量,当此计数超过 (n - X))
  • 剩下要测试的输入数量加上打开的输入的运行计数小于 X。(或在计算关闭输入的情况下类似)。

这个技巧相对简单,但是计算提前退出测试所需值的额外成本可能会抵消[静态]提前退出所带来的收益。

技巧 3:使用反向逻辑:计算关闭而不是打开的输入的数量。 (或两者都算)。
这种方法的成本/收益取决于要测试的输入的数量(问题的 X)和我们可能对输入的统计先验(是在给定时间相对均匀地输入的数量)分布式或我们往往只有少数输入打开(或关闭))。

solution proposed by Chris Acheson 为 Trick 2 和 Trick 3 的使用提供了一个基线。假设我们可以对输入状态的分布做出一些假设,那么这个基线的额外性能改进将被驱动为这样的“先验” “:在计算输入本身之前完成的一些快速启发式方法将确定我们应该计算哪个状态(打开或关闭或两者),我们应该测试哪个限制等,并分支到算法的相应“版本” .

如果我们知道给定输入打开或关闭的个别概率,那么额外的收益也是可能的,因为我们会首先测试最可能(或最不可能)的概率,以快速达到我们的“短路”价值”。

关于这些优化算法的最佳/最坏“复杂性”
假设

  • 在给定时间打开的输入数量是均匀分布的
  • 所有输入在给定时间都有 50% 的开启变化
  • X 在 1 和 n 之间随机选择

技巧 #2 和 #3 的组合平均可以是 O(X/2)(我需要做数学计算,但这似乎是正确的)。但是,我认为用number of operations 相对于 X 和/或 n 来谈论更明智,而不是滥用 O 表示法...
假设所有操作的成本大致相同

  • 初始化计数器或变量
  • 测试输入或变量
  • 加法或减法
  • 等

计算完成给定算法所需的操作总数更容易也更准确,因此可以使用这些计数来处理各种最佳/最差/平均情况,以帮助确定特定算法。
为了说明这一点,仅系统地计算所有输入并仅在最后比较计数器的幼稚实现将具有 O(n) 复杂性,并且在所有情况下都在大约 1 + 2*n + 1 次操作中完成。这样的算法可能被证明是整体的,比一个更高级的算法更好,比如在最好、平均和最坏的情况下分别是 O(X)、O((X+n)/2) 和 O(n) , 在这些相同的情况下,可以很好地使用 X*3、(X+n)* 1.5 和 n*3 操作。

【讨论】:

  • 鉴于原作者提出的第一个解决方案,可以公平地假设“N 个输入中的 X 个或多个为真”。
  • 为了检查 X 输入是否准确,需要检查 每个 输入,这并不完全正确。示例:如果 X == 1,并且前两个输入为真,我们可能会停止进一步检查。所以检查的平均次数可以低于N。对于最坏的情况,当然需要所有N次检查。
  • @Vlad:区分依赖于输入分布的平均复杂度和不依赖于输入分布的平均复杂度很重要。例如,对于任何输入,具有随机枢轴的 3 路快速排序是平均情况 O(n*log(n)),而如果我输入大量没有真实值的输入,则程序进行的平均检查次数将始终是。
【解决方案2】:

此版本对于接近零或 N 的 X 值非常有效:

true_counter = 0
false_counter = 0
max_false = N - X

if(input 1)
   true_counter++
   if(counter >= X)
      return true
else
   false_counter++
   if(false_counter > max_false)
      return false

// and so on

【讨论】:

  • 干得好,克里斯,这是一种非常通用的逻辑短路方法。注意:我认为问题是要查找是否正好有 X 输入,因此您的第一个短路测试应该是 if (counter > X) return false
  • 我选择了“X 或更多输入”,因为这就是 OP 的示例代码所做的。在“恰好 X 输入”的情况下,您的方式有效,并且只有在我们到达最后时才会返回 true。
【解决方案3】:

计算真值是最快的方法。您自己的计数器将允许超过 X 为真,但问题意味着您想要一个特定的值 - 没什么大不了的,但如果您想要至少 10 个(但更多是可以的),那么您可以在每次增量后检查计数器尽早反击并中止。

另一方面,如果将标志打包到一个单词中,则有更快的方法来计算 1。最后,计数 1 是要走的路。顺便说一句,C 中的 False 为零,True 为 1,因此您可以将它们加在一起以计算 True。

【讨论】:

  • 当我们检查的值是数字时,添加方法失败。 (例如,如果我们检查 n 个整数中有超过 x 个计算结果为 false,但也许其中一个等于 2,这会导致数学错误)
  • @Mike:您可以使用y != 0 将非零y 值折叠为1。
  • 将适当的值计算到min(X,n-X)会更快
【解决方案4】:

在布尔值只是一个值为 1 或 0 的 signed char 的编程语言中,我们可以简单地这样做:

if (input1 + input2 + input3 + input4 + ... + inputX > n)

【讨论】:

  • 当您处理数字输入和虚假值时,这会崩溃,否则我认为这将是我的最爱。
【解决方案5】:

一般情况下没有次线性算法:您必须查看每个输入。所以数数就好了。

平均案例运行时间取决于您的平均案例。克里斯的想法是在计数确定结果后立即停止计数,这在许多情况下会有所帮助。

除此之外,它实际上归结为适当的数据结构。您询问了位域:http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetNaive

【讨论】:

    【解决方案6】:

    mjv 给出的答案相当彻底地涵盖了顺序算法,但由于您提到了电路逻辑,我觉得并行算法和物理算法也需要轻轻地解决。

    如果您有 N 个输入到数字电路(或单元处理器),您可以通过递归添加对并传播结果,在 O(log n) 时间内计算它们:

    [1, 1, 0, 1, 1, 1, 0, 1] -> [(1+1), (0+1), (1+1), (0+1)] -> [(2+1), (2+1)] -> [(3+3)]

    这为我们提供了 N 个加法,但它们可以并行化为 log(2,N) 代(假设您有足够的处理器/加法器来同时运行 N/2 个操作......)

    此算法有多种变体以利用阈值问题要求,但它们大多不值得,除非阈值预计非常低(例如,14000 个输入中的 10 个)。

    【讨论】:

    • 我喜欢这种方法,但我会重申我之前提出的观点。当您处理数字数据和虚假值时,添加值会分崩离析。
    • 那么,作为第一轮加法的一部分,将真值转换为 1,将假值转换为 0。
    【解决方案7】:
    import Data.List (foldl')
    main :: IO ()
    xoutofn :: Num a => (a1 -> Bool) -> [a1] -> a
    xoutofn pred ns = foldl' test 0 (map pred ns) where 
                        test x (True)  = x+1
                        test x (False) = x
    
    main = print $ xoutofn predicate [1 .. 1000] where
        predicate x = x > 500
    

    可以像上面的人所做的那样在找到 x 后立即停止的地方进行短路,但我喜欢这个版本的简单性。

    运行时间为 O(n),因为将列表中的每个项目转换为布尔值是根据需要发生的(因为懒惰),因此它只需要遍历列表一次。

    $ time ./xoutofn
    500
    
    real    0m0.003s
    user    0m0.000s
    sys     0m0.000s
    

    【讨论】:

      猜你喜欢
      • 2010-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-04-30
      • 2019-09-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多