配置器中的组合数

时间:2009-07-20 16:30:28

标签: algorithm math combinatorics combinations

我被要求编写一个例程来决定产品配置器中可能的组合数量。

配置器非常简单。尽管它具有比这更多的功能,但它可以被建模为几个“无线电组”(如UI控件),其中必须选择n个选项之一。

唯一可以使用的约束是规则,如果选择了一个选项,则无法选择其他选项。

所以我想要做的是在给定一组选项组和约束的情况下计算可配置的不同产品的数量。

我使用Inclusion-exclusion principle做了一个天真的方法来解决这个问题。但是据我所知,基于此方法的任何算法都应该在O(2 ^ n)中运行,这将无法工作。当然有几种可能的优化可以提供适当的运行时间,但仍然可以很容易地构建最坏的情况。

这就是我现在所处的位置。有什么建议吗?

更新

我意识到我没有解释规则如何应用得足够好。

有几个组有选项。必须在每个组中选择一个且仅一个选项。组中可以有一个或多个选项。

只有一种约束。如果选择某个组中的选项A,则无法选择其他组中的选项B.可以有任意数量的约束,没有限制有多少约束/规则适用于选项组或选项本身。

所以一个例子是:

第1组:
x1 x2 x3 x4 x5

第2组:
y1 y2 y3

第3组:
z1 z2 z3 z4

约束:
x1< - > y2 *
x1< - > Z4
y2< - > Z2

*如果在group1中选择了选项x1,则无法选择组2中的选项y2。

使用包含 - 排除我会计算组合数

组合= C 无规则 - C r [1] - C r [2] - C r < / sub> [3] + C r [1,2] + C r [1,3] + C r [2,3 ] - C r [1,2,3]

哪里

C 无规则 = 5 * 3 * 4

C r [a,b,c] =违反规则a,b和c的组合数。

遗憾的是,该方法需要2 ^ |规则|计算

11 个答案:

答案 0 :(得分:3)

答案 1 :(得分:2)

如果您有N个选项组,每个组都有Xi个选项(0<=i<N),那么

X0*X1*...*X(N-1)

为您提供所需的答案。换句话说,乘以每组选项的大小。

答案 2 :(得分:1)

如果每个参数和n约束都有Cim个可能值的参数,则配置数量的上限如下(忽略约束)。

N0 = C1 * C2 * ... * Cn

ci == x => cj != y形式的单个约束不允许以下数量的配置。

        N
Dk = -------
     Ci * Cj

因此,通过从忽略约束的上限减去不允许的配置来获得配置数。

N = prod(Ci, i = 1...n) * (1 - sum(1 / (Cxj * Cyj), j = 1...m))

此处xjyj是来自j - 约束的两个参数索引。

示例

Parameters    n = 4

Parameter 1   C1 = 4   0 1 2 3 
Parameter 2   C2 = 3   4 5 6 
Parameter 3   C3 = 2   X Y
Parameter 4   C4 = 3   7 8 9

Constraints   m = 2

Constraint 1  c2 == 4 => c3 != X
Constraint 2  c3 == X => c4 != 9

N0 = 4 * 3 * 2 * 3 = 72

D1 = 72 / (3 * 2) = 12
D2 = 72 / (2 * 3) = 12

N = 72 - (12 + 12) = 48

<强>更新

我认为这还不完全正确,因为它没有考虑约束的依赖性。

答案 3 :(得分:1)

一般情况下没有捷径。这并不像你想象的那么糟糕。请参阅下面的“重新思考”。

2 ^ n真的那么糟糕吗?我们在这里讨论了多少排除规则?对于每个配置器,您实际上只需执行一次,除非规则/选项集在运行中不断变化并且需要动态重新计算。如果确实有大量的规则,那么我就不会寻求一个确切的解决方案 - 只考虑k阶交叉点并说“组合数量至少是/最多......”。可能还有其他筛选方法可以让您快速得出答案的界限。

另请注意:如果您只考虑实际需要的排除项,那么2 ^ n仅仅是一个上限,而您实际的计算次数可能会明显少于任何实际情况。也就是说,如果C [1,2]为零,那么C [1,2,...]也是如此。考虑一下:对于每个约束,如果它们共享任何共同的选项,则将“clump”约束集合在一起。很明显,你的实际运行时间将由最大的“丛”的大小来定义(是的,可能与n一样大)。


重新思考:在大多数案例中,C [x,y]将为零。约束只能与涉及不同组的其他约束重叠。换句话说,(x1-y1)只能与(x1-z1)或(y1-z2)或其他东西重叠,而不是(x1-y2)。类似地,一组约束只能与新组重叠:(x1&lt; - &gt; y1)的组合与(y1&lt; - &gt; z2)没有相互作用(x3&lt; - &gt; ; z2)(x组已经固定在x1)。您只需考虑包含/排除,您添加到组合中的每个规则都会将以前未触及的组添加到组合中。所以你实际上是O(2 G ),其中G是组的数量(也可能是基于组大小的不同界限)。更容易管理!

