解决依赖限制问题

12
我是一位有用的助手,可以为您翻译文本。
我遇到了一个典型的依赖关系解决问题。我以为我朝着正确的方向前进,但现在我遇到了障碍,不确定如何继续下去。
背景
在已知的宇宙(所有工件及其依赖项的缓存)中,每个工件和版本之间存在1对n的关系,每个版本可能包含不同的依赖项。例如:
A
  1.0.0
    B (>= 0.0.0)
  1.0.1
    B (~> 0.1)
B
  0.1.0
  1.0.0

给定一组“需求限制条件”,我希望找到最佳解决方案(其中“最佳”是仍然满足所有限制条件的最高版本)。以下是一个带有解决方案的示例“需求限制条件”:

solve!('A' => '~> 1.0') #=> {"A" => "1.0.1", "B" => "0.1.0"}

实际上,有更多的需求:

solve!('A' => '~> 1.0', 'B' => '>= 0.0.0', 'C' => '...', 'D' => '...')

版本遵循语义化版本控制标准

我尝试了

当前的解决方案使用回溯,性能不佳。我进行了一些调查,并发现性能问题是由于宇宙的大小而导致的。我决定尝试一种替代方法,为仅仅需求集构建一个“可能性”DAG图:

class Graph
  def initialize
    @nodes = {}
    @edges = {}
  end

  def node(object)
    @nodes[object] ||= Set.new
    self
  end

  def edge(a, b)
    node(a)
    node(b)

    @nodes[a].add(b)

    self
  end

  def nodes
    @nodes.keys
  end

  def edges
    @nodes.values
  end

  def adjacencies(node)
    @nodes[node]
  end
end

我会构建一个来自宇宙中所有可能解决方案的DAG。这将极大地减少可能性,并为我提供了一个实际的图形,其中包含可以使用的真实工件。

def populate(artifact)
  return if loaded?(artifact)

  @graph.node(artifact)

  artifact.dependencies.each do |dependency|
    versions_for(dependency).each do |dependent_artifact|
      @graph.edge(artifact, dependent_artifact)
      populate(dependent_artifact)
    end
  end
end

private

def versions_for(dependency)
  possibles = @universe.versions(dependency.name, dependency.constraint)

  # Short-circuit if there are no versions for this dependency,
  # since we know the graph won't be solvable.
  raise "No solution for #{dependency}!" if possibles.empty?

  possibles
end

因此,从前面的示例图中可以看出,如果我有需求 'A','>= 0.0.0',那么我的DAG将如下所示:

+---------+   +---------+
| A-1.0.0 |   | A-1.0.1 |
+---------+   +---------+
       /  \        |
      /    \       |
     /      \      |
    /        \     |
+---------+   +---------+
| B-1.0.0 |   | B-0.1.0 |
+---------+   +---------+

由于A-1.0.0的可能值为“B的任何值”,但A-1.0.1的约束条件为“0.1系列中的任何B”。目前这个工作正常(带有完整的测试套件),符合预期。

换句话说,DAG获取抽象依赖约束并创建一个“真实”的图,其中每条边都是一个依赖关系,每个顶点(我称之为节点)都是一个实际的构件。如果存在解决方案,则它在这个图中的某个地方。

遗憾的是,这就是我卡住的地方。我无法想出一种算法或过程来找到通过这个图的“最佳”路径。我也不确定如何检测图是否可解。

我做了一些研究,我认为拓扑排序(tsort)是我所需要的过程。然而,该算法确定依赖项的插入顺序,而不是最佳解决方案。

我相当确定这是一个np-hard问题,并且可能具有低效的运行时间。我认为使用DAG会减少我必须进行的比较次数。我的假设错了吗?有更好的数据结构可以使用吗?

最终思考

  • 我将此问题标记为“Ruby”,因为我正在使用Ruby,但我正在寻找伪代码/方向。这不是一个家庭作业问题-我真的在努力学习。
  • 我尽可能地提供了尽可能多的背景信息,但如果您需要特定主题的更多细节,请留下评论。这已经是一篇冗长的文章,但我还有更多的代码可以分享。

3
这很困难。请查看以下问题:https://dev59.com/i2Uo5IYBdhLWcg3wnwn2#16337095 - David Eisenstat
你看过 Bundler 做的事情吗? - Gene
1
我有。他们的解析器非常特定于Bundler和Ruby生态系统。 - sethvargo
你能百分之百保证依赖图是无环的吗?如果我没记错,类似树形的SAT问题是可处理的。编辑:我记得用于类似树形SAT的算法叫做“警告传递”,但我找不到一个好的简短介绍。如果我有时间的话,可能会自己写一个。 - Andy Jones
1
据我所知,只有当约束图具有低(分支、树等)宽度时,这些算法才可以被证明是有效的。一般的DAG可能具有线性宽度。 - David Eisenstat
找到了我读有关WP的论文,你是对的,他们的结果是它收敛了,而不是它有效地收敛了。干杯! - Andy Jones
1个回答

2

我不是这个问题的专家,我提出了一个完整的解决方案,但并不是最优的,因为有很多可以优化的地方。

算法很简单,理想情况下是一个递归集合交集DFS:

算法

定义

Define: Name as String on format [ .* ]
Define: Version as String on format [ dd.dd.dd ]
Define: Revision as { Name, Version, Requirement }
Define: Range<T> as { min<T>, max<T> }
Define: Condition as { Name, Range<Version> }
Define: Requirement as Set<Revision> OR as Set<Condition>
Define: Component as { Name, Range<Version>, Requirement }
Define: System as Set<Component>

输入

Input: T as System aka basis
Input: C as Set<Condition> aka conditions to apply

初始化

Init: S as Set<Condition> = { S[i] as Condition | S[i] = {T[i].Name,T[i].Range} }
Init: Q as Stack<Condition> = { push(q) | q[i] = C[i] }

进程

for (Condition c in C)
{
    S.find(c.Name).apply(c)
}

While (Q.size > 0)
{
    Condition q = Q.pop()

    switch (T.assert(S.find(q.Name),q))
    {
      case VALID:
        S.find(q.Name).apply(q)
        q.push(S.find(q.Name).Requirement)

      case INVALID:
        S.find(q.Name).set(INVALID)

      case IDENTICAL:
      case SKIP:
    }
}

return S aka Solution

操作

Stack.push 在栈的前端插入一个项目

Stack.pop 从栈的前端移除一个项目

System.assert(Condition a, Condition b):
    if (a is INVALID) then return SKIP
    else if (b.Range = a.Range) then IDENTICAL
    else if (b.Range - a.Range = {}) then VALID
    else INVALID

Set.find(x) 根据条件 x 搜索一个项目

Condition.apply(Condition b) = { this.Name, intersection(this.Range,b.Range) }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接