Serwis Edukacyjny w I-LO w Tarnowie ![]() Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej Autor artykułu: mgr Jerzy Wałaszek |
©2025 mgr Jerzy Wałaszek |
ProblemW danym grafie należy znaleźć ścieżkę o najmniejszej sumie wag krawędzi, która przechodzi dokładnie jeden raz przez każdy wierzchołek i wraca do wierzchołka startowego. |
Wyobraźmy sobie komiwojażera, który podróżuje od miasta do miasta, sprzedając swoje produkty lub zawierając różne oferty handlowe. Wyrusza z miasta rodzinnego, po czym jego trasa przebiega dokładnie jeden raz przez każde z miast. Na koniec komiwojażer powraca do swojego miasta rodzinnego. Z oczywistych powodów chce, aby trasa podróży była najkrótszą ze wszystkich możliwych tras. W ten sposób powstaje problem wędrującego komiwojażera (ang. TSP – Travelling Salesman Problem).
W terminologii grafów miasta są wierzchołkami grafu, a trasy pomiędzy nimi to krawędzie z wagami. Waga krawędzi może odpowiadać odległości pomiędzy miastami połączonymi tą krawędzią, czasowi podróży lub kosztom przejazdu – zależy, co chcemy w podróżny komiwojażera zminimalizować. Trasa komiwojażera jest cyklem przechodzącym przez każdy wierzchołek grafu dokładnie jeden raz – jest to zatem cykl Hamiltona.
Znalezienie właściwego cyklu Hamiltona jest zadaniem bardzo trudnym obliczeniowo. Wyobraźmy sobie graf zupełny (ang. complete graph – graf, w którym każdy wierzchołek jest połączony z każdym) o 10 wierzchołkach.
Ile różnych cykli Hamiltona zawiera taki graf? Otóż pierwszą krawędź
cyklu można wybrać na 9 różnych sposobów, ponieważ każdy wierzchołek grafu
jest połączony krawędzią z pozostałymi dziewięcioma. Po wyborze pierwszej
krawędzi pozostaje nam 8 możliwych wyborów, później
LH = 9×8×7×6×5×4×3×2×1 = 9! = 362880
Dla n wierzchołków:
LH = (n-1)×(n-2)×…3×2×1 = (n-1)!
Wynik jest bardzo niekorzystny, ponieważ prowadzi do wykładniczej klasy
złożoności obliczeniowej
Grafy odwzorowujące rzeczywiste sieci połączeń zwykle nie są zupełne – ekonomicznie nieuzasadnione byłoby budowanie osobnych dróg pomiędzy każdą parą miast. Zwykle drogi przebiegają od jednego miasta do drugiego, a w niektórych się rozchodzą. Dlatego opisany powyżej przypadek jest przypadkiem pesymistycznym. Jednakże problem dalej posiada wykładniczą klasę złożoności obliczeniowej. Rozważmy dla przykładu graf posiadający 100 wierzchołków. Załóżmy, iż każdy wierzchołek łączy się z czterema innymi wierzchołkami grafu. Zatem ich stopień wynosi 4. W pesymistycznym przypadku ilość cykli Hamiltona do przebadania może wynosić:
Z pierwszego wierzchołka możemy pójść na 4 różne sposoby, z drugiego na 3, z trzeciego na 3, …, i z 99 na trzy. Otrzymujemy zatem:
LH = 4×399 ≈ 3100 ≈ 5, 15×1047
Zatem dla grafu posiadającego n wierzchołków o stopniu s otrzymamy
LH ≈ (s-1)n
Czyli liczba cykli lub ścieżek do przebadania rośnie wykładniczo.
Nawet na szybkim komputerze wyznaczenie rozwiązania może przekroczyć
stulecia. Problemy algorytmiczne o złożoności wykładniczej są traktowane
jako nierozwiązywalne dla
Istnieją algorytmy znajdujące przybliżone rozwiązania problemu wędrującego komiwojażera w czasie wielomianowym, lecz są one bardzo zaawansowane i trudne w implementacji. Jeśli liczba wierzchołków w grafie nie jest duża (np. do 20) i graf nie jest grafem zupełnym (złożoność O(n!)), to rozwiązanie problemu komiwojażera możemy uzyskać prostym algorytmem, który wyznacza wszystkie cykle Hamiltona, liczy sumę wag krawędzi i zwraca cykl o najmniejszej sumie wag. Rozwiązanie to jest o tyle dobre, iż zawsze zwróci cykl optymalny, a nie przybliżony. Również jest łatwe do zrozumienia i implementacji. Podstawowa wada to wykładnicza złożoność obliczeniowa, która uniemożliwia analizę większych grafów.
K01: SH.push(v) ; Odwiedzony wierzchołek dopisujemy do ścieżki K02: Jeśli SH nie zawiera n wierzchołków, ; Jeśli brak ścieżki Hamiltona, to idź do kroku K10 ; przechodzimy do wyszukiwania K03: Jeśli nie istnieje krawędź z v do v0, ; Jeśli ścieżka Hamiltona nie jest cyklem, to idź do kroku K17 ; odrzucamy ją K04: dH ← dH + waga krawędzi z v do v0 ; Uwzględniamy w sumie wagę ostatniej krawędzi cyklu K05: Jeśli dH ≥ d, ; Jeśli znaleziony cykl jest gorszy od bieżącego, to idź do kroku K08 ; odrzucamy go K06: d ← dH ; Zapamiętujemy sumę wag cyklu K07: Skopiuj stos SH do stosu S ; oraz sam cykl Hamiltona K08: dH ← dH - waga krawędzi z v do v0 ; Usuwamy wagę ostatniej krawędzi z sumy K09: Idź do kroku K17 K10: vs[v] ← true ; Wierzchołek zaznaczamy jako odwiedzony, ; aby nie był ponownie wybierany przez DFS K11: Dla każdego sąsiada u wierzchołka v: ; Przechodzimy przez listę sąsiedztwa wykonuj kroki K12…K15 K12: Jeśli vs[u] = true, ; Omijamy wierzchołki odwiedzone to następny obieg pętli K11 K13: dH ← dH + waga krawędzi z v do u ; Obliczamy nową sumę wag krawędzi ścieżki K14: TSP(n,graf,u,v0,d,dH,S,SH,vs) ; Wywołujemy rekurencyjnie poszukiwanie cyklu K15: dH ← dH - waga krawędzi z v do u ; Usuwamy wagę krawędzi z sumy K16: vs[v] ← false ; Zwalniamy bieżący wierzchołek K17: SH.pop() ; Usuwamy bieżący wierzchołek ze ścieżki K18: Zakończ
K01: Utwórz i wyzeruj vs, S i SH K02: d ← ∞; dH ← 0 ; Początkową sumę ustawiamy jako największą z możliwych K03: TSP(n,graf,u,v0,d,dH,S,SH,vs) ; Wyszukiwanie cyklu Hamiltona rozpoczynamy ; od wierzchołka startowego K04: Jeśli S.empty() = false, ; Sprawdzamy, czy algorytm znalazł cykl Hamiltona. to pisz S oraz d ; Jeśli tak, to wypisujemy go. inaczej pisz "NO HAMILTONIAN CYCLE" K05: Zakończ
Uwaga: większość parametrów wejściowych może być zrealizowana jako zmienne globalne, co znacznie uprości wywołania rekurencyjne.
Uwaga: Zanim uruchomisz program, przeczytaj wstęp do tego artykułu, w którym wyjaśniamy funkcje tych programów oraz sposób korzystania z nich. |
Poniższy program jest przykładową
realizacją algorytmu rozwiązywania problemu komiwojażera.
Z uwagi na wykładniczą klasę złożoności obliczeniowej program
potrafi w rozsądnym czasie znaleźć rozwiązanie tylko dla grafów o niewielkiej
liczbie węzłów (powiedzmy, do 20).
Definicja danych wejściowych jest następująca: W pierwszym wierszu program odczytuje kolejno liczbę Jako wierzchołek startowy program przyjmuje wierzchołek Na wyjściu zostaje wypisany cykl Hamiltona o najmniejszej sumie wag krawędzi oraz wartość tej sumy. Jeśli graf nie zawiera cyklu Hamiltona, to pojawi się napis: NO HAMILTONIAN CYCLE. |
![]() |
8 16 0 1 2 0 2 2 0 3 4 0 4 3 1 2 2 1 5 1 1 6 1 2 4 2 2 5 1 3 5 2 3 7 3 4 6 4 4 7 5 5 6 2 5 7 2 6 7 2 |
Pascal// Problem komiwojażera // Data: 22.03.2014 // (C)2014 mgr Jerzy Wałaszek //--------------------------- program TSProblem; // Typy dla dynamicznej // macierzy sąsiedztwa type // Wiersz tablicy sąsiedztwa nptr = array of boolean; // Wiersz macierzy wag krawędzi wnptr = array of integer; // Zmienne globalne var n,m,v0,d,dh,sptr,shptr : integer; // Macierz sąsiedztwa A : array of nptr; // Macierz wag W : array of wnptr; // Stosy w tablicy S, Sh : array of integer; // Tablica odwiedzin vs : array of boolean; // Rekurencyjna procedura // poszukiwania cyklu Hamiltona // o najmniejszej sumie wag krawędzi // v - wierzchołek bieżący //---------------------------------- procedure TSP(v : integer); var u : integer; begin // zapamiętujemy na stosie // bieżący wierzchołek Sh[shptr] := v; inc(shptr); // jeśli brak ścieżki Hamiltona, // to jej szukamy if shptr < n then begin // Oznaczamy bieżący wierzchołek // jako odwiedzony vs[v] := true; // Przeglądamy sąsiadów // wierzchołka v for u := 0 to n-1 do // Szukamy nieodwiedzonego // jeszcze sąsiada if A[v][u] and not vs[u] then begin // Dodajemy wagę krawędzi // v - u do sumy inc(dh, W[v][u]); // Rekurencyjnie wywołujemy // szukanie cyklu Hamiltona TSP(u); // Usuwamy wagę krawędzi // z sumy dec(dh, W[v][u]); end; // Zwalniamy bieżący wierzchołek vs[v] := false; end // Jeśli znaleziona ścieżka // jest cyklem Hamiltona, else if A[v0][v] then begin // to sprawdzamy, // czy ma najmniejszą sumę wag inc(dh, W[v][v0]); // Jeśli tak, if dh < d then begin // To zapamiętujemy tę sumę d := dh; // oraz kopiujemy stos Sh do S for u := 0 to shptr-1 do S[u] := Sh[u]; sptr := shptr; end; // Usuwamy wagę krawędzi // v - v0 z sumy dec(dh, W[v][v0]); end; // Usuwamy bieżący // wierzchołek ze ścieżki dec(shptr); end; //********************** //*** Program główny *** //********************** var i, j, x, y, z : integer; begin // Czytamy liczbę // wierzchołków i krawędzi read(n, m); // Tworzymy struktury // dynamiczne i inicjujemy je SetLength(S, n); SetLength(Sh, n); SetLength(vs, n); SetLength(A, n); SetLength(W, n); for i := 0 to n-1 do begin SetLength(A[i], n); SetLength(W[i], n); for j := 0 to n-1 do begin A[i][j] := false; W[i][j] := 0; end; vs[i] := false; end; sptr := 0; shptr := 0; // Odczytujemy dane wejściowe for i := 0 to m-1 do begin read(x, y, z); // Krawędź x - y A[x][y] := true; A[y][x] := true; // Waga krawędzi x - y W[x][y] := z; W[y][x] := z; end; writeln; // Rozpoczynamy algorytm d := MAXINT; dh := 0; v0 := 0; TSP(v0); if sptr > 0 then begin for i := 0 to sptr-1 do write(S[i], ' '); writeln(v0); writeln('d = ', d); end else writeln('NO HAMILTONIAN CYCLE'); writeln; // Usuwamy tablice dynamiczne SetLength(S, 0); SetLength(Sh, 0); SetLength(vs, 0); for i := 0 to n-1 do begin SetLength(A[i], 0); SetLength(W[i], 0); end; SetLength(A, 0); SetLength(W, 0); end. |
// Problem komiwojażera // Data: 22.03.2014 // (C)2014 mgr Jerzy Wałaszek //--------------------------- #include <iostream> using namespace std; const int MAXINT = 2147483647; // Zmienne globalne int n,m,v0,d,dh,sptr,shptr; // Macierz sąsiedztwa bool **A; // Macierz wag krawędzi int **W; // Stosy w tablicy int *S, *Sh; // Tablica odwiedzin bool *vs; // Rekurencyjna procedura // poszukiwania cyklu Hamiltona // o najmniejszej sumie // wag krawędzi // v - wierzchołek bieżący //----------------------------- void TSP(int v) { int u; // zapamiętujemy na stosie // bieżący wierzchołek Sh[shptr++] = v; // jeśli brak ścieżki // Hamiltona, to jej szukamy if(shptr < n) { // Oznaczamy bieżący // wierzchołek // jako odwiedzony vs[v] = true; // Przeglądamy sąsiadów // wierzchołka v for(u = 0; u < n; u++) // Szukamy nieodwiedzonego // jeszcze sąsiada if(A[v][u] && !vs[u]) { // Dodajemy wagę // krawędzi v - u do sumy dh += W[v][u]; // Rekurencyjnie // wywołujemy szukanie // cyklu Hamiltona TSP(u); // Usuwamy wagę // krawędzi z sumy dh -= W[v][u]; } // Zwalniamy // bieżący wierzchołek vs[v] = false; } // Jeśli znaleziona ścieżka // jest cyklem Hamiltona, else if(A[v0][v]) { // to sprawdzamy, // czy ma najmniejszą // sumę wag dh += W[v][v0]; // Jeśli tak, if(dh < d) { // to zapamiętujemy tę sumę d = dh; // oraz kopiujemy // stos Sh do S for(u = 0; u < shptr; u++) S[u] = Sh[u]; sptr = shptr; } // Usuwamy wagę krawędzi // v - v0 z sumy dh -= W[v][v0]; } // Usuwamy bieżący // wierzchołek ze ścieżki shptr--; } //********************** //*** Program główny *** //********************** int main() { int i, j, x, y, z; // Czytamy liczbę // wierzchołków i krawędzi cin >> n >> m; // Tworzymy struktury // dynamiczne i inicjujemy je S = new int [n]; Sh = new int [n]; vs = new bool [n]; A = new bool * [n]; W = new int * [n]; for(i = 0; i < n; i++) { A[i] = new bool [n]; W[i] = new int [n]; for(j = 0; j < n; j++) { A[i][j] = false; W[i][j] = 0; } vs[i] = false; } sptr = shptr = 0; // Odczytujemy dane wejściowe for(i = 0; i < m; i++) { cin >> x >> y >> z; // Krawędź x - y A[x][y] = A[y][x] = true; // Waga krawędzi x - y W[x][y] = W[y][x] = z; } cout << endl; // Rozpoczynamy algorytm d = MAXINT; dh = v0 = 0; TSP(v0); if(sptr) { for(i = 0; i < sptr; i++) cout << S[i] << " "; cout << v0 << endl; cout << "d = " << d << endl; } else cout << "NO HAMILTONIAN CYCLE" << endl; cout << endl; // Usuwamy tablice dynamiczne delete [] S; delete [] Sh; delete [] vs; for(i = 0; i < n; i++) { delete [] A[i]; delete [] W[i]; } delete [] A; delete [] W; return 0; } |
Basic' Problem komiwojażera ' Data: 22.03.2014 ' (C)2014 mgr Jerzy Wałaszek '--------------------------- Const MAXINT = 2147483647 ' Zmienne globalne Dim Shared As Integer n,m,v0,d Dim Shared As Integer dhx,sptr,shptr ' Macierz sąsiedztwa Dim Shared As Byte Ptr Ptr A ' Macierz wag krawędzi Dim Shared As Integer Ptr Ptr W ' Stosy w tablicy Dim Shared As Integer Ptr S, Sh ' Tablica odwiedzin Dim Shared As Byte Ptr vs ' Rekurencyjna procedura ' poszukiwania cyklu Hamiltona ' o najmniejszej sumie wag krawędzi ' v - wierzchołek bieżący '---------------------------------- Sub TSP (ByVal v As Integer) Dim As Integer u ' zapamiętujemy na stosie ' bieżący wierzchołek Sh[shptr] = v shptr += 1 ' jeśli brak ścieżki Hamiltona, ' to jej szukamy If shptr < n Then ' Oznaczamy bieżący ' wierzchołek jako odwiedzony vs[v] = 1 ' Przeglądamy sąsiadów ' wierzchołka v For u = 0 To n-1 ' Szukamy nieodwiedzonego ' jeszcze sąsiada If (A[v][u] = 1) AndAlso _ (vs[u] = 0) Then ' Dodajemy wagę ' krawędzi v - u do sumy dhx += W[v][u] ' Rekurencyjnie wywołujemy ' szukanie cyklu Hamiltona TSP(u) ' Usuwamy wagę ' krawędzi z sumy dhx -= W[v][u] End If Next ' Zwalniamy bieżący wierzchołek vs[v] = 0 ' Jeśli znaleziona ścieżka ' jest cyklem Hamiltona, ElseIf A[v0][v] = 1 Then ' to sprawdzamy, ' czy ma najmniejszą sumę wag dhx += W[v][v0] ' Jeśli tak, If dhx < d Then ' To zapamiętujemy tę sumę d = dhx ' oraz kopiujemy stos Sh do S For u = 0 To shptr-1 S[u] = Sh[u] Next sptr = shptr End If ' Usuwamy wagę krawędzi ' v - v0 z sumy dhx -= W[v][v0] End If ' Usuwamy bieżący ' wierzchołek ze ścieżki shptr -= 1 End Sub '********************** '*** Program główny *** '********************** Dim as Integer i, j, x, y, z Open Cons For Input As #1 ' Czytamy liczbę ' wierzchołków i krawędzi Input #1, n, m ' Tworzymy struktury ' dynamiczne i inicjujemy je S = New Integer [n] Sh = New Integer [n] vs = New Byte [n] A = New Byte Ptr [n] W = New Integer Ptr [n] For i = 0 To n-1 A[i] = New Byte [n] W[i] = New Integer [n] For j = 0 To n-1 A[i][j] = 0 W[i][j] = 0 Next vs[i] = 0 Next sptr = 0 shptr = 0 ' Odczytujemy dane wejściowe For i = 0 To m-1 Input #1, x, y, z ' Krawędź x-y A[x][y] = 1 A[y][x] = 1 ' Waga krawędzi x-y W[x][y] = z W[y][x] = z Next Close #1 Print ' Rozpoczynamy algorytm d = MAXINT dhx = 0 v0 = 0 TSP(v0) If sptr > 0 Then For i = 0 To sptr-1 Print S[i]; Next Print v0 Print " d =";d Else Print "NO HAMILTONIAN CYCLE" End If Print ' Usuwamy tablice dynamiczne Delete [] S Delete [] Sh Delete [] vs For i = 0 To n-1 Delete [] A[i] Delete [] W[i] Next Delete [] A Delete [] W End |
Python
(dodatek)# Problem komiwojażera # Data: 7.03.2025 # (C)2025 mgr Jerzy Wałaszek #--------------------------- MAXINT = 2147483647 # Rekurencyjna procedura # poszukiwania cyklu Hamiltona # o najmniejszej sumie wag krawędzi # v - wierzchołek bieżący #---------------------------------- def tsp(v): global s,sh,shptr,sptr,n,m,vs,a,dhx,w,v0,d # zapamiętujemy na stosie # bieżący wierzchołek sh[shptr] = v shptr += 1 # jeśli brak ścieżki Hamiltona, # to jej szukamy if shptr < n: # Oznaczamy bieżący # wierzchołek jako odwiedzony vs[v] = True # Przeglądamy sąsiadów # wierzchołka v for u in range(n): # Szukamy nieodwiedzonego # jeszcze sąsiada if a[v][u] and not vs[u]: # Dodajemy wagę # krawędzi v - u do sumy dhx += w[v][u] # Rekurencyjnie wywołujemy # szukanie cyklu Hamiltona tsp(u) # Usuwamy wagę # krawędzi z sumy dhx -= w[v][u] # Zwalniamy bieżący wierzchołek vs[v] = False # Jeśli znaleziona ścieżka # jest cyklem Hamiltona, elif a[v0][v]: # to sprawdzamy, # czy ma najmniejszą sumę wag dhx += w[v][v0] # Jeśli tak, if dhx < d: # To zapamiętujemy tę sumę d = dhx # oraz kopiujemy stos Sh do S for u in range(shptr): s[u] = sh[u] sptr = shptr # Usuwamy wagę krawędzi # v - v0 z sumy dhx -= w[v][v0] # Usuwamy bieżący # wierzchołek ze ścieżki shptr -= 1 #********************** #*** Program główny *** #********************** # Czytamy liczbę # wierzchołków i krawędzi q = input().split() n = int(q[0]) m = int(q[1]) # Tworzymy struktury # dynamiczne i inicjujemy je s = [0] * n sh = [0] * n vs = [False] * n a = [] w = [] for i in range(n): q = [False] * n r = [0] * n a.append(q) w.append(r) del q,r sptr = 0 shptr = 0 # Odczytujemy dane wejściowe for i in range(m): q = input().split() x = int(q[0]) y = int(q[1]) z = int(q[2]) # Krawędź x-y a[x][y] = True a[y][x] = True # Waga krawędzi x-y w[x][y] = z w[y][x] = z print() # Rozpoczynamy algorytm d = MAXINT dhx = 0 v0 = 0 tsp(v0) if sptr > 0: for i in range(sptr): print(s[i],end=" ") print(v0) print(" d =",d) else: print("NO HAMILTONIAN CYCLE") print() # Usuwamy tablice dynamiczne del s,sh,vs for i in range(n): a[i] = None w[i] = None del a,w |
Wynik: | |
![]() |
|
8 16 0 1 2 0 2 2 0 3 4 0 4 3 1 2 2 1 5 1 1 6 1 2 4 2 2 5 1 3 5 2 3 7 3 4 6 4 4 7 5 5 6 2 5 7 2 6 7 2 0 1 6 7 3 5 2 4 0 d = 16 |
![]() |
Zespół Przedmiotowy Chemii-Fizyki-Informatyki w I Liceum Ogólnokształcącym im. Kazimierza Brodzińskiego w Tarnowie ul. Piłsudskiego 4 ©2025 mgr Jerzy Wałaszek |
Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone pod warunkiem podania źródła oraz niepobierania za to pieniędzy.
Pytania proszę przesyłać na adres email:
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.