答案 4 :(得分:1)

修改

此算法不正确。我在另一篇文章中提出了另一个答案,在更糟糕的情况下仍然是2 ^ N,但否则可能会给出更好的结果。

这个在选择的例子中起作用,因为y2是x1的排除集的一部分,并且两个第一个约束基于x1。不过,我现在看到需要做些什么。它仍然接近2 ^ N,但有一些优化可以带来显着的收益。

要修复此算法,组合规则的格式必须为set(ox)&lt; - &gt;集(OY)。要编写它们,对于你编写的左手oX的每个约束,你也可以用你已经编写的每个规则制作它的其他组合,如果oX不是组合规则右边的一部分,也不是group与组的左侧相同

对于完全独立的约束,这是2 ^ N.否则,您通过执行以下操作来减少N:

  • 用一个共同的左手统一约束
  • 不计算相互排斥的规则组合,这可分为:
    • 不合并同一组中的选项规则
    • 没有组合规则,其中一方的左侧出现在另一方的右侧

我不认为修复此算法是值得的。它的内存很重,而且它与我的备用答案具有相同的顺序,它更轻松。

结束修改

让我们转过来。算法怎么样:

  1. 根据需要修复规则,以确保适用于规则o1 <-> o2group(o1) < group(o2)
  2. 通过将所有规则oX <-> o?折叠为单个规则oX <-> Set(o?)
  3. 来计算“组合”规则
  4. 通过从中删除每个规则的左侧选项
  5. 来计算“干净”的一组组
  6. 通过将左侧选项的组替换为左侧选项本身,并从其他组中减去规则右侧的选项,计算干净集中的备用集,每个组合规则一个。
  7. 对于每组,请通过乘以集合中每个组中的选项数来计算组合数。
  8. 添加步骤5中的所有结果。
  9. 让我们看看这个工作:

    Group 1:
    x1 x2 x3 x4 x5
    
    Group 2:
    y1 y2 y3
    
    Group 3:
    z1 z2 z3 z4
    
    Constraints (already fixed):
    x1 <-> y2 *
    x1 <-> z4
    y2 <-> z2
    
    Composed rules:
    x1 <-> (y2, z4)
    y2 <-> (z2)
    
    Clean set of groups:
    x2x3x4x5, y1y3, z1z2z3z4
    
    Alternate sets:
    x1, y1y3, z1z2z3 (first composed rule)
    x2x3x4x5, y2, z1z3z4 (second composed rule)
    
    Totals:
    4 * 2 * 4 = 32
    1 * 2 * 3 = 6
    4 * 1 * 3 = 12
    
    Total: 50
    

    现在,这个算法可能不正确。现在,我无法清楚地认为它是正确的还是其他的 - 我已经太长时间接近这个问题了。但是,让我们检查一下这个例子:

    c(no rules) = 60
    c1 => 4
    c2 => 3
    c3 => 5
    c12 => 1
    c13 => 1
    c23 => 0
    c123 => 0
    
    c(no rules) - c1 - c2 - c3 + c12 + c13 + c23 - c123 = 50
    

    如果我的算法是正确的,它似乎是多项式的。同样,现在我想不够清楚,我需要考虑集合中操作的大O.

    以下是Scala的实现:

    case class GroupOption(id: Int, option: Int)
    case class Group(id: Int, options: Set[Int])
    case class Rule(op1: GroupOption, op2: GroupOption)
    case class ComposedRule(op: GroupOption, set: Set[GroupOption])
    
    object ComputeCombinations {
      def fixRules(rules: Set[Rule]) = {
        rules map (rule => if (rule.op1.id > rule.op2.id) Rule(rule.op2, rule.op1) else rule)
      }
    
      def ruledOptions(id: Int, rules: Set[Rule]): Set[Int] = (
        rules 
        filter (rule => rule.op1.id == id)
        map (rule => rule.op1.option)
      )
    
      def cleanseSet(groups: Set[Group], rules: Set[Rule]) = {
        groups map (group => 
          Group(group.id, group.options -- ruledOptions(group.id, rules)))
      }
    
      def composeRules(rules: Set[Rule]): Set[ComposedRule] = Set(
        (
          rules.toList
          sort (_.op1.id < _.op1.id)
          foldLeft (List[ComposedRule]())
          ) { (list, rule) => list match {
            case ComposedRule(option, set) :: tail if option == rule.op1 =>
              ComposedRule(option, set + rule.op2) :: tail
            case _ => ComposedRule(rule.op1, Set(rule.op2)) :: list
          }} : _*
      )
    
      def subset(groups: Set[Group], composedRule: ComposedRule) = (
        groups
        filter (_.id != composedRule.op.id)
        map (group => Group(group.id, group.options -- 
                                      (composedRule.set 
                                       filter (_.id == group.id)
                                       map (_.option)
                                      )))
      )
    
      def subsets(groups: Set[Group], composedRules: Set[ComposedRule]) = (
        composedRules map (composedRule => subset(groups, composedRule))
      )
    
      def combinations(groups: Set[Group]) = (
        groups.toList map (_.options.size) reduceLeft (_*_)
      )
    
      def allCombinations(groups: Set[Group], rules: Set[Rule]) = {
        val fixedRules = fixRules(rules)
        val composedRules = composeRules(fixedRules)
        val cleanSet = cleanseSet(groups, fixedRules)
        val otherSets = subsets(cleanSet, composedRules)
        val allSets = otherSets + cleanSet
        val totalCombinations = allSets.toList map (set => combinations(set)) reduceLeft (_+_)
        totalCombinations
      }
    }
    
    object TestCombinations {
      val groups = Set(Group(1, Set(1, 2, 3, 4, 5)),
                       Group(2, Set(1, 2, 3)),
                       Group(3, Set(1, 2, 3, 4)))
      val rules = Set(Rule(GroupOption(1, 1), GroupOption(2, 2)),
                      Rule(GroupOption(1, 1), GroupOption(3, 4)),
                      Rule(GroupOption(2, 2), GroupOption(3, 2)))
      def test = ComputeCombinations.allCombinations(groups, rules)
    }
    

