5
votes

Trouvez efficacement les composants connectés dans un graphe non orienté en tenant compte de l'équivalence transitive

J'ai un ensemble de nœuds et une fonction foo (u, v) qui peut déterminer si deux nœuds sont égaux. Par «égal», j'entends l'équivalence transitive: Si 1 == 2 et 2 == 3 alors 1 == 3 et aussi: Si 1 == 2 code > et 1! = 4 puis 2!=4

Lorsqu'on me donne un ensemble de nœuds, je peux trouver tous les composants connectés dans le graphique en passant toutes les combinaisons possibles de nœuds à foo (u, v) (qui renvoie des résultats prédéterminés à des fins de présentation uniquement - ce n'est pas la vraie fonction! strong>) fonction et la construction des bords nécessaires. Comme ceci:

1==2  # ok
1==3  # ok
1!=4  # ok
1!=5  # ok
2==3  # redundant check, if 1==2 and 1==3 then 2==3 
2!=4  # redundant check, if 1!=4 and 1==2 then 2!=4 
2!=5  # redundant check, if 1!=5 and 1==2 then 2!=5
3!=4  # redundant check, if 1!=4 and 1==3 then 3!=4
3!=5  # redundant check, if 1!=5 and 1==3 then 3!=5
4==5  # ok

le problème avec cette approche est que je reçois de nombreux contrôles redondants que je voudrais éviter:

import networkx as nx
import itertools
from matplotlib import pyplot as plt


def foo(u, v):
    # this function is simplified, in reality it will do a complex 
    # calculation to determine whether nodes are equal.
    EQUAL_EDGES = {(1, 2), (2, 3), (1, 3), (4, 5)}
    return (u, v) in EQUAL_EDGES


def main():
    g = nx.Graph()
    g.add_nodes_from(range(1, 5 + 1))
    for u, v in itertools.combinations(g.nodes, 2):
        are_equal = foo(u, v)
        print '{u}{sign}{v}'.format(u=u, v=v, sign='==' if are_equal else '!=')
        if are_equal:
            g.add_edge(u, v)

    conn_comps = nx.connected_components(g)
    nx.draw(g, with_labels=True)
    plt.show()
    return conn_comps


if __name__ == '__main__':
    main()
