Subsecvența de sumă maximă în C++
Subsecvența de sumă maximă este o problemă clasică de programare dinamică, întâlnită adesea la concursurile de informatică, într-o formă sau alta. În acest articol voi prezenta o soluție în bazată pe divide et impera, și două soluții în dintre care una folosește programare dinamică. De asemenea, voi arăta și cum putem rezolva eficient extinderea problemei la două dimensiuni.
Enunț
Se dă un vector cu elemente întregi, indexate de la Să se determine suma maximă a elementelor unei subsecvențe a vectorului dat, precum și o astfel de subsecvență. O subsecvență a vectorului reprezintă o succesiune de elemente de forma cu
Putem la fel de bine ca în loc de subsecvență să-i spunem doar secvență, dar având în vedere că în literatura de specialitate problema se numește Maximum Sum Subsequence, am păstrat termenul de subsecvență. Apropo, cuvântul este subsequence deoarece sequence este șirul de numere.
De exemplu, pentru o subsecvență de sumă maximă este suma elementelor acesteia fiind Soluția nu este neapărat unică. Pentru acest exemplu, o altă subsecvență de sumă maximă este care se întâmplă să fie întreg vectorul.
Soluții naive
O soluție imediată este să calculăm suma fiecărei subsecvențe în parte și să reținem pe parcurs suma maximă. Numărul de subsecvențe ale unui vector cu elemente este Putem demonstra asta foarte ușor, observând că numărul de subsecvențe care se termină pe poziția este căci începutul poate lua orice valoare din mulțimea Având în vedere că fiecărei subsecvențe îi calculăm suma elementelor în timp liniar, complexitatea finală a algoritmului devine
int ans = -2e9;for (int i = 1; i <= n; i++) for (int j = i; j <= n; j++) { int sum = 0; for (int k = i; k <= j; k++) sum += v[k]; ans = max(ans, sum); }
Putem optimiza această soluție calculând suma elementelor unei subsecvențe cu capătul din stânga fixat în pe măsură ce ne extindem în dreapta cu capătul Astfel, obținem un algoritm în complexitate ceea ce, în continuare, nu este mulțumitor.
int ans = -2e9;for (int i = 1; i <= n; i++) { int sum = 0; for (int j = i; j <= n; j++) { sum += v[j]; ans = max(ans, sum); }}
Soluție în – Divide et impera
Soluția în folosește paradigma divide et impera, fiind un algoritm ce ilustrează foarte bine principiul acestei metode de programare. Chiar dacă soluția nu este optimă, nu înseamnă că nu e utilă. Combinată cu un arbore de intervale, această metodă poate fi folosită pentru a rezolva probleme în care trebuie să găsim eficient subsecvența de sumă maximă pe anumite intervale dintr-un vector.
Iată deci cum funcționează algoritmul:
Divide: Împărțim vectorul în două părți aproximativ egale.
Stăpânește: Calculăm recursiv subsecvența de sumă maximă din fiecare jumătate.
Combină: Subsecvența de sumă maximă din întreg vectorul se poate afla fie în prima jumătate fie în a doua jumătate fie începe în prima jumătate și se termină în a doua Valorile corespunzătoare primelor două cazuri au fost calculate deja la pasul precedent. Pentru a rezolva al treilea caz, procedăm astfel: Îl fixăm pe în și pe în Îl decrementăm pe până la ținând pe parcurs suma maximă a unei secvențe care începe în și se termină în Procedăm similar și cu Adunând cele două sume, obținem rezultatul pentru al treilea caz. Nu ne mai rămâne decât să calculăm maximul dintre cele trei valori.
Desigur, cazul de bază este acela când secvența curentă are un singur element. În acest caz, returnăm respectiva valoare. Iată mai jos o implementare elegantă a acestei soluții. Funcția se apelează inițial cu parametrii și
int dei(int l, int r) { if (l == r) return v[l]; int bstL = -2e9, sumL = 0; for (int i = (l + r) / 2; i >= l; i--) bstL = max(bstL, sumL += v[i]); int bstR = -2e9, sumR = 0; for (int j = (l + r) / 2 + 1; j <= r; j++) bstR = max(bstR, sumR += v[j]); return max(bstL + bstR, max( dei(l, (l + r) / 2), dei((l + r) / 2 + 1, r) ));}
Complexitate
La fel ca majoritatea algoritmilor divide et impera, avem de a face cu o complexitate de ordinul lucru care poate fi observat ușor din arborele de execuție al funcției
Pe fiecare nivel, suma lungimilor secvențelor corespunzătoare apelurilor este Cum pentru fiecare apel în etapa combină efectuăm pași, complexitatea totală pe un nivel este În plus, adâncimea arborelui este aproximativ iar de aici putem trage concluzia că algoritmul nostru are complexitatea
Soluție în – Sume parțiale
Soluția următoare folosește doar conceptul de sume parțiale, și dintr-un motiv sau altul este destul de nepopulară. Probabil e mai șmecher să folosești programare dinamică Să notăm cu a a sumă parțială a lui Avem pentru iar Astfel, putem afirma că suma subsecvenței este egală cu Deci, suma maximă a unei subsecvențe ce se termină în se obține scăzând din cea mai mică sumă parțială cu
Așadar, ideea este să parcurgem vectorul menținând suma prefixului curent cât și suma parțială minimă din acest prefix La fiecare pas calculăm suma maximă a unei subsecvențe ce se termină pe poziția curentă și actualizăm maximul global dacă e cazul. Complexitatea este în mod evident Iată implementarea acestei metode:
int ans = -2e9, ps = 0, mn = 0;for (int i = 1; i <= n; i++) { ps += v[i]; ans = max(ans, ps - mn); mn = min(mn, ps);}
Soluție în – Programare dinamică
Această soluție folosește metoda programării dinamice și se numește Algoritmul lui Kadane. Dacă nu sunteți familiarizați cu programarea dinamică, nu este o problemă, aceasta fiind poate cea mai simplă formă posibilă de dinamică. Din nou, ideea este să calculăm pentru fiecare poziție din vector suma maximă a unei subsecvențe ce se termină în Vom nota această valoare cu Recurența este următoarea:
Explicația este simplă: Răspunsul pentru poate fi obținut din cel pentru adăugând elementul însă s-ar putea să fie negativ, caz în care este mai bine să considerăm doar elementul curent.
vector<int> dp(n + 1);int ans = -2e9;for (int i = 1; i <= n; i++) { dp[i] = max(dp[i - 1], 0) + v[i]; ans = max(ans, dp[i]);}
Putem reduce complexitatea în spațiu de la la renunțând la vectorul și ținând în schimb o singură variabilă egală cu valoarea stării precedente
int ans = -2e9, sum = 0;for (int i = 1; i <= n; i++) { sum = max(sum, 0) + v[i]; ans = max(ans, sum);}
Iar dacă tot am precizat în enunț că trebuie să determinăm și o subsecvență de sumă maximă, hai s-o facem măcar de data asta! Reținem în și capetele răspunsului, iar în capătul din stânga al subsecvenței curente de sumă maximă.
int ans = -2e9, l = 0, r = 0;int sum = 0, i = 0;for (int j = 1; j <= n; j++) { if (sum > 0) sum += v[j]; else { i = j; sum = v[j]; } if (sum > ans) { ans = sum; l = i; r = j; }}
Extinderea la 2D
Enunțul problemei noastre se poate extinde la două dimensiuni astfel: Dându-se o matrice cu linii și coloane, să se determine suma maximă a unei submatrice a lui De fapt chiar de aici a pornit totul: Problema inițială era cea 2D, dar pentru că nu se găseau soluții prea bune, s-a propus studiul problemei 1D pentru a înțelege mai bine mecanismul unui algoritm optim. Puteți citi aici istoricul problemei; mi se pare destul de interesant.
Soluția cea mai ineficientă are complexitatea și presupune calcularea sumei elementelor din fiecare submatrice posibilă. O putem reduce la precalculând sumele parțiale 2D în Dar se poate și mai bine, generalizând una dintre soluțiile liniare pentru problema unidimensională. O voi alege pe cea cu programare dinamică.
Ideea este să ne fixăm două linii și ale matricei, și să reducem problema găsirii submatricei de sumă maximă care începe pe linia și se termină pe linia la problema găsirii subsecvenței de sumă maximă într-un vector. În acest sens, vom crea un vector de lungime cu proprietatea Aplicând Algoritmul lui Kadane pentru fiecare pereche de linii cu și reținând maximul, am rezolvat problema în Mă rog, dacă e să fim riguroși, este Avem de la numărul de perechi și de la algoritmul liniar aplicat pentru fiecare astfel de pereche. Și de la actualizarea lui care este creat (sau inițializat cu pentru fiecare
int ans = -2e9;for (int i = 1; i <= m; i++) { vector<int> v(n + 1); for (int j = i; j <= m; j++) { for (int k = 1; k <= n; k++) v[k] += a[j][k]; int sum = 0; for (int k = 1; k <= n; k++) { sum = max(sum, 0) + v[k]; ans = max(ans, sum); } }}
Iată un exemplu pentru a vizualiza mai bine algoritmul:
Submatricea de sumă maximă este cea colorată cu roșu, și are suma elementelor Vectorul asociat perechii de linii este Subsecvența de sumă maximă a acestuia este are suma și îi corespunde submatricei cu colțurile în și care este chiar răspunsul.
Probleme recomandate
- Perle2 clasică
- Expresie2 evaluare de expresii
- Easygraph subsecvență de sumă maximă pe DAG
- Metrou2 dinamică pe biți, (aproape) subsecvență de sumă maximă pe graf
- SequenceQuery arbori de intervale + soluția cu divide et impera
- Maxq SequenceQuery + update-uri
- The Sacred Texts Maxq 2D
Dacă aveți vreo întrebare despre problema subsecvenței de sumă maximă, nu ezitați să o adresați mai jos, într-un comentariu