Problema comis-voiajorului [Programare dinamică]

Problema comis-voiajorului [Programare dinamică]

Într-un articol mai vechi am prezentat problema comis-voiajorului și rezolvarea ei prin backtracking. Totuși, pentru date de intrare mici (numărul de noduri sub 2020 există o soluție mai eficientă, bazată pe programare dinamică. Tehnica folosită se numește dinamică pe stări exponențiale, sau dinamică pe configurații. În continuare, vom avea în vedere restricțiile din problema Ciclu hamiltonian de cost minim de pe InfoArena.

Enunț

Se dă un graf orientat ponderat cu n18n \le 18 vârfuri (indexate de la 00 și mm arce. Fiecare arc (i,j)(i, j) are asociat un cost ad[i][j]\mathrm{ad}[i][j] Să se determine un ciclu hamiltonian de cost minim în acest graf, sau să se afișeze "Nu exista solutie" dacă graful nu este hamiltonian. Fiind vorba despre un graf orientat, corect ar fi să spunem circuit hamiltonian, dar voi păstra formularea de pe InfoArena.

Soluție prin programare dinamică

Faptul că numărul de noduri este foarte mic (maxim 1818 ne duce cu gândul că probabil o soluție exponențială se va încadra în timp. Dacă ați mai rezolvat până acum vreo problemă de dinamică pe stări exponențiale, sau dacă vă pricepeți la programare dinamică în general, probabil că deja v-ați prins cum arată subproblemele. Dacă nu, veți afla acum tehnica prin care se abordează o astfel de problemă.

Formularea subproblemelor

Dinamica va fi dp[mask][i]=\mathrm{dp}[mask][i] = costul minim al unui drum ce vizitează exact o singură dată fiecare nod din submulțimea maskmask care începe din nodul 00 și se termină în nodul ii sau \infty în caz că nu există un astfel de drum. Veți afla mai încolo de ce am fixat începutul drumului în 00 Parametrul maskmask este o mască de biți ce codifică o submulțime de noduri ale grafului: Bitul kk este setat la 11 dacă și numai dacă nodul kk face parte din submulțimea respectivă.

Găsirea relației de recurență

Acum că am formulat subproblema, este relativ simplu să deducem recurența. Dacă drumul se termină în nodul ii cu siguranță penultimul nod din componența drumului va fi un nod jj pentru care există arcul (j,i)(j, i) Evident, costul minim al unui drum optim pentru submulțimea maskmask ce se termină în (j,i)(j, i) este egal cu costul minim al unui drum optim pentru submulțimea mask ^ (1 << i), la care se adaugă costul arcului (j,i)(j, i) adică ad[j][i]\mathrm{ad}[j][i] (Prin mask ^ (1 << i) se codifică submulțimea obținută eliminându-l pe ii din maskmask Este echivalent cu mask - (1 << i).) Calculăm această valoare pentru fiecare jj posibil, și alegem minimul dintre ele, recurența fiind:

dp[mask][i]=minj,(j,i){dp[mask2i][j]+ad[j][i]}\mathrm{dp}[mask][i] = \min_{\forall j, \exists (j, i)} \left\{\mathrm{dp}[mask - 2^i][j] + \mathrm{ad}[j][i]\right\}

Folosirea rezultatelor

După ce am calculat dinamica, mai avem de făcut ceva pentru a ajunge la soluție. În dp[2n1][i]\mathrm{dp}[2^n - 1][i] avem costul minim al unui drum hamiltonian pentru întregul graf. Pentru a-l transforma în ciclu, mai rămâne să-i adăugăm la final arcul (i,0)(i, 0) Acum se poate vedea de ce am fixat nodul de start în 00 Dacă n-ar fi fost fixat, n-am fi știut ce arc să adăugăm pentru a forma ciclu, așa că ar fi trebuit să mai avem o dimensiune la dinamică – nodul de start. Însă, acesta poate fi ales de la început, deoarece ciclul final oricum trebuie să conțină toate nodurile. Așadar, rezultatul problemei va fi:

sol=min0i<n{dp[2n1][i]+ad[i][0]}\mathrm{sol} = \min_{0 \le i \lt n} \left\{\mathrm{dp}[2^n - 1][i] + \mathrm{ad}[i][0]\right\}

Dacă am obținut \infty înseamnă că nu există soluție.

Implementare în C++

Câteva detalii de implementare: În in[i]\mathrm{in}[i] am reținut o listă cu nodurile jj pentru care există arcul (j,i)(j, i) iar în ad[i][j]\mathrm{ad}[i][j] costul arcului (i,j)(i, j) Am avut nevoie și de liste și de matrice de adiacență pentru a putea parcurge vecinii unui nod eficient, dar și pentru a accesa ad[i][0]\mathrm{ad}[i][0] în O(1)O(1)

Pentru calcularea stărilor dinamicii, am început cu cazul de bază (dp[1][0]=0\htmlClass{katexified}{(} \mathrm{dp}[1][0] = 0 iar pe restul submulțimilor le-am parcurs din 22 în 22 pentru că submulțimile care nu au bitul 00 setat la 11 nu sunt valide. Infinitul l-am reprezentat prin constanta 1e9, care este suficient de mare pentru problema noastră.

Problema comis-voiajorului
#include <bits/stdc++.h>
using namespace std;
ifstream fin("hamilton.in");
ofstream fout("hamilton.out");
int main() {
int n, m; fin >> n >> m;
vector<vector<int>> in(n), ad(n, vector<int>(n, 1e9));
for (int i = 0; i < m; i++) {
int x, y, z; fin >> x >> y >> z;
in[y].push_back(x); ad[x][y] = z;
}
vector<vector<int>> dp(1 << n, vector<int>(n, 1e9));
dp[1][0] = 0;
for (int mask = 3; mask < (1 << n); mask += 2) // Parcurg submulțimile.
for (int i = 1; i < n; i++) // Parcurg biții din mask.
if (mask & (1 << i)) // Testez dacă bitul e setat la 1.
for (int j : in[i]) // Dacă da, parcurg nodurile j care intră în i.
dp[mask][i] = min(dp[mask][i], dp[mask ^ (1 << i)][j] + ad[j][i]); // Actualizez dp-ul.
int sol = 1e9;
for (int i = 1; i < n; i++)
sol = min(sol, dp[(1 << n) - 1][i] + ad[i][0]);
if (sol == 1e9)
fout << "Nu exista solutie\n";
else
fout << sol << '\n';
return 0;
}

Complexitate

Se vede clar că numărul de stări ale dinamicii crește exponențial din cauza numărului de submulțimi ale nodurilor. De aici și numele de programare dinamică pe stări exponențiale. Complexitatea în spațiu a algoritmului este O(n2n)O(n \cdot 2^n)

Timpul de calculare a unei stări dp[mask][i]\mathrm{dp}[mask][i] este egal cu numărul de noduri jj ce „intră” în ii Astfel, timpul necesar pentru calcularea tuturor stărilor ce îl au ca prim parametru pe maskmask se amortizează la O(m)O(m) Deci, complexitatea în timp a algoritmului este O(m2n)O(m \cdot 2^n)

Probleme recomandate

Problemele recomandate nu sunt legate tocmai de problema comis-voiajorului, dar se folosesc de conceptul de programare dinamică pe stări exponențiale. Cea mai interesantă mi se pare problema Pavare, pentru că îmbină backtracking-ul cu dinamica. Pe lângă aceste probleme, un exercițiu bun este să vă gândiți cum se poate reconstitui soluția având deja calculată matricea dp\mathrm{dp} Dacă aveți vreo întrebare despre această tehnică de programare dinamică, o puteți lăsa într-un comentariu :smile:

Mulțumesc că ai citit acest articol.
Dacă vrei să susții blogul, poți cumpăra un abonament de 2$.

patreon

Lasă un comentariu!

0 comentarii