Voici un problème: votre entreprise affecte des entrepreneurs pour exécuter les contrats. Vous regardez vos listes et décidez quels entrepreneurs sont disponibles pour un contrat d'un mois et passez en revue vos contrats disponibles pour voir lesquels 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 classique Algorithme hongrois .
L'algorithme hongrois (également connu sous le nom d'algorithme de Kuhn-Munkres) est un algorithme de temps polynomial, qui maximise le poids de poids sur un graphe bipartite pondéré. Ici, les entrepreneurs et les contrats peuvent être modélisés sous forme de graphique bipartite, leur efficacité étant le 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 et l'importance de cette modification.
Le problème du débit de pointe 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 rester dans aucune longueur de tuyau ou de raccord de tuyau (les endroits où se rencontrent différentes longueurs de tuyau).
En apportant certaines modifications au graphique, le problème d'allocation peut devenir un problème de débit de pointe.
Les idées nécessaires pour résoudre ces problèmes surgissent dans de nombreuses disciplines mathématiques et d'ingénierie, souvent des concepts similaires sont connus sous des noms différents 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 sur 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 brève introduction à la théorie des ensembles.
Les implémentations de cet article représentent les problèmes sous forme de graphes dirigés (digraphe).
UNE digrafo il en a deux les attributs , setOfNodes Oui setOfArcs . Ces deux attributs sont ensembles (collections désordonnées). Dans les blocs de code de cet article, j'utilise 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.)
UNE nœud n
Il est composé de deux attributs:
n.uid
: A Identifiant unique .
Cela signifie que pour l'un des deux nœuds 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
Il 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 dans le nœuds dans le digraphe . L'existence de arc a
implique qu'il existe une relation 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
non impliquent qu'une relation similaire entre a.toNode
et a.fromNode
Ça existe.
comment apprendre c++ en ligne
En effet, dans un graphe non orienté, la relation exprimée n'est pas nécessairement symétrique.
Nœuds Oui arcs peut être utilisé pour définir un digraphe , mais par commodité, dans les algorithmes suivants, un digraphe en utilisant comme un dictionnaire .
Voici une méthode qui peut convertir la représentation graphique ci-dessus en une représentation de dictionnaire similaire à est :
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 un autre qui le transforme en dictionnaire de dictionnaires, une autre opération qui vous 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 le dictionnaire le représente, il mentionnera G_as_dict
pour référence.
Parfois, il est utile de prendre un nœud d'un digraphe G
jusqu'à votre 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()
Lors de l'identification des graphiques, les gens utilisent parfois les termes nœud et vertex pour désigner le même concept; la même chose est dite des termes arc et bord.
Il existe deux représentations graphiques en Python, est qui utilise des dictionnaires et autre qui utilise des objets pour représenter des graphiques. La représentation dans ce post se situe entre ces deux représentations communes.
Ceci est ma représentation d'un digraphe . Il y en a beaucoup comme ça, mais c'est le mien.
Soit S_Arcs
être un séquence définitif (collection ordonnée) de arcs en un digraphe G
à tel point 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
comme a.toNode = b.fromNode
.
À partir du premier arc dans S_Arcs
, et en continuant jusqu'au dernier arc dans S_Arcs
, rassemble (dans l'ordre trouvé) tous nœuds n
comme nous les définissons ci-dessus entre chaque deux arcs consécutive dans S_Arcs
. Marquer 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 Chemin pour digraphe G
.
Sinon, appelez S_Arcs
ongle via de list(S_Nodes)[0]
a list(S_Nodes)[-1]
dans le digraphe G
.
Appeler pour nœud s
une nœud source dans digraphe G
si s
est dans G.setOfNodes
et G.setOfArcs
cela n'a pas arc a
comme 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
Appeler pour nœud t
une nœud terminal dans le digraphe G
si t
est dans G.setOfNodes
et G.setOfArcs
cela n'a pas arc a
comme 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
UNE Couper cut
d'un digraphe G
connecté c'est un sous-ensemble de arcs de G.setOfArcs
lequel partie l'ensemble des nœuds G.setOfNodes
dans G
. G
est connecté si chaque nœud n
dans G.setOfNodes
a au moins un arc a
dans G.setOfArcs
comme n = a.fromNode
ou n = a.toNode
, mais pas a.fromNode != a.toNode
.
Cut = namedtuple('Cut', ['setOfCutArcs'])
La définition ci-dessus fait référence au sous-ensemble de arcs , mais vous pouvez également définir une partition du nœuds de G.setOfNodes
.
Pour les fonctions predecessors_of
et successors_of
, n
c'est un nœud dans l'ensemble G.setOfNodes de digraphe G
, et cut
c'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 du 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
c'est un Couper du digraphe G
si: (get_first_part(cut, G).union(get_second_part(cut, G)) == G.setOfNodes) y (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)) y (y in get_second_part(cut, G) ) y (x != y)
. Quand il nœud x
en un coupe x-y cut
c'est un nœud source et le nœud y
dans le coupe x-y c'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
a n.datum
qui est un FlowNodeDatum
:
FlowNodeDatum = namedtuple('FlowNodeDatum', ['flowIn','flowOut'])
Attribuer chacun arc a
, où a
est un G.setOfArcs
et a.datum
qui est un FlowArcDatum
.
FlowArcDatum = namedtuple('FlowArcDatum', ['capacity','flow'])
flowNodeDatum.flowIn
et flowNodeDatum.flowOut
ils sont nombres réels positif .
flowArcDatum.capacity
et flowArcDatum.flow
ce sont aussi des nombres réels positifs.
javascript obtenir la date et l'heure
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})
La Digraph G
représente maintenant le réseau de flux .
La couler de G
fait référence au flux a.flow
pour tous les arcs a
dans G
.
Laisse le digraphe G
représenter un réseau de flux .
La réseau de flux représenté par G
avoir flux réalisables Oui:
Pour chaque nœud n
dans G.setOfNodes
à l'exception de nœuds source Oui 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 confinement de conservation .
La condition 2 est appelée confinement de capacité .
La capacité de coupe d'un coupe s-t stCut
avec un nœud source s
et un nœud terminal t
d'une réseau de flux représenté par un digraphe G
il 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)
Être un coupe s-t d'une réseau de flux représenté par un digraphe G
.
stCut
c'est lui capacité de coupe minimale de la réseau de flux représenté par G
s'il n'y en a pas d'autre coupe s-t stCutCompetitor
dans celle-ci réseau de flux comme:
cut_capacity(stCut, G) Dépouiller les flux
Je voudrais me référer à digraphe quel serait le résultat de prendre un digraphe G
et effacer toutes les données de flux de tous nœuds dans G.setOfNodes
et aussi tout 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 de pointe
Ongle 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 de pointe Oui:
(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)
Marquez 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 de débit de pointe
Soit maxFlowProblem
représenter un problème de débit de pointe . La solution au problème maxFlowProblem
peut être représenté par un réseau de flux représenté comme un digraphe H
.
La Digraph H
est une solution réalisable pour lui problème de débit de pointe à l'entrée python maxFlowProblem
Oui:
-
strip_flows(maxFlowProblem.G) == strip_flows(H)
.
-
H
c'est une 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 le digraphe
K
comme strip_flows(G) == strip_flows(K)
et find_node_by_uid(t.uid,G).flowIn .
Alors H
c'est aussi une solution optimal pour maxFlowProblem
.
En d'autres termes, un solution de débit de pointe réalisable peut être représenté avec un digraphe , Quoi:
-
C'est identique à digraphe G
du correspondant problème de débit de pointe à l'exception du flux n.datum.flowIn
, n.datum.flowOut
et le flux a.datum.flow
de l'un des nœuds Oui arcs ils peuvent être différents.
-
Représente un réseau de flux Qu'est ce qui ne va pas avec ça flux réalisables .
Et cela peut représenter un solution optimale de débit de pointe si en plus:
- Le
flowIn
pour lui nœud qui correspond à nœud terminal dans le problème de débit de pointe est aussi grand que possible (lorsque les conditions 1 et 2 sont toujours satisfaites).
Oui digraphe H
représente un solution de débit de pointe : find_node_by_uid(s.uid,H).flowOut = find_node_by_uid(t.uid,H).flowIn
cela découle de la débit maximum, théorème de cisaillement minimum (discuté ci-dessous). De manière informelle, puisque H
avoir flux viables cela signifie que le couler ne peut pas être `` créé '' (à l'exception du nœud source s
) ni 'détruit' (à l'exception de nœud terminal t
) en traversant n'importe quel (autre) nœud ( contraintes de conservation ).
Depuis un problème de débit de pointe contient un seul nœud source s
et un seul nœud terminal t
, chaque flux 'créé' dans s
doit être 'détruit' dans t
vague réseau de flux non avoir flux réalisables (la restriction de conservation avait été violée).
Laisse le digraphe H
représenter un solution de débit de pointe réalisable ; la valeur ci-dessus est appelée Valeur de débit s-t de H
.
Il permet:
mfps=MaxFlowProblemState(H, maxFlowProblem.sourceNodeUid, maxFlowProblem.terminalNodeUid, maxFlowProblem.maxFlowProblemStateUid)
Cela signifie que mfps
c'est un état successeur de maxFlowProblem
, ce qui signifie 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
Ceci est un affichage d'un mfps
avec ses maxFlowProb
associée. Chaque arc a
dans l'image, il a une étiquette, ces étiquettes sont a.datum.flowFrom/a.datum.flowTo
, chacune nœud n
dans l'image, il a une étiquette et ces étiquettes sont n.uid {n.datum.flowIn / a.datum.flowOut}
.

