U nastavku priče o algoritmima bavićemo se konstrukcijom algoritma koji rade sa nizovima. Kako će naglasak biti na idejama za konstrukciju algoritama, onda je razumno ignorisati konkretne tipove podataka (celi broj, niska,...), pa ćemo za ovu priliku smatrati da element je podatak kome tip nije preciziran.
Posledica takovog shvatanja je da npr. proces sortiranja u kome su jedine operacije uređivanje i kopiranje će biti opisan istim algoritmom i za niz celih brojeva i za niz imena, jer razlike postoje samo u realizaciji, ali ideje na kojima se zasniva algoritam su iste. Ako se radi o sortiranju elemenata nekog skupa (uređivanje po veličini), svojstva koja se podrazumevaju su:
elementi se mogu upoređivati na jednakost
elementi su iz potpuno uređenog skupa
elementi se mogu kopirati
Niz
čine ga elementi istog tipa
veličina niza je broj elemenata u njemu i veličina niza je fiksirana (tj. mora biti unapred poznata, jer se unapred mora znati i veličina dodeljene memorije)
ako niz ima n elemenata, onda je prvi element a[0], a poslednji element niza a[n-1].
ako je prvi element niza na lokaciji p, onda je k-ti element na lokaciji:
p+(k-1)*br_bajtova_za _1_element
Na višem programskom jeziku o poziciji elementa se ne razmišlja, jer taj posao obavlja kompajler
Prednost: jednako vreme pristupa elementima
Nedostaci: elementi su istog tipa, veličina je fiksirana
P1. Za zadate cele brojeve a1, a2,...,an pronaći maksimalnu vrednost sume ∑ k=i..j ak, gde 1<=i<=n, 1<=j<=n i u slučaju nepraznog podniza i <= j
Smatrati da maksimalna vrednost sume elemenata podniza niza a je nula, ako su svi članovi podniza negativni.
Test-primer:
n |
niz |
max suma podniza |
podniz |
---|---|---|---|
3 |
2, 4, 6 |
12 |
2,4,6 |
6 |
1, -3, 4,-2, -1, 6 |
7 |
4,-2, -1, 6 |
11 |
1, 2, 3, 4, -100, 20, -5, -6, 10, 8, -10 |
27 |
20, -5, -6, 10, 8 |
Pre algoritma, razmotrimo gore navedenu napomenu, odnosno mogućnost pojave praznog podniza.
U slučaju da su na ulazu svi brojevi negativni, izlaz će biti 0 (po formulaciji problema). Iako bi izlaz mogao biti negativni broj najmanje aposolutne vrednosti. Razlog leži u situaciji pojave praznog podniza (sa 0 elemenata), koji jeste podniz i čija suma jeste 0. Kako je prazni podniz niz sa uzastopnim članovima, jasno je da je njegova suma kandidat za rešenje , pa tako uvek postoji podniz uzastopnih elemenata čija suma je 0.
Problem br. 12 je interesantan za diskusiju, jer postoji mnogo algoritama koji rešavaju formulisani problem, ali perfomanse tih algoritama drastično variraju.
Slede četiri algoritma koji rešavaju problem 12
brute force algoritam reda O(n3)
poboljšani algoritam reda O(n2)
divide-and-conquer algoritam reda O(n log n)
algoritam red O(n)
1 |
input: a, n |
|
2 |
output: maxSum |
|
3 |
{ |
|
4 |
/*inicijalizacije*/ |
|
5 |
|
maxSum=0; |
6 |
|
/* sekStart i sekEnd su početak i završetak trenutne sekvence-kandidata za rešenje*/ |
7 |
|
sekStart=0; |
8 |
|
sekEnd=-1; |
9 |
|
for (i=0; i < n; i++) |
10 |
|
for (j=i;j <n; j++) |
11 |
|
{ |
12 |
|
tekSum=0; /* suma tekuce sekvence*/ |
13 |
|
|
14 |
|
for (k=i; k <= j; k++) |
15 |
|
tekSum += a[k]; /*formiranje sume tekuce sekvence*/ |
16 |
|
|
17 |
|
if (tekSum > maxSum) |
18 |
|
{ |
19 |
|
maxSum=tekSum; |
20 |
|
sekStart=i; /*indeks pocetka sekvence-kandidata za resenje*/ |
21 |
|
sekEnd=j; /*zavrsetak sekvence-kandidata za resenje*/ |
22 |
|
} |
23 |
|
} |
24 |
} |
|
Na
vreme izvršavanja algoritma dominatno utiče unutrašnji
for ciklus (14,15).
Postoje 4 naredbe:
inicijalizacija k=i
test k <= j
sabiranje u 15
inkrementacija k++
Broj
ponavljanja fragmenta 15 dominira među ova četiri izraza.
Zašto?
Dakle, potrebno je izračunati ukupan broj
izvršavanja fragmenta 15 u čitavom algoritmu.
Broj ponavljanja fragmenta 15 jednak je broju uređenih
trojki (i,j,k) za koje važi 1 <= i <= k <= j <=
n2, jer indeks i prolazi čitav niz, dok j
prolazi vrednosti od indeksa i do indeksa poslednjeg člana
niza, a k ima vrednosti izmeđi [i, j].
Ukupan broj tih trojki je n(n+1)(n+2)/6 tj. O(n3).
Zašto?
Može li se ukloniti petlja
14 i time poboljšati algoritam?
Može, jer a[i] + a[i+1]
+...+ a[j ]= (a[i] + a[i+1] +...+ a[j-1] ) + a[j] = A[j-1] + a[j],
tj. odbaciće se redundantnost ako se na već izračunatu
sumu A[j-1] doda a[j]. Zato algoritam kvadratne složenosti glasi:
1 input: a, n 2 output: maxSum 3 /* seqStart , seqEnd reprezentuju trenutnu sekvencu kandidata */ 4 5 { 6 maxSum = 0; 7 seqStart = 0; 8 seqEnd = -1; 9 10 11 12 for( i = 0; i < n; i++ ) 13 { 14 tekSum = 0; 15 for( j = i; j < n; j++ ) 16 { 17 tekSum += a[ j ]; 18 19 if( tekSum > maxSum ) 20 { 21 maxSum = tekSum; 22 seqStart = i; 23 seqEnd = j; 24 } 25 } 26 } 27 28 29 } 30
U ovom algoritmu spoljašnji for ciklus se prolazi n puta, unutar kog je ugnježden for ciklus koji se prolazi potencijalno n puta unutar kog su 1-koraġne naredbe dodele, sabiranja i poređenja, te oba for ciklusa imaju potencijalno n*n iteracija sa 1-koraġnim operacijam =>: vremenska složenost je O(n2).
Može li se ukloniti petlja 15 i time poboljšati algoritam?
Može, jer i algoritam broj
2 ima tzv. iscrpno pretraživanje svih mogućih rešenja,
tj. isprobavanje svih mogućih podnizova,
a razlika
algoritma 2 i algoritma 1 je što se kod algoritma 2 provera
svakog uzastopnog podniza obavi za O(1) vreme, a kod algoritma 1 za
O(n) vreme.
Međutim broj mogućih podnizova je O(n2),
te poboljšanje algoritam se može postići samo ako se
nađe način da se neki podnizovi eliminišu kako
kandidati i da se ne vrši izračunavanje njihovih suma i
poređenje sa tekućim maksimumom. Ali, to je tema algoritma
4.
/* algoritam tipa "zavadi, pa vladaj" reda O(n log n) */
/* - Rekurzivni algoritam. - Nalazi maximum sume u podnizu a[levo..desno]. - Ne radi se odrzavanje trenutne sekvence-kandidata za resenje */ 1 input: a, n 2 output: maxSum 3 { 4 maxSum = n > 0 ? maxSumRec( a, 0, n - 1 ) : 0; 5 } 6 private static int maxSumRec (a, left, right) 7 input: a, left, right 8 output: max { 9 maxLeftBorderSum = 0, maxRightBorderSum = 0; 10 leftBorderSum = 0, rightBorderSum = 0; 11 center = ( left + right ) / 2; 12 13 if( left == right ) /* bazni slucaj */ 14 return a[ left ] > 0 ? a[ left ] : 0; 15 16 /*rekurzivni pozivi po podnizovima*/ 17 maxLeftSum = maxSumRec( a, left, center ); 18 maxRightSum = maxSumRec( a, center + 1, right ); 19 20 for( i = center; i >= left; i-- ) 21 { 22 leftBorderSum += a[ i ]; 23 if( leftBorderSum > maxLeftBorderSum ) 24 maxLeftBorderSum = leftBorderSum; 25 } 26 27 for( i = center + 1; i <= right; i++ ) 28 { 29 rightBorderSum += a[ i ]; 30 if( rightBorderSum > maxRightBorderSum ) 31 maxRightBorderSum = rightBorderSum; 32 } 33 34 max= max3( maxLeftSum, maxRightSum, 35 maxLeftBorderSum + maxRightBorderSum ); 36 } 36 37 /* maximum tri broja */ 38 Algoritam max3( a, b, c ) 39 input: a,b,c 40 output: max 3 broja 41 { 42 return a > b ? a > c ? a : c : b > c ? b : c; 43 } 44
U
ovom algoritmu se koristi tehnika ZAVADI PA VLADAJ
(divide-and-conquer). Taj metod dekompozicije obuhvata
podelu ulaza na manje podskupove, njihovu obradu rekurzivnim
pozivom procedure (ZAVADI), i objedinjavanje dobijenih rešenja
(PA VLADAJ). Najčešće se problem deli u
potprobleme jednake veličine, tradicionalno se onaj fragment
algoritma koji ima bar dva rekurzivna poziva naziva
ZAVADI-PA-VLADAJ algoritam. Naravno, podproblemi (tj podskupovi
ulaza) se ne smeju preklapati, da bi se izbeglo prekomerni pozivi i
zračunavanja kakva npr. postoje u algoritmu koji rekurzijom
izračunava Fibonačijeve brojeve.
Ova tehnika se
koristi kod problema nalaženja konture skupa pravougaonika ili
sortiranja učešljavanjem ili quick sort-a.
Podniz maksimalne sume može:
kompletno pripadati levoj polovini niza ili
kompletno pripadati desnoj polovini niza ili
početi u levoj polovini niza, a zavr5iti u desnoj polovini niza
Zato je ideja sledeća
rekurzivno izračunati maksimalnu sumu podniza koji kompletno pripada levoj polovini niza (koraci 17)
rekurzivno izračunati maksimalnu sumu podniza koji kompletno pripada desnoj polovini niza (koraci 18)
u dva uzastopna ciklusa izračunati maksimalnu sumu podniza koji počinje u levoj polovini niza, a zavr5iti u desnoj polovini niza (koraci 20-32)
naći maksimum među te tri sume (koraci 34-35)
Kako
rekurzija zahteva i specificiranje baznog slučaja, tj. izlaz
je kada ulaz problema se sastoji samo od jednog elementa. U
linijama 13-14 obezbeđen je izlaz iz rekurzije, jer kada
left==right, onda rezultat je ili nula ili vrednost jedinog,
nenegativnog elementa.
U linijam 17 i 18 izvode se rekurzivni
pozivi, i ti pozivi se uvek obavljaju nad potproblemima manje
dimenzije od polaznog, tj. tim pozivima se napreduje ka baznom
slučaju, tj. izlazu iz rekurzije.
Složenost algoritma je
O(n log n). Zašto?
Označimo sa T(n) broj
potrebnih vremenskih jedinica za rešavanje problema
maksimalne sume podniza u nizu dimenzije n.
Tada je
T(1)=1 (linije 13-14)
T(n)= 2 T(n/2) + O(n) ( 2 rekurzije u linijama 17-18, plus linije 20-32 za const*N operacija račinaju maksimalnu sumu) )
Ova difrencna jednačina može da se reši tehnikom teleskopskih suma, ali i pozivom na Teoremu 2.1 iz knjige profesora Živkovića.
Teorema 2.1. Asimpototsko ponašanje niza T(n), rešenja difrencne jednačine
T(n) = a T(n/b) + c n k, a, b, c, k >=0, b !=0 i zadato je T(1)
dato je sa:
T(n)= O (n log b a ) za a > b k
T(n)= O (n k log n ) za a = b k
T(n)= O (n k ) za a < b k
Kako je u algoritmu 3:
a=2, b=2, a=b (tj. k=1), onda je T(n) = O (n log n)
Vratimo se algoritmu 2 i njegovoj modifikaciji, tj. može li se ukloniti petlja 15 i time poboljšati algoritam?
Može, jer i algoritam
broj 2 ima tzv. iscrpno pretraživanje svih mogućih rešenja,
tj. isprobavanje svih mogućih podnizova,
a razlika
algoritma 2 i algoritma 1 je što se kod algoritma 2 provera
svakog uzastopnog podniza obavi za O(1) vreme, a kod algoritma 1 za
O(n) vreme.
Međutim broj mogućih podnizova je O(n2),
te poboljšanje algoritam se može postići samo ako se
nađe način da se neki podnizovi eliminišu kako
kandidati i da se ne vrši izračunavanje njihovih suma i
poređenje sa tekućim maksimumom. To je tema algoritma 4.
OZNAKE:
Neka A[i,j] je podniza: a[i], a[i+1],...,a[j], neka S[i,j] =
a[i]+a[i+1] +...+ a[j]
Stav 1
Neka A[i,j] je ma koja sekvenca za koju je S[i,j] < 0. Ako je q
> j, onda A[i,q] nije podniz maksimalne sume.
DOKAZ: Jasno
je da: S[i,q]=S[i,j] + S[j+1, q]. Kako je S[i,j] < 0, onda je
S[i,q] < S[j+1, q] => A[i,q] ne može biti podniz maksimalne
sume.
Stav 2 Za ma koje i,
neka A[i,j] je prvi podniz za koji je S[i,j] < 0. Tada za neko p
takvo da i <= p <= j, p <= q važi da: ili A[p,q] nije
podniz maksimalne sume ili ima sumu jednaku sa do tada pronađenim
maksimuom.
DOKAZ:
Ako p==i, onda važi stav 1, tj. A[p,q] nije podniz maksimalne sume.
Inače,
S[i,q]=S[i,p-1]+S[p,q]
Kako j je najmanji indeks za koji
S[i,j] <0, onda je S[i, p-1] > = 0, pa je S[i,q] > S[p,q]
Ako je q > j (videti SLIKA 1), onda po stavu 1 sledi A[i,q]
nije podniz maksimalne sume, te to nije ni A[p,q]. Inače
(videti SLIKA 2), podniz A[p,q] ima sumu jednaku bar već
pronađenom podnizu A[i,q].
KOJI JE SMISAO UPOTREBE STAVA
1 i STAVA 2 u KONSTRUKCIJI ALGORITMA 4?
Po stavu 1 se u
algoritmu 2 može izaći iz unutrašnje petlje ako je
tekSum < 0 i to se koristi u liniji 21 u algoritmu 4, dok stav 2
nam govori da u liniji 23 se može preći sa indeksa i na j+1. U
algoritmu 4, skaniranje podnizova se obavlja uvećanjem indeksa
j, tako da jedini for ciklus iterira najviše n puta, a telo
for ciklusa ima 1-koračne naredbe dodele, sabiranja uz dva
poređenja, te složenost je O(n).
SLIKA 1
i |
j |
j+1 |
|
q |
S[i,q] |
||||
> = 0 |
< = S[i,q] |
|||
p-1 |
p |
SLIKA 2
i |
|
|
q |
j |
S[i,q] |
||||
> = 0 |
< = S[i,q] |
|||
p-1 |
p |
/* algoritam reda O(n) */ seqStart, seqEnd reprezentuju trenutnu sekvencu kandidata */ 1 input: a, n 2 output: maxSum 3 4 { 5 maxSum = 0; 6 tekSum = 0; 7 seqStart = 0; 8 seqEnd = -1; 9 10 11 for( i = 0, j = 0; j < n; j++ ) 12 { 13 thisSum += a[ j ]; 14 15 if( thisSum > maxSum ) 16 { 17 maxSum = thisSum; 18 seqStart = i; 19 seqEnd = j; 20 } 21 else if( thisSum < 0 ) 22 { 23 i = j + 1; 24 thisSum = 0; 25 } 26 } 27 28 29 }
P2. Porazmisliti o varijanti ovog zadatka sa proizvodom umesto sume.