Como eu estava procurando loops simples

ícone da bibliotecaAcordei no final da tarde e decidi - está na hora de criar um novo recurso na minha biblioteca . E por um lado e verifique o gráfico quanto a ciclos para corrigir e acelerar. No almoço da manhã, criei um novo recurso, aprimorei o código, fiz a representação do gráfico de forma conveniente e cheguei ao problema de encontrar todos os ciclos simples no gráfico. Bebendo um copo de água fria, abri o Google, digitei "pesquisar todos os ciclos simples em um gráfico" e vi ...



Não vi muito ... embora fossem apenas quatro horas da manhã. Alguns links para os algoritmos link1 , link2 e muitas dicas queé hora de dormirPode haver muitos ciclos no gráfico e, no caso geral, o problema não é solucionável. E outra pergunta sobre Habré sobre esse assunto, cuja resposta também não me salvou - eu já sabia sobre a pesquisa em profundidade.



Mas, uma vez resolvido, mesmo o NP resolverá um problema difícil em P , mais eu bebo água, não café - e não pode terminar abruptamente. E eu sabia que, dentro da estrutura da minha tarefa, deveria ser possível encontrar todos os ciclos em um curto espaço de tempo, sem supercomputadores - não a ordem de grandeza para mim.



Vamos desviar um pouco do detetive e entender por que eu preciso.



O que é essa biblioteca?



A biblioteca chamada DITranquility é escrita em Swift e sua tarefa é injetar dependências . A biblioteca lida com a tarefa de injeção de dependência com um estrondo, possui recursos que outras bibliotecas Swift não conseguem e faz isso com boa velocidade.



Mas por que eu deveria adquirir o hábito de procurar loops no gráfico de dependência?



Por uma questão de killerfichi - se a biblioteca executa a funcionalidade principal com um estrondo, você está procurando maneiras de melhorá-la e melhorá-la. Um killefic está verificando a correção do gráfico de dependência - é um conjunto de verificações diferentes que permitem evitar problemas durante a execução, o que economiza tempo de desenvolvimento. E a verificação de ciclos se destaca separadamente entre todas as verificações, pois essa operação leva muito mais tempo. E até recentemente, não civilizava mais tempo.



, , , . , "Graph API" . "Graph API" — , :



  • - . , , , .
  • , - — graphvis.
  • .


— , ...







, :



  • MacBook pro 2019, 2,6 GHz 6-Core i7, 32 Gb, Xcode 11.4, Swift 5.2
  • Swift 300+ ( )
  • 900
  • 2000
  • 40
  • 7000


debug , release, debug.



, 95 .



3 , .



1.



. , .

, . Graph API , , . . , , , .



, , — . , , , :



  • — , .
  • — ,


, , , . , — , . — ?



, - :



Graph:
    vertices: [Vertex]
    adjacencyList: [[Edge]]

Vertex:
    more information about vertex

Edge:
    toIndices: [Int]
    information about edge


Vertex , Edge , , .

, . , , , , .



2.



, 4 , , , , , — " , , ". :



func findAllCycles() -> [Cycle] {
  result: [Cycle] = []
  for index in vertices {
    result += findCycles(from: index)
  }

  return result
}

func findCycles(from index: Int) -> [Cycle] {
  result: [Cycle] = []
  dfs(startIndex: index, currentIndex: index, visitedIndices: [], result: &result)

  return result
}

func dfs(startIndex: Int,
         currentIndex: Int,
         // visitedIndices   
         visitedIndices: Set<Int>,
         // result   -  
         result: ref [Cycle]) {
  if currentIndex == startIndex && !visitedIndices.isEmpty {
    result.append(cycle)
    return
  }

  if visitedIndices.contains(currentIndex) {
    return
  }

  visitedIndices += [currentIndex]

  for toIndex in adjacencyList[currentIndex] {
    dfs(startIndex: startIndex, currentIndex: toIndex, visitedIndices: visitedIndices, result: &result)
  }
}


, 10 … , , — - ...



, — ? , ? , dfs , 30 . 900 * 1000000 = 900.000.000 dfs…