Je veux éviter de courir avec une complexité temporelle O (n ^ 2). Quelle est la bonne façon (ou peut-être une fonction existante dans n'importe quelle bibliothèque python) pour trouver efficacement tous les composants connectés par une fonction foo (u, v) personnalisée?


2 commentaires

Vous ne pouvez pas éviter la complexité O (n ^ 2) . Tout ce que vous pouvez faire est d'éviter les comparaisons redondantes


Si la fonction foo calcule l'égalité de deux nœuds en respectant l'égalité transitoire entre tous les nœuds, alors pourquoi ne faites-vous pas en premier lieu cette fonction pour gérer les redondances? Ou l'inverse, comment cette fonction garantit-elle qu'elle respecte l'égalité transitoire entre tous les nœuds?


3 Réponses :


1
votes

Ce que vous essayez vraiment de faire n'est pas clair, mais voici une solution qui ne vérifie qu'un seul élément dans chaque groupe équivalent:

nodes2place = range(1, 6)
cclist = []

for u in nodes2place:
    node_was_placed=False
    for icc in range(len(cclist)):
        if foo(u, cclist[icc][0]):
            cclist[icc].append(u)
            node_was_placed=True
            break

    # node doesn't fit into existing cc so make a new one
    if not node_was_placed:
        cclist.append([u])


2 commentaires

cette solution est toujours O (n ^ 2) dans le pire des cas lorsqu'il n'y a pas de connexions entre les nœuds


Oui, c'est toujours O (n ^ 2) dans le pire des cas, mais ce n'est pas clair pour moi si vous pouvez éviter cela à cause de la façon dont vous avez formulé le problème de recherche de relation d'équivalence. BTW, avez-vous réellement une structure graphique sous-jacente ou supposez-vous que vous en avez besoin pour trouver vos composants? Lorsque vous avez posé la question, il semble que vous construisez un graphique à partir de la relation, et non l'inverse.



1
votes

Vous pouvez suivre les arêtes qui sont transitivement égales ou inégales dans deux dictionnaires respectifs. Pour chaque combinaison d'arêtes, vous pouvez effectuer quelques vérifications simples en temps O (1) pour voir si le calcul serait redondant. Sinon, vous faites le calcul à partir des premiers principes, puis, selon que les arêtes sont égales ou inégales, vous mettez à jour les dictionnaires ci-dessus avec les informations nécessaires. Vous devrez toujours faire des vérifications d'égalité C (n, 2) car c'est le nombre de combinaisons que vous parcourez, mais pour un certain nombre d'entre elles, la décision peut être prise instantanément.

Le dictionnaire equal_edges est plus simple à expliquer, alors commençons par cela. La paire d'arêtes 1-2 est égale, mais comme ni 1 ni 2 n'existent en tant que clés (le dict est vide pour l'instant), nous créons l'ensemble {1, 2} et l'attachons aux deux equal_edges [1] et equal_edges [2] . Nous rencontrons alors la paire d'arêtes égales 1-3. Puisque equal_edges [1] existe maintenant, nous ajoutons 3 à ses nœuds transitivement égaux. Mais comme cet ensemble est partagé entre les deux bords 1 et 2, il est mis à jour aux deux endroits. Il faut aussi maintenant attacher ce même ensemble à equal_edges [3] . Les trois arêtes font référence au même ensemble en mémoire, c'est-à-dire {1, 2, 3} , nous ne dupliquons donc aucune donnée. Maintenant, quand il s'agit de vérifier la paire de bords égaux 2-3, soit 3 dans equal_edges [2] ou 2 dans equal_edges [3] nous permet de contourner tous les calculs lourds.

Pour les unequal_edges , la logique est quelque peu similaire, mais nous devons également nous référer au dictionnaire equal_edges pour les arêtes transitivement inégales. Par exemple, la paire d'arêtes 1-4 est inégale. Mais comme 1 est transitivement égal à 2 et 3, nous devons avoir unequal_edges [4] = equal_edges [1] . Il serait redondant de définir unequal_edges [1] = {4} , ou unequal_edges [2] = {4} , etc. En effet, ces informations peuvent être obtenues à partir de unequal_edges [4] . Cela signifie simplement que pour une paire ab transitivement inégale, nous devons faire une double vérification, c'est-à-dire a dans unequal_edges [b] ou b dans unequal_edges [a] .

1==2 # ok
1==3 # ok
1!=4 # ok
1!=5 # ok
2==3 # redundant
2!=4 # redundant
2!=5 # redundant
3!=4 # redundant
3!=5 # redundant
4==5 # ok


0 commentaires

0
votes

Vous pouvez utiliser 0 pour représenter l'égalité et math.inf pour représenter l'inégalité sous forme de poids d'arête. Ensuite, pour chaque paire de nœuds u, v , vous pouvez calculer la longueur du chemin de u à v et en fonction du résultat, décider si le (lourd) La vérification des nœuds doit être invoquée ou non:

g = nx.Graph()
g.add_nodes_from(range(1, 6))
for u, v in it.combinations(g.nodes, 2):
    try:
        path = nx.shortest_path(g, u, v)
    except nx.NetworkXNoPath:
        new_weight = 0 if func(u, v) else math.inf
    else:
        weights = list(x['weight'] for x in it.starmap(g.get_edge_data, zip(path[:-1], path[1:])))
        if min(weights) == math.inf:
            new_weight = 0 if func(u, v) else math.inf
        elif max(weights) == math.inf:
            new_weight = math.inf
        else:
            new_weight = 0
    g.add_edge(u, v, weight=new_weight)

Si vous n'aimez pas ces arêtes infinies dans votre graphique, vous pouvez soit:

  • Supprimez-les une fois le graphique construit,
  • ou maintenez un graphique final en parallèle avec le graphique à l'infini et à la fin, gardez simplement le dernier.


0 commentaires