portaldacalheta.pt
  • Principal
  • Science Des Données Et Bases De Données
  • Conception Mobile
  • Design De Marque
  • Personnes Et Équipes Produit
Science Des Données Et Bases De Données

Débit maximal et problème d'affectation linéaire



Voici un problème: votre entreprise affecte des sous-traitants pour exécuter les contrats. Vous parcourez vos listes et décidez quels sous-traitants sont disponibles pour un engagement d'un mois et vous parcourez vos contrats disponibles pour voir lesquels d'entre eux sont pour des tâches d'un mois. Étant donné que vous savez avec quelle efficacité chaque entrepreneur peut exécuter chaque contrat, comment attribuez-vous des entrepreneurs pour maximiser l'efficacité globale pour ce mois?

Ceci est un exemple du problème d'affectation, et le problème peut être résolu avec le Algorithme hongrois .



Correspondance bipartite



L'algorithme hongrois (également connu sous le nom d'algorithme de Kuhn-Munkres) est un algorithme de temps polynomial qui maximise la correspondance de poids dans un graphe bipartite pondéré. Ici, les entrepreneurs et les contrats peuvent être modélisés sous forme de graphe bipartite, avec leur efficacité en tant que poids des arêtes entre l'entrepreneur et les nœuds du contrat.



Dans cet article, vous découvrirez une implémentation de l'algorithme hongrois qui utilise le Algorithme d'Edmonds-Karp pour résoudre le problème d'affectation linéaire. Vous apprendrez également comment l'algorithme d'Edmonds-Karp est une légère modification du Ford-Fulkerson méthode et comment cette modification est importante.

Le problème du débit maximal

Le problème de débit maximal lui-même peut être décrit de manière informelle comme le problème du déplacement d'un fluide ou d'un gaz à travers un réseau de tuyaux d'une source unique à un seul terminal. Ceci est fait en supposant que la pression dans le réseau est suffisante pour garantir que le fluide ou le gaz ne peut pas s'attarder dans n'importe quelle longueur de tuyau ou de raccord de tuyau (ces endroits où différentes longueurs de tuyau se rencontrent).



En apportant certaines modifications au graphique, le problème d'affectation peut être transformé en un problème de flux maximal.

Préliminaires

Les idées nécessaires pour résoudre ces problèmes surviennent dans de nombreuses disciplines mathématiques et d'ingénierie, souvent des concepts similaires sont connus sous différents noms et exprimés de différentes manières (par exemple, matrices de contiguïté et listes de contiguïté). Puisque ces idées sont assez ésotériques, des choix sont faits quant à la manière dont ces concepts seront généralement définis pour un contexte donné.



Cet article ne supposera aucune connaissance préalable au-delà d'une petite introduction à la théorie des ensembles.

Les implémentations de cet article représentent les problèmes sous forme de graphes dirigés (digraphe).



Graphiques

À digraphe a deux les attributs , setOfNodes et setOfArcs . Ces deux attributs sont ensembles (collections non ordonnées). Dans les blocs de code de cet article, j'utilise en fait Python frozenset , mais ce détail n’est pas particulièrement important.

DiGraph = namedtuple('DiGraph', ['setOfNodes','setOfArcs'])

(Remarque: ce code et tous les autres codes de cet article sont écrits en Python 3.6.)



Nœuds

À nœud n est composé de deux attributs:

  • n.uid: A unique identifier .



    Cela signifie que pour deux nœuds quelconques x et y,

x.uid != y.uid
  • n.datum: Ceci représente n'importe quel objet de données.
Node = namedtuple('Node', ['uid','datum'])

Les arcs

Une arc a est composé de trois attributs:

  • a.fromNode: Ceci est un nœud , comme défini ci-dessus.

  • a.toNode: Ceci est un nœud , comme défini ci-dessus.

  • a.datum: Ceci représente n'importe quel objet de données.

Arc = namedtuple('Arc', ['fromNode','toNode','datum'])

L'ensemble des arcs dans le digraphe représente une relation binaire sur le nœuds dans le digraphe . L'existence de arc a implique qu'une relation existe entre a.fromNode et a.toNode.

Dans un graphe orienté (par opposition à un graphe non orienté), l'existence d'une relation entre a.fromNode et a.toNode Est-ce que ne pas impliquent qu'une relation similaire entre a.toNode et a.fromNode existe.

En effet, dans un graphe non orienté, la relation exprimée n'est pas nécessairement symétrique.

Graphiques

Nœuds et arcs peut être utilisé pour définir un digraphe , mais pour plus de commodité, dans les algorithmes ci-dessous, un digraphe sera représenté en utilisant comme un dictionnaire .

Voici une méthode qui peut convertir la représentation graphique ci-dessus en une représentation dictionnaire similaire à Celui-ci :

def digraph_to_dict(G): G_as_dict = dict([]) for a in G.setOfArcs: if(a.fromNode not in G.setOfNodes): err_msg = 'There is no Node {a.fromNode.uid!s} to match the Arc from {a.fromNode.uid!s} to {a.toNode.uid!s}'.format(**locals()) pdg(G) raise KeyError(err_msg) if(a.toNode not in G.setOfNodes): err_msg = 'There is no Node {a.toNode.uid!s} to match the Arc from {a.fromNode.uid!s} to {a.toNode.uid!s}'.format(**locals()) pdg(G) raise KeyError(err_msg) G_as_dict[a.fromNode] = (G_as_dict[a.fromNode].union(frozenset([a]))) if (a.fromNode in G_as_dict) else frozenset([a]) for a in G.setOfArcs: if(a.fromNode not in G.setOfNodes): err_msg = 'There is no Node {a.fromNode.uid!s} to match the Arc from {a.fromNode.uid!s} to {a.toNode.uid!s}'.format(**locals()) raise KeyError(err_msg) if a.toNode not in G_as_dict: G_as_dict[a.toNode] = frozenset([]) return G_as_dict

Et en voici une autre qui le convertit en dictionnaire de dictionnaires, une autre opération qui sera utile:

def digraph_to_double_dict(G): G_as_dict = dict([]) for a in G.setOfArcs: if(a.fromNode not in G.setOfNodes): err_msg = 'There is no Node {a.fromNode.uid!s} to match the Arc from {a.fromNode.uid!s} to {a.toNode.uid!s}'.format(**locals()) raise KeyError(err_msg) if(a.toNode not in G.setOfNodes): err_msg = 'There is no Node {a.toNode.uid!s} to match the Arc from {a.fromNode.uid!s} to {a.toNode.uid!s}'.format(**locals()) raise KeyError(err_msg) if(a.fromNode not in G_as_dict): G_as_dict[a.fromNode] = dict({a.toNode : frozenset([a])}) else: if(a.toNode not in G_as_dict[a.fromNode]): G_as_dict[a.fromNode][a.toNode] = frozenset([a]) else: G_as_dict[a.fromNode][a.toNode] = G_as_dict[a.fromNode][a.toNode].union(frozenset([a])) for a in G.setOfArcs: if(a.fromNode not in G.setOfNodes): err_msg = 'There is no Node {a.fromNode.uid!s} to match the Arc from {a.fromNode.uid!s} to {a.toNode.uid!s}'.format(**locals()) raise KeyError(err_msg) if a.toNode not in G_as_dict: G_as_dict[a.toNode] = dict({}) return G_as_dict

Lorsque l'article parle d'un digraphe comme représenté par un dictionnaire, il mentionnera G_as_dict pour s'y référer.

Parfois, il est utile de récupérer un nœud de digraphe G par lui à travers son uid (identifiant unique):

def find_node_by_uid(find_uid, G): nodes = {n for n in G.setOfNodes if n.uid == find_uid} if(len(nodes) != 1): err_msg = 'Node with uid {find_uid!s} is not unique.'.format(**locals()) raise KeyError(err_msg) return nodes.pop()

En définissant des graphiques, les gens utilisent parfois les termes nœud et vertex pour désigner le même concept; la même chose est vraie des termes arc et bord.

Deux représentations graphiques populaires en Python sont Celui-ci qui utilise des dictionnaires et un autre qui utilise des objets pour représenter des graphiques. La représentation dans cet article se situe quelque part entre ces deux représentations couramment utilisées.

C'est mon digraphe représentation. Il y en a beaucoup comme ça, mais celui-ci est le mien.

Promenades et chemins

Soit S_Arcs être un fini séquence (collection ordonnée) de arcs dans un digraphe G tel que si a est un arc dans S_Arcs sauf le dernier, et b suit a dans la séquence, alors il doit y avoir un nœud n = a.fromNode dans G.setOfNodes tel que a.toNode = b.fromNode.

À partir du premier arc dans S_Arcs, et continue jusqu'au dernier arc dans S_Arcs, collecter (dans l'ordre rencontré) tout nœuds n tel que défini ci-dessus entre deux arcs dans S_Arcs. Étiqueter la collection commandée de nœuds collectées lors de cette opération S_{Nodes}.

def path_arcs_to_nodes(s_arcs): s_nodes = list([]) arc_it = iter(s_arcs) step_count = 0 last = None try: at_end = False last = a1 = next(arc_it) while (not at_end): s_nodes += [a1.fromNode] last = a2 = next(arc_it) step_count += 1 if(a1.toNode != a2.fromNode): err_msg = 'Error at index {step_count!s} of Arc sequence.'.format(**locals()) raise ValueError(err_msg) a1 = a2 except StopIteration as e: at_end = True if(last is not None): s_nodes += [last.toNode] return s_nodes
  • Si seulement nœud apparaît plus d'une fois dans la séquence S_Nodes puis appelez S_Arcs une Marche sur digraphe G.

  • Sinon, appelez S_Arcs une chemin de list(S_Nodes)[0] à list(S_Nodes)[-1] sur digraphe G.

Nœud source

Appel nœud s à nœud source dans digraphe G si s est dans G.setOfNodes et G.setOfArcs ne contient pas arc a tel que a.toNode = s.

def source_nodes(G): to_nodes = frozenset({a.toNode for a in G.setOfArcs}) sources = G.setOfNodes.difference(to_nodes) return sources

Nœud terminal

Appel nœud t à nœud terminal dans digraphe G si t est dans G.setOfNodes et G.setOfArcs ne contient pas arc a tel que a.fromNode = t.

def terminal_nodes(G): from_nodes = frozenset({a.fromNode for a in G.setOfArcs}) terminals = G.setOfNodes.difference(from_nodes) return terminals

Coupures et coupures s-t

