www.agora.ro

Principiul optimalității

Programarea dinamică în
problemele NP-Complete

Mihai Oltean

Pentru a aplica metoda programării dinamice este necesar să putem descompune problema inițială într-o serie de subprobleme, incluse unele în altele. Pornim apoi, cu calculul, de la subproblemele cele mai simple (care nu au nici o subproblemă în subordine) către subproblemele cele mai complexe (care se descompun direct, sau indirect) în tot mai multe subprobleme. În majoritatea cazurilor întâlnite, numărul subproblemelor este polinomial. Numărul subproblemelor în care se descompune problema inițială este de data aceasta exponențial.

Drum hamiltonian

Un comis-voiajor trebuie să viziteze o singură dată fiecare din cele n+1 orașe, plecând dintr-un oraș oarecare și întorcându-se tot în acel oraș. Între oricare două orașe există un drum direct care are atașat un cost numit distanță. Care drum minimizează distanța totală parcursă de comis-voiajor?

Rezolvare

Numerotăm orașele cu numere distincte din intervalul [0, n].

Fără a pierde din generalitate, deoarece fiecare tur este ciclic, putem fixa originea în orașul 0. Presupunem că la un anumit pas al unui ciclu optim, care începe cu orașul 0, s-a ajuns la orașul i și au mai rămas k orașe j1, j2, ..., jk nevizitate înainte de a ne întoarce în 0. Este clar că, deoarece turul este optimal, acel drum care pornește din orașul i, trece prin orașele j1, j2, ..., jk și se întoarce în 0, trebuie să fie optim (să aibă lungime minimă), deoarece în caz contrar întregul ciclu nu ar mai fi optim, deoarece s-ar putea alege o cale mai scurtă de la i astfel încât să treacă prin orașele j1, j2, ..., jk, iar apoi să revină în 0.

Să definim deci:

c[i, j1, j2, ..., jk] = lungimea unui drum minim între orașul i și orașul 0 care trece o singură dată prin fiecare din orașele nevizitate j1, j2, ..., jk.

Soluția problemei se va afla în c[0, j1, j2, ..., jN].

Să notăm cu d[i,j] distanțele directe dintre două orașe i și j. Atunci, ca o consecință a observației anterioare avem:

Această formulă este o aplicație directă a principiului optimalității.

Pentru calcularea acestei formule începem cu:

c[i, j] = d[i, j] + d[j, 0],

iar apoi calculăm pe c[i, j1, j2], apoi pe c[i, j1, j2, j3] etc., iar în final pe c[0, j1, j2, ..., jN].

Secvența de valori jm care minimizează expresia de mai sus, conduce la drumul minim.

Să vedem cum implementăm aceste formule într-o complexitate cât mai redusă. Cum se vede și din formula optimalității, numărul de parametri ai funcției c este mare (în cel mai defavorabil caz N+1). Dar, deoarece numerele j1, j2, ...,jk nu trebuie stocate în ordine, pentru reținerea lor nu este nevoie de un șir de lungime k, ci de o mulțime având cardinalitatea k. Se știe că mulțimile în Turbo Pascal sunt reprezentate la nivel de bit, deci în loc să avem o reprezentare a numerelor j1, j2, ..., jk pe k octeți, vom avea nevoie de doar [k/8]+1 octeți.

Nici codificarea sub formă de mulțimi nu este foarte economă din punct de vedere al memoriei ocupate. Orașele sunt etichetate cu numere întregi din intervalul [0, n]. La pasul unu (inițial) trebuie să calculăm numerele c[i, j1] care sunt în număr de N. La pasul 2 trebuie să calculăm numerele c[i, j1, j2] care sunt în număr de , ..., la un pas k trebuie să calculăm numerele c[i, j1, j2, ..., jk] care sunt în număr de pentru un i fixat. Submulțimile j1, j2, ..., jk ale unei mulțimi de N elemente sunt în număr de . Acestea se pot genera în ordine lexicografică (ca și în cartea de telefon). Mai mult decât atât, dându-se o submulțime având cardinalitatea k a unei mulțimi de cardinal N, se poate obține direct numărul ei de ordine lexicografică. De exemplu, submulțimea {1, 2, ..., k} are numărul de ordine lexicografică 1, iar submulțimea {N-k+1, ..., N} are numărul de ordine lexicografică . Procedeul este valabil și invers, adică, dându-se un număr de ordine lexicografică se poate genera direct (printr-un algoritm de complexitate O(k)) submulțimea corespunzătoare de cardinalitate k.

Deci, revenind la problema noastră, nu este nevoie de reținerea întregii submulțimi ci doar de un singur număr care să indice numărul de ordine lexicografică. Cel mai mare număr de ordine lexicografică este acela pentru care k este jumătate din N.

Pentru a putea rezolva problema pentru un număr cât mai mare de orașe, stocarea acestei matrice se va face în Heap, adică accesul se va face în genul c[i]^[j]. Deoarece în momentul în care stocăm funcția c[i, j1, j2, ..., jk], trebuie să reținem și cardinalul mulțimii {j1, j2, ..., jk}; la impementare vom avea de a face cu încă un parametru: lungimea combinării. Deci matricea în care vom construi soluția are trei parametri c[i, k]^[w], unde i are semnificația de mai sus, k este lungimea combinării, iar w este numărul de ordine lexicografică al combinării (j1, j2, ..., jk) în cadrul mulțimii combinărilor de n elemente luate câte k.

