Arus Maksimum dan Masalah Penugasan Linier
Diterbitkan: 2022-03-11Inilah masalahnya: Bisnis Anda menugaskan kontraktor untuk memenuhi kontrak. Anda melihat daftar nama Anda dan memutuskan kontraktor mana yang tersedia untuk pertunangan satu bulan dan Anda melihat melalui kontrak yang tersedia untuk melihat mana di antara mereka yang untuk tugas satu bulan. Mengingat bahwa Anda tahu seberapa efektif setiap kontraktor dapat memenuhi setiap kontrak, bagaimana Anda menugaskan kontraktor untuk memaksimalkan efektivitas keseluruhan untuk bulan itu?
Ini adalah contoh dari masalah penugasan, dan masalah tersebut dapat diselesaikan dengan algoritma Hungaria klasik.
Algoritma Hungaria (juga dikenal sebagai algoritma Kuhn-Munkres) adalah algoritma waktu polinomial yang memaksimalkan pencocokan bobot dalam graf bipartit berbobot. Di sini, kontraktor dan kontrak dapat dimodelkan sebagai grafik bipartit, dengan efektivitasnya sebagai bobot tepi antara kontraktor dan simpul kontrak.
Pada artikel ini, Anda akan belajar tentang implementasi algoritma Hungaria yang menggunakan algoritma Edmonds-Karp untuk menyelesaikan masalah penugasan linier. Anda juga akan mempelajari bagaimana algoritma Edmonds-Karp adalah sedikit modifikasi dari metode Ford-Fulkerson dan bagaimana modifikasi ini penting.
Masalah Aliran Maksimum
Masalah aliran maksimum itu sendiri dapat digambarkan secara informal sebagai masalah pemindahan sejumlah fluida atau gas melalui jaringan pipa dari satu sumber ke satu terminal. Hal ini dilakukan dengan asumsi bahwa tekanan dalam jaringan cukup untuk memastikan bahwa cairan atau gas tidak dapat berlama-lama di sepanjang pipa atau fitting pipa (tempat-tempat di mana panjang pipa yang berbeda bertemu).
Dengan membuat perubahan tertentu pada grafik, masalah penugasan dapat diubah menjadi masalah aliran maksimum.
Persiapan
Ide-ide yang diperlukan untuk memecahkan masalah ini muncul dalam banyak disiplin matematika dan teknik, seringkali konsep serupa dikenal dengan nama yang berbeda dan diekspresikan dengan cara yang berbeda (misalnya, matriks ketetanggaan dan daftar ketetanggaan). Karena ide-ide ini cukup esoteris, pilihan dibuat mengenai seberapa umum konsep-konsep ini akan didefinisikan untuk pengaturan tertentu.
Artikel ini tidak akan mengasumsikan pengetahuan sebelumnya di luar sedikit teori himpunan pengantar.
Implementasi pada postingan ini merepresentasikan permasalahan sebagai graf berarah (digraph).
DiGraphs
Digraf memiliki dua atribut, setOfNodes dan setOfArcs . Kedua atribut ini adalah himpunan (koleksi tidak berurutan). Di blok kode pada posting ini, saya sebenarnya menggunakan frozenset Python, tetapi detail itu tidak terlalu penting.
DiGraph = namedtuple('DiGraph', ['setOfNodes','setOfArcs'])
(Catatan: Ini, dan semua kode lain dalam artikel ini, ditulis dengan Python 3.6.)
Node
Sebuah node n
terdiri dari dua atribut:
n.uid
: Pengidentifikasi unik.Ini berarti bahwa untuk setiap dua simpul
x
dany
,
x.uid != y.uid
-
n.datum
: Ini mewakili objek data apa pun.
Node = namedtuple('Node', ['uid','datum'])
busur
Busur a
terdiri dari tiga atribut:
a.fromNode
: Ini adalah node , seperti yang didefinisikan di atas.a.toNode
: Ini adalah node , seperti yang didefinisikan di atas.a.datum
: Ini mewakili objek data apa pun.
Arc = namedtuple('Arc', ['fromNode','toNode','datum'])
Himpunan busur di digraf mewakili relasi biner pada simpul di digraf . Adanya arc a
menyiratkan bahwa ada hubungan antara a.fromNode
dan a.toNode
.
Dalam graf berarah (berlawanan dengan graf tak berarah), keberadaan hubungan antara a.fromNode
dan a.toNode
tidak menyiratkan bahwa ada hubungan serupa antara a.toNode
dan a.fromNode
.
Hal ini karena pada graf tak berarah, hubungan yang diekspresikan belum tentu simetris.
DiGraphs
Node dan arc dapat digunakan untuk mendefinisikan digraph , tetapi untuk kemudahan, dalam algoritme di bawah ini, digraph akan direpresentasikan sebagai kamus.
Berikut adalah metode yang dapat mengubah representasi grafik di atas menjadi representasi kamus yang mirip dengan ini:
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
Dan inilah satu lagi yang mengubahnya menjadi kamus kamus, operasi lain yang akan berguna:
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
Ketika artikel berbicara tentang digraf yang diwakili oleh kamus, artikel tersebut akan menyebutkan G_as_dict
untuk merujuknya.
Terkadang sangat membantu untuk mengambil simpul dari digraf G
melalui uid
(pengidentifikasi unik):
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()
Dalam mendefinisikan graf, orang terkadang menggunakan istilah simpul dan simpul untuk merujuk pada konsep yang sama; hal yang sama berlaku untuk istilah busur dan tepi.
Dua representasi grafik populer di Python adalah yang satu ini menggunakan kamus dan satu lagi yang menggunakan objek untuk mewakili grafik. Representasi dalam posting ini berada di antara dua representasi yang umum digunakan ini.
Ini adalah representasi digraph saya. Ada banyak yang seperti itu, tapi yang ini milikku.
Jalan dan Jalan
Misalkan S_Arcs
merupakan barisan berhingga (kumpulan terurut) dari busur -busur dalam digraf G
sedemikian sehingga jika a
adalah sembarang busur dalam S_Arcs
kecuali yang terakhir, dan b
mengikuti a
dalam barisan tersebut, maka harus ada simpul n = a.fromNode
di G.setOfNodes
sedemikian rupa sehingga a.toNode = b.fromNode
.
Mulai dari busur pertama di S_Arcs
, dan berlanjut hingga busur terakhir di S_Arcs
, kumpulkan (dalam urutan yang ditemukan) semua node n
seperti yang didefinisikan di atas di antara masing-masing dua busur berurutan di S_Arcs
. Beri label kumpulan node yang diurutkan yang dikumpulkan selama operasi ini 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
Jika ada simpul yang muncul lebih dari satu kali dalam urutan
S_Nodes
maka panggilS_Arcs
a Walk pada digrafG
.Jika tidak, panggil
S_Arcs
jalur darilist(S_Nodes)[0]
kelist(S_Nodes)[-1]
pada digraphG
.
Node Sumber
Panggil simpul s
sebagai simpul sumber dalam digraf G
jika s
ada di G.setOfNodes
dan G.setOfArcs
tidak mengandung busur a
sedemikian rupa sehingga 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
simpul terminal
Panggil simpul t
sebagai simpul terminal dalam digraf G
jika t
ada di G.setOfNodes
dan G.setOfArcs
tidak mengandung busur a
sedemikian rupa sehingga 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
Pemotongan dan Pemotongan st
Potongan cut
dari digraf terhubung G
adalah subset dari busur dari G.setOfArcs
yang mempartisi himpunan node G.setOfNodes
di G
. G
terhubung jika setiap simpul n
di G.setOfNodes
dan memiliki setidaknya satu busur a
di G.setOfArcs
sedemikian rupa sehingga n = a.fromNode
atau n = a.toNode
, tetapi a.fromNode != a.toNode
.
Cut = namedtuple('Cut', ['setOfCutArcs'])
Definisi di atas mengacu pada subset dari busur , tetapi juga dapat mendefinisikan partisi dari node G.setOfNodes
.
Untuk fungsi predecessors_of
dan successors_of
, n
adalah simpul di himpunan G.setOfNodes dari digraf G
, dan cut
adalah potongan dari 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
Biarkan cut
menjadi potongan digraf 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
adalah potongan digraf G
jika: (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
disebut xy cut if (x in get_first_part(cut, G)) and (y in get_second_part(cut, G) ) and (x != y)
. Jika simpul x
pada cut
xy adalah simpul sumber dan simpul y
pada pemotongan xy adalah simpul terminal , maka pemotongan ini disebut pemotongan st .
STCut = namedtuple('STCut', ['s','t','cut'])
Jaringan Aliran
Anda dapat menggunakan digraf G
untuk mewakili jaringan aliran.
Tetapkan setiap node n
, di mana n
berada di G.setOfNodes
sebuah n.datum
yang merupakan FlowNodeDatum
:
FlowNodeDatum = namedtuple('FlowNodeDatum', ['flowIn','flowOut'])
Tetapkan setiap busur a
, di mana a
ada di G.setOfArcs
dan a.datum
yang merupakan FlowArcDatum
.
FlowArcDatum = namedtuple('FlowArcDatum', ['capacity','flow'])
flowNodeDatum.flowIn
dan flowNodeDatum.flowOut
adalah bilangan real positif.
flowArcDatum.capacity
dan flowArcDatum.flow
juga bilangan real positif.
Untuk setiap node node n
di 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})
Digraf G
sekarang mewakili jaringan aliran .
Aliran G
mengacu pada a.flow
untuk semua busur a
di G
.
Aliran yang Layak
Biarkan digraf G
mewakili jaringan aliran .
Jaringan aliran yang diwakili oleh G
memiliki aliran yang layak jika:
Untuk setiap node
n
diG.setOfNodes
kecuali node sumber dan node terminal :n.datum.flowIn = n.datum.flowOut
.Untuk setiap busur
a
diG.setOfNodes
:a.datum.capacity <= a.datum.flow
.
Kondisi 1 disebut kendala konservasi.
Kondisi 2 disebut batasan kapasitas.
Kapasitas potong
Kapasitas potong dari st cut stCut
dengan node sumber s
dan node terminal t
dari jaringan aliran yang diwakili oleh digraf G
adalah:
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
Potongan Kapasitas Minimum
Biarkan stCut = stCut(s,t,cut)
menjadi potongan pertama dari jaringan aliran yang diwakili oleh digraf G
.
stCut
adalah pemotongan kapasitas minimum dari jaringan aliran yang diwakili oleh G
jika tidak ada stCutCompetitor stCutCompetitor
dalam jaringan aliran ini sehingga:
cut_capacity(stCut, G) < cut_capacity(stCutCompetitor, G)
Melucuti Arusnya
Saya ingin merujuk ke digraf yang akan menjadi hasil dari mengambil digraf G
dan menghapus semua aliran data dari semua node di G.setOfNodes
dan juga semua busur di 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)
Masalah Aliran Maksimum
Jaringan aliran direpresentasikan sebagai digraf G
, simpul sumber s
di G.setOfNodes
dan simpul terminal t
di G.setOfNodes
, G
dapat mewakili masalah aliran maksimum jika:
(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)
Beri label representasi ini:
MaxFlowProblemState = namedtuple('MaxFlowProblemState', ['G','sourceNodeUid','terminalNodeUid','maxFlowProblemStateUid'])
Di mana sourceNodeUid = s.uid
, terminalNodeUid = t.uid
, dan maxFlowProblemStateUid
adalah pengidentifikasi untuk instance masalah.
Solusi Aliran Maksimum
Biarkan maxFlowProblem
mewakili masalah aliran maksimum . Solusi untuk maxFlowProblem
dapat diwakili oleh jaringan aliran yang direpresentasikan sebagai digraf H
.
Digraph H
adalah solusi yang layak untuk masalah aliran maksimum pada input python maxFlowProblem
jika:
strip_flows(maxFlowProblem.G) == strip_flows(H)
.H
adalah jaringan aliran dan memiliki aliran yang layak .
Jika selain 1 dan 2:
- Tidak ada jaringan aliran lain yang diwakili oleh digraf
K
sepertistrip_flows(G) == strip_flows(K)
danfind_node_by_uid(t.uid,G).flowIn < find_node_by_uid(t.uid,K).flowIn
.
Maka H
juga merupakan solusi optimal untuk maxFlowProblem
.
Dengan kata lain solusi aliran maksimum yang layak dapat direpresentasikan oleh sebuah digraf , yang:
Identik dengan digraf
G
dari masalah aliran maksimum yang sesuai dengan pengecualian bahwan.datum.flowIn
,n.datum.flowOut
dana.datum.flow
dari setiap node dan busur mungkin berbeda.Merupakan jaringan aliran yang memiliki aliran yang layak .
Dan, itu dapat mewakili solusi aliran maksimum yang optimal jika tambahan:
-
flowIn
untuk node yang berkorespondensi dengan terminal node dalam masalah aliran maksimum adalah sebesar mungkin (bila kondisi 1 dan 2 masih terpenuhi).
Jika digraf H
mewakili solusi aliran maksimum yang layak : find_node_by_uid(s.uid,H).flowOut = find_node_by_uid(t.uid,H).flowIn
mengikuti dari aliran max, teorema min cut (dibahas di bawah). Secara informal, karena H
diasumsikan memiliki aliran yang layak , ini berarti bahwa aliran tidak dapat 'diciptakan' (kecuali pada simpul sumber s
) atau 'dihancurkan' (kecuali pada simpul terminal t
) saat melintasi simpul (lainnya) ( konstrain konservasi ).
Karena masalah aliran maksimum hanya berisi satu simpul sumber s
dan satu simpul terminal t
, semua aliran 'diciptakan' pada s
harus 'dihancurkan' pada t
atau jaringan aliran tidak memiliki aliran yang layak ( konstrain konservasi akan dilanggar ).
Biarkan digraf H
mewakili solusi aliran maksimum yang layak ; nilai di atas disebut st Flow Value dari H
.
Membiarkan:
mfps=MaxFlowProblemState(H, maxFlowProblem.sourceNodeUid, maxFlowProblem.terminalNodeUid, maxFlowProblem.maxFlowProblemStateUid)
Ini berarti bahwa mfps
adalah status penerus dari maxFlowProblem
, yang berarti bahwa mfps
seperti maxFlowProblem
dengan pengecualian bahwa nilai a.flow
untuk busur a
di maxFlowProblem.setOfArcs
mungkin berbeda dari a.flow
untuk busur a
di 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 st flow') return flow_to_t
Berikut adalah visualisasi mfps
beserta maxFlowProb
yang terkait. Setiap busur a
pada gambar memiliki label, label ini adalah a.datum.flowFrom / a.datum.flowTo
, setiap simpul n
pada gambar memiliki label, dan label ini adalah n.uid {n.datum.flowIn / a.datum.flowOut}
.
Aliran potong st
Biarkan mfps
mewakili MaxFlowProblemState
dan biarkan stCut
mewakili potongan mfps.G
. Aliran potong stCut
didefinisikan:
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
st cut flow adalah jumlah arus dari partisi yang berisi simpul sumber ke partisi yang berisi simpul terminal dikurangi jumlah arus dari partisi yang berisi simpul terminal ke partisi yang berisi simpul sumber .
Aliran Maks, Pemotongan Min
Biarkan maxFlowProblem
mewakili masalah aliran maksimum dan biarkan solusi untuk maxFlowProblem
diwakili oleh jaringan aliran yang direpresentasikan sebagai digraf H
.
Biarkan minStCut
menjadi potongan kapasitas minimum dari jaringan aliran yang diwakili oleh maxFlowProblem.G
.
Karena dalam masalah aliran maksimum , aliran hanya berasal dari satu simpul sumber dan berakhir pada satu simpul terminal dan, karena kendala kapasitas dan kendala konservasi , kita tahu bahwa semua aliran yang memasuki maxFlowProblem.terminalNodeUid
harus melewati setiap st cut , khususnya harus melewati minStCut
. Ini berarti:
get_flow_value(H, maxFlowProblem) <= cut_capacity(minStCut, maxFlowProblem.G)
Memecahkan Masalah Aliran Maksimum
Ide dasar untuk memecahkan masalah aliran maksimum maxFlowProblem
adalah memulai dengan solusi aliran maksimum yang diwakili oleh digraf H
. Titik awal seperti itu dapat berupa H = strip_flows(maxFlowProblem.G)
. Tugasnya kemudian menggunakan H
dan dengan beberapa modifikasi serakah dari nilai a.datum.flow
dari beberapa busur a
di H.setOfArcs
untuk menghasilkan solusi aliran maksimum lain yang diwakili oleh digraf K
sedemikian rupa sehingga K
masih tidak dapat mewakili jaringan aliran dengan aliran yang layak dan get_flow_value(H, maxFlowProblem) < get_flow_value(K, maxFlowProblem)
. Selama proses ini berlanjut, kualitas ( get_flow_value(K, maxFlowProblem)
) dari solusi aliran maksimum yang paling baru ditemui ( K
) lebih baik daripada solusi aliran maksimum lainnya yang telah ditemukan. Jika proses mencapai titik yang diketahui bahwa tidak ada perbaikan lain yang mungkin, proses dapat dihentikan dan akan mengembalikan solusi aliran maksimum yang optimal.
Uraian di atas bersifat umum dan melewatkan banyak bukti seperti apakah proses seperti itu mungkin atau berapa lama waktu yang dibutuhkan, saya akan memberikan beberapa detail lagi dan algoritme.
Aliran Maks, Teorema Potong Min
Dari buku Flows in Networks oleh Ford dan Fulkerson, pernyataan teorema aliran maks, teorema min cut (Teorema 5.1) adalah:
Untuk setiap jaringan, nilai aliran maksimal dari
s
ket
sama dengan kapasitas pemotongan minimum dari semua pemotongan yang memisahkans
dant
.
Menggunakan definisi dalam posting ini, yang diterjemahkan menjadi:
Solusi untuk maxFlowProblem
yang diwakili oleh jaringan aliran yang direpresentasikan sebagai digraf H
optimal jika:
get_flow_value(H, maxFlowProblem) == cut_capacity(minStCut, maxFlowProblem.G)
Saya suka bukti teorema ini dan Wikipedia memiliki satu lagi.
Teorema max flow, min cut digunakan untuk membuktikan kebenaran dan kelengkapan metode Ford-Fulkerson .
Saya juga akan memberikan bukti teorema di bagian setelah augmenting paths .
Metode Ford-Fulkerson dan Algoritma Edmonds-Karp
CLRS mendefinisikan metode Ford-Fulkerson seperti ini (bagian 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
Grafik sisa
Grafik Residu dari jaringan aliran yang direpresentasikan sebagai digraf G
dapat direpresentasikan sebagai digraf 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)
mengembalikan jumlaha.datum.capacity
untuk semua busur dalam subsetG.setOfArcs
di mana busura
berada di subset jikaa.fromNode = n
dana.toNode = u
.agg_n_to_u_cap(n,u,G_as_dict)
mengembalikan jumlaha.datum.flow
untuk semua busur dalam subsetG.setOfArcs
di mana busura
berada di subset jikaa.fromNode = n
dana.toNode = u
.
Secara singkat, graf residual G_f
mewakili tindakan tertentu yang dapat dilakukan pada digraf G
.
Setiap pasangan node n,u
dalam G.setOfNodes
dari jaringan aliran yang diwakili oleh digraf G
dapat menghasilkan 0, 1, atau 2 busur pada graf residual G_f
dari G
.
Pasangan
n,u
tidak menghasilkan busur apa pun diG_f
jika tidak ada busura
diG.setOfArcs
sedemikian rupa sehinggaa.fromNode = n
dana.toNode = u
.Pasangan
n,u
menghasilkan busura
diG_f.setOfArcs
di manaa
mewakili busur berlabel busur aliran dorong darin
keu
a = Arc(n,u,datum=ResidualNode(n_to_u_cap_sum - n_to_u_flow_sum))
jikan_to_u_cap_sum > n_to_u_flow_sum
.Pasangan
n,u
menghasilkan busura
diG_f.setOfArcs
di manaa
mewakili busur berlabel busur aliran tarik darin
keu
a = Arc(n,u,datum=ResidualNode(n_to_u_cap_sum - n_to_u_flow_sum))
jikan_to_u_flow_sum > 0.0
.
Setiap busur aliran push di
G_f.setOfArcs
mewakili tindakan penambahan totalx <= n_to_u_cap_sum - n_to_u_flow_sum
aliran ke busur di subset dariG.setOfArcs
di mana busura
berada di subset jikaa.fromNode = n
dana.toNode = u
Setiap busur aliran tarikan di
G_f.setOfArcs
mewakili tindakan pengurangan totalx <= n_to_u_flow_sum
aliran ke busur dalam himpunan bagian dariG.setOfArcs
di mana busura
berada dalam himpunan bagian jikaa.fromNode = n
dana.toNode = u
.
Melakukan tindakan push atau pull individu dari G_f
pada busur yang berlaku di G
mungkin menghasilkan jaringan aliran tanpa aliran yang layak karena kendala kapasitas atau kendala konservasi mungkin dilanggar dalam jaringan aliran yang dihasilkan .
Berikut adalah visualisasi grafik residual dari contoh visualisasi sebelumnya dari solusi aliran maksimum , dalam visualisasi masing-masing busur a
mewakili a.residualCapacity
.
Menambah Jalur
Biarkan maxFlowProblem
menjadi masalah aliran maks , dan biarkan G_f = get_residual_graph_of(G)
menjadi grafik residual dari maxFlowProblem.G
.
Jalur augmenting augmentingPath
untuk maxFlowProblem
adalah jalur apa pun dari find_node_by_uid(maxFlowProblem.sourceNode,G_f)
ke find_node_by_uid(maxFlowProblem.terminalNode,G_f)
.
Ternyata augmenting path augmentingPath
dapat diterapkan ke solusi aliran maks yang diwakili oleh digraf H
menghasilkan solusi aliran maks lainnya yang diwakili oleh digraf K
di mana get_flow_value(H, maxFlowProblem) < get_flow_value(K, maxFlowProblem)
jika H
tidak optimal .
Berikut caranya:
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
Di atas, TOL
adalah beberapa nilai toleransi untuk pembulatan nilai aliran dalam jaringan. Hal ini untuk menghindari ketidaktepatan cascading perhitungan floating point. Jadi, misalnya, saya menggunakan TOL = 10
yang berarti membulatkan hingga 10 angka penting.
Biarkan K = augment(augmentingPath, H)
, maka K
mewakili solusi aliran maks yang layak untuk maxFlowProblem
. Agar pernyataan benar, jaringan aliran yang diwakili oleh K
harus memiliki aliran yang layak (tidak melanggar batasan kapasitas atau batasan konservasi .
Inilah alasannya: Dalam metode di atas, setiap simpul yang ditambahkan ke jaringan aliran baru yang diwakili oleh digraf K
adalah salinan tepat dari simpul dari digraf H
atau simpul n
yang memiliki nomor yang sama ditambahkan ke n.datum.flowIn
sebagai itu n.datum.flowOut
. Ini berarti bahwa kendala konservasi terpenuhi di K
selama itu dipenuhi di H
. Batasan konservasi terpenuhi karena kami secara eksplisit memeriksa bahwa setiap busur baru a
dalam jaringan memiliki a.datum.flow <= a.datum.capacity
; jadi, selama busur dari himpunan H.setOfArcs
yang disalin tanpa dimodifikasi ke K.setOfArcs
tidak melanggar batasan kapasitas , maka K
tidak melanggar batasan kapasitas .
Benar juga bahwa get_flow_value(H, maxFlowProblem) < get_flow_value(K, maxFlowProblem)
jika H
tidak optimal .
Inilah alasannya: Agar jalur augmentingPath
ada dalam representasi digraf dari grafik residual G_f
dari masalah aliran maks maxFlowProblem
maka busur terakhir a
pada augmentingPath
harus berupa busur 'push' dan harus memiliki a.toNode == maxFlowProblem.terminalNode
. Jalur augmentasi didefinisikan sebagai jalur yang berakhir pada simpul terminal dari masalah aliran maksimum yang merupakan jalur augmentasi . Dari definisi graf residual , jelas bahwa busur terakhir dalam jalur augmentasi pada graf residual tersebut harus berupa busur 'push' karena setiap busur ' tarik' b
di jalur augmentasi akan memiliki b.toNode == maxFlowProblem.terminalNode
dan b.fromNode != maxFlowProblem.terminalNode
dari definisi path . Selain itu, dari definisi path , jelas bahwa simpul terminal hanya dimodifikasi satu kali dengan metode augment
. Jadi augment
memodifikasi maxFlowProblem.terminalNode.flowIn
tepat satu kali dan itu meningkatkan nilai maxFlowProblem.terminalNode.flowIn
karena busur terakhir di augmentingPath
harus menjadi busur yang menyebabkan modifikasi di maxFlowProblem.terminalNode.flowIn
selama augment
. Dari definisi augment
yang berlaku untuk 'push' arcs , maxFlowProblem.terminalNode.flowIn
hanya dapat ditingkatkan, tidak dikurangi.
Beberapa Bukti dari Sedgewick dan Wayne
Buku Algorithms, edisi keempat oleh Robert Sedgewich dan Kevin Wayne memiliki beberapa bukti yang bagus dan singkat (halaman 892-894) yang akan berguna. Saya akan membuatnya ulang di sini, meskipun saya akan menggunakan bahasa yang sesuai dengan definisi sebelumnya. Label saya untuk buktinya sama dengan di buku Sedgewick.
Proposisi E: Untuk setiap digraf H
yang mewakili solusi aliran maksimum yang layak untuk masalah aliran maksimum maxFlowProblem
, untuk setiap stCut
get_stcut_flow(stCut,H,maxFlowProblem) = get_flow_value(H, maxFlowProblem)
.
Bukti: Biarkan stCut=stCut(maxFlowProblem.sourceNode,maxFlowProblem.terminalNode,set([a for a in H.setOfArcs if a.toNode == maxFlowProblem.terminalNode]))
. Proposisi E berlaku untuk stCut
langsung dari definisi nilai aliran st . Misalkan di sana kita ingin memindahkan beberapa node n
dari partisi-s ( get_first_part(stCut.cut, G)
) dan ke dalam partisi-t (get_second_part(stCut.cut, G))
, untuk melakukannya kita perlu mengubah stCut.cut
, yang dapat mengubah stcut_flow = get_stcut_flow(stCut,H,maxFlowProblem)
dan membatalkan proposisi E . Namun, mari kita lihat bagaimana nilai stcut_flow
akan berubah saat kita melakukan perubahan ini. simpul n
berada pada kesetimbangan yang berarti bahwa jumlah aliran ke simpul n
sama dengan jumlah aliran keluar (ini diperlukan untuk H
untuk mewakili solusi yang layak ). Perhatikan bahwa semua aliran yang merupakan bagian dari stcut_flow
yang memasuki simpul n
memasukinya dari partisi s (aliran yang memasuki simpul n
dari partisi t baik secara langsung maupun tidak langsung tidak akan dihitung dalam nilai stcut_flow
karena menuju ke arah yang salah berdasarkan definisi). Selain itu, semua aliran keluar n
akhirnya (langsung atau tidak langsung) mengalir ke simpul terminal (dibuktikan sebelumnya). Ketika kita memindahkan node n
ke dalam partisi-t, semua aliran yang masuk n
dari partisi-s harus ditambahkan ke nilai stcut_flow
yang baru; namun, semua aliran yang keluar dari n
harus dikurangi dari nilai stcut_flow
yang baru; bagian aliran yang menuju langsung ke partisi-t dikurangi karena aliran ini sekarang internal ke partisi-t baru dan tidak dihitung sebagai stcut_flow
. Bagian aliran dari n
menuju ke node di partisi s juga harus dikurangi dari stcut_flow
: Setelah n
dipindahkan ke partisi t, aliran ini akan diarahkan dari partisi t ke partisi s dan seterusnya tidak boleh diperhitungkan dalam stcut_flow
, karena aliran ini dihilangkan aliran masuk ke partisi s dan harus dikurangi dengan jumlah aliran ini, dan aliran keluar dari partisi s ke partisi t (di mana semua mengalir dari st harus berakhir) harus dikurangi dengan jumlah yang sama. Karena simpul n
berada pada keseimbangan sebelum proses, pembaruan akan menambahkan nilai yang sama ke nilai stcut_flow
baru saat dikurangi sehingga meninggalkan proposisi E benar setelah pembaruan. Validitas proposisi E kemudian mengikuti dari induksi pada ukuran partisi-t.
Berikut adalah beberapa contoh jaringan aliran untuk membantu memvisualisasikan kasus yang kurang jelas di mana proposisi E berlaku; pada gambar, area merah menunjukkan partisi-s, area biru mewakili partisi-t, dan busur hijau menunjukkan potongan st . Pada gambar kedua, aliran antara node A dan node B meningkat sedangkan aliran ke terminal node t tidak berubah.:

Akibat wajar: Tidak ada nilai aliran pemotongan st yang dapat melebihi kapasitas pemotongan st mana pun .
Proposisi F. (aliran maks, teorema potong min): Biarkan f
menjadi aliran st . 3 kondisi berikut ini setara:
Terdapat sebuah st cut yang kapasitasnya sama dengan nilai aliran
f
.f
adalah aliran maksimum .Tidak ada jalur augmentasi sehubungan dengan
f
.
Kondisi 1 menyiratkan kondisi 2 secara wajar. Kondisi 2 menyiratkan kondisi 3 karena keberadaan jalur tambahan menyiratkan adanya aliran dengan nilai yang lebih besar, bertentangan dengan maksimalitas f
. Kondisi 3 menyiratkan kondisi 1: Biarkan C_s
menjadi himpunan semua node yang dapat dicapai dari s
dengan jalur augmentasi di grafik residual . Biarkan C_t
menjadi busur yang tersisa , maka t
harus berada di C_t
(dengan asumsi kami). Busur yang melintasi dari C_s
ke C_t
kemudian membentuk potongan st yang hanya berisi busur a
di mana a.datum.capacity = a.datum.flow
atau a.datum.flow = 0.0
. Jika sebaliknya, maka simpul- simpul yang dihubungkan oleh busur dengan sisa kapasitas ke C_s
akan berada dalam himpunan C_s
karena akan ada jalur tambahan dari s
ke simpul semacam itu. Aliran melintasi potongan pertama sama dengan kapasitas potongan pertama (karena busur dari C_s
ke C_t
memiliki aliran yang sama dengan kapasitas) dan juga dengan nilai aliran pertama (dengan proposisi E ).
Pernyataan aliran maks, teorema min cut ini menyiratkan pernyataan sebelumnya dari Flows in Networks.
Akibat wajar (properti integral): Ketika kapasitas adalah bilangan bulat, terdapat aliran maks bernilai bilangan bulat, dan algoritma Ford-Fulkerson menemukannya.
Bukti: Setiap jalur augmentasi meningkatkan aliran dengan bilangan bulat positif, kapasitas minimum yang tidak terpakai di busur 'push' dan arus di busur 'tarik' , yang semuanya selalu bilangan bulat positif.
Ini membenarkan deskripsi metode Ford-Fulkerson dari CLRS . Metodenya adalah terus menemukan jalur augmentasi dan menerapkan augment
ke maxFlowSolution
terbaru yang menghasilkan solusi yang lebih baik, hingga tidak ada lagi jalur augmentasi yang berarti bahwa solusi aliran maksimum terbaru adalah optimal .
Dari Ford-Fulkerson ke Edmonds-Karp
Pertanyaan yang tersisa mengenai pemecahan masalah aliran maksimum adalah:
Bagaimana seharusnya jalur tambahan dibangun?
Apakah metode akan berhenti jika kita menggunakan bilangan real dan bukan bilangan bulat?
Berapa lama waktu yang dibutuhkan untuk mengakhiri (jika ya)?
Algoritme Edmonds-Karp menetapkan bahwa setiap jalur tambahan dibangun oleh pencarian pertama yang luas (BFS) dari grafik sisa ; ternyata keputusan poin 1 di atas ini juga akan memaksa algoritma untuk berhenti (poin 2) dan memungkinkan untuk menentukan kompleksitas ruang dan waktu asimtotik.
Pertama, inilah implementasi 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
Saya menggunakan deque dari modul koleksi python.
Untuk menjawab pertanyaan 2 di atas, saya akan memparafrasekan bukti lain dari Sedgewick dan Wayne: Proposisi G. Jumlah jalur augmenting yang dibutuhkan dalam algoritma Edmonds-Karp dengan N
node dan A
arc paling banyak NA/2
. Bukti: Setiap jalur augmenting memiliki busur bottleneck - busur yang dihapus dari grafik residual karena sesuai dengan busur 'push' yang menjadi terisi penuh atau busur 'tarik' yang melaluinya aliran menjadi 0. Setiap kali arc menjadi bottleneck arc , panjang dari setiap augmenting path yang melaluinya harus bertambah dengan faktor 2. Hal ini karena setiap node dalam suatu path mungkin muncul hanya sekali atau tidak sama sekali (dari definisi path ) karena path sedang dieksplorasi dari jalur terpendek ke terpanjang itu berarti bahwa setidaknya satu node lagi harus dikunjungi oleh jalur berikutnya yang melewati node bottleneck tertentu yang berarti tambahan 2 busur di jalur sebelum kita tiba di node . Karena jalur augmentasi paling panjang N
setiap busur dapat berada di paling banyak N/2
jalur augmentasi , dan jumlah total jalur augmentasi paling banyak NA/2
.
Algoritma Edmonds-Karp dieksekusi dalam O(NA^2)
. Jika paling banyak jalur NA/2
akan dieksplorasi selama algoritma dan menjelajahi setiap jalur dengan BFS adalah N+A
maka suku yang paling signifikan dari produk dan karenanya kompleksitas asimtotiknya adalah O(NA^2)
.
Biarkan mfp
menjadi 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
Versi di atas tidak efisien dan memiliki kompleksitas yang lebih buruk daripada O(NA^2)
karena ia membangun solusi aliran maksimum baru dan grafik residual baru setiap kali (daripada memodifikasi digraf yang ada seiring kemajuan algoritma). Untuk mendapatkan solusi O(NA^2)
yang sebenarnya, algoritme harus mempertahankan digraf yang mewakili status masalah aliran maksimum dan grafik residual yang terkait. Jadi algoritme harus menghindari iterasi pada busur dan simpul yang tidak perlu dan memperbarui nilainya dan nilai terkait dalam grafik residual hanya jika diperlukan.
Untuk menulis algoritma Edmonds Karp yang lebih cepat, saya menulis ulang beberapa potongan kode dari atas. Saya harap membaca kode yang menghasilkan digraf baru dapat membantu dalam memahami apa yang terjadi. Dalam algoritma cepat, saya menggunakan beberapa trik baru dan struktur data Python yang tidak ingin saya bahas secara mendetail. Saya akan mengatakan bahwa a.fromNode
dan a.toNode
sekarang diperlakukan sebagai string dan uid ke node . Untuk kode ini, biarkan mfps
menjadi 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 st 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
Berikut adalah visualisasi bagaimana algoritma ini memecahkan contoh jaringan aliran dari atas. Visualisasi menunjukkan langkah-langkah sebagaimana tercermin dalam digraf G
yang mewakili jaringan aliran paling mutakhir dan sebagaimana tercermin dalam grafik sisa jaringan aliran tersebut. Jalur augmentasi dalam grafik residual ditampilkan sebagai jalur merah, dan digraf yang mewakili masalah himpunan simpul dan busur yang dipengaruhi oleh jalur augmentasi yang diberikan disorot dalam warna hijau. Dalam setiap kasus, saya akan menyoroti bagian grafik yang akan diubah (merah atau hijau) dan kemudian menunjukkan grafik setelah perubahan (hanya dalam warna hitam).
Berikut visualisasi lain tentang bagaimana algoritma ini memecahkan contoh jaringan aliran yang berbeda. Perhatikan bahwa yang ini menggunakan bilangan real dan berisi banyak busur dengan nilai fromNode
dan toNode
yang sama.
**Perhatikan juga bahwa karena Busur dengan ResidualDatum 'menarik' mungkin menjadi bagian dari Jalur Augmentasi, node yang terpengaruh di DiGraph Jaringan Terbang _mungkin tidak berada di jalur di G!
.
Grafik Bipartit
Misalkan kita memiliki digraf G
, G
adalah bipartit jika node di G.setOfNodes
dapat dipartisi menjadi dua set ( part_1
dan part_2
) sehingga untuk sembarang busur a
di G.setOfArcs
tidak mungkin benar bahwa a.fromNode
di part_1
dan a.toNode
di part_1
. Juga tidak mungkin benar bahwa a.fromNode
di part_2
dan a.toNode
di part_2
.
Dengan kata lain G
adalah bipartit jika dapat dipartisi menjadi dua set node sedemikian rupa sehingga setiap busur harus menghubungkan sebuah node dalam satu set ke node di set lainnya.
Menguji Bipartit
Misalkan kita memiliki digraf G
, kita ingin menguji apakah itu bipartit . Kita bisa melakukannya di O(|G.setOfNodes|+|G.setOfArcs|)
dengan mewarnai graf menjadi dua warna.
Pertama, kita perlu membangkitkan digraf baru H
. Grafik ini akan memiliki himpunan simpul yang sama dengan G
, tetapi akan memiliki lebih banyak busur daripada G
. Setiap busur a
di G
akan membuat 2 busur di H
; busur pertama akan identik dengan a
, dan busur kedua membalikkan direktur 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)
Pencocokan dan Pencocokan Maksimum
Misalkan kita memiliki digraf G
dan matching
adalah subset dari busur dari G.setOfArcs
. matching
adalah pencocokan jika untuk dua busur a
dan b
dalam matching
: len(frozenset( {a.fromNode} ).union( {a.toNode} ).union( {b.fromNode} ).union( {b.toNode} )) == 4
. Dengan kata lain, tidak ada dua busur dalam pencocokan yang berbagi simpul .
Matching matching
, adalah pencocokan maksimum jika tidak ada alt_matching
lain yang cocok di G
sehingga len(matching) < len(alt_matching)
. Dengan kata lain, matching
adalah pencocokan maksimum jika himpunan busur terbesar dari G.setOfArcs
masih memenuhi definisi pencocokan (penambahan busur yang belum ada dalam pencocokan akan mematahkan definisi pencocokan ).
Pencocokan matching
maksimum adalah pencocokan sempurna jika setiap untuk simpul n
di G.setOfArcs
terdapat busur a
dalam matching
di mana a.fromNode == n or a.toNode == n
.
Pencocokan Bipartit Maksimum
Pencocokan maksimum bipartit adalah pencocokan maksimum pada digraf G
yang bipartit .
Mengingat bahwa G
adalah bipartit , masalah menemukan pencocokan bipartit maksimum dapat ditransformasikan menjadi masalah aliran maksimum yang dapat diselesaikan dengan algoritma Edmonds-Karp dan kemudian pencocokan bipartit maksimum dapat dipulihkan dari solusi ke masalah aliran maksimum .
Biarkan bipartition
menjadi bipartisi dari G
.
Untuk melakukan ini, saya perlu membuat digraph baru ( H
) dengan beberapa node baru ( H.setOfNodes
) dan beberapa busur baru ( H.setOfArcs
). H.setOfNodes
berisi semua node di G.setOfNodes
dan dua node lagi , s
( node sumber ) dan t
( node terminal ).
H.setOfArcs
akan berisi satu busur untuk setiap G.setOfArcs
. Jika busur a
ada di G.setOfArcs
dan a.fromNode
ada di bipartition.firstPart
dan a.toNode
ada di bipartition.secondPart
maka sertakan a
di H
(menambahkan FlowArcDatum(1,0)
).
Jika a.fromNode
ada di bipartition.secondPart
dan a.toNode
ada di bipartition.firstPart
, maka sertakan Arc(a.toNode,a.fromNode,FlowArcDatum(1,0))
di H.setOfArcs
.
Definisi graf bipartit memastikan bahwa tidak ada busur yang menghubungkan setiap node di mana kedua node berada di partisi yang sama. H.setOfArcs
juga berisi busur dari node s
ke setiap node di bipartition.firstPart
. Terakhir, H.setOfArcs
berisi busur setiap simpul di bipartition.secondPart
ke simpul t
. a.datum.capacity = 1
untuk semua a
di H.setOfArcs
.
Pertama partisi node di G.setOfNodes
dua set terpisah ( part1
dan part2
) sedemikian rupa sehingga tidak ada busur di G.setOfArcs
yang diarahkan dari satu set ke set yang sama (partisi ini dimungkinkan karena G
adalah bipartit ). Selanjutnya, tambahkan semua busur di G.setOfArcs
yang diarahkan dari part1
ke part2
ke dalam H.setOfArcs
. Kemudian buat simpul sumber tunggal s
dan simpul terminal tunggal t
dan buat beberapa busur lagi
Kemudian, buat 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
Penutup Node Minimal
Penutup simpul dalam digraf G
adalah kumpulan simpul ( cover
) dari G.setOfNodes
sedemikian rupa sehingga untuk setiap busur a
dari G.setOfArcs
ini harus benar: (a.fromNode in cover) or (a.toNode in cover)
.
Penutup simpul minimal adalah himpunan simpul terkecil yang mungkin dalam graf yang masih merupakan penutup simpul . Teorema Konig menyatakan bahwa dalam graf bipartit , ukuran pencocokan maksimum pada grafik tersebut sama dengan ukuran penutup simpul minimal , dan ini menyarankan bagaimana penutup simpul dapat dipulihkan dari pencocokan maksimum :
Misalkan kita memiliki bipartisi bipartition
dan pencocokan matching
maksimum . Tentukan digraf baru H
, H.setOfNodes=G.setOfNodes
, busur di H.setOfArcs
adalah gabungan dari dua himpunan.
Set pertama adalah busur a
dalam matching
, dengan perubahan bahwa jika a.fromNode in bipartition.firstPart
dan a.toNode in bipartition.secondPart
maka a.fromNode
dan a.toNode
ditukar dalam busur yang dibuat memberikan a.datum.inMatching=True
busur seperti itu a.datum.inMatching=True
untuk menunjukkan bahwa atribut tersebut diturunkan dari busur dalam .
Himpunan kedua adalah arc a
NOT in matching
, dengan perubahan bahwa jika a.fromNode in bipartition.secondPart
dan a.toNode in bipartition.firstPart
maka a.fromNode
dan a.toNode
ditukar di arc yang dibuat (berikan arc tersebut a.datum.inMatching=False
Atribut salah).
Selanjutnya, jalankan depth first search (DFS) mulai dari setiap node n
di bipartition.firstPart
yang bukan n == a.fromNode
atau n == a.toNodes
untuk setiap arc a
dalam matching
. Selama DFS, beberapa node dikunjungi dan beberapa tidak (simpan informasi ini di bidang n.datum.visited
). Penutup simpul minimum adalah gabungan simpul {a.fromNode for a in H.setOfArcs if ( (a.datum.inMatching) and (a.fromNode.datum.visited) ) }
dan simpul {a.fromNode for a in H.setOfArcs if (a.datum.inMatching) and (not a.toNode.datum.visited)}
.
Ini dapat ditunjukkan untuk memimpin dari pencocokan maksimum ke penutup simpul minimal dengan bukti oleh kontradiksi, ambil beberapa busur a
seharusnya tidak tercakup dan pertimbangkan keempat kasus mengenai apakah a.fromNode
dan a.toNode
termasuk (apakah sebagai toNode
atau fromNode
) ke busur apa pun dalam pencocokan matching
. Setiap kasus mengarah ke kontradiksi karena urutan DFS mengunjungi node dan fakta bahwa matching
adalah pencocokan maksimum .
Misalkan kita memiliki fungsi untuk menjalankan langkah-langkah ini dan mengembalikan himpunan simpul yang terdiri dari penutup simpul minimal ketika diberi digraf G
, dan pencocokan matching
maksimum :
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
Masalah Penugasan Linier
Masalah penugasan linier terdiri dari menemukan pencocokan bobot maksimum dalam grafik bipartit berbobot.
Masalah seperti yang di awal posting ini dapat dinyatakan sebagai masalah penugasan linier . Mengingat satu set pekerja, satu set tugas, dan fungsi yang menunjukkan profitabilitas penugasan satu pekerja untuk satu tugas, kami ingin memaksimalkan jumlah semua tugas yang kami buat; ini adalah masalah penugasan linier .
Asumsikan bahwa jumlah tugas dan pekerja sama, meskipun saya akan menunjukkan bahwa asumsi ini mudah dihilangkan. Dalam implementasinya, saya merepresentasikan bobot busur dengan atribut a.datum.weight
untuk busur a
.
WeightArcDatum = namedtuple('WeightArcDatum', [weight])
Algoritma Kuhn-Munkres
Algoritma Kuhn-Munkres memecahkan masalah penugasan linier . Implementasi yang baik dapat memakan waktu O(N^{4})
, (di mana N
adalah jumlah node dalam digraf yang mewakili masalah). Implementasi yang lebih mudah dijelaskan membutuhkan O(N^{5})
(untuk versi yang meregenerasi DiGraphs ) dan O(N^{4})
untuk (untuk versi yang mempertahankan DiGraphs ). Ini mirip dengan dua implementasi berbeda dari algoritma Edmonds-Karp .
Untuk deskripsi ini, saya hanya bekerja dengan grafik bipartit lengkap (di mana setengah node berada di satu bagian bipartisi dan setengah lainnya di bagian kedua). Dalam pekerja, motivasi tugas ini berarti ada pekerja sebanyak tugas.
Ini sepertinya kondisi yang signifikan (bagaimana jika set ini tidak sama!) tetapi mudah untuk memperbaiki masalah ini; Saya berbicara tentang bagaimana melakukannya di bagian terakhir.
Versi algoritme yang dijelaskan di sini menggunakan konsep busur bobot nol yang berguna. Sayangnya, konsep ini hanya masuk akal ketika kita menyelesaikan minimalisasi (jika alih-alih memaksimalkan keuntungan dari penugasan tugas pekerja kita, kita malah meminimalkan biaya penugasan tersebut).
Untungnya, mudah untuk mengubah masalah penugasan linier maksimum menjadi masalah penugasan linier minimum dengan mengatur setiap busur a
bobot ke Ma.datum.weight
di mana M=max({a.datum.weight for a in G.setOfArcs})
. Solusi dari masalah pemaksimalan awal akan identik dengan solusi masalah meminimalkan setelah bobot busur diubah. Jadi untuk sisanya, asumsikan bahwa kita membuat perubahan ini.
Algoritma Kuhn-Munkres menyelesaikan pencocokan bobot minimum dalam graf bipartit berbobot dengan urutan pencocokan maksimum dalam graf bipartit tak berbobot. Jika a kami menemukan kecocokan sempurna pada representasi digraf dari masalah penugasan linier , dan jika bobot setiap busur dalam pencocokan adalah nol, maka kami telah menemukan pencocokan bobot minimum karena pencocokan ini menunjukkan bahwa semua simpul dalam digraf telah dicocokkan dengan busur dengan biaya serendah mungkin (tidak ada biaya yang bisa lebih rendah dari 0, berdasarkan definisi sebelumnya).
Tidak ada busur lain yang dapat ditambahkan ke pencocokan (karena semua simpul sudah cocok) dan tidak ada busur yang harus dihapus dari pencocokan karena busur pengganti yang mungkin akan memiliki nilai bobot paling tidak sama besar.
Jika kita menemukan kecocokan maksimum dari subgraf G
yang hanya berisi busur berbobot nol , dan itu bukan pencocokan sempurna , kita tidak memiliki solusi lengkap (karena pencocokannya tidak sempurna ). Namun, kita dapat menghasilkan digraf baru H
dengan mengubah bobot busur di G.setOfArcs
sedemikian rupa sehingga busur berbobot 0 baru muncul dan solusi optimal H
sama dengan solusi optimal G
. Karena kami menjamin bahwa setidaknya satu busur berbobot nol dihasilkan pada setiap iterasi, kami menjamin bahwa kami akan mencapai pencocokan sempurna dalam tidak lebih dari |G.setOfNodes|^{2}=N^{2} iterasi tersebut.
Misalkan dalam bipartition bipartition
, bipartition.firstPart
berisi node yang mewakili pekerja, dan bipartition.secondPart
mewakili node yang mewakili tugas.
Algoritma dimulai dengan menghasilkan digraf baru H
. H.setOfNodes = G.setOfNodes
. Beberapa busur di H
dihasilkan dari simpul n
di bipartition.firstPart
. Setiap simpul n
tersebut menghasilkan busur b
di H.setOfArcs
untuk setiap busur a
di bipartition.G.setOfArcs
di mana a.fromNode = n
atau a.toNode = n
, b=Arc(a.fromNode, a.toNode, a.datum.weight - z)
di mana z=min(x.datum.weight for x in G.setOfArcs if ( (x.fromNode == n) or (x.toNode == n) ))
.
Lebih banyak busur di H
dihasilkan dari simpul n
di bipartition.secondPart
. Setiap simpul n
tersebut menghasilkan busur b
di H.setOfArcs
untuk setiap busur a
di bipartition.G.setOfArcs
di mana a.fromNode = n
atau a.toNode = n
, b=Arc(a.fromNode, a.toNode, ArcWeightDatum(a.datum.weight - z))
where z=min(x.datum.weight for x in G.setOfArcs if ( (x.fromNode == n) or (x.toNode == n) ))
.
KMA: Selanjutnya, bentuk digraf baru K
yang hanya terdiri dari busur berbobot nol dari H
, dan simpul yang datang pada busur tersebut . Bentuklah bipartition
pada node di K
, lalu gunakan solve_mbm( bipartition )
untuk mendapatkan pencocokan ( matching
) yang maksimal pada K
. Jika matching
adalah pencocokan sempurna di H
( busur dalam matching
terjadi pada semua node di H.setOfNodes
) maka matching
adalah solusi optimal untuk masalah penugasan linier .
Jika tidak, jika matching
tidak sempurna , buat penutup simpul minimal K
menggunakan node_cover = get_min_node_cover(matching, bipartition(K))
. Selanjutnya, definisikan z=min({a.datum.weight for a in H.setOfArcs if a not in node_cover})
. Tentukan 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)}
Simbol !=
pada ekspresi sebelumnya bertindak sebagai operator XOR Kemudian arcs = arcs1.union(arcs2.union(arcs3))
Selanjutnya, H=DiGraph(nodes,arcs)
Kembali ke label KMA.Algoritma berlanjut sampai diperoleh pencocokan sempurna.Pencocokan ini juga merupakan solusi dari masalah penugasan linier .
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
Implementasi ini adalah O(N^{5})
karena menghasilkan pencocokan matching
maksimum baru pada setiap iterasi; mirip dengan dua implementasi Edmonds-Karp sebelumnya, algoritma ini dapat dimodifikasi sehingga dapat melacak pencocokan dan menyesuaikannya dengan cerdas untuk setiap iterasi. Ketika ini selesai, kompleksitasnya menjadi O(N^{4})
. Versi yang lebih maju dan lebih baru dari algoritme ini (memerlukan beberapa struktur data yang lebih canggih) dapat berjalan di O(N^{3})
. Detail implementasi yang lebih sederhana di atas dan implementasi yang lebih maju dapat ditemukan di posting ini yang memotivasi posting blog ini.
Tak satu pun dari operasi pada bobot busur mengubah tugas akhir yang dikembalikan oleh algoritma. Inilah alasannya: Karena grafik input kami selalu merupakan grafik bipartit lengkap , solusi harus memetakan setiap node dalam satu partisi ke node lain di partisi kedua, melalui busur antara dua node ini. Perhatikan bahwa operasi yang dilakukan pada bobot busur tidak pernah mengubah urutan (diurutkan berdasarkan bobot) dari insiden busur pada simpul tertentu.
Jadi, ketika algoritme berakhir pada pencocokan bipartit lengkap yang sempurna, setiap simpul diberi busur berbobot nol , karena urutan relatif busur dari simpul itu tidak berubah selama algoritma, dan karena busur berbobot nol adalah busur termurah yang mungkin dan pencocokan bipartit lengkap yang sempurna menjamin bahwa satu busur seperti itu ada untuk setiap simpul . Ini berarti bahwa solusi yang dihasilkan memang sama dengan solusi dari masalah penugasan linier asli tanpa modifikasi bobot busur .
Tugas Tidak Seimbang
Sepertinya algoritme ini sangat terbatas karena seperti yang dijelaskan, ia hanya beroperasi pada grafik bipartit lengkap (di mana setengah node berada di satu bagian bipartisi dan setengah lainnya di bagian kedua). Pada pekerja, motivasi tugas ini berarti bahwa ada pekerja sebanyak tugas (tampaknya cukup membatasi).
Namun, ada transformasi mudah yang menghilangkan batasan ini. Misalkan ada lebih sedikit pekerja daripada tugas, kami menambahkan beberapa pekerja dummy (cukup untuk membuat grafik yang dihasilkan menjadi grafik bipartit lengkap ). Each dummy worker has an arc directed from the worker to each of the tasks. Each such arc has weight 0 (placing it in a matching gives no added profit). After this change the graph is a complete bipartite graph which we can solve for. Any task assigned a dummy worker is not initiated.
Suppose that there are more tasks than workers. We add some dummy tasks (enough to make the resulting graph a complete bipartite graph ). Each dummy task has an arc directed from each worker to the dummy task. Each such arc has a weight of 0 (placing it in a matching gives no added profit). After this change the graph is a complete bipartite graph which we can solve for. Any worker assigned to dummy task is not employed during the period.
A Linear Assignment Example
Finally, let's do an example with the code I've been using. I'm going to modify the example problem from here. We have 3 tasks: we need to clean the bathroom , sweep the floor , and wash the windows .
The workers available to use are Alice , Bob , Charlie , and Diane . Each of the workers gives us the wage they require per task. Here are the wages per worker:
Clean the Bathroom | Sweep the Floor | Wash the Windows | |
---|---|---|---|
Alice | $2 | $3 | $3 |
Bob | $3 | $2 | $3 |
Charlie | $3 | $3 | $2 |
dian | $9 | $9 | $1 |
If we want to pay the least amount of money, but still get all the tasks done, who should do what task? Start by introducing a dummy task to make the digraph representing the problem bipartite.
Clean the Bathroom | Sweep the Floor | Wash the Windows | Do Nothing | |
---|---|---|---|---|
Alice | $2 | $3 | $3 | $0 |
Bob | $3 | $2 | $3 | $0 |
Charlie | $3 | $3 | $2 | $0 |
dian | $9 | $9 | $1 | $0 |
Supposing that the problem is encoded in a digraph , then kuhn_munkres( bipartition(G) )
will solve the problem and return the assignment. It's easy to verify that the optimal (lowest cost) assignment will cost $5.
Here's a visualization of the solution the code above generates:
Hanya itu saja. You now know everything you need to know about the linear assignment problem.
You can find all of the code from this article on GitHub.
Bacaan Lebih Lanjut di Blog Teknik Toptal:
- Ilmu Data Grafik Dengan Python/NetworkX