À Couper cut d'un connecté digraphe G est un sous-ensemble de arcs de G.setOfArcs lequel partitions l'ensemble des nœuds G.setOfNodes dans G. G est connecté si chaque nœud n dans G.setOfNodes et a au moins un arc a dans G.setOfArcs tel que soit n = a.fromNode ou n = a.toNode, mais a.fromNode != a.toNode.

Cut = namedtuple('Cut', ['setOfCutArcs'])

La définition ci-dessus fait référence à un sous-ensemble de arcs , mais il peut également définir une partition du nœuds de G.setOfNodes.

Pour les fonctions predecessors_of et successors_of, n est un nœud en set G.setOfNodes de digraphe G, et cut est un Couper de G:

def cut_predecessors_of(n, cut, G): allowed_arcs = G.setOfArcs.difference(frozenset(cut.setOfCutArcs)) predecessors = frozenset({}) previous_count = len(predecessors) reach_fringe = frozenset({n}) keep_going = True while( keep_going ): reachable_from = frozenset({a.fromNode for a in allowed_arcs if (a.toNode in reach_fringe)}) reach_fringe = reachable_from predecessors = predecessors.union(reach_fringe) current_count = len(predecessors) keep_going = current_count - previous_count > 0 previous_count = current_count return predecessors def cut_successors_of(n, cut, G): allowed_arcs = G.setOfArcs.difference(frozenset(cut.setOfCutArcs)) successors = frozenset({}) previous_count = len(successors) reach_fringe = frozenset({n}) keep_going = True while( keep_going ): reachable_from = frozenset({a.toNode for a in allowed_arcs if (a.fromNode in reach_fringe)}) reach_fringe = reachable_from successors = successors.union(reach_fringe) current_count = len(successors) keep_going = current_count - previous_count > 0 previous_count = current_count return successors

Soit cut être un Couper de digraphe G.

def get_first_part(cut, G): firstPartFringe = frozenset({a.fromNode for a in cut.setOfCutArcs}) firstPart = firstPartFringe for n in firstPart: preds = cut_predecessors_of(n,cut,G) firstPart = firstPart.union(preds) return firstPart def get_second_part(cut, G): secondPartFringe = frozenset({a.toNode for a in cut.setOfCutArcs}) secondPart = secondPartFringe for n in secondPart: succs= cut_successors_of(n,cut,G) secondPart = secondPart.union(succs) return secondPart

cut est un Couper de digraphe G si: (get_first_part(cut, G).union(get_second_part(cut, G)) == G.setOfNodes) and (len(get_first_part(cut, G).intersect(get_second_part(cut, G))) == 0) cut s'appelle un coupe x-y si (x in get_first_part(cut, G)) and (y in get_second_part(cut, G) ) and (x != y). Quand le nœud x dans un coupe x-y cut est un nœud source et nœud y dans le coupe x-y est un nœud terminal , ensuite ceci Couper s'appelle un coupe s-t .

STCut = namedtuple('STCut', ['s','t','cut'])

Réseaux de flux

Vous pouvez utiliser un digraphe G pour représenter un réseau de flux.

Attribuer chacun nœud n, où n est dans G.setOfNodes un n.datum c'est-à-dire un FlowNodeDatum:

FlowNodeDatum = namedtuple('FlowNodeDatum', ['flowIn','flowOut'])

Attribuer chacun arc a, où a est dans G.setOfArcs et a.datum c'est-à-dire un FlowArcDatum.

FlowArcDatum = namedtuple('FlowArcDatum', ['capacity','flow'])

flowNodeDatum.flowIn et flowNodeDatum.flowOut sont positif nombres réels .

flowArcDatum.capacity et flowArcDatum.flow sont également des nombres réels positifs.

Pour chaque nœud nœud n dans G.setOfNodes:

n.datum.flowIn = sum({a.datum.flow for a in G.Arcs if a.toNode == n}) n.datum.flowOut = sum({a.datum.flow for a in G.Arcs if a.fromNode == n})

Digraph G représente maintenant un réseau de flux .

La couler de G fait référence au a.flow pour tous arcs a dans G.

Flux réalisables

Laisser digraphe G représenter un réseau de flux .

La réseau de flux représenté par G a flux réalisables si:

  1. Pour chaque nœud n dans G.setOfNodes à l'exception de nœuds source et nœuds terminaux : n.datum.flowIn = n.datum.flowOut.

  2. Pour chaque arc a dans G.setOfNodes: a.datum.capacity <= a.datum.flow.

La condition 1 est appelée un contrainte de conservation .

La condition 2 est appelée un contrainte de capacité .

Capacité de coupe

La capacité de coupe d'un coupe s-t stCut avec nœud source s et nœud terminal t d'un réseau de flux représenté par un digraphe G est:

def cut_capacity(stCut, G): part_1 = get_first_part(stCut.cut,G) part_2 = get_second_part(stCut.cut,G) s_part = part_1 if stCut.s in part_1 else part_2 t_part = part_1 if stCut.t in part_1 else part_2 cut_capacity = sum({a.datum.capacity for a in stCut.cut.setOfCutArcs if ( (a.fromNode in s_part) and (a.toNode in t_part) )}) return cut_capacity

Coupe de capacité minimale

Soit stCut = stCut(s,t,cut) haricot coupe s-t d'un réseau de flux représenté par un digraphe G.

stCut est le coupe de capacité minimale du réseau de flux représenté par G s'il n'y en a pas d'autre coupe s-t stCutCompetitor dans ce réseau de flux tel que:

cut_capacity(stCut, G)

Dépouiller les flux

Je voudrais me référer au digraphe ce serait le résultat de prendre un digraphe G et en supprimant toutes les données de flux de tous les nœuds dans G.setOfNodes et aussi tous les arcs dans G.setOfArcs.

def strip_flows(G): new_nodes= frozenset( (Node(n.uid, FlowNodeDatum(0.0,0.0)) for n in G.setOfNodes) ) new_arcs = frozenset([]) for a in G.setOfArcs: new_fromNode = Node(a.fromNode.uid, FlowNodeDatum(0.0,0.0)) new_toNode = Node(a.toNode.uid, FlowNodeDatum(0.0,0.0)) new_arc = Arc(new_fromNode, new_toNode, FlowArcDatum(a.datum.capacity, 0.0)) new_arcs = new_arcs.union(frozenset([new_arc])) return DiGraph(new_nodes, new_arcs)

Problème de débit maximum

À réseau de flux représenté comme un digraphe G, a nœud source s dans G.setOfNodes et un nœud terminal t dans G.setOfNodes, G peut représenter un problème de débit maximal si:

(len(list(source_nodes(G))) == 1) and (next(iter(source_nodes(G))) == s) and (len(list(terminal_nodes(G))) == 1) and (next(iter(terminal_nodes(G))) == t)

Nommez cette représentation:

MaxFlowProblemState = namedtuple('MaxFlowProblemState', ['G','sourceNodeUid','terminalNodeUid','maxFlowProblemStateUid'])

Où sourceNodeUid = s.uid, terminalNodeUid = t.uid et maxFlowProblemStateUid est un identifiant pour l'instance de problème.

Solution à débit maximal

Soit maxFlowProblem représenter un problème de débit maximal . La solution à maxFlowProblem peut être représenté par un réseau de flux représenté comme un digraphe H.

Digraph H est un réalisable solution à la problème de débit maximal à l'entrée python maxFlowProblem si:

  1. strip_flows(maxFlowProblem.G) == strip_flows(H).

  2. H est un réseau de flux et a flux réalisables .

Si en plus de 1 et 2:

  1. Il ne peut y en avoir d'autre réseau de flux représenté par digraphe K tel que strip_flows(G) == strip_flows(K) et find_node_by_uid(t.uid,G).flowIn .

Alors H est aussi un optimal solution à maxFlowProblem.

En d'autres termes un solution de débit maximum réalisable peut être représenté par un digraphe , lequel:

  1. Est identique à digraphe G du correspondant problème de débit maximal à l'exception que n.datum.flowIn, n.datum.flowOut et le a.datum.flow de l'un des nœuds et arcs peut être différent.

  2. Représente un réseau de flux qui a flux réalisables .

Et, cela peut représenter un solution de débit maximal optimal si en plus:

  1. Le flowIn pour le nœud correspondant au nœud terminal dans le problème de débit maximal est aussi grand que possible (lorsque les conditions 1 et 2 sont toujours satisfaites).

Si digraphe H représente un solution de débit maximum réalisable : find_node_by_uid(s.uid,H).flowOut = find_node_by_uid(t.uid,H).flowIn cela découle de la débit max, théorème de coupe min (discuté ci-dessous). De manière informelle, puisque H est supposé avoir flux réalisables cela signifie que couler ne peut pas être «créé» (sauf à nœud source s) ni «détruit» (sauf à nœud terminal t) en traversant n'importe quel (autre) nœud ( contraintes de conservation ).

llc s corp contre llc c corp

Depuis un problème de débit maximal ne contient qu'un seul nœud source s et un seul nœud terminal t, tous les flux 'créés' à s doit être «détruit» à t ou la réseau de flux Est-ce que ne pas avoir flux réalisables (la contrainte de conservation aurait été violé).

Laisser digraphe H représenter un solution de débit maximum réalisable ; la valeur ci-dessus est appelée Valeur de débit s-t de H.

Laisser:

mfps=MaxFlowProblemState(H, maxFlowProblem.sourceNodeUid, maxFlowProblem.terminalNodeUid, maxFlowProblem.maxFlowProblemStateUid)

Cela signifie que mfps est un état successeur de maxFlowProblem, ce qui signifie simplement que mfps est exactement comme maxFlowProblem à l'exception que les valeurs de a.flow pour les arcs a dans maxFlowProblem.setOfArcs peut être différent de a.flow pour les arcs a dans mfps.setOfArcs.

def get_mfps_flow(mfps): flow_from_s = find_node_by_uid(mfps.sourceNodeUid,mfps.G).datum.flowOut flow_to_t = find_node_by_uid(mfps.terminalNodeUid,mfps.G).datum.flowIn if( (flow_to_t - flow_from_s) > 0): raise Exception('Infeasible s-t flow') return flow_to_t

Voici une visualisation d'un mfps avec son associé maxFlowProb. Chaque arc a dans l'image a une étiquette, ces étiquettes sont a.datum.flowFrom / a.datum.flowTo, chacune nœud n dans l'image a une étiquette, et ces étiquettes sont n.uid {n.datum.flowIn / a.datum.flowOut}.

