【问题标题】:Dependency Algorithm - find a minimum set of packages to install依赖算法 - 找到要安装的最小包集
【发布时间】:2015-05-25 00:30:03
【问题描述】:

我正在研究一种算法,其目标是找到安装包“X”的最小包集。

我会用一个例子更好地解释:

X depends on A and (E or C)
A depends on E and (H or Y)
E depends on B and (Z or Y)
C depends on (A or K)
H depends on nothing
Y depends on nothing
Z depends on nothing
K depends on nothing

解决办法是安装:A E B Y。

这是描述示例的图像:

有没有一种算法可以在不使用蛮力方法的情况下解决问题?

我已经阅读了很多有关 DFS、BFS、Dijkstra 等算法的知识...... 问题是这些算法无法处理“OR”条件。

更新

我不想使用外部库。

算法不必处理循环依赖。

更新

一种可能的解决方案是计算每个顶点的所有可能路径,并且对于可能路径中的每个顶点,执行相同的操作。 因此,X 的可能路径是 (A E),(A C)。现在,对于这两个可能路径中的每个元素,我们可以执行相同的操作:A = (E H),(E Y) / E = (B Z),(B Y) 等等...... 最后,我们可以将每个顶点的可能路径组合在一个 SET 中,并选择具有最小长度的路径。

你怎么看?

【问题讨论】:

  • 是否需要蓝色依赖项和一个红色依赖项?
  • 要获得一个包所需的所有少量依赖项,您需要所有 AND 依赖项和尽可能多的 or 依赖项。我再举一个例子。 A = B,C, D|F|G,H|L 因此,要获得 A 的最少依赖数,需要 B,C,[ D F G ] 和 [ H L ] 之间只有 1 个或之间的选择方式是得到最短路径。
  • 我假设这形成了一个 DAG?
  • 注意,你的颜色代码不够,它不会告诉你括号放在哪里。
  • @KLi,我建议将您的解决方案作为答案发布,而不是将其放在问题中。所以可以直接评论。我也认为,这张图片根本无助于理解你的例子;最好删除它。

标签: java python algorithm mathematical-optimization minimization


【解决方案1】:

不幸的是,考虑到问题实际上是NP-hard(但甚至不是NP-complete),几乎没有希望找到比蛮力更好的算法。

这个问题的 NP-hardness 证明是最小的vertex cover 问题(众所周知是 NP-hard 而不是 NP-complete)很容易简化为它:

给定一个图表。让我们为图的每个顶点 v 创建包 Pv。还创建包 X 什么“和”-需要(PuPv) 用于图的每条边 (u, v)。为了满足X,找到要安装的最小包集。那么v在图iff的最小顶点覆盖中对应的包Pv在安装集中。