答案 5 :(得分:1)

这可能不是一个直接有用的答案,所以请随意忽略它......但是;我自己目前没有使用类似的系统;并且坦率地说除了琐碎的例子我不确定尝试计算有效组合的数量是有用的。作为一个例子,我正在研究的模型有(例如)18000个候选项目,分布在80多个选项上,一些是单选/一些多选。除了最小的模型之外,在知道数字时没有好处,因为你根本不会把它写成完整的真值表;您非常强制运行规则(即删除任何不再有效的内容,根据需要自动选择任何内容,并根据需要检查没有规则被破坏)。这不一定是个问题;我当前的代码在~450ms内处理这个模型(作为一个web服务),其中大部分实际上是处理输入/输出xml所花费的时间。如果输入/输出不是xml,我认为它将是~150ms,这很酷。

我想说服我的雇主开源发动机;不过,这是另一天的战斗。

答案 6 :(得分:0)

它不会只是x ^ n,其中n是选项的数量,x是每个选项的选择数量?

答案 7 :(得分:0)

我认为Zac正在思考正确的方向。查看表达式中的组合数,可以看出二阶项Cr [i,j]远小于C [k]项。 想象一个立方体,其中每个轴都是一组选项。多维数据集中的每个点代表一个特定的选项组合。一阶C [k]校正排除了立方体的两个表面之间的一系列选项。二阶校正C [i,j]仅在两个这样的线在立方体中的空间中的一个点(选项组合)相遇时发生。因此,无论您拥有多少组,更高阶的修正总是越来越小。如果坚持

组合= C(无规则) - Cr [1] - Cr [2] - Cr [3]

你最终会得到组合数量的下限。现在您已经了解了第一阶修正的大小,并考虑了上面的立方体的观察,您甚至可以估计二阶修正的数量级。这取决于团体的数量。然后,您的算法可以决定是继续使用更高的订单还是停止。

答案 8 :(得分:0)

Daniel's post的评论:

你的算法看起来不错,但我无法说服自己它真的有用,所以我安装了scala并做了一些测试。不幸的是,我没有得到正确的结果。

例如考虑这种情况:

Group 1:
a1 a2 a3 a4 a5

Group 2:
b1 b2 b3

Group 3:
c1 c2 c3 c4

Group 4:
d1 d2 d3 d4 d5

Rules:
a1 <-> b2
a1 <-> c2
b2 <-> c2
b2 <-> d1
a2 <-> d2

我使用此配置配置了我的基本筛选算法并获得了以下结果( 227 组合):

Without rules => 300
Rules: [1] => 20
Rules: [2] => 15
Rules: [3] => 25
Rules: [4] => 20
Rules: [5] => 12
Order: 1 => 208 (diff: -92)
Rules: [1, 2] => 5
Rules: [1, 3] => 5
Rules: [2, 3] => 5
Rules: [1, 4] => 4
Rules: [2, 4] => 1
Rules: [3, 4] => 5
Rules: [1, 5] => 0
Rules: [2, 5] => 0
Rules: [3, 5] => 1
Rules: [4, 5] => 0
Order: 2 => 234 (diff: 26)
Rules: [1, 2, 3] => 5
Rules: [1, 2, 4] => 1
Rules: [1, 3, 4] => 1
Rules: [2, 3, 4] => 1
Rules: [1, 2, 5] => 0
Rules: [1, 3, 5] => 0
Rules: [2, 3, 5] => 0
Rules: [1, 4, 5] => 0
Rules: [2, 4, 5] => 0
Rules: [3, 4, 5] => 0
Order: 3 => 226 (diff: -8)
Rules: [1, 2, 3, 4] => 1
Rules: [1, 2, 3, 5] => 0
Rules: [1, 2, 4, 5] => 0
Rules: [1, 3, 4, 5] => 0
Rules: [2, 3, 4, 5] => 0
Order: 4 => 227 (diff: 1)
Rules: [1, 2, 3, 4, 5] => 0
Order: 5 => 227 (diff: 0)

