Решение ограничений зависимости

У меня есть классическая проблема решения зависимостей. Я думал, что я направился в правильном направлении, но теперь я попал в блокпост, и я не уверен, как действовать дальше.

Фон

В известном юниверсе (кеше всех артефактов и их зависимостях) каждая взаимосвязь между артефактами и версиями имеет 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 являются "любыми B в серии 0.1". В настоящее время он работает (с полным набором тестов), как ожидалось.

Другими словами, DAG берет абстрактные ограничения зависимостей и создает "реальный" график, где каждое ребро является зависимостью, и каждая вершина (я называл ее a node) является фактическим артефактом. Если решение существует, оно находится где-то на этом графике.

И, к сожалению, здесь я застреваю. Я не могу придумать алгоритм или процедуру, чтобы найти "лучший" путь через этот граф. Я также не знаю, как определить, разрешен ли граф.

Я провел некоторое исследование, и я думал, что топологическая сортировка (tsort) - это тот процесс, который мне нужен. Однако этот алгоритм определяет порядок вставки для зависимостей, а не лучшее решение.

Я уверен, что это проблема np-hard и, вероятно, будет иметь неэффективное время выполнения. Я, хотя использование DAG уменьшит количество сравнений, которые я должен выполнить. Неужели я ошибаюсь в этом предположении? Есть ли лучшая структура данных для использования?

Заключительные мысли

  • Я отметил этот вопрос "Ruby", потому что я использую Ruby, но я ищу psuedo-code/direction. Это не проблема домашних заданий - я искренне пытаюсь учиться.
  • Я попытался дать как можно больше фона, но, пожалуйста, оставьте комментарий, если вы хотите получить более подробную информацию по определенной теме. Это уже длинный пост, но у меня есть больше кода, который я могу предоставить.

Ответ 1

Я не эксперт в этой проблеме, я предлагаю полное решение, которое не является оптимальным, поскольку есть много вещей, которые можно оптимизировать.

Алгоритм прост, он идеально рекурсивный набор . пересечение 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) }