Visualisation du débit maximum

Flux de coupe s-t

Soit mfps représentent un MaxFlowProblemState et laissez stCut représenter un Couper de mfps.G. La couper le flux de stCut est défini:

def get_stcut_flow(stCut,mfps): s = find_node_by_uid(mfps.sourceNodeUid, mfps.G) t = find_node_by_uid(mfps.terminalNodeUid, mfps.G) part_1 = get_first_part(stCut.cut,mfps.G) part_2 = get_second_part(stCut.cut,mfps.G) s_part = part_1 if s in part_1 else part_2 t_part = part_1 if t in part_1 else part_2 s_t_sum = sum(a.datum.flow for a in mfps.G.setOfArcs if (a in stCut.cut.setOfCutArcs) and (a.fromNode in s_part) ) t_s_sum = sum(a.datum.flow for a in mfps.G.setOfArcs if (a in stCut.cut.setOfCutArcs) and (a.fromNode in t_part) ) cut_flow = s_t_sum - t_s_sum return cut_flow

débit de coupure s-t est la somme des flux de la partition contenant le nœud source à la partition contenant le nœud terminal moins la somme des flux de la partition contenant le nœud terminal à la partition contenant le nœud source .

Débit maximum, coupe minimum

Soit maxFlowProblem représenter un problème de débit maximal et laissez la solution à maxFlowProblem être représenté par un réseau de flux représenté comme digraphe H.

Soit minStCut Soit le coupe de capacité minimale du réseau de flux représenté par maxFlowProblem.G.

Parce que dans le problème de débit maximal le flux provient d'un seul nœud source et se termine en un seul nœud terminal et, à cause du contraintes de capacité et le contraintes de conservation , nous savons que tout le flux entrant maxFlowProblem.terminalNodeUid doit traverser tout coupe s-t , en particulier il doit traverser minStCut. Ça signifie:

get_flow_value(H, maxFlowProblem) <= cut_capacity(minStCut, maxFlowProblem.G)

Résolution du problème de débit maximal

L'idée de base pour résoudre un problème de débit maximal maxFlowProblem est de commencer par un solution à débit maximal représenté par digraphe H. Un tel point de départ peut être H = strip_flows(maxFlowProblem.G). La tâche consiste alors à utiliser H et par certains glouton modification du a.datum.flow valeurs de certains arcs a dans H.setOfArcs produire un autre solution à débit maximal représenté par digraphe K tel que K ne peut pas encore représenter un réseau de flux avec flux réalisables et get_flow_value(H, maxFlowProblem) . Tant que ce processus se poursuit, la qualité (get_flow_value(K, maxFlowProblem)) du plus récent solution à débit maximal (K) est meilleur que tout autre solution à débit maximal qui a été trouvé. Si le processus atteint un point où il sait qu'aucune autre amélioration n'est possible, le processus peut se terminer et il renverra le optimal solution à débit maximal .

La description ci-dessus est générale et ignore de nombreuses preuves, par exemple si un tel processus est possible ou combien de temps cela peut prendre, je vais donner quelques détails supplémentaires et l'algorithme.

Le débit maximum, théorème de coupe minimum

Du livre Flux dans les réseaux par Ford et Fulkerson , la déclaration du débit max, théorème de coupe min (Théorème 5.1) est:

Pour tout réseau, la valeur du débit maximal de s à t est égale à la capacité de coupe minimale de toutes les coupes séparant s et t.

En utilisant les définitions de cet article, cela se traduit par:

La solution à un maxFlowProblem représenté par un réseau de flux représenté comme digraphe H est optimal si:

get_flow_value(H, maxFlowProblem) == cut_capacity(minStCut, maxFlowProblem.G)

J'aime cette preuve du théorème et Wikipedia a un autre .

La débit max, théorème de coupe min est utilisé pour prouver l'exactitude et l'exhaustivité des Méthode Ford-Fulkerson .

Je vais également donner une preuve du théorème dans la section après augmenter les chemins .

La méthode Ford-Fulkerson et l'algorithme Edmonds-Karp

CLRS définit la méthode Ford-Fulkerson comme ceci (section 26.2):

FORD-FULKERSON-METHOD(G, s, t): initialize flow f to 0 while there exists an augmenting path p in the residual graph G_f augment flow f along

Graphique résiduel

La Graphique résiduel d'un réseau de flux représenté comme le digraphe G peut être représenté comme un digraphe G_f:

ResidualDatum = namedtuple('ResidualDatum', ['residualCapacity','action']) def agg_n_to_u_cap(n,u,G_as_dict): arcs_out = G_as_dict[n] return sum([a.datum.capacity for a in arcs_out if( (a.fromNode == n) and (a.toNode == u) ) ]) def agg_n_to_u_flow(n,u,G_as_dict): arcs_out = G_as_dict[n] return sum([a.datum.flow for a in arcs_out if( (a.fromNode == n) and (a.toNode == u) ) ]) def get_residual_graph_of(G): G_as_dict = digraph_to_dict(G) residual_nodes = G.setOfNodes residual_arcs = frozenset([]) for n in G_as_dict: arcs_from = G_as_dict[n] nodes_to = frozenset([find_node_by_uid(a.toNode.uid,G) for a in arcs_from]) for u in nodes_to: n_to_u_cap_sum = agg_n_to_u_cap(n,u,G_as_dict) n_to_u_flow_sum = agg_n_to_u_flow(n,u,G_as_dict) if(n_to_u_cap_sum > n_to_u_flow_sum): flow = round(n_to_u_cap_sum - n_to_u_flow_sum, TOL) residual_arcs = residual_arcs.union( frozenset([Arc(n,u,datum=ResidualDatum(flow, 'push'))]) ) if(n_to_u_flow_sum > 0.0): flow = round(n_to_u_flow_sum, TOL) residual_arcs = residual_arcs.union( frozenset([Arc(u,n,datum=ResidualDatum(flow, 'pull'))]) ) return DiGraph(residual_nodes, residual_arcs)
  • agg_n_to_u_cap(n,u,G_as_dict) renvoie la somme de a.datum.capacity pour tous arcs dans le sous-ensemble de G.setOfArcs où arc a est dans le sous-ensemble si a.fromNode = n et a.toNode = u.

  • agg_n_to_u_cap(n,u,G_as_dict) renvoie la somme de a.datum.flow pour tous arcs dans le sous-ensemble de G.setOfArcs où arc a est dans le sous-ensemble si a.fromNode = n et a.toNode = u.

En bref, le graphe résiduel G_f représente certaines actions qui peuvent être effectuées sur le digraphe G.

Chaque paire de nœuds n,u dans G.setOfNodes du réseau de flux représenté par digraphe G peut générer 0, 1 ou 2 arcs dans le graphe résiduel G_f de G.

  1. La paire n,u ne génère aucun arcs dans G_f si il n'y a pas arc a dans G.setOfArcs tel que a.fromNode = n et a.toNode = u.

  2. La paire n,u génère le arc a dans G_f.setOfArcs où a représente un arc étiqueté un arc à flux poussé de n à u a = Arc(n,u,datum=ResidualNode(n_to_u_cap_sum - n_to_u_flow_sum)) si n_to_u_cap_sum > n_to_u_flow_sum.

  3. La paire n,u génère le arc a dans G_f.setOfArcs où a représente un arc étiqueté un arc de flux de traction de n à u a = Arc(n,u,datum=ResidualNode(n_to_u_cap_sum - n_to_u_flow_sum)) si n_to_u_flow_sum > 0.0.

  • Chaque arc à flux poussé dans G_f.setOfArcs représente l'action d'ajouter un total de x <= n_to_u_cap_sum - n_to_u_flow_sum flux vers arcs dans le sous-ensemble de G.setOfArcs où arc a est dans le sous-ensemble si a.fromNode = n et a.toNode = u.

  • Chaque arc de flux de traction dans G_f.setOfArcs représente l'action de soustraire un total de x <= n_to_u_flow_sum flux vers arcs dans le sous-ensemble de G.setOfArcs où arc a est dans le sous-ensemble si a.fromNode = n et a.toNode = u.

Performer un individu poussez ou tirez action de G_f sur le applicable arcs dans G pourrait générer un réseau de flux sans pour autant flux réalisables parce que le contraintes de capacité ou la contraintes de conservation pourrait être violé dans le réseau de flux .

Voici une visualisation du graphe résiduel de l'exemple précédent de visualisation d'un solution à débit maximal , dans la visualisation chacun arc a représente a.residualCapacity.

Visualisation du débit maximum (résiduel)

Augmenter le chemin

Soit maxFlowProblem être un problème de débit maximum , et laissez G_f = get_residual_graph_of(G) Soit le graphe résiduel de maxFlowProblem.G.

Une chemin augmentant augmentingPath pour maxFlowProblem est un chemin de find_node_by_uid(maxFlowProblem.sourceNode,G_f) à find_node_by_uid(maxFlowProblem.terminalNode,G_f).

Il s'avère qu'une augmentation chemin augmentingPath peut être appliqué à un solution de débit max représenté par digraphe H générer un autre solution de débit max représenté par digraphe K où get_flow_value(H, maxFlowProblem) si H n'est pas optimal .

Voici comment:

def augment(augmentingPath, H): augmentingPath = list(augmentingPath) H_as_dict = digraph_to_dict(H) new_nodes = frozenset({}) new_arcs = frozenset({}) visited_nodes = frozenset({}) visited_arcs = frozenset({}) bottleneck_residualCapacity = min( augmentingPath, key=lambda a: a.datum.residualCapacity ).datum.residualCapacity for x in augmentingPath: from_node_uid = x.fromNode.uid if x.datum.action == 'push' else x.toNode.uid to_node_uid = x.toNode.uid if x.datum.action == 'push' else x.fromNode.uid from_node = find_node_by_uid( from_node_uid, H ) to_node = find_node_by_uid( to_node_uid, H ) load = bottleneck_residualCapacity if x.datum.action == 'push' else -bottleneck_residualCapacity for a in H_as_dict[from_node]: if(a.toNode == to_node): test_sum = a.datum.flow + load new_flow = a.datum.flow new_from_node_flow_out = a.fromNode.datum.flowOut new_to_node_flow_in = a.toNode.datum.flowIn new_from_look = {n for n in new_nodes if (n.uid == a.fromNode.uid)} new_to_look = {n for n in new_nodes if (n.uid == a.toNode.uid) } prev_from_node = new_from_look.pop() if (len(new_from_look)>0) else a.fromNode prev_to_node = new_to_look.pop() if (len(new_to_look)>0) else a.toNode new_nodes = new_nodes.difference(frozenset({prev_from_node})) new_nodes = new_nodes.difference(frozenset({prev_to_node})) if(test_sum > a.datum.capacity): new_flow = a.datum.capacity new_from_node_flow_out = prev_from_node.datum.flowOut - a.datum.flow + a.datum.capacity new_to_node_flow_in = prev_to_node.datum.flowIn - a.datum.flow + a.datum.capacity load = test_sum - a.datum.capacity elif(test_sum <0.0): new_flow = 0.0 new_from_node_flow_out = prev_from_node.datum.flowOut - a.datum.flow new_to_node_flow_in = prev_to_node.datum.flowIn - a.datum.flow load = test_sum else: new_flow = test_sum new_from_node_flow_out = prev_from_node.datum.flowOut - a.datum.flow + new_flow new_to_node_flow_in = prev_to_node.datum.flowIn - a.datum.flow + new_flow load = 0.0 new_from_node_flow_out = round(new_from_node_flow_out, TOL) new_to_node_flow_in = round(new_to_node_flow_in, TOL) new_flow = round(new_flow, TOL) new_from_node = Node(prev_from_node.uid, FlowNodeDatum(prev_from_node.datum.flowIn, new_from_node_flow_out)) new_to_node = Node(prev_to_node.uid, FlowNodeDatum(new_to_node_flow_in, prev_to_node.datum.flowOut)) new_arc = Arc(new_from_node, new_to_node, FlowArcDatum(a.datum.capacity, new_flow)) visited_nodes = visited_nodes.union(frozenset({a.fromNode,a.toNode})) visited_arcs = visited_arcs.union(frozenset({a})) new_nodes = new_nodes.union(frozenset({new_from_node, new_to_node})) new_arcs = new_arcs.union(frozenset({new_arc})) not_visited_nodes = H.setOfNodes.difference(visited_nodes) not_visited_arcs = H.setOfArcs.difference(visited_arcs) full_new_nodes = new_nodes.union(not_visited_nodes) full_new_arcs = new_arcs.union(not_visited_arcs) G = DiGraph(full_new_nodes, full_new_arcs) full_new_arcs_update = frozenset([]) for a in full_new_arcs: new_from_node = a.fromNode new_to_node = a.toNode new_from_node = find_node_by_uid( a.fromNode.uid, G ) new_to_node = find_node_by_uid( a.toNode.uid, G ) full_new_arcs_update = full_new_arcs_update.union( {Arc(new_from_node, new_to_node, FlowArcDatum(a.datum.capacity, a.datum.flow))} ) G = DiGraph(full_new_nodes, full_new_arcs_update) return G

Dans ce qui précède, TOL est une valeur de tolérance pour arrondi les valeurs de débit dans le réseau. C'est pour éviter la cascade imprécision des calculs en virgule flottante . Ainsi, par exemple, j'ai utilisé TOL = 10 pour signifier arrondir à 10 chiffres significatifs .

Soit K = augment(augmentingPath, H), alors K représente un solution de débit maximum réalisable pour maxFlowProblem. Pour que la déclaration soit vraie, le réseau de flux représenté par K doit avoir flux réalisables (ne pas violer le contrainte de capacité ou la contrainte de conservation .

Voici pourquoi: dans la méthode ci-dessus, chaque nœud ajouté au nouveau réseau de flux représenté par digraphe K est soit une copie exacte d'un nœud de digraphe H ou un nœud n qui a eu le même numéro ajouté à son n.datum.flowIn comme son n.datum.flowOut. Cela signifie que le contrainte de conservation est satisfait dans K tant qu'il était satisfait en H. La contrainte de conservation est satisfait car nous vérifions explicitement que tout nouveau arc a dans le réseau a a.datum.flow <= a.datum.capacity; ainsi, tant que le arcs de l'ensemble H.setOfArcs qui ont été copiés sans modifications dans K.setOfArcs ne violez pas le contrainte de capacité , alors K ne viole pas le contrainte de capacité .

Il est également vrai que get_flow_value(H, maxFlowProblem) si H n'est pas optimal .

Voici pourquoi: pour un chemin augmentant augmentingPath d'exister dans le digraphe représentation de la graphe résiduel G_f d'un problème de débit maximum maxFlowProblem puis le dernier arc a sur augmentingPath doit être une «poussée» arc et il doit avoir a.toNode == maxFlowProblem.terminalNode. Une chemin augmentant est défini comme celui qui se termine au nœud terminal du problème de débit maximum pour lequel c'est un chemin augmentant . D'après la définition du graphe résiduel , il est clair que le dernier arc dans un chemin augmentant sur ça graphe résiduel doit être une «poussée» arc parce que tout 'pull' arc b dans le chemin augmentant aura b.toNode == maxFlowProblem.terminalNode et b.fromNode != maxFlowProblem.terminalNode de la définition de chemin . De plus, d'après la définition de chemin , il est clair que le nœud terminal n'est modifié qu'une seule fois par le augment méthode. Ainsi augment modifie maxFlowProblem.terminalNode.flowIn exactement une fois et cela augmente la valeur de maxFlowProblem.terminalNode.flowIn parce que le dernier arc dans le augmentingPath doit être le arc qui provoque la modification de maxFlowProblem.terminalNode.flowIn pendant augment. D'après la définition de augment comme il s’applique à «pousser» arcs , le maxFlowProblem.terminalNode.flowIn peut seulement être augmenté, pas diminué.

Quelques preuves de Sedgewick et Wayne

Le livre Algorithmes, quatrième édition par Robert Sedgewich et Kevin Wayne a des preuves merveilleuses et courtes (pages 892-894) qui seront utiles. Je vais les recréer ici, même si j'utiliserai un langage adapté aux définitions précédentes. Mes étiquettes pour les épreuves sont les mêmes que dans le livre de Sedgewick.

Proposition E: Pour toute digraphe H représentant un solution de débit maximum réalisable à un problème de débit maximal maxFlowProblem, pour tout stCut get_stcut_flow(stCut,H,maxFlowProblem) = get_flow_value(H, maxFlowProblem).

Preuve: Soit stCut=stCut(maxFlowProblem.sourceNode,maxFlowProblem.terminalNode,set([a for a in H.setOfArcs if a.toNode == maxFlowProblem.terminalNode])). Proposition E vaut pour stCut directement à partir de la définition de valeur de débit s-t . Supposons que là nous souhaitons déplacer certains nœud n de la s-partition (get_first_part(stCut.cut, G)) et dans la t-partition (get_second_part(stCut.cut, G)), pour ce faire, nous devons changer stCut.cut, ce qui pourrait changer stcut_flow = get_stcut_flow(stCut,H,maxFlowProblem) et invalider proposition E . Cependant, voyons comment la valeur de stcut_flow va changer à mesure que nous apportons ce changement. nœud n est à l'équilibre, ce qui signifie que la somme des flux dans nœud n est égal à la somme des flux qui en sortent (cela est nécessaire pour que H représente un solution réalisable ). Notez que tous les flux qui font partie du stcut_flow entrer nœud n y entre à partir de la s-partition (flux entrant nœud n à partir de la partition t, directement ou indirectement, n'aurait pas été compté dans le stcut_flow valeur car il se dirige dans la mauvaise direction en fonction de la définition). De plus, tout le flux sortant n finira par couler (directement ou indirectement) dans le nœud terminal (prouvé plus tôt). Quand on bouge nœud n dans la partition t, tout le flux entrant n de la s-partition doit être ajouté au nouveau stcut_flow valeur; cependant, tout le flux sortant n doit être soustrait du nouveau stcut_flow valeur; la partie du flux se dirigeant directement vers la partition t est soustraite car ce flux est maintenant interne à la nouvelle partition t et n'est pas compté comme stcut_flow. La partie du flux de n se diriger vers nœuds dans la s-partition doit également être soustrait de stcut_flow: Après n est déplacé dans la partition t, ces flux seront dirigés de la partition t vers la partition s et ne doivent donc pas être pris en compte dans le stcut_flow, puisque ces flux sont supprimés de l'entrée dans le s- et doit être réduit de la somme de ces flux, et le flux sortant de la partition s vers la partition t (où tous les flux de st doivent aboutir) doit être réduit du même montant. Comme nœud n était à l'équilibre avant le processus, la mise à jour aura ajouté la même valeur au nouveau stcut_flow valeur comme il a soustrait laissant ainsi proposition E vrai après la mise à jour. La validité de proposition E découle alors de l'induction sur la taille de la partition t.

Voici quelques exemples réseaux de flux pour aider à visualiser les cas les moins évidents où proposition E tient; dans l'image, les zones rouges indiquent la partition s, les zones bleues représentent la partition t et le vert arcs indiquer un coupe s-t . Dans la deuxième image, circulez entre nœud A et nœud B augmente tandis que le débit dans nœud terminal t ne change pas:

Corollaire: Non débit de coupure s-t la valeur peut dépasser la capacité de tout coupe s-t .

Proposition F. (débit max, théorème de coupe min): Soit f haricot flux s-t . Les 3 conditions suivantes sont équivalentes:

  1. Il existe un coupe s-t dont la capacité est égale à la valeur du débit f.

  2. f est un débit max .

  3. Il n'y a pas chemin augmentant par rapport à f.

La condition 1 implique la condition 2 par le corollaire. La condition 2 implique la condition 3 car l'existence d'un chemin augmentant implique l'existence d'un flux avec des valeurs plus grandes, contredisant la maximalité de f. La condition 3 implique la condition 1: Soit C_s être l'ensemble de tous nœuds accessible depuis s avec un chemin augmentant dans le graphe résiduel . Soit C_t être le reste arcs , alors t doit être dans C_t (par notre hypothèse). La arcs passage de C_s à C_t puis formez un coupe s-t qui ne contient que arcs a où soit a.datum.capacity = a.datum.flow ou a.datum.flow = 0.0. S'il en était autrement, le nœuds connecté par un arc avec capacité résiduelle restante à C_s serait dans l'ensemble C_s puisqu'il y aurait alors un chemin augmentant de s à un tel nœud . Le flux à travers le coupe s-t est égal au coupe s-t capacité (depuis arcs de C_s à C_t ont un débit égal à la capacité) et aussi à la valeur du flux s-t (par proposition E ).

Cette déclaration de la débit max, théorème de coupe min implique la déclaration précédente de Flux dans les réseaux .

Corollaire (propriété d'intégralité): Lorsque les capacités sont des nombres entiers, il existe un flux maximal à valeur entière et l'algorithme de Ford-Fulkerson le trouve.

Preuve: Chacun chemin augmentant augmente le débit d’un entier positif, le minimum des capacités inutilisées dans le «push» arcs et les flux dans le «pull» arcs , qui sont toujours des entiers positifs.

Cela justifie le Méthode Ford-Fulkerson description de CLRS . La méthode consiste à continuer à trouver augmenter les chemins et appliquer augment au plus tard maxFlowSolution trouver de meilleures solutions, jusqu'à ce qu'il n'y en ait plus chemin augmentant ce qui signifie que le dernier solution à débit maximal est optimal .

De Ford-Fulkerson à Edmonds-Karp

Les questions restantes concernant la résolution problèmes de débit maximal sont:

  1. Comment doit augmenter les chemins être construit?

  2. La méthode se terminera-t-elle si nous utilisons des nombres réels et non des entiers?

  3. Combien de temps faudra-t-il pour se terminer (si c'est le cas)?

La Algorithme d'Edmonds-Karp précise que chaque chemin augmentant est construit par un largeur de la première recherche ( BFS ) du graphe résiduel ; il s'avère que cette décision du point 1 ci-dessus forcera également l'algorithme à se terminer (point 2) et permettra au complexité asymptotique du temps et de l'espace être déterminé.

Tout d'abord, voici un BFS la mise en oeuvre:

def bfs(sourceNode, terminalNode, G_f): G_f_as_dict = digraph_to_dict(G_f) parent_arcs = dict([]) visited = frozenset([]) deq = deque([sourceNode]) while len(deq) > 0: curr = deq.popleft() if curr == terminalNode: break for a in G_f_as_dict.get(curr): if (a.toNode not in visited): visited = visited.union(frozenset([a.toNode])) parent_arcs[a.toNode] = a deq.extend([a.toNode]) path = list([]) curr = terminalNode while(curr != sourceNode): if (curr not in parent_arcs): err_msg = 'No augmenting path from {} to {}.'.format(sourceNode.uid, terminalNode.uid) raise StopIteration(err_msg) path.extend([parent_arcs[curr]]) curr = parent_arcs[curr].fromNode path.reverse() test = deque([path[0].fromNode]) for a in path: if(test[-1] != a.fromNode): err_msg = 'Bad path: {}'.format(path) raise ValueError(err_msg) test.extend([a.toNode]) return path

J'ai utilisé un deque du module de collections python .

Pour répondre à la question 2 ci-dessus, je paraphraserai une autre preuve tirée de Sedgewick et Wayne : Proposition G. Le nombre de augmenter les chemins nécessaire dans le Edmonds-Karp algorithme avec N nœuds et A arcs est au plus NA/2. Preuve: chaque chemin augmentant a un goulot arc - une arc qui est supprimé du graphe résiduel car cela correspond soit à un «push» arc qui devient rempli à pleine capacité ou un «pull» arc à travers lequel le flux devient 0. Chaque fois qu'un arc devient un goulot d'étranglement arc , la longueur de tout chemin augmentant à travers elle doit être multipliée par 2. En effet, chaque nœud dans un chemin peut apparaître une seule fois ou pas du tout (d'après la définition de chemin ) depuis le chemins sont explorés du plus court chemin au plus long, cela signifie qu'au moins un nœud doit être visité par le prochain chemin qui traverse le goulot d'étranglement particulier nœud cela signifie 2 supplémentaires arcs sur le chemin avant d'arriver au nœud . Depuis le chemin augmentant est de longueur au plus N chaque arc peut être allumé au plus N/2 augmenter les chemins , et le nombre total de augmenter les chemins est au plus NA/2.

La Algorithme d'Edmonds-Karp s'exécute dans O(NA^2). Si au plus NA/2 chemins seront explorés pendant l'algorithme et en explorant chaque chemin avec BFS est N+A alors le terme le plus significatif du produit et donc la complexité asymptotique est O(NA^2).

Soit mfp être un maxFlowProblemState.

def edmonds_karp(mfp): sid, tid = mfp.sourceNodeUid, mfp.terminalNodeUid no_more_paths = False loop_count = 0 while(not no_more_paths): residual_digraph = get_residual_graph_of(mfp.G) try: asource = find_node_by_uid(mfp.sourceNodeUid, residual_digraph) aterm = find_node_by_uid(mfp.terminalNodeUid, residual_digraph) apath = bfs(asource, aterm, residual_digraph) paint_mfp_path(mfp, loop_count, apath) G = augment(apath, mfp.G) s = find_node_by_uid(sid, G) t = find_node_by_uid(tid, G) mfp = MaxFlowProblemState(G, s.uid, t.uid, mfp.maxFlowProblemStateUid) loop_count += 1 except StopIteration as e: no_more_paths = True return mfp

La version ci-dessus est inefficace et a une complexité pire que O(NA^2) car il construit un nouveau solution à débit maximal et un nouveau graphe résiduel à chaque fois (plutôt que de modifier digraphes à mesure que l'algorithme avance). Pour arriver à un vrai O(NA^2) solution l'algorithme doit maintenir à la fois le digraphe représentant le état de problème de débit maximal et ses associés graphe résiduel . L'algorithme doit donc éviter d'itérer arcs et nœuds inutilement et mettre à jour leurs valeurs et les valeurs associées dans le graphe résiduel seulement si nécessaire.

Pour écrire un plus vite Edmonds Karp algorithme, j'ai réécrit plusieurs morceaux de code ci-dessus. J'espère qu'en parcourant le code qui a généré un nouveau digraphe a été utile pour comprendre ce qui se passe. Dans l’algorithme rapide, j’utilise de nouvelles astuces et des structures de données Python que je ne veux pas détailler. Je dirai que a.fromNode et a.toNode sont désormais traités comme des chaînes et des uids pour nœuds . Pour ce code, laissez mfps être un maxFlowProblemState

import uuid def fast_get_mfps_flow(mfps): flow_from_s = {n for n in mfps.G.setOfNodes if n.uid == mfps.sourceNodeUid}.pop().datum.flowOut flow_to_t = {n for n in mfps.G.setOfNodes if n.uid == mfps.terminalNodeUid}.pop().datum.flowIn if( (flow_to_t - flow_from_s) > 0.00): raise Exception('Infeasible s-t flow') return flow_to_t def fast_e_k_preprocess(G): G = strip_flows(G) get = dict({}) get['nodes'] = dict({}) get['node_to_node_capacity'] = dict({}) get['node_to_node_flow'] = dict({}) get['arcs'] = dict({}) get['residual_arcs'] = dict({}) for a in G.setOfArcs: if(a.fromNode not in G.setOfNodes): err_msg = 'There is no Node {a.fromNode.uid!s} to match the Arc from {a.fromNode.uid!s} to {a.toNode.uid!s}'.format(**locals()) raise KeyError(err_msg) if(a.toNode not in G.setOfNodes): err_msg = 'There is no Node {a.toNode.uid!s} to match the Arc from {a.fromNode.uid!s} to {a.toNode.uid!s}'.format(**locals()) raise KeyError(err_msg) get['nodes'][a.fromNode.uid] = a.fromNode get['nodes'][a.toNode.uid] = a.toNode lark = Arc(a.fromNode.uid, a.toNode.uid, FlowArcDatumWithUid(a.datum.capacity, a.datum.flow, uuid.uuid4())) if(a.fromNode.uid not in get['arcs']): get['arcs'][a.fromNode.uid] = dict({a.toNode.uid : dict({lark.datum.uid : lark})}) else: if(a.toNode.uid not in get['arcs'][a.fromNode.uid]): get['arcs'][a.fromNode.uid][a.toNode.uid] = dict({lark.datum.uid : lark}) else: get['arcs'][a.fromNode.uid][a.toNode.uid][lark.datum.uid] = lark for a in G.setOfArcs: if a.toNode.uid not in get['arcs']: get['arcs'][a.toNode.uid] = dict({}) for n in get['nodes']: get['residual_arcs'][n] = dict() get['node_to_node_capacity'][n] = dict() get['node_to_node_flow'][n] = dict() for u in get['nodes']: n_to_u_cap_sum = sum(a.datum.capacity for a in G.setOfArcs if (a.fromNode.uid == n) and (a.toNode.uid == u) ) n_to_u_flow_sum = sum(a.datum.flow for a in G.setOfArcs if (a.fromNode.uid == n) and (a.toNode.uid == u) ) if(n_to_u_cap_sum > n_to_u_flow_sum): flow = round(n_to_u_cap_sum - n_to_u_flow_sum, TOL) get['residual_arcs'][n][u] = Arc(n,u,ResidualDatum(flow, 'push')) if(n_to_u_flow_sum > 0.0): flow = round(n_to_u_flow_sum, TOL) get['residual_arcs'][u][n] = Arc(u,n,ResidualDatum(flow, 'pull')) get['node_to_node_capacity'][n][u] = n_to_u_cap_sum get['node_to_node_flow'][n][u] = n_to_u_flow_sum return get def fast_bfs(sid, tid, get): parent_of = dict([]) visited = frozenset([]) deq = coll.deque([sid]) while len(deq) > 0: n = deq.popleft() if n == tid: break for u in get['residual_arcs'][n]: if (u not in visited): visited = visited.union(frozenset({u})) parent_of[u] = get['residual_arcs'][n][u] deq.extend([u]) path = list([]) curr = tid while(curr != sid): if (curr not in parent_of): err_msg = 'No augmenting path from {} to {}.'.format(sid, curr) raise StopIteration(err_msg) path.extend([parent_of[curr]]) curr = parent_of[curr].fromNode path.reverse() return path def fast_edmonds_karp(mfps): sid, tid = mfps.sourceNodeUid, mfps.terminalNodeUid get = fast_e_k_preprocess(mfps.G) no_more_paths, loop_count = False, 0 while(not no_more_paths): try: apath = fast_bfs(sid, tid, get) get = fast_augment(apath, get) loop_count += 1 except StopIteration as e: no_more_paths = True nodes = frozenset(get['nodes'].values()) arcs = frozenset({}) for from_node in get['arcs']: for to_node in get['arcs'][from_node]: for arc in get['arcs'][from_node][to_node]: arcs |= frozenset({get['arcs'][from_node][to_node][arc]}) G = DiGraph(nodes, arcs) mfps = MaxFlowProblemState(G, sourceNodeUid=sid, terminalNodeUid=tid, maxFlowProblemStateUid=mfps.maxFlowProblemStateUid) return mfps

Voici une visualisation de la façon dont cet algorithme résout l'exemple réseau de flux d'en haut. La visualisation montre les étapes telles qu'elles sont reflétées dans le digraphe G représentant les plus à jour réseau de flux et comme ils se reflètent dans le graphe résiduel de ce réseau de flux. Augmenter les chemins dans le graphe résiduel sont représentés par des chemins rouges et les digraphe représentant le problème l'ensemble des nœuds et arcs affecté par une donnée chemin augmentant est surligné en vert. Dans chaque cas, je vais mettre en évidence les parties du graphique qui seront modifiées (en rouge ou en vert), puis afficher le graphique après les modifications (juste en noir).

Visualisation du débit maximum

Visualisation du débit maximum (résiduel)

Voici une autre visualisation de la façon dont cet algorithme résout un exemple différent réseau de flux . Notez que celui-ci utilise des nombres réels et contient plusieurs arcs avec le même fromNode et toNode valeurs.

qu'est-ce que le bootstrap dans la conception de sites Web

** Notez également que comme les arcs avec un ResidualDatum «pull» peuvent faire partie du chemin d’augmentation, les nœuds affectés dans le DiGraph du réseau flottant _peuvent ne pas être sur un chemin dans G!.

Graphiques bipartites

Supposons que nous ayons un digraphe G, G est bipartite s'il est possible de partitionner le nœuds dans G.setOfNodes en deux ensembles (part_1 et part_2) tels que pour tout arc a dans G.setOfArcs il Ne peut pas être vrai que a.fromNode dans part_1 et a.toNode dans part_1. Il aussi ne peut pas être vrai que a.fromNode dans part_2 et a.toNode dans part_2.

En d'autres termes G est bipartite s'il peut être partitionné en deux ensembles de nœuds tel que chaque arc doit connecter un nœud dans un ensemble à un nœud dans l'autre ensemble.

Test bipartite

Supposons que nous ayons un digraphe G, nous voulons tester si c'est bipartite . Nous pouvons le faire dans O(|G.setOfNodes|+|G.setOfArcs|) par une coloration gourmande du graphe en deux couleurs.

Premièrement, nous devons générer un nouveau digraphe H. Ce graphique aura le même ensemble de nœuds comme G, mais il en aura plus arcs que G. Chaque arc a dans G créera 2 arcs dans H; la première arc sera identique à a, et le second arc renverse le directeur de a (b = Arc(a.toNode,a.fromNode,a.datum)).

Bipartition = coll.namedtuple('Bipartition',['firstPart', 'secondPart', 'G']) def bipartition(G): nodes = frozenset(G.setOfNodes arcs = frozenset(G.setOfArcs) arcs = arcs.union( frozenset( {Arc(a.toNode,a.fromNode,a.datum) for a in G.setOfArcs} ) ) H = DiGraph(nodes, arcs) H_as_dict = digraph_to_dict(H) color = dict([]) some_node = next(iter(H.setOfNodes)) deq = coll.deque([some_node]) color[some_node] = -1 while len(deq) > 0: curr = deq.popleft() for a in H_as_dict.get(curr): if (a.toNode not in color): color[a.toNode] = -1*color[curr] deq.extend([a.toNode]) elif(color[curr] == color[a.toNode]): print(curr,a.toNode) raise Exception('Not Bipartite.') firstPart = frozenset( {n for n in color if color[n] == -1 } ) secondPart = frozenset( {n for n in color if color[n] == 1 } ) if( firstPart.union(secondPart) != G.setOfNodes ): raise Exception('Not Bipartite.') return Bipartition(firstPart, secondPart, G)

Correspondances et correspondances maximales

Supposons que nous ayons un digraphe G et matching est un sous-ensemble de arcs à partir de G.setOfArcs. matching est un correspondant à si pour deux arcs a et b dans matching: len(frozenset( {a.fromNode} ).union( {a.toNode} ).union( {b.fromNode} ).union( {b.toNode} )) == 4. En d'autres termes, pas deux arcs dans un correspondant à Partagez un nœud .

Correspondant à matching, est une correspondance maximale s'il n'y en a pas d'autre correspondant à alt_matching dans G tel que len(matching) . En d'autres termes, matching est un correspondance maximale s'il s'agit du plus grand ensemble de arcs de G.setOfArcs qui satisfait toujours la définition de correspondant à (l'ajout de tout arc pas déjà dans la correspondance cassera le correspondant à définition).

À correspondance maximale matching est un correspondance parfaite si tout pour nœud n dans G.setOfArcs il existe un arc a dans matching où a.fromNode == n or a.toNode == n.

Correspondance bipartite maximale

À correspondance bipartite maximale est un correspondance maximale dix un digraphe G lequel est bipartite .

Étant donné que G est bipartite , le problème de trouver un correspondance bipartite maximale peut être transformé en un problème de débit maximal résoluble avec le Edmonds-Karp l'algorithme puis le correspondance bipartite maximale peuvent être récupérés de la solution au problème de débit maximal .

Soit bipartition être un bipartition de G.

Pour ce faire, je dois générer un nouveau digraphe (H) avec de nouveaux nœuds (H.setOfNodes) et de nouveaux arcs (H.setOfArcs). H.setOfNodes contient tous les nœuds dans G.setOfNodes et deux de plus hochement de tête , s (à nœud source ) et t (une nœud terminal ).

H.setOfArcs contiendra un arc pour chaque G.setOfArcs. Si un arc a est dans G.setOfArcs et a.fromNode est dans bipartition.firstPart et a.toNode est dans bipartition.secondPart puis incluez a dans H (ajout d'un FlowArcDatum(1,0)).

Si a.fromNode est dans bipartition.secondPart et a.toNode est dans bipartition.firstPart, puis incluez Arc(a.toNode,a.fromNode,FlowArcDatum(1,0)) dans H.setOfArcs.

La définition d'un bipartite graphique garantit que non arc connecte tout nœuds où les deux nœuds sont dans la même partition. H.setOfArcs contient également un arc de nœud s pour chaque nœud dans bipartition.firstPart. Enfin, H.setOfArcs contient un arc chaque nœud dans bipartition.secondPart à nœud t. a.datum.capacity = 1 pour tous a dans H.setOfArcs.

Partitionnez d'abord le nœuds dans G.setOfNodes les deux disjoint définit (part1 et part2) tels que non arc dans G.setOfArcs est dirigé d'un ensemble vers le même ensemble (cette partition est possible car G est bipartite ). Ensuite, ajoutez tout arcs dans G.setOfArcs qui sont dirigés depuis part1 à part2 dans H.setOfArcs. Puis créez un single nœud source s et un seul nœud terminal t et créez-en plus arcs

Ensuite, construisez un maxFlowProblemState.

def solve_mbm( bipartition ): s = Node(uuid.uuid4(), FlowNodeDatum(0,0)) t = Node(uuid.uuid4(), FlowNodeDatum(0,0)) translate = {} arcs = frozenset([]) for a in bipartition.G.setOfArcs: if ( (a.fromNode in bipartition.firstPart) and (a.toNode in bipartition.secondPart) ): fark = Arc(a.fromNode,a.toNode,FlowArcDatum(1,0)) arcs = arcs.union({fark}) translate[frozenset({a.fromNode.uid,a.toNode.uid})] = a elif ( (a.toNode in bipartition.firstPart) and (a.fromNode in bipartition.secondPart) ): bark = Arc(a.toNode,a.fromNode,FlowArcDatum(1,0)) arcs = arcs.union({bark}) translate[frozenset({a.fromNode.uid,a.toNode.uid})] = a arcs1 = frozenset( {Arc(s,n,FlowArcDatum(1,0)) for n in bipartition.firstPart } ) arcs2 = frozenset( {Arc(n,t,FlowArcDatum(1,0)) for n in bipartition.secondPart } ) arcs = arcs.union(arcs1.union(arcs2)) nodes = frozenset( {Node(n.uid,FlowNodeDatum(0,0)) for n in bipartition.G.setOfNodes} ).union({s}).union({t}) G = DiGraph(nodes, arcs) mfp = MaxFlowProblemState(G, s.uid, t.uid, 'bipartite') result = edmonds_karp(mfp) lookup_set = {a for a in result.G.setOfArcs if (a.datum.flow > 0) and (a.fromNode.uid != s.uid) and (a.toNode.uid != t.uid)} matching = {translate[frozenset({a.fromNode.uid,a.toNode.uid})] for a in lookup_set} return matching

Couverture minimale des nœuds

Une couverture de nœud dans un digraphe G est un ensemble de nœuds (cover) à partir de G.setOfNodes tel que pour tout arc a de G.setOfArcs cela doit être vrai: (a.fromNode in cover) or (a.toNode in cover).

Une couverture de nœud minimale est le plus petit ensemble possible de nœuds dans le graphique qui est toujours un couverture de nœud . Le théorème de König stipule que dans un bipartite graphe, la taille du correspondance maximale sur ce graphique est égale à la taille de couverture de nœud minimal , et cela suggère comment le couverture de nœud peut récupérer d'un correspondance maximale :

Supposons que nous ayons le bipartition bipartition et le correspondance maximale matching. Définissez un nouveau digraphe H, H.setOfNodes=G.setOfNodes, le arcs dans H.setOfArcs sont l'union de deux ensembles.

Le premier ensemble est arcs a dans matching, avec le changement que si a.fromNode in bipartition.firstPart et a.toNode in bipartition.secondPart puis a.fromNode et a.toNode sont échangés dans le créé arc donner un tel arcs a a.datum.inMatching=True attribut pour indiquer qu'ils sont dérivés de arcs dans un correspondant à .

Le deuxième ensemble est arcs a PAS dans matching, avec le changement que si a.fromNode in bipartition.secondPart et a.toNode in bipartition.firstPart puis a.fromNode et a.toNode sont échangés dans le créé arc (donnez un tel arcs a a.datum.inMatching=False attribut).

Ensuite, exécutez un première recherche en profondeur ( DFS ) à partir de chaque nœud n dans bipartition.firstPart qui n'est ni n == a.fromNode ni n == a.toNodes pour toute arc a dans matching. Pendant le DFS, certains nœuds sont visités et certains ne le sont pas (stockez ces informations dans un champ n.datum.visited). La couverture minimale des nœuds est l'union du nœuds {a.fromNode for a in H.setOfArcs if ( (a.datum.inMatching) and (a.fromNode.datum.visited) ) } et le nœuds {a.fromNode for a in H.setOfArcs if (a.datum.inMatching) and (not a.toNode.datum.visited)}.

Cela peut être démontré à partir d'un correspondance maximale à un couverture de nœud minimal par un preuve par contradiction , prends en arc a qui n'a pas été prétendument couvert et examinez les quatre cas pour savoir si a.fromNode et a.toNode appartiennent (que ce soit en tant que toNode ou fromNode) à arc dans correspondant à matching. Chaque cas conduit à une contradiction en raison de l'ordre de visite de DFS nœuds et le fait que matching est un correspondance maximale .

Supposons que nous ayons une fonction pour exécuter ces étapes et renvoyer l'ensemble de nœuds comprenant le couverture de nœud minimal quand on lui donne le digraphe G, et le correspondance maximale matching:

ArcMatchingDatum = coll.namedtuple('ArcMatchingDatum', ['inMatching' ]) NodeMatchingDatum = coll.namedtuple('NodeMatchingDatum', ['visited']) def dfs_helper(snodes, G): sniter, do_stop = iter(snodes), False visited, visited_uids = set(), set() while(not do_stop): try: stack = [ next(sniter) ] while len(stack) > 0: curr = stack.pop() if curr.uid not in visited_uids: visited = visited.union( frozenset( {Node(curr.uid, NodeMatchingDatum(False))} ) ) visited_uids = visited.union(frozenset({curr.uid})) succ = frozenset({a.toNode for a in G.setOfArcs if a.fromNode == curr}) stack.extend( succ.difference( frozenset(visited) ) ) except StopIteration as e: stack, do_stop = [], True return visited def get_min_node_cover(matching, bipartition): nodes = frozenset( { Node(n.uid, NodeMatchingDatum(False)) for n in bipartition.G.setOfNodes} ) G = DiGraph(nodes, None) charcs = frozenset( {a for a in matching if ( (a.fromNode in bipartition.firstPart) and (a.toNode in bipartition.secondPart) )} ) arcs0 = frozenset( { Arc(find_node_by_uid(a.toNode.uid,G), find_node_by_uid(a.fromNode.uid,G), ArcMatchingDatum(True) ) for a in charcs } ) arcs1 = frozenset( { Arc(find_node_by_uid(a.fromNode.uid,G), find_node_by_uid(a.toNode.uid,G), ArcMatchingDatum(True) ) for a in matching.difference(charcs) } ) not_matching = bipartition.G.setOfArcs.difference( matching ) charcs = frozenset( {a for a in not_matching if ( (a.fromNode in bipartition.secondPart) and (a.toNode in bipartition.firstPart) )} ) arcs2 = frozenset( { Arc(find_node_by_uid(a.toNode.uid,G), find_node_by_uid(a.fromNode.uid,G), ArcMatchingDatum(False) ) for a in charcs } ) arcs3 = frozenset( { Arc(find_node_by_uid(a.fromNode.uid,G), find_node_by_uid(a.toNode.uid,G), ArcMatchingDatum(False) ) for a in not_matching.difference(charcs) } ) arcs = arcs0.union(arcs1.union(arcs2.union(arcs3))) G = DiGraph(nodes, arcs) bip = Bipartition({find_node_by_uid(n.uid,G) for n in bipartition.firstPart},{find_node_by_uid(n.uid,G) for n in bipartition.secondPart},G) match_from_nodes = frozenset({find_node_by_uid(a.fromNode.uid,G) for a in matching}) match_to_nodes = frozenset({find_node_by_uid(a.toNode.uid,G) for a in matching}) snodes = bip.firstPart.difference(match_from_nodes).difference(match_to_nodes) visited_nodes = dfs_helper(snodes, bip.G) not_visited_nodes = bip.G.setOfNodes.difference(visited_nodes) H = DiGraph(visited_nodes.union(not_visited_nodes), arcs) cover1 = frozenset( {a.fromNode for a in H.setOfArcs if ( (a.datum.inMatching) and (a.fromNode.datum.visited) ) } ) cover2 = frozenset( {a.fromNode for a in H.setOfArcs if ( (a.datum.inMatching) and (not a.toNode.datum.visited) ) } ) min_cover_nodes = cover1.union(cover2) true_min_cover_nodes = frozenset({find_node_by_uid(n.uid, bipartition.G) for n in min_cover_nodes}) return min_cover_nodes

Le problème d'affectation linéaire

Le problème d'affectation linéaire consiste à trouver une correspondance de poids maximum dans un graphe biparti pondéré.

Des problèmes comme celui du tout début de cet article peuvent être exprimés en problème d'affectation linéaire . Étant donné un ensemble de travailleurs, un ensemble de tâches et une fonction indiquant la rentabilité d'une affectation d'un travailleur à une tâche, nous voulons maximiser la somme de toutes les affectations que nous effectuons; c'est un problème d'affectation linéaire .

Supposons que le nombre de tâches et de travailleurs soit égal, même si je montrerai que cette hypothèse est facile à supprimer. Dans la mise en œuvre, je représente poids d'arc avec un attribut a.datum.weight pour un arc a.

WeightArcDatum = namedtuple('WeightArcDatum', [weight])

Algorithme de Kuhn-Munkres

L'algorithme de Kuhn-Munkres résout le problème d'affectation linéaire . Une bonne implémentation peut prendre O(N^{4}) time, (où N est le nombre de nœuds dans le digraphe représentant le problème). Une implémentation plus simple à expliquer prend O(N^{5}) (pour une version qui régénère Graphiques ) et O(N^{4}) for (pour une version qui maintient Graphiques ). Ceci est similaire aux deux implémentations différentes du Edmonds-Karp algorithme.

Pour cette description, je travaille uniquement avec des graphes bipartis complets (ceux où la moitié nœuds sont dans une partie du bipartition et l'autre moitié dans la seconde partie). Chez le travailleur, la motivation des tâches signifie qu'il y a autant de travailleurs que de tâches.

Cela semble être une condition importante (et si ces ensembles ne sont pas égaux!) Mais il est facile de résoudre ce problème; Je parle de la façon de faire cela dans la dernière section.

La version de l'algorithme décrite ici utilise le concept utile de arcs de poids nul . Malheureusement, ce concept n'a de sens que lorsque nous résolvons un minimisation (si, plutôt que de maximiser les bénéfices de nos affectations de tâches, nous réduisions au contraire le coût de ces affectations).

Heureusement, il est facile de transformer un problème d'affectation linéaire maximum dans une problème d'affectation linéaire minimum en réglant chaque arc a poids à M-a.datum.weight où M=max({a.datum.weight for a in G.setOfArcs}). La solution à l'original maximiser le problème sera identique à la solution minimiser le problème après le arc les poids sont modifiés. Donc, pour le reste, supposons que nous apportons ce changement.

La Algorithme de Kuhn-Munkres résout correspondance de poids minimum dans un graphe biparti pondéré par une séquence de correspondances maximales en non pondéré bipartite graphiques. Si un nous trouvons un correspondance parfaite sur le digraphe représentation de la problème d'affectation linéaire , et si le poids de chaque arc dans le correspondant à est zéro, alors nous avons trouvé le correspondance de poids minimum puisque cette correspondance suggère que tout nœuds dans le digraphe a été apparié par un arc avec le coût le plus bas possible (aucun coût ne peut être inférieur à 0, sur la base des définitions antérieures).

Aucun autre arcs peut être ajouté au correspondant à (parce que tout nœuds sont déjà appariés) et non arcs doit être retiré du correspondant à car tout remplacement possible arc aura au moins une valeur pondérale aussi élevée.

Si nous trouvons un correspondance maximale du sous-graphe de G qui ne contient que arcs de poids nul , et ce n'est pas un correspondance parfaite , nous n'avons pas de solution complète (puisque le correspondant à n'est pas parfait ). Cependant, nous pouvons produire un nouveau digraphe H en changeant les poids de arcs dans G.setOfArcs d'une manière que le nouveau poids 0 arcs apparaissent et la solution optimale de H est la même que la solution optimale de G. Puisque nous garantissons qu'au moins un arc de poids nul est produit à chaque itération, nous garantissons que nous arriverons à un correspondance parfaite en pas plus de | G.setOfNodes | 2 = N 2 ces itérations.

Supposons que dans bipartition bipartition, bipartition.firstPart contient nœuds représentant les travailleurs, et bipartition.secondPart représente nœuds représentant des tâches.

L'algorithme commence par générer un nouveau digraphe H. H.setOfNodes = G.setOfNodes. Certains arcs dans H sont générés à partir de nœuds n dans bipartition.firstPart. Chacun de ces nœud n génère un arc b dans H.setOfArcs pour chaque arc a dans bipartition.G.setOfArcs où a.fromNode = n ou a.toNode = n, b=Arc(a.fromNode, a.toNode, a.datum.weight - z) où z=min(x.datum.weight for x in G.setOfArcs if ( (x.fromNode == n) or (x.toNode == n) )).

Plus arcs dans H sont générés à partir de nœuds n dans bipartition.secondPart. Chacun de ces nœud n génère un arc b dans H.setOfArcs pour chaque arc a dans bipartition.G.setOfArcs où a.fromNode = n ou a.toNode = n, b=Arc(a.fromNode, a.toNode, ArcWeightDatum(a.datum.weight - z)) où z=min(x.datum.weight for x in G.setOfArcs if ( (x.fromNode == n) or (x.toNode == n) )).

KMA: Ensuite, formez un nouveau digraphe K composé uniquement du arcs de poids nul de H, et le nœuds incident sur ceux arcs . Formez un bipartition sur le nœuds dans K, puis utilisez solve_mbm( bipartition ) pour obtenir un correspondance maximale (matching) sur K. Si matching est un correspondance parfaite dans H (la arcs dans matching sont incident en tout nœuds dans H.setOfNodes) puis le matching est une solution optimale au problème d'affectation linéaire .

Sinon, si matching n'est pas parfait , générer le couverture de nœud minimal de K en utilisant node_cover = get_min_node_cover(matching, bipartition(K)). Ensuite, définissez z=min({a.datum.weight for a in H.setOfArcs if a not in node_cover}). Définissez nodes = H.setOfNodes, arcs1 = {Arc(a.fromNode,a.toNode,ArcWeightDatum(a.datum.weigh-z)) for a in H.setOfArcs if ( (a.fromNode not in node_cover) and (a.toNode not in node_cover)}, arcs2 = {Arc(a.fromNode,a.toNode,ArcWeightDatum(a.datum.weigh)) for a in H.setOfArcs if ( (a.fromNode not in node_cover) != (a.toNode not in node_cover)}, arcs3 = {Arc(a.fromNode,a.toNode,ArcWeightDatum(a.datum.weigh+z)) for a in H.setOfArcs if ( (a.fromNode in node_cover) and (a.toNode in node_cover)}. Le != le symbole dans l'expression précédente agit comme un XOR opérateur. Puis arcs = arcs1.union(arcs2.union(arcs3)). Ensuite, H=DiGraph(nodes,arcs). Revenir à l'étiquette KMA . L'algorithme continue jusqu'à ce qu'un correspondance parfaite est produit. Cette correspondant à est aussi la solution au problème d'affectation linéaire .

def kuhn_munkres( bipartition ): nodes = bipartition.G.setOfNodes arcs = frozenset({}) for n in bipartition.firstPart: z = min( {x.datum.weight for x in bipartition.G.setOfArcs if ( (x.fromNode == n) or (x.toNode == n) )} ) arcs = arcs.union( frozenset({Arc(a.fromNode, a.toNode, ArcWeightDatum(a.datum.weight - z)) }) ) for n in bipartition.secondPart: z = min( {x.datum.weight for x in bipartition.G.setOfArcs if ( (x.fromNode == n) or (x.toNode == n) )} ) arcs = arcs.union( frozenset({Arc(a.fromNode, a.toNode, ArcWeightDatum(a.datum.weight - z)) }) ) H = DiGraph(nodes, arcs) assignment, value = dict({}), 0 not_done = True while( not_done ): zwarcs = frozenset( {a for a in H.setOfArcs if a.datum.weight == 0} ) znodes = frozenset( {n.fromNode for n in zwarcs} ).union( frozenset( {n.toNode for n in zwarcs} ) ) K = DiGraph(znodes, zwarcs) k_bipartition = bipartition(K) matching = solve_mbm( k_bipartition ) mnodes = frozenset({a.fromNode for a in matching}).union(frozenset({a.toNode for a in matching})) if( len(mnodes) == len(H.setOfNodes) ): for a in matching: assignment[ a.fromNode.uid ] = a.toNode.uid value = sum({a.datum.weight for a in matching}) not_done = False else: node_cover = get_min_node_cover(matching, bipartition(K)) z = min( frozenset( {a.datum.weight for a in H.setOfArcs if a not in node_cover} ) ) nodes = H.setOfNodes arcs1 = frozenset( {Arc(a.fromNode,a.toNode,ArcWeightDatum(a.datum.weigh-z)) for a in H.setOfArcs if ( (a.fromNode not in node_cover) and (a.toNode not in node_cover)} ) arcs2 = frozenset( {Arc(a.fromNode,a.toNode,ArcWeightDatum(a.datum.weigh)) for a in H.setOfArcs if ( (a.fromNode not in node_cover) != (a.toNode not in node_cover)} ) arcs3 = frozenset( {Arc(a.fromNode,a.toNode,ArcWeightDatum(a.datum.weigh+z)) for a in H.setOfArcs if ( (a.fromNode in node_cover) and (a.toNode in node_cover)} ) arcs = arcs1.union(arcs2.union(arcs3)) H = DiGraph(nodes,arcs) return value, assignment

Cette implémentation est O(N^{5}) car il génère un nouveau correspondance maximale matching à chaque itération; similaire aux deux implémentations précédentes de Edmonds-Karp cet algorithme peut être modifié pour garder une trace de l'appariement et l'adapter intelligemment à chaque itération. Lorsque cela est fait, la complexité devient O(N^{4}). Une version plus avancée et plus récente de cet algorithme (nécessitant des structures de données plus avancées) peut s'exécuter dans O(N^{3}). Les détails de l'implémentation plus simple ci-dessus et de l'implémentation plus avancée peuvent être trouvés sur ce post ce qui a motivé ce billet de blog.

Aucune des opérations sur arc les poids modifient l'affectation finale renvoyée par l'algorithme. Voici pourquoi: puisque nos graphiques d'entrée sont toujours graphes bipartis complets une solution doit cartographier chacun nœud d'une partition à l'autre nœud dans la deuxième partition, via le arc entre ces deux nœuds . Notez que les opérations effectuées sur le arc poids ne change jamais l'ordre (trié par poids) du arcs incident sur un nœud .

Ainsi, lorsque l'algorithme se termine à un correspondance bipartite complète parfaite chaque nœud se voit attribuer un arc de poids zéro , puisque l'ordre relatif des arcs à partir de ce nœud n'a pas changé pendant l'algorithme et depuis un arc de poids zéro est le moins cher possible arc et le correspondance bipartite complète parfaite garantit qu'un tel arc existe pour chacun nœud . Cela signifie que la solution générée est bien la même que la solution de l'original problème d'affectation linéaire sans aucune modification de arc poids.

Affectations déséquilibrées

Il semble que l'algorithme soit assez limité car, comme décrit, il ne fonctionne que sur graphes bipartis complets (ceux où la moitié du nœuds sont dans une partie du bipartition et l'autre moitié dans la seconde partie). Chez le travailleur, la motivation des tâches signifie qu'il y a autant de travailleurs que de tâches (cela semble assez limitatif).

Cependant, il existe une transformation facile qui supprime cette restriction. Supposons qu'il y ait moins de travailleurs que de tâches, nous ajoutons des travailleurs fictifs (assez pour faire du graphique résultant un graphe bipartite complet ). Chaque ouvrier factice a un arc dirigé du travailleur vers chacune des tâches. Chacun de ces arc a le poids 0 (le placer dans une correspondance ne donne aucun profit supplémentaire). Après ce changement, le graphique est un graphe bipartite complet pour laquelle nous pouvons résoudre. Aucune tâche affectée à un travailleur factice n'est lancée.

Supposons qu'il y ait plus de tâches que de travailleurs. Nous ajoutons quelques tâches factices (assez pour faire du graphique résultant un graphe bipartite complet ). Chaque tâche factice a un arc dirigé de chaque travailleur vers la tâche factice. Chacun de ces arc a un poids de 0 (le placer dans une correspondance ne donne aucun profit supplémentaire). Après ce changement, le graphique est un graphe bipartite complet pour laquelle nous pouvons résoudre. Tout travailleur affecté à une tâche factice n'est pas employé pendant la période.

Un exemple d'affectation linéaire

Enfin, faisons un exemple avec le code que j'utilise. Je vais modifier l'exemple de problème de Ici . Nous avons 3 tâches: nous devons nettoyer la salle de bain , Balayez le sol , et nettoyer les vitres .

Les travailleurs disponibles sont Alice , Bob , Charlie , et Diane . Chacun des travailleurs nous donne le salaire dont il a besoin par tâche. Voici les salaires par travailleur:

Nettoyer la salle de bain Balayez le sol Nettoyer les vitres
Alice 2 $ 3 $ 3 $
Bob 3 $ 2 $ 3 $
Charlie 3 $ 3 $ 2 $
Diane 9 $ 9 $ 1 $

Si nous voulons payer le moins d'argent possible tout en réalisant toutes les tâches, qui devrait faire quelle tâche? Commencez par introduire une tâche factice pour rendre le digraphe représentant le problème biparti.

Nettoyer la salle de bain Balayez le sol Nettoyer les vitres Ne fais rien
Alice 2 $ 3 $ 3 $ 0 $
Bob 3 $ 2 $ 3 $ 0 $
Charlie 3 $ 3 $ 2 $ 0 $
Diane 9 $ 9 $ 1 $ 0 $

Supposons que le problème soit encodé dans un digraphe , alors kuhn_munkres( bipartition(G) ) résoudra le problème et retournera la mission. Il est facile de vérifier que l'attribution optimale (coût le plus bas) coûtera 5 USD.

Voici une visualisation de la solution générée par le code ci-dessus:

Visualisation du débit maximum

Visualisation du débit maximum (résiduel)

C'est ça. Vous savez maintenant tout ce que vous devez savoir sur le problème d'affectation linéaire.

Vous pouvez trouver tout le code de cet article sur GitHub .

Python et Finance - Optimisez vos feuilles de calcul

Processus Financiers

Python et Finance - Optimisez vos feuilles de calcul
Conquérir la recherche de chaînes avec l'algorithme Aho-Corasick

Conquérir la recherche de chaînes avec l'algorithme Aho-Corasick

Science Des Données Et Bases De Données

Articles Populaires
Explorer la fonctionnalité Get & Transform d'Excel
Explorer la fonctionnalité Get & Transform d'Excel
Comment aborder les wrappers pour les propriétés Swift
Comment aborder les wrappers pour les propriétés Swift
ApeeScape s'associe à Guidant Global pour offrir un accès à la demande au réseau Elite de pigistes
ApeeScape s'associe à Guidant Global pour offrir un accès à la demande au réseau Elite de pigistes
Tutoriel Apache Spark Streaming: Identifier les hashtags Twitter tendances
Tutoriel Apache Spark Streaming: Identifier les hashtags Twitter tendances
Conception accessible vs conception inclusive (avec infographie)
Conception accessible vs conception inclusive (avec infographie)
 
L'intégration continue d'iOS avec le serveur Xcode expliquée
L'intégration continue d'iOS avec le serveur Xcode expliquée
Meilleurs éditeurs de programmation? Une bataille sans fin sans vainqueur clair
Meilleurs éditeurs de programmation? Une bataille sans fin sans vainqueur clair
Comment GWT déverrouille la réalité augmentée dans votre navigateur
Comment GWT déverrouille la réalité augmentée dans votre navigateur
Webpack ou Browserify & Gulp: quel est le meilleur?
Webpack ou Browserify & Gulp: quel est le meilleur?
Business Analyst - Stratégie & Analytics
Business Analyst - Stratégie & Analytics
Articles Populaires
  • comment faire une analyse de cohorte
  • société c vs société s vs llc
  • que pouvez-vous faire avec la programmation c
  • comment obtenir la certification aws
  • dans quoi est écrit Linux
Catégories
  • Science Des Données Et Bases De Données
  • Conception Mobile
  • Design De Marque
  • Personnes Et Équipes Produit
  • © 2022 | Tous Les Droits Sont Réservés

    portaldacalheta.pt