【讨论】:

  • 即使在不允许循环依赖的情况下,即使在 OR 子句中允许的选择不超过两个时,这个证明仍然成立。
  • 说最小集(或顶点)覆盖是 NP-hard 但不是 NP-complete 是正确的,但可能会不必要地混淆人们。所有这样的离散优化问题(“找到最小值......”)都可以简单地转换为等价的决策问题(“是否存在大小 是 NP -完成。
  • @j_random_hacker,一个不是NP-complete的问题不能变成一个等价的NP-complete问题,因为如果某个问题存在等价的NP-complete问题,那么这个问题本身应该是NP-complete.... 并且可能,明确指出一个问题既是 NP-hard 又不是 NP-complete 是合理的,因为它表明问题的解决方案不仅难以找到,而且难以验证。
  • 通过“等效”,我的意思是一种算法可以多时间转换为另一种算法。例如,要将最小化问题转化为多项式多项式决策问题,只需解决 k=0, 1, 2, ... 的决策问题,直到返回 YES 答案。然后,为了恢复解决方案,从可能性集合中重复删除 1 个任意项(即 1 个顶点用于顶点覆盖)并解决 k'=k-1 的决策问题:如果答案为否,则将顶点包含在解决方案中.
  • @j_random_hacker,我知道任何离散优化问题都可以通过解决一系列决策问题来解决。这在问题分析中可能很有用,但不能使解决方案计算和验证更容易,即仍然不可能将任何非 NP 完全 NP-hard 问题更改为 NP 完全问题。所以,你的评论对我来说仍然没有任何意义......还有另一个原因表明这个问题既是 NP 难的,也不是 NP 完全的:如果突然 P,任何 NP 完全问题都可以在多项式时间内解决=NP,但一般来说,NP-hard 问题并非如此。
【解决方案2】:

“我觉得“或”有问题(图像没有为我加载)。 这是我的推理。假设我们采用像 Dijkstras 这样的标准最短路径算法,然后使用相等的权重来找到最佳路径。 以你为例 从以下 2 个选项中选择最佳 Xr

Xr= X+Ar+Er
Xr= X+Ar+Cr

其中 Ar = 是树 A=H(和后续孩子)或 A=Y(和后续孩子)中的最佳选择

这个想法是首先为每个 or 选项分配标准权重(因为 and 选项不是问题)。 稍后,对于每个 or 选项,我们使用它的子节点重复该过程,直到我们没有更多 or 选项。

然而,我们需要首先定义,什么是最佳选择,假设依赖数量最少,即最短路径是标准。 通过上述逻辑,我们为 X 分配 1 的权重。然后

X=1
X=A and E or C hence X=A1+E1 and X=A1+C1
A= H or Y, assuming H and Y are  leaf node hence A get final weight as 1
hence , X=1+E1 and X=1+C1

Now for E and C
E1=B1+Z1 and B1+Y1 . C1=A1 and C=K1.
Assuming B1,Z1,Y1,A1and K1 are leaf node 

E1=1+1 and 1+1 . C1=1 and C1=1
ie E=2 and C=1

Hence
X=1+2 and X=1+1 hence please choose X=>C as the best route

希望这可以清除它。 我们还需要处理循环依赖 X=>Y=>Z=>X ,在这里我们可以将这些节点在父节点或叶节点级别分配为零并处理依赖。”

【讨论】:

  • 当然,请向我们更新您的发现。此外,需要涵盖发现 2 个或更多具有相同权重的选项的小案例。在这种情况下,可以应用其他标准来做出选择。
  • 如果 X 取决于 A 和(B 或 C)而 B 取决于 D 和(F 或 G)怎么办?算法如何处理这个问题,以便将 B 作为 A 的依赖项,你必须也有 D,因为它对 B 是必需的?
  • 这就是我包含 Ar notation 的原因。 Ar = 是树 A=H(and later child's) 或 A=Y(and later child's) 的最佳选择。现在根据您的新案例 X=A 和 Br 或 Cr ,我们的想法是将默认权重 1 分配给根,直到其完整的子树得到解决和计算。之后,根节点获得新的权重,而不是原来的默认权重。参见原始问题,E 也具有相同的行为。 X 取决于 A 和(E 或 C)A 取决于 E 和(H 或 Y).. .. ..
  • 所以我们的算法本质上是“对于每个可选节点,即或选项,分配默认权重,然后在计算出可选节点的实际权重后修改其权重”所以会有相当数量的返回和第四,即在分配和修订周期(或分配标志和修订-de-falg,准确地说)的过程中循环/递归。一旦我们计算出这样的权重并找出我们的短路路径,我们就可以将该路径信息用于任何现实世界的用途(例如,用于设置构建路径或 jar 的下载序列)
【解决方案3】:

我实际上认为图表是解决这个问题的合适结构。注意 A 和(E 或 C)(A 和 E)或(A 和 C)。因此,我们可以用以下一组有向边来表示 X = A 和(E 或 C):

A <- K1
E <- K1
A <- K2
C <- K2
K1 <- X
K2 <- X

本质上,我们只是分解语句的逻辑并使用“虚拟”节点来表示 AND。

假设我们以这种方式分解所有逻辑语句(ANDS 为虚拟 Ki 节点,否则为有向边)。然后,我们可以将输入表示为 DAG 并递归遍历 DAG。我认为下面的递归算法可以解决这个问题:

定义:
节点 u - 当前节点。
S - 访问的节点集。
children(x) - 返回 x 的外邻。

算法:

shortestPath u S = 
if (u has no children) {
    add u to S
    return 1
} else if (u is a dummy node) {
  (a,b) = children(u)
  if (a and b are in S) {
    return 0
  } else if (b is in S) { 
    x = shortestPath a S
    add a to S
    return x
  } else if (a in S) {
    y = shortestPath b S
    add b to S
    return y
  } else {
    x = shortestPath a S
    add a to S
    if (b in S) return x
    else {
        y = shortestPath b S
        add b to S
        return x + y
    }
  }
} else {
  min = Int.Max
  min_node = m
  for (x in children(u)){
    if (x is not in S) {
      S_1 = S
      k = shortestPath x S_1
      if (k < min) min = k, min_node = x
    } else {
      min = 1
      min_node = x
    }
  }
  return 1 + min
}

分析: 这是一个完全顺序的算法,(我认为)最多遍历每条边一次。

【讨论】:

    【解决方案4】:

    这里的很多答案都集中在由于其 NP-hard 状态,这是一个理论上的难题。虽然这意味着您在准确解决问题时会遇到渐近较差的性能(给定当前的解决方案技术),但对于您的特定问题数据,您仍然可以快速(足够)解决它。例如,尽管问题在理论上具有挑战性,但我们能够准确地解决巨大的旅行商问题实例。

    在您的情况下,解决问题的一种方法是将其表述为混合整数线性程序,其中每个包i 都有一个二进制变量x_i。您可以将需求A requires (B or C or D) and (E or F) and (G) 转换为x_A &lt;= x_B + x_C + x_D ; x_A &lt;= x_E + x_F ; x_A &lt;= x_G 形式的约束,并且您可以使用x_P = 1 要求将包P 包含在最终解决方案中。准确求解这样一个模型相对简单;例如,您可以在 python 中使用纸浆包:

    import pulp
    
    deps = {"X": [("A"), ("E", "C")],
            "A": [("E"), ("H", "Y")],
            "E": [("B"), ("Z", "Y")],
            "C": [("A", "K")],
            "H": [],
            "B": [],
            "Y": [],
            "Z": [],
            "K": []}
    required = ["X"]
    
    # Variables
    x = pulp.LpVariable.dicts("x", deps.keys(), lowBound=0, upBound=1, cat=pulp.LpInteger)
    
    mod = pulp.LpProblem("Package Optimization", pulp.LpMinimize)
    
    # Objective
    mod += sum([x[k] for k in deps])
    
    # Dependencies
    for k in deps:
        for dep in deps[k]:
            mod += x[k] <= sum([x[d] for d in dep])
    
    # Include required variables
    for r in required:
        mod += x[r] == 1
    
    # Solve
    mod.solve()
    for k in deps:
        print "Package", k, "used:", x[k].value()
    

    这会输出最小的包集:

    Package A used: 1.0
    Package C used: 0.0
    Package B used: 1.0
    Package E used: 1.0
    Package H used: 0.0
    Package Y used: 1.0
    Package X used: 1.0
    Package K used: 0.0
    Package Z used: 0.0
    

    对于非常大的问题实例,这可能需要很长时间才能解决。您可以接受使用超时的潜在次优解决方案(请参阅here),或者您可以从默认的开源求解器转移到 gurobi 或 cplex 等商业求解器,这可能会更快。

    【讨论】:

      【解决方案5】:

      补充 Misandrist 的答案:您的问题是 NP-complete NP-hard(请参阅dened 的答案)。

      编辑:这是将Set Cover 实例 (U,S) 直接简化为“包装问题”实例:使地面集 U 的每个点 z 成为 X 的 AND 要求. 使 S 中涵盖点 z 的每个集合成为 z 的 OR 要求。然后包问题的解决方案给出了最小集覆盖。

      同样,您可以询问单调布尔电路的哪个令人满意的分配具有最少的真实变量,请参阅这些lecture notes

      【讨论】:

      • 将问题减少到 Set cover 并不表明它是 NP 完全的。相反,您需要将 NP 完全问题简化为该问题。例如,我可以将 2-SAT 简化为 3-SAT,但 2-SAT 很容易,3-SAT 是 NP-complete。
      • @Valentas,你确定要减少吗?在这种情况下,该图最有可能形成一个 DAG。
      • OP 确实说过“它不必处理周期”
      • 我描述的结构是有向无环图,就像问题中的图片一样。
      • 归约是正确的,但是这个问题不是NP-compete,因为最小集覆盖问题本身不是NP-compete。减少最小顶点覆盖也提供了更一般的证明,因为它只需要两个 OR-alternatives;见我的answer
      【解决方案6】:

      由于图由两种不同类型的边组成(AND 和 OR 关系),我们可以将算法分成两部分:搜索所有需要节点后继的节点,并搜索我们必须从中选择的所有节点一个单节点 (OR)。

      节点包含一个包,一个必须是这个节点的后继节点的列表(AND),一个可以是这个节点的后继节点的列表(OR)和一个标记算法中哪个步骤的标志节点被访问过。

      define node: package p , list required , listlist optional , 
                   int visited[default=MAX_VALUE]
      

      主例程将输入转换为图形并在起始节点处开始遍历。

      define searchMinimumP:
          input: package start , string[] constraints
          output: list
      
          //generate a graph from the given constraint
          //and save the node holding start as starting point
          node r = getNode(generateGraph(constraints) , start)
      
          //list all required nodes
          return requiredNodes(r , 0)
      

      requiredNodes 搜索作为节点后继者的所有节点(通过 1 条或多条边的 AND 关系连接到 n)。

      define requiredNodes:
          input: node n , int step
          output: list
      
          //generate a list of all nodes that MUST be part of the solution
          list rNodes
          list todo
      
          add(todo , n)
      
          while NOT isEmpty(todo)
              node next = remove(0 , todo)
              if NOT contains(rNodes , next) AND next.visited > step
                  add(rNodes , next)
                  next.visited = step
      
          addAll(rNodes , optionalMin(rNodes , step + 1))
      
          for node r in rNodes
              r.visited = step
      
          return rNodes
      

      optimalMin 在可选邻居 (OR) 的所有可能解决方案中搜索最短的解决方案。该算法是蛮力的(将检查所有可能的邻居选择。

      define optionalMin:
          input: list nodes , int step
          output: list
      
          //find all possible combinations for selectable packages
          listlist optSeq
          for node n in nodes
              if NOT n.visited < step
                  for list opt in n.optional
                      add(optSeq , opt)
      
          //iterate over all possible combinations of selectable packages
          //for the given list of nodes and find the shortest solution
          list shortest
          int curLen = MAX_VALUE
      
          //search through all possible solutions (combinations of nodes)
          for list seq in sequences(optSeq)
              list subseq
      
              for node n in distinct(seq)
                  addAll(subseq , requiredNodes(n , step + 1))
      
              if length(subseq) < curLen
                  //mark all nodes of the old solution as unvisited
                  for node n in shortest
                      n.visited = MAX_VALUE
      
                  curLen = length(subseq)
                  shortest = subseq
              else
                  //mark all nodes in this possible solution as unvisited
                  //since they aren't used in the final solution (not at this place)
                  for node n in subseq
                      n.visited = MAX_VALUE
      
           for node n in shorest
               n.visited = step
      
           return shortest
      

      基本思想如下:从起始节点开始并搜索必须是解决方案一部分的所有节点(仅通过遍历 AND 关系即可从起始节点到达的节点)。现在对于所有这些节点,算法会搜索可选节点 (OR) 与所需节点最少的组合。

      注意:到目前为止,这个算法并不比蛮力好多少。一旦找到更好的方法,我会立即更新。

      【讨论】:

      • 但是如果一个节点被另外两个节点共享为依赖会发生什么?以我为例:A 有两个或依赖关系,Y 和 H。最短路径是 A E B Y 而不是 A E B H,因为 A 和 E 共享相同的依赖关系。事实上,如果你选择 H 而不是 Y,你的最短路径是 A E B H Y。你如何处理这种情况?希望你能理解我。
      • 你说得对,我的解决方案只适用于树木。我会解决的
      【解决方案7】:

      我的代码是here

      场景:

      表示约束。

      X : A&(E|C)
      A : E&(Y|N)
      E : B&(Z|Y)
      C : A|K
      

      准备两个变量target和result。 将节点 X 添加到目标。

      target = X, result=[]
      

      将单个节点 X 添加到结果中。 将节点 X 替换为其在目标中的依赖项。

      target = A&(E|C), result=[X]
      

      将单个节点 A 添加到结果中。 将节点 A 替换为其在目标中的依赖项。

      target = E&(Y|N)&(E|C), result=[X, A]
      

      单个节点 E 必须为真。 所以 (E|C) 总是正确的。 将其从目标中移除。

      target = E&(Y|N), result=[X, A]
      

      将单个节点 E 添加到结果中。 将节点 E 替换为目标中的依赖节点。

      target = B&(Z|Y)&(Y|N), result=[X, A, E]
      

      将单个节点 B 添加到结果中。 将节点 B 替换为目标中的依赖节点。

      target = (Z|Y)&(Y|N), result=[X, A, E, B]
      

      不再有单个节点。 然后展开目标表达式。

      target = Z&Y|Z&N|Y&Y|Y&N, result=[X, A, E, B]
      

      将 Y&Y 替换为 Y。

      target = Z&Y|Z&N|Y|Y&N, result=[X, A, E, B]
      

      选择节点数最少的词条。 将术语中的所有节点添加到目标。

      target = , result=[X, A, E, B, Y]
      

      【讨论】:

      • 这个算法看起来是正确的,但是效率非常低。瓶颈是扩展步骤。即使输入不是那么大(例如 100 个不同的(A|B)s),程序不仅会永远运行(这是expected),而且还会为扩展的target 消耗yottabytes 的内存。
      • @dened 是的,你是对的。问题是找到具有最少节点数的术语。我认为有快捷算法。至少,有一些算法可以在多项式时间内找到更好的(可能不是最好的)解决方案。这只是我的直觉。
      【解决方案8】:

      我建议您首先将图形转换为AND-OR Tree。完成后,您可以在树中搜索最佳路径(您可以在其中选择“最佳”的含义:节点中包的最短、最低内存占用等)。

      我提出的建议是,安装 X 的条件类似于install(X) = install(A) and (install(E) or install(C)),是将 OR 节点(在本例中:E 和 C)分组为单个节点,例如 EC,然后转换install(X) = install(A) and install(EC) 中的条件。

      或者,基于 AND-OR Tree 理念,您可以使用分组理念创建自定义 AND-OR Graph。通过这种方式,您可以使用 graph traversal 算法的改编版本,这在某些情况下可能更有用。

      另一个解决方案是使用Forward Chaining。您必须按照以下步骤操作:

      1. 变换(这里只是重写条件):

        A 和(E 或 C)=> X

        E 和(H 或 Y)=> A

        B 和(Z 或 Y)=> E

      进入

      (A and E) or (A and C) => X
      (E and H) or (E and Y) => A
      (B and Z) or (B and Y) => E
      
      1. 将 X 设为目标。
      2. 插入 B、H、K、Y、Z 作为事实。
      3. 运行正向链接并在第一次出现 X(目标)时停止。在这种情况下,这应该是实现目标的最短途径(只需记住跟踪已使用的事实)。

      如果有什么不清楚的地方请告诉我。

      【讨论】:

        【解决方案9】:

        这是Constraint Satisfaction Problem 的示例。有许多语言的约束求解器,甚至有些可以在通用 3SAT 引擎上运行,因此可以在 GPGPU 上运行。

        【讨论】:

          【解决方案10】:

          解决此问题的另一种(有趣)方法是使用遗传算法。

          遗传算法很强大,但你必须使用很多参数才能找到更好的参数。

          遗传步骤如下:

          一个。 Creation : 随机若干个体,第一代 (例如:100)

          b. mutation : 低百分比的变异(例如:0,5%)

          c。 Rate:对所有个体进行评分(也称为适应度)。

          d。 复制 : 选择(使用比率)一对并创建孩子(例如:2 个孩子)

          e。 选择:选择Parent和Child来创建一个新的世代(例如:逐代保持100个个体)

          f。 循环 :返回步骤“a”并重复所有过程多次(例如:400代)

          g. Pick :选择具有最大速率的上一代个体。 个人将是您的解决方案。

          这是你必须决定的:

          1. 为您的个人寻找遗传密码

          您必须将问题的可能解决方案(称为个人)表示为遗传密码。

          在您的情况下,它可能是一组字母,表示尊重约束 OR 和 NOT 的节点。

          例如:

          [ A E B Y ], [ A C K H ], [A E Z B Y] ...

          1. 想办法给个人打分

          要知道一个人是否是一个好的解决方案,您必须对其进行评分,以便与其他人进行比较。

          在您的情况下,这可能非常简单:单个速率 = 节点数 - 单个节点数

          例如:

          [ A E B Y ] = 8 - 4 = 4

          [ A E Z B Y] = 8 - 5 = 3

          [ A E B Y ] 比 [ A E Z B Y ] 更好

          1. 选择

          由于个人的费率,我们可以选择一对进行复制。

          例如使用Genetic Algorithm roulette wheel selection

          1. 复制

          从一对个体中创建一些(例如 2 个)子(另一个个体)。

          例如:

          从第一个节点中取出一个节点并将其与第二个节点的节点交换。

          进行一些调整以适应“或,和”约束。

          [ A E B Y ], [ A C K H ] => [ A C E H B Y ], [ A E C K B Y]

          注意:这不是复制它的好方法,因为孩子比父母更有价值。也许我们可以交换一系列节点。

          1. 突变

          您只需更改选定个体的遗传密码。

          例如:

          • 删除一个节点

          • 进行一些调整以适应“或,和”约束。

          如您所见,实现起来并不难,但要针对特定​​问题进行设计并控制不同的参数(突变百分比、速率系统、繁殖系统、个体数量、代数,...)

          【讨论】:

            猜你喜欢
            • 2021-08-08
            • 2014-06-24
            • 2014-07-05
            • 2019-02-11
            • 2017-10-18
            • 1970-01-01
            • 1970-01-01
            • 2017-06-28
            • 2015-04-07
            相关资源
            最近更新 更多