Estou tentando determinar o algoritmo com melhor eficiência de tempo para realizar a tarefa descrita abaixo.
Eu tenho um conjunto de registros. Para este conjunto de registros, tenho dados de conexão que indicam como os pares de registros desse conjunto se conectam entre si. Isso basicamente representa um gráfico não direcionado, com os registros sendo os vértices e os dados de conexão as arestas.
Todos os registros no conjunto têm informações de conexão (ou seja, nenhum registro órfão está presente; cada registro no conjunto se conecta a um ou mais outros registros no conjunto).
Quero escolher dois registros quaisquer do conjunto e ser capaz de mostrar todos os caminhos simples entre os registros escolhidos. Por "caminhos simples", quero dizer os caminhos que não têm registros repetidos no caminho (ou seja, apenas caminhos finitos).
Nota: Os dois registros escolhidos sempre serão diferentes (ou seja, o vértice inicial e final nunca serão os mesmos; sem ciclos).
Por exemplo:
Se eu tiver os seguintes registros: A, B, C, D, E e o seguinte representa as conexões: (A, B), (A, C), (B, A), (B, D), (B, E), (B, F), (C, A), (C, E), (C, F), (D, B), (E, C), (E, F), (F, B), (F, C), (F, E) [onde (A, B) significa que o registro A se conecta ao registro B]
Se eu escolher B como meu registro inicial e E como meu registro final, gostaria de encontrar todos os caminhos simples através das conexões de registro que conectariam o registro B ao registro E.
Todos os caminhos que conectam B a E: B-> E B-> F-> E B-> F-> C-> E B-> A-> C-> E B-> A-> C-> F-> E
Este é um exemplo, na prática posso ter conjuntos contendo centenas de milhares de registros.
fonte
Respostas:
Parece que isso pode ser feito com uma pesquisa em profundidade no gráfico. A pesquisa em profundidade encontrará todos os caminhos não cíclicos entre dois nós. Este algoritmo deve ser muito rápido e escalar para gráficos grandes (a estrutura de dados do gráfico é esparsa, por isso usa apenas a quantidade de memória necessária).
Percebi que o gráfico que você especificou acima tem apenas uma aresta que é direcional (B, E). Foi um erro de digitação ou é realmente um gráfico direcionado? Esta solução funciona independentemente. Desculpe não ter conseguido fazer em C, sou um pouco fraco nessa área. Espero que você seja capaz de traduzir este código Java sem muitos problemas.
Graph.java:
Search.java:
Resultado do programa:
fonte
O Dicionário online de Algoritmos e Estruturas de Dados do Instituto Nacional de Padrões e Tecnologia (NIST) lista esse problema como " todos os caminhos simples" e recomenda uma pesquisa em profundidade . CLRS fornece os algoritmos relevantes.
Uma técnica inteligente usando Redes de Petri é encontrada aqui
fonte
Aqui está o pseudocódigo que criei. Este não é um dialeto de pseudocódigo particular, mas deve ser simples o suficiente para seguir.
Alguém quer escolher isso separadamente.
[p] é uma lista de vértices que representam o caminho atual.
[x] é uma lista de caminhos onde atendem aos critérios
[s] é o vértice de origem
[d] é o vértice de destino
[c] é o vértice atual (argumento para a rotina PathFind)
Suponha que haja uma maneira eficiente de pesquisar os vértices adjacentes (linha 6).
fonte
Como a implementação de DFS não recursiva existente fornecida nesta resposta parece estar quebrada, deixe-me fornecer uma que realmente funcione.
Eu escrevi isso em Python, porque acho que é bastante legível e organizado por detalhes de implementação (e porque tem a
yield
palavra-chave útil para implementar geradores ), mas deve ser bastante fácil de portar para outras linguagens.Este código mantém duas pilhas paralelas: uma contendo os nós anteriores no caminho atual e outra contendo o índice vizinho atual para cada nó na pilha de nós (para que possamos retomar a iteração através dos vizinhos de um nó quando o retirarmos a pilha). Eu poderia também ter usado uma única pilha de pares (nó, índice), mas imaginei que o método de duas pilhas seria mais legível e talvez mais fácil de implementar para usuários de outras linguagens.
Este código também usa um
visited
conjunto separado , que sempre contém o nó atual e quaisquer nós na pilha, para me permitir verificar com eficiência se um nó já faz parte do caminho atual. Se a sua linguagem tiver uma estrutura de dados de "conjunto ordenado" que fornece operações push / pop semelhantes a pilha e consultas de associação eficientes, você pode usar isso para a pilha de nós e se livrar dovisited
conjunto separado .Alternativamente, se você estiver usando uma classe / estrutura mutável personalizada para seus nós, você pode simplesmente armazenar um sinalizador booleano em cada nó para indicar se ele foi visitado como parte do caminho de pesquisa atual. É claro que esse método não permite que você execute duas pesquisas no mesmo gráfico em paralelo, caso você queira fazer isso por algum motivo.
Aqui estão alguns códigos de teste que demonstram como a função fornecida acima funciona:
Executar este código no gráfico de exemplo fornecido produz a seguinte saída:
Observe que, embora este gráfico de exemplo não seja direcionado (ou seja, todas as suas arestas vão para os dois lados), o algoritmo também funciona para gráficos direcionados arbitrários. Por exemplo, remover a
C -> B
borda (removendoB
da lista de vizinhos deC
) produz a mesma saída, exceto para o terceiro caminho (A -> C -> B -> D
), que não é mais possível.Ps. É fácil construir gráficos para os quais algoritmos de pesquisa simples como este (e os outros fornecidos neste tópico) têm um desempenho muito fraco.
Por exemplo, considere a tarefa de encontrar todos os caminhos de A a B em um grafo não direcionado onde o nó inicial A tem dois vizinhos: o nó alvo B (que não tem outros vizinhos além de A) e um nó C que faz parte de um clique de n +1 nós, como este:
É fácil ver que o único caminho entre A e B é o direto, mas um DFS ingênuo iniciado no nó A perderá O ( n !) Tempo explorando inutilmente os caminhos dentro do clique, mesmo que seja óbvio (para um humano) que nenhum desses caminhos pode levar a B.
Também é possível construir DAGs com propriedades semelhantes, por exemplo, fazendo com que o nó inicial A conecte o nó de destino B e a dois outros nós C 1 e C 2 , ambos os quais se conectam aos nós D 1 e D 2 , ambos os quais se conectam a E 1 e E 2 e assim por diante. Para n camadas de nós organizados dessa forma, uma busca ingênua por todos os caminhos de A a B acabará perdendo O (2 n ) tempo examinando todos os possíveis becos sem saída antes de desistir.
É claro, a adição de um bordo para o nó de destino B a partir de um dos nós no bando (diferente de C), ou a partir da última camada do DAG, iria criar um exponencialmente grande número de possíveis caminhos de A a B, e um o algoritmo de busca puramente local não pode realmente dizer com antecedência se encontrará ou não tal borda. Assim, em certo sentido, a baixa sensibilidade de saída dessas pesquisas ingênuas se deve à falta de consciência da estrutura global do gráfico.
Embora existam vários métodos de pré-processamento (como a eliminação iterativa de nós folha, a procura de separadores de vértice de nó único, etc.) que poderiam ser usados para evitar alguns desses "becos sem saída em tempo exponencial", não conheço nenhum geral truque de pré-processamento que poderia eliminá-los em todos os casos. Uma solução geral seria verificar em cada etapa da pesquisa se o nó de destino ainda pode ser alcançado (usando uma subprocura) e retroceder mais cedo se não for - mas, infelizmente, isso tornaria a pesquisa significativamente mais lenta (na pior das hipóteses , proporcionalmente ao tamanho do gráfico) para muitos gráficos que não contêm esses becos sem saída patológicos.
fonte
for path in find_simple_paths(graph, "A", "D"): print(" -> ".join(path))
, oprint
que faltava o parêntese.Aqui está uma versão recursiva logicamente melhor em comparação com o segundo andar.
Resultado do programa
fonte
Solução em código C. É baseado em DFS que usa memória mínima.
fonte
Isso pode ser tarde, mas aqui está a mesma versão C # do algoritmo DFS em Java de Casey para percorrer todos os caminhos entre dois nós usando uma pilha. A legibilidade é melhor com recursiva como sempre.
fonte
neighbours.Reverse()
? É issoList<T>.Reverse
?Resolvi um problema semelhante a este recentemente, em vez de todas as soluções que estava interessado apenas na mais curta.
Eu usei uma pesquisa iterativa 'largura primeiro' que usou uma fila de status 'cada uma das quais continha um registro contendo um ponto atual no gráfico e o caminho percorrido para chegar lá.
você começa com um único registro na fila, que possui o nó inicial e um caminho vazio.
Cada iteração através do código tira o item do topo da lista e verifica se é uma solução (o nó que você chegou é aquele que você deseja, se for, estamos prontos), caso contrário, ele constrói um novo item de fila com os nós conectando-se ao nó atual e caminhos corrigidos que são baseados no caminho do nó anterior, com o novo salto anexado no final.
Agora, você poderia usar algo semelhante, mas quando encontrar uma solução, em vez de parar, adicione essa solução à sua 'lista de encontrados' e continue.
Você precisa manter o controle de uma lista de nós visitados, para que você nunca volte atrás em si mesmo, caso contrário, você terá um loop infinito.
se você quiser um pouco mais de pseudocódigo poste um comentário ou algo assim, e irei elaborar.
fonte
Acho que você deve descrever o seu verdadeiro problema por trás disso. Digo isso porque você pede algo eficiente em termos de tempo, mas o conjunto de respostas para o problema parece crescer exponencialmente!
Portanto, eu não esperaria um algoritmo melhor do que algo exponencial.
Eu voltaria atrás e examinaria todo o gráfico. Para evitar ciclos, salve todos os nós visitados ao longo do caminho. Quando você voltar, desmarque o nó.
Usando recursão:
Ou isso está errado?
editar: Ah, e eu esqueci: você deve eliminar as chamadas recursivas utilizando aquela pilha de nós
fonte
O princípio básico é que você não precisa se preocupar com gráficos. Esse é um problema padrão conhecido como problema de conectividade dinâmica. Existem seguintes tipos de métodos a partir dos quais você pode fazer com que os nós estejam conectados ou não:
Aqui está o código C que tentei com complexidade de tempo mínima O (log * n) Isso significa que para 65536 lista de arestas, é necessário 4 pesquisas e 2 ^ 65536, 5 pesquisas. Estou compartilhando minha implementação do algoritmo: Curso de Algoritmo da Universidade de Princeton
DICA: Você pode encontrar a solução Java no link compartilhado acima com as explicações adequadas.
fonte
find_paths [s, t, d, k]
Esta pergunta é antiga e já foi respondida. No entanto, nenhum mostra talvez um algoritmo mais flexível para realizar a mesma coisa. Então, vou jogar meu chapéu no ringue.
Eu pessoalmente acho
find_paths[s, t, d, k]
útil um algoritmo do formulário , onde:Usando a forma infinita de sua linguagem de programação para
d
ek
lhe dará todos os caminhos§.§ obviamente, se você estiver usando um grafo dirigido e você quer todos sem direção caminhos entre
s
et
você terá que executar este em ambos os sentidos:Função Auxiliar
Eu pessoalmente gosto de recursão, embora possa ser difícil algumas vezes, de qualquer forma, primeiro vamos definir nossa função auxiliar:
Função principal
Com isso fora do caminho, a função central é trivial:
Primeiro, vamos notar algumas coisas:
[]
é uma lista não inicializada, substitua-a pelo equivalente para sua linguagem de programação de escolhapaths_found
é passado por referência . É claro que a função de recursão não retorna nada. Lide com isso de forma adequada.graph
está assumindo alguma forma dehashed
estrutura. Existem várias maneiras de implementar um gráfico. De qualquer forma,graph[vertex]
obtém uma lista de vértices adjacentes em um gráfico direcionado - ajuste de acordo.fonte
Aqui está um pensamento que surgiu na minha cabeça:
fonte
Pelo que eu posso dizer, as soluções fornecidas por Ryan Fox ( 58343 , Christian ( 58444 ) e você ( 58461 ) são as melhores possíveis . Não acredito que a travessia em largura ajude neste caso, como você irá não obter todos os caminhos. Por exemplo, com bordas
(A,B)
,(A,C)
,(B,C)
,(B,D)
e(C,D)
você vai ter caminhosABD
eACD
, mas nãoABCD
.fonte
Encontrei uma maneira de enumerar todos os caminhos, incluindo os infinitos que contêm loops.
http://blog.vjeux.com/2009/project/project-shortest-path.html
Encontrando Caminhos Atômicos e Ciclos
O que queremos fazer é encontrar todos os caminhos possíveis que vão do ponto A ao ponto B. Uma vez que existem ciclos envolvidos, você não pode simplesmente percorrer e enumerar todos eles. Em vez disso, você terá que encontrar o caminho atômico que não faça loop e os menores ciclos possíveis (você não quer que seu ciclo se repita).
A primeira definição que fiz de um caminho atômico é um caminho que não passa pelo mesmo nó duas vezes. Porém, descobri que não estava aproveitando todas as possibilidades. Após alguma reflexão, descobri que os nós não são importantes, mas as bordas são! Portanto, um caminho atômico é um caminho que não passa pela mesma borda duas vezes.
Esta definição é útil, ela também funciona para ciclos: um ciclo atômico do ponto A é um caminho atômico que vai do ponto A e termina no ponto A.
Implementação
Para obter todo o caminho a partir do ponto A, vamos percorrer o gráfico recursivamente a partir do ponto A. Ao passar por um filho, faremos um link filho -> pai para conhecer todas as arestas que já cruzou. Antes de irmos para aquela criança, devemos percorrer essa lista vinculada e ter certeza de que a borda especificada ainda não foi percorrida.
Quando chegamos ao ponto de destino, podemos armazenar o caminho que encontramos.
Um problema ocorre quando você deseja liberar a lista vinculada. É basicamente uma árvore encadeada na ordem inversa. Uma solução seria fazer um duplo link dessa lista e, quando todos os caminhos atômicos forem encontrados, liberar a árvore do ponto de partida.
Mas uma solução inteligente é usar uma contagem de referência (inspirada na Garbage Collection). Cada vez que você adiciona um link para um dos pais, você adiciona um à sua contagem de referência. Então, ao chegar ao final de um caminho, você retrocede e fica livre enquanto a contagem de referência é igual a 1. Se for maior, basta remover um e parar.
Procurar o ciclo atômico de A é o mesmo que procurar o caminho atômico de A para A. No entanto, existem várias otimizações que podemos fazer. Em primeiro lugar, ao chegarmos ao ponto de destino, queremos salvar o caminho apenas se a soma dos custos das arestas for negativa: queremos apenas passar por ciclos absorventes.
Como você viu anteriormente, todo o gráfico está sendo percorrido ao procurar um caminho atômico. Em vez disso, podemos limitar a área de pesquisa ao componente fortemente conectado que contém A. Encontrar esses componentes requer uma simples travessia do gráfico com o algoritmo de Tarjan.
Combinando Caminhos Atômicos e Ciclos
Neste ponto, temos todos os caminhos atômicos que vão de A a B e todos os ciclos atômicos de cada nó, que nos resta organizar tudo para obter o caminho mais curto. A partir de agora vamos estudar como encontrar a melhor combinação de ciclos atômicos em um caminho atômico.
fonte
Conforme habilmente descrito por alguns dos outros cartazes, o problema em poucas palavras é o de usar um algoritmo de pesquisa em profundidade para pesquisar recursivamente no gráfico todas as combinações de caminhos entre os nós finais em comunicação.
O algoritmo em si começa com o nó inicial que você fornece, examina todos os seus links de saída e progride expandindo o primeiro nó filho da árvore de pesquisa que aparece, pesquisando cada vez mais profundamente até que um nó de destino seja encontrado ou até encontrar um nó que não tem filhos.
A pesquisa então retrocede, retornando ao nó mais recente que ainda não terminou de explorar.
Eu fiz um blog sobre esse assunto recentemente, postando um exemplo de implementação C ++ no processo.
fonte
Somando-se à resposta de Casey Watson, aqui está outra implementação Java. Inicializando o nó visitado com o nó inicial.
fonte