***Combinations: 227***

但是在scala中使用此代码:

  val groups = Set(Group(1, Set(1, 2, 3, 4, 5)),
                   Group(2, Set(1, 2, 3)),
                   Group(3, Set(1, 2, 3, 4)),
                   Group(4, Set(1, 2, 3, 4, 5)))

  val rules = Set(Rule(GroupOption(1, 1), GroupOption(2, 2)),
                  Rule(GroupOption(1, 1), GroupOption(3, 2)),
                  Rule(GroupOption(2, 2), GroupOption(3, 2)),
                  Rule(GroupOption(2, 2), GroupOption(4, 1)),
                  Rule(GroupOption(1, 2), GroupOption(4, 2)))

我得到了答案 258

我已经用筛子方法检查了计算结果,看起来是正确的。也许有可能修复你的算法?我无法真正理解错误。

答案 9 :(得分:0)

你的问题很不可行。

  • 计算解决方案的数量是#P-complete,即使您将每组放射线框限制为两个选项
  • 检查是否存在与约束一致的任何选项选项是NP-complete
  • 如果您将每组放射线框限制为两个选项(2SAT),则检查是否有任何选择与约束一致的选项可以非常快速地完成

所以一般不要指望多项式算法;这种算法的存在意味着P = NP。

你能做什么:

  • 使用近似算法。根据我链接的维基百科文章,它们通常对他们来说是可以接受的。
  • 使用SAT求解器http://en.wikipedia.org/wiki/SAT_solver或相关工具进行计数(遗憾的是我不知道);人们创造了许多启发式方法,而且程序通常比自制解决方案快得多。甚至还有SAT比赛,所以这个领域目前正在迅速扩大。
  • 检查您是否需要这样的一般性。也许你的问题有一个额外的假设,这将改变其复杂性。

证明:

  1. 计算解决方案的数量很容易显示为#P,因此将2SAT减少到此就足够了。拿一些2SAT实例,比如
  2. (p1或不是p2)和(p2或不是p3)

    允许用户选择p1,p2,p3的值。您可以轻松地形成约束,从而强制解决此问题。因此,可能的配置数量= p1,p2,p3的可能赋值数量,使布尔公式为真。

    1. 您可以轻松检查是否允许选择某些选项,因此这是NP,因此足以将3SAT减少到此。拿一些3SAT实例,比如
    2. (p1或p2或不是p3)和(p2或不是p1或p4)

      提供选项:

      组p1 p1true p1false

      组p2 p2false p2true

      组p3 p3false p3true

      组p4 p4false p4true

      group clause_1 c1a c1b c1c

      group clause_2 c2a c2b c2c

      clause_1将控制第一个子句:(p1或p2或不是p3)。确切地说,如果选择了p1true,c1a将是可检查的,如果选择了p2true,则c1b将是可检查的,如果选择了p3false,则c1c将是可检查的。所以限制是:

      p1false&lt; - &gt; C1A

      p2false&lt; - &gt; C1B

      p3true&lt; - &gt; C1C

      与第2条相同,constaints

      p2false&lt; - &gt; C2A

      p1true&lt; - &gt; C2B

      p4false&lt; - &gt; C2C

      如果用户能够选择所有答案(因此配置数量> 0),他将证明存在一些变量p1,...,p4的估值,使3SAT实例成立。相反,如果用户将无法选择与假设一致的答案(可用配置的数量= 0),则3SAT实例将不会令人满意。所以知道答案是否是&gt; 0表示知道3SAT实例是否可解。

      当然,这种减少是多项式时间。结束证明。

      如果你对答案可能为0这一事实不满意:如果忽视这些配置程序,它仍然是NP难的。 (如果没有选择“假”,你会给所有组添加一些“虚假”选项并允许指数级选择。这个解释起来比较复杂,所以我会根据要求进行。)

答案 10 :(得分:0)

上面sdcvvc的优秀答案中简要提到了这一点,但是蒙特卡罗的近似值是否足够好?生成N个随机配置(选择N使得实验的功能足够高:我不知道在这里帮助你),然后测试它们中有多少与你的约束兼容。将该比例外推到配置空间的其余部分。