Flux de cisaillement
Soit mfps
représentent un état de problème MaxFlowProblemState
et laissez stCut
représenter un Couper de mfps.G
. La écoulement de cisaillement stCut
est défini comme ceci:
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
coupure de débit s-t est la somme des flux de la partition contenant le nœud source pour la partition contenant le nœud terminal moins la somme des flux de la partition contenant les nœud terminal pour la partition contenant le nœud source .
Débit maximum, cisaillement minimum
Quoi maxFlowProblem
représenter un problème de débit de pointe et que la solution à maxFlowProblem
est représenté par un réseau de flux représenté comme digraphe H
.
Soit minStCut
la capacité de coupe minimale de la réseau de flux représenté par maxFlowProblem.G
.
Parce que dans le flux de débit de pointe le flux provient d'un seul nœud source et se termine par un seul nœud Terminal et, en raison de restrictions de capacité et les restrictions de conservation , on sait 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)
Solution au problème de débit de pointe
L'idée de base pour résoudre un problème de débit de pointe maxFlowProblem
est de commencer par un solution de débit de pointe 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 des valeurs a.datum.flow
de certaines arcs a
dans H.setOfArcs
produire un autre solution de débit de pointe représenté par digraphe K
de sorte que K
ne peut pas encore représenter un réseau de flux avec flux viables et get_flow_value(H, maxFlowProblem) . Tant que ce processus se poursuit, la qualité (get_flow_value(K, maxFlowProblem)
) du solution de débit de pointe (K
) trouvé, récemment, meilleur que tout autre solution de débit de pointe qui a été trouvé. Si le processus atteint un point où il sait qu'aucune amélioration supplémentaire n'est possible, le processus peut se terminer et renverra le solution de débit de pointe optimum .
La description ci-dessus est générale et omet de nombreux tests tels que, si un tel processus est possible ou combien de temps cela peut prendre, je vais donner plus de détails et l'algorithme.
Le théorème d'écoulement maximum, cisaillement minimum
Du livre Flux dans les réseaux de Ford y Fulkerson , la déclaration de Théorème d'écoulement maximum, cisaillement minimum (Théorème 5.1) est:
Pour tout réseau, la valeur de débit maximale de s
a t
est égale à la capacité de coupe minimale de toutes les coupes qui séparent 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
il est optimal Oui:
get_flow_value(H, maxFlowProblem) == cut_capacity(minStCut, maxFlowProblem.G)
J'aime ce test du théorème et Wikipedia a autre .
La le théorème d'écoulement maximum, cisaillement minimum est utilisé pour démontrer l'exactitude et l'exhaustivité des Méthode Ford-Fulkerson .
Je donnerai également une preuve du théorème dans la section après moyens d'augmenter .
La méthode Ford-Fulkerson et l'algorithme Edmonds-Karp
CLRS définir la méthode Ford-Fulkerson (section 26.2) comme ceci:
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'une réseau de flux représenté comme le digraphe 'G' peut être représenté par 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)
revenir à la somme de a.datum.capacity
pour les arcs dans le sous-ensemble de G.setOfArcs
où il 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 les arcs dans le sous-ensemble de G.setOfArcs
où il 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
de la 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 pas arcs dans G_f
s'il n'y a pas arc a
dans G.setOfArcs
comme 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é comme un flux impulsionnel de n
a u
a = Arco (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é comme un tirez l'arc de flux de n
a u
a = Arco (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 flux x <= n_to_u_cap_sum - n_to_u_flow_sum
à arcs dans le sous-ensemble de G.setOfArcs
où il 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 flux x <= n_to_u_flow_sum
à arcs dans le sous-ensemble de G.setOfArcs
où arc a
est dans le sous-ensemble si a.fromNode = n
et a.toNode = u
.
Effectuer une action individuelle telle que Poussez ou encore de G_f
dans les arcs applicable dans G
pourrait générer un réseau de flux sans pour autant flux réalisables parce que le contraintes de capacité ou contraintes de conservation pourrait être violée dans le réseau de flux généré .
Voici une visualisation du graphe résiduel de l'exemple précédent d'affichage d'un solution de débit de pointe , dans l'affichage chaque arc a
représente a.residualCapacity
.

Voie croissante
Soit maxFlowProblem
Être un problème de débit de pointe , et laissez G_f = get_residual_graph_of (G)
être graphe résiduel de maxFlowProblem.G
.
Ongle en haut augmentingPath
pour maxFlowProblem
est un via de find_node_by_uid(maxFlowProblem.sourceNode,G_f)
a find_node_by_uid(maxFlowProblem.terminalNode,G_f)
.
Il s'avère qu'un chemin d'augmentation augmentingPath
peut être appliqué à un solution de débit de pointe représenté par le digraphe H
générer un autre solution de débit de pointe représenté par le digraphe K
où get_flow_value (H, maxFlowProblem) si H
ce n'est pas optimum .
Ici vous voyez 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 arrondir les valeurs de débit dans le réseau. Ceci afin d'éviter la cascade de imprécision des calculs en virgule flottante . Par exemple, j'ai utilisé 'TOL = 10' pour arrondir à 10 chiffres significatifs .
Quitter K = augment (augmentingPath, H)
, puis K
représente un solution de débit de pointe 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 restriction de capacité vague restriction 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 une copie exacte d'un nœud de digraphe H
ou un nœud n
qui a eu le même numéro ajouté à votre n.datum.flowIn
comme votre n.datum.flowOut
. Cela signifie que le restriction de conservation elle est accomplie en «K» tant qu'elle est accomplie en «H». La restriction de conservation est satisfait car nous vérifions explicitement que tout arc nouveau a
dans le réseau, vous avez a.datum.flow <= a.datum.capacity
; par conséquent, tant que le arcs de l'ensemble H.setOfArcs
qui ont été copiés inchangés dans K.setOfArcs
ne violez pas le restriction de capacité , alors K
ne viole pas le restriction de capacité .
Il est également vrai que get_flow_value (H, maxFlowProblem) si H
ce n'est pas optimum .
Voici pourquoi: pour un augmentation de l'itinéraire augmentingPath
existe dans le digraphe la représentation de graphe résiduel G_f
d'un problème de débit de pointe maxFlowProblem
puis le dernier arc a
dans augmentingPath
Doit être un arc 'Push' et doit avoir a.toNode == maxFlowProblem.terminalNode
. Ongle chemin d'augmentation est défini comme un se terminant par nœud terminal du problème de débit de pointe pour qui est un chemin d'augmentation . D'après la définition de graphe résiduel , il est clair que le dernier arc dans une moyen d'augmenter dans ce graphe résiduel Doit être un arc 'Push' parce que tout arc 'Tirer' b
dans la moyen d'augmenter aura b.toNode == maxFlowProblem.terminalNode
et b.fromNode! = maxFlowProblem.terminalNode
de la définition de via . De plus, d'après la définition de via , il est clair que le nœud terminal Il n'est modifié qu'une seule fois avec la méthode augment
. Donc augment
modifier maxFlowProblem.terminalNode.flowIn
exactement une fois et augmente la valeur de maxFlowProblem.terminalNode.flowIn
parce que le dernier arc dans augmentingPath
ça doit être lui arc qui provoque la modification de maxFlowProblem.terminalNode.flowIn
pendant augment
. D'après la définition de augment
, puisqu'elle s'applique à arcs 'Pousser', le maxFlowProblem.terminalNode.flowIn
il ne peut qu'être augmenté et non diminué.
Quelques preuves de Sedgewick et Wayne
Le livre Algorithmes, quatrième édition par Robert Sedgewich et Kevin Wayne a de merveilleux tests courts (pages 892-894) qui seront utiles. Je vais les recréer ici, même si j'utiliserai un langage conforme aux définitions ci-dessus. Mes étiquettes pour les tests sont les mêmes que dans le livre de Sedgewick.
Proposition E: Pour n'importe qui digraphe H
qui représente un solution de débit de pointe réalisable encore problème de débit de pointe maxFlowProblem
, pour tout stCut
get_stcut_flow(stCut,H,maxFlowProblem) = get_flow_value(H, maxFlowProblem)
.
Test: Quitter stCut=stCut(maxFlowProblem.sourceNode,maxFlowProblem.terminalNode,set([a for a in H.setOfArcs if a.toNode == maxFlowProblem.terminalNode]))
. Proposition E est détenu par stCut
directement à partir de la définition de valeur de débit s-t . Supposons que nous voulions déplacer un nœud n
à partir de la partition s (get_first_part(stCut.cut, G)
) et sur la 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
cela changera à mesure que nous procéderons à ce changement. La nœud n
est en équilibre, ce qui signifie que la somme des flux en nœud n
est égal à la somme du flux de this (cela est nécessaire pour que H
représente un solution réalisable ).
Notez que tout le flux qui fait partie de stcut_flow
venir dans nœud n
y entre à partir de la partition s (le flux entrant nœud n
puisque la partition t le fait directement ou indirectement à partir de cela, elle n'aurait pas été comptée dans la valeur stcut_flow
parce que ça va dans la mauvaise direction selon la définition). Notez que tout le flux qui fait partie du stcut_flow
qui entre dans le nœud n
y entre à partir de la partition s (le flux qui entre dans le nœud n
de la partition t directement ou indirectement n'ont pas été comptés dans la valeur stcut_flow
parce que vous vous dirigez dans la mauvaise direction en fonction de la définition).
Aussi, tout le flux qui sort n
finira par couler (directement ou indirectement) dans le nœud terminal (démontré ci-dessus). Lorsque nous déplaçons le nœud n
dans la partition t, tout le flux entrant n
de la partition s doit être ajoutée à la nouvelle valeur stcut_flow
; cependant, tout le flux qui sort n
doit être soustraite de la nouvelle valeur stcut_flow
; la partie du flux qui va directement à la partition t est soustraite, puisque ce flux est maintenant interne à la nouvelle partition t et n'est pas compté comme stcut_flow
.
technologie iot et maison intelligente
La partie du flux à partir de la position n
dans nœuds de la partition s doit également être soustrait de stcut_flow
: Après n
se déplace vers la partition t, ces flux seront dirigés à partir de la position de la partition t dans la partition s et ne doivent donc pas être pris en compte dans le stcut_flow
, car ces flux éliminent l'entrée dans la partition s et doivent être réduits 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 se terminer) doivent être réduits du même montant. Comme lui nœud n
était en équilibre avant le processus, la mise à jour aura ajouté la même valeur à la nouvelle valeur stcut_flow
lorsqu'il est soustrait, laissant le proposition E effacer après la mise à niveau. La validité de la proposition E puis on continue dans l'induction sur la taille de la partition t.
Voici quelques exemples de réseaux de flux pour aider à visualiser les cas les moins évidents où le proposition E il se maintient; Dans l'image, les zones rouges indiquent la partition s, les zones bleues représentent la partition t et les arcs le vert indique un couper s-t **. Dans la deuxième image, le flux entre ** node Vers et nœud B augmente tandis que le débit nœud terminal t ne change pas.


Corollaire: Aucune valeur de écoulement de cisaillement s-t peut dépasser la capacité de coupe s-t .
Proposition F. (débit maximum et théorème de coupure minimum): Soit f
être un flux s-t . Les 3 conditions suivantes sont équivalentes:
-
Il y a une coupure s-t dont la capacité est égale à la valeur du débit f
.
-
f
c'est un débit de pointe .
-
Il n'y a pas moyen d'augmenter 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 croissant implique l'existence d'un flux avec des valeurs plus élevées, contredisant le maximum de «f». La condition 3 implique la condition 1: Soit C_s
l'ensemble de tous nœuds accessible depuis s
avec un augmentation de l'itinéraire dans le graphe résiduel . Soit C_t
Les arcs restants , alors t
doit être dans C_t
(selon notre hypothèse). Les arcs qui traversent de C_s
a C_t
puis formez un coupe s-t contenant seulement arcs a
où a.datum.capacity = a.datum.flow
ou a.datum.flow = 0.0
. Sinon, le nœuds connecté par un arc avec la capacité résiduelle restante a C_s
serait dans l'ensemble C_s
depuis, il y aurait un augmentation de la piste de s
encore nœud . Le flux à travers le coupe s-t égale la capacité de coupe s-t (depuis le arcs de C_s
a C_t
ont un débit égal à la capacité) et aussi à la valeur du débit s-t (pour proposition E ).
Cette déclaration du théorème débit max, coupe min implique la déclaration ci-dessus de Flux dans les réseaux .
Corollaire (propriété d'intégralité): Lorsque les capacités sont des nombres entiers, il existe un flux de valeurs entier maximal et l'algorithme de Ford-Fulkerson le trouve.
Test: chaque moyen d'augmenter augmente le débit d'un entier positif, le minimum des capacités inutilisées dans les arcs poussez et coule dans les arcades traction , qui sont toujours des entiers positifs.
Cela justifie la description de la méthode Ford-Fulkerson de CLRS . La méthode consiste à continuer à trouver routes croissantes et appliquer augment
jusqu'au dernier maxFlowSolution
trouver de meilleures solutions, jusqu'à ce que plus itinéraire croissant , ce qui signifie que le dernier solution de débit de pointe il est optimum .
De Ford-Fulkerson à Edmonds-Karp
Les questions restantes concernant la résolution de problèmes de débit de pointe ils sont:
-
Comment devraient-ils être construits chemins d'augmentation ?
-
La méthode se terminera-t-elle si nous utilisons des nombres réels et non des nombres entiers?
-
Combien de temps faudra-t-il pour terminer (si c'est le cas)?
La Algorithme d'Edmonds-Karp préciser que chacun chemin d'augmentation est construit par un recherche d'amplitude ( BFS ) du graphe résiduel ; Il s'avère que cette décision à partir du point 1 forcera également l'algorithme à se terminer (point 2) et permettra au temps asymptotique et complexité de l'espace être déterminé.
Tout d'abord, voici une implémentation BFS :
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, je vais paraphraser une autre preuve de Sedgewick et Wayne : Proposition G. Le numéro de voies d'augmentation nécessaire dans l'algorithme Edmonds-Karp avec des nœuds N
Oui arcs est au plus NA/2
. Test: chaque chemin d'augmentation a un nœud de cou arc - une arc qui est supprimé de graphe résiduel car il correspond bien à un arc poussez qui est rempli à pleine capacité ou un arc traction à travers lequel le flux devient 0. Chaque fois qu'un arc devient un goulot d'étranglement arc , la longueur de tout chemin d'augmentation à travers elle devrait être multipliée par 2. En effet, chaque nœud dans une route peut apparaître une seule fois ou pas du tout (d'après la définition de route ) Depuis le itinéraires sont explorés du plus court route ce qui signifie qu'au moins un nœud plus doit être visité par l'itinéraire suivant à travers le goulot d'étranglement nœud ce qui signifie 2 supplémentaires arcs sur la route avant d'arriver dans le nœud . Depuis le chemin d'incrément est la longueur maximale N
, chaque arc peut avoir un maximum de N/2
, chemins d'incrémentation, et le nombre total de chemins d'incrémentation est au plus NA/2
.
La Algorithme d'Edmonds-Karp s'exécute sur O(NA^2)
. Oui au plus les chemins NA/2
sera exploré au cours de l'algorithme et explorer chaque trajectoire avec BFS est N+A
puis le terme le plus significatif du produit et donc la complexité asymptotique O(NA^2)
.
Soit mfp
que ce soit 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
L'ancienne version est inefficace et a une complexité pire que O(NA^2)
comme il construit un nouveau solution de débit de pointe et un nouveau graphe résiduel à chaque fois (au lieu de modifier le digraphes existants à mesure que l'algorithme progresse). Pour arriver à une solution O(NA^2)
true l'algorithme doit garder à la fois le digrafo que représente le état de problème de débit de pointe et son graphe résiduel ** associé . Par conséquent, l'algorithme doit éviter de répéter inutilement ** des arcs Oui nœuds et mettre à jour leurs valeurs et les valeurs associées dans le graphe résiduel seulement lorsque cela est nécessaire.
Pour écrire un algorithme Edmonds Karp plus rapidement, j'ai réécrit plusieurs extraits de code ci-dessus. J'espère parcourir le code qui a généré un nouveau digrafo Cela a été utile pour comprendre ce qui se passe. Dans l'algorithme rapide, j'utilise de nouvelles astuces et structures de données Python que je ne veux pas détailler. Je vais dire 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 reflétées dans le digrafo G
représentant le réseau de flux dernier et comment ils se reflètent dans le graphe résiduel de ce réseau de flux. Les moyens d'augmenter dans le graphe résiduel sont représentés par des itinéraires rouges et les digrafo qui représente le problème l'ensemble de nœuds Oui arcs affecté par un dé la voie d'augmentation est surligné en vert. Dans chaque cas, je vais mettre en évidence les parties du graphique qui seront modifiées (rouge ou vert) puis afficher le graphique après les modifications (noir uniquement).


Voici une autre visualisation de la façon dont cet algorithme résout un exemple différent réseau de flux . Notez que cela utilise des nombres réels et contient plusieurs arcs avec les mêmes valeurs fromNode
et toNode
.
Notez également que, étant donné que les arcs avec un ResidualDatum 'pull' peuvent faire partie du chemin ascendant, les nœuds affectés dans le diagramme du réseau de flux ne se trouvent pas sur un chemin dans G!
.
Graphiques bipartites
Supposons que nous ayons un digrafo G
, G
il 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
non C'est peut-être vrai que a.fromNode
dans part_1
et a.toNode
dans part_1
. Ni C'est peut-être vrai que a.fromNode
dans part_2
et a.toNode
dans part_2
.
En d'autres termes, G
il est bipartition s'il peut être divisé en deux ensembles de nœuds de telle manière que chacun arc doit connecter un nœud dans un ensemble à un nœud dans l'autre ensemble.
Test bipartite
Supposons que nous ayons un digrafo G
, nous voulons tester si c'est bipartition . Nous pouvons le faire dans O(|G.setOfNodes|+|G.setOfArcs|)
par la couleur gourmande du nuancier bicolore.
Premièrement, nous devons générer un nouveau digrafo H
. Ce graphique aura le même ensemble de nœuds comme G
, mais en aura plus arcs que G
. Chaque arc a
dans G
créera 2 arcs dans H
; le premier arc sera identique à a
, et le second arc investit 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)
Matchs et matchs maximum
Supposons que nous ayons un digrafo G
et matching
est un sous-ensemble de arcs de G.setOfArcs
. matching
c'est un correspondant à oui pour deux digrafo a
et b
dans matching
: len(frozenset( {a.fromNode} ).union( {a.toNode} ).union( {b.fromNode} ).union( {b.toNode} )) == 4
. En d'autres termes, non arc dans une coïncidence Partagez un nœud .
meilleures pratiques de conception de sites Web 2016
La coïncidence matching
, est une correspondance maximale s'il n'y en a pas d'autre accord alt_matching
dans G
à tel point que len(matching) . En d'autres termes, matching
c'est une correspondance maximale S'il s'agit de la plus longue série de arcs dans G.setOfArcs
qui satisfait toujours à la définition de combinaison (en ajoutant tout arc qui n'est pas déjà dans le match cassera le combinaison définition).
Ongle combinaison maximale matching
c'est une combinaison parfaite oui pour chacun nœud n
dans G.setOfArcs
il y a arc a
dans matching
où a.fromNode == n or a.toNode == n
.
Combinaison bi-partitionnée maximale
Ongle combinaison bi-partitionnée maximale c'est une combinaison maximale en un digrafo G
Qu'est que c'est bi-partitionné .
Depuis G
il est bi-partitionné , le problème de trouver un combinaison maximale bipartite peut être transformé en un problème de débit de pointe résoluble avec algorithme par Edmonds-Karp et puis le couplage bipartite maximal peut être récupéré de la solution à problème de débit de pointe .
Soit bipartition
être un bipartition de G
.
Pour cela, j'ai besoin de générer un nouveau digrafo (H
) avec de nouveaux nœuds (H.setOfNodes
) et certains arcs nouveau (H.setOfArcs
). H.setOfNodes
contient tout nœuds dans G.setOfNodes
et deux nœuds plus, s
(une nœud source ) et t
(une nœud terminal ).
H.setOfArcs
contiendra un arc pour chaque G.setOfArcs
. Si un arc a
est un G.setOfArcs
et a.fromNode
est dans bipartition.firstPart
et a.toNode
est dans bipartition.secondPart
puis inclut a
dans H
(ajout d'un FlowArcDatum(1,0)
).
Si a.fromNode
est un bipartition.secondPart
et a.toNode
est un bipartition.firstPart
, alors incluez Arc(a.toNode,a.fromNode,FlowArcDatum(1,0))
dans H.setOfArcs
.
Définition d'un graphique bi-partitionné garantit que non arc connecter tout nœud où les deux nœuds ils sont sur la même partition. H.setOfArcs
contient également un arc de nœud s
à chaque nœud dans bipartition.firstPart
. Enfin, H.setOfArcs
contient un arc chaque nœud dans bipartition.secondPart
une nœud t
. a.datum.capacity = 1
pour tous a
dans H.setOfArcs
.
Première partition du nœuds dans G.setOfNodes
les deux ensembles (part1
et part2
) séparé tellement que non arc dans G.setOfArcs
passe d'un ensemble au même ensemble (cette partition est possible car G
est bi-partitionné ). Puis ajoutez tous les arcs dans G.setOfArcs
qui sont dirigés depuis le part1
au part2
vers H.setOfArcs
. Ensuite, créez un seul nœud la source s
et un seul nœud Terminal t
et créez plus arcs
Puis 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
C'est un ensemble de nœuds (cover
) à partir de G.setOfNodes
tellement que pour tout arc a
de G.setOfArcs
cela doit être vrai: (a.fromNode en cubierta) o (a.toNode en cubierta)
.
Une couverture de nœud minimale est le plus petit ensemble de nœuds dans le graphique qui est toujours un couverture de nœud . Le théorème de König indique que sur un graphe bi-partitionné , la taille du correspondance maximale dans ce graphique est égal à la taille du couverture minimale des nœuds , et suggère comment la couverture de nœud peut être récupérée à partir d'un correspondance maximale :
Supposons que nous ayons le bipartition bipartition
et la correspondance maximale matching
. Définissez un nouveau digrafo H
, H.setOfNodes=G.setOfNodes
, le arcs dans H.setOfArcs
ils sont l'union de deux ensembles.
Le premier ensemble de arcs a
dans matching
, avec le changement que si a.fromNode in bipartition.firstPart
et a.toNode en bipartition.secondPart
puis a.fromNode
et a.toNode
sont échangés dans le acro créé ils donnent un tel arcs un attribut a.datum.inMatching = True
pour indiquer qu'ils proviennent de arcs dans une coïncidence .
Le deuxième ensemble est arcs a
PAS dans matching
, avec le changement que si a.fromNode en bipartition.secondPart
et a.toNode en bipartition.firstPart
puis a.fromNode
et a.toNode
sont échangés dans le arc créé (donne un tel arc un attribut a.datum.inMatching = False
).
Puis lancez un première recherche en profondeur ( DFS ) de chaque nœud n
dans bipartition.firstPart
qui n'est pas n == a.fromNode
ni n == a.toNodes
pour n'importe qui arc a
dans matching
. Pendant DFS, certains nœuds sont visités et d'autres non (stockez ces informations dans un champ n.datum.visited
). La couverture minimale des nœuds est l'union de nœuds {a.fromNode para a en H.setOfArcs si ((a.datum.inMatching) y (a.fromNode.datum.visited))}
et nœuds {a.fromNode para a en H.setOfArcs si (a.datum.inMatching) y (no a.toNode.datum.visited)}
.
Cela peut être montré pour conduire à partir d'un correspondance maximale à une couverture minimale des nœuds par un preuve par contradiction , prendre un arc a
qui n'était pas censé être couvert et examinez les quatre cas pour savoir si a.fromNode
et a.toNode
appartiennent (soit comme toNode
ou fromNode
) à tout arc dans coïncidence matching
. Chaque cas conduit à une contradiction en raison de l'ordre dans lequel DFS visite nœuds et le fait que matching
c'est un correspondance maximale .
Supposons que nous ayons une fonction pour exécuter ces étapes et renvoyer l'ensemble de nœuds qui comprend le couverture minimale des nœuds quand il reçoit le digrafo 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 coïncidence maximale des poids dans un graphe biparti pondéré.
Des problèmes comme celui qui apparaît au début de cet article peuvent être exprimés comme un problème affectation linéaire . Étant donné un ensemble de travailleurs, un ensemble de tâches et une fonction qui indique 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 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 du temps O(N^{4})
, (où N
est le nombre de nœuds dans le digrafo représentant le problème). Une implémentation plus simple à expliquer prend O (N ^ {5})
(pour une version qui régénère digraphos ) et O (N ^ {4})
for (pour une version qui garde le digraphos ). Ceci est similaire aux deux implémentations différentes de l'algorithme Edmonds-Karp .
Pour cette description, je travaille uniquement avec des graphiques bipartites complets (ceux où la moitié des nœuds font partie de la 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 ce problème est facile à résoudre; Je parle de la façon de le faire dans la dernière section.
La version de l'algorithme décrite ici utilise le concept utile de arcs de poids zéro . Malheureusement, ce concept n'a de sens que lorsque nous résolvons un minimisation (Si, au lieu de maximiser les avantages de nos affectations de tâches, nous minimisons plutôt le coût de ces affectations).
qu'est-ce qu'un architecte de solutions certifié aws
Heureusement, il est facile de convertir un problème d'affectation linéaire maximum en un problème d'affectation linéaire minimal établissant chacun des poids de arc a
dans M-a.datum.weight
où M=max({a.datum.weight for a in G.setOfArcs})
. La solution au problème de maximisation l'original sera identique à la solution minimiser le problème après le changement des poids d'Arc . Donc pour le reste, supposons que nous fassions ce changement.
La Algorithme de Kuhn-Munkres résoudre poids minimum dans un graphique bi-partitionné pondéré par une séquence de matchs maximum dans les graphiques non pondéré bipartite. Si on en trouve un match parfait dans la représentation du digraphe du problème d'affectation linéaire et si le poids de chacun arc dans la coïncidence est zéro, nous avons donc trouvé le poids minimum poids puisque cette coïncidence suggère que tout nœuds dans le digraphos ils ont été joint pour un arc avec le coût le plus bas possible (aucun coût ne peut être inférieur à 0, selon les définitions précédentes).
Aucun autre arcs peut être ajouté au coïncidence (Puisque toutes nœuds sont déjà jumelés) et non arcs doit être retiré du coïncident car tout remplacement possible arc aura au moins une valeur pondérale aussi élevée.
Si nous trouvons un correspondance maximale du subgrafo de G
contenant seulement arcs de poids zéro , et ce n'est pas un match parfait , nous n'avons pas de solution complète (puisque la combinaison ce n'est pas parfaite ). Cependant, nous pouvons produire un nouveau digrafo H
changer les poids de arcs dans G.setOfArcs
pour que le nouveau poids 0 apparaisse arcs et la solution optimale de 'H' est la même que la solution optimale de 'G'. Puisque nous garantissons qu'au moins un arc poids zéro se produit à chaque itération, nous garantissons que nous atteindrons un match parfait en pas plus de | G.setOfNodes | 2 = N 2 dans de telles itérations.
Supposons que dans le bipartition bipartition
, bipartition.firstPart
contient nœuds représentant les travailleurs, et bipartition.secondPart
Cela représente nœuds qui en même temps représente une preuve.
L'algorithme commence par générer un nouveau digrafo H
. H.setOfNodes = G.setOfNodes
. Certains arcs dans H
ils sont nœuds n
généré dans bipartition.firstPart
. Chaque 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 par nœuds n
dans bipartition.secondPart
. Chacun de ces nœuds 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: Puis formez un nouveau digrafo K
composé uniquement du arcs de poids zéro de H
, et le nœuds incident dans ces arcs . Formez un bipartition
dans les nœuds dans K
, puis utilisez solve_mbm( bipartition )
Pour en avoir un combinaison maximale (matching
) dans K
. Si matching
c'est une combinaison parfaite dans H
(Les arcs dans matching
ils sont incident dans tous les nœuds dans H.setOfNodes
) puis matching
est une solution optimale pour problème d'affectation linéaire .
Sinon, si matching
ce n'est pas parfait , génère le couverture minimale des nœuds de K
en utilisant node_cover = get_min_node_cover(matching, bipartition(K))
. Définissez ensuite 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 ci-dessus agit comme un opérateur XOR . Puis arcs = arcs1.union(arcs2.union(arcs3))
. Puis H=DiGraph(nodes,arcs)
. Revenir à la marque KMA . L'algorithme continue jusqu'à ce qu'un combinaison parfaite . Est combinaison est aussi la solution pour 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 combinaison maximale matching
à chaque itération; similaire aux deux implémentations précédentes de Edmonds-Karp Cet algorithme peut être modifié pour suivre le match 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 être exécutée sur 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 qui a motivé ce blog.
Aucune des opérations sur les poids du arc modifie l'affectation finale renvoyée par l'algorithme. Par conséquent: puisque nos graphiques d'entrée sont toujours graphiques bi-partitionnés complets , une solution doit attribuer à chaque nœud d'une partition à l'autre nœud dans la deuxième partition, à travers le arc entre ces deux nœuds . Notez que les opérations effectuées sur les poids de arc ne changez jamais l'ordre (trié par poids) des arcs incidents à aucun nœud particulier .
Par conséquent, lorsque l'algorithme se termine par un bi-partition parfaite et complète , chaque nœud se voit attribuer un poids d'arc nul , puisque l'ordre relatif des arcs de quoi nœud n'a pas changé pendant l'algorithme et depuis un arc poids zéro c'est l'arc le moins cher possible et le complément bipartite complet parfait garantit qu'il y a un arc pour chaque nœud . Cela signifie que la solution générée est en fait la même que la solution du problème cartographie linéaire d'origine sans aucune modification des poids de arc .
Allocations déséquilibrées
Il semble que l'algorithme soit assez limité, car comme décrit, il n'opère que sur graphiques bipartites complets (ceux où la moitié des nœuds font partie de la bipartition et l'autre moitié dans la seconde partie). Chez l'ouvrier, la motivation des tâches signifie qu'il y a autant d'ouvriers qu'il y a de tâches (cela semble assez limitant).
Cependant, il existe une transformation simple 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 graphique bipartite complet ). Chaque travailleur fictif a un arc dirigé par le travailleur vers chacune des tâches. Chacun de ces arc a un poids 0 (le placer dans un match ne donne aucun avantage supplémentaire). Après ce changement, le graphique est un graphique bipartite complet que nous pouvons résoudre. Aucune tâche assignée à un travailleur fictif n'est démarrée.
Supposons qu'il y ait plus de tâches que de travailleurs. Nous avons ajouté quelques tâches factices (assez pour faire du graphique résultant un graphe bi-partitionné complet ). Chaque tâche fictive a un arc dirigé de chaque travailleur vers la tâche fictive. Chacun de ces arc il a un poids de 0 (le placer dans un match ne donne aucun avantage supplémentaire). Après ce changement, le graphique est un graphe bi-partitionné complet que nous pouvons résoudre. Tout travailleur affecté à la tâche fictive n'est pas employé pendant la période.
Exemple d'affectation linéaire
Enfin, nous allons faire un exemple avec le code que j'ai utilisé. Je vais modifier l'exemple de problème de ici . Nous avons 3 tâches: nous avons besoin nettoyer la salle de bain , balayer le sol , Y nettoyer les vitres .
Les travailleurs disponibles sont Alice , Bob , Charlie Oui 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 entrer une tâche factice pour le digraphe qui représente le problème bipartite.
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 $
En supposant que le problème est codé dans un digrafo , alors kuhn_munkres( bipartition(G) )
résoudra le problème et retournera la mission. Il est facile de vérifier que l'allocation optimale (coût le plus bas) coûtera 5 $.
Voici une visualisation de la solution que le code ci-dessus génère:


C'est tout . 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 .