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 .
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 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.
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).
À 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œ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'])
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.
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.
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
.
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
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
À 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'])
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
.
Laisser digraphe G
représenter un réseau de flux .
La réseau de flux représenté par G
a flux réalisables si:
Pour chaque nœud n
dans G.setOfNodes
à l'exception de nœuds source et nœuds terminaux : n.datum.flowIn = n.datum.flowOut
.
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é .
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
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:
-
strip_flows(maxFlowProblem.G) == strip_flows(H)
.
-
H
est un réseau de flux et a flux réalisables .
Si en plus de 1 et 2:
- 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:
-
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.
-
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:
- 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}
.

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
.
-
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
.
-
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
.
-
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
.

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:
-
Il existe un coupe s-t dont la capacité est égale à la valeur du débit f
.
-
f
est un débit max .
-
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:
-
Comment doit augmenter les chemins être construit?
-
La méthode se terminera-t-elle si nous utilisons des nombres réels et non des entiers?
-
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).


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:


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 .