Complexitatea va fi în acest caz de ordinul O(N32N), dacă facem abstracție de determinarea numărului de ordine lexicografică.

Programul care calculează drumul de cost minim este următorul.

Listing HAM.PAS

Reconstituirea soluției se face fie prin stocarea într-o altă matrice a poziției de proveniență a minimului nume-relor c[i]^[j], fie prin reconstituirea (de data aceasta de sus în jos) a soluției optime.

Listing procedura Reconstituie

Ieșire

Drumul minim are lungimea: 29

Drumul este: 0 1 3 2 4 0

Intrare

30 115 10 1115 0 6 2010 6 0 21 20 2 0

Ieșire

Drumul minim are lungimea: 37

Drumul este: 0 2 1 3 0

Putem reduce cantitatea de memorie folosită, deoarece știm că numerele c[i, k], depind doar de numerele c[i, k-1], adică doar de submulțimile de cardinal k-1 a mulțimii numerelor j1, j2, ..., jk.

În locul matricei c vom avea acum doi vectori c1 și c2.

c1,c2:array[0..MaxN] of ^TVectorDinamic;

Noua procedură Calcul este:

Listing procedura Calcul

În acest caz, reconstituirea soluției nu se mai face așa de simplu, ea necesitând refacerea matricei c.

Se presupune că nu între toate orașele există legături directe. Determinați în acest caz formulele de calcul ale numerelor c[i, j1, ..., jk] și implementați într-un program ceea ce ați obținut. Calculați drumul elementar de lungime maximă dintre două noduri ale acestui graf.

Diferență minimă

Se dă un șir de n cifre (n par). Se cere ca din cifrele date să se formeze două numere (fiecare de lungime [n/2]) astfel încât diferența dintre ele să fie minimă.

Restricții

Datele de intrare se citesc dintr-un fișier text având următoarea structură:

- pe prima linie se află numărul n (par);

- pe următoarea linie se dau cele n cifre, despărțite printr-un spațiu.

Soluție

Presupunem că la un anumit pas al unei soluții optime, punem a i-a cifră (din șirul de cifre dat) la primul număr și a j-a cifră la cel de al doilea număr și au mai rămas 2k cifre j1, j2, ..., j2k nefolosite. Construirea celor două numere se face în paralel (deoarece au și lungimi egale), începând cu cifra cea mai semnificativă și continuând spre cifra cea mai nesemnificativă.

Este clar că, deoarece diferența dintre numere este în final minimă, trebuie ca diferența dintre numerele care au cifrele cele mai semnificative a i-a, respectiv a j-a și care mai folosesc cifrele j1, j2, ..., j2k, să fie de asemenea minimă, deoarece în caz contrar întreaga diferență nu ar mai fi optimă pentru că s-ar putea face o altă distribuție mai bună a numerelor j1, j2, ..., j2k.

Să definim deci:

c[i, j, j1, j2, ..., j2k] = diferența dintre două numere care încep cu a i-a respectiv a j-a cifră (dintre cele date) și care mai folosesc și cifrele j1, j2, ..., j2k.

Soluția problemei se va afla printre numerele c[i, j, j1, j2, ..., jN-2].

Fie a vectorul în care am citit cifrele de la intrare. Avem:

Această formulă este o aplicație directă a principiului optimalității.

Pentru calcularea acestei formule începem cu:

c[i, j]=a[i]-a[j],

iar apoi calculăm pe c[i, j, j1, j2], apoi pe c[i, j, j1, j2, j3, j4] etc.

Secvența de valori (jp, jq) care minimizează expresia de mai sus conduce la soluția optimă.

Compexitatea acestui algoritm este O(N52N).

Listing IMPART.PAS

Intrare Ieșire

4 Diferenta minima: 1

1 1 9 2 Primul numar: 19

Al doilea numar: 21

Intrare Ieșire

6 Diferenta minima: 2

9 9 2 0 1 1 Primul numar: 201

Al doilea numar: 199

Ca și în cazul problemei drumului hamiltonian se poate implementa un program care să nu mai necesite parametrul k în matricea c. În acest caz reconstituirea numerelor care au generat această soluție s-ar face mai greu.

Concluzie

Ceea ce am propus în aceste pagini este o modalitate specială de a rezolva problemele NP-complete (sau chiar unele probleme polinomiale despre care nu suntem siguri că algoritmul propus este optim) folosind metoda programării dinamice. Ideea algoritmului este identică în ambele probleme rezolvate mai sus, de aceea, cu ajutorul ei se pot soluționa o serie de probleme având ordin de complexitate exponențial.

Bibliografie

1. Colecția Journal of the ACM

2. Mihai Oltean, Proiectarea și implementarea algoritmilor, Editura Computer Libris Agora, Cluj-Napoca, 1999.

Mihai Oltean este doctorand al Universității "Babeș-Bolyai" din Cluj și poate fi contactat pe adresa moltean@cs.ubbcluj.ro.

[cuprins]