? , , … ZZzzz...



3.



, , , , , - … , , , !



, , , , . — , , . , , , - :



func findAllCycles() -> [Cycle] {
  globalVisitedIndices: Set<Int> = []
  result: [Cycle] = []
  for index in vertices {
    if globalVisitedIndices.containts(index) {
      continue
    }
    result += findCycles(from: index, globalVisitedIndices: &globalVisitedIndices)
  }

  return result
}

func findCycles(from index: Int, globalVisitedIndices: ref Set<Int>) -> [Cycle] {
  result: [Cycle] = []
  dfs(currentIndex: index, visitedIndices: [], globalVisitedIndices, &globalVisitedIndices, result: &result)

  return result
}

func dfs(currentIndex: Int,
         // visitedIndices   
         visitedIndices: Set<Int>,
         // globalVisitedIndices   -  
         globalVisitedIndices: ref Set<Int>,
         // result   -  
         result: ref [Cycle]) {

  if visitedIndices.contains(currentIndex) {
    //  visitedIndices ,   ,     
    result.append(cycle)
    return
  }

  visitedIndices += [currentIndex]

  for toIndex in adjacencyList[currentIndex] {
    dfs(currentIndex: toIndex, visitedIndices: visitedIndices, globalVisitedIndices: &globalVisitedIndices, result: &result)
  }
}


" , " … . 10 , , , , :



  • , , , .
  • "" — , .


, . , , .



4.



, . , , , , , , . , ? . , , run… , — , … , … … , . :



if visitedIndices.contains(currentIndex) {


, , . :





B->E->C , if . , :

A->B->E->C->B!.. C, . A->B->D->A.

A->C->B->D->A , C .



, dfs , .



5.



, . , , , dfs 30 , 1-2 . :





"Big" - , A.



! Big C, , A B, , C , , A.



? , , , . . O(N^2) , .



, :



func findAllCycles() -> [Cycle] {
  reachableIndices: [Set<Int>] = findAllReachableIndices()
  result: [Cycle] = []
  for index in vertices {
    result += findCycles(from: index, reachableIndices: &reachableIndices)
  }

  return result
}

func findAllReachableIndices() -> [Set<Int>] {
  reachableIndices: [Set<Int>] = []
  for index in vertices {
    reachableIndices[index] = findAllReachableIndices(for: index)
  }
  return reachableIndices
}

func findAllReachableIndices(for startIndex: Int) -> Set<Int> {
  visited: Set<Int> = []
  stack: [Int] = [startIndex]
  while fromIndex = stack.popFirst() {
    visited.insert(fromIndex)

    for toIndex in adjacencyList[fromIndex] {
      if !visited.contains(toIndex) {
        stack.append(toIndex)
      }
    }
  }

  return visited
}

func findCycles(from index: Int, reachableIndices: ref [Set<Int>]) -> [Cycle] {
  result: [Cycle] = []
  dfs(startIndex: index, currentIndex: index, visitedIndices: [], reachableIndices: &reachableIndices, result: &result)

  return result
}

func dfs(startIndex: Int,
         currentIndex: Int,
         visitedIndices: Set<Int>,
         reachableIndices: ref [Set<Int>],
         result: ref [Cycle]) {
  if currentIndex == startIndex && !visitedIndices.isEmpty {
    result.append(cycle)
    return
  }

  if visitedIndices.contains(currentIndex) {
    return
  }

  if !reachableIndices[currentIndex].contains(startIndex) {
    return
  }

  visitedIndices += [currentIndex]

  for toIndex in adjacencyList[currentIndex] {
    dfs(startIndex: startIndex, currentIndex: toIndex, visitedIndices: visitedIndices, result: &result)
  }
}


, , , 5 — . — 15 , 6-7 . , , , — .



6. ?



, , — - . , , - .

, , , .

— " , , , ?". . 5 .



, 250, , , :



func findAllCycles() -> [Cycle] {
  reachableIndices: [Set<Int>] = findAllReachableIndices()
  result: [Cycle] = []
  for index in vertices {
    result += findCycles(from: index, reachableIndices: &reachableIndices)
  }

  return result
}

func findAllReachableIndices() -> [Set<Int>] {
  reachableIndices: [Set<Int>] = []
  for index in vertices {
    reachableIndices[index] = findAllReachableIndices(for: index)
  }
  return reachableIndices
}

func findAllReachableIndices(for startIndex: Int) -> Set<Int> {
  visited: Set<Int> = []
  stack: [Int] = [startIndex]
  while fromIndex = stack.popFirst() {
    visited.insert(fromIndex)

    for toIndex in adjacencyList[fromIndex] {
      if !visited.contains(toIndex) {
        stack.append(toIndex)
      }
    }
  }

  return visited
}

func findCycles(from index: Int, reachableIndices: ref [Set<Int>]) -> [Cycle] {
  result: [Cycle] = []
  dfs(startIndex: index, currentIndex: index, visitedIndices: [], reachableIndices: &reachableIndices, result: &result)

  return result
}

func dfs(startIndex: Int,
         currentIndex: Int,
         visitedIndices: Set<Int>,
         reachableIndices: ref [Set<Int>],
         result: ref [Cycle]) {
  if currentIndex == startIndex && !visitedIndices.isEmpty {
    result.append(cycle)
    return
  }

  if visitedIndices.contains(currentIndex) {
    return
  }

  if currentIndex < startIndex || !reachableIndices[currentIndex].contains(startIndex) {
    return
  }

  visitedIndices += [currentIndex]

  for toIndex in adjacencyList[currentIndex] {
    dfs(startIndex: startIndex, currentIndex: toIndex, visitedIndices: visitedIndices, result: &result)
  }
}


: if currentIndex < startIndex.



, run, , — … 6 ? , … .



, , , . — , A, , A . A, .



, , /.

— , . 5-10% .



6.



5-6 , , ! . , Swift , .

, , 6 … , . — " ? ...". — . — , .



3 , , . , Apple , (, , ). Swift popLast(), . .



( Swift)
var visited: Set<Int> = []
var stack: [Int] = [startVertexIndex]
while let fromIndex = stack.first {
  stack.removeFirst()

  visited.insert(fromIndex)
  for toIndex in graph.adjacencyList[fromIndex].flatMap({ $0.toIndices }) {
    if !visited.contains(toIndex) {
      stack.append(toIndex)
    }
  }
}

return visited


c ( Swift)
var visited: Set<Int> = []
var stack: [Int] = [startVertexIndex]
while let fromIndex = stack.popLast() {
  visited.insert(fromIndex)
  for toIndex in graph.adjacencyList[fromIndex].flatMap({ $0.toIndices }) {
    if !visited.contains(toIndex) {
      stack.insert(toIndex, at: 0)
    }
  }
}

return visited


, , — , Swift 5-10%.





? — 95 , 2.5-3 , .



, , — .

, , 15 , .



— , , , .



Swift .





, , , .



, "" -. , - :



  • graphvis — .
  • — , , , .
  • , .


P.S. 5 , . , 1.5 — 4.5 . .



P.P.S. , . , , //.



UPD: . dot , .

.



UPD: banshchikov Donald B. Johnson. swift.

, .

:



_
(debug) 2.4-2.5 36.2-44.5
() 0.25-0.33 32.1-36.4
6920* 5736
* 5736 5736


O tempo foi medido apenas para a pesquisa de ciclos. Uma biblioteca de terceiros converte os dados de entrada em uma conveniente, mas essa conversão não deve levar mais que 0,1 segundos. E como você pode ver, o tempo difere por uma ordem de magnitude (e no release por duas). E uma diferença tão grande não pode ser atribuída a uma implementação não ideal.



  • Como escrevi, na minha biblioteca, as arestas não são apenas uma transição de um vértice para outro. Essas informações não podem ser passadas para uma implementação de terceiros, portanto, o número de loops encontrados não corresponde. Por esse motivo, os resultados são pesquisados ​​em todos os ciclos exclusivos ao longo dos vértices, excluindo as arestas, para garantir que o resultado corresponda.



All Articles