Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Leksione Te Algoritmikes 2020-2021

Download as pdf or txt
Download as pdf or txt
You are on page 1of 271

FAKULTETI I EKONOMISË

DEPARTAMENTI STATISTIKË DHE INFORMATIKË E ZBATUAR

LEKSIONE PËR LËNDËN ALGORITMIKË

VITI AKADEMIK 2020 - 2021

Prof. Dr. DHIMITRI TOLE

Tiranë, shtator 2020


Përmbajtja i

Tabela e Përmbajtjes
1 Hyrje....................................................................................................................................... 1
1.1 Çfarë është një algoritëm? ............................................................................................... 2
1.2 Fazat e zgjidhjes algoritmike së problemit ...................................................................... 2
1.2.1 Të kuptuarit e problemit............................................................................................ 3
1.2.2 Përcaktimi i kapaciteteve llogaritëse......................................................................... 4
1.2.3 Përcaktimi i tipit të zgjidhjes .................................................................................... 4
1.2.4 Përcaktimi i teknikës së hartimit të algoritmit .......................................................... 4
1.2.5 Hartimi i algoritmit ................................................................................................... 5
1.2.6 Mënyra e përshkrimit të algoritmit ........................................................................... 5
1.2.7 Të provuarit e korrektësisë së algoritmit................................................................... 5
1.2.8 Analiza e cilësive të algoritmit ................................................................................. 6
1.2.9 Programimi i algoritmit............................................................................................. 8
1.3 Të dhënat dhe organizimi i tyre ..................................................................................... 10
1.4 Instruksionet................................................................................................................... 11
1.5 Përmbledhje ................................................................................................................... 12
1.6 Ushtrime për Kapitullin 1 .............................................................................................. 12

2 Përshkrimi i algoritmeve ................................................................................................... 14


2.1 Elementet bazë të gjuhës algoritmike ............................................................................ 14
2.2 Të dhënat........................................................................................................................ 15
2.2.1 Tipet e të dhënave ................................................................................................... 16
2.2.2 Variablat .................................................................................................................. 16
2.2.3 Emërtimi i variablave .............................................................................................. 16
2.2.4 Konstantet ............................................................................................................... 17
2.2.5 Shprehjet ................................................................................................................. 17
2.2.6 Tabelat..................................................................................................................... 18
2.3 Instruksioni i vlerëdhënies ............................................................................................ 20
2.4 Instruksionet e hyrje/daljeve .......................................................................................... 22
2.4.1 Instruksioni i hyrjes së të dhënave .......................................................................... 22
2.4.2 Instruksioni i daljes së rezultateve .......................................................................... 23
2.5 Instruksioni i kushtëzuar ................................................................................................ 24
2.5.1 Instruksioni i kushtëzuar me 1-dalje. ...................................................................... 25
2.5.2 Instruksioni i kushtëzuar me 2-dalje ....................................................................... 25
2.5.3 Instruksione të kushtëzuara të përfshira .................................................................. 26
2.6 Instruksioni ciklik .......................................................................................................... 26
Përmbajtja ii

2.6.1 Cikli while...do ........................................................................................................ 27


2.6.2 Cikli do...while ....................................................................................................... 30
2.6.3 Cikli for .................................................................................................................. 31
2.6.4 Instruksione ciklikë të përfshirë .............................................................................. 33
2.6.5 Ndërprerja e parakohëshme e ciklit ........................................................................ 33
2.6.6 Gabimet më të zakonshme në cikle ........................................................................ 34
2.6.7 Krahasimi i cikleve ................................................................................................. 35
2.7 Funksionet ...................................................................................................................... 35
2.7.1 Funksione që kthejnë vlerë ..................................................................................... 37
2.7.2 Funksione që nuk kthejnë vlerë .............................................................................. 38
2.7.3 Cilin tip funksioni të përdorim? .............................................................................. 39
2.7.4 Pikëdalja nga funksioni ........................................................................................... 39
2.7.5 Disa funksione të gatshme ...................................................................................... 40
2.7.6 Përmbledhje ............................................................................................................ 41
2.8 Ushtrime për kapitullin 2 ............................................................................................... 42

3 Probleme me algoritme iterativë ....................................................................................... 45


3.1 Përdorimi i cikleve për llogaritje numerike ................................................................... 45
3.2 Përdorimi i cikleve përpunimin e tabelave .................................................................... 48
3.3 Përmbledhje ................................................................................................................... 54
3.4 Ushtrime për kapitullin 3 ............................................................................................... 55

4 Probleme me algoritme rekursivë ..................................................................................... 58


4.1 Përdorimi i rekursionit për llogaritje numerike ............................................................ 58
4.2 Perdorimi i rekursionit për përpunimin e tabelave ........................................................ 65
4.3 Përdorimi i rekursionit për zgjidhjen e problemeve përpunuese ................................... 67
4.4 Rekursioni dhe iteracioni .............................................................................................. 68
4.5 Përmbledhje ................................................................................................................... 69
4.6 Ushtrime kapitulli 4.6 .................................................................................................... 70

5 Analiza e efektshmërisë së algoritmeve............................................................................ 72


5.1 Kuadri i analizës ............................................................................................................ 72
5.1.1 Mjedisi llogaritës në modelin teorik ....................................................................... 73
5.1.2 Përmasa e të dhënave .............................................................................................. 75
5.1.3 Mënyrat e llogaritjes të kohës së ekzekutimit ......................................................... 75
5.1.4 Varësia e efektshmërisë nga cilësitë e veçanta të të dhënave ................................. 79
5.2 Analiza asimptotike ....................................................................................................... 81
5.2.1 Paraqitja joformale e simboleve asimptotike .......................................................... 84
Përmbajtja iii

5.2.2 Përkufizimet formale të simboleve asimptotike...................................................... 85


5.2.3 Si kryhet analiza asimptotike .................................................................................. 90
5.2.4 Klasat themelore të efektshmërisë .......................................................................... 91
5.2.5 Mangësitë e analizës asimptotike............................................................................ 92
5.2.6 Keqkuptimet e zakonshme ..................................................................................... 93
5.3 Analiza e efektshmërisë së algoritmeve iterativë të disa problemeve ........................... 93
5.4 Analiza e efektshmërisë së algoritmeve rekursivë të disa problemeve......................... 98
5.5 Analiza empirike e efektshmërisë së algoritmeve ....................................................... 109
5.6 Përmbledhje ................................................................................................................. 113
5.7 Formula të dobishme për analizën e efektshmërisë së algoritmeve ............................ 114
5.8 Ushtrime për Kapitullin 5 ............................................................................................ 114

6 Teknika e forcës brutale dhe kërkimi shterues .............................................................. 126


6.1 Renditja ........................................................................................................................ 127
6.1.1 Algoritmi i renditjes Selection sort ...................................................................... 128
6.1.2 Algoritmi i renditjes Bubble sort ......................................................................... 130
6.1.3 Algoritmi i renditjes Insertion sort ........................................................................ 131
6.2 Kërkimi i motivit.......................................................................................................... 133
6.3 Problemi i çiftit të pikave më të afërta dhe i mbështjellëses konvekse ....................... 135
6.4 Kërkimi shterues .......................................................................................................... 140
6.4.1 Problemi i tregtarit shëtitës ................................................................................... 140
6.4.2 Problemi i çantës së shpinës ................................................................................. 141
6.4.3 Problemi i caktimit të detyrave ............................................................................. 142
6.5 Përmbledhje ................................................................................................................. 143
6.6 Ushtrime për kapitullin 6 ............................................................................................. 143

7 Teknika zvogëlo-dhe-sundo ............................................................................................. 149


7.1 Redukto me një faktor konstant ................................................................................... 149
7.2 Zvogëlo me një madhësi variabël ................................................................................ 152
7.3 Përmbledhje ................................................................................................................. 157
7.4 Ushtrime për Kapitullin 7 ............................................................................................ 158

8 Teknika ndaj-dhe-sundo .................................................................................................. 161


8.1 Metoda të zgjidhjes së rekurrencave ............................................................................ 162
8.2 Algoritmi i renditjes Mergesort ................................................................................... 165
8.3 Algoritmi i renditjes Quicksort .................................................................................... 169
8.4 Shumëzimi i numrave të plotë të mëdhenj dhe shumëzimi i matricave ...................... 175
8.5 Problemi i çiftit të pikave më të afërta dhe i mbështjellëses konvekse ....................... 179
Përmbajtja iv

8.6 Përmbledhje ................................................................................................................. 182


8.7 Ushtrime për Kapitullin 8 ............................................................................................ 183

9 Teknika transformo-dhe-sundo ....................................................................................... 187


9.1 Thjeshtimi i rastit ......................................................................................................... 187
9.1.1 Pararenditja ........................................................................................................... 187
9.1.2 Eliminimi i Gausit ................................................................................................. 190
9.1.3 Shpërthimi LU dhe zbatime të tij .......................................................................... 194
9.2 Ndryshimi i paraqitjes .................................................................................................. 197
9.2.1 Rregulli i Hornerit ................................................................................................. 198
9.2.2 Llogaritja e fuqisë së një numri ............................................................................ 199
9.3 Reduktimi i problemit .................................................................................................. 202
9.4 Përmbledhje ................................................................................................................. 204
9.5 Ushtrime për kapitullin 9 ............................................................................................. 205

10 Kompromisi hapsirë−kohë ............................................................................................. 209


10.1 Renditja me anë të numërimit .................................................................................... 209
10.2 Problemi i kërkimit të motivit .................................................................................... 213
10.3 Përmbledhje ............................................................................................................... 216
10.4 Ushtrime për kapitullin 10 ......................................................................................... 217

11 Programimi dinamik ...................................................................................................... 219


11.1 Tre probleme bazë...................................................................................................... 221
11.1.1 Problemi i rreshtit me monedha .......................................................................... 221
11.1.2 Problemi i kusurit të monedhave ........................................................................ 223
11.1.3 Problemi i grumbullimit të monedhave .............................................................. 224
11.2 Problemi i çantës së shpinës ..................................................................................... 226
11.3 Funksionet memorizues ............................................................................................ 228
11.4 Përmbledhje ............................................................................................................... 229
11.5 Ushtrime për kapitullin 11 ......................................................................................... 230

12 Teknika lakmitare........................................................................................................... 231


12.1 Problemi i kusurit të monedhave ............................................................................... 232
12.2 Problemi i zgjedhjes së aktivitetit .............................................................................. 233
12.3 Teknika lakmitare dhe programimi dinamik.......................................................... 234
12.4 Përmbledhje ............................................................................................................... 234

13 Kufizimet e fuqisë së algoritmeve .................................................................................. 235


13.1 Argumentat e kufirit më të ulët .................................................................................. 236
Përmbajtja v

13.1.1 Kufijt më të ulët të qartë ..................................................................................... 236


13.1.2 Argumentat e informacionit teorik...................................................................... 237
13.1.3 Argumentat e kundërshtarit................................................................................. 238
13.1.4 Reduktimi i problemit ......................................................................................... 238
13.2 Pemët e vendimit........................................................................................................ 239
13.3 Vështirësitë e algoritmeve numerike ......................................................................... 241
13.3.1 Gabimet e këputjes .............................................................................................. 241
13.3.2 Gabimet e rrumbullakimit ................................................................................... 243
13.4 Përmbledhje ............................................................................................................... 248

14 Përballja me kufizimet e fuqisë së algoritmeve ............................................................ 249


14.1 Metoda Backtracking ................................................................................................. 250
14.1.1 Problemi i n-mbretëreshave ................................................................................ 250
14.1.2 Problemi i nënbashkësisë së shumave ................................................................ 252
14.1.3 Shënime të përgjithshme ..................................................................................... 253
14.2 Branch-and-bound...................................................................................................... 253
14.3 Algoritme për zgjidhjen e ekuacioneve jolineare ...................................................... 256
14.3.1 Metoda e përgjysmimit ....................................................................................... 257
14.3.2 Metoda e pozicionit fals ...................................................................................... 260
14.3.3 Metoda e Njutonit ............................................................................................... 261
14.4 Përmbledhje ............................................................................................................... 263
Përmbajtja vi
1 Hyrje |1

1 Hyrje

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të shpjegojë kuptimin e termit algoritëm;


• të njohë cilësitë e algoritmit;
• të identifikojë etapat e zgjidhjes algortimike të problemit;
• të shpjegojë rolin e të dhënave në një algoritëm;
• të kuptojë rolin e instruksionit në algoritëm.

Në shkencën e informatikës (computing), algoritmet (algorithms) zënë një vend të


rëndësishëm, për të mos thënë vendin kryesor. Një algoritëm i caktuar zgjidh probleme që
kanë të njëjtën strukturë por me të dhëna (data) të ndryshme. Të gjitha veprimet e përcaktuara
në një algoritëm ekzekutohen mbi disa të dhëna. Fjala “data” është numri shumës i fjalës
“datum”. Pasi të dhënat të jenë organizuar, përpunuar, strukturuar ose paraqitur në një
kontekst të dhënë për t’i bërë ato të dobishme atëherë ato quhen informacion.

Të dhënat janë termi, që mund të jetë i ri për fillestarët, por që është shumë interesant dhe i
thjeshtë për ta kuptuar. Mund të jetë diçka si emri i një njeriu ose një vendi ose një numër,
etj. Të dhënat janë emri që u jepet fakteve dhe njësive themelore të tillë si emrat dhe shifrat.
Si të dhëna mund të përmendim peshat, çmimet, kostot, numri i artikujve të shitur, emrat e
studentëve, notat, adresat, kodet e taksave, targat e automjeteve, pikturave, tingujve, videove
ose edhe programe kompjuterike, etj.

Në përgjithësi, të dhënat janë grupe me karaktere të bashkuara dhe të përkthyera me qëllime


specifike, zakonisht analize. Nëse e dhëna nuk është vendosur në kontekst, ato nuk vlejnë gjë
për njeriun apo kompjuterin.

Pse duhet të studjohen algoritmet? Për ata që duan të bëhen profesionistë në informatikë, ka
arsye teorike dhe praktike. Nga pikpamja praktike, është e domosdoshme që të fitohen
njohuri për një bashkësi të algoritmeve standardë të rëndësishëm që ekzistojnë në fusha të
ndryshme të informatikës; si dhe me qenë në aftë për të hartuar algoritme të rinj të efektshëm.
Nga pikpamja teorike studimi i algoritmeve, i quajtur edhe algoritmikë, (algorithmics) përbën
bazën e informatikës.

Një arsye tjetër për të studjuar algoritmet është dobia e tyre për zhvillimin e aftësive analitike.
Para së gjithash, algoritmet duhet të shihen si një kategori e veçantë për zgjidhjes e problemit,
jo si përgjigje por si procedura të sakta për të përftuar përgjigjet. Për rrjedhim, një teknikë e
caktuar për hartimin e algoritmit mund të interpretohet si një strategji për zgjidhjen e
problemit, që mund të jetë e dobishme pavarësisht nga fakti se në të është përfshirë një
kompjuter.

Fjala Algorithm, shqipëruar në algoritëm, rrjedh nga emri i matematicienit persian Abu
Abdullah Muhammad ibn Musa Al-Khwarizmi, që ka jetuar rreth viteve 800 të e.r.

Gjatë shekujve janë krijuar shumë algoritme. Ndërsa dhjetëvjeçarët e fundit, kompjuterat
kanë favorizuar zhvillimin e shumë algoritmeve të rinj.
1 Hyrje |2

1.1 Çfarë është një algoritëm?

Megjithëse nuk ka një përkufizim të pranuar nga të gjithë, mund të thuhet se ka një
marrëveshje të përgjithshme se çfarë nënkupton termi algoritëm:

Një algoritëm është një varg veprimesh të qarta për të zgjidhur një problem, domethënë
për të përftuar një rezultat të kërkuar për çdo të dhënë të ligjshme, brenda një kohe të
fundme.

Ky përkufizim mund të ilustrohet me anë të një diagramë të thjeshtë (Figura 1.1).

Problemi

Algoritmi

Të dhëna Ekzekutuesi Rezultati


(input-i) (kompjuteri, njeriu) (output-i)

Figura 1.1 Kuptimi i algoritmit

Përdorimi i fjalës “veprime” në përkufizimin e algoritmit, nënkupton që ekziston diçka apo


dikush që është i aftë të nënkuptojë dhe të ndjekë veprimet e dhëna. Ne e quajmë këtë
“kompjuter”, duke patur parasysh që para se të shpikeshin kompjuterët elektronikë, fjala
“kompjuter” nënkuptonte një njëri të përfshirë në kryerjen e llogaritjeve numerike. Në kohët
e sotme, natyrisht, kompjuterët janë ato pajisje elektronike të kudo ndodhura që janë të
domosdoshme në pjesën më të madhe të aktiviteteve që kryejmë. Gjithashtu, vëmë në dukje
se, megjithëse pjesa më e madhe e algoritmeve janë me të vërtetë të menduar për zbatimin në
kompjuter, koncepti i algoritmit nuk varet nga një supozim i tillë.

1.2 Fazat e zgjidhjes algoritmike së problemit

Le të përsëritim atë që kemi thënë për konceptin e algoritmit:

Ne i konsiderojmë algoritmet si një zgjidhje proceduriale për problemet.

Këto zgjidhje nuk janë përgjigjet por veprime të caktuara për të përftuar përgjigjet. Është ky
theksim mbi procedura konstruktive të përcaktuara me saktësi që e bëjnë informatikën të
dallueshme nga disiplinat e tjera. Në veçanti, kjo e dallon atë nga matematika teorike,
zbatuesit e të cilës, kënaqen thjesht me të provuarit e ekzistencës së një zgjidhjeje të një
problemi dhe ndoshta edhe me shqyrtimin e cilësive të zgjidhjes.

Më poshtë do të paraqesim dhe do të diskutojmë shkurtimisht një varg hapash tipike që duhet
të ndërmarim për hartimin dhe analizën e një algoritmi të dhënë (figura 1.2).
1 Hyrje |3

Të kuptuarit e problemit që do të zgjidhet

Marrja e vendimit për:


- kapacitetet llogaritëse
- tipin e zgjidhjes (e saktë apo e përafërt)
- teknikën e hartimit të algoritmit

Hartimi i algoritmit

Të provuarit e korrektësisë së algoritmit

Analizimi i cilësive së algoritmit

Programimi i algoritmit

Figura 1.2 Procesi i hartimit dhe analizës së algoritmit

1.2.1 Të kuptuarit e problemit


Nga pikpamja praktike, para se të hartohet një algoritëm, gjëja e parë e nevojshme, është që
të kuptohet plotësisht problemi i dhënë. Të lexohet me kujdes përshkrimi i problemit dhe të
bëhen pyetje nëse kihet dyshim mbi problemin, të zgjidhen raste të përmasave të vogla me
dorë, të mendohet për rastet e veçanta dhe përsëri të bëhen pyetje nëse e shihet e
domosdoshme.

Ekzistojnë disa probleme tipike që shfaqen shumë shpesh në zbatimet llogaritëse. Nëse
problemi në shqyrtim është njëri prej tyre, atëherë ju mund të përdoret një algoritëm të njohur
për ta zgjidhur atë. Natyrisht, ai ndihmon për të kuptuar si funksionon një algoritëm i tillë si
dhe për të njohur pikat e forta dhe të dobëta të tij, veçanërisht kur jepet mundësia për të
zgjedhur ndër disa algoritme të gatshëm. Por shumë herë, nuk gjendet një algoritëm të
gatshëm dhe prandaj duhet të hartohet algoritmin.

Një e dhënë për një algoritëm specifikon një rast (instance) të problemit që zgjidh algoritmi.
Është shumë e rëndësishme që të specifikohet me saktësi bashkësia e rasteve që mund të
zgjidhë algoritmi. Nëse nuk arrihet të bëhet kjo atëherë algoritmi mund të punojë me
korrektësi për pjesën më të madhe të të dhënave, por mund të dështojë për disa vlera
1 Hyrje |4

“kufitare”. Një algoritëm korrekt nuk është ai që punon në pjesën më të madhe të rasteve por
ai që punon në mënyrë korrekte për të gjitha të dhënat e ligjshme.

1.2.2 Përcaktimi i kapaciteteve llogaritëse


Pasi të kuptohet plotësisht një problem, duhet të përcaktohen se për cilat janë kapacitetet
llogaritëse ka nevojë algoritmi. Pjesa më e madhe e algoritmeve që janë sot në përdorim
janë ende të destinuar për t’u programuar në një kompjuter të asambluar afërsisht sipas
modelit të makinës së von Neumann-it, një arkitekturë kompjuteri e skicuar nga
matematikani i famshëm hungarezo-amerikan John von Neumann (1903-1957), në
bashkëpunim me A. Burks dhe H. Goldstine në vitin 1946. Thelbi i kësaj arkitekture është e
ashtuquajtura makinë me akses të rastit (random-access machine, RAM). Principi themelor
për këtë tip arkitekture është që instruksionet ekzekutohen njëri pas tjetrit, një veprim në një
kohë. Si rrjedhim, algoritmet e hartuar për t’u ekzekutuar në makina të tilla quhen algoritme
sekuenciale (sequential algorithms).

A duhet të shqetësohemi për shpejtësinë dhe sasinë e kujtesës të kompjuterit që kemi në


dispozicion? Nëse jemi duke hartuar një algoritëm si një eksperimentim shkencor përgjigja
është jo. Siç do ta shohim më vonë, shumë informatikanë preferojnë të studjojnë algoritmet
në pavarësi nga karakteristikat teknike të një kompjuteri të veçantë. Nëse jemi duke hartuar
një algoritëm si një mjet praktik, përgjigja mund të varet nga problemi që duhet të zgjidhet.
Bile kompjuterat e “ngadaltë” të sotëm janë në mënyrë të paimagjinueshme të shpejtë. Si
rrjedhim, në shumë situata, nuk duhet të shqetësoheni nëse një kompjuter po e kryen ngadalë
një punë. Megjithatë, ka probleme të rëndësishëm që nga natyra e tyre janë shumë të
ndërlikuar, që ose duhet të përpunojnë një volum shumë të madh të dhënash, ose kenë të
bëjnë me zbatime ku koha është faktor kritik. Në situata të tilla, është e domosdoshme që të
jemi të ndërgjegjshëm për shpejtësinë dhe kujtesën në dispozicion të një sistemi kompjuterik
të veçantë.

1.2.3 Përcaktimi i tipit të zgjidhjes


Më pas, vendimi kryesor është zgjedhja ndërmjet një zgjidhjeje të saktë të problemit apo një
zgjidhjeje të përafërt. Pse duhet të zgjedhim një zgjidhje të përafërt të problemit? Së pari,
sepse thjesht problemi nuk mund të zgjidhet në mënyrë të saktë për pjesën më të madhe të të
dhënave; si shembuj mund të përmendim gjetjen e rrënjës katrore, zgjidhjen e ekuacioneve
jolinearë, dhe njehsimin e integraleve të caktuar. Së dyti, algoritmet e disponueshëm për
zgjidhjen e saktë të një problemi mund të jenë shumë të ngadaltë përshkak të
ndërlikueshmërisë të brendshme të problemit. Kjo ndodh, në veçanti, për problemet që kanë
një numër të madh zgjedhjesh.

1.2.4 Përcaktimi i teknikës së hartimit të algoritmit


Tani, pas përcaktimit të të gjitha komponenteve të zgjidhjes algoritmike të problemit, si do të
hartohet një algoritëm për të zgjidhur një problem të dhënë. Kjo është çështja kryesore që do
të trajtojmë në këtë cikël leksionesh duke mësuar disa nga teknikat e përgjithshme të hartimit
të algoritmeve. Çfarë është një teknikë e hartimit të algoritmit?

Një teknikë e hartimit të algoritmit (algorithm design technique) ose shpesh e quajtur
edhe strategji apo paradigëm është një rrugë (approach) e përgjithshme për zgjidhjen
algoritmike të problemit që është e zbatueshme për një tërësi problemesh nga fusha të
ndryshme të informatikës.
1 Hyrje |5

Në këtë cikël të leksioneve do të paraqiten teknikat e forcës brutale, zvogëlo-dhe-sundo,


ndaj-dhe-sundo, transformo-dhe-sundo, kompromisi hapsirë-kohë, programimi dinamik dhe
teknika lakmitare. Ky klasifikim është përshtatur nga libri i A. Levitin "Introduction to
Design and Analysis of Algorithms". Teknikat e mësipërme mbështeten në disa ide
themelore që janë provuar se janë të dobishme në projektimin e algoritmeve.

Të mësuarit e këtyre teknikave ka një rëndësi të madhe për arsyet që pasojnë.

Së pari, ato shërbejnë si udhërëfyes për hartimin e algoritmeve për probleme të reja. Nuk
është e vërtetë sigurisht, që secila nga këto teknika të përgjithshme do të jetë
domosdoshmërisht e përdorshme për çdo problem që mund të takohet. Por marrë sëbashku,
ato përbëjnë një tërësi mjetesh të fuqishme për zgjidhjen algoritmike të problemeve.

Së dyti, algoritmet janë gurthemeli në shkencën e informatikës. Çdo shkencë është e


interesuar të klasifikojë subjektet e saj kryesore, dhe informatika nuk bën përjashtim.
Teknikat e hartimit të algoritmeve bëjnë të mundshëm klasifikimin e algoritmit në përputhje
me idenë në të cilën bazohet; prandaj ato shërbejnë si një rrugë e natyrshme për
kategorizimin dhe studimin e algoritmeve.

1.2.5 Hartimi i algoritmit


Megjithëse teknikat e hartimit të algoritmeve na furnizojnë me një bashkësi të fuqishme
mënyrash për zgjidhjen algoritmike të problemit, hartimi i një algoritmi për një problem të
veçantë mund të jetë ende një sfidë. Disa teknika hartimi, thjesht mund të jenë të
pazbatueshme për problemin në fjalë. Ndonjëherë, është e domosdoshme të kombinohen disa
teknika, dhe ka algoritme që është e vështirë të specifikohen si zbatime të teknikave të
njohura të hartimit. Bile edhe kur një teknikë e veçantë hartimi është e zbatueshme, përftimi i
një algoritmi shpesh kërkon një zgjuarsi jo të vogël nga ana e hartuesit të algoritmit. Me
pratikë, të dyja detyrat, zgjedhja ndër teknikat e përgjithshme dhe zbatimi i tyre, bëhen më të
lehta.

1.2.6 Mënyra e përshkrimit të algoritmit


Metodat për zgjidhjen e problemeve zakonisht shprehen në një gjuhë matematikore.
Megjithëse ajo është një gjuhë tepër rigoroze, ajo nuk është e aftë gjithmonë të përshkruajë
algoritmet mbasi nuk lejon specifikimin e disa aspekteve të cilat janë të rëndësishëm në një
algoritëm.

Nuk ekziston gjuhë standarde kushtuar përshkrimit të algoritmeve. Përdorimi i gjuhës së folur
(gjuha natyrale) do të ishte mjaft tërheqës; por, dykuptimësia (ambiguity) e trashëguar e çdo
gjuhe të folur e bën mjaft të vështirë përshkrimin e ngjeshur dhe të qartë të algoritmeve.
Për paraqitjen e algoritmeve, në këtë cikël leksionesh, është zgjedhur pseudokodi
(pseudocode). Fjalët kryesore, për të shprehur veprimet e algoritmit, do të huazohen nga
gjuha angleze për të qenë sa më afër me gjuhët e programimit. Pseudokodi është i
‘’ngjashëm’’ me gjuhët e programimit si C, C++, Java, Python, etj.

1.2.7 Të provuarit e korrektësisë së algoritmit


Sapo një algoritëm të specifikohet duhet të provohet korrektësia e tij. Domethënë, të provohet
që algoritmi prodhon një rezultat të kërkuar për çdo të dhënë fillestare të ligjshme në një sasi
kohe të fundme.
1 Hyrje |6

Për disa algoritme, të provuarit e korrektësisë është shumë i lehtë; ndërsa për disa të tjerë
mund të duket shumë i ndërlikuar. Një teknikë e zakonshme për të provuar korrektësinë e
algoritmit është përdorimi i induksionit matematik, mbasi iteracionet e algoritmit furnizojnë
një sekuencë natyrale të hapave, që janë të nevojshme për vërtetime të tilla.

Mund të jetë me vlerë të vëmë në dukje që, megjithëse gjurmimi i ekzekutimit të algoritmit
për disa të dhëna fillestare të veçanta mund të jetë një veprim që ja vlen, ai nuk mund të
provojë korrektësinë e algoritmit në mënyrë përfundimtare. Nga ana tjetër për të provuar që
një algoritëm i caktuar nuk është korrekt mjafton të gjejmë një rast të të dhënave fillestare për
të cilat algoritmi dështon.

Në këtë cikël me leksione nuk do të trajtohet korrektësia e algoritmeve.

1.2.8 Analiza e cilësive të algoritmit


Një algoritëm është një metodë hap-pas-hapi për zgjidhjen e një problemi, që në mënyrë
tipike merr të dhëna dhe nxjerr rezultate. Veç këtyre dy cilësive (të dhënat dhe rezultatet)
një algoritëm duhet të gëzojë edhe cilësitë e mëposhtme:

Të jetë i përgjithshëm. Algoritmi zbatohet për bashkësi të dhënash dhe jo vetëm për një të
dhënë të veçantë. Kështu një algoritëm duhet të hartohet për një klasë të tërë problemesh dhe
jo për një rast të veçantë të problemit.

Të përfundojë. Një algoritëm duhet të përshkruhet më anë të një numri të fundëm vëprimesh
të cilët nga ana e tyre duhet të ekzekutohen një numër të fundëm herësh. Algoritmi nuk mund
të përpunojë struktura të pafundme. E thënë ndryshe një algoritëm duhet të përfundojë,
d.m.th. të ndalojë pas një numri të fundëm veprimesh.

Të mos ketë dy-kuptime. Veprimet në një algoritëm duhet të specifikohen me rigorozitet,


d.m.th. pa dykuptueshmëri. Në çdo hap algoritmit duhet të dihet me saktësi se cili veprim do
të kryhet pas tij. E thënë ndryshe, një algoritëm duhet të karakterizohet nga saktësia, që
nënkupton që të gjitha hapat e tij duhet të specifikohen me saktësi.

Të jetë i efektshëm. Algoritmet janë të dobishëm vetëm nëse ata kanë nevojë për një sasi të
arsyeshme të burimeve kompjuterike. Me burime kompjuterike kuptojmë kujtesën për
rezervim dhe kohën e ekzekutimit.

Shembuj
1. Algoritmi duhet të jetë i përgjithshëm. Le të pretendojmë se kemi shpikur një algoritëm të
ri për të renditur një varg vlerash numerike në rendin rritës. Metoda që do të zbatojmë
mbështetet tek idea që për çdo çift elementesh të bashkëngjitur (duke filluar nga çifti i parë
dhe duke përfunduar në çiftin e fundit) kontrollon nëse elementi pasardhës është më i vogël
se paraardhësi. Nëse është e vërtetë këto dy vlera shkëmbehen. Përndryshe nuk bëhet asnjë
veprim. Le të zbatojmë këtë ide për vargun e të dhënave {2,1,4,3,5}.

Hapi 1. [ 2,1,4,3,5 ]. Krahasohet çifti (2,1). Vlerat shkëmbehen


Hapi 2. [ 1,2,4,3,5 ]. Krahasohet çifti (2,4). Vlerat nuk shkëmbehen.
Hapi 3. [ 1,2,4,3,5 ]. Krahasohet çifti (4,3). Vlerat shkëmbehen.
Hapi 4. [ 1,2,3,4,5 ]. Krahasohet çifti (4,5). Vlerat nuk shkëmbehen.
1 Hyrje |7

Si rezultat i këtyre 4 hapave vargu fillestar i vlerave është i renditur. A është i përgjithshëm
ky algoritëm renditje? Jo, nëse e zbatojmë algoritmin ndaj vargut [3,2,1,4,5] mbrijmë në
gjendjen përfundimtare [2,1,3,4,5]. Pra me një kundërshembull e hodhëm poshtë pretendimin
se algoritmi ishte i përgjithshëm.

2. Ekzistojnë probleme që nuk mund të zgjidhen me ndonjë algoritëm. Le të konsiderojmë


një numër natyral n dhe problemet e mëposhtme: (i) formoni bashkësinë e pjestuesve të n-së;
(ii) formoni bashkësinë e shumëzuesve të n-së.

Është e lehtë të hartohet një algoritëm për të zgjidhur problemin e parë. Problemi i dytë është
i keqshtruar nga pikpamja algoritmike. Duke qenë se bashkësia e shumëzuesve është e
pafundme, përsa kohe që ne nuk mund dimë një kriter ndalimi, ne nuk mund te zhvillojmë një
algoritëm për të zgjidhur problemin. Ne vetëm mund të hartojmë ndonjë algoritëm që të
gjenerojë të gjithë shumëzuesit që janë më të vegjël se një vlerë e dhënë.

3. Një algoritëm duhet të përfundojë. Le të konsiderojmë vargun e mëposhtëm të


instruksioneve:

Hapi 1. Vendos vlerën 1 tek x;


Hapi 2. Shto vlerën 2 tek x;
Hapi 3. Nëse x është i barabartë me 100 Ndalo, përndryshe shko tek Hapi 2.

Shohim që x asnjëherë nuk do të marrë vlerën 100, kështu që vargu i veprimeve nuk do të
ndalojë asnjëherë. Për këtë arsye sekuenca e mësipërme nuk mund të konsiderohet si një
algoritëm korrekt ndërkohë që qëllimi ynë ishte formimi i të gjithë numrave tek me të vegjël
se 100.

4. Veprimet në një algoritëm nuk duhet të kenë dykuptueshmëri. Le të konsiderojmë vargun


e mëposhtëm të veprimeve:

Hapi 1. Vendos vlerën 0 tek x;


Hapi 2. Ose zmadho x me 1 ose zvogëlo x me 1;
Hapi 3. Nëse x ∈[-7,7] shko tek Hapi 2, përndryshe Ndalo.

Përderisa nuk ka ndonjë kriter me anë të të cilit të mund të vendoset nëse x do të zmadhohet
apo të zvogëlohet, sekuenca nuk mund të konsiderohet si algoritëm. Dykuptueshmëria mund
të zgjidhet duke përdorur një gjuhë përshkrimi rigoroze. Le të konsiderojmë që Hapi 2 të
zëvendësohet me anë të :

Hapi 2. Hidh një monedhë. Nëse bie "figurë" atëherë zmadho x me 1 në të kundërt
zvogëlo me 1;

Në këtë rast përshkrimi nuk ka më dykuptueshmëri pavarsisht se në hedhje të ndryshme të


monedhës përftohen rezultate të ndryshme. Por çfarë mund të themi për përfundimin e
algoritmit? Nëse përftojmë në mënyrë alternative "figurë" dhe "shifër" vargu i veprimeve do
të jetë i pafundëm. Megjithatë ekziston mundësia për të përftuar 8 herë rresht "figurë" dhe
algoritmi do të ndalojë. Meqenëse probabiliteti i ndalimit është rigorozisht pozitiv algoritmi
mund të konsiderohet si korrekt. Megjithatë me modifikimin e Hapi 2, algoritmi bëhet i
rastësishëm.
1 Hyrje |8

5. Një algoritëm duhet të ndalojë pas kalimit të një kohe të arsyeshme. Le të konsiderojmë që
zgjidhja e një problemi kërkon përpunimin e n të dhënave dhe që numri i veprimeve
elementare T(n) varet nga kjo n. Le të supozojmë që çdo veprim elementar kryhet në 10-3
sekonda dhe që përmasa e problemit është n = 100. Nëse përdoret një algoritëm për të cilin
T(n) = n atëherë algoritmi do të zgjaste 100x 10-3=10-1 sekonda. Nga ana tjetër nëse do të
përdoret një algoritëm, numri i veprimeve elementare të të cilit do të ishte T(n) = 2n atëherë
algoritmi do të zgjaste 1027 sekonda që do të ishte 1019 vite!

1.2.9 Programimi i algoritmit


Duhet të kemi parasysh dallimin e domosdoshëm ndërmjet algoritmit dhe programit.
Algoritmi përshkruan një metodë zgjidhjeje për një problem të dhënë dhe ka një karakter të
përgjithshëm, që lejon të zbatohet në pjesën më të madhe, ndoshta në të gjitha gjuhët e
programimit. Një program nuk është gjë tjetër veçse përkthimi i këtij algoritmi në një gjuhë të
caktuar dhe që ka kuptim vetëm për një kompilator apo interpretues të gjuhës në fjalë. Më
poshtë paraqiten tre programe për të njëjtin problem, llogaritjen e faktorialit të një numri
natyror, respektivisht në gjuhët e programimit Java, Python dhe C++.

a. Programi për llogaritjen e faktorialit në gjuhën Java

import java.util.Scanner;

public class TestFaktorial


{
public static void main(String[] args )
{
Scanner keyboard = new Scanner(System.in);
int n;
//
System.out.print("Jepni numrin: ");
n = keyboard.nextInt();
System.out.println("Faktoriali = " + llogaritFaktorial(n)); // thirrja e funksionit
}

public static long llogaritFaktorial(int n) { // deklarimi i funksionit


long fact = 1;
for (int i = 2; i <= n; i++)
{
fact = fact * i;
}
return fact;
}
}
1 Hyrje |9

b. Programi për llogaritjen e faktorialit në gjuhën Python

def faktorial(n): # deklarimi i funksionit


fakt = 1
i=1
for i in range(2, n+1):
fakt = fakt * i
return fakt

n = int(input("Jep numrin? ")) # thirrja e funksionit


print ("Faktoriali= ", faktorial(n))

c. Programi për llogaritjen e faktorialit në gjuhën C++

#include<stdio.h>

long faktorial(int);

main()
{
int n;
long fact = 1;

printf("Jepni nje numer\n");


scanf("%d",&n);

printf("%d! = %ld\n", n, faktorial(n)); // thirrja e funksionit

return 0;
}

long faktorial(int n) // deklarimi i funksionit


{
int c;
long rezultat = 1;

for( c = 1 ; c <= n ; c++ )


rezultat = rezultat*c;

return ( rezultat );
}

Programimi (ose e thënë ndryshe kodimi) i një algoritmi përmban edhe një rrezik edhe një
përfitim. Rreziku lidhet me mundësinë që kalimi nga algoritmi në program të bëhet ose në
mënyrë të parregullt ose në mënyrë krejtësisht të paefektshme. Disa shkencëtarë të njohur te
informatikës, besojnë shumë se, megjithëse korrektësia e një algoritmi është provuar me
1 Hyrje | 10

rigorozitet matematik, programi nuk mund të quhet korrekt. Ata kanë zhvilluar teknika të
veçanta për të kryer vërtetime të tilla por fuqia e teknikave të tilla për verifikimin formal
është e kufizuar në programe shumë të vegjël.

Në praktikë, vlefshmëria e programeve ende vazhdon të provohet me anë të testimit. Testimi i


programeve të kompjuterit është më tepër një art sesa një shkencë por kjo nuk do të thotë se
nuk ka asgjë për të mësuar prej tij.

Një program që funksionon, jep një mundësi të re, për të kryer një analizë empirike të
algoritmit në të cilin ai mbështetet. Një analizë e tillë bazohet në matjet kohore të programit
për të dhëna të ndryshme dhe më pas në analizën e matjeve të kryera.

Në përfundim, le të theksojmë përsëri mësimin kryesor të procesit të pikturuar në figurën 1.2:

Si rregull, një algoritëm i mirë është një rezultat i përpjekjeve të përsëritura dhe
ripunimit.

1.3 Të dhënat dhe organizimi i tyre

Vlerat e të dhënave rezervohet në variabla. Këto janë njësi që mbajnë informacion të


rëndësishëm për problemin që do të zgjidhet. Në varësi të rolit që ato kanë në algoritëm, e
dhëna mund të jetë konstante (literalë, vlerë nuk mund të ndryshohet gjatë ekzekutimit të
algoritmit) ose variabël (vlera e saj mund të ndryshohet gjatë ekzekutimit të algoritmit). Në
kapitullin pasardhës do të flitet më me detaje për të dhënat dhe llojet e tyre.

Tipet bazë të Strukturave të të dhënave


Për t’u përpunuar të dhënat organizohen në struktura. Një strukturë për të dhënat është një
mjet i veçantë i organizmit të të dhënave në mënyrë që ato të përdoren me efektivitet. Çdo gjë
që mund të rezervojë të dhëna mund të quhet si strukturë e të dhënave. Nga ana e
përmbajtjes, strukturat e të dhënave klasifikohen në struktura të thjeshta dhe në struktura të
përbëra.

Struktura të thjeshta që rezervojnë një vlerë të vetme që mund të jetë një numër, një vlerë
logjike apo një karakter. Këto struktura njihen edhe si struktura primitive të të dhënave.

Strukturat e përbëra ose ndryshe strukturat komplekse përdoren për të rezervuar sasi të mëdha
dhe të lidhura ndërmjet tyre (ka një marrëdhënie). Disa nga këto struktura të të dhënave
abstrakte janë listuar në figurën 1.3. Këto struktura të të dhënave ne i zgjedhim në bazë të
tipit të veprimit që do të kryhet.

Në ciklin tonë të leksioneve do të përdoren strukturat primitive dhe struktura e përbërë e


quajtur tabelë (array). Në semestrin e dytë do të zhvilloni lëndën “Strukturat e të dhënave” ku
do të trajtohen strukturat e tjera të të dhënave.
1 Hyrje | 11

Strukturat e të dhënave

Strukturat primitive Strukturat komplekse

e plotë reale logjike karakter tabelat listat skedarët

listat lineare Listat jo-lineare

Stivat Rradhët Pemët Grafet

Figura 1.3 Strukturat e të dhënave

1.4 Instruksionet

Veprimet algoritmike përshkruhen me anë të instruksioneve të thjeshtë dhe/ose të strukturuar.


Instruksionet ndahen në:

Instruksioni i vlerëdhënies. Ajo lejon t'i caktohet një vlerë një variabli. Vlera e caktuar mund
të jetë rezultati i vlerësimit të një shprehje. Një shprehje përshkruan një llogaritje mbi disa të
dhëna dhe ajo përmban operandë (të dhënat mbi të cilat zbatohet) dhe operatorë (specifikon
llojin e llogaritjeve që bëhen mbi operandët).

Instruksionet e Hyrje/daljeve të të dhënave. Këta instruksione përdoren për hyrjen e të


dhënave në algoritëm dhe daljen e rezultateve të përpunimit nga algoritmi.

Instruksionet e kontrollit. Zakonisht instruksionet në një algoritëm ekzekutohen sipas


rradhës që janë përshkruar. Kur është e nevojshme që të modifikohet rradha e ekzekutimit
atëherë duhet të përdoret një instruksion i quajtur "shko tek". Po ashtu, kur është e nevojshme
një group me instruksione mund të përsëriten.

Duke kombinuar dy ose më shumë instruksione themelore mund të përftohen struktura


përpunimi (ose instruksione të përbërë) si:

Struktura sekuenciale. Ajo përbëhet të paktën nga dy instruksione (të thjeshta ose të përbëra)
të cilat ekzekutohen sipas radhës së dhënë.

Struktura e kushtëzuar. Ajo lejon të përshkruhen situata kur në varësi të një kushti
ekzekutohet një hap tjetër përpunimi. Kushti zakonisht është një shprehje logjike (ajo
1 Hyrje | 12

vlerësohet në një vlerë logjike: true ose false). Kjo lloj situatë shfaqet për shembull, kur
përpiqemi të vlerësojmë një funksion matematik si:

−1 𝑛ë𝑠𝑠 𝑥 < 0
𝑓(𝑥) = � 0 𝑛ë𝑠𝑠 𝑥 = 0
1 𝑛ë𝑠𝑠 𝑥 > 0

Struktura përsëritëse. Shërben për specifikimin e situatave kur një hap përpunimi i dhënë do
ekzekutohet në mënyrë të përsëritur. Përsëritja realizohet në dy mënyra të ndryshme:

• përsëritja me anë të iteracionit;


• përsëritja me anë të rekursionit.

Një strukturë e tillë shfaqet për shembull kur është e nevojshme të llogariten shuma të
fundme si ∑𝑛𝑖=1 𝑖. Në këtë rast hapi përpunues i cili përsëritet është shtimi i një termi tek
shuma dhe mbledhja do të vazhdojë derisa nuk janë shtuar të n termat.

Në vitin 1966, C. Böhm dhe G. Jacopini 1, vërtetuan matematikisht se një algoritëm i


çfardoshëm mund të jetë kombinim i tri strukturave: struktura e njëpasnjëshme (sequence),
struktura e kushtëzuar (selection) dhe struktura përsëritëse (repetition).

1.5 Përmbledhje

• një algoritëm është një varg me instruksione të qarta për të zgjidhur një problem në një
kohë të fundme. Një e dhënë fillestare specifikon një rast të caktuar të problemit që zgjidh
algoritmi;
• një algoritëm, për një problem të caktuar, përveç se duhet të ketë të dhëna dhe të nxjerrë
rezultate duhet edhe të jetë i përgjithshëm, të përfundojë, të mos ketë dy kuptime e të jetë
i efektshëm;
• algoritmi mund të përshkruhet në gjuhë natyrale ose me pseudokod; gjithashtu ata mund
të zbatohen si programe kompjuteri;
• teknikat e hartimit të algoritmeve (ose “strategji” ose “paradigma”) janë mënyra për të
zgjidhur problemet në mënyrë algoritmike, të zbatuara për klasa problemesh të fushave të
ndryshme të llogaritjeve;
• një algoritëm i çfardoshëm mund të jetë kombinim i strukturës së njëpasnjëshme ,
strukturës së kushtëzuar dhe strukturës përsëritëse;
• të dhënat e një algoritmi mund të jenë dhe/ose të pastrukturuara ose të strukturuara;
• një algoritëm i mirë është zakonisht një rezultat i ripunimit dhe përpjekjeve të përsëritura.

1.6 Ushtrime për Kapitullin 1

1. Kryeni disa kërkime në internet mbi al-Khorezmi (ose al-Khwarizmi), njeriu prej të
cilit rrjedh emri “algorithm”. Në veçanti duhet të mësoni se çfarë kanë të përbashkët
fjalët algorithm dhe algebra.

1
Corrado, B. and Jacopini, G. (May 1966). "Flow Diagrams, Turing Machines and Languages with Only Two
Formation Rules". Communications of the ACM 9 (5): 366–371.
1 Hyrje | 13

2. Duke patur parasysh sistemin e patentave që zbatohet për shpikjet, mendoni se algoritmet
duhet të regjistrohen si patenta (shpikësit të kenë të drejtën e autorit)?

3. Shkruani drejtimet e lëvizjes për të shkuar nga shtëpia e banimit në fakultet me


saktësinë e kërkuar nga një algoritëm.

4. Shkruani një recetë për gatimin e ushqimit më të pëlqyer nga ju me saktësinë që kërkon
një algoritëm.

5. Përshkruani punën e një automati që mbush shishet në një fabrikë uji me saktësinë që
kërkon një algoritëm.

6. Përshkruani punën për përgatitjen e kafes me anë të një ekspresi, me saktësinë që


kërkon një algoritëm.

7. Çfarë mund te thoni për veprimin e mëposhtëm kur a dhe b dy numra të çfardoshëm:

Hapi 1: T'i jepet variablit x çdonjëra nga a ose b

8. Çfarë mund të thoni nëse ekzekutohet vargu i mëposhtëm të veprimeve:

Hapi 1. T’i jepet variablit x vlera 0;


Hapi 2. Shto 2 tek x;
Hapi 3. Nëse x është i barabartë me 99 atëherë Ndalo përndryshe Shko tek Hapi 2

9. Çfarë mund të thoni duke konsideruar vargun e mëposhtëm të veprimeve:

Hapi 1.Vendos vlerën 0 tek x;


Hapi 2. Ose zmadho x me 1 ose zvogëlo x me 1;
Hapi 3. Në qoftë se x ∈ [-10, 10] shko tek Hapi 2, përndryshe Ndal.

10. Cila nga formulat e mëposhtme mund të konsiderohet si një algoritëm i gatshëm për të
llogaritur sipërfaqen, S, të një trekëndëshi, kur gjatësitë e brinjëve të të cilit jepen nga
numrat pozitivë a, b, c?
a. Sip = √p(p – a)(p-b)(p-c), ku p = (a + b + c) /2
b. S = ½ sin α, ku α është këndi ndërmjet brinjëve b dhe c
c. S = ½ a∙ha, ku ha është lartësia mbi bazën a.
2 Përshkrimi i algoritmeve | 14

2 Përshkrimi i algoritmeve

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të njohë elementet bazë të gjuhës algoritimike;


• të njohë të dhënat dhe rolin e variablave si mbartës së të dhënave;
• të njohë të dhënat e thjeshta dhe të dhënat e organizuara në formë grupi (tabelë);
• t’u japë vlerë variablave nëpërmjet instruksionit të vlerëdhënies;
• t’u japë vlerë variablave nëpërmjet instruksionit të hyrjes;
• të nxjerrë rezultate nga algortimi nëpërmjet instruksionit të daljes;
• të shpjegojë ndryshimin ndërmjet vlerëdhënies dhe zgjedhjes;
• të përdorë instruksionin if për të bërë zgjedhje një zgjedhje të thjeshtë në një algoritëm;
• të përdorë instruksionin if…else për të bërë zgjedhje ndërmjet dy mundësive në një
algoritëm;
• të përdorë instruksionin e përfshirë if…else për të bërë zgjedhje të shumëfishta në një
algoritëm;
• të shpjegoje termin iteracion;
• të përsëritë një bllok duke përdorur ciklin for;
• të përsëritë një bllok duke përdorur ciklin while;
• të përsëritë një bllok duke përdorur instruksionin do…while;
• të zgjedhë ciklin më të përshtatshëm për një pune të caktuar;
• të përdorë instruksionin break për të përfunduar një cikël;
• të shpjegojë kuptimin e termit funksion;
• të deklarojë dhe të përdorë funksione;
• të thërresë një funksion;
• të dallojë funksionet që kthejnë një vlerë dhe funksionet që nuk kthejnë një vlerë.

Përshkrimi e algoritmeve siç thamë do ta kryejmë me anë të gjuhës së pseudokodit. Gjuha e


pseudokodit mbështetet në një fjalor të vogël që përmban disa fjalë për të paraqitur
instruksionet dhe identifikatorët e përdorur për specifikimin e të dhënave. Si të gjitha gjuhët
ajo mbështetet në disa rregulla por që janë më pak shtërnguese sesa ato të gjuhëve të
programimit. Në pseudokod shprehjet aritmetike ose logjike shkruhen ashtu si në
matematikë.

2.1 Elementet bazë të gjuhës algoritmike

Elementet bazë të gjuhës algoritmike janë: shenjat themelore, fjalët, instruksionet, komentet,
algoritmi.

Shenjat themelore

Për të paraqitur gjuhën pseudokod do të përdoren shenjat e mëposhtme:

• Shkronjat: 26 shkronjat e gjuhës angleze (nuk ka dallim ndërmjet shkronjave të mëdha


dhe të vogla)
• Shifrat arabe: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
• Operatorët aritmetikë: + (mbledhja), – (zbritja), * (shumëzimi), / (pjesëtimi)
2 Përshkrimi i algoritmeve | 15

• Operatorët e krahasimit: = (baraz), < (rigorozisht më i vogël), ≤ (më e vogël ose baraz),
> (më i madh), ≥ (më i madh ose baraz), ≠ (i ndryshëm)
• Operatorët logjike: and (edhe), or (ose), not (jo)
• Operatori i vlerëdhënies: ←
• Shënja të veçanta ndarëse, ku dallojmë:
- Kllapat gjarpëruese respektivisht hapëse dhe mbyllëse { , }, që përdoren për të
formuar një bllok me instruksione dhe duhet të jenë gjithmonë të çiftuara.
- Kllapat katrore respektivisht hapëse dhe mbyllëse, [ ], që përdoren për të kufizuar
indekset në variablat me indeks.
- Kllapat e rrumbullakta respektivisht (, ). Ato kanë dy role. Përdoren për të
kufizuar listën e parametrave formalë në kokën e një funksionit gjatë deklarimit
dhe të parametrave faktikë gjatë thirrjes së tij për ekzekutim. Po ashtu përdorin si
ndarës në shprehjet e tipeve të ndryshme për modifikimin e përparësisë së kryerjes
së veprimeve.
- Pikëpresja ; , që përdoret për të treguar fundin e një instruksioni.
- Presja , e përdorur për të ndarë indekset në një tabelë dy përmasore.
- Shenja slash i dyfishtë //, i përdorur për të vendosur komente sqaruese që nuk
ekzekutohen në algoritëm.

Fjalët
Fjala formohen me anë të shkronjave dhe/ose shifrave. Fjalët ndahen në dy grupe. Në grupin
e parë futen fjalët çelës të gjuhës (key words) ose ndryshe fjalë të rezervuara të cilat përdoren
gjithmonë me një kuptim plotësisht të përcaktuar për formimin e instruksioneve. Ndërsa në
grupin e dytë futen fjalët që krijohen nga përdoruesi për emërtimin e variablave dhe të
elementeve të tjerë të gjuhës.

Fjalët çelës të cilat do të përdoren me një kuptim plotësisht të përcaktur janë:

and, break, do, else, false, for, if, input, or, print, return, step, then,to, true, while

Instruksionet
Fjalitë, të cilat në gjuhën algoritmike quhen instruksione, formohen me anë të fjalëve çelës
dhe disa shenjave të veçanta. Ato shërbejnë për të kryer veprimet e vlerëdhënies, hyrje/daljes
së të dhënave, zgjedhjen dhe përsëritjen.

Blloku
Një grup me instruksione fizikisht të njëpasnjëshme quhet bllok. Blloku mund të jetë edhe një
instruksion i vetëm.

Algoritmi
Shkruhet në formë rreshtash me një instruksion në rresht.

2.2 Të dhënat

Për zgjidhjen e një problemi duhet të kryhen veprime mbi të dhënat. Të dhënat janë lënda e
parë për algoritmin. Të dhënat përpunohen me anë të instruksioneve. Instruksionet kryejnë
2 Përshkrimi i algoritmeve | 16

llogaritjet dhe kontrollojnë rradhën e kryerjes së veprimeve për zgjidhjen e një problemi të
caktuar. Të dhënat në algoritme paraqiten nëpërmjet konstanteve dhe variablave.

2.2.1 Tipet e të dhënave

Një nga karakteristikat kryesore të të dhënës është tipi i saj. Në botën reale, ne përdorim gjatë
gjithë kohës të dhëna pa e vrarë mendjen për tipin e saj. Tipi i një të dhëne karakterizon
natyrën e saj, a është numër i plotë, numër real, ngjyrë, tingull, etj. Tipi i të dhënës përcakton
edhe veprimet që mund të kryhen me to, p.sh mund të mblidhen numrat por ndoshta jo
ngjyrat.

Tipet e të të dhënave që do të përdorim në këtë cikël me leksione janë:

• Numerik i plotë (integer) që përfaqëson të dhëna numerike të plota, të cilët mund të jenë
pozitive, negative duke përfshirë edhe vlerën 0. Për shembull: 2, -2, 0, 12345, -9876, etj.
• Numerik real (float) që përfaqson të dhëna numerike reale, që përmbajnë pikën dhjetore.
Për shembull: 123.34, +12.46, -12.9, 5.0, 0.0, 0.001, etj.
• Logjik (boolean), që përfaqëson dy vlerat logjike: true (E vërtetë) dhe false (E gabuar).
• Karakter (character) që përfaqëson çdo shkronjë të alfabetit, çdo shifër ose simbole të
tjera, të cilat sistemi i kodimit të brendshëm të kompjuterit është i aftë t’i paraqesë. Për
shembull: A, a, W, 0, 1, +, - , *, $, etj.
• String që përfaqson një varg me karaktere që do ta përdorim për mesazhe.

2.2.2 Variablat
Një variabël është një emër që referon një vlerë të një tipi të caktuar. Një variabël mund ta
imagjinojmë si një vend në të cilën mund të shkruajmë diçka. Në një çast të caktuar, një
variabël do të shërbejë për një të dhënë të caktuar, të quajtur vlera e variablit, e cila mund të
ndryshojë në çdo çast gjatë procesit llogaritës. Vlera e një variabli mund të ndryshojë shumë
herë gjatë ekzekutimit të algoritmit por vetëm me anë të veprimit themelor në algoritmikë,
veprimit të vlerëdhënies. Pra një variabël ka një emër, një tip dhe një vlerë.

2.2.3 Emërtimi i variablave


Variablat emërtohen me anë të emrave. Emrat nuk janë gjë tjetër veçse fjalë të krijuara nga
përdoruesi që formohen me anë të shkronjave, shifrave. Emri duhet të fillojnë medoemos me
një shkronjë. Nuk ka kufizim përsa i përket sasisë së shkronjave që përdorim për të emërtuar
një variabël. Nuk bëhet dallim ndërmjet shkronjave të mëdha dhe të vogla.

Ja disa emra korrektë:

a a10 n vleraFillestare Paga klienti shumaGjithsej s count i

dhe disa emra jokorrekt:

1a (fillon me shifër); paga 1 (përmban një hapsirë)


2 Përshkrimi i algoritmeve | 17

2.2.4 Konstantet
Ndonjëherë kërkohet që disa të dhëna të ruajnë të njëjtën vlerë gjatë gjithë ekzekutimit të
algoritmit, pra të jenë konstante (literalë). Një konstante është një vlerë e veçantë, pamja e së
cilës tregon vlerën që ka. Konstantet mund të jenë të tipit numerik, karakter, bulean dhe
string. Konstantet numerike shkruhen si në matematikë. Konstantet karakter dhe string
përfshihen në thojza kur përdoren në një algoritëm. Konstante buleane janë dy: true (e
vërtetë) dhe false (e gabuar).

Shembuj
Konstante numerike të plota: +5, 5, -5, 0.
Konstante reale: +5.0, 5.25, -5.5, 0.0.
Konstante karakter: “A”, “a”, “$”.
Konstante string: “Tirana”, “notat e studenteve”
Konstantet logjike: true, false.

2.2.5 Shprehjet
Një shprehje është një kombinim i ligjshëm variablave, i konstanteve, operatorëve (me
përjashtim të operatorit të vlerëdhënies), kllapave të rumbullakta dhe funksioneve që
interpretohet si një llogaritje që prodhon një vlerë të një tipi të caktuar. Tipi i vlerës varet nga
tipi i variablave, konstanteve dhe operatorëve që marrin pjesë në të.

Sipas tipit të vlerës që prodhon, shprehjet klasifikohen në:

• shprehje numerike, të plota ose reale, ku vlera e saj është një numër i plotë ose real;
• shprehje logjike, ku vlera e saj është një vlerë logjike;
• shprehje tekst, ku rezultati është një varg shenjash të tipit karakter.

Shprehjet numerike
Shprehja numerike është një vlerë që përftohet si rezultat i veprimeve aritmetike. Një
shprehje numerike (e plotë ose reale), ashtu si në matematikë, përbëhet nga të dhëna
numerike, të përfaqësuara nga vlera konstante numerike, variabla numerikë, funksione
numerikë, operatorë të veprimeve aritmetike si dhe nga kllapat e rrumbullakta. Variablat e
përdoruar në shprehje duhet të kenë marrë vlerë më parë me anë të një veprimi vlerëdhënie
ose me anë të funksionit të hyrjes së të dhënave nga algoritmi. Vlera e shprehjes numerike
llogaritet duke i u nënështruar rregullave të matematikës.

Nëse në një shprehje numerike ka më tepër së një operator, rregullat që do të zbatohen se cili
veprim do të kryhet i pari jepen më poshtë:

• Të gjitha shprehjet në kllapa vlerësohen të parat


• Operatorët * dhe / kryhen të dytët
• Operatorët + dhe – kryhen të fundit.

Nëse ka më tepër se një operator shumëzimi ose pjesëtimi në një shprehje, atëherë operatori
më i majtë kryhet i pari. Në mënyrë të ngjashme, nëse ka më shumë se një operator
mbledhjeje ose zbritjeje në një shprehje atëherë veprimet kryhen nga e majta në të djathtë.
Për shembull shprehja a + b + c llogaritet sikur të jetë shkruar (a + b) + c.

Shembuj
2 Përshkrimi i algoritmeve | 18

3 + 5; a+b; 2*(a - b)/2; b*b – 4*a*c.

Shprehjet e krahasimit dhe logjike


Shprehja e krahasimit formohet nga shprehje numerike dhe nga operatorët e krahasimit
ndërsa shprehja logjike formohet nga shprehje krahasimi dhe nga operatorët logjikë, (Tabela
2.1). Ne mund të përdorim edhe termin shprehje buleane 2.

Tabela 2.1 Shprehje krahasimi dhe logjike


Shprehja Tipi Vlera e shprehjes kur ( x = 2 dhe y = 3)
x < y Krahasim true
x ≤ y Krahasim true
x = y Krahasim false
x ≠ y Krahasim true
x ≥ y Krahasim false
x > y Krahasim false
x < 7 and y = 12 Logjik false
x ≠ y or y ≥ 10 Logjik true
not (x ≤ y) Logjik false

Përparësia e operatorëve në një shprehje


Operatorët e veprimeve aritmetike kanë përparësi ndaj atyre të krahasimit, të cilët kanë
përparësi ndaj atyre logjikë, me përjashtim të operatorit not, (Tabela 2.2). Operatori not, që
është një operator vetëm me një operand ka përparësinë më të lartë, pas kllapave të
rrumbullakta. Brenda të njëjtit nivel përparësie rradha e kryerjes së veprimeve është nga e
majta në të djathtë.

Tabela 2.2 Rradha e përparësisë së operatorëve


Rradha e përparësisë Operatorët Koment
1 (më e larta) () për të ndryshuar rradhën e veprimeve
2 not mohimi logjik
3 * / përparësia nga e majta në të djathtë
4 + – përparësia nga e majta në të djathtë
5 & bashkimi i të dhënave të tipit string
6 = ≠ < ≤ > ≥ përparësia nga e majta në të djathtë
7 and edhe logjik
8 (më e ulta) or ose logjike

2.2.6 Tabelat
Në bashkësinë e strukturave të të dhënave një rol të veçantë luan struktura e të dhënave e
quajtur tabelë (array). Kjo strukturë ka një rol të dyfishtë: shërben për të mbajtur një grup me
të dhëna, të njëjta sipas tipit, për përpunime të ndryshme si dhe shërben si bazë për krijimin
2
George Bool, 1815-1864, matematicien, filozof dhe logjicist anglez.
2 Përshkrimi i algoritmeve | 19

e strukturave të tjera të të dhënave. Nga mënyra e organizimit tabelat ndahen në tabela një-
dimensionale (vektor), dy-dimensionale (matricë) dhe shumë-dimensionale.

Tabela një-dimensionale
Tabela një-dimensionale (koncepti i vektorit në matematikë) është një grup me elemente që
bëhen të aksesueshëm me anë të specifikimit të një vlere numerike të plotë që quhet indeks.

Karakteristikat më të rëndësishme të tabelës janë:

• Elementet e një tabele të caktuar kanë të njëjtin tip të dhëne.


• Çdo element i tabelës mund të aksesohet në mënyrë të drejtëpërdrejtë.
• Çdo element i tabelës ka një emër të vetëm që përbëhet nga emri i tabelës dhe nga indeksi
i përfshirë në kllapa katrore. Ky element quhet variabël me indeks. Indeksi nuk është gje
tjetër veçse adresa e elementit. Ai tregon pozicionin e një elementi në tabelë.
• Indeksi mund të jetë një konstante numerike e plotë ose një variabël i tipit të plotë ose
një shprehje numerike, rezultati i së cilës duhet të jetë një vlerë numerike e plotë.
• Koha e aksesit, për lexim ose shkrim, për çdo element të tabelës është e njëjtë pavarsisht
nga pozicioni i tij në tabelë.
• Ndërmjet dy elementeve të tabelës nuk ka hapsirë për një element tjetër.

Tabela është një strukturë e tillë e të dhënave që furnizohet vetëm me dy veprime elementare:
(i) të kapet vlera e një element të caktuar dhe (ii) t’i jepet vlerë (të shkruhet) një elementi të
caktuar. Gjithë veprimet e tjera mund të realizohen vetëm me anë të algoritmeve të thjeshtë
apo të ndërlikuar në varësi të problemit.

Një tabelë mund të organizohet me një numër të fiksuar elementesh ose me një numër
variabël. Sasia elementeve që përmban tabela quhet përmasë e tabelës. Përmasa e tabelës
zmadhohet me 1 nëse në tabelë shtohet një element dhe pakësohet me 1 nëse nga tabela
largohet një element.

Për një tabelë me përmasë n, indeksi do të jetë një numër i plotë ndërmjet 0 dhe n – 1 ose
ndërmjet 1 dhe n. Në raste të veçanta do të përdorim edhe indekse jonumerike si për
shembull, shkronjat e alfabetit.

Nëse një tabelë prej n elementesh është indeksuar nga 0 tek n – 1 atëherë elementi i parë
tabelës referohet si a[0], elementi i dytë referohet si a[1], elementi i shtatë referohet si a[6]
dhe elementi i fundit referohet si a[n-1]. Në përgjithësi elementi i itë i tabelës referohet si a[i –
1] (Tabela 2.3). Elementet e tabelës të referuar janë variabla me indeks.

Tabela 2.3 Një tabelë një dimensionale me n elemente, indeksuar nga 0 tek n -1
pozicioni 0 1 i n–1
referimi a[0] a[1] ··· a[i] ··· a[n–1]
vlera 10 8 –7 25

Për një element të caktuar a[i], paraardhësi i tij është elementi a[i – 1] ndërsa pasardhësi është
elementi a[i + 1]. Elementi i parë nuk ka paraardhës dhe elementi i fundit nuk ka pasardhës.

Emrat e tabelave ashtu si emrat e variablave të tjerë mund të formohen vetëm me anë të
shkronjave dhe shifrave. Emri i tabelës nuk mund të fillojë me shifër.
2 Përshkrimi i algoritmeve | 20

Variablat me indeks mund të marrin vlerë me anë të instruksionit të vlerëdhënies, hyrjes së të


dhënave si dhe të marrin pjesë në shprehjet aritmetike dhe në shprehjet logjike. Për shembull
nëse i = 5 dhe j = 6 atëherë instruksioni

a[i + j] ← a[i + j] + 1;

shton 1 tek elementi a[11] i tabelës.

Nëse një tabelë prej n elementesh është indeksuar nga 1 tek n atëherë elementi i parë i
tabelës referohet si a[1], elementi i dytë referohet si a[2], elementi i shtatë referohet si a[7]
dhe elementi i fundit referohet si a[n]. Në përgjithësi elementi i itë i tabelës referohet si a[i]
(Tabela 2.4).

Tabela 2.4 Një tabelë një dimensionale me n elemente, indeksuar nga 1 tek n
pozicioni 1 2 i+1 n
referimi a[1] a[2] ··· a[i+1] ··· a[n]
vlera 10 8 -7 25

Tabela dy-dimensionale
Një tabelë dy-dimensionale (shpesh quhet edhe matricë) mund të përfytyrohet si e përbërë
nga m tabela një-dimensionale me n elemente secila (Tabela 2.5). Në kuptimin gjeometrik
rreshtat shtrihen horizontalisht ndërsa shtyllat vertikalisht. Për të aksesuar një element të
caktuar të saj duhet të përdoren dy indekse. Indeksi i parë tregon rreshtin ndërsa indeksi i
dytë tregon shtyllën. Indekset ndahen nga njëri tjetri me presje dhe përfshihen në kllapa
katrore. Vlerat e indeksit të rreshtave përfshihen në segmentin [0, m – 1] ose [1, m] ndërsa
vlerat e indeksit të shtyllave përfshihen në segmentin [0, n – 1] ose [1, n]. Me përjashtim të
faktit që një element i caktuar aksesohet me anë të dy indekseve, të gjitha cilësitë që zotëron
tabela një-dimensionale i zotëron edhe tabela dy-dimensionale.

Tabela 2.5 Një tabelë 2-dimensionale me m rreshta dhe n shtylla


a[1, 1] a[1, 2] ··· a[1, j] ··· a[1, n]
a[2, 1] a[2, 2] ··· a[2, j] ··· a[2, n]
··· ··· ··· ··· ··· ···
a[i, 1] a[i, 2] ··· a[i, j] ··· a[i, n]
··· ··· ··· ··· ··· ···
a[m, 1] a[m, 2] ··· a[m, j] ··· a[m, n]

2.3 Instruksioni i vlerëdhënies

Instruksioni i vlerëdhënies (assignment statement) i jep një vlerë një variabli brenda
algoritmit. Vlera që rezervohet mund të jetë një vlerë numerike (e plotë ose reale), një vlerë
logjike, një karakter, apo një string. Në përgjithësi kjo vlerë formohet si rezultat i llogaritjes
së një shprehjeje. Për të paraqitur veprimin e vlerëdhënies në pseudokod do të përdorim një
simbol grafik, shenjën e shigjetës me majë majtas, ←. Theksojmë se është i vetmi instruksion
që mund të ndryshojë vlerën e një variabli.

Forma e përgjithshme e instrksionit të vlerëdhënies ështëË


2 Përshkrimi i algoritmeve | 21

emerVariabli ← shprehje;

Si funksionon veprimi i vlerëdhënies? Së pari vlerësohet shprehja (llogaritet vlera e saj) dhe
pastaj, vlera që rezulton, kopjohet tek variabli i caktuar. Vëmë në dukje që kjo vlerë
zëvendëson vlerën e “vjetër” që ka patur variabli.

Ja disa shembuj të vlerëdhënies.

a ← 8; // variabli a merr vlerën e plotë 8


a ← 9; // variabli a merr vlerën 9
b ← 15.5; // variabli b merr vlerën reale 15.5
a ← 2 * b + a; // variabli a merr vlerën reale 40.0
a ← a – b + 2 * (a – 1 ); // variabli a merr vlerën reale 102.5
përgjigjja ← true; // variabli a merr vlerën buleane true
test ← a < b; // variabli test merr vlerën buleane false
qyteti ← “Tirana”; // variabli qyteti merr vleren string Tirana

Fakti që vlera e vjetër e një variabli eleminohet nga vlera e re gjatë një veprimi vlerëdhënie
na shpie në përdorimin e një variabli të tretë në rast se duam të shkëmbejmë (swap) vlerat e
dy variablave.

Problemi. Të shkëmbehen vlerat e dy variablave të dhënë a, b.

// Algoritmi 2.1 Shkëmben vlerat e dy variablave


// Të dhëna: a, b, dy vlera
// Rezultati: a, b me vlera të shkëmbyera

swap(a,b) {
temp ← a;
a ← b;
b ← temp;
}

Nga pikëpamja funksionale disa nga përdorimet më tipike të veprimit të vlerëdhënies për të
dhëna numerike janë inicializimi i një variabli, zmadhimi i vlerës së një numëratori,
zvogëlimi i vlerës së një numëratori dhe akumulimi.

Inicializimi i një variabli numerik


Me marrëveshje, në fillim të një algoritmi, vlerat e variablave janë të papërcaktuara (në një
farë mënyre ata përmbajnë “nuk ka rëndësi se çfarë përmbajnë”). Kështu që shpesh është e
domosdoshme, që para përdorimit, atyre t’u jepet një vlere fillestare e caktuar. Për të dalluar
këtë vlerëdhënie të veçantë përdoret termi inicializim. Zakonisht thuhet se një variabël
inicializohet atëherë kur në të dërgohet një vlerë konstante, zakonisht 0 ose 1, por nuk
përjashtohen edhe vlera të tjera. Variabla të tillë përdoren zakonisht si numëratorë dhe
përdoren për të numëruar.

Për shembull, instruksioni count ← 0 bën që vlera 0 të rezervohet në variablin e quajtur


count.
2 Përshkrimi i algoritmeve | 22

Zmadhimi me 1 njësi i vlerës së një numëratori


Një nga veprimet më të shpeshta në instruksionin ciklik është zmadhimi me një njësi i vlerës
aktuale të numëratorit të ciklit. Për shembull, instruksioni count ← count + 1 nënkupton që
vlerës aktuale të variablit count i shtohet vlera 1 dhe rezultatin e dërgon përsëri tek variabli
count, duke zmadhuar në këtë mënyrë vlerën e tij të mëparshme.

Për shembull, nëse variabli count ka si vlerë aktuale 5, pas ekzekutimit të instruksionit

count ← count + 1

vlera e re aktuale e variablit count do të jetë 6.

Zvogëlimi me 1 njësi i vlerës së një numëratori


Një nga veprimet, jo dhe aq të shpeshta në strukturat ciklike është zvogëlimi me një njësi e
vlerës aktuale të numëratorit të ciklit. Për shembull, instruksioni count ← count – 1,
nënkupton që vlera aktuale e variablit count pakësohet me 1 dhe rezultati e dërgohet përsëri
tek variabli count, duke zvogëluar vlerën e tij të mëparshme.

Për shembull, nëse variabli count ka si vlerë aktuale 5, pas ekzekutimit të instruksionit

count ← count – 1

vlera e re aktuale e variablit count do të ketë vlerën 4.

Akumulimi i vlerave
Gjatë llogaritjes së shumës së një vargu numrash veprimi bazë është përdorimi i një variabli
si akumulator (grumbullues) në të cilin vlerës aktuale i shtohet vlera e re. Për shembull,
instruksioni shuma ← shuma + vleraRradhës, nënkupton që vlerës aktuale të variablit shuma
i shton vlerën aktuale të variablit vleraRradhës, duke ndryshuar vlerën e tij të mëparshme.

Për shembull, nëse variabli shuma ka si vlerë aktuale 4 ndërsa variabli vleraRradhës ka si
vlerë aktuale 6 atëherë pas ekzekutimit të instruksionit

shuma ← shuma + vleraRradhës

vlera e re aktuale e variablit shuma do të jetë 10.

2.4 Instruksionet e hyrje/daljeve

Në lëndën e algoritmikës nuk do të trajtohet me detaje problemi i komunikimit të të dhënave


me algoritmin mbasi nuk komunikojmë me ndonjë kompjuter. Me komunikim të të dhënave
kuptojmë dy procese: hyrjen e të dhënave në algoritëm dhe daljen e të dhënave nga algoritmi
(input/output data).

2.4.1 Instruksioni i hyrjes së të dhënave


Instruksioni i hyrjes (input statements) mundëson që t’i jepet vlerë një variabli në mënyrë
bashkëbiseduese, gjatë ekzekutimit të algortimit.
2 Përshkrimi i algoritmeve | 23

Formati i përgjithshëm i instruksionit të hyrjes së të dhënave është

input (listë me variabla)

ku, lista me variabla është një listë me variabla të ndarë me presje.

Shembull: input (distanca, shpejtesia)

Ky instruksion kërkon nga përdoruesi dy vlera dhe i rezervon ata respektivisht në variablat
distanca dhe shpejtesia.

Në kuadrin e algoritmikës, do të supozohet gjithmonë se përdoruesi do të japë vlera të


pranueshme, d.m.th., që respektojnë kushtëzimin e përcaktuar nga tipi i variablit dhe
problemi. Ndaj të dhënave nuk do të zbatohen kontrolle të ndryshme të vlefshmërisë së tyre
mbasi ato janë detyra specifike të programimit.

2.4.2 Instruksioni i daljes së rezultateve


Instruksioni i daljes (output statements) nga algoritmi, kryhet nga instruksioni print. Ky
instruksion na jep mundësinë që të shohim vlerat variablave ose të një shprehjeje, (në fillim
llogaritjet shprehja dhe pastaj afishohet vlera).

Formati i përgjithshëm i instruksionit të daljes së të dhënave është

print (shprehje)

ku, shprehje është një varg me variabla, konstante, etj.

Shembull: print ("Koha e udhetimit: ", koha)

Pasi paraqitëm tre instruksionet bazë jemi në gjendje të shkruajmë një algoritëm të plotë.

Problemi.Të llogaritet koha e udhëtimit kur njohim distancën e përshkruar dhe shpejtësinë
mesatare të udhëtimit.

// Algoritmi 2.2 Llogarit kohën e udhëtimit


// Të dhëna: distanca, shpejtësia, respektivisht distanca e peeshkruar dhe shpejtësia
// Rezultati: koha, koha e udhëtimit
,
input (distanca); // lexon distancën e përshkruar
input (shpejtesia); // lexon shpejtësinë mesatare
koha ← distanca / shpejtesia; // llogarit kohën e udhëtimit
print ("Koha e udhetimit: ", koha); // afishon kohën e udhëtimit
}

Gjurmimi i algoritmit
Për të shkruar algoritme të efektshëm, një algoritmist e ka të nevojshme të zhvillojë aftësinë
për të gjurmuar ekzekutimin e një algoritmi në letër me të dhëna eksperimentale sikur të jetë
2 Përshkrimi i algoritmeve | 24

kompjuter. Për këtë ai simulon ekzekutimin e algoritmit dhe regjistron në letër vlerat e të
gjithë variablave që preken nga ekzekutimi i çdo instruksioni me rradhë.

Nëpërmjet këtij ekzekutimi ne nuk provojmë korrektësinë e algoritmit, por thjesht mund të
gjejmë gabimet e rënda. Ky ekzekutim mund të tregojë vetëm prezencën e gabimeve por jo
mungesën e tyre.

Një ekzekutim në letër bëhet duke krijuar një tabelë të quajtur tabela e gjurmës (trace table).
Në këtë tabelë, përdoruesi shton shtylla për çdo variabël apo për shprehje të cilat ai mendon
se janë të rëndësishme apo për kushtet e vendosura. Pas krijimit të tabelës përdoruesi
ekzekuton me mendje algoritmin hap pas hapi, duke mbushur tabelën e gjurmës me vlerat e
variablave ose të shprehjes nëse ato ndryshojnë. Duke kryer këtë proces mund të vëmë re
pozicionin e saktë në cilin gjërat shkojnë keq brenda algoritmit, variablat papritmas mbajnë
një vlerë që nuk pritet ose shprehjet nuk kanë gjendjen që duhet të kenë.

Le të gjurmojme algoritmin shkembimVlere të paraqitur më lart me shembullin: a = 3 dhe b =


5, (Tabela 2.6).

Tabela 2.6 Gjurmimi i algoritmit shkembimVlere


Vlera e variablave
Numri i rreshtit që ekzekutohet
a b temp
Para fillimit të ekzekutimit 3 5 ?
1 3 5 3
2 5 5 3
3 5 3 3

Vendosja e shenjës pikëpyetje (?) shërben për të treguar se për një variabël të caktuar nuk
mund të themi se çfarë vlerë ka në rast se ai nuk ka marrë një vlerë me anë të instruksionit të
vlerëdhënies ose hyrjes së të dhënave.

2.5 Instruksioni i kushtëzuar

Në jetën e përditshme, situatat ku jemi të detyruar të bëjmë një zgjedhje (të marrim një
vendim) janë të shumta. Për shembull: nëse bie shi dhe duhet të dalim nga shtëpia duhet të
marrim çadrën. Llogaritja e rrënjëve të një ekuacioni të shkallës së dytë varet nga shenja e
dallorit. Nëse dallori është pozitiv ka rrënjë reale dhe nëse dallori është negativ nuk ka rrënjë
reale.

Në probleme të tilla, kryhen veprime të ndryshme në varësi të vlerës të një kushti. Në


përgjithësi kushti është një shprehje krahasimi ose logjike, vlera e të cilës është e vërtetë ose
e gabuar (true ose false ).

Në algoritmikë, situatat ku veprimet janë të ndryshme në varësi të rezultatit të një kushti


drejtohen nga instruksioni i kushtëzuar (conditional statements). Dy trajtat bazë të
instruksionit të kushtëzuar janë: zgjedhja me 1-dalje dhe zgjedhja me 2-dalje. Në pseudokod
këto struktura quhen respektivisht if...then dhe if...then...else .

Instruksioni i kushtëzuar është një instruksion që drejton rradhën e kryerjes së veprimeve. Ai


nuk ndryshon vlera.
2 Përshkrimi i algoritmeve | 25

2.5.1 Instruksioni i kushtëzuar me 1-dalje.


Paraqitur me anë të pseudokodit, forma e përgjithshme e zgjedhjes me 1-dalje është

if (kusht) then {bllok};

Nëse kushti është i vërtetë atëherë ekzekutohet blloku i instruksioneve që ndodhen pas fjalës
then i përfshirë në kllapa gjarpëruese. Nëse ka vetëm një instruksion atëherë kllapat
gjarpëruese mund të mos vihen. Ilustrimi i instruksionit në figurën 2.1 (a).

(a) (b)

kushti kushti

bllok bllok1 bllok2

Figura 2.1 Diagrama e instruksionit (a) if...then dhe (b) if...then...else

Problem. Të hartohet një algoritëm që llogarit vlerën absolute të një numri.

Kuptimi matematik i vlerës absolute të një numri negativ është mohimi aritmetik i këtij
numri, përndryshe është vetë numri.

// Algoritmi 2.3 Llogarit vlerën absolute të një numri


// Të dhëna: x, vlerë numerike
// Rezultati: x, vlera absolute

vleraAbsolute () {
input (x); // jep numrin
if x < 0 then // kontrollon shenjën e numrit
x ← – x;
print (x); // afishon rezultatin
}

2.5.2 Instruksioni i kushtëzuar me 2-dalje


Paraqitur me anë të pseudokodit, forma e përgjithshme e zgjedhjes me 2-dalje është

if (kusht) then {bllok1} else {bllok2};

Në ekzekutimin e kushtit, nëse rezultati i vlerësimit është i vërtetë atëherë ekzekutohet bllok1
i instruksioneve. Nëse rezultati i vlerësimit është i gabuar atëherë ekzekutohet bllok2 i
instruksioneve. Ilustrimi i instruksionit në figurën 2.1 (b).

Problem. Jepen dy numra të ndryshëm. Të gjendet vlera më e madhe


2 Përshkrimi i algoritmeve | 26

// Algoritmi 2.4 Llogarit vlerën më të madhe të dy numrave


// Të dhëna: n1, n2 dy vlera numerike
// Rezultati; vlera më e madhe

maksimum() {
then max ← n1
else max ← n2;
print(max);
}

Në algoritmin e mësipërm rreshtat 1, 2 dhe 5 do të ekzekutohen gjithmonë. Rreshti 3


ekzekutohet vetëm nëse vlera e variablit n1 është më e madhe sesa vlera e variablit n2 ndërsa
dhe rreshti 4 do të ekzekutohet vetëm nëse vlera e variablit n1 është më e vogël sesa vlera e
variablit n2. Pra kushti në rreshtin 2 përcakton rrugëtimin e algoritmit për një të dhënë
fillestare të përcaktuar.

2.5.3 Instruksione të kushtëzuara të përfshira


Procesi i testimit të disa kushteve njëri pas tjetrit dhe përgjigja në mënyrë të përshtatshme
mund të përshkruhen me anë të një strukture që quhet instruksione të kushtëzuara të përfshirë
(nested if’s).

Problem. Të përcaktohet grupmosha e një individi kur njihet mosha e plotë e tij, mbështetur
në klasifikimin e mëposhtëm: “Fëmijë”, kur mosha është më e vogël 16 vjeç; “I ri” kur
mosha është nga 16 deri në 25 vjeç (përfshirë); “I rritur” kur mosha është nga 26 deri në 65
(përfshirë) vjeç dhe i “I moshuar” kur mosha është mbi 65 vjeç.

// Algoritmi 2.5 Llogarit kategoritë e moshës


// Të dhëna: mosha e plotë
// Rezultati: grupmosha

llogaritMosha() {
input(mosha);
if mosha > 65
then grupMosha ← “I moshuar”
else if mosha ≥ 26
then grupMosha ← “I rritur”
else if mosha ≥ 16
then grupMosha ← “I ri”
else grupMosha ← “Fëmijë”;
print(grupMosha);
}

2.6 Instruksioni ciklik

Instruksionet që kemi parë deri tani nuk mjaftojnë për të paraqitur zgjidhjen algoritmike të
problemeve, karakteristika e të cilëve është përsëritja e veprimeve. Ndër probleme të tilla
mund të përmendim: gjetjen e shumës së një vargu me numra; gjetjen e vlerës më të madhe,
2 Përshkrimi i algoritmeve | 27

kontrollin e ekzistencës së një fjale në një tekst të dhënë, renditjen e një vargu me numra
sipas vlerës rritëse apo zbritëse, kërkimi i një vlere në një varg me numra, etj.

Për zgjidhjen e problemeve me përsëritje të veprimeve të ngjashme është krijuar instruksioni


ciklik (loop statement). Instruksioni quhet ciklik mbasi logjikisht formohet një lak brenda të
cilit instruksionet ekzekutohen njëri pas tjetrit dhe pastaj veprimi kthehet në fillim të ciklit
dhe kjo ndodh përsëri dhe përsëri (sigurisht jo pafund).

Për shprehjen e përsëritjeve me anë të ciklit në pseudokod janë krijuar tre lloje të
instruksioneve ciklike, të quajtur cikli while...do, do...while dhe for .

Të gjitha ciklet kanë të njëjtën strukturë formale: (i) inicializim i variablave të përfshirë në
cikël; (ii) instruksionet që përsëriten (trupi i ciklit); (iii) kushti i vazhdimit të përsëritjes (ose
ndryshe kushti i ndalimit).

Instruksioni ciklik është një instruksion që drejton rradhën e kryerjes së veprimeve por nuk
ndryshon vlera.

2.6.1 Cikli while...do


Instruksioni ciklik while...do ka dy karakteristika kryesore: kontrolli i vazhdimësisë bëhet
në fillim të ciklit dhe numri i përsëritjeve mund të jetë i njohur ose i panjohur paraprakisht.

Forma e përgjithshme e instruksionit while...do është

while (kusht) do {trupi i ciklit};

Nga ana kuptimore, ky cikël, mundëson ekzekutimin e instruksioneve të ashtuquajturit trup i


ciklit që përfshihen ndërmjet dy kllapave gjarpëruese në varësi të vërtetësisë të kushtit të
vazhdimësisë, i cili vlerësohet para se të fillojnë të ekzekutohen instruksionet që përbëjnë atë.
Nëse kushti është i pavërtetë që në fillim atëherë trupi i ciklit nuk do të ekzekutohet. Ilustrimi
grafik i instruksionit në figurën 2.2.

Inicializimi i drejtuesit të ciklit

true false
kushti

Trupi i ciklit

Figura 2.2 Diagrama e instruksionit while...do


Cili është mekanizmi që realizon përsëritjen? Përsëritja realizohet nga drejtuesi i ciklit.
Drejtuesi i ciklit mund të jetë një variabël ose një shprehje. Gjatë zhvillimit të një
përsëritjeje, ai kalon në gjendjet e mëposhtme:

• Inicializimi i drejtuesit para fillimit të ciklit;


• Testimi i drejtuesit në kushtin e vazhdimit
2 Përshkrimi i algoritmeve | 28

• Përditësimi i drejtuesit në trupin e ciklit, zakonisht si instruksioni i fundit të ciklit.

a) Cikël me numërator

Thuhet se një cikël është i drejtuar nga numëratori (counter-controlled loops) atëherë kur
numri i përsëritjeve është i njohur para se të fillojnë përsëritjet. Përsëritja komandohet nga një
variabël. Cikli i drejtuar nga numëratori njihet edhe me emërtimin cikël i përcaktuar (define
loop).

Problem. Të hartohet një algoritëm për llogaritjen e shumës së n termave të parë të vargut
numerik an = n për n > 0.

// Algoritmi 2.6 Llogarit shumën e n numrave të parë natyrorë


// Të dhëna: n, një numër i plotë
// Rezultati: shuma e numra të plotë nga 1 deri tek n (përfshirë)

shumaNatyror() {
input (n) // leximi i numrit të termave
shuma ← 0; // inicializimi i akumulatorit
i ← 1; // inicializimi i numëratorit
while (i ≤ n) do{ // kontrolli i vazhdimësisë së ciklit
shuma ← shuma + i; // akumulimi i pagës së rradhës
i ← i + 1; // përditësimi i numëratorit
}
print(shuma) // afishimi i rezultatit
}

b) Cikël me sentinel

Në disa situata të caktuara numri i përsëritjeve të një problemi nuk dihet ose nuk ja vlen të
shpenzohet kohë për përcaktimin e tij. Ciklet që realizojnë situata të tilla quhen cikle të
papërcaktuar (undefined loop). Si probleme tipike ku mund të zbatohet një cikël i
papërcaktuar janë kontrolli i vlefshmërisë së të dhënave (data validation) gjatë futjes
interaktive e të dhënave nga tastiera. Për të drejtuar ciklin ne përdorim një variabël i cili
mban të dhënat reale por një vlerë e veçantë do të shërbejë për t'i dhënë fund përsëritjes. Kjo
vlerë e veçantë quhet sentinel (rojë) dhe nuk duhet të bën pjesë në bashkësinë e mundshme të
të dhënave.

Problem. Të verifikohet fjalëkalimi i një përdoruesi.

E dhëna e vetme që do të hyjë në algoritëm, nëpërmjet instruksionit input, rreshti 1, është:


fjalëkalimi i përdoruesit, siguria, e tipit tekst.

// Algoritmi 2.7 Verifikimi i fjalëkalimit


// Të dhëna: një string që identifikon një fjalëkalimi
// Rezultati: dalje nga algoritmi vetëm nëse fjalëkalimi është i saktë

verifikimFjaleKalimi() {
input (siguria); // inicializimi i drejtuesit të ciklit
2 Përshkrimi i algoritmeve | 29

while (siguria ≠ "nuk_e_gjen_dot") do { // kontrolli i vazhdimit të ciklit


print “Fjalëkalimi i gabuar! Rijepeni përsëri"
input (siguria); // përditësimi i drejtuesit
}
}

Algoritmi i mësipërm do të vazhdojë përsëritjen për aq kohë sa fjalëkalimi i dhënë nuk është i
njëjtë me atë regjistruar, "nuk_e_gjen_dot". Si sentinel shërben pikërisht konstantja string
"nuk_e_gjen_dot". Variabli drejtues i përsëritjeve siguria inicializohet në rreshtin e parë të
algoritmit, testohet në rreshtin e dytë dhe përditësohet në rreshtin e katërt.

Problem. Të llogaritet shuma e pikëve të fituara nga të gjithë atletët në një aktivitet sportiv
ku zhvillohen disa gara të ndryshme.

E dhëna që do të hyjë në algoritëm, nëpërmjet instruksionit input është pikeTeFituara nga


atletët, e tipit numerik i plotë pozitiv.

// Algoritmi 2.8 Llogarit shumën e disa vlerave


// Të dhëna: Një varg vlera të transmetuara në mënyrë interaktive
// Rezultati: shuma e vlerave

llogaritPike() {
input (pikeTeFituara); // inicializimi i drejtuesit të ciklit
shuma ← 0;
while (pikeTeFituara > 0) do { // kontrolli i vazhdimit të ciklit
shuma ← shuma + pike;
input (pikeTeFituara); // përditësimi i drejtuesit të ciklit
}
print (shuma)
}

Algoritmi i mësipërm drejtohet nga variabli pikeTeFituara. Përsa kohë që vlera e rradhës do të
jetë një vlerë normale, d.m.th. pozitive ajo do të shtohet në akumulatorin shuma. Si sentinel
shërben pikërisht konstantja 0. Me përfundimin e leximit të vlerave pozitive duhet të
komunikohet vlera 0. Në këtë rast kontrolli i vazhdimit të ciklit do të rezultojë false dhe
përsëritja do të ndërpritet dhe do të afishohet rezultati në rreshtin e fundit të algoritmit.
Variabli drejtues i përsëritjeve pikeTeFituara inicializohet në rreshtin e parë, testohet në
rreshtin e tretë dhe përditësohet në rreshtin e pestë.

c) Cikël me llogaritje

Disa probleme kanë nevojë për të kontrolluar përsëritjen nëpërmjet testimit të rezultatit të
dëshëruar. Kjo realizohet me anë ciklit me llogaritje. Cikli me llogaritje është një kombinim
midis ciklit me numërator dhe ciklit me sentinel. Dalja nga cikli varet nga një vlerë fillestare
e dhënë nga përdoruesi dhe nga llogaritjet e kryera mbi të.

Problem. Një klient dëshëron të investojë në formë depozite në bankë një sasi parash me një
normë vjetore interesi të njohur. Ai dëshëron të dijë se pas sa vitesh depozita e vendosur do të
kalojë një shumë të caktuar.
2 Përshkrimi i algoritmeve | 30

Të dhënat që do të hyjnë në algoritëm, nëpërmjet instruksionit input, janë: sasia e parasë që


do të investojë klienti, depozitaFillim, numër i plotë; norma e interesit në përqindje, interesi,
numër real; dhe shuma që dëshërohet të fitohet nga klienti, depozitaFund, numër është numër
real.

// Algoritmi 2.9 Llogaritja e viteve të investimit


// Të dhëna: depozita fillestar, numri i viteve dhe norma e interesit
// Rezultati: depozita në fund të periudhës së investimit

llogaritViteInvestimi() {
input(depozitaFillim, depozitaFund, interesi) // hyrja e të dhënave
viteDepozitim ← 0;
depozitaAktuale ← depozitaFillim; // inicializimi i drejtuesit të ciklit
while (depozitaAktuale < depozitaFund) do { // kontrolli i vazhdimësisë
viteDepozitim ← viteDepozitim + 1;
depozitaAktuale ← depozitaAktuale * (1 + interesi); // përditësimi i drejtuesit
}
print (viteDepozitim) // dalja e rezultatit
}

Në algoritmin e mësipërm variabli drejtues është depozitaAktuale. Ai inicializohet në rreshtin


e tretë, testohet për vazhdimin e llogaritjeve në rreshtin e katërt dhe përditësohet në rreshtin e
gjashtë të algoritmit. Cikli do të vazhdojë për aq kohë sa depozita aktuale nuk kalon shumën
e caktuar.

2.6.2 Cikli do...while


Cikli do...while është i ngjashëm me ciklin while...do përsa i përket funksionimit. I vetmi
ndryshim është se kontrolli i vazhdimësisë së përsëritjes bëhet në fund të ciklit.

Forma e përgjithshme e instruksionit do...while është:

do { trupi i ciklit } while kusht;

Nga ana kuptimore instruksioni ciklik do..while , mundëson ekzekutimin të paktën një herë
të trupit të ciklit para se të vlerësohet kushti i vazhdimit të përsëritjes. Nëse kushti i vazhdimit
të përsëritjes është i vërtetë vazhdon ekzekutimi i instruksioneve përndryshe realizohet dalja
nga cikli. Ilustrimi grafik i instruksionit në figurën 2.3.
2 Përshkrimi i algoritmeve | 31

Inicializimi i drejtuesit të

Trupi i ciklit

tru false
kusht

Figura 2.3 Diagrama e instruksionit do...while

Cikli do...while është mjaft i përshtatshëm në procesin e kontrollit të vlefshmërisë së të


dhënave gjatë kapjes interaktive të tyre, për shembull të fjalëkalimit (password).

Fragmenti i mëposhtëm ilustron një përdorim të ciklit do...while, i cili nuk lejon daljen nga
cikli nëse variabli siguria, që rezervon fjalëkalimin e nuk ka të marrë vlerën
"nuk_e_gjen_dot".

// Algoritmi 2.10 verifikon fjalëkalimin


// Të dhëna: një string që identifikon një fjalëkalimi
// Rezultati: dalje nga algoritmi vetëm nëse fjalëkalimi është i saktë

verifikim() {
do
print “Fjalëkalimi?”
input (siguria);
while (siguria ≠ "nuk_e_gjen_dot");
}

2.6.3 Cikli for


Cikli for është i ngjashëm me ciklin while...do në rastin kur njihet numri i përsëritjeve dhe
drejtohet nga një numërator. Vlerat e tij ndryshojnë nga një vlerë fillestare v1 në një vlerë
përfundimtare v2 duke u rritur/zvogëluar me një vlerë hap.

Forma e përgjithshme e ciklit for është:

for v ← v1 to v2 [step hap] do { trupi i ciklit} ;

Shënim. Shprehja në kllapa katrore, [step hap], nuk është e detyrueshme të përdoret nëse
variabli hap ka vlerën 1. Në përgjithësi në tekstet e algoritmikës dhe të gjuhëve të
programimit kllapat katrore tregojnë se shprehja ndërmjet kllapave është një shprehje jo e
detyrueshme (fakultativë).

I njëvlerëshmi i tij në formatin e ciklit while...do (kur hap > 0) do të ishte:


2 Përshkrimi i algoritmeve | 32

v ← v1;
while (v ≤ v2) do{
bllok;
v ← v + hap;
}

Shprehja në kllapa gjarpërueshe përbën trupin e ciklit. Trupi i ciklit mund të përbëhet nga një
instruksion i vetëm ose nga një varg instruksionesh që shprehin veprimet që duhet të
ekzekutohen. Ilustrimi grafik i instruksionit for në figurën 2.4

v ← v1

true false
v1≤ v2

Trupi i ciklit
(përfshin edhe
v ←v + hap)

Figura 2.4 Diagrama e instruksionit for

Le të shohim tani se si përdoret instruksioni ciklik for për të llogaritur shumën e n termave të
parë të vargut të numrave të plotë an = n për n ≥ 1.

Në algoritëm do të përdoren tre variabla i, n, shuma për të rezervuar respektivisht variablin


drejtues të ciklit, numrin e termave dhe shumën e tyre. Variabli akumulator shuma, në fillim
do të inicializohet me vlerën 0. Në çdo hap të ciklit shuma do të shtohet me vlerën e variablit
ciklik, që në realitet përfaqson termin e rradhës.

// Algoritmi 2.11 Llogarit shumën e termave të një vargu


// Të dhëna: Një numër i plotë, n
// Rezultati: Shuma e numrave nga 1 deri në n (përfshirë)

shumaVarg() {
input(n); // leximi i numrit të termave
shuma ← 0; // inicializimi i shumës
for i ← 1 to n do
shuma ← shuma + i; // shuma e pjesshme
print (shuma)
}
2 Përshkrimi i algoritmeve | 33

2.6.4 Instruksione ciklikë të përfshirë


Kohëmatja është një shembull i përkryer i përsëritjeve të përfshira. Sekondat qarkojnë
brenda minutës, të cilat nga ana e tyre qarkojnë brenda orës, etj.

Në algoritmikë, vendosja e një cikli në trupin e një cikli tjetër formojnë atë që quhet cikle të
përfshirë (nested loops). Kur kemi dy cikle të përfshirë, cikli i përfshirë në trupin e ciklit
tjetër quhet cikli i brendshëm ndërsa tjetri quhet cikli i jashtëm. Cikli i jashtëm ka kontrollin e
numrit të përsëritjeve të ciklit të brendshëm.

Në ciklet e përfshirë, në përsëritjen e parë të ciklit të jashtëm bën që të ekzekutohet i gjithë


cikli i brendshëm. Cikli i brendshëm përsëritet aq herë sa është specifikuar. Kur cikli i
brendshëm përfundon, atëherë cikli i jashtëm ekzekutohet për herë të dytë, duke mundësuar
përsëritjen e plotë të ciklit të brendshëm. Përsëritja e jashtme vazhdon për aq sa cikli i
jashtëm të ekzekutohet numrin e përcaktuar të herëve.

Ciklet e përfshirë përdoren gjërësisht në zgjidhjen e problemeve të ndryshme, sidomos në ato


raste kur të dhënat janë vendosur në tabela 1 dhe 2-dimensionale (vektor, matrica) si për
shembull për zgjidhjen e sistemeve të ekuacioneve linearë.

Problem. Të afishohen të gjitha orët dhe minutat e një dite. Të fillohet me “0 orë dhe 0
minuta”, të vazhdohet më tej: “0 orë dhe 1 minuta”, “0 orë dhe 2 minuta”, ..., “0 orë dhe 59
minuta”, “1 orë 0 dhe minuta”, “1 orë dhe 1 minuta”, .... dhe të përfundojë në afishimin e “23
orë e 59 minuta”. Shihet që cikli i parë do të shërbejë për formimin e orëve ndërsa cikli i dytë
për minutat. Variabli ora do të zmadhohet vetëm pasi të kalojnë 60 minuta.

Algoritmi 2.12 Afishon orën


// Të dhëna: nuk ka të dhëna nga jashtë. Janë integruar në algoritëm
// Rezultati: Afishimi i kohës çdo minutë

afishoOren () {
ora ← 0; // inicializimi i orëve
while ora < 24 do { // cikli i jashtëm
minuta ← 0; // inicializimi i minutave
while minuta < 60 do { // cikli i brendshëm
print (ora, "orë", " dhe ", minuta, "minuta"); // afishimi i rradhës
minuta ← minuta +1; // përditësimi i minutave
}
ore ← ore + 1; // përditësimi i orëve
}

Ciklet e përfshirë mund të jenë cikle të përbërë vetëm nga një tip cikli por mund të jenë edhe
cikle ku marrin pjesë cikle të tipeve të ndryshëm .

2.6.5 Ndërprerja e parakohëshme e ciklit


Cikli është krijuar për të vazhduar gjatë gjithë kohës që kushti i vazhdimit do të jetë i vërtetë.
Në raste të caktuara mund të duhet që të ndryshohet kjo cilësi. Për të ndërprerë ekzekutimin
e mëtejshëm të një cikli do të përdoret instruksioni break. Pavarësisht se cili është kushti i
përfundimit të ciklit, instruksioni break bën që ekzekutimi të kalojë në instruksionin
2 Përshkrimi i algoritmeve | 34

pasardhës të instruksionit ciklik që u ndërpre. Instruksioni break ndërpret vetëm ciklin në të


cilin është vendosur. Ai nuk ndërpret tërësisht një cikël të përfshirë.

Shembull.
for i ← 1 to 10 do {
if (i = 5)
then break;
print(i);
}

Si rezultat i përdorimit të instruksionit break ky fragment do të afishojë vetëm numrat 1,2,3,4.

2.6.6 Gabimet më të zakonshme në cikle


Gabimet më të zakonshme që lidhen me përdorimin e cikleve janë:

• Përdorimi i gabuar i krahasimit në kushtin e vazhdimit të ciklit while;


• Mospërditësimi i numëratorit të ciklit;
• Përfshirja në trupin e ciklit e instruksioneve që duhet të jenë jashtë ciklit;
• Lënia jashtë trupit të ciklit e instruksioneve që duhet të përfshihen;
• Mosinicializimi i numëratorit të ciklit.

Përdorimi i gabuar i krahasimit në kushtin e drejtimit të ciklit while


Supozojme se problemi kërkon që të ekzekutohet n herë trupi i një cikli. Në rast se do të
shkruajmë një fragment si më poshtë atëherë trupi i ciklit do të përsëritet n – 1 herë.

i ←0;
while i < n – 1 do {
// instruksione të tjera
i ← i + 1;
}

Në rast se shkruajmë një fragment si më poshtë atëherë trupi i ciklit do të përsëritet n + 1


herë.

i ←0;
while i ≤ n do {
// instruksione të tjera
i ← i + 1;
}

Ky lloj gabimi quhet gabimi-me-një (off-by-one error). Gjithashtu janë të mundshme


variacione të tjera por në përgjithësi, cikli ekzekutohet një herë më shumë ose një herë më
pak. Ky lloj gabimi mund të shkaktohet nga vendosja e gabuar e vlerës fillestare në drejtuesin
e ciklit ose nga mos shkrimi korrekt i kushtit të vazhdimit të ciklit.

Mospërditësimi i numëratorit të ciklit


Harresa e përditësimit të drejtuesit të ciklit në ciklet while…do ose do…while shpie në atë
që quhet cikël i pafundëm (infinite loop). Cikli i pafundëm ndodh në ato raste kur nuk bëhet
përparim në drejtim të përfundimit të ciklit. Ka shumë raste kur mbrihet në cikël të
2 Përshkrimi i algoritmeve | 35

pafundëm. Shembulli i mëposhtëm ilustron problemin në të cilin përpiqemi të afishojmë


shumën e të gjithë numrave nga 5 deri tek 10.

i ← 5;
shuma ← 0;
while i ≤ 10 do {
shuma ← shuma + i;
}

Në fragmentin e mësipërm do të përfundohet në një cikël pafund për shkak se nuk është
përditësuar numëratori i ciklit brenda trupit të ciklit. Numëratori qëndron i njëjtë në çdo
përsëritje të ciklit dhe nuk bëhet përparim drejt përfundimit të ciklit. Kodi korrekt vijon më
poshtë:

i ← 5;
shuma ← 0;
while i ≤ 10 do {
shuma ← shuma + i;
i ← i + 1;
}

Përfshirja në trupin e ciklit e instruksioneve që duhet të jenë jashtë ciklit


Gabimet e përfshirjes të instruksioneve që nuk duhen dhe mos përfshirja e instruksioneve që
duhen ndodh më shpesh nga një gabim i përdorimit të kllapave gjarpëruese. Mirëpo, kur ka
më tepër se një instruksion në trupin e ciklit, ne duhet t’i përfshijmë ato në kllapa gjarpëruese.
Në të kundërt, vetëm instruksioni i parë do të ekzekutohet. Kllapat gjarpëruese mund të mos
përdoren kur trupi i ciklit përbëhet nga një instruksion i vetëm.

2.6.7 Krahasimi i cikleve


Cikli while është më i përgjithshmi. Me anë të atij mund të realizohen të gjitha situatat
përsëritëse. Ai mund ta zëvendësojë shumë mirë ciklin for. Por nga pikpamja praktike cikli
for për shumë probleme krijon një kod më të qartë dhe më të ngjeshur sesa cikli while. Po
ashtu mund t’i shmangemi përdorimit të ciklit do…while por ka raste kur ai është më i
logjikshëm dhe lehtëson punën dhe kodin.

Në tabelën 2.7 paraqitet një pamje e tre cikleve përsa i përket tipit të kontrollit dhe
vendndodhjes së kushtit.

Tabela 2.7 Llojet e cikleve dhe tipi i kontrollit


Lloji i ciklit Tipi i kontrollit të ciklit Vendndodhja e kushtit
While...do numërator sentinel në fillim
do...while numërator sentinel në fund
for numërator - në fillim

2.7 Funksionet

Një nga strategjitë më të thjeshta të zgjidhjes së një problemi është shpërbërja e problemit në
nënprobleme dhe përpjekja për t'i zgjidhur ata në mënyrë të pavarur. Kështu algoritmi që i
korrespondon të gjithë problemit do të përbëhet nga disa nënalgoritme secili prej të cilëve i
2 Përshkrimi i algoritmeve | 36

përket një nënproblemi. Ndonjëherë nënproblemet janë të ngjashme kështu që mund të


zgjidhen duke përdorur të njëjtën metodë ndoshta të zbatuar me të dhëna të ndryshme. Kështu
që ne mund të konsiderojmë se tek nënalgoritmet janë zbatuar disa të dhëna të përgjithshme
(gjenerike) të cilat do të zëvendësohen nga vlerat aktuale vetëm atëherë kur nënalgoritmi do
të thirret dhe ekzekutohet. Këto të dhëna të përgjithshme zakonisht quhen parametra formalë.
Një nënalgoritëm ose do të kthejë si rezultat një vlerë ose do të modifikojë disa nga variablat
e algoritmit. Ne pseudokod nënalgoritmet do t'i quajmë funksione.
𝑛!
Le të konsiderojmë problemin e llogaritjes së koeficentit binomial �𝑛𝑘� = për një n
𝑘!(𝑛−𝑘)!
dhe k të caktuar. Për të llogaritur këtë vlerë me anë të formulës së mësipërme është e
nevojshme që të kryhet tri herë llogaritja e funksionit faktorial, respektivisht për vlerët n, k
dhe n – k. Për të shmangur përsëritjen e kodit për secilin faktorial, hartohet një funksion me
një parametër formal në hyrje (pa e lidhur me ndonjë vlerë konkrete) për llogaritjen e
faktorialit dhe më pas ky funksion thirret (aktivizohet, përdoret, ekzekutohet) në të
ashtuquajturin algoritmin kryesor tri herë, me vlerat e caktura të n, k dhe n – k.

Koncepti i funksionit është një nga konceptet më të rëndësishme në ndërtimin e programeve.


Funksionet zbatohen me anë të mekanizmave të ndryshëm në gjuhët e ndryshme të
programimit. Pavarësisht mënyrës së zbatimit, koncepti i funksionit është i njëjti: funksioni
është një grup instruksionesh (ndoshta edhe një i vetëm) i aftë për të shkëmbyer të dhëna me
funksionet e tjerë nëpërmjet të ashtuquajturve parametra formalë.

Të gjitha gjuhët e programimit përdorin konceptin e funksionit por e emërtojnë me emra të


ndryshëm si për shembull, metodë, procedurë, sub, subroutine, etj.

Në kuptimin formal funksioni në një program nuk është një njësi drejtpërsëdrejti e
ekzekutueshme. Ai mund të ekzekutohet vetëm nëpërmjet programit kryesor (main program).
Rradha e vendosjes së funksioneve (kur ka më tepër se një funksion) në programin kryesor
përcaktohet nga logjika e problemit. Ndërkaq një funksion mund të thërrasë një ose disa
funksione të tjerë. Gjithashtu një funksion mund të thërrasë vetveten. Në këtë rast kemi të
bëjmë me atë që quhet rekursion (recursion).

Pse është i dobishëm ndërtimi i programeve si asamblim i funksioneve të pavaruara? Hartimi


i programit në trajtë funksionesh është i dobishëm sepse:

• Një problem i ndërlikuar zgjidhet më mirë nëse ndahet në nënprobleme më të vogla, të


cilat sigurisht që zgjidhen me lehtë;
• Kontrolli është më i plotë dhe me rigoroz mbasi çdo pjesë mund të testohet në mënyrë të
pavarur;
• Funksionet e testuara mund të përdoren në probleme të tjera;
• Mundësohet ndarja (share) e të dhënave ndërmjet funksioneve të ndryshëm;

Nga mënyra e funksionimit, funksionet ndahen në dy tipe: në tipin e parë futen funksionet që
kthejnë vlerë. Vlera i kthehet funksionit që e thirri me anë të instruksionit return . Ndërsa në
tipin e dytë futen funksionet që nuk kthejnë vlerë me anë të instruksionit return . Të parët
zakonisht përdoren për llogaritjen e ndonjë vlere ndërsa të dytët për ekzekutimin e logjikës së
përpunimit.
2 Përshkrimi i algoritmeve | 37

2.7.1 Funksione që kthejnë vlerë

Koncepti i funksionit të tipit të parë në algoritmikë është i ngjashëm me konceptin e


funksionit në matematikë. Cilësitë e një funksioni janë:

• Parametrat formale të funksionit shërbejnë vetëm për hyrje të të dhënave;


• Numri i parametrave formalë është i pandryshuar për një funksion të caktuar (≥ 0)
• Një funksion ka vetëm një tip, që është tipi i vlerës që kthen;
• Gjatë thirrjes për ekzekutim, si parametra faktikë mund të përdoren variabla, konstante
por edhe rezultatet e ndonjë funksioni tjetër;
• Vlera e kthyer specifikohet nga instruksioni return i vendosur në një vend të
përshtatshëm që përcaktohet nga llogjika e përpunimit;
• Instruksioni return përveç se kthen vlerën njëkohësisht përfundon edhe ekzekutimin e
mëtejshëm të funksionit duke e kaluar ekzekutimin në funksionin që e ka thirrur;
• Në varësi të logjikës së përpunimit në një funksion mund të vendosen një ose disa
instruksione return .

Në përgjithësi emri i një funksioni është ose një emër (për shembull, minimum) ose një pyetje
për shembull, (ështëBosh).

Struktura e përgjithshme e një moduli të tipit funksion është si më poshtë:

//Struktura e funksionit që kthen një vlerë


emriFunksioni ([parametra formalë për hyrje]) {
// komente të mundshme
instruksion

instruksion
return vlera_qe_kthen
}

Thirrja (ekzekutimi) e funksionit (në funksionin kryesor apo në ndonjë funksion tjeter) bëhet
duke shkruar emrin e tij, pasuar nga lista e parametrave efektive, nëse ka të tillë, të përfshirë
në kllapa. Kllapat do të përdoren edhe nëse lista e parametrave formalë është boshe.

Problem. Të hartohet një algoritëm i tipit funksion që kthen vlerën më të vogël të dy


numrave.

// Algoritmi 2.13 Funksion që kthen vlerën më të vogël ndërmjet të dy vlerave


// Të dhëna: a, b, dy numra të ndryshëm
// Rezultate: kthen vlerën më të vogël

minimum1(a, b) {
if a > b
then return b
else return a
}
2 Përshkrimi i algoritmeve | 38

Në funksionin minimum1, variablat a dhe b shërbejnë si parametra formalë për hyrje të të


dhënave dhe rezultati sapo llogaritet i kthehet me anë të instruksionit return, funksionit që e
ka thirrur.

Ekzekutimi i modulit për një rast të caktuar në funksionin kryesor mund të kryhet si më
poshtë.

// Algoritmi 2.14 Algoritmi kryesor për llogaritjen e vlerës më të vogël të dy numrave

main() {
input (a, b); // marrja e të dhënave fillestare
print (minimum1(a, b)); // thirrja e funksionit
return // mbyllja e algoritmit kryesor
}

2.7.2 Funksione që nuk kthejnë vlerë


Funksionet që nuk kthejnë vlerë janë funksione që pranojnë parametra formale për hyrje të
dhënash si dhe për modifikim variablash. Ata nuk përdorin instruksionin return për të kthyer
rezultatin. Instruksioni return do të përdoret vetëm për t’u kthyer në funksionin që e ka
thirrur.

Cilësitë e një funksioni të tillë janë:

• Numri i parametrave formalë është i pandryshuar (≥ 0).


• Parametrat formalë mund të shërbejnë ose vetëm për hyrje të dhënash ose vetëm për
modifikim të parametrave aktuale ose njëkohësisht për hyrje të dhënash dhe modifikim të
variablave.
• Gjatë thirrjes për ekzekutim, si parametra faktikë mund të përdoren variablat, konstantet
por edhe rezultatet e ndonjë funksioni tjetër.

Në përgjithësi, për emërtimin e një funksioni, zgjidhet një folje që ka të bëjë me thelbin e
problemit që zgjidh (për shembull, rendit).

Struktura e përgjithshme e një funksioni të tipit dytë është si më poshtë:

// Struktura e funksionit që nuk kthen vlerë


emri_funksionit ([parametra formale për hyrje, për dalje, për hyrje-dhe-dalje]) {
// komente të mundshme
instruksion;
...
instruksion;
return
}

Funksionet që nuk kthejnë vlerë nuk mund të përdoret në shprehjet e ndryshme llogaritëse
ashtu siç mund të përdoren funksionet që kthejnë vlerë.

Problem. Të shkruhet një funksion që nuk kthen vlerë për të gjetur vlerën më të vogël të dy
numrave të ndryshëm.
2 Përshkrimi i algoritmeve | 39

// Algoritmi 2.15 Funksion që nuk kthen vlerë për gjetjen e vlerës më të vogël të dy
// numrave
// Gjen më të voglin e dy numrave të dhënë të, a dhe b
// Të dhëna: a, b dy numra
// Rezultate: min, vlera më e vogël

minimum2(a, b, min) {
if a > b
then min ← b
else min ← a
return
}

Një funksion thirret për ekzekutim në funksionin kryesor duke treguar emrin, pasuar nga lista
e parametrave faktike, nëse ka të tillë, të përfshirë në kllapa.

// Algoritmi 2.16 Algoritmi kryesor për llogaritjen e vlerës më të vogël


// Të dhëna: a, b dy numra të ndryshëm
// Rezultate: vlera më e vogël

main() {
input (a, b) // marrja e të dhënave fillestare
minimum2(a, b, min) // thirrja e funksionit
print (min) // afishimi i rezultatit
return
}

2.7.3 Cilin tip funksioni të përdorim?


Studentët pyesin shpesh se cilin tip funksioni të përdorim për zgjidhjen e një problemi.
Përgjigja është: varet nga logjika e problemit.

2.7.4 Pikëdalja nga funksioni


Pikëdalja nga funksioni realizohet nga instruksioni return. Në programimin e strukturuar
ekziston një debat përsa i përket mënyrës së daljes nga ekzekutimi: të ketë një pikëdalje të
vetme apo disa.

Grupi që mbron rregullin e një pikëdaljeje të vetme, i quajtur grupi i “të pastërve”, mbron
rregullin e diskutueshëm, se çdo algoritëm duhet të ketë vetëm një instruksion return në
fund të tij. Arsyetimi i tyre mbështetet në faktin se gjurmimi i ekzekutimit është më i lehtë
nëse njohim me saktësi se ku përfundon logjikisht algoritmi.

Për grupin kundërshtar, megjithëse një pikëdalje e vetme është e këndëshme, kodi i algorimit
bëhet shumë i vështirë përshkak të vendosjes dhe testimit të variablave logjikë shtesë, të cilët
janë të nevojshëm për të zbatuar stilin e pikëdaljes së vetme.
2 Përshkrimi i algoritmeve | 40

Pjesa më e madhe e programistëve mbështesin idenë se një kod i thjeshtë e i lexueshëm është
më i rëndësishëm sesa ndjekja e çdo rregulli arbitrar. Nëse zbatimi i rregullit të pikëdaljes të
vetme ndërlikon kodin atëherë është një zgjedhje e “gabuar”.

Në funksionin minimum2 llogaritet vlera më e vogël dhe vendoset në variablin min. Më pas
kjo vlerë, printohet në funksionin kyesor.

Në algoritmin minimum1 kishim dy pikëdalje ndërsa në algoritmin minimum2 kemi një


pikëdalje të vetme.

2.7.5 Disa funksione të gatshme

Në këtë cikël leksionesh do të përdoren disa funksione të gatshme pa patur nevojë që të


hartojmë algoritmin përkatës, nëse nuk specifikohet ndryshe.

• Funksioni mod. Funksioni mod llogarit mbetjen e pjesëtimit të dy numrave të plotë, e


quajtur ndryshe modul. Formati i përdorimit mod (m, n) ose m mod n ose m % n, ku m
dhe n dy variabla numerikë të plotë. Rezultati është negativ vetëm nëse m është negative.
Për shembull, nëse shkruajmë x ← mod(11,4) atëherë variabli x do të marrë vlerën 3.

• Funksioni div. Funksioni div llogarit pjesën e plotë të herësit të pjesëtimit të dy numrave
të plotë. Quhet dhe pjesëtimi i plotë. Formati i përdorimit div (m, n) ose m div n, ku m dhe
n janë dy variabla numerikë të plotë. Për shembull, nëse shkruajmë k ← div (11, 4) atëherë
variabli k do të marrë vlerën 2.

• Funksioni ceiling. Funksioni ceiling 3, llogarit më të voglin nga numrat e plotë që është më
i madh se një numër i dhënë (e thënë ndryshe llogarit tavanin e një numri). Formati i
përdorimit ceiling (x). Për shembull, nëse shkruajmë m ← ceiling (4.3) atëherë variabli m do
të marrë vlerën 5. Shpesh për të shprehur këtë veprim mund të përdoret edhe çifti i
simboleve grafikë ⌈ dhe ⌉ në formën x ← ⌈4.3⌉.

• Funksioni floor. Funksioni floor llogarit më të madhin nga numrat e plotë që është më i
vogël se një numër i dhënë (e thënë ndryshe llogarit dyshemenë e një numri real). Formati
i përdorimit floor (x). Për shembull nëse shkruajmë m ← floor (4.3) atëherë variabli m do të
marrë vlerën 4. Shpesh për të shprehur këtë veprim mund të përdoret edhe çifti i
simboleve grafikë ⌊dhe ⌋ në formën x ← ⌊4.3⌋.

• Funksioni log. Funksioni log llogarit logaritmin e një numri pozitiv sipas përkufizimit log x
b = a. Në përgjithësi në algoritmike, një shkrim i logaritmit pa cituar bazën e logaritmit
nënkupton bazën 2, domethënë, log b nënkupton log 2 b. Formati i përdorimit log (n). Për
shembull, nëse shkruajme m ← log(4) atëherë variabli m do të marrë vlerën 2.

• Funksioni sqr. Funksioni sqr llogarit rrënjën katrore të një numri. Formati i përdorimit sqr
(x). Për shembull, nëse shkruajmë y ← sqr (4) atëherë variabli y do të marrë vlerën 2.
Mundet që për të shprehur këtë veprim të përdoret edhe simboli grafik √.

3
Konceptet floor and ceiling janë futur nga Kenneth E. Iverson në vitin 1962.
2 Përshkrimi i algoritmeve | 41

• Funksioni random. Funksioni random prodhon një numër real të pseudorastësishëm në


intervalin [0, m) ku m një numër i plotë pozitiv. Format i përdorimit random[0, m). Për
shembull, nëse shkruajmë y ← random[0,3) gjenerohet një numër real në gjysmë intervalin
[0, 3), 2.23614901. Në një program numri i shifrave pas pikës dhjetore varet nga tipi i
caktuar i të dhënës numerike.

2.7.6 Përmbledhje

• Në këtë cikël leksionesh algoritmet do të paraqiten me anë të pseudokodit. Paraqitja e


pseudokodit do të bëhet me fjalë të gjuhës angleze për të qenë sa më e afërt me mënyrën e
paraqitjes me anë të gjuhëve të programimit.
• Në një algoritëm, është e mundur, që një bashkësi instruksionesh (veprimesh) bazë, të
mund të kombinohen në vetëm tri struktura: e njëpasnjëshme (sequence), zgjedhëse
(selection) dhe përsëritëse (repetition).
• Struktura zgjedhëse është një strukturë algoritmike kontrolli, që ka për qëllim të drejtojë
ekzekutimin e një vargu me instruksione në varësi të vlerës së një kushti. Instruksionet e
kësaj strukture shfaqen në dy formate: zgjedhja me 1-dalje, if...then dhe zgjedhja me 2-
dalje, if...then...else.
• Struktura përsëritëse është një strukturë algoritmike kontrolli, që ka për qëllim të drejtojë
ekzekutimin e një vargu me instruksione, një numër të caktuar herësh, numër që edhe
mund të mos njihet paraprakisht. Instruksionet e kësaj strukture shfaqen në tre formate:
while...do, do...while, for.
• Funksioni është një grup instruksionesh (ndoshta edhe një i vetëm) i aftë për të
shkëmbyer të dhëna me funksionet e tjerë nëpërmjet të ashtuquajturve parametra formalë.
Funksionet ndahen në funksione që kthejnë vlerë dhe funksione që nuk kthejnë vlerë.
• Funksionet që kthejnë vlerë pranojnë parametra formale vetëm për hyrje të të dhënave
dhe kthejnë vetëm një rezultat me anë të instruksionit return.
• Funksionet që nuk kthejnë vlerë pranojnë parametra formale për hyrjen e të dhënave dhe
për modifikimin e parametrave dhe nuk kthejnë rezultat me anë të instruksionit return.
2 Përshkrimi i algoritmeve | 42

2.8 Ushtrime për kapitullin 2

1. Të shkruhet një algoritëm për llogaritjen e sipërfaqes së një katërkëndëshi kënddrejtë kur
njihen brinjët e tij, a dhe b.

2. Të shkruhet një algoritëm për të llogaritur rrënjën e ekuacionit ax + b = 0, kur njihen


koeficientët a dhe b.

3. Të shkruhet një algoritëm për të llogaritur rrënjët reale të ekuacionit ax2 + bx + c = 0, ku


koeficientët a, b dhe c janë të çfardoshëm. Supozohet se njihet funksioni për llogaritjen e
rrënjës katrore.

4. Jepet një numër i plotë. Të verifikohet nëse numri është çift apo tek?

5. Jepen si të dhëna tre vlera të çfardoshme. Të gjendet vlera më e madhe.

6. Jepen si të dhëna tre vlera të çfardoshme. Të renditen tre vlerat në rendin rritës.

7. Jepen si të dhëna fillestare dy numra si dhe një veprim aritmetik. Të gjendet rezultati
pasi të zbatohet veprimi aritmetik ndaj të dhënave.

8. Mbi çmimin e një produkti zbatohet një zbritje prej p1 përqind nëse ai kushton më pak se
S lekë dhe p2 përqind nëse kushton jo më pak se S lekë. Të futet nga tastiera çmimi C
dhe të llogaritet se sa do të paguhet sipas rregullit të përshkruar më sipër.

9. Një punonjës, që udhëton shpesh me automobilin e tij për punë, mund të zgjedhë një
nga mënyrat e mëposhtme të pagesës së karburantit nga ana e firmës:
a. Mënyra e parë. A lekë për kilometër udhëtim.
b. Mënyra e dytë. Një vlerë e pandryshueshme prej L lekë plus pagesën e naftës kur
dihet se automobili tij harxhon H litra naftë për 100 km dhe se nafta kushton C lekë
për litër. Duke njohur sasinë e kilometrave të përshkruara zgjidhni se cila pagesë
është më e përshtatshme për punonjësin.

10. A prodhojnë tre fragmentet e mëposhtëm të njëjtin rezultat kur r është një numër i plotë
pozitiv?

prod ← 1; prod ← 1; prod ← 1;


i ← 0; i ← 0; i ← 0;
while i ≤ 5 do { while i ≤ 5 do { while i < 6 do {
prod ← prod * r ; i ← i + 1; i ← i + 1;
i ← i + 1; prod ← prod * r ; prod ← prod * r ;
} } }
print (prod) print (prod) print (prod)

11. Çfarë prodhon secili nga fragmentet e mëposhtme algoritmike?

i ← 0; i ← 0; while i ≤ 20 do
while i < 20 do while i ≤ 20 do i ← i + 5;
i ← i + 5; i ← i + 5; print (i);
print (i); print (i);
2 Përshkrimi i algoritmeve | 43

12. Çfarë kthen fragmenti vijues algoritmik kur jepet m, numër i plotë pozitiv?

n ← m;
t ← 0;
while t < 10 do {
t ← t + 1;
n ← n + 1;}
return n

13. Janë dhënë fragmentet algoritmike të mëposhtme. Çfarë mund të thoni për ta kur n është
një numër i plotë pozitiv?

a. while n > 0 do
n←2*n

b. while n ≥ 0 do{
m ← 1 / n;
n ← n – 1;
}

c. shuma ← 0;
while i < 10 do
shuma ← shuma + i;

14. Pedagogu ka marrë si detyrë të vlerësojë provimet e një grupi studentësh. Provimi
përbëhet nga 10 pyetje. Për çdo pyetje, përgjigja e saktë vlerësohet me 1 pikë, nëse
përgjigja e gabuar vlerësohet me -1 pikë dhe mospergjigjja vlerësohet me 0 pikë. Të
përshkruhet puna e pedagogut me saktësinë që kërkon një algoritëm për vlerësimin e çdo
studenti.

15. Nga tastiera lexohet një fjali në formën karakter pas karakteri. Përfundimi i fjalisë
përcaktohet nga karakteri ∙ (pikë). Të hartohet një algoritëm që llogarit numrin e
zanoreve.

16. Hartoni një algoritëm për të mbedhur dy numra të plotë duke përdorur vetëm zmadhim
dhe zvogëlim me një njësi.

17. Të hartohet një algoritëm për të llogaritur shumën e 10 numrave pasues të një numri
të plotë të dhënë n.

18. Të hartohet një algoritëm për të llogaritur shumën e disa numrave që paraprijnë një
numër të plotë të dhënë n, deri sa shuma e tyre të mos e kalojë numrin e dhënë.

19. Të hartohet një algoritëm që llogarit sasinë e numrave pozitivë dhe sasinë e numrave
negativë në një varg numrash që lexohen nga tastiera kur nuk dihet paraprakisht sasia
e tyre.

20. Të llogaritet tabela e shumëzimit për dy numra të plotë të dhënë pozitivë m dhe n.
2 Përshkrimi i algoritmeve | 44

21. Të afishohen të gjitha orët, minutat dhe sekondat e mundshme të një dite.

22. Të përshkruhet një algoritëm që përkëmben vlerat e variablave x dhe y duke përdorur
vetëm veprimin e mbledhjes dhe të zbritjes. Cili është numri minimal i këtyre veprimeve
aritmetike për të zgjidhur këtë problem.

23. Më poshtë paraqitet një sekuencë që shkëmben vlerat e dy elemente duke përdorur një
variabël të përkohshëm. Instruksioni i parë mungon. Cili duhet të jetë ai?

?
xi ← xj;
xj ← temp;

24. Shkruani një algoritëm që inicializon me vlerën 0 një tabelë me n elemente.

25. Shkruni një algoritëm që realizon hyrjen në mënyrë interaktive të n vlerave në një tabelë.

26. Shkruani një algoritëm që afishon vlerat e një tabele me n elemente.

27. Le të supozojmë se është hartuar algoritmi i mëposhtëm. Çfarë kthen ai?

// Algoritmi: Gjeni se çfarë llogarit


// Të dhëna: Një tabelë T[1:n] me n numra të plotë
// Rezultati: ?

mister(n, T) {
for i ← 1 to div(n, 2) do {
if (T[i] ≠ T[n+1 – i])
then return false;
}
return true
}

28. Le të supozojmë se është hartuar algoritmi i mëposhtëm. Çfarë kthen ai?

// Algoritmi gjeni se çfarë llogarit


// Të dhëna: Një tabele T[1:n] me n numra të plotë dhe z një numër i plotë
// Rezultati: ?
mister (n, T, z) {
j ← T[1];
for i ← 2 to n do
if ((z = 0) and (j > T[i]))
then j ← T[i]
else if ((z ≠ 0) and (j < T[i]))
then j ← T[i];
return j
}
3 Probleme me algoritme iterativë | 45

3 Probleme me algoritme iterativë

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të njohë qasjen iterative për hartimin e algoritmeve;


• të përdorë ciklet për zgjidhjen e problemeve numerike;
• të përdorë ciklet për llogaritjen e vlerave të disa funksioneve;
• të përdorë ciklet për zgjidhjen e problemeve me të dhëna të organizuara në tabela një
përmasore;
• të përdorë ciklet për zgjidhjen e problemeve me të dhëna të organizuara në tabela dy
përmasore;
• të përdorë ciklet për zgjidhjen e problemeve me të dhëna të organizuara në tabela
korresponduese;

Kapitullin e dytë kemi thënë që struktura algoritmike përsëritëse mund të realizohet me një
qasje iterative ose rekursive. Qasja iterative mbështet në idenë që zgjidhja e një problemi
mund të përftohet duke përsëritur në mënyrë të vazhdueshme të njëjtin varg me instruksione
(sigurisht mbi variabla që dora-dorës modifikohen) gjatë gjithë kohës që kushti i
vazhdimësisë vazhdon të jetë i vërtetë. Instruksionet që realizojnë iteracionin janë
while...do , do...while dhe for . Më poshtë trajtohet zgjidhja e disa problemeve me anë të
algoritmeve iterativë.

3.1 Përdorimi i cikleve për llogaritje numerike

Problemet që do të trajtohen janë llogaritja e faktorialit, funksionit fuqi, funksionit


eksponencial, rrenjes katrore dhe e numrit pi-grek.

Llogaritja e faktorialit
Paraqitjen e algoritmeve me strukturë iterative, do ta fillojmë me një problem të thjeshtë
numerik: gjetjen e prodhimit të n numrave të parë natyrorë. Në matematikë, faktorial i një
numri të plotë natyror, shënuar me simbolin n! dhe që lexohet faktoriali i n-së ose n-faktorial,
quhet produkti i numrave të plotë rigorozisht pozitivë, më të vegjël ose të barabartë me n.
Thuhet se shënimi n! është shpikur nga Christian Kramp në vitin 1808. Faktoriali luan një rol
të rëndësishëm në kombinatorikë, mbasi n! është numri i permutacioneve të n objekteve të
ndryshëm. Ai shfaqet në shumë formula matematike si tek binomi i Njutonit, tek shpërthimi
në seri të Tejlorit, etj.

Në trajtë matematike përkufizimi i faktorialit jepet nga shprehja:

n! = ∏𝑛𝑖=1 𝑖 = 1∙2∙3∙∙∙(n – 1) ∙ n nëse n ≥ 1 (3.1)

dhe me marrëveshje, për shumë qëllime praktike, pranohet që 0! = 1.

Duke u mbështetur në formulën (3.1) algoritmi i mëposhtëm llogarit faktorialin e një numri n
në mënyrë iterative.

// Algoritmi 3.1 Llogaritja e faktorialit në mënyrë iterative


// Të dhëna: n ≥ 0, numër i plotë
3 Probleme me algoritme iterativë | 46

// Rezultati: fakt, faktoriali i n-së

faktIterativ (n) {
fakt ← 1; // inicializimi i rezultatit
for i ← 1 to n do
fakt ← i * fakt; // llogaritja e faktorialit të i-së
return fakt;
}

Nëse n = 0 atëherë cikli nuk ekzekutohet asnjëherë dhe si rezultat kthehet vlera 1.

Llogaritja e funksionit fuqi


Le të shohim tani se si do të përdoret instruksioni ciklik for për të llogaritur fuqinë me
eksponent jonegativ të një numri të caktuar, an. Në algoritëm do të përdoren tre variabla a, n,
fuqia për të caktuar respektivisht vlerën për të cilën do të llogaritet fuqia, eksponentin e fuqisë
dhe vlerën e fuqisë. Variabli fuqia në fillim do të inicializohet me vlerën 1. Në çdo hap të
ciklit variabli fuqia do të shumëzohet me variablin a dhe rezultati do të dërgohet tek variabli
fuqia.

// Algoritmi 3.2 Llogaritja e fuqisë së një numri


// Të dhëna: a, një numër real dhe n ≥ 0, eksponenti, numër i plotë
// Rezultati: fuqia

llogaritjaFuqise(x, n) {
fuqia ← 1.0; // inicializimi i fuqisë
for i ← 1 to n do
fuqia ← fuqia * a; // llogaritja e a i
return fuqia
}

Llogaritja e funksionit eksponencial


Shumë funksione trashendentë si funksioni eksponencial, funksionet trigonometrike, etj,
mund të llogariten me saktësi mjaft të mirë duke i përafruar me anë të polinomeve. Le të
shohim llogaritjen e funksionit eksponencial y = ex për një vlerë të caktuar reale x. Një nga
formulat e përafërta por jo një nga më të efektshmet, për përafrimin i tij me anë të polinomit,

𝑥 𝑥2 𝑥𝑛
𝑒𝑥 ≈ 1 + + + ⋯+
1! 2! 𝑛!

Sa më e lartë të jetë shkalla e polinomit aq më e sakte do të jetë vlera e llogaritur për numrin
e Neperit e. Pa hyrë në detaje të analizës numerike, degë e matematikës që trajton saktësinë e
përafrimit, japim algoritmin e mëposhtëm.

// Algoritmi 3.3 Llogarit vlerën e funksionit eksponencial për një vlerë të


// caktuar
// Të dhëna: x, numri dhe n ≥ 0, n i plotë pozitiv
// Rezultati: exp
3 Probleme me algoritme iterativë | 47

ekspo(x, n) {
exp ← 1; // inicializimi i rezultatit
for i ← 1 to n do
exp ← exp + fuqia(x,n)/faktIterativ(n); //
return exp
}

Llogaritja e rrënjës katrore


Llogaritja e √𝑎 për a > 0 mund të kryhet me anë të gjetjes së rrënjës të ekuacionit x2 – a = 0.
Metoda e Newton-Raphson thotë që rrënja përftohet nga zgjidhja e ekuacionit jolinear x2 – a
= 0 me anë të përafrimeve të njëpasnjëshme sipas formulës iterative

𝑓(𝑥𝑛 ) 𝑥𝑛2 − 𝑎 𝑥𝑛2 + 𝑎 1 𝑎


𝑥𝑛+1 = 𝑥𝑛 − = 𝑥𝑛 − = = (𝑥𝑛 + )
𝑓 ′ (𝑥𝑛 ) 2𝑥𝑛 2𝑥𝑛 2 𝑥𝑛

duke filluar me një përafrim fillestar të pëlqyeshëm. Llogaritjet vazhdojnë ndërkohë që nuk
është arritur saktësia e kërkuar.

Algoritmi 3.4 Llogarit rrënjën katrore të një numri real pozitiv


// Të dhëna: a, numër real pozitiv dhe eps, saktësia numër real pozitiv
// Rezultati: xn1, përafrimi për rrënjën katrore të numrit a

rrenjaKatrore(a, eps) {
xn ← a; // inicializim (përafrimi fillestar)
xn1← xn + a/xn // llogaritet përafrimi pasardhës
while abs(xn1 - xn) > eps do { // kontrolli i saktësisë
xn ← xn1;
xn1 ← (a/xn + xn) / 2.0 // llogaritja e përafrimit të ri
}
return xn1
}

Çfarë do të ndodhte nëse do të kërkonim llogaritjen e √0 ? Çfarë mund të bëjmë?

Llogaritja e numrit π
Ekzistojnë shumë formula për llogaritjen e numrit 𝜋 (Pi grek). Njëra prej tyre është përafrimi
i tij me anë të shumës algjebrike të mëposhtme. Llogaritjet ndërpriten kur termi i rradhës që
shtohet bëhet më i vogël sesa një numër pozitiv i dhënë që më parë.

𝜋 1 1 1 1 1
≈1− + − + − +⋯
4 3 5 7 9 11

// Algoritmi 3.5 Llogaritja e numrit π (pi grek)


// Të dhëna: eps, numër real i vogël që përcakton saktësinë e përafrimit
// Rezultati: pi, numri pi grek

llogaritPi(eps) {
shenja ← 1.0; // inicializimi i shenjës së termit të radhës
emrues ← 1.0; // inicializimi i emruesit
3 Probleme me algoritme iterativë | 48

pi ← 1.0; // inicializimi i rezultatit, termi i parë


do
shenja ← − shenja; // ndryshimi i shenjës së termit të radhës
emrues ← emrues + 2; // përftimi i emruesit
termi ← shenja / emrues; // termi i rradhës
pi ← pi + termi; // rezultati i pjesshëm
while abs(termi) > eps // kontrolli i kushtit të vazhdimit
return 4*pi
}

3.2 Përdorimi i cikleve përpunimin e tabelave

Algoritmet iterativë janë tepër të dobishëm për zgjidhjen e problemeve ku të dhënat mund të
vendosen në tabela. Me të vërtetë, algoritme të shumtë që na interesojnë në lidhje me tabelat,
kërkojnë kontrollin e elementeve të tabelës në një farë rradhe, më shpesh në rradhën rritëse të
indekseve të saj. Përpunimi i secilit prej elementeve është i njëjtë, vetëm indekset ndryshojnë.
Për këtë arsye një cikël është shumë i përshtatshëm për këto lloj përpunimesh.

Nga pikpamja funksionale, veprimet më të dhëna të organizuara në tabela, mund të ndahen në


dy klasa kryesore:

Veprime llogaritjeje, që kanë të bëjnë me përftimin e disa karakteristikave të drejtpërdrejta


ose të përpunuara të të dhënave si: ekzistenca e një vlere të caktuar në tabelë ; gjetja e
pozicionit të një vlere të caktuar; kërkimi i disa vlerave të veçanta si vlera më e madhe ose
më e vogël, vlera mesatare; mesorja, elementi i ktë më i vogël; kërkimi i disa agregateve
tipikë statistikorë si shuma e elementeve, shpeshtësia e elementeve, moda, etj.

Veprime organizimi, që kanë të bëjnë me përgatitjen e të dhënave për përpunim si: marrja e
të dhënave nga jashtë, afishimi i vlerave, futja e nje elementi të ri; fshirja e një elementi;
modifikimi i një elementi; renditja e vlerave të elementeve sipas një kriteri të caktuar, etj.

Shuma e elementeve të një tabele


Problemi. Jepet një tabelë me n elemente. Të llogaritet shuma e elementeve të saj.

Veprimi i llogaritjes së shumës së elementeve të një tabele, është një nga veprimet më të
zakonshme kur përpunohen një grup me të dhëna numerike, sidomos në statistikë.

Algoritmi bazohet në arsyetimin e mëposhtëm. Në një variabël të quajtur shuma do të


vendosim rezultatin e kërkuar. Le të supozojmë se kemi llogaritur shumën e i – 1 elementeve
të parë dhe e kemi vendosur në variablin e quajtur shuma. Atëherë, në hapin e itë, për të
përftuar shumën e i elementeve të parë mjafton të shtojmë tek variabli shuma elementin e itë.
Algoritmi i mëposhtëm realizon zgjidhjen e problemit.

ALGORITMI 3.6 Llogaritja e shumës së elementeve të një tabele


// Të dhëna: a[0:n-1] tabelë me n elemente numerikë
// Rezultati: shuma, shuma e elementeve të tabelës

shumaTabele (n, a) {
3 Probleme me algoritme iterativë | 49

shuma ← 0; // inicializimi i rezultatit


for i ← 0 to n – 1 do
shuma ← shuma + a[i]; // rezultati i pjesshëm
return shuma; // rezultati përfundimtar
}

Gjetja e vlerës më të madhe


Problemi. Të gjendet vlera më e madhe në një varg prej n vlerash të të njëjtit tip të vendosur
në një tabelë.

Le të shënojmë me vmax , vlerën më të madhe në tyre. Vlera më e madhe gëzon cilësinë që


është jo më e vogël se çdo element tjetër. Duke kontrolluar këtë vlerë me çdo element tjetër
kemi përftuar rregullin e gjetjes së vlerës më e madhe. Në fillim supozohet se vlera më e
madhe është vlera e parë (mund të jetë edhe një vlerë e çfardoshme nga të dhënat ose çdo
vlerë rigorozisht më e vogël sesa çdo vlerë nga të dhënat fillestare). Më pas, në çdo përsëritje,
kontrollohet nëse vlera e rradhës është më e madhe se vmax . Nëse kjo është e vërtetë atëherë
si vlerë më e madhe vmax bëhet vlera e rradhës. Me përfundimin e kërkimit variabli vmax
do të ketë vlerën më të madhe.

Algoritmi 3.7 Gjetja e vlerës më të madhe në një tabelë


// Të dhëna: një tabelë a[0:n–1] me numra të plotë
// Rezultati: vmax, vlera më e madhe në tabelë

vleraMax(n, a) {
vmax ← a[0]; // inicializimi i vlerës më të madhe me vlerën e parë
i ← 1;
while (i ≤ n-1) do {
if (a[i] > vmax )
then vmax ← a[i]; // gjendet vlera më e madhe aktuale
i ← i + 1;
}
return vmax
}

Kërkimi i pozicionit të një vlere të caktuar


Problemi. Jepet një tabelë me n elemente. Të gjendet në cilin pozicion ndodhet një vlerë e
caktuar k.

Kërkimi në një tabelë, për të përcaktuar pozicionin ose ekzistencën e një vlere të caktuar,
është një veprim i zakonshëm në jetën e përditshme. Për shembull, dikush ka marrë pjesë në
një konkurs dhe kërkon të dijë se i sati është renditur ose se sa pikë ka fituar.

Cila është metoda më e thjeshtë për të kërkuar në një tabelë? Bëhet një kontroll i
njëpasnjëshëm i elementeve derisa ose të arrihet një përputhje me elementin e kërkuar ose të
mbrihet në fund të të dhënave pa asnjë përputhje. Në rast përputhjeje kthehet pozicioni ku u
krye përputhja përndryshe kthehet një sinjal që vë në dukje këtë fakt. Ky lloj kërkimi quhet
kërkimi i njëpasnjëshëm (sequential search) ose kërkimi linear (linear search).
3 Probleme me algoritme iterativë | 50

Më poshtë janë hartuar tre algoritme të cilët ndryshojnë nga mënyra e përfundimit të
kërkimit.

Algoritmi i mëposhtëm kerkoElement, kërkon në një tabelë me n elemente dhe të indeksuar


nga 0 tek n – 1, ekzistencën e një vlere të quajtur k. Përdorimi i një cikli while me një kusht
kontrolli që mundëson vazhdimin e kontrollit kur ka akoma elemente për të kërkuar, është
shumë i përshtatshëm. Algoritmi duhet të jetë i aftë që të dalë nga cikli sapo vlera k të gjendet
dhe të iterojë në të gjithë elementet nëse është e domosdoshme. Rezultati që do të prodhojë
algoritmi është ose pozicioni i vlerës së kërkuar ose një mesazh në rast dështimi, identifikuar
me vlerën –1.

Algoritmi 3.8 Kërkimi linear i thjeshtë


// Të dhëna: tabela a[0:n–1] me n elemente dhe një vlerë k e caktuar
// Rezultati: një indeks i, i tillë që k = a[i] ose –1, nëse vlera k nuk gjendet në
// tabelë

kerkoElement1(n, a, k) {
i ← 0;
while (i < n) do
{ if A[i] = k
then return i; // kthehet pozicioni i vlerës k
i ← i + 1;
}
return –1
}

Nëse vërejmë me kujdes kushtin e kontrollit të ciklit në rreshtin e dytë të algoritmit dhe
kushtin e përputhshmërisë në rreshtin e tretë mund të shohim që me këta dy kushte mund të
formohet një kusht i ri logjik për kontrollin e vazhdimit të ciklit, në formën ((i < n) and (A[i]
≠ k)), që do të thotë që përsëritja do të vazhdojë për aq kohë sa të ketë elemente dhe nuk është
realizuar përputhja. Algoritmi mund të riformulohet si më poshtë:

// Algoritmi 3.9 Kërkimi linear me kusht logjik


// Të dhëna: tabela a[0:n–1] me n elemente dhe një vlerë k e caktuar
// Rezultati: një indeks i, i tillë që k = a[i] ose −1, nëse vlera k nuk gjendet

kerkoElement2(n, a, k) {
i ← 0;
while (i < n) and (a[i] ≠ k) do
i ← i + 1;
if ( i < n)
then return i
else return −1;
}

Një marifet (sentinela) përdoret shpesh në zbatimin e kërkimit linear: nëse shtojmë në fund të
të dhënave vlerën që kërkohet, kërkimi do të jetë i suksesshëm dhe prandaj mund të
eleminohet kërkimi i njëkohshëm i fundit të tabelës dhe i elementit në kushtin e përfundimit
të kërkimit.
3 Probleme me algoritme iterativë | 51

// ALGORITMI 3.10 // Kërkimi linear me sentinel


// Të dhëna: tabela a[0:n-1] me n elemente dhe një vlerë k e caktuar
// Rezultati: i, pozicioni i vlerës së kërkuar ose −1 nëse vlera nuk gjendet

kerkoElement3(n, a, k) {
a[n] ← k;
i ← 0;
while (a[i] ≠ k do
i ← i + 1;
if (i < n)
then return i
else return −1
}

Fshirja e një elementi nga tabela


Problemi. Të fshihet një element i një tabele me n elemente (n ≥ 2), kur jepet pozicioni i
elementit që do të fshihet.

Fshirja e një elementi nënkupton që ai të mos ekzistojë më në tabelë (jo të zëvendësohet vlera
me ndonjë tjetër) dhe tabela duhet të ketë një element më pak. Për të fshirë një element nga
një tabelë duhet që vlerat e elementeve pasues të zhvendosen me nga një pozicion në drejtim
të pozicionit të elementit të fshirë dhe më pas përmasa e tabelës të zvogëlohet me 1 njësi.
Nëse elementi që do të fshihet do të jetë i fundit nuk do të bëhet asnjë zhvendosje por do të
zvogëlohet përmasa e tabelës me 1 njësi.

Për shembull, nëse tabela e dhënë përmban numrat [3, 6, -3, 7, 9] dhe duhet fshihet elementi
që ndodhet në pozicionin e katërt, pas procesit të largimit tabela do të ketë përbërjen [3, 6, -3,
9] duke patur edhe një element më pak.

// Algoritmi 3.11 Fshirja e një elementi në tabelë


// Të dhëna: Një tabelë a[0:n–1] dhe poz, pozicioni i elementit që do të fshihet
// Rezultati: Tabela a, me n–1 elementë

fshiElement(n, a, poz) {
if (0 ≤ poz) and (poz ≤ n–1)
then {for i ← poz to n – 2 do
a[i] ← a[ i + 1];
n ← n – 1;
}
else print(”Pozicion i pamundur për fshirje elementi”)
return
}

Shtimi i një elementi në tabelë


Problemi. Jepet një tabelë me n elemente. Të shtohet në tabelë një element i ri në një
pozicion të dhënë.
3 Probleme me algoritme iterativë | 52

Të shtosh një element në një tabelë në një pozicion të caktuar do të thotë t’i zhvendosesh të
gjithë elementet pas pozicionit të dhënë, për t’i hapur vend elementit.

Për shembull, nëse tabela e dhënë përbëhet nga vlerat [3, 6, 7, 4, 9] dhe vlera e dhënë është 8
që do të vendoset në pozicionin 2, gjendja e tabelës pas vendosjes së vlerës do të jetë: [3, 8, 6,
7, 4, 9].

Algoritmi 3.12 Shtimi i një elementi në një pozicion të caktuar në një tabelë
// Të dhëna: Një tabelë a[0:n –1] me n elemente, n ≥ 1,
// vl dhe poz, respektivisht vlera dhe pozicioni ku do të vendoset kjo
// vlerë në tabelë
// Rezultati: Tabela a, të cilës i është shtuar një element

shtoElement(n, a, vl, poz) {


n ← n + 1; // zmadhohet përmasa e tabelës me një
i ← n –1
while i ≥ poz + 1 do
a[i] ← a[i – 1]; // zhvendosja tek pasardhësi
i←i–1
a[poz] ← vl; // kopjohet vlera në pozicionin e duhur
return
}

Deri tani kemi paraqitur algoritme që përmbajnë përpunimin e nje tabele të vetme. Ndërkaq
në praktikë ka shume probleme ku të dhënat mund të paraqiten në disa tabela. Shembulli i
mëposhtëm është një rast tipik i kërkimit në dy tabela të ndryshme.

Problemi. Jepet tabela tblEmra që përmban emrat e studenteve fitues për në Fakultetin e
Ekonomisë dhe tabela tblPike që përmban pikët e studentëve. Secila tabelë ka n elemente.
Elementit në pozicionin e i-të të tabelës me emra i korrespondon elementi i i-të në tabelën e
pikëve. Të verifikohet nëse një student i caktuar, em, ka fituar p pikë.

// Algoritmi 3.13 Kërkim në tabela korresponduese


// Të dhëna: Dy tabela tblEmra[0:n –1] dhe tblPike[0: n-1] me n elemente, ku n ≥
1;
// em dhe p, respektivisht emri i një studenti dhe pikët e fituara 1
// Rezultati: një mesazh pohimi ose mohimi

verifikoPiket(n, tblEmra, tblPike, em, p) {


for i ← 0 To n - 1 do
if tblEmra [i] = em
then if tblPike [i] = p
then return “Ka fituar ”
else return “Nuk ka fituar”;
return “Studenti nuk gjendet në listë”
}
3 Probleme me algoritme iterativë | 53

Gjetja e shumës së elementeve të një tabele dy-dimensionale


Problemi. Jepet një tabelë dy-dimensionale me m rreshta dhe n shtylla. Të llogaritet shuma e
elementeve të saj.

Në një variabël të quajtur shuma do të vendosim rezultatin e kërkuar. Aksesi tek elementi i
rradhës do të mundësohet nga një strukturë ciklike e përfshirë e përbërë nga dy cikle. Le të
supozojmë se kemi llogaritur shumën e elementeve në i – 1 rreshtat e parë dhe e kemi
vendosur në variablin e quajtur shuma. Atëherë, për rreshtin e itë, do të fillojmë nga shtylla e
parë deri tek shtylla e fundit duke shtuar elementet respektivë nëpërmjet ciklimit të një
variabli j me vlerat nga 0 deri në n–1.

Algoritmi 3.14 Llogaritja e shumës së elementeve në tabelën dy-dimensionale


// Të dhëna: m dhe n > 0 dhe a[0:m–1,0:n–1] një tabelë dy-dimensionale
// Rezultati: shuma, shuma e elementeve të tabelës

llogaritShumaTabele2D(m, n, a){
shuma ← 0; // inicializimi i akumulatorit
for i ← 0 to m–1 do
for j ← 0 to n–1 do
shuma ← shuma + a[i, j]; // akumulimi
return shuma
}

Problemi. Jepet një tabelë dy-dimensionale me m rreshta dhe n shtylla. Të gjendet rreshti që
ka shumën më të madhe.

Algoritmi 3.15 Gjetja e rreshtit me shumën më të madhe në tabelë 2-D


// Të dhëna: m, n > 0 dhe a[0:m–1], a[0:n–1]
// Rezultati: rreshtiMax, indeksi i rreshtit me shumën më të madhe

gjejRreshtinMax(m, n, a) {
shumaMax ← 0; // inicializimi i shumës më të madhe
for i ← 0 to m–1 do { // për çdo rresht të tabelës
shumaAktuale ← 0; // inicializimi i shumës së rreshtit të
rradhës
for j ← 0 to n–1 do { // për çdo shtyllë të tabelës
shumaAktuale ← shumaAktuale + a[i, j]; // përftimi i shumës së rradhës
if shumaAktuale > shumaMax {
shumaMax← shumaAktuale; // përftimi i shumës më të madhe
rreshtiMax ← i}
}
}
}
return rreshtiMax
}
3 Probleme me algoritme iterativë | 54

Shumëzimi i dy tabelave dy-dimensionale kuadratike


Problemi. Le të jenë dhënë dy tabela dy-dimensionale kuadratike të rendit n, a dhe b. Të
hartohet një algoritëm që llogarit produktin e tyre p = a⋅b. Nga përkufizimi i shumëzimit të
matricave, p është një tabelë kuadratike e rendit n, elementet e së cilës llogariten si produkt
skalar i rreshtave të tabelës a me shtyllat e tabelës b:

c[i, j] = ∑𝑛𝑘=1 𝑎[𝑖, 𝑘] ∗ 𝑏[𝑘, 𝑗] për çdo çift indeksesh 1≤ i, j≤ n .

Shumëzimi i matricave është i mundshëm jo vetëm për matrica kuadratike por edhe për ato
drejtkëndëshe mjafton që të plotësohet kriteri i mundësisë së shumëzimit të dy matricave
domethënë, numri i shtyllave të matricës parë të jetë i barabartë me numrin e rreshtave të
matricës së dytë.
A B C
.

rreshti i . * = C[i, j ]

shtylla j
Figura 3.1 Shumëzimi i dy tabelave kuadratike të rendit n

Algoritmi 3.16 Shumëzimi i dy tabelave dy-dimensionale kuadratike


// Të dhëna: n > 0 dhe a, b dy tabela kuadratike të rendit n
// Rezultati: Një tabelë kuadratike c = a x b, e rendit n

shumëzimTabelaKuadratike(n, a, b, c)
for i ← 0 to n-1 do
for j ← 0 to n-1 do {
c[i, j] ← 0; // inicializimi i elementit të rradhës
for k ← 0 to n-1 do
c[i, j] ← c[i, j] + a[i, k] * b[k, j] //formimi i elementit të rradhës
}
return
}

3.3 Përmbledhje

• algoritmet iterativë përshtaten mjaft mirë për llogaritjen e funksioneve matematikorë;


• algoritmet iterativë kanë për bazë strukturat ciklike. Algoritmet iterativë përshtaten mjaft
mirë për përpunimin e të dhënave të vendosura në tabela;
• nga pikpamja funksionale, veprimet me tabelat, mund të ndahen në dy kategori kryesore:
(i) veprime organizimi, dhe (ii) veprime kërkimi;
• veprimet e organizimit kanë të bëjnë me përgatitjen e tabelave për përpunimin e të
dhënave që ato mbajnë si futja e një vlere, fshirja e një vlere, renditja e elementeve, etj;
• veprimet e kërkimit kanë të bëjnë me përftimin e disa karakteristikave të drejtpërdrejta
ose të përpunuara të të dhënave si: kërkimi i një vlere të caktuar apo pozicionit të saj;
3 Probleme me algoritme iterativë | 55

kërkimi i disa vlerave të veçanta si vlera më e madhe, më e vogël, vlera mesatare e


elementeve, vlera medianë e elementeve, elementi i ktë më i vogël, etj; kërkimi i disa
agregateve tipike si shuma e elementeve, frekuenca e elementeve, etj.

3.4 Ushtrime për kapitullin 3

1. Jepet vargu numerik {2, 4, 6,…}. Të hartohet një algoritëm për të llogaritur shumën e n
termave të parë të vargut të mësipërm, ku n numër i plotë pozitiv.

2. Të hartohet një algoritëm që llogarit shumën e n termave të formuara sipas rregullit: 2∙5
– 8∙11 + 14∙17 – 20 ∙23 + ∙∙∙, ku n është një numër i plotë pozitiv.

3. Të hartohet një algoritëm që llogarit shumën e n termave të formuar sipas rregullit: 1∙4 +
3∙7 + 5∙10 + ∙∙∙, dhe n është një numër i plotë pozitiv.

4. Jepet vargu numerik {1, 1/2, 1/4, 1/8, ∙∙∙}. Të hartohet një algoritëm që llogarit shumën e
termave të këtij vargu që nuk janë më të vogla se një numër pozitiv, i dhënë.

5. Të hartohet një algoritëm që lokalizon shfaqjen e fundit të elementit më të vogël në një


tabelë me numra të plotë.

6. Jepet një tabelë me numra të plotë si dhe një numër i plotë i caktuar. Të kopjohen në një
tabelë tjetër, të gjithë elementet e tabelës së dhënë, që janë të ndryshëm nga vlera e dhënë.

7. Jepet një tabelë me numra të plotë. Të krijohet një tabelë e re që nuk përmban elementin e
parë të tabelës së dhënë.

8. Jepet një tabelë me numra të plotë pozitivë. Të krijohen dy tabela të tjera që të


përmbajnë respektivisht numrat çift dhe numrat tek të tabelës së dhënë.

9. Jepet një tabelë a[1..n] me n numra të plotë, elementet e të cilës janë të renditur në rendin
rritës (thuhet se elementet e tabelës janë të renditur në rendin rritës nëse plotësohen
kushtet a[1] < a[2] < …< a[n]). Të hartohet një algoritëm që vendos një numër të plotë c
të dhënë, në tabelë pa prishur renditjen (vetëm mund ta zbusë atë). Është e sigurtë që
numri c nuk është më i vogël sesa vlera më e vogël në tabelë, po ashtu ai nuk është më i
madh sesa vlera më e madhe.

10. Jepet një tabelë me n numra të plotë. Të gjenden sa vlera, janë më të vogla sesa 50% e
vlerës mesatare të elementeve të tabelës.

11. Jepen dy tabela me numra të plotë v1 dhe v2, me përmasa përkatësisht n1 dhe n2. Të
verifikohet nëse tabelat v1 dhe v2 janë të barabarta.

12. Jepen dy tabela me të njëjtën përmasë secila prej të cilave përbëhet nga vlera të
ndryshme. Të hartohet një algoritëm që përcakton nëse të dy tabelat kanë të njëjtët
elemente.

13. Jepet një tabelë a me n elemente. Përcaktoni nëse gjenden në këtë tabelë të paktën dy
indekse i dhe j (0 ≤ i ≠ j ≤ n-1) të tillë që a[i] = a[j].
3 Probleme me algoritme iterativë | 56

14. Jepen dy tabela me numra të plotë a dhe b me n elemente secila si dhe një numër i plotë
sh. Të verifikohet nëse ekzistojnë dy indekse i, j ∈[0:n-1] të tillë që a[i] + a[j] = sh.

15. Të hartohet një algoritëm që, gjen fjalën më të gjatë të një fjalie të shkruar në gjuhën
shqipe, të vendosur në një tabelë me karaktere. Fjalët ndahen nga njëra tjera me shenjën
e hapsirës (shkronjat dyshe janë të vendosura në dy elemente fqinjë të tabelës. Fundi i
fjalisë dallohet nga shenja pikë (.).

16. Jepen dy tabela me numra të plotë. A kanë këto dy tabela ndonjë element të përbashkët.

17. Jepen dy tabela v1 dhe v2, përkatësisht me n1 dhe n2 elemente me numra të plotë. Të
formohet një tabelë me elementet e tabelës v1 që nuk janë në tabelën v2.

18. Një modë në një varg me numra të plotë është ai numër që shfaqet jo më pak herë sesa
secili nga elementet e tjerë të vargut. Hartoni një algoritëm që gjen një modë në një varg
me numra të plotë të renditur në rendin jozbritës.

19. Jepet një tabelë me numra të plotë. Të verifikohet nëse tabela është e renditur në rendin
rritës.

20. Hartoni një algoritëm që në një tabelë të dhënë gjen pozicionin e elementit të fundit që
është i barabartë me ndonjë nga elementet e mëparshëm.

21. Jepet një tabelë t me n elemente ku çdo element i së cilës është një shkronjë alfabeti
(shkronjat dyshe janë të vendosur në dy elemente fqinjë). Të llogaritet numri i zanoreve
[a, e, ë, i, o, u, y], që ndodhen në të.

22. Hartoni një algoritëm, që në një tabelë me numra të plotë të dhënë, gjen të parin element
që është më i vogël se elementi paraardhës i tij në tabelë.

23. Le të jetë dhënë një tabelë me n elemente. Të hartohet një algoritëm që ndërton një tabelë
të re, po të përmasës n, elementet e të cilës janë komponentet e tabelës së dhënë të
shumëzuar me një numër të dhënë c, dhe të rezervuar në rendin e kundërt në lidhje me
tabelën e dhënë.

24. Në një tabelë me n numra të plotë, grupin e parë të përbërë nga 2 elemente të
njëpasnjëshëm të barabarte e largoni nga tabela.

25. Jepen tre tabela me nga n elemente secila: tabela e parë përmban emrat disa artikujve (pa
përsëritje artikulli); tabela e dytë përmban sasitë respektive, shprehur në njësitë përkatëse
të matjes, ndërsa tabela e tretë përmban çmimet mesatare të shitjes në lekë. Të hartohet
një algoritëm që llogarit:

a. Vlerën e mallit gjithsej në lekë.


b. Përqindjet në vlerë që zë çdo artikull karshi vlerës gjithsej.
c. Afishon të gjithë artikujt vlera e të cilëve është mbi L lekë, ku L është një numër i
dhënë që më parë.
d. Kontrolloni nëse sasia e artikullit të dhënë a1 është më e madhe se sasia e artikullit
a2.
3 Probleme me algoritme iterativë | 57

26. Në tre tabela me n elemente secila, janë të dhëna respektivisht: emrat e studentëve, notat
në lëndën matematikë dhe notat në lëndën statistikë. Nëse një student nuk është paraqitur
në provim atëherë në pozicionet përkatëse respektive shënohet simboli – 1. Të hartohet
një algoritëm që llogarit:

a. Cila është nota që takohet më shpesh në matematikë dhe në statistikë? A është e


njëjta notë?
b. A ka ndonjë notë kaluese që nuk është marrë nga studentët?
c. Largoni nga tabelat përkatëse studentët që nuk janë futur në të dy provimet.

27. Jepet një tabelë dy-dimensionale me numra të plotë me m rreshta dhe n shtylla. Gjeni se
në cilin pozicion ndodhet elementi me vlerën më të vogël. A është i vetëm ai?

28. Jepet një tabelë dy-dimensionale me numra të plotë me m rreshta dhe n shtylla. Tregoni
nëse ka të paktën dy shtylla të barabarta.

29. Jepet një tabelë dy-dimensionale me m rreshta dhe n shtylla e përbërë nga numra të
plotë. Tregoni nëse ka të paktën dy shtylla të njëpasnjëshme të barabarta.

30. Jepet një tabelë dy-dimensionale me numra të plotë me m rreshta dhe n shtylla. Hartoni
një algoritëm që gjen shtyllën që ka shumën më të madhe të elementeve.

31. Jepet një tabelë dy-dimensionale me m rreshta dhe m shtylla. Tregoni nëse ajo është një
tabelë simetrike (tabela quhet simetrike nëse a[i,j] = a[j,i] për çdo çift elementesh (i, j).

32. Jepet një tabelë dy-dimensionale me m rreshta dhe m shtylla. Llogaritni shumën e
elementeve mbi diagonalen kryesore dhe shumën e elementeve nën diagonalen kryesore.
Cila shumë është më e madhe?

33. Të verifikohet nëse një tabelë dy-dimensionale me përmasa n x n është diagonale (a[i, j]
= 0, për i ≠ j).

34. Jepen tre tabela dhe pikërisht: tabela e parë përmban emrat e n studentëve, tabela e dytë
përmban emrat e m lëndëve dhe tabela e tretë përmban notat e çdo studenti në çdo lëndë.
Duke patur këto të dhëna të hartohet një algoritëm që:

a. Llogarit notën mesatare të çdo lënde


b. Llogarit notën mesatare të çdo studenti
c. Të krijohen tabela të reja që do të përmbajnë vetëm studentët që e kanë notën
mesatare më të vogël se 8.
4 Probleme me algoritme rekursive | 58

4 Probleme me algoritme rekursivë

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të njohë qasjen rekursive për hartimin e algoritmeve;


• të përdorë rekursionin për llogaritjen e vlerave të funksioneve;
• të përdorë ciklet për zgjidhjen e problemeve me të dhëna të organizuara në tabela një
përmasore;
• të përdorë rekursionin për zgjidhjen e problemeve që kryejnë një përpunim;
• të kuptojë dhe të shprehë dallimet ndërmjet qasjes iterative dhe rekursive për zgjidhjen e
problemeve me përsëritje.

Rekursioni (recursion) është një nga konceptet kryesorë në algoritmikë dhe konsiderohet si
një mjet i dobishëm dhe i fuqishëm për zgjidhjen e shumë problemeve. Rekursioni është një
qasje alternative për të zgjidhur një problem, e ndryshme nga iteracioni.

Rekursioni është një metodë që zgjidh një problem në funksion të versioneve më të vegjël të
të njëjtit problem. Meqenëse problemi bëhet më i vogël çdo herë, procesi përfundon në një
problem që mund të zgjidhet drejtpërsëdrejti.

Në shumë raste, përdorimi i rekursionit na lejon që të specifikojmë në mënyrë shumë të


natyrshme një zgjidhje të thjeshtë të një problemi që ndryshe do të ishte shumë e vështirë për
t’u zgjidhur. Për këtë arsye, rekursioni është një mjet i rëndësishëm dhe i fuqishëm për
zgjidhjen e problemeve si në matematikë ashtu edhe në algoritmikë.

Algoritme me strukturë rekursive mund të përdoren në probleme të tilla si:

• Llogaritje numerike në rast se problemi mund të shprehet matematikisht në formë


rekursive si për shembull faktoriali, numrat e Fibonacit, etj;
• Veprime me tabela si renditja, kërkimi, etj;
• Lojra të ndryshme si për shembull kullat e Hanoi, problemi i mbretëreshave (Queens
Problem) dhe problemi i kuajve (Knights Problem) në lojën e shahut, krijimi i një
labirinti, gjetja e rrugës në një labirint, etj;
• Vizatime të kurbave si për shembull shkallëzimi i vizores, kurbat e Sierpinsk-it, Koch-ut,
etj.

Nga pikpamja strukturore algoritmet rekursive mund të hartohen si funksione që kthejnë një
vlerë ose funksione që nuk kthejnë vlerë por që kryejnë një përpunim.

4.1 Përdorimi i rekursionit për llogaritje numerike

Në këtë seksion do të studjohet zgjidhja algoritmike e disa problemeve, rezultati i të cilave


është një vlerë e vetme (pavarësisht nga tipi i saj). Këta janë probleme që kanë të bëjnë me
llogaritjen e vlerave të funksioneve me argumenta numrat e plotë, të vargjeve numerikë si
dhe probleme kërkimi në tabela.
4 Probleme me algoritme rekursive | 59

Llogaritja e faktorialit

Faktoriali i një numri mund të shprehet edhe në një formë tjetër të ndryshme nga ajo që pamë
në kapitullin 3, në të ashtuquajturën trajtë rekursive, si produkt i numrit n me faktorialin e (n
– 1) numrave të mëparshëm pra:

( )
n! = � 𝑛 ∙ 𝑛 − 1 ! 𝑛ë𝑠𝑠 𝑛 ≥ 1
1 𝑛ë𝑠𝑠 𝑛 = 0
(4.1)

Le të përqëndrohemi në këtë përkufizim. Vërejmë tre faktet e mëposhtme:

 ekzistencën e një rasti të thjeshtë që ka përgjigje të drejtpërdrejtë vlerën 1, për n = 0 (i


ashtuquajturi rasti bazë)
 përsëritjen e vetvetes por me përmasë më të vogël, ( n – 1 )!, për n ≥1
 lidhjen ndërmjet problemit të përmasës n dhe atij më të vogël n – 1, në formën
𝑛 ∙ (𝑛 − 1)!, (hapi rekursiv)

Ka një çështje të rëndësishme dhe mundet edhe çoroditëse në lidhje me rekursionin dhe
pikërisht: a nuk kemi të bëjmë me një logjikë të gabuar? Përgjigja është Jo. Megjithëse jemi
duke përcaktuar një funksion në varësi të vetvetes, ne nuk jemi duke përcaktuar një rast të
veçantë të funksionit në varësi të vetvetes. Me fjalë të tjera, vlerësimi i 5! duke përdorur 5! do
të ishte e gabuar. Vlerësimi i 5! duke llogaritur 4! do të ishte e gabuar veçse nëse 4! do të
llogaritej duke përdorur 5!

Duke patur për bazë përkufizimin (4.1), mund të hartohet një algoritëm për llogaritjen e
faktorialit në funksion të argumentit n. Hapi rekursiv, n * (n – 1)!, është i vendosur në
rreshtin 3 të algoritmit. Kjo nënkupton që rezultati i thirrjes së rradhës të funksionit
përcaktohet duke shumëzuar rezultatin e thirrjes pasardhëse të tij (me argument (n – 1) të
problemit me numrin n. Në këtë algoritëm funksioni identifikohet nga emri i algoritmit faktR.
Rasti bazë është i vendosur në rreshtin 2.

// Algoritmi 4.1 Llogaritja e faktorialit në mënyrë rekursive


// Të dhëna: n ≥ 0, një numër i plotë jonegativ
// Rezultati: Faktoriali i numrit n

faktR(n) {
if n = 0
then return 1
else return n * faktR(n − 1);
}

Gjurmimi i rekursionit

Një nga aspektet më të vështira të rekursionit është të provuarit që funksioni rekursiv kryen
atë që i kërkohet. Në përgjithësi, gjurmimi i ekzekutimit të një algoritmi na mundëson të
shohim se si funksionon algoritmi. Me algoritmet iterativë kjo kryhet në mënyrë të natyrshme
duke afishuar përmbajtjet e variablave hap pas hapi. Për algoritmet rekursive një mënyrë e
ngjashme mund të shkaktojë pështjellime. Në vend të saj, më mirë të imagjinohet që çdo
thirrje e algoritmit, krijon një kopje të re të tij, kështu që, nëse një funksion thirret disa herë,
4 Probleme me algoritme rekursive | 60

atëherë krijohen aq kopje sa herë thirret ai. Në figurën 4.1 paraqitet gjurma e llogaritjes së
faktorialit, për n = 3. Rezultati që kthen algoritmi është vlera 6.
faktR(3) faktR(2) faktR(1) faktR(0)
(3 = 0) ( e gabuar) (2 = 0) ( e gabuar) (1 = 0) (e gabuar) (0 = 0) (e vërtetë)

adr1: 3 * faktR(2) adr2: 2 * faktR(1) adr3: 1 * faktR(0) adr4: 1

return 6 return 2 return 1 return 1

Figura 4.1 Gjurmimi i ekzekutimit të algoritmit për llogaritjen e 3!

Thirrja faktR(3) krijon një kopje të funksionit me n = 3 dhe fillon ekzekutimin e kopjes në
rreshtin 1 duke vlerësuar kushtin e krahasimit. Meqënëse kushti (3 = 0) është i gabuar,
ekzekutimi kalon në rreshtin 3 duke kërkuar llogaritjen e shprehjes (3 * faktR(2)). Kjo
shprehje nuk mund të llogaritet mbasi nuk njihet faktR(2) dhe ajo rezervohet në një vend të
caktuar të kujtesës, le të themi adr1, në pritje të llogaritjes së faktR(2).

Thirrja faktR(2) krijon një kopje të re të funksionit me n = 2 dhe fillon ekzekutimin e saj në
rreshtin 1 duke vlerësuar kushtin e krahasimit. Meqënëse kushti (2 = 0) është i gabuar,
ekzekutimi kalon në rreshtin 3 duke kërkuar llogaritjen e (2 * faktR(1)). Kjo shprehje nuk
mund të llogaritet mbasi nuk njihet faktR(1) dhe ajo rezervohet në një vend të caktuar të
kujtesës, le të themi adr2, në pritje të llogaritjes së faktR(1).

Thirrja faktR(1) krijon një kopje të re të funksionit me n = 1 dhe fillon ekzekutimin e saj në
rreshtin 1 duke vlerësuar kushtin e krahasimit. Meqënëse kushti (1 = 0) është i gabuar,
ekzekutimi kalon në rreshtin 3 duke kërkuar llogaritjen e (1 * faktR(0)). Kjo shprehje nuk
mund të llogaritet mbasi nuk njihet faktR(0) dhe ajo rezervohet në një vend të caktuar të
kujtesës, le të themi adr3, në pritje të llogaritjes së faktR(0).

Thirrja faktR(0) krijon një kopje të re të funksionit me n = 0 dhe fillon ekzekutimin e saj në
rreshtin 1 duke vlerësuar kushtin e krahasimit. Meqënëse kushti (0 = 0) është i vërtetë
ekzekutimi kalon në rreshtin 2 duke i kthyer kopjes që e thirri vlerën 1 (që është vlera e
faktR(0)). Kjo kopje e shumëzon vlerën e kthyer 1 me 1 dhe i kthen kopjes që e thirri vlerën 1
(që është vlera e faktR(1)). Kjo kopje këtë vlerë të kthyer 1 e shumëzon me 2 dhe i kthen
kopjes që e thirri vlerën 2 (që është vlera e faktR(2)). Kjo kopje e fundit e algoritmit, e
shumëzon vlerën e kthyer 2 me vlerën 3 dhe i kthen vlerën 6 funksionit kryesor, i cili e ka
thirrur atë.

Vëmë në dukje që funksioni për llogaritjen e faktorialit në trajtë rekursive është më elegant se
funksioni në trajtë iterative mbasi algoritmi nuk është gjë tjetër veçse përkufizimi i problemit.

Lista e kushteve të funksionimit të rekursionit


Më poshtë paraqitet një listë e kushteve (checklist) që duhet të plotësohen për siguruar të
paktën funksionimin e rekursionit (kujdes, jo që të japë rezultatin e duhur).

1. Një algoritëm rekursiv duhet të ketë të paktën një rast ndalimi (i quajtur rasti bazë)
dhe të paktën një thirrje rekursive (mund te ketë edhe më tepër se një të tillë)
4 Probleme me algoritme rekursive | 61

2. Kontrolli për rastin e ndalimit duhet të ekzekutohet para thirrjes rekursive të


funksionit
3. Problemi duhet të ndahet në probleme më të vegjël në mënyrë që thirrjet e algoritmit
të jenë më afër rastit të ndalimit sesa problemi origjinal.
4. Thirrja rekursive nuk duhet t’i shmanget rastit të ndalimit

Le të shohim tani nëse algoritmi i mësipërm, faktR, realizuar në trajtën e një algoritmi
rekursiv, i plotëson kriteret e lartpërmendura të listë-kontrollit:

 Ka një rast bazë, n = 0 (rreshti i parë i algoritmit) dhe një thirrje rekursive, n ≥ 1 (rreshti
tretë).
 Nëse mbrijmë në thirrjen rekursive atëherë ne duhet të vlerësojmë nëse n = 0, kështu
gjithmonë testojmë për rastin e ndalimit.
 Thirrja rekursive është faktR(n − 1) (rreshti tretë). Në qoftë se n > 1 atëherë n − 1 është
më afër rastit të ndalimit (n = 0).
 Përderisa n është numur i plotë dhe thirrjet rekursive zvogëlohen me 1 nuk do të ishte e
mundur që t'i shmanget rastit të ndalimit.

Nëse do të kërkonim vlerësimin e faktR(−1) atëherë kjo do të kërkonte thirrjen e faktR(−2),


faktRf(−3), e kështu me rradhë. Përderisa në këtë mënyrë rasti bazë nuk do të arrihet atëherë
algoritmi nuk do të jetë në gjendje të japë përgjigje (Si duhet të veprojmë për të shmangur
këtë rast?).

Për llogaritjen e faktorialit me anë të kompjuterit, nuk ka rëndësi algoritmi që përdoret,


rekursiv apo iterativ. Problemi kryesor në llogaritjen e faktorialit qëndron në madhësinë e
vlerave të tij mbasi ato rriten shumë shpejt, duke tejkaluar kapacitetin mbajtës të fjalës të
kujtesës të caktuar për rezultatin. Për shembull, 10! = 3,628,800 dhe 20!=
2,432,902,008,176,640,000. Në disa nga gjuhët e programimit një mesazh i tipit “integer
overflow error” sinjalizon tejkalimin e kapacitetit mbajtës dhe programi në kompjuter ndalon
ekzekutimin e mëtejshëm. Ndërkaq janë krijuar metoda llogaritëse të veçanta që mundësojnë
llogaritjen e faktorialit për vlera të mëdha të n-së. Ndërsa në gjuhën Python llogaritjet kryhen
pavarësisht madhësisë së numrave që përftohen.

Shënim
Realizimi i algoritmeve rekursivë në kompjuter bëhet me anë të strukturave të të dhënave të
tipit stivë, që funksionojnë sipas parimit “Hyn i pari−del i fundit” (FILO, First In − Last
Out). Me këto struktura, formohen nga kompilatori, të ashtuquajturit activation records ose
activation frames, gjatë fazës së kompilimit të programit. Këto struktura mbajnë gjithë të
dhënat e nevojshme, si vlerat e ndërmjetme ashtu edhe adresat e kthimit.

Llogaritja e shumës së numrave natyrorë

Problemi. Le të jetë dhënë vargu i n numrave të parë natyrorë 1, 2, 3, ⋅⋅⋅ , n dhe kërkohet që
të llogaritet shuma e tyre, 𝑆 = ∑𝑛𝑖=1 𝑖 , me anë të ndonjë algoritmi rekursiv.

Për të përcaktur problemin në funksion të një problemi më të vogël të të njëjtit tip veprojmë
si më poshtë:
Së pari le të shkruajmë shumën e n numrave të parë natyrorë në formën:

1 + 2 + ⋅⋅⋅ + n = [1 + 2 + ... + (n − 1)] + n (4.2)


4 Probleme me algoritme rekursive | 62

Duke shënuar me S(n) = 1 + 2 + ⋅⋅⋅ + n, atëherë për n ≥ 1, do të kemi lidhjen rekursive:

S(n) = S(n − 1) + n (4.3)

Për të përcaktuar rastin bazë kemi disa mundësi. Mund të konsiderojmë si rast bazë situatën
kur n = 0 dhe në këtë rast shuma e termave është e barabartë me 0 (nuk ka asnjë term për të
mbledhur) ose situatën kur n = 1 dhe shuma e termave është e barabartë me 1 (vetëm vlera 1
për t'u mbledhur). Shohim se çdo thirrje rekursive zvogëlon përmasën e problemit me një dhe
rasti bazë do të arrihet mbasi n është pozitive.

Algoritmi 4.2 Llogaritja e shumës së n numrave natyrorë në mënyrë rekursive


// Të dhëna: n ≥ 0, numër natyror
// Rezultati: shuma e n numrave të parë natyrorë

llogaritShumaNumraveNatyrore(n) {
if n = 0
then return 0
else return n + shumaNumraNatyrore(n − 1);
}

Por ka edhe një formulë fare të thjeshtë për llogaritjen e shumës së n numrave të parë
natyrorë dhe pikërisht: S(n) = n · (n + 1 ) / 2, që i dedikohet Gauss-it.

Shumëzimi i dy numrave natyrorë me anë të veprimit të mbledhjes

Problemi. Jepen dy numra natyrore m dhe n. Të llogaritet produkti i tyre m∙n.

Për të shprehur idenë rekursive le të llogaritim produktin 6 x 3 por jo me anë të shumëzimit


por me anë të mbledhjes. Do të supozojmë se dimë vetëm të mbledhim dhe dhe se shumëzimi
me 1 nuk e ndryshon rezultatin. Në këto kushte, të shumëzosh 6 me 3 është njëlloj sikur të
mbledhësh 6-ën tri herë rradhazi. Problemi i shumëzimit të 6 me 3 mund të ndahet në dy
probleme, P1, P2, si më poshtë:

P1. Shumëzimi i 6 me 2
P2. Zmadhimi me 6 i rezultatit të problemit P1

Përderisa ne njohim vetëm rregullin e mbledhjes dhe jo atë të shumëzimit, ne mund të


zgjidhim problemin P2 por nuk mund të zgjidhim dot problemin P1. Megjithatë problemi P1
është më i thjeshtë se problemi fillestar. Ne mund ta ndajmë atë në dy probleme P1.1 dhe
P1.2 si më poshtë, duke mbritur në tre probleme për të zgjidhur, dy prej të cilëve janë
mbledhje:

P1. Shumëzimi i 6 me 2.
P1.1 Shumëzimi 6 me 1
P1.2 Shtimi 6 tek rezultati
P2. Shtimi me 6 i rezultatit të problemit 1
4 Probleme me algoritme rekursive | 63

Kështu që duke zgjidhur problemin P1.1 (përgjigja është 6) dhe problemin P1.2, ne marrim
zgjidhjen për problemin P1 (përgjigja është 12). Duke zgjidhur problemin problemin P2 do të
përftojmë përgjigjen përfundimtare, që është vlera 18.

Duke përgjithsuar rezultatin për shumëzimin e një numri të dhënë m me një numër tjetër n,
duke mbajtur të fiksuar m dhe duke e coptuar problemin në funksion të n-së, rasti i ndalimit
takohet kur kushti n = 1 është i vërtetë. Në këtë rast përgjigja do të jetë m (m ∙ 1= m). Në
qoftë se n do të jetë më e madhe se 1 atëherë hapi rekursiv do të jetë:
m + shumëzo(m, n − 1);
Ky hap rekursiv e ndan problemin fillestar në dy nënprobleme më të thjeshtë:

shumëzo m me n – 1
shto m tek rezultati

I pari nga këta probleme zgjidhet duke thirrur algoritmin përsëri duke patur si argument të
dytë
n – 1. Në qoftë se argumenti i dytë i ri është më i madh se 1, atëherë do të ketë një thirrje
shtesë të algoritmit.

Algoritmi 4.3 Shumëzimi i dy numrave natyrorë, versioni rekursiv


// Të dhëna: numrat e plotë m dhe n (n > 0)
// Rezultat: produkti m ∙ n

shumëzoNumraNatyrore(m, n) {
if n = 1
then return m
else return m + shumëzim (m, n − 1)
}

Algoritmi i mësipërm është realizuar si një funksion me dy parametra por që ndryshon vetëm
njëri prej tyre. Është e mundshme që të shkruhen funksione rekursivë në të cilët ndryshojnë të
dy parametrat. Në përgjithësi, kur një funksion rekursiv ka disa parametra, ne duhet të
zgjedhim me kujdes parametrat, përmasa e të cilëve do të zvogëlohet në hapin rekursiv.

Në figurën 4.2 paraqitet gjurmimi i algoritmit të shumëzimit me anë të mbledhjes, prodhuar


nga një ekzekutim i algoritmit për m = 6 dhe n = 3. Rezultati që prodhon algoritmi është vlera
18.
shumëzim(6, 3) shumëzim(6, 2) shumëzim(6, 1)
m = 6, n = 3 m = 6, n = 2 m = 6, n = 1
3 = 1 (i gabuar) 2 = 1 (i gabuar) 1 = 1 ( i vërtetë )
adr1: 6 + shumëzim(6, 2) adr2: 6 + shumëzim(6, 1) adr3: 6

return 18 return 12 return 6

Figura 4.2 Gjurmimi i algoritmit të shumëzimit të numrave 6 me 3 me anë të mbledhjes


4 Probleme me algoritme rekursive | 64

Llogaritja e termave të vargut Fibonaçi

Problemi. Vargu i numrave të Fibonaçit është krijuar nga matematikani italian Leonardi i
Pizës (i ashtuquajturi Fibonacci) në shekullin e trembëdhjetë për të modeluar rritjen e
popullatës së minjve. Për të llogaritur numrin e minjve të lindur në muajin e n-të ai përcaktoi
formulën rekurrente,
F(n) = F(n − 1) + F(n − 2),
(4.4)

me vlerat e mëposhtme në rastin bazë: F0 = 0 dhe F1 = 1. Llogaritja e disa vlerave jep vargun
e mëposhtëm,
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,…

Formula e Fibonaçit nuk rezultoi një vlerësues i mirë për parashikimin e numrit të brejtësve
por ajo u mirëprit në aplikime të tjera mbasi ka cilësi interesante. Ekzistojnë aplikime
interesante të numrave të Fibonaçit edhe vetë në informatikë. Janë zbuluar edhe vargje të
tjerë numerikë të ngjashëm me vargun Fibonaçi që përdoren në parashikimin e çmimeve të
mallrave dhe të stoqeve.

Algoritmi fibR, është një algoritëm rekursiv për llogaritjen e termit të n-të të vargut të
Fibonaçit.

Algoritmi 4.4 Llogaritja e numrit të Fibonaçit në versionin rekursiv


// Të dhëna: n ≥ 0, një numër i plotë jonegativ
// Rezultati: termi i n t ë i vargut Fibonaçi

fibR (n) {
if n = 0
then return 0
else if n = 1
then return 1
else return fibR(n − 1) + fibR(n − 2)
}

Algoritmi për llogaritjen e numrave të Fibonaçit, realizuar në trajtën e një funksioni rekursiv,
përbëhet nga dy thirrje zvogëluese.

Megjithëse relacioni rekurrencial i llogaritjes së numrave të Fibonaçit është i lehtë për t’u
programuar në trajtë rekursive, ky algoritëm nuk është aspak i efektshëm mbasi çdo hap
rekursiv prodhon dy thirrje të funksionit Fibonaçi, të cilat kërkojnë llogaritjen e vlerave
tashmë të llogaritura. Thirrjet e ekuacionit rekurrencial të Fibonaçit (4.4), mund të paraqiten
në trajtë grafike, në formën e një pemë por me rrënjë lart të quajtur pema e rekursionit
(recursion tree). Nyjet e pemës përmbajnë thirrjet ndërsa shigjetat tregojnë thirrjet pasuese.
Dinamika e thirrjeve përfundon gjithmonë në rastet bazë.

Në figurën 4.3 paraqitet pema e thirrjeve për llogaritjen e numrit të pestë të vargut me anë të
thirrjes fibR(5). Nga pema e thirrjeve për llogaritjen e numrit të pestë të vargut të Fibonaçit,
fibR(5), shihet se fibR(2) llogaritet 3 herë, fibR(3) llogaritet 3 herë, etj. Kjo tregon se kemi një
përsëritje të llogaritjeve të njëjta, gjë që është një shpenzim i tepërt i kohës. Ky problem nuk
4 Probleme me algoritme rekursive | 65

shfaqet në zbatimet iterative të llogaritjes së numrave të Fibonaçit, siç do ta shohim në një


leksion të mëvonshëm.

Gjithashtu, në pemën e rekursionit, janë vendosur edhe vlerat që kthen çdo thirrje rekursive
(brenda rrathëve të vegjël). Në përfundim të ekzekutimit, thirrja fibR(5) kthen vlerën 5.

fibR(5)
5

fibR(4) fibR(3)
3 2

fibR(1)
fibR(3) fibR(2) fibR(2)
1
2 1 1

fibR(2) fibR(1) fibR(1) fibR(0) fibR(1) fibR(0)


1 1 0 1 0
1

fibR(1) fibR(0)
1 0

Figura 4.3 Pema e rekursionit për llogaritjen e termit të vargut Fibonaçi për n = 5

Ndërsa në figurën 4.4, numrat e vendosur mbi shigjetat tregojnë rradhën e ekzekutimit të çdo
thirrjeje rekursive, për llogaritjen e termit të vargut Fibonaçi për n = 5.

fibR(5)
1 10

fibR(4) fibR(3)
2 7 11 14

fibR(3) fibR(2) fibR(2) fibR(1)


3 6 12 13
8 9

fibR(2) fibR(1) fibR(1) fibR(0) fibR(1) fibR(0)

4 5

fibR(1) fibR(0)

Figura 4.4 Rradha e thirrjeve të rekursive për llogaritjen e termit Fibonaçi për n = 5

4.2 Perdorimi i rekursionit për përpunimin e tabelave

Shumë probleme ku si strukturë e të dhënave është zgjedhur tabela mund të zgjidhen në


mënyrë algoritmike me anë të rekursionit. Por leverdia e rekursionit do të shihet në një nga
leksionet e ardhme për renditjen e të dhënave. Në këtë seksion do të trajtohen probleme që
kërkojnë llogaritjen e një vlere në të dhëna të organizuara në tabela.

Shuma e elementeve të një tabele


Problemi. Të hartohet një algoritëm rekursiv që gjen shumën e elementeve të një tabele të
dhënë a me n elementë pozitivë.

Ky problem është i ngjashëm me atë të llogaritjes së shumës të n numrave të parë natyrorë.


4 Probleme me algoritme rekursive | 66

Së pari, si rast bazë (n = 0), mund të konsiderojmë tabelën me një element dhe në këtë rast
algoritmi do të kthejë vlerën a[0].

Së dyti le të shprehin shumën e n elementeve (problemi i përmasës n) të tabelës në


formën:
𝑎[0] + 𝑎[1] + ⋯ + 𝑎[𝑛 − 1] = �������������������
������������������� (𝑎[0] + 𝑎[1] + ⋯ + 𝑎[𝑛 − 2]) + �����
𝑎[𝑛 − 1]
𝑆(𝑛) 𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝 𝑖 𝑝ë𝑟𝑟𝑟𝑟ë𝑠 𝑛 𝑆(𝑛−1)𝑝𝑝𝑝𝑝𝑝𝑝𝑝 𝑖 𝑝ë𝑟𝑟𝑟𝑟ë𝑠 𝑛−1 𝑡𝑡𝑡𝑡𝑡 𝑖 𝑛−𝑡ë

Duke shënuar me S(n–1) = a[0] + a[1] + ⋅⋅⋅ + a[n–2], atëherë për n > 0, do të kemi lidhjen
rekursive:

S(n) = S(n − 1) + a[n–1],

që shpreh lidhjen e një problemi të përmasës n me një problem të përmasës n – 1. Shohim se


çdo thirrje rekursive zvogëlon përmasën e problemit me 1 dhe rasti bazë do të arrihet mbasi n
është pozitive. (Të mos harrojmë se në trajtimin e mësipërm përmasa e problemit është n dhe
tabela është e indeksuar nga 0 në n-1.

// Algoritmi 4.5 Llogaritja shumës së elementeve të një tabele me teknikën


// rekursive
// Të dhëna: një tabelë me n elemente, a[0:n–1]
// Rezultati: shuma e elementeve të tabelës

llogaritShumaTabelë1D(n, a) {
if n = 1
then return a[0]
else return a[n–1] + shumaTabelë1D(n – 1, a)
}

Kontrolli i ekzistencës së një vlere të caktuar në një tabelë


Algoritmet që kthejnë vlerat logjike true apo false gjithashtu mund të shkruhen në trajtë
rekursive. Funksioni rezultat përcaktohet nga vlerësimi i një shprehjeje logjike që përmban
një thirrje rekursive. Si shëmbull tipik është problemi i kërkimit të ekzistencës së një vlere në
një tabelë me n elemente.

Problemi. Të hartohet një algoritëm rekursiv që kontrollon nëse një vlerë e caktuar k ndodhet
në një tabelë a me n elemente që janë numra natyrorë.

Rasti bazë ndodh kur tabela është me 1 element (n = 1). Nëse ky element i vetëm është i
barabartë me vlerën kërkuar atëherë kjo vlerë është në tabelë dhe algoritmi do të kthejë
konstanten logjike true . Nëse elementi i vetëm nuk është i barabartë me vlerën e kërkuar
atëherë algoritmi do të kthjejë konstanten logjike false .

Në rastin e përgjithshëm, kur n ≥ 2, nëse elementi A[n] është i barabartë me vlerën e


kërkuar atëherë kjo vlerë ndodhet në tabelë përndryshe përsëritim kërkimin në problemin e (n
– 1)-të .
4 Probleme me algoritme rekursive | 67

// Algoritmi 4.6 kerkoVlereRekursiv(n, a, target)


// Kontrolli i ekzistencës së një vlere në një tabelë në mënyrë rekursive
// Të dhëna: k, natyror; n > 0, dhe një tabelë a[0:n–1] me numra natyrorë
// Rezultati: Vlera logjike true nëse k është në tabelën a; përndryshe false

if (n = 0) {
then return false
else {if (A[n–1] = target)
then return true
else return kerkoVlereRekursiv(n – 1, A, target)
}
}

4.3 Përdorimi i rekursionit për zgjidhjen e problemeve përpunuese

Ka probleme për të cilët mund të hartohen algoritme rekursive me funksione që nuk kthejenë
vlerë por kryejnë një përpunim.

Le të konsiderojmë problemin e afishimit n herë të fjalës “Përshëndetje”. Ne mund të


shkruajmë një përcaktim rekursiv për këtë problem si më poshtë:

mesazh(0): mos bëj asgjë


mesazh(1): print (“Përshëndetje”); mesazh(0)
mesazh(2): print (“Përshëndetje”); mesazh(1)

Ky skicim na shpie në hartimin e algoritmit të mëposhtëm rekursiv në trajtë funksioni.

// Algoritmi 4.7 Afishimi n herë i fjalës “Përshëndetje”


// Të dhëna: n > 0, natyror
// Rezultati: afishimi n herë i fjalës “Përshëndetje”

afishimMesazh(n) {
if (n ≤ 0)
then return
else {print (“Përshëndetje”);
return afishimMesazh (n – 1)
}
}

Vëmë në dukje rastin bazë për algoritmet rekursive të këtij tipi. Në këtë rast, rasti bazë është
një rast i degjeneruar, nuk bën asnjë veprim. Në këtë kuptim, ai paraqet përfundimin e
shpërbërjes të problemit në probleme të ngjashëm më të vegjël.

Afishimi i numrave të plotë mbrapsht


Të hartohet një algoritëm rekursiv i tipit procedurë që afishon vargun e numrave natyrorë
duke filluar nga një numër i dhënë n deri tek numri 1. Për shembull kur n = 3 do të kemi
afishimin e tre numrave në rradhën 3, 2, 1. Ky quhet problemi i printimit mbrapsht.
4 Probleme me algoritme rekursive | 68

Duke patur në konsideratë se si funksionon algoritmi rekursiv, këtu do të bëjmë një marifet,
instruksioni i afishimit do të vendoset para thirrjes rekursive të algoritmit.

Algoritmi 4.8 Afishimi i numrave të plotë në rendin zbritës duke filluar nga n
// Të dhëna: n > 0, natyror
// Rezultati: afishimi i vlerave: n, ... 2, 1

afishimNumraMbrapsht(n) {
if (n > 0) {
then {print (n);
return afishimNumraMbrapsht(n – 1);
}
else return
}

Thirrja e parë për ekzekutim në programin kryesor do të jetë afishimNumraMbrapsht(n).Vemë


gjithashtu në dukje faktin që instruksioni i afishimit të vlerës, print , është vendosur para
thirrjes rekursive.

Në figurën 4.5 paraqitet një gjurmim i algoritmit afishimNumraMbrapsht, krijuar nga një
ekzekutim i procedurës me vlerë fillestare n = 2. Rezultati që prodhon algoritmi është vargu i
numrave 2, 1.
afishimMbrapsht(3) afishimMbrapsht(2) afishimMbrapsht(1) afishimMbrapsht(0)
3 > 0 ( i vërtetë ) 2 > 0 ( i vërtetë ) 1 > 0 ( i vërtetë ) 0 > 0 ( i gabuar )
print (3) print (2) print (1)
adr1: afishimMbrapsht(2) adr2: afishimMbrapsht(1) adr3: afishimMbrapsht(0)

return return return return

Figura 4.5 Gjurmimi i algoritmit afishimMbrapsht për n = 3

4.4 Rekursioni dhe iteracioni

Pyetjes se kush është më i mirë iteracioni apo rekursioni nuk mund t'i jepet një përgjigje e
prerë. Megjithatë disa ekuilibra ekzistojnë. "Matematikanët" preferojnë më shumë
rekursionin. Zgjidhjet shpesh janë më të shkurtëra, më afër me shpirtin abstrakt të
matematikës por që zgjidhjet e mira rekursive mund të jenë më të vështira për t'i formuluar
dhe testuar. Ndërkaq "programistat" preferojnë zgjidhjet iterative. Në njëfarë mënyre ato
duken më tërheqëse për ta por kontrolli i tyre është më pak "magjik".

Është një fakt që mund të provohet lehtë që asnjëherë nuk kemi nevojë për rekursionin; çdo
algoritëm rekursiv mund të shkëmbehet me një algoritëm iterativ (ashtu siç mund të provohet
që një program mund të shkruhet vetëm në binar).

Përgjigja më e mirë ndaj pyetjes se kur duhet të përdoret rekursioni është e thjeshtë, atëherë
kur ndjehet e nevojshme për t’u përdorur. Asnjëherë nuk mund të thuhet thjesht “përdor
rekursionin” ashtu siç nuk mund të thuhet “përdor iteracionin”. Kur harton një algoritëm,
herë-herë përdor ciklin, herë-herë përdor tabelat, herë-herë kryen hyrje/dalje dhe herë-herë
përdor rekursionin.
4 Probleme me algoritme rekursive | 69

Një rregull i thjeshtë por mjaft i mirë është që rekursioni të përdoret atëherë kur janë duke u
përpunuar të dhëna të vendosura në struktura të përcaktuara rekursivisht. Për shembull, nëse
vlerësojmë një shprehje arithmetike, kllapat mund të jenë përdorur për të rrethuar një
“nënshprehje”, e cila duhet të vlerësohet së pari dhe në këtë rast rekursioni është më i miri. E
vetmja mënyrë e arsyeshme është që të shkruhet një algoritëm rekursiv për vlerësimin e
shprehjes; cikli nuk është më e mira.

Një rregull tjetër i njëvlefshëm është paraqitja e strukturave të përfshira me anë të rekursionit.
Për shëmbull, në shumë gjuhë programimi, çdo instruksion mund të përfshihet në një
instruksion if , bile edhe një if tjetër. Ky është një përdorim i pastër i rekursionit nga çdo
procesues: kompilator, preprocessor, interpretues, debug-ues, etj.
Por, përdorimi i rekursionit e ka një kosto në kohë dhe në kujtesë por sidomos në kujtesë
mbasi çdo nënproblem kërkon hapsirën e vet që është e njëjtë sa ajo e problemit të
mëparshëm.

Për momentin, pa kaluar në detaje, mund të paraqesim të mirat dhe të metat e rekursionit:

Të mirat e rekursionit janë:


• një qasje e natyrshme për disa tipe problemesh;
• një kuadër konceptual interesant;
• një zgjidhje intuitive e disa problemeve të vështirë.

Të metat e rekusionit janë:


• algoritmet rekursive përdorin një sasi të madhe kujtese të kompjuterit, e cila zmadhohet
me zmadhimin e përmasave të problemit;
• algoritmet rekursivë mund të jenë konfuzues në zhvillim dhe të ndërlikuar në debug-im e
tij;
• kërkojnë një mënyrë të ndryshme të menduari (por a është e metë kjo!).

4.5 Përmbledhje

• rekursioni është një metodë që zgjidh një problem duke e zvogëluar atë në probleme më
të vegjël, të ngjashëm me vetveten;
• rekursioni ofron një alternativë ndaj cikleve mbasi konsiderohet më elegant dhe me më
pak gabime meqenëse zgjidhja e problemit është e njëjtë me përcaktimin e tij;
• rekursioni funksionon në dy faza: Në fazën e parë, llogaritet çfarë është e mundur dhe
mbahet shënim për ato që nuk mundet të llogariten. Në fazën e dytë, ndodh përftimi i
vlerave të pamundura që të llogariteshin më parë, derisa të përfundojë algoritmi;
• çdo funksion rekursiv duhet të ketë një hap zvogëlimi rekursiv, i cili zgjidh problemin në
funksion të të njëjtit problem me një përmasë më të vogël dhe një ose disa raste
përfundimi të cilat zgjidhin atë për problemet me përmasën më të vogël;
• thirrjet e njëpasnjëshme të versioneve të zvogëluara të problemit vendosen stivë deri sa të
mbrihet në rastin bazë, pas të cilit ato merren nga stiva sapo versionet e zvogëluara
kthejnë me sukses rezultatet e tyre tek thirrësi;
• çdo thirrje e stivuar krijon kopjen e saj me parametra formalë dhe kthen vlerën nga një
zonë e kujtesës e quajtur stivë;
• në ngjashmëri me problemet, që kane si rezultat një vlerë edhe problemet që nuk kthejnë
një vlerë të vetme mund të realizohen në mënyrë rekursive.
4 Probleme me algoritme rekursive | 70

4.6 Ushtrime kapitulli 4.6

1. Paraqitni me formula rekursive (jo algoritme) vargjet e mëposhtme:


a. 1, 1/2, 1/4, 1/8, 1/16,…
b. 1, -1/2, 1/4, -1/8, 1/16,…
c. 2, 0.2, 0.002, 0.002,…

2. Të shkruhen 5 termat e parë të vargjeve të mëposhtëm të përcaktuar në mënyrë rekursive:


2, 𝑛=1
a. 𝑎𝑛+1 = � 𝑎𝑛 , 𝑛 > 1
𝑎𝑛 + 1
1, 𝑛 = 1
b. 𝑎𝑛+1 = �
3𝑎𝑛 , 𝑛 > 1

3. Çfarë llogaritin funksionet rekursivë të mëposhtëm?


a. 𝑓1(𝑛) = 2 ∙ 𝑓1(𝑛 − 1) + 𝑓1(𝑛 − 1)

3 𝑛≤3
b. 𝑓2 (𝑛) = � 3 ∗ 𝑓 (𝑛 − 1) 𝑛≥4
2

0 𝑛=0
c. 𝑓3 (𝑛) = �𝑓 (𝑛 + 1) ∗ 𝑓 (𝑛) 𝑛>0
3 3

4. Të hartohet një algoritëm, që kur jepet shuma e një depozite dhe interesi vjetor, të mund
të llogaritë se sa do të bëhet shuma e depozitës pas n vitesh.

5. Të hartohet një algoritëm rekursiv dhe një algoritëm iterativ që llogarit termin e ntë të
vargut {-4, 1, 6, 11, …}.

6. Të hartohet një algoritëm rekursiv dhe një algoritëm iterativ që llogarit termin e ntë të
vargut {3, 9, 27, 81, ...}.

7. Të hartohet një algoritëm rekursiv dhe një algoritëm iterativ që llogarit shumën 𝑆=
∑𝑛𝑖=1(𝑎 − 𝑖/𝑎)

8. Të hartohet një funksion rekursiv që kur jepen dy numra të plotë m dhe d, llogarit
shumën e të gjithë numrave të plotë të përfshirë ndërmjet tyre (pa përfshirë kufijtë).

9. Çfarë afishon algoritmi i mëposhtëm kur n është një numër i plotë pozitiv?

// Algoritmi nuk dihet rezultati


// Të dhëna: n > 0, natyror
// Rezultati: ?

mister(n) {
if (n = 0)
then print ("Fund")
else { print (n);
return mister(n – 1)
}
4 Probleme me algoritme rekursive | 71

10. Të hartohet një algoritëm rekursiv që llogarit termin e ntë të vargut të përcaktuar nga
kushtet: a0 = 1, a1 = 2, a2 = 3 dhe an = an-1 + an-2 + an-3 për n = 3, 4, 5…

11. Të hartohet një algoritëm rekursiv që kontrollon nëse një numër i dhënë natyror n ≥ 2
është numër i thjeshtë?

Udhëzim. Të bazohemi tek idea që të kontrollojmë numrat nga 2 deri tek √n e numrit të
dhënë. Nëse mbetja e pjesëtimit është 0 do të thotë që numri nuk është i thjeshtë. Nëse
mbetja e pjesëtimit është e ndryshme nga zero vazhdojmë me numrin pasardhës (me
kusht që të jetë më i vogël se √n ).

12. Të afishohen vlerat e një tabele me n elemente duke përdorur instruksionin print .

13. Në një tabelë janë vendosur n karaktere. Të hartohet një algoritëm rekursiv që llogarit se
sa herë shfaqet karakteri c në tabelë.

14. Në një tabelë janë vendosur n karaktere. Kërkohet të hartohet një algoritëm rekursiv që i
vendos karakteret në radhitjne e anasjelltë (Për shembull, përmbajtja “ABC”, do të
kthehet në “CBA”.

15. Në një tabelë janë vendosur n karaktere. Të hartohet një algoritëm që kthen si përgjigje
true nëse karakteret formojnë një palindromë dhe false në të kundërtën (një fjalë apo
fjali quhet palindromë nëse kur lexohet nga e majta në të djathtë po ashtu kur lexohet nga
e djathta në të majtë, kuptimi është i njëjtë, për shembull, fjala “radar” është
palindromë).

16. Jepet një tabelë A me n numra të plotë. Të hartohet një algoritëm rekursiv që gjen vlerën
më të vogël.

17. Hartoni një algoritëm që gjen vlerën më të madhe, në një tabelë të dhënë me numra të
plotë të përmasës n.

18. Të hartohet një algoritëm rekursiv që llogarit shumën e n elementeve të vendosur në një
tabelë a.

19. Të hartohet një algoritëm rekursiv që kopjon një tabelë a me n elemente në një tabelë
tjetër b me po n elemente.

20. Jepet një tabelë me n elemente. Të hartohet një algoritëm që llogarit më të voglën nga
diferencat në vlerë absolute ndërmjet një elementi dhe paraardhësit të tij (duke përjashtuar
të parin).

21. Jepet një tabele me n elemente. Të hartohet një algoritëm që llogarit më të madhen nga
shumat ndërmjet një elementi dhe pasardhësit të tij (duke përjashtuar të fundit).
5 Analiza e efektshmërisë së algoritmeve | 72

5 Analiza e efektshmërisë së algoritmeve

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të kuptojë nevojën e analizës së efektshmërisë së algortimeve;


• të njohë dhe të përshkruajë modelin teorik të analizës së algoritmeve;
• të identifikojë faktorët objektivë nga varet koha e ekzekutimit të algoritmeve;
• të kuptojë shënimet e Landaut
• të njohë dhe të përdorë analizën asimptotike të efekshmërisë kohore të algoritmeve;
• të shprehë mangësitë e analizës asimptotike
• të analizojë efektshmërinë kohore të algoritmeve iterativë;
• të analizojë efektshmërinë kohore të algoritmeve rekursivë;
• të njohë analizën empirike të efektshmërisë kohore të algortimeve

Për të kryer një punë gjëja më e rëndësishme është që të hartohet një algoritëm korrekt.
Megjithatë, ndonjëherë, megjithëse një algoritëm mund të jetë korrekt, ai mund të mos
përdoret në rast se kërkon shumë kohë që të ekzekutohet. Për shembull, algoritmi i kërkimit
linear është korrekt por çfarë ndodh nëse tabela përmban 1010 elementë? Edhe nëse një
element i tabelës përpunohet në 10 −6 sekonda, atëherë do të duheshin 10,000 sekonda ose
afërsisht 3 orë për të kërkuar një element. Kështu që e rëndësishme është që algoritmi të jetë
korrekt dhe i efektshëm. Të kuptuarit e kohës së ekzekutimit është gjithashtu i rëndësishëm
për të krahasuar efektshmërinë e dy algoritmeve të ndryshëm për të njëjtin problem.

Në këtë kapitull do të trajtohet analiza e efektshmërisë së algoritmeve. Analiza e algoritmeve


(algorithm analysis) është degë e shkencës së informatikës që studjon performancën e
algoritmeve. Qëllimi praktik i analizës së algoritmit është parashikimi i performancës së
algoritmeve të ndryshëm për marrë vendimet e duhura në hartimin e programit përkatës për
problemin në shqyrtim.

Objektivat e këtij leksioni janë: (i) të kuptojmë se çfarë është efektshmëria (efficiency) e
algoritmeve; (ii) të paraqesim mjetet me anë të cilave ajo analizohet dhe (iii) të zhvillojë disa
aftësi për të analizuar efektshmërinë e algoritmeve, domethënë, që kur jepen dy a më shumë
algoritme për të njëjtin problem të jemi në gjendje të përcaktojmë se cili është më i
efektshëm.

Në vitet 50-60 të shekullit të kaluar, shumë matematikanë dhe informatikanë, krijuan fushën
e analizës së algoritmeve duke dhënë kontributin e tyre. Një rol të veçantë, ka luajtur Donald
Knuth, me veprën e tij me tre volume, të quajtur “The Art of Computer Programming”, në të
cilën hodhi bazat e analizës së algoritmeve.

5.1 Kuadri i analizës

Në këtë cikël të leksioneve ne do të merremi kryesisht me kohën e ekzekutimit të algoritmit.


Por në raste të veçanta do të shqyrtojmë dhe kujtesën e nevojshme për ekzekutimin e tij.
Shpesh herë analiza e kohës së ekzekutimit quhet edhe llogaritja e kompleksitetit të
algoritmit.
5 Analiza e efektshmërisë së algoritmeve | 73

Ka shumë faktorë që përcaktojnë kohën e ekzekutimit të algoritmit. Ndërmjet tyre më të


rëndësishmit janë: (i) ndërlikueshmëria (complexity) e algoritmit, (ii) karakteristikat e të
dhënave fillestare (dataset); dhe (iii) sistemi kompjuterik (computer system) i përdorur për
ekzekutimin e programit.

Në periudhat e herëshme të llogaritjeve kompjuterike, që të dyja burimet, koha dhe hapsira,


ishin jashtëzakonisht të kufizuara. Një gjysëm shekulli të shpikjeve të pandalshme në
teknologji e kanë përmirësuar dukshëm shpejtësinë e kompjuterit dhe madhësinë e kujtesës.
Tani kujtesa e kërkuar nga algoritmi nuk paraqet problem në vetvete, por vëmë në dukje
diferencën e shpejtësisë ndërmjet kujtesës qendrore dhe asaj dytësore si dhe madhësinë e
kujtesës caché. Por ndërkaq, çështja e kohës nuk është zvogëluar me të njëjtën shkallë.
Përveç kësaj, eksperimentet kërkimore kanë treguar që për shumë probleme, ne mund të
kryejmë më shumë progres në kohë sesa në hapsirë. Prandaj, duke ndjekur traditën e shumë
teksteve shkollore, do të përqendrohemi më shumë në efektshmërinë kohore, por kuadri
analitik i paraqitur zbatohet po ashtu shumë mirë edhe për efektshmërinë hapsinore si dhe në
burimet e tjera të kompjuterit.

Për të analizuar efektshmërinë kohore të algoritmeve janë të mundshme tre modele: (i)
modeli teorik, nëpërmjet vlerësimit të numrit të veprimeve elementare që kryen algoritmi; (ii)
modeli empirik, nëpërmjet matjes së kohës së ekzekutimit me anë të orës dhe (iii) një
kombinim i dy modeleve të para. Në këtë cikël leksionesh do të trajtohet kryesisht modeli
teorik.

5.1.1 Mjedisi llogaritës në modelin teorik

Një faktor i rëndësishëm që influencon në efektshmërinë e një algoritmi është kompjuteri në


të cilin ai do të ekzekutohet. Shpejtësia e punës së një kompjuteri përcaktohen nga:

• Hardware-i (procesori, kujtesa qendrore dhe kujtesa caché, hard disku);


• Gjuha e programimit në të cilën është specifikuar algoritmi (Java, C++, Python);
• Kompiluesi/interpretuesi i gjuhës së programimit;
• Sistemi i operimit të kompjuterit (Windos, Mac OS, Unix, Linux)
• Etj.

Një analizë e detajuar e efektshmërisë së një algoritmi që do të merrte në shqyrtim të të gjithë


faktorët e sapo përmendur do të ishte shumë e vështirë dhe një sipërmarrje kohëhumbur. Për
më tepër rezultatet e një analize të tillë nuk i rezistojnë kohës. Ritmi i shpejtë i ndryshimit në
teknologji do të thotë që rezultatet e analizave të tilla ka pak të ngjarë që të zbatohen në
gjeneratat e ardhshme të hardware-it dhe software-it. Prandaj në modelin teorik nuk merret në
konsideratë kompjuteri konkret.

Në modelin teorik, për të shmangur një kompjuter konkret, është krijuar një kompjuter
hipotetik i quajtur Makinë me Akses të Rastit (Random Access Machine, RAM). Ky
kompjuter ka vetëm një procesor dhe një kujtesë qendrore të pakufizuar. Në këtë kompjuter
nuk ka kujtesë të tipit caché apo hard disk. Ky kompjuter ka aftësinë të marrë të dhëna dhe të
nxjerrë rezultate. Në kompjuterin RAM, instruksionet ekzekutohen njëri pas tjetrit dhe pa
konkurencë ndërmjet tyre.
5 Analiza e efektshmërisë së algoritmeve | 74

Supozohet se procesori i kompjuterit RAM është i aftë të kryejë veprimet elementare që janë
të zakonshme në kompjuterat e sotëm si:

• katër veprimet aritmetike themelore;


• veprimet e krahasimit dhe veprimet logjike;
• veprimin e vlerëdhënies; aksesin në një element të tabelës,
• veprimin e leximit të variablave të thjeshtë;
• veprimin e printimit të mesazheve apo variablave të thjeshtë;
• veprimin e thirrjes së funksionit;
• veprimin e kthimit të rezultatit nga funksioni;
• mbetjen e pjesëtimit;
• pjesëtimin e plotë;
• llogaritjen e pjesës së plotë të poshtme dhe të sipërme të një numri;
• veprime të zhvendosjeve të biteve;
• etj.

Veprimet e mësipërme përbëjnë atë që do ta quajmë bashkësia e veprimeve elementare


(primitivë) ekzekutuese. Në kompjuterat realë secili nga veprimet elementare kryhet brenda
një kohe konstante shumë të vogël. Ndërkaq për kompjuterin hipotetik RAM do të
supozohet se secili nga veprimet elementare shpenzon ekzaktësisht 1 njësi kohore.
Theksojmë se instruksioni ciklik nuk konsiderohet si veprim elementar. Ai përbëhet nga
veprime elementare numri i të cilave varet nga përmasa e të dhënave, n.

Modeli teorik nuk është një model i përsosur. Ai është një model i thjeshtuar i mënyrës së
veprimit të kompjuterave realë. Dyshimi kryesor ndaj tij është se ai duke qenë kaq i thjeshtë a
do të jetë në gjendje të japë përfundime dhe analiza të besueshme në raste konkrete, praktike.
Për shembull, aktualisht, shumëzimi i dy numrave kërkon më shumë kohë se mbledhja e dy
numrave në pjesën më të madhe të procesorëve, gjë që realisht bie ndesh me supozimin bazë.
Koha e aksesit në kujtesë ndryshon shumë në varësi të faktit se ku janë vendosur të dhënat në
kujtesën caché apo në hard disk. Pavarësisht këtyre kundërshtive, modeli teorik, është një
model mjaft i mirë për të kuptuar mënyrën e sjelljes së një algoritmi në një kompjuter real. Ai
vë në dukje sjelljen e algoritmit në kompjuterat realë ndërkaq është edhe e lehtë të punohet
me të. Ne do ta përdorim këtë model mbasi ai është i dobishëm në praktikë.

Çdo model ka një fushë mbulimi ku ai është i vlefshëm. Le të marim për shembull një model
që e konsideron tokën si të sheshtë. Mund të kundërshtohet duke thënë se është model i keq
përderisa toka nuk është e sheshtë. Megjithatë, kur hapen themelet e pallateve, modeli
llogaritës mbështetet në hipotezën se toka është e sheshtë. Më tej, është shumë më lehtë të
manipulohet një model i sheshtë i tokës megjithëse nuk është e tillë.

E njëjta situatë ndodh edhe me modelin teorik të llogaritjeve. Ne bëjmë një thjeshtim, që në
përgjithësi është i dobishëm. Është tepër e vështirë të hartojmë një algoritëm të tillë, që
modeli teorik të na jepte rezultate krejtësisht të gabuara, shumë më të mira apo shumë më të
këqia në praktikë sesa ato që parashikon modeli. Qendrueshmëria e modelit teorik na lejon që
të analizojmë algoritmet në një rrugë të pavarur nga kompjuteri real në çdo kohë.
5 Analiza e efektshmërisë së algoritmeve | 75

5.1.2 Përmasa e të dhënave


Numëri i të dhënave ose thënë ndryshe përmasa e problemit është një nga karakteristikat e të
dhënave nga e cila varet efektshmëria e një algoritmi të caktuar. Numri i të dhënave të një
algoritmi nuk është e thënë që të jetë i barabartë me numrin e variablave që shërbejnë për
hyrjen e të dhënave. Për shembull, nëse një algoritëm ka për hyrje të dhënave një tabelë me n
elemente atëherë përmasa e të dhënave do të jetë n dhe jo 1. Për shembull, sa më shumë
elemente të ketë një tabelë aq më shumë zgjat llogaritja e shumës së elementeve të saj.
Prandaj, është logjike të shqyrtojmë efektshmërinë e algoritmit si funksion i një parametri, po
e quajmë n, që tregon përmasën e të dhënave të algoritmit.

Në shumë raste, zgjedhja e një parametri të tillë është e drejpërdrejtë. Për shembull, si i tillë
do të jetë përmasa e tabelës për problemet e renditjes, kërkimit, gjetjes së elementit më të
vogël, dhe të shumë problemeve të tjera që lidhen me tabelat. Për problemin e vlerësimit të
një polinomi p(x) = anxn + … + a0 të rendit n, do të jetë rendi i polinomit ose numri i
koeficentëve të tij (një më shumë se rendi i polinomit). Do të shihet se një diferencë e tillë
është krejt e parëndësishme për analizën e efektshmërisë.

Zgjedhja e një përmase të përshtatshme mund të influencohet nga veprimet që kryhen në


algoritmin në fjalë. Për shembull, cila duhet të përdoret si përmasë e të dhënave në një
algoritëm që kryen kontrollin gramatikor? Nëse algoritmi kqyr karakteret individuale të të
dhënave, ne duhet ta shprehim përmasën në funksion të numrit të karaktereve; nëse ai punon
me anë të procesimit të fjalëve, duhet të numërojmë numrin e tyre si të dhënë.

Ne duhet të bëjmë një vërejtje të veçantë për përcaktimin e përmasës së të dhënave për
probleme të tilla si kontrolli i të qënurit numër i thjeshtë për një numër të plotë pozitiv n ose
për probleme të tjera të ngjashme. Si e dhënë këtu është thjesht një numër dhe është madhësia
e këtij numri që përcakton përmasën e të dhënave. Në situata të tilla, preferohet që përmasa e
të dhënave të përcaktohet nga numri i shifrave b (biteve) në paraqitjen binare të numrit n që
shprehet me anë të formulës
b = ⌊log2n⌋ + 1 (5.1)
Kjo metrikë jep zakonisht një ide më të mirë mbi efektshmërinë e algoritmit në fjalë.

5.1.3 Mënyrat e llogaritjes të kohës së ekzekutimit


Zakonisht një algoritëm formohet nga një ndërthurje e sekuencave, zgjedhjeve dhe
përsëritjeve të cilat shprehen me anë të veprimeve elementare. Pranuam më sipër që në
kompjuterin RAM, ku do të ekzekutohet algoritmi, të gjitha veprimet elementare shpenzojnë
një sasi kohe të njëjtë të barabartë me 1 njësi kohore. Në këto kushte, për të llogaritur kohën e
ekzekutimit të algoritmit si një funksion i sasisë së të dhënave fillestare T(n), mjafton të
llogaritim numrin e veprimeve elementare të kryera nga ai. Kjo llogaritje është e mundshme
mbasi veprimet elementare mund të identifikohen në algoritmin e paraqitur me anë të
pseudokodit.

Ndër modelet e krijuara për llogaritjem e kohës së ekzekutimit në këtë cikël të leksioneve do
të përqendrohemi

• në modelin e numrimit të të gjitha veprimeve elementare;


• në modelin e numrimit të veprimit elementar më të kushtueshëm d.m.th., që shpenzon më
shumë kohë;
• në modelin asimptotik.
5 Analiza e efektshmërisë së algoritmeve | 76

Rregullat e mëposhtme do të na ndihmojnë për të kryer vlerësimin e kohës së ekzekutimit të


një algoritmi nëpërmjet numërimit të veprimeve elementare.

Rregulli 1. Numri i veprimeve elementare për strukturën e njëpasnjëshme (sekuencën).


Çdo rresht që përmban veprime të njëpasnjëshme si vlerëdhënie, llogaritja e shprehjes,
indeksime në tabela, thirrja e një procedure, kthimi i vlerës nga procedura, lexime dhe
afishime të variablave të thjeshtë, etj, kontribuon me një numër konstant veprimesh në kohën
e ekzekutimit.

Fragmenti Numri i veprimeve Lloji i veprimit


1. input (a, b); 1 lexim
2. c ← a + b; 2 mbledhje, vlerëdhënie
3. print (c) ; 1 afishim

Duke qenë se në rreshtat me numër 1 dhe 3 gjejmë nga një veprim elementar dhe në rreshtin
2 gjejmë dy veprime elementare, dhe secili instruksion ekzekutohet vetëm një herë atëherë
koha e përgjithshme e ekzekutimit të fragmentit do të jetë: T(n) = 1 + 2 + 1 = 4. Në këtë
fragment koha e ekzekutimit është një funksion konstant, që nuk varet nga sasia e të dhënave
fillestare. Sa herë që të ekzekutohet ky fragment, gjithmonë koha e ekzekutimit do të jetë 4
njësi kohore.

Nga ana tjetër nëse veprim kryesor konsiderohet mbledhja në rreshtin 2, atëherë koha e
ekzekutimit do të jetë T(n) = 1.

Rregulli 2: Numri i veprimeve elementare për strukturën zgjedhëse


Forma e plotë e një instruksioni të kushtëzuar me dy dalje është :

if (kusht) then Bllok_instruksione1 else Bllok_instruksione2

Për këtë instruksion numri i veprimeve asnjëherë nuk është më i madh sesa numri i
veprimeve për realizimin e kushtit plus më të madhin nga numri i veprimeve që përbëjnë
Bllok_instruksione1 ose Bllok_instruksione2. Është e qartë se ky vlerësim na shpie në një
mbivlerësim të kohës së ekzekutimit por asnjëherë në një nënvlerësim të saj. Le të llogarisim
koston e fragmentit të thjeshtë të mëposhtëm:

Fragmenti Numri i veprimeve Lloji i veprimit


1. if (vlera > 0) 1 krahasim
2. then { poz ← poz + 1; 2 mbledhje, vlerëdhënie
3. sh ← sh + vlera} 2 mbledhje, vlerëdhënie
4. else neg ← neg + 1; 2 mbledhje, vlerëdhënie

Në rast se kushti në rreshtin 1 është i vërtetë atëherë ekzekutohen rreshtat 2 dhe 3 që


përmbajnë 4 veprime elementare dhe në rast se kushti nuk është i vërtetë ekzekutohet rreshti
4 që përmban 2 veprime elementare. Në këtë mënyrë në rastin më të keq, numri i veprimeve
elementare do të jetë kontribut i rreshtave 1, 2 dhe 3. Koha e ekzekutimit do të jetë: T(n) =
1+2 + 2 = 5, që është një funksion konstant.

Në rast se si veprim kryesor do të konsiderohet krahasimi në rreshtin 1 atëherë koha e


ekzekutimit do të ishte K(n) = 1.
5 Analiza e efektshmërisë së algoritmeve | 77

Rregulli 3. Numri i veprimeve elementare për strukturën iterative të thjeshtë


Le të llogaritim kohën e ekzekutimit të fragmentit të mëposhtëm të realizuar me ciklin
while:

Fragmenti Veprime Përsëritje Gjithsej Lloji i veprimit


veprime
1. shuma ← 0; 1 1 1 vlerëdhënie
2. i ← 1; 1 1 1 vlerëdhënie
3. while (i ≤ n) do { 1 n +1 n+1 krahasime
4. shuma ← shuma + i; 2 n 2n mbledhje dhe vlerëdhënie
5. i ← i + 1; } 2 n 2n mbledhje dhe vlerëdhënie

T(n) = 1 + 1 + (n + 1) + 2n + 2n = 5n + 3

Koha e ekzekutimit të fragmentit të mësipërm është një funksion linear.

Nëse si veprim elementar kryesor do të konsiderohet mbledhja atëherë koha e ekzekutimit do


të shprehet si M(n) = n, që është përsëri një funksion linear.

Llogaritjet për të njëjtin fragment iterativ por të realizuar me instruksionin for në mënyrë të
përmbledhur paraqiten më poshtë:

Fragmenti Numri i veprimeve Lloji i veprimit


1. shuma ← 0; 1 vlerëdhënie
2. for i ← 1 to n do 3· n +2 vlerëdhënie, mbledhje, krahasime
3. shuma ← shuma + i; 2· n mbledhje, vlerëdhënie

dhe koha e ekzekutimit do të jetë: T(n) = 1 + 3n + 2 + 2n = 5n + 3

Për të llogaritur kohën e ekzekutimit në funksion të numrit të herëve që ekzekutohet veprimi i


mbledhjes në rreshtin 3, mund të përdorim formulën

M(n) = ∑𝑛𝑖=1 1 = n

Rregulli 4. Numri i veprimeve për struktura iterative të përfshira


Analiza e cikleve të përfshirë (nested loops) fillohet nga cikli më i jashtëm deri në ciklin më
të brendshëm. Koha e përgjithshme e ekzekutimit e një intruksioni brenda një grupi të cikleve
të përfshirë është sa koha llogaritëse e instruksionit shumëzuar me produktin e të gjithë
përmasave të të gjithë cikleve. Le të shohim fragmentin e mëposhtëm:

Fragmenti Veprime Përsëritje Gjithsej Lloji i veprimit


veprime
0. i ← 1; 1 1 1 vlerëdhënie
1. shuma ← 0; 1 1 1 vlerëdhënie
2. while (i ≤ n) do { 1 n+1 n+1 krahasime
3. j ← 1; 1 n n vlerëdhënie
4. while (j ≤ n) do { 1 n·(n +1) n·(n + 1) krahasime
5 Analiza e efektshmërisë së algoritmeve | 78

5. shuma ← shuma + 1; 2 n·n 2·n2 mbledhje; vlerëdhënie


6. j ← j + 1}; 2 n·n 2·n2 mbledhje; vlerëdhënie
7. i ← i + 1}; 2 n 2·n mbledhje; vlerëdhënie

Duke mbledhur numrin e të gjitha veprimeve elementare funksioni i kohës së ekzekutimit do


të jetë një polinom i shkallës së dytë si më poshtë

T(n) = 1 + 1 + (n + 1) + n + n·(n + 1) + 2·n·n + 2·n·n + 2·n = 5n2 + 5n + 3

Shënim. Shpesh herë për thjeshtuar llogaritjet, duke qenë se nuk influencon ndjeshëm në
kohën e llogaritjeve, kontrolli i (n + 1)-të për daljen nga cikli mund të mos merret në
konsideratë.

Duke konsideruar veprim më të rëndësishëm mbledhjen në rreshtin 5, numri i tyre do të jetë


M(n) = n2 ndërsa duke konsideruar veprim më të rëndësishëm krahasimin në rreshtin 4, numri
i krahasimeve do të jetë K(n) = ∑𝒏𝒊=𝟏 ∑𝒏+𝟏
𝒋=𝟏 1 = n(n+1) = n +n.
2

Në vazhdim, do të përqendrohemi kryesisht tek veprimi elementar më i kushtueshëm për të


analizuar efektshmërinë kohore të algoritmeve. Si rregull, nuk është e vështirë të identifikohet
veprimi elementar kryesor i një algoritmi. Zakonisht, veprimi që shpenzon më shumë kohë,
është në ciklin më të brendshëm të algoritmit. Si shembull, pjesa më madhe e algoritmeve të
renditjes punojnë duke krahasuar elementet e tabelës me njëri tjetrin; për algoritme të tillë,
veprimi kryesor është është një krahasim elementesh. Një shembull tjetër, algoritmet për
probleme matematikore përfshijnë një ose disa nga katër veprimet aritmetike: mbledhje,
zbritje, shumëzim dhe pjesëtim. Nga këta katër veprime, konsumuesi më i madh i kohës është
pjesëtimi, pasuar nga shumëzimi dhe pastaj mbledhja dhe zbritja, nga të cilët dy të fundit
praktikisht konsiderohen të njëjtë. 4

Le të shohim tani disa zbatime interesante të modelit të krijuar.

1. Le të jetë copEl koha e ekzekutimit të veprimit elementar kryesor në një kompjuter të


caktuar, e shprehur në një nga njësitë matëse të kohës (sekonda, milisekonda apo
nanosekonda) dhe le të jetë C(n) numri i herëve që ky veprim ka nevojë të ekzekutohet për
këtë algoritëm. Atëherë ne mund të vlerësojmë kohën e ekzekutimit T(n) të një programi që
zbaton këtë algoritëm në këtë kompjuter me anë të formulës

T(n) ≈ copElC(n)

Sigurisht që kjo formulë duhet të përdoret me kujdes. Numri C(n) nuk përmban asnjë
informacion për veprimet e tjera që nuk janë kryesore, dhe në fakt, vetë numërimi shpesh
kryhet vetëm me përafërsi. Për më tepër, konstantja copEl është gjithashtu një vlerë e përafërt,
vlera e së cilës nuk është gjithmonë e lehtë për t’u përcaktuar. Megjithatë, për vlera të n
shumë të madha, formula mund të japë një vlerësim të arsyeshëm të kohës së ekzekutimit të
algoritmit.

4
Në disa kompjutera, shumëzimi nuk shpenzon më shumë kohë se mbledhje apo zbritja
5 Analiza e efektshmërisë së algoritmeve | 79

2. Gjithashtu ky model bën të mundshëm t’u përgjigjemi pyetjeve të tilla si “Sa herë më
shpejt do të mund të ekzekutohet ky algoritëm në një kompjuter që është 10 herë më i shpejtë
se ai që kemi?” Përgjigja është e qartë, 10 herë.

3. Duke supozuar se C(n) = ½ n2 , sa më shumë do zgjasë ekzekutimi i algoritmit nëse


dyfishojmë sasinë e të dhënave? Përgjigja do të jetë katër herë më shumë

1 2
𝑇(2𝑛) 𝑐𝑜𝑜 𝐶(2𝑛) 2 (2𝑛)
= = =4
𝑇(𝑛) 𝑐𝑜𝑜 𝐶(𝑛) 1 2
𝑛
2

Vëmë në dukje se ne ishim të aftë t’i përgjigjemi pyetjes të fundit pa njohur vlerën aktuale të
cop; ajo thjesht eliminohet në raportin e krijuar. Gjithashtu vëmë në dukje që ½, faktori
shumëzues në formulën për numrin C(n), gjithashtu eliminohet.

4. Duke shënuar me csh kohën e veprimit elementar më të shpejtë, cng kohën e veprimit
elementar më të ngadaltë dhe T(n) numrin e veprimeve elementare të llogaritura në mund të
përftojmë një fushështrirje të kohës të ekzekutimit të algoritmit, K(n),

T(n) csh ≤ K(n) ≤ cngT(n)

5.1.4 Varësia e efektshmërisë nga cilësitë e veçanta të të dhënave

Më sipër thamë se është e arsyeshme që të masim efektshmërinë e një algoritmi si një


funksion i një parametri që tregon sasinë e të dhënave. Për shembull, nëse duam të llogaritim
shumën e elementeve të një tabele me 4 elemente, duke përdorur një cikël, pavarësisht
vlerave konkrete të saj gjithmonë do të kryhen 28 veprime elementare (10 vlerëdhënie, 8
mbledhje, 5 krahasime, 4 aksese në tabelë, 1 kthim vlere). Pra në këtë problem numri i
veprimeve elementare nuk varet nga të dhënat elementare. Në varësi të përmasës n të tabelës
koha e ekzekutimit do të jetë gjithmonë T(n) = 6n + 4.

Por ka shumë algoritme, për të cilat koha e ekzekutimit varet jo vetëm nga sasia e të dhënave,
por gjithashtu edhe nga vetë vlera e të dhënave ose siç thuhet ndryshe nga cilësitë e veçanta
të të dhënave. Për shembull, nëse duam të llogaritim vlerën më të madhe në një tabelë me 4
elemente, duke përdorur një cikël, numri i veprimeve elementare mund të ndryshojë nga rasti
në rast. P.sh., për tabelën [1, 2, 3, 4] numri i veprimeve elementare do të jetë 23 (8
vlerëdhënie, 7 krahasime, 7 aksese në tabelë, 3 mbledhje dhe 1 kthim vlere) ndërsa për
tabelën [4, 3, 2, 1] numri i veprimeve elementare do të jetë 20 (5 vlerëdhënie, 7 krahasime, 4
aksese në tabelë, 3 mbledhje dhe 1 kthim vlere). Në rast se kemi një tabelë me n elemente ku
pasardhësi është më i madh se pararadhësi atëherë koha e ekzekutimit do të jetë T(n) = 7n - 2.

Le të konsiderojmë një problem tjetër, kërkimin linear, që e kemi parë në një nga leksionet e
kaluara. Ai është një algoritëm i thjeshtë që kërkon për një vlerë të caktuar k në një tabelë
prej n elementesh me anë të kontrollit të njëpasnjëshëm të elementeve derisa ose ndodh një
përputhje me vlerën e kërkuar ose të gjithë elementet janë kontrolluar dhe vlera nuk gjendet.

Algoritmi 5.1 Kërkimi linear i thjeshtë


// Të dhëna: tabela a[0:n–1] me n elemente dhe një vlerë k e caktuar
// Rezultati: një indeks i, i tillë që k = a[i] ose –1, nëse vlera k nuk gjendet në
5 Analiza e efektshmërisë së algoritmeve | 80

// tabelë

kerkoElement1(n, a, k) {
i ← 0;
while (i < n) do
{ if A[i] = k
then return i; // kthehet pozicioni i vlerës k
i ← i + 1;
}
return –1
}

Është e qartë se koha e ekzekutimit të këtij algoritmi do të varet nga vlera e k-së për të njëjtën
sasi me të dhëna n. Në rast se vlera k nuk ndodhet fare në tabelë ose ndodhet në pozicionin e
fundit të saj atëherë do të duhen të kryhen n krahasime për të arritur në këtë përfundim. Në
këto dy raste algoritmi bën numrin më të madh të mundshëm të krahasimeve. Shkencëtarët e
informatikës këtij skenari i kanë gjetur një emër të lezetshëm duke e quajtur analiza në rastin
më të keq (worst-case analysis) të të cilësive të veçanta të të dhënave. Le të shënojmë me
K(n) numrin e krahasimeve si funksion të parametrit n dhe për të identifikuar rastin që ndodh
i vëmë nënshkrimin keq, në formën Kkeq(n) = n.

Efektshmëria në rastin më të keq e një algoritmi është efektshmëria e tij për rastin e të
dhënave të përmasës n, për të cilën algoritmi ekzekutohet në kohën më të madhe, ndër të
gjitha të dhënat me po këtë përmasë.

Rruga për përcaktuar efektshmërinë në rastin më të keq është e drejtpërdrejtë: analizohet


algoritmi për të parë se për çfarë lloj të dhënash prodhohet vlera më e madhe e numrit të
veprimit kryesor K(n) ndër të gjitha të dhënat e përmasës n dhe pastaj llogaritet vlera e rastit
më të keq Kkeq(n). (Për kërkimin linear, përgjigja është e qartë. Metodat për të administruar
situata më pak të qarta do të shpjegohen në seksionet pasardhëse të këtij kapitulli.)

Është e qartë që analiza e rastit më të keq furnizon një informacion shumë të rëndësishëm
mbi efektshmërinë e algoritmit duke kufizuar kohën e tij të ekzekutimit nga sipër. Me fjalë të
tjera ai siguron që për çfarëdo lloj rasti të të dhënave të përmasës n, koha e ekzekutimit nuk
do ta tejkalojë Kkeq(n), që është koha e ekzekutimit për të dhënën në rastin më të keq. Është e
rëndësishme të njihet koha më e keqe mbasi mund të ketë procese kritike dhe ne duhet ta
njohim këtë kohë.

Krahas efektshmërisë në rastin më të keq kemi edhe efektshmërinë në rastin më mirë të një
algoritmi. Kjo i përket rastit kur algoritmi, për çdo të dhënë të përmasës n ekzekutohet më
shpejt sesa me të gjitha të dhënat e mundshme të po kësaj përmase.

Prandaj, ne mund ta analizojmë efektshmërinë në rastin më të mirë si më poshtë. Së pari,


përcaktojmë llojin e të dhënave për të cilat numri i veprimeve të veprimit kryesor K(n) do të
jetë më i vogli i mundshëm ndër të gjitha të dhënat e mundshme të përmasës n. (Vëmë në
dukje se rasti më i mirë nuk do të thotë e dhëna më vogël; ai nënkupton të dhënën me
përmasë n, për cilën algoritmi ekzekutohet më shpejt.) Pastaj, përcaktojmë vlerën e K(n) për
këtë të dhënë më të përshtatshme. Për shembull, të dhënat më të mira për kërkimin linear janë
tabelat e përmasës n, elementi i parë i të cilave është i barabartë me vlerën që kërkohet; si
rrjedhim Kmirë(n) = 1 për këtë algoritëm.
5 Analiza e efektshmërisë së algoritmeve | 81

Analiza e efektshmërisë në rastin më të mirë është larg të qënurit aq e rëndësishme sa


efektshmëria e rastit më të keq. Por nga ana tjetër nuk është plotësisht e pavlerë, përkundrazi.
Edhe pse nuk presim që të kemi rastet më të mira të të dhënave, mund të jemi në gjendje të
përfitojmë nga fakti që për disa algoritme, një pjesë e mirë e të dhënave mund të jenë afër
rastit më të mirë. Për shembull, ekziston një algoritëm renditjeje (Insertion sort), që do ta
studjojmë më vonë, për të cilin rasti më i mirë i të dhënave është kur tabela është tashmë e
renditur dhe algoritmi në këtë rast punon shumë shpejt. Dhe nga ana tjetër rasti i
efektshmërisë më të mirë, keqësohet vetëm pak kur të dhënat janë pothuajse të renditura.
Prandaj një algoritëm i tillë mund të jetë mjaft i përshtatshëm për të zbatime që kanë të dhëna
të tilla. Dhe, sigurisht, nëse efektshmëria në rastin më të mirë e një algoritmi është e
pakënaqshme, atëherë duhet ta braktisim atë algoritëm pa qenë nevoja për analiza të tjera.

Nga diskutimi i deritanishëm, duhet të jetë e qartë se as rasti më i keq dhe as rasti më i mirë
nuk japin informacion mbi sjelljen e algoritmit për të dhëna në raste “tipike” apo “të rastit”.
Ky është informacioni që efektshmëria në rastin mesatar (average-case efficiency) përpiqet të
japë. Ne nuk do ta trajtojmë efektshmërinë në rastin mesatar mbasi kërkon njohuri nga teoria
e probabilitetit.

Në leksionet në vazhdim, objektivi ynë kryesor do të jetë kryesisht përcaktimi i kohës së


ekzekutimit në rastin më të keq (më të pafavorshëm), domethënë, koha më e madhe e
ekzekutimit për të dhëna të përmasës n. Argumentat për këtë zgjedhje janë:

• koha e ekzekutimit e lidhur me rastin më të keq është një kufi i sipërm i kohës së
ekzekutimit e lidhur me një rast të çfardoshëm. Njohja e kësaj vlere na jep mundësinë që
të kemi sigurinë se algoritmi nuk do të shpenzojë kurrë një kohë më të madhe se ky kufi.
Pra nuk është e nevojshme të bëjmë kërkime që të gjejmë një rast më të keq se ky (më
keq se kaq nuk ka);
• për disa algoritme rasti më i keq ndodh shumë shpesh;
• nuk janë të pakta rastet kur rasti më i mirë është po aq i keq sa rasti më i keq.

Ndërsa në aspektin praktik, morali i këtij diskutimi është:

• në rast se njohim mënyrën e shpërndarjes të të dhënave fillestare atëherë preferohet që


analiza të mbështetet në rastin mesatar;
• në rast se nuk njohim mënyrën e shpërndarjes së të dhënave fillestare atëherë duhet që
analiza të mbështetet në rastin më të keq të organizimit të të dhënave fillestare.

5.2 Analiza asimptotike

Nëse e quajmë “të saktë” llogaritjen e kohës së ekzekutimit me anë të numërimit të


veprimeve elementare, brenda kuadrit të paraqitur më lart, ajo nuk siguron përparësi shtesë në
krahasim me një analizë të përafërt që do të parashtrojmë më poshtë. Analiza e saktë jep
funksionin e saktë polinomial që lidh përmasën e të dhënave me kohën e ekzekutimit, ndërsa
analiza e përafërt jep fuqinë e përmasës nga e cila varet koha ekzekutimit. Për shembull koha
e saktë e ekzekutimit të një algoritmi mund të jetë T(n) = 2⋅n 2 + 2⋅n + 3. Në këtë rast koha e
përafërt e ekzekutimit do të jetë T(n)= 2⋅n 2. Pra, fuqia më e lartë e n është ajo që ka peshë me
kalkulimin e kohës së përafërt të ekzekutimit. Bile edhe konstantet që ndodhen tek termi me
5 Analiza e efektshmërisë së algoritmeve | 82

shkallë më të lartë nuk kanë rëndësi në sjelljen e funksionit për vlera të mëdha të n-së ose
ndryshe sjelljen asimptotike të funksionit.

Duke shprehur kohën e ekzekutimit të algoritmit si një funksion të përmasës së problemit


bëhet e mundshme që të krahasohen algoritmet e ndryshëm për të njëjtin problem në mënyrë
asimptotike. Për shembull, nëse dimë që koha e ekzekutimit të algoritmit A është
proporcionale me përmasën n të të dhënave dhe koha e ekzekutimit të algoritmi B tendon të
jetë proporcional n2 atëherë është e arsyeshme që të presim që algoritmi A të jetë më i
shpejtë se algoritmi B për vlera të mëdha të n-së.

Le të supozojmë se i kemi analizuar dy algoritmet dhe kemi përcaktuar kohën e ekzekutimit


të tyre në funksion të përmasës së problemit si 200n + 1 për algoritmin A dhe 2n2+2n+3 për
algoritmin B.

Tabela e mëposhtme paraqet kohën e ekzekutimit për këta algoritme për përmasa të
ndryshme të problemit.

Koha e ekzekutimit
Përmasa e problemit
n Algoritmi A Algoritmi B Raporti i kohëve të
TA(n)=200n+1) TB(n)=2n2+2n+3 ekzekutimit
TA(n)/TB(n)
1 201 7 28.714
10 2,001 223 8.973
100 20,001 20,203 0.990
1000 200,001 2,002,003 0.100
10000 2,000,001 200,020,003 0.010
100000 20,000,001 20,000,200,003 0.001

Për n = 10, algoritmi A duket shumë i keq; ai kërkon 28 herë më shumë kohë sesa algoritmi
B. Por për n = 100 ata janë pothuajse të njëjtë dhe për vlera më të mëdha të n-së algoritmi A
është shumë më i mirë.

Arsyeja kryesore është se për vlera të mëdha të n-së, çdo funksion që përmban një term n2 do
të rritet më shpejt sesa një funksion, termi drejtues i të cilit është n. Termi drejtues është termi
me fuqinë më të madhe.

Për algoritmin A, termi drejtues ka një koeficient 200, dhe kjo është arsyeja se pse algoritmi B
performon më mirë sesa algoritmi A për n të vogla. Por pavarësisht koeficentëve, gjithmonë
do të ketë një vlerë n për të cilën n2 > bn.
I njëjti argument zbatohet edhe për termat jo drejtues. Bile edhe nëse koha e ekzekutimit të
algoritmit A do të ishte n + 1000000 ai do të ishte ende më i mirë se algoritmi B për n mjaft të
mëdha.

Në përgjithësi ne presim që një algoritëm me një term drejtues të vogël të jetë një algoritëm i
mirë për probleme me përmasa të mëdha, por për probleme të vegjël, mund të ketë një pikë
kthese ku një algoritëm tjetër të jetë më i mirë. Pozicioni i pikës së kthesës varet nga detajet e
algoritmeve, të dhënat dhe hardware-i, kështu që ato zakonisht nuk merrem parasysh për
qëllimet e analizës asimptotike, por kjo nuk do të thotë që ato të harrohen.
5 Analiza e efektshmërisë së algoritmeve | 83

Nëse të dy algoritmet kanë të njëjtin rend për termin drejtues, është e vështirë të pohosh se
cili është më i miri; përsëri përgjigja varet nga detajet. Kështu për qëllimet e analizës
algoritmike, funksionet me të njëjtin term drejtues quhet të njëvlershëm, bile edhe nëse kanë
koeficiente të ndryshëm. Për shembull 2n, 1000n dhe n + 1 kanë të njëjtin rend rritjeje, ata
futen në të njëjtën klasë, në klasën lineare.

Për vlera të mëdha të n-së, është rendi i rritjes së funksionit që influencon; për t’u bindur për
këtë mjafton të shohim tabelën 5.1, e cila përmban vlerat e disa funksioneve me rëndësi të
veçantë në analizën e algoritmeve.

Tabela 5.1 Vlerat (disa të përafërta) e disa funksioneve të rëndësishëm për analizën e
algoritmeve

n log2n n n log2n n2 n3 2n n!
10 3.3 101
3.3∙101
102
10 3
10 3
3.6∙106
102 6.6 102 6.6∙102 104 106 1.3∙1030 9.3∙10157
103 10.0 103 1.0∙103 106 109
104 13.0 104 1.3∙104 108 1012
105 17.0 105 1.7∙105 1010 1015
106 20.0 106 2.0∙106 1012 1018

Madhësisa e numrave në tabelën 5.1 ka një domethënie të thellë për analizën e algoritmeve.
Funksioni që rritet më ngadalë ndërmjet tyre është funksioni logaritmik. Në fakt, ai rritet aq
ngadalë, saqë ne duhet ta prisnim që një program, që është zbatimi i një algoritmi, në të cilin
veprimi kryesor rritet në mënyrë logaritmike me rritjen e përmasave të problemit, të
ekzekutohet praktikisht shumë shpejt për të dhëna të përmasave reale. Gjithashtu, vëmë në
dukje se, megjithëse numri i veprimeve varet nga baza e logaritmit, formula e transformimit
loga n = loga b logb n
bën të mundur kalimin nga një bazë në tjetrën, duke e lënë numrin e veprimeve përsëri
logaritmik por me një faktor shumëzues konstant. Kjo është arsyeja se pse nuk e përdorim
bazën e logaritmit dhe shkruajmë thjesht log n në situatat kur ne interesohemi thjesht për
funksionin e rendit të rritjes me afërsinë e një faktori konstant.

Nga ana tjetër të spektrit ndodhen funksioni eksponencial 2n dhe funksioni faktorial n! Të dy
këta funksione rriten aq shpejt saqë vlerat e tyre bëhen jashtëzakonisht të mëdha edhe për
vlera shumë të vogla të n. (Kjo është arsyeja sepse nuk janë përfshirë vlerat e tyre në tabelën
5.1 për n > 102) Për shembull, do të duheshin rreth 4∙1010 vite, për një kompjuter që bën një
trillion (1012) veprime për sekondë, për të ekzekutuar 2100 veprime. Megjithëse kjo është
pakrahasueshmërisht e shpejtë nëse duhet të ekzekutoheshin 100! veprime, që është 4.5 bilion
(4.5∙109) vite, mosha e vlerësuar e planetit Tokë. Ekziston një diferencë e mahnitëshme
ndërmjet rendit të rritjes të funksionit 2n dhe n!, por sidoqoftë të dyve shpesh i u referohemi si
funksione me rritje eksponenciale (ose thjesht eksponencial) pavarësisht faktit që, duke folur
rigorozisht, vetëm i pari është i tillë. Gjëja e fundit që do të thonim, që është e rëndësishme të
kujtohet është që “Algoritmet që kërkojnë një numër eksponencial veprimesh praktikisht
vlejnë vetëm për zgjidhjen e problemeve me përmasa shumë të vogla”.

Një tjetër mënyrë për të vlerësuar diferencën cilësore ndërmjet rendeve të rritjes të
funksioneve të dhënë në tabelën 5.1 është që të konsiderojnë përgjigjen e tyre, le të themi kur
dyfishohet vlera e argumentit të tyre, n. Funksioni log2 n, rritet në vlerë vetëm 1 njësi (mbasi
5 Analiza e efektshmërisë së algoritmeve | 84

log2 (2n) = log2 2 + log2 n = 1 + log2 n); funksioni linear dyfishohet, funksioni linearitmik n
log2 n rritet lehtë më tepër se dy herë; funksioni kuadratik n2 dhe funksioni kubik n3
katërfishohen dhe tetëfishohen, respektivisht (mbasi (2n)2 = 4n2 dhe (2n)3 = 8n3); vlera e 2n
ngrihet në katror (mbasi 22n = (2n)2; dhe n! rritet më tepër se kaq (po, matematikanët refuzuan
të bashkëpunojnë për të dhënë një përgjigje të saktë për n!).

Për të krahasuar dhe klasifikuar rende të tilla funksionesh, matematikanët (informatikanët)


përdorin simbolikën e Landau-t 5 ose ndryshe shënimet asimptotike: O (O e madhe-big oh), Ω
(omega e madhe), dhe Θ (theta e madhe). Në fillim, do të paraqesim kuptimin e simboleve në
mënyrë joformale, dhe pastaj, pasi të jepen disa shembuj, do të paraqiten përkufizimet
formale të tyre. Në diskutimet e mëposhtme, f(n) dhe g(n) mund të jenë funksione të
çfardoshëm jonegativë të përcaktuar në bashkësinë e numrave natyralë. Në kontekstin për të
cilin jemi të interesuar, f(n) do të jetë koha e ekzekutimit të një algoritmi (që në përgjithësi
shpreh numrin e veprimeve elementare të tij T(n)), dhe g(n) do të jetë një funksion i thjeshtë
për t’u krahasuar me të si për shembull n, nlg n, n2, etj.

5.2.1 Paraqitja joformale e simboleve asimptotike

Kufiri i sipërm
Për të përshkruar sjelljes e kohës së ekzekutimit të një algoritmi kur rritet përmasa e të
dhënave përdoren disa terma. Këta terma dhe simbolet e lidhura me ta, tregojnë saktësisht se
cili aspekt i sjelljes së algoritmit po përshkruhet. Një prej tyre është kufiri i sipërm për rritjen
e kohës së ekzekutimit të algoritmit. Ai tregon shkallën më të lartë të rritjes që mund të arrijë
algoritmi.

Deklarimet mbi kufirin e sipërm të algoritmit duhet t'i bëjmë në lidhje me përmasën n të të
dhënave. Ne e matim këtë kufi të sipërm pothuajse gjithmonë në rastin më të mirë, në rastin
më të keq ose rastin mesatar. Kështu, ne nuk duhet të themi, "klasifikoni këtë algoritëm që ka
një kufi të sipërm të rritjes të tij të rendit n2". Ne duhet të themi diçka të ngjashme me, "ky
algoritëm ka një kufi të sipërm të rritjes së tij të rendit n2 në rastin më të keq".
Meqë fraza "ka një kufi të sipërm të rritjes së tij të rendit f(n)" është e gjatë dhe meqenëse
përdoret shpesh në analizën e algoritmeve, është pranuar një simbolikë e veçantë e huazuar
nga matematika, quajtur simbolika O e madhe (big-Oh). Nëse kufiri i sipërm për rendin e
rritjes së një algoritmi (le të themi në rastin më të keq), atëherë ne duhet të shkruajmë që ky
algoritëm bën pjesë në bashkësinë O(f(n)) në rastin më të keq (ose thjesht "është në O(f(n)) në
rastin më të keq"). Për shembull, nëse n2 rritet po aq shpejt sa T(n) (që është koha e
ekzekutimit të algoritmit tonë) për rastin më të keq të të dhënave, ne duhet të themi që
algoritmi është "në O(n2) në rastin e keq."

Simboli O-e madhe cilëson një kufi të sipërm. Me fjalë të tjera, simboli O-e madhe formulon
një pohim mbi sasinë më të madhe të disa burimeve (zakonisht koha) që kërkohet nga
algoritmi për të dhëna të përmasës n (në rastin më të keq, më të mirë apo mesatar).

5
Edmund Landau, 1877-1938, matematikan gjerman, që ka punuar në fushat e teorisë së numrave dhe analizës
së kompleksitetit
5 Analiza e efektshmërisë së algoritmeve | 85

Në mënyrë joformale, O(g(n)) është bashkësia e të gjithë funksioneve që e kanë rendin e


rritjes më të vogël ose të barabartë me g(n) (me afërsinë e një faktori konstant, kur n shkon në
infinit). Kështu, për të dhënë disa shembuj, pohimet e mëposhtme janë të gjitha të vërteta:

2n+1 ∈ O(n) 100n + 5 ∈ O(n2) 1


n(n - 1) ∈ O(n2)
2

n3 ∉ O(n2) 0.00001n3 ∉ O(n2) n4 + n + 1 ∉ O(n2)

Kufiri i poshtëm
Një simbol i ngjashëm është krijuar për të përshkruar sasinë më të vogël të burimeve që ka
nevojë një algoritëm për një bashkësi të dhënash. Në ngjashmëri me simbolin O-e madhe, kjo
është një masë e rendit të rritjes së algoritmit. Dhe përsëri si simboli O-e madhe jemi duke
vlerësuar burimet që kërkohen për një klasë të caktuar të të dhënave: të dhëna të përmasës n
në rastin më të keq, më të mirë apo mesatar.

Kufiri i poshtëm për një algoritëm (ose për një problem) shënohet me anë të simbolit Ω dhe
shqiptohet "Omega e madhe" ose thjesht Omega.

Shënimi Ω(g(n)), nënkupton bashkësinë e të gjithë funksioneve që e kanë rendin e rritjes të


njëjtë ose më të madh se g(n) (me afërsinë e një faktori konstant kur n shkon në infinit).
Për shembull,
1
n3 ∈ Ω(n2), n(n - 1) ∈ Ω(n2), por 100n + 5 ∉ Ω(n2)
2

Simboli Θ
Konceptet për O-e madhe dhe Ω-e madhe na japin një mundësi të përshkruajmë një kufi të
sipërm për një algoritëm (nëse mund të gjejmë një funksion për numrin më të madh të
veprimeve për një klasë të caktuar të përmasave n) dhe një kufi të poshtëm për një algoritëm
(nëse mund të gjejmë një funksion për numrin më të vogël për një klasë të përcaktuar të
përmasës n). Kur kufijt e sipërm dhe të poshtëm janë të njëjtë me afërsinë e një faktori
konstant ne e tregojmë këtë me anë të simbolit Θ-e madhe.

Shënimi, Θ(g(n)) nënkupton bashkësinë e të gjithë funksioneve që kanë rendin e rritjes të


njëjtë me g(n) (me afërsinë e një faktori konstant kur n shkon në infinit). Kështu çdo funksion
kuadratik an2 + bn + c me a > 0 bën pjesë Θ(n2), por të tillë janë, ndër një pafundësi
funksionesh të tjerë, edhe n2 + sin n dhe n2 + log n.

5.2.2 Përkufizimet formale të simboleve asimptotike


a) Shënimi O e madhe
Përkufizim 1. Thuhet se një funksion f(n) bën pjesë në bashkësinë O(g(n)), shkruhet f(n) ∈
O(g(n)), në qoftë se f(n) është i kufizuar nga sipër nga funksioni g(n) shumëzuar me një
konstante, për n të mëdha, domethënë, nëse ekziston një konstante pozitive c dhe një numër
jonegativ i plotë n0 , i tillë që

f(n) ≤ c·g(n) për çdo n ≥ n0


5 Analiza e efektshmërisë së algoritmeve | 86

(Shprehja f(n) ∈O(g(n)) lexohet: “f e n-së është O-e madhe e g-së të n-së” ose “f është O-e
madhe e g-së”).

Figura 5.1 ilustron në mënyrë grafike kuptimin e shënimit O, ku për hir të qartësisë vizuale,
n është paraqitur si një vlerë reale.

Figura 5.1 Paraqitja grafike e shënimit O-e-


madhe

Shembull. Të provohet që 100n + 5 ∈ O(n).

Me të vërtetë, për çdo n ≥ 5 do të kemi,

100n + 5 ≤ 100n + n = 101n

Kështu, si vlera për konstantet c dhe n0, që kërkon përkufizimi, ne mund të marrim
respektivisht 101 dhe 5.

Vëmë në dukje se përkufizimi na lë një farë dorë të lirë në zgjedhjen e vlerave të veçanta për
konstantet c dhe n0. Për shembull, ne mund të themi gjithashtu se për çdo n ≥ 1 do të kemi,

100n + 5 ≤ 100n + 5n = 105n,

që kënaq përkufizimin për c = 105 dhe n0 = 1.

b) Shënimi Ω
Përkufizim 2. Thuhet se një funksion f(n) bën pjesë në bashkësinë Ω(g(n)), shkruhet f(n) ∈
Ω(g(n)), në qoftë se f(n) është i kufizuar nga poshtë nga funksioni g(n), shumëzuar me një
konstante pozitive, domethënë, nëse ekziston një konstante pozitive c dhe një numër i plotë
jonegativ n0 i tillë që

f(n) ≥ c·g(n) për çdo n ≥ n0.

(Shprehja f(n) ∈ Ω(g(n)) lexohet: “f e n-së është omega e madhe e g-së të n-së” ose “f
është omega e madhe e g-së”.)

Në figurën 5.2 paraqet në mënyrë grafike kuptimi i shënimit Ω.


5 Analiza e efektshmërisë së algoritmeve | 87

Figura 5.2 Paraqitja grafike e shënimit Ω-e-madhe

Shembull. Të provohet që n3 ∈ Ω(n2).


Është e qartë se n3 ≥ n2 për çdo n ≥ 0, dhe mjafton që të zgjedhim c = 1 dhe n0 = 0.

c) Shënimi Θ
Përkufizim 3. Thuhet se një funksion f(n) bën pjesë në bashkësinë e funksioneve Θ(g(n))
(shkruhet f(n) ∈ Θ(g(n)), në qoftë se f(n) është i kufizuar nga sipër dhe nga poshtë nga
funksioni g(n) shumëzuar me dy konstante pozitive për n të mëdha, domethënë, nëse
ekzistojnë konstantet pozitive c1 dhe c2 dhe një numër i plotë pozitiv n0 i tillë që

c1 g(n) ≤ f(n) ≤ c2 g(n), për çdo n ≥ n0.

(Shënimi f(n) ∈ Θ(g(n)) lexohet: “f e n-së është theta e madhe e g-së të n-së” ose “f është
theta e madhe e g-së”.)

Figura 5.3 Paraqitja grafike e shënimit Θ-e-madhe

Shembull. Të provohet që ½ n(n – 1) ∈ Θ(n2).

Së pari, le të provojmë mosbarazimin e djathtë (kufiri i sipërm):

½ n(n – 1) = ½ n2 –½ n ≤ ½ n2 për çdo n ≥ 0.

Së dyti, le të provojmë mosbarazimin e majtë (kufirin e poshtëm):

½ n(n – 1) = ½ n2 – ½ n ≥ ½ n2 – ½ n ½ n = ¼ n2 (për çdo n ≥ 2).

Prandaj, mjafton të zgjedhim c2 = ¼ , c1 = ½ , dhe n0 = 2.


5 Analiza e efektshmërisë së algoritmeve | 88

Veti të shënimeve asimptotike


Sapo përcaktohet funksioni i kohës së ekzekutimit të një algoritmi, është mjaft e thjeshtë që të
përcaktohen shprehjet për funksionin me anë të simboleve O-e madhe, Ω dhe Θ. Nuk është e
nevojshme (kur nuk kërkohet e kundërta për qëllime mësimore) të përdoren përkufizimet
formale për analizën asimptotike. Në vend të saj mund të përdoren vetitë e mëposhtme për të
përcaktuar formën më të thjeshtë.

1. Vetia tranzitive.
Nëse f(n) ∈O(g(n)) dhe g(n) ∈ O(h(n)) atëherë f(n) ∈ O(h(n))

2. Faktori konstant.
Nëse f(n) ∈ O(kg(n)) për ndonjë konstante k > 0 atëherë f(n) ∈ O(g(n))

3. Teorema e mbledhjes. Në qoftë se T1(n) ∈ O(g1(n)) dhe T2(n) ∈ O(g2(n)), atëherë

T1(n) + T2(n) ∈ O(max{g1(n), g2(n)}).

4. Teorema e shumëzimit. Në qoftë se T1(n) ∈ O(f(n)) dhe T2(n) ∈ O(g(n)) atëherë

T1(n) · T2(n) ∈ O(f(n) · g(n))

Vetia e parë thotë që nëse ndonjë funksion g(n) është një kufi i sipërm për funksionin e kohës
së ekzekutimit, atëherë çdo kufi i sipërm për g(n) është gjithashtu një kufi i sipërm për
funksionin e kohës së ekzekutimit.

Një cilësi e ngjashme vlen edhe për shënimin Ω: nëse g(n) është një kufi i poshtëm për
funksionin e kohës së ekzekutimit, atëherë çdo kufi i poshtëm për g(n) është gjithashtu një
kufi i poshtëm për funksionin e kohës së ekzekutimit. Në mënyrë të ngjashme edhe për
shënimin Θ.

Thelbi i vetisë së dytë është se mund të mos marrim parasysh konstantet shumëzuese në
funksion kur përdorim shënimin O-e madhe. Ky rregull vlen edhe për shënimet Ω dhe Θ.

Vetia e tretë thotë që nëse një algoritëm përbëhet nga dy pjesë të njëpasnjëshme (qoftë këta
dy instruksione ose dy seksione), atëherë në konsideratë mund të merret vetëm pjesa që
shpenzon më shumë kohë. Ky rregull vlen edhe për shënimet Ω dhe Θ.

Vetia e katërt përdoret për të analizuar ciklet e thjeshtë në algoritme. Nëse disa veprime
përsëriten një numër të caktuar herësh dhe çdo përsëritje ka të njëjtën kosto, atëherë kosto e
përgjithshme është e barabartë me koston e çdo veprimi shumëzuar me numrin e herëve që
veprimi përsëritet. Ky rregull vlen edhe për shënimet Ω dhe Θ.

Duke konsideruar tre rregullat e parë në tërësi, ne mund të mos marrim parasysh të gjitha
konstantet dhe të gjithë termat e rendeve me të ulta për të përcaktuar shkallën e rritjes
asimptotike për çdo funksion të kohë së ekzekutimit. Mosmarrja parasysh e termave me rend
më të ulët është e arsyeshme kur kryhet analiza asimptotike. Termat e rendeve më të larta
tejkalojnë shpejt termat e rendeve të ulta në kontributin e tyre në koston e përgjithshme kur n
bëhet e madhe. Kështu nëse T(n) = 3n4 + 5n2 atëherë T(n) është në O(n4). Termi n2
5 Analiza e efektshmërisë së algoritmeve | 89

kontribuon relativisht pak në shpenzimin përgjithshëm të kohës së ekzekutimit. Këto rregulla


thjeshtimi do t'i përdorim kur të kryejme analizë e kohës së ekzekutimit të një algoritmi më
tej në leksione.

Rregulli i limitit
Megjithëse përkufizimet formale të shënimeve O, Ω dhe Θ janë të domosdoshme për të
provuar cilësitë e funksioneve, ata nuk përdoren shpesh për të krahasuar dy funksione të
veçantë për shkak të vështirësive relative gjatë procesit të vërtetimit.

Përdorimi i limitit është shpesh më i përshtatshëm mbasi ai përfiton nga teknikat e fuqishme
të njehsimit diferencial për llogaritjen e limiteve, të tilla si rregulli i L’Hopital-it që e kthen
llogaritjen e limitit të raportit të funksioneve në llogaritjen e limitit të raportit të derivateve të
tyre:
𝑓(𝑛) 𝑓′ (𝑛)
lim𝑛→∞ = lim
𝑔(𝑛) 𝑛→∞ 𝑔′ (𝑛)

si dhe nga formula e Stirlingut


𝑛 𝑛
𝑛! ≈ √2𝜋𝜋 � � për n mjaft të mëdha.
𝑒
Mund të ndodhin tre raste kryesore 6:

⎧ 0 𝑎𝑎ëℎ𝑒𝑒ë 𝑓(𝑛) ∈ 𝑂�𝑔(𝑛)� �𝑑ℎ𝑒 𝑓(𝑛) ∉ Θ�g(n)��


𝑓(𝑛) ⎪
lim = 𝑐>0 𝑎𝑎ëℎ𝑒𝑒ë 𝑓(𝑛) ∈ Θ�𝑔(𝑛)�
𝑛→∞ 𝑔(𝑛) ⎨
⎪ ∞ 𝑎𝑎ëℎ𝑒𝑒ë 𝑔(𝑛) ∈ Ω�𝑓(𝑛)� �𝑑ℎ𝑒 𝑓(𝑛) ∉ Θ�𝑔(𝑛)��

Më poshtë jepen tre shembuj të përdorimit të limitit për të krahasuar rendin e rritjes së dy
funksioneve të caktuar.

Shembulli 1. Të krahasohen rendet e rritjes të funksioneve 12 n(n - 1) dhe n2 (ky është një nga
shembujt e përdorur në fillim të këtij seksioni për të ilustruar përkufizimet.)

1
𝑛(𝑛 − 1) 1 𝑛2 − 𝑛 1 1 1
lim 2 2
= lim 2
= lim ( 1 − ) =
𝑛→∞ 𝑛 2 𝑛→∞ 𝑛 2 𝑛→∞ 𝑛 2

Meqenëse limiti është i barabartë me një konstante pozitive, funksionet kanë të njëjtin rend
1
rritjeje ose simbolikisht n(n - 1) ∈ Θ(n2).
2

Shembulli 2. Të krahasohet rendi i rritjes së log 2 𝑛 dhe √𝑛 (në ndryshim nga shembulli 1,
përgjigja nuk duket e menjëhershme).
1
log2 𝑛 (log2 𝑛)′ (log2 𝑒) √𝑛
lim𝑛→∞ = lim𝑛→∞ = lim𝑛→∞ 1
𝑛
= 2 log 2 𝑒 lim𝑛→∞ =0
√𝑛 �√𝑛�′ 𝑛
2 √𝑛
Meqënëse limiti është i barabartë me zero, log 2 𝑛 ka një rend rritjeje më të vogël sesa √𝑛.

6
Rasti i katërt, “limiti nuk ekziston” ndodh rrallë në praktikën aktuale të analizimit të algoritmeve. Kjo është një
arsye më shumë për të thënë kjo mënyrë e krahasimit të rendit të rritjes është më pak e përgjithshme sesa ajo që
bazohet në përkufizimet e O, Ω dhe Θ.
5 Analiza e efektshmërisë së algoritmeve | 90

Shembulli 3. Të krahasohen rendet e rritjes te funksioneve n! dhe 2n. Duke përfituar nga
përparësitë e formulës së Stirlingut do të kemi:

𝑛 𝑛
𝑛! √2𝜋𝜋 � 𝑒 � 𝑛𝑛 𝑛 𝑛
lim𝑛→∞ = lim𝑛→∞ = lim𝑛→∞ √2𝜋𝜋 = lim𝑛→∞ √2𝜋𝜋 � � = ∞
2𝑛 2𝑛 2𝑛 𝑒 𝑛 2𝑒

Kështu, megjithëse 2n rritet shumë shpejt, por n! rritet akoma më shpejt. Ne mund të
shkruajmë që n! ∈ Ω(2n).

5.2.3 Si kryhet analiza asimptotike

Analiza asimptotike e një algoritmi përcakton kohën e ekzekutimit në funksion të simboleve


të Landau-it. Për të kryer analizën asimptotike përcaktohet numri i veprimeve elementare (në
rastin më të keq nëse ka të tillë) në funksion të përmasës së problemit dhe më pas shprehet ky
funksion me anë të simboleve të Landau-ut. Përderisa faktorët konstante dhe termat e rendit
më të ulët gjithsesi nuk merren parasysh atëherë nuk i marim parasysh ata gjatë numrimit të
veprimeve.
Shembuj e mëposhtëm ndihmojnë në të kuptuarit e analizës asimptotike.

Shembulli 1. Analiza e një veprimi të një vlerëdhënie

a ←b

Meqenëse instruksioni i vlerëdhënies konsumon një kohë konstante T(n) = 1, atëherë ai është
në Θ(1).

Shembulli 2. Një cikël i thjeshtë for.

sh ← 0;
for i ← 1 to n do
sh ← sh + n

Rreshti i parë është Θ(1). Cikli for përsëritet n herë. Rreshti i tretë shpenzon një kohë
konstante, kështu duke shfrytëzuar vetinë 4, koha e ekzekutimit për këta dy rreshtat që
përbëjnë ciklin është në Θ(n). Nga rregulli 3 gjithë fragmenti është gjithashtu në Θ(n).

Shembulli 3. Fragment algoritmik me disa cikle edhe të përfshirë.

sh ← 0;
for i ← 1 to n do
for j ← 1 to i do
sh ← sh + 1;
for k ← 0 to n-1 do
A[k] ← n;

Ky fragment përbëhet nga tre instruksione të ndara: instruksioni i parë është një vlerëdhënie
dhe pasohet nga dy cikle for; njëri cikël i përfshirë dhe tjetri cikël i thjeshtë. Cikli i dytë for
trajtohet njëlloj si shembulli i dytë dhe është në Θ(n). Cikli i parë është një cikël i dyfishtë.
5 Analiza e efektshmërisë së algoritmeve | 91

Fillojmë arsyetimin nga pjesa më e brendshme në të jashtmen të ciklit të parë. Shprehja sh ←


sh + 1 kërkon kohë konstante. Meqenëse cikli i brendshëm ekzekutohet i herë koha e
ekzekutimit të tij është i. Cikli i jashtëm ekzekutohet n herë por për çdo ekzekutim koha e
ciklit të brendshëm është e ndryshme mbasi i ndryshon në çdo përsëritje. Cikli i jashtëm
fillon me vlerën 1, pastaj 2 e kështu me rradhë deri tek vlera n. Kështu që koha e shpenzuar
është sa shuma e numrave 1 + 2 + … + n, e cila është e barabartë me n(n+1)/2 që është në
Θ(n2). Si përfundim koha e ekzekutimit të tre fragmenteve është Θ(1) + Θ(n) + Θ(n2) që është
Θ(n2).

Shembulli 4. Jo të gjithë ciklet e përfshirë janë në Θ(n2).

sh ← 0;
k←1;
while k ≤ n do {
for j ← 1 to n do
sh ← sh + 1;
k←2*k
}

Kur analizojmë këtë fragment për lehtësi do të supozojmë që n është fuqi e dyshit. Cikli i
jashtëm, rreshti i dytë ekzekutohet (log + 1) herë mbasi në çdo përsëritje të tij variabli ciklik
shumëzohet me dy deri sa të bëhet n. Duke qenë se cikli i brendshëm gjithmonë ekzekutohet
n herë, koha e përgjithshme e ekzekutimit e fragmentit bën pjese ne Θ(n lgn).

5.2.4 Klasat themelore të efektshmërisë


Megjithëse matematika e shënimeve O, Ω, Θ duket si pak mistike, ata shërbejnë mjaft mirë
për të krijuar një sistem klasifikimi (rating system) për efektshmërinë e algoritmeve. Ato na
tregojnë llojin e sjelljes që mund të presim nga algoritmet ndaj përgjigje të faktit kur përmasa
e të dhënave bëhet gjithmonë e më e madhe. Këto klasa janë listuar në tabelën 5.2 në rendin
rritës të rendit të tyre të rritjes, së bashku me emrat e tyre dhe disa komente.

Tabela 5.2 Klasat themelore asimptotike


Klasa Emri Komente

1 konstant Shkurt, klasa më e mirë e efektshmërisë, por ekzistojnë të jepen


shumë pak shembuj të vlefshëm.
log n logaritmike Në mënyrë tipike ndodh kur përmasa e një problemi
reduktohet me një faktor konstant në çdo iteracion të algoritmit.
n lineare Algoritme që iterojnë një tabelë të përmasës n (për shembull,
kërkimi linear bie në këtë klasë).
n log n pothuajse Shumë algoritme që hartohen në bazë teknikës ndaj-dhe-sundo,
lineare duke përfshirë algoritmet Mergesort dhe Quicksort ndodhen
në këtë klase, në rastin mesatar.
n2 kuadratike Tipikisht karakterizon efektshmërinë e një algoritmi me dy cikle
të përfshirë. Algoritmet elementarë të renditjes dhe disa
algoritme me matrica kuadratike të rendit n janë shembuj
standard.
n3 kubike Tipikisht karakterizon efektshmërinë e algoritmeve me tre cikle
të përfshirë. Disa algoritme nga algjebra lineare bien në këtë
klasë.
5 Analiza e efektshmërisë së algoritmeve | 92

2n eksponenciale Është tipike për algoritmet që prodhojnë të gjitha nënbashkësitë


e një bashkësie me n elemente. Shpesh termi eksponencial
përdoret në një kuptim më të gjerë për të përfshirë këtë si dhe
rende më të larta
n! faktoriale Është tipike për algoritmet që prodhojnë të gjithë permutacionet
e një bashkësie me n elemente.

Shënim 1. Nuk ka ndonjë rregull të veçantë se kur duhet të përdoret secili shënim në
vlerësimet e klasave të efektshmërisë së algoritmeve. Një pjesë e autorëve preferojnë
përdorimin e shënimit O e madhe bile edhe në rastet kur ai është më pak shprehës sesa
shënimi Θ. Për më tej në diskutimet për kompleksitetin kohor apo hapsinor, në përgjithësi, do
të përqendrohemi në shënimin O, duke e përdorur atë për të karakterizuar kohën e
ekzekutimit të algoritmit në rastin më të keq.

Shënim 2. Do të përpiqemi që të gjejmë funksionin më të vogël të vlefshëm që karakterizon


kohën e ekzekutimit të algoritmit. Prandaj, përderisa teknikisht, një funksion që është O(n)
është gjithmonë O(n2), do ta karakterizojmë një funksion të tillë si O(n) dhe do të duhet ta
konsiderojmë jo të rregullt që të karakterizohet si O(n2).

Shënim 3. Nuk është mirë që të futen konstantet ose termat e rendeve më të ulta brenda
këtyre shënimeve, domethënë, nuk duhet të themi T(n) ∈ O(2n2) ose T(n) ∈ O(n2+n). Në të dy
rastet forma e rregullt do të ishte T(n) ∈ O(n2).

5.2.5 Mangësitë e analizës asimptotike

Në praktikë, për të zgjedhur ndërmjet algoritmeve të ndryshëm për të njëjtin problem, krahas
analizës asimptotike, duhet të kihen parasysh edhe konsiderata të tjera. Ndonjëherë një
algoritëm me sjellje të keqe asimptotike mund të jetë më i preferueshëm. Për hir të
diskutimit, le të jetë algoritmi A (O(n3)) asimptotikisht më i mirë se algoritmi B (O(106n2)).
Ja disa nga çështjet që nuk duhet të lihen pas dore me algoritmet që kanë sjellje më të mirë
asimptotike:

• Ndërlikueshmëria (kompleksiteti) e zbatimit. Shpesh herë, algoritmet me kompleksitet


më të mirë janë më të ndërlikuar. Kjo mund të shpjerë në rritje të kohës së programimit
dhe të koeficientëve në funksionin e kohës së ekzekutimit.
• Të dhënat me përmasa të vogla. Analiza asimptotike nuk merr parasysh të dhëna me
përmasa të vogla. Në të dhënat me përmasa të vogla, faktorët konstantë ose të rendeve më
të vogla mund të mbizotërojnë kohën e ekzekutimit, duke shkaktuar që algoritmi B të
sillet më mirë se algoritmi A.
• Rasti më i keq ndaj performacës në rastin mesatar. Nëse algoritmi A ka sjellje më të mirë
në rastin më të keq sesa algoritmi B, por sjellja mesatare e algoritmit B pritet të jetë më e
mirë atëherë algoritmi B do të ishte një zgjedhje më e mirë sesa algoritmi A. Anasjelltas,
nëse rasti më i keq i performancës së B është i papranueshëm (le themi për rrezik jete apo
për arsye të misioneve kritike), A duhet të vazhdojë të përdoret.
5 Analiza e efektshmërisë së algoritmeve | 93

5.2.6 Keqkuptimet e zakonshme

Analiza asimptotike është një temat më të vështira nga pikpamja e të kuptuarit për studentët e
vitit të parë të informatikës. Shumë studentë me konceptin e shkallës (rendit) të rritjes dhe me
analizën asimptotike turbullohen dhe krijojnë keqkuptime mbi konceptet ose terminologjinë.
Më poshtë paraqiten disa pika në përpjekje për t'i shmangur probleme të tilla.

Një nga keqkuptimet ka të bëjë me dallimin ndërmjet konceptit të kufijve të sipërm dhe të
poshtëm. Për pjesën më të madhe të algoritmeve që ju do të takoni, është e lehtë të njohësh
vlerën e vërtetë për këta algoritme. Duke njohur plotësisht funksionin e kohës së ekzekutimit,
kufiri i sipërm dhe kufiri i poshtëm janë të njëjtë. Kështu që dallimi ndërmjet një kufiri të
sipërm dhe një kufiri të poshtëm është i vlefshëm vetëm kur nuk ka njohuri të plota për atë që
do të matet. Koncepti Θ do të përdoret kur nuk ka diferencë përsa i përket shkallës së rritjes
së kufirit të poshtëm dhe kufirit të sipërm.

Është një gabim i zakonshëm të ngatërohen konceptet e kufirit të sipërm dhe kufirit të
poshtëm nga njëra anë dhe rastit më të keq dhe rastit më të mirë nga ana tjetër. Secili nga
rastet më i mirë, më i keq apo mesatar na jep një rast konkret që mund ta zbatojmë nga një
algoritëm për të përftuar kohën e ekzekutimit. Kufijtë e sipërm dhe kufijtë e poshtëm
përshkruajnë kuptimin që kemi për shkallën e rritjes për kohën e ekzekutimit. Kështu për të
përcaktuar shkallën e rritjes për një algoritëm ose problem ne duhet të përcaktojmë se çfarë
duam të matim (rastin më të mirë, rastin më të keq apo rastin mesatar) dhe gjithashtu
përshkrimin se çfarë duam të dimë për shkallën e rritjes për këtë funksion të kohës së
ekzekutimit (O-e madhe, Ω-e madhe, Θ-e madhe).

Kufiri i sipërm për një algoritëm nuk është i njëjtë me rastin më të keq për këtë algoritëm për
ndonjë të dhënë të përmasës n. Ajo që po kufizohet nuk është koha aktuale e ekzekutimit (të
cilën mund ta përcaktoni një vlerë të dhënë të n-së) por më tepër shkalla e rritjes për kohën e
ekzekutimit. Nuk mund të ketë shkallë rritjeje për një pikë të veçantë, siç është një vlerë e
caktuar e n-së. Shkalla e rritjes zbatohet për përcaktimin e ndryshimit të kohës së
ekzekutimit kur ndodh ndryshimi i përmasave të të dhënave. Në ngjashmëri, kufiri i poshtëm
nuk është i njëjtë me rastin më të mirë për një vlerë të dhënë të përmasës n.

Një keqkuptim tjetër i zakonshëm është në interpretimin e rastit më të mirë sikur ai ndodh
atëherë kur përmasa e të dhënës është sa më e vogël që të jetë e mundur ose që rasti më i keq
ndodh kur përmasa është sa më e madhe. Ajo që është korrekte është që rastet më të mira apo
më të këqia ndodhin për çdo përmasë të mundshme të të dhënave. Domethënë, të gjitha të
dhënat, le të themi e një përmase i, një (ose disa) nga të dhënat e përmasës i është më e mira
dhe një (ose disa) nga përmasat i është më e keqe. Shpesh (por jo gjithmonë!), ne mund të
karakterizojme rastin më të mirë të të dhënave për një përmasë arbitrare dhe ne mund të
karakterizojmë rastin më të keq po për një përmasë arbitrare. Idealisht, ne mund të
përcaktojmë shkallën e rritjes për rastin më të mirë, më të keq, mesatar kur përmasa e të
dhënave rritet.

5.3 Analiza e efektshmërisë së algoritmeve iterativë të disa problemeve

Në këtë seksion, në mënyrë sistematike do të zbatojmë kuadrin e përgjithshëm të trajtuar në


seksionin 5.1 për të analizuar efektshmërinë kohore të algoritmeve iterativë. Llogaritja e
kohës së ekzekutimit të algoritmeve iterativë është e drejpërdrejtë.
5 Analiza e efektshmërisë së algoritmeve | 94

Plani i përgjithshëm për analizën e efektshmërisë kohore të algoritmeve iterativë


1. Të përcaktohet një parametër që tregon përmasën e të dhënave.
2. Të identifikohet veprimi kryesor i algoritmit (si rregull ai ndodhet në trupin e ciklit më të
brendshëm).
3. Të kontrollohet nëse numri i herëve që ekzekutohet veprimi kryesor varet vetëm nga
përmasa e të dhënave. Por nëse ai varet edhe nga ndonjë cilësi shtesë e të dhënave
atëherë rasti më i keq dhe rasti më i mirë, nëse është e nevojshme, duhet të trajtohen
ndarazi.
4. Të ndërtohet shuma që shpreh numrin e herëve që ekzekutohet veprimi kryesor i
algoritmit.
5. Duke përdorur formulat standarde dhe rregullat e manipulimit të shumave, ose të gjendet
një formulë për numrin e veprimeve ose në fund të fundit të përcaktohet rendi i tij të
rritjes.

Le të fillojmë me një shembull shumë të thjeshtë që demonstron të gjithë hapat tipike që


duhet të ndërmerren për të analizuar algoritme të tillë.

Problemi. Le të konsiderojmë problemin e gjetjes së vlerës më të madhe ndër elementet e një


tabele të përmasës n. Më poshtë vijon pseudokodi i një algoritmi standard për zgjidhjen e
problemit që e kemi trajtuar në kapitullin e tretë.

Algoritmi 5.2 Gjetja e vlerës më të madhe në një tabelë


// Të dhëna: një tabelë a[0:n–1] me numra të plotë
// Rezultati: vmax, vlera më e madhe në tabelë

vleraMax(n, a) {
vMax ← a[0]; // inicializimi i vlerës më të madhe me vlerën e parë
i ← 1;
while (i ≤ n-1) do {
if (a[i] > vMax )
then vMax ← a[i]; // gjendet vlera më e madhe aktuale
i ← i + 1;
}
return vmax
}

Si përmasë e të dhënave në këtë problem është e qartë, që është numri i elementeve në tabelë,
domethënë, n. Veprimet që shprehin logjikën e përpunimit ndodhen në trupin e ciklit while .
Janë dy veprime në trupin e ciklit: krahasimi a[i] > vMax dhe dhënia e vlerës vMax ← a[i]. Cili
nga këta dy veprime do të konsiderohet si kryesori? Përderisa krahasimi ekzekutohet në çdo
përsëritje të ciklit ndërsa dhënia e vlerës jo, ne duhet të konsiderojmë që krahasimi është
veprimi elementar kryesor i algoritmit. Vëmë në dukje që numri i krahasimeve do të jetë i
njëjtë për çdo tabelë të përmasës n; prandaj, në funksion të kësaj zgjedhjeje, nuk është e
nevojshme të bëjmë dallim për rastin më të keq, mesatar apo më të mirë.

Le të shënojmë me K(n) numrin e herëve që ekzekutohet ky krahasim dhe të përpiqemi të


gjejmë një formulë që e shpreh atë në funksion të përmasës n. Algoritmi kryen një krahasim
5 Analiza e efektshmërisë së algoritmeve | 95

në çdo ekzekutim të ciklit, i cili përsëritet për çdo vlerë të variablit ciklik i në segmentin [1,
n-1]. Prandaj, përftojmë shumën e mëposhtme për K(n):

K(n) = ∑n−1
i=1 1.

Kjo është një shumë e thjeshtë për t’u llogaritur sepse nuk bën gjë tjetër veçse vlera 1
përsëritet n – 1 herë. Kështu që,
i=1 1= n – 1 ∈ Θ(n)
K(n) = ∑n−1

Në shtojcën në fund të kapitullit, janë dhënë disa formula të shumimit dhe të rregullave që
përdoren më shpesh në analizën e algoritmeve. Në veçanti, ne do të përdorim shpesh dy
rregullat bazë të manipulimit të shumave

∑si=p cai = c ∑si=p ai (R1)

∑si=p(ai ± bi ) = ∑si=p ai ± ∑si=p bi , (R2)

dhe dy formulat e shumimit

∑si=p 1 = s − p + 1 , ku p ≤ s janë numra të plotë (S1)

n(n+1) 1
∑ni=0 i = ∑ni=1 i = 1 + 2 + ⋯ + n = ≈ n2 ∈ Θ(n2 ) (S2)
2 2

Vëmë në dukje se formula ∑𝑛−1𝑖=1 1 = 𝑛 − 1, që u përdor në shembullin 1, është një rast i


veçantë i formulës (S1) për p = 1 dhe s = n – 1.

Problemi. Të përcaktohet nëse elementet e një tabele janë unikë (të ndryshëm). Ky është një
problem vendimi mbasi ne duam të njohim nëse të gjithë elementet janë të ndryshëm apo
përsëritet ndonjë element. Problemeve të vendimit u përgjigjemi me mesazh “Po“/“Jo“ ose
duke kthyer vlerat logjike true /false . Algoritmi i mëposhtëm e zgjidh problemin por nuk
është më i efektshmi.

// Algoritmi 5.3 Të përcaktohet nëse të gjithë elementet e një tabele janë të


// ndryshëm
// Të dhëna: një tabelë a[0:n – 1] me n elemente
// Rezultati: Kthen true nëse elementet janë unikë, përndryshe false

janëUnikë1 (n, a) {
for i ← 0 to n – 2 do
for j ← i + 1 to n – 1 do
if (A[i] = A[j])
then return false;
return true
}

Si shkallë e natyrshme për të matur përmasën e të dhënave përsëri shërben numri i


elementeve të tabelës, n. Përderisa cikli më i brendshëm përmban një veprim të vetëm
(krahasimi i dy elementeve), dhe ne mund ta konsiderojmë atë si veprimin elementar kryesor
5 Analiza e efektshmërisë së algoritmeve | 96

të algoritmit. Ndërkaq, vëmë në dukje se numri i elementeve që krahasohen nuk varet vetëm
nga n po edhe nga fakti nëse ka elemente të barabartë në tabelë dhe nëse ka, cilin pozicion ata
zënë. Do ta përqendrojmë analizën vetëm në rastin më të keq.

Nga përkufizimi, rasti më i keq i të dhënave ndodh në një tabelë për të cilën numri i
krahasimit të elementeve Kkeq(n) është më i madhi ndër të gjitha tabelat e përmasës n. Një
vëzhgim i ciklit më të brendshëm na vë në dukje se ekzistojnë dy lloj të dhënash për rastin më
të keq: të dhëna për të cilat cikli nuk përfundon para kohe, tabela me gjithë elementet e
ndryshëm dhe tabela në të cilën dy elementet e fundit janë i vetmi çift i elementeve të
barabartë. Për të tilla të dhëna, kryhet një krahasim për çdo përsëritje të ciklit të brendshëm,
domethënë, për çdo vlere të variablit ciklik j ndërmjet kufijve të tij i + 1 dhe n – 1; kjo
përsëritet për çdo vlerë të ciklit të jashtëm, domethënë, për çdo vlerë të variablit ciklik i
ndërmjet kufijve të tij 0 dhe n – 2. Në përputhje me këtë do të kemi
n−2 n−1 n−2 n−2
K keq (n) = ∑i=0 ∑j=i+1 1 = ∑i=0 [(n − 1) − (i + 1) + 1] = ∑i=0 (n − 1 − i)

(n−2)(n−1)
= ∑n−2 n−2 n−2
i=0 (n − 1) − ∑i=0 i = (n − 1) ∑i=0 1 − 2

(n−2)(n−1) (n−1)n 1
= (n − 1)2 − 2
=
2
≈ n2 ∈ Θ(n2 ).
2

Ne gjithshtu mund ta llogaritim shumën ∑𝑛−2


𝑖=0 (𝑛 − 1 − 𝑖) më shpejt si më poshtë:

(n−1)n
∑n−2
i=0 (n − 1 − i) = (n − 1) + (n − 2) + ⋯ 1 = ,
2

ku barazimi i fundit është përftuar duke zbatuar formulën e shumimit (S2). Vëmë në dukje
gjithashtu se ky rezultat është plotësisht i parashikueshëm: në rastin më të keq, algoritmi
duhet të krahasojë të gjithë n(n – 1)/2 çiftet e ndryshme të n elementeve të tij.

Problemi. Jepen dy matrica a dhe b, të rendit n x n, të llogaritet efektshmëria kohore e


algoritmit që llogarit produktin e dy matricave, bazuar në përkufizimin e produktit të
matricave.

Algoritmi shumëzimTabelaKuadratike, i paraqitur në një nga leksionet e kaluara, jepet përsëri


më poshtë:

// Algoritmi 5.4 Shumëzimi i dy tabelave dy-dimensionale kuadratike


// Të dhëna: n > 0 dhe a, b dy tabela kuadratike të rendit n
// Rezultati: Një tabelë kuadratike c = a x b, e rendit n

shumëzimTabelaKuadratike(n, a, b, c)
for i ← 0 to n-1 do
for j ← 0 to n-1 do {
c[i, j] ← 0; // inicializimi i elementit të rradhës
for k ← 0 to n-1 do
c[i, j] ← c[i, j] + a[i, k] * b[k, j] //formimi i elementit të rradhës
}
return
}
5 Analiza e efektshmërisë së algoritmeve | 97

Përmasa e të dhënave do shprehet nëpërmjet rendit n, të matricave. Në ciklin më të


brendshëm ekzistojnë dy veprime aritmetike: shumëzimi dhe mbledhja, që në parim mund të
hyjnë në konkurrencë për t’u caktuar si veprimi kryesor i algoritmit. Në realitet, nuk është e
nevojshme të zgjedhim ndërmjet tyre, mbasi në çdo përsëritje të ciklit më të brendshëm secili
prej tyre ekzekutohet ekzaktësisht një herë. Kështu duke numëruar njërin, ne kemi numëruar
tjetrin. Megjithatë, duke ndjekur një traditë të vendosur, ne do të konsiderojmë shumëzimin si
veprimin kryesor (shih seksionin 5.1). Le të ndërtojmë një shumatore për numrin e
përgjithshëm të shumëzimeve Sh(n) të ekzekutuara nga algoritmi.

Është e qartë se ka vetëm një shumëzim që ekzekutohet në çdo përsëritje të ciklit të më të


brendshëm, i cili qeveriset nga variabli k që ndryshon nga 0 deri në n-1. Prandaj, numri i
shumëzimeve të kryera për çdo çift të vlerave specifike të variablave i dhe j është

∑𝑛−1
𝑘=0 1.

dhe numri i përgjithëm i shumëzimeve Sh(n) do të shprehet nga shuma e trefishtë e


mëposhtme:
n−1 n−1
Sh(n)= ∑n−1
i=0 ∑j=0 ∑k=0 1.

Tani, mund ta llogaritim këtë shumë duke përdorur formulën (S1) dhe rregullin (R1) të dhënë
më lart. Duke filluar nga cikli më i brendshëm, shuma ∑𝑛−1
𝑘=0 1, e cila është e barabartë me n,
do të kemi
n−1 n−1
Sh(n)= ∑n−1 n−1 n−1 n−1 2 3
i=0 ∑j=0 ∑k=0 1 = ∑i=0 ∑j=0 n = ∑i=0 n = n .

Ky shembull është shumë i thjeshtë saqë rezultati i sipërm mund të nxirret pa manipulimet e
shumave. Si? Algoritmi llogarit n2 elemente të matricës produkt. Secili element i matricës
produkt llogaritet si produkt skalar i një rreshti me n elemente të matricës së parë me një
shtyllë me n elemente të matricës së dytë, fragment që kërkon n shumëzime. Kështu që numri
i përgjithshëm i shumëzimeve është n ∙ n2 = n3.

Në se duam që të vlerësojmë kohën e ekzekutimit të algoritmit në një kompjuter të caktuar,


mund ta bëjmë këtë me anë të produktit

T(n) ≈ cshSh(n) = cshn3,

ku csh, është koha e një shumëzimi në kompjuterin në fjalë.

Ne mund të marrim një vlerësim më të saktë nëse futim në llogaritje edhe kohën e shpenzuar
për mbledhjet, kështu:

T(n) ≈ cshSh(n) + cmM(n) = cshn3 + cmn3 = (csh + cm) n3,

ku cm është koha e një mbledhjeje. Vëmë në dukje se vlerësimi ndryshon vetëm me një faktor
konstant dhe nuk ndryshon rendi i rritjes. Përsëri asimptotikisht algoritmi bën pjesë në Θ(n3).

Nga shembujt e mësipërm mund të jetë krijuar përshtypja e gabuar se plani i përcaktuar më
lart arrin gjithmonë lehtësisht të analizojë një algoritëm iterativ. Por një zmadhim i ndryshëm
5 Analiza e efektshmërisë së algoritmeve | 98

i variablit ciklik ose një shumë tepër e ndërlikuar për t’u analizuar, janë disa nga pengesat që
mund të duken të pakalueshme. Pavarësisht nga këto pengesa, plani funksionon për mrekulli
siç do ta shohim më tej.

Si shembull i fundit, le të konsiderojmë një algoritëm në të cilin variabli i ciklit ndryshon në


një mënyrë të ndryshme nga ata të shembujve të mësipërm.

Problemi. Llogaritja e numrit të shifrave në paraqitjen binare të një numri të plotë të dhënë
në sistemin me bazë dhjetë.

// Algoritmi 5.5 bitCount (n)


// Llogarit numrin e shifrave binare
// Të dhëna: Një numër i plotë pozitiv n
// Rezultati: count, numri i shifrave në paraqitjen binare të n

bitCount (n) {
count ← 1;
while (n > 1) do {
count ← count +1;
n ← floor(n / 2);}
return count
}

Përmasa e problemit në këtë algoritëm është numri i dhënë n. Së pari vëmë re që veprimi
elementar që ekzekutohet më shpesh në këtë algoritëm nuk është në trupin e ciklit while, por
është krahasimi n > 1, që përcakton nëse trupi i ciklit do të ekzekutohet. Por kjo zgjedhje nuk
është kaq e rëndësishme mbasi numri i herëve që do të ekzekutohet krahasimi është më i
madh se numri i përsëritjeve të trupit të ciklit ekzaktësisht me 1.

Një veçori e rëndësishme e këtij shembulli është fakti që variabli i ciklit, në këtë rast n, merr
një numër më të vogël vlerash ndërmjet kufirit të poshtëm dhe të sipërm (ndërmjet 1 dhe n);
prandaj do të përdorim një rrugë të ndryshme për të llogaritur numrin e herëve të ekzekutimit
të ciklit. Përderisa vlera e n, përgjysmohet në çdo përsëritje të ciklit, përgjigja duhet të jetë
rreth log2 n. Formula e saktë, për llogaritjen e numrit të herëve që krahasimi n > 1 duhet të
ekzekutohet, është ⌊log2 n⌋ + 1, sa ç'është numri i shifrave në paraqitjen binare të n në
përputhje me formulën (5.1). Pra bën pjesë në Θ(log n)

5.4 Analiza e efektshmërisë së algoritmeve rekursivë të disa problemeve

Në këtë seksion, do të shohim se si do të zbatojmë kuadrin e përgjithshëm për analizën e


algoritmeve rekursivë. Analiza e efektshmërisë së një algoritmi rekursiv është e ndryshme
nga ajo e një algoritmi iterativ. Ajo është disi më e vështirë. Shpërbërja rekursive e kryer nga
një algoritëm reflektohet në mënyrë të drejtpërdrejtë në analizën kohës së llogaritjeve që do të
kryhet. Koha e ekzekutimit të kësaj klase algoritmesh varet nga permasa e të dhënave
fillestare, nga numri i nënproblemeve si dhe nga koha që duhet për shpërbërjen.

Do të fillojmë me shembullin e thjeshtë, llogaritjen e faktorialit që e kemi parë në leksionet e


mëparshme.
5 Analiza e efektshmërisë së algoritmeve | 99

Problemi. Të llogaritet funksioni faktorial F(n) = n! për një numër të plotë të çfardoshëm
jonegativ n.

// Algoritmi 5.6
// Llogarit faktorialin e një numri të plotë
// Të dhëna: Një numër i plotë jonegativ, n ≥ 0
// Rezultate: vlera e n!

F( n) {
if (n = 0)
then return 1
else return n * F(n – 1);
}

Për thjeshtësi, do të konsiderojmë vetë n-në si indikator të përmasës së të dhënave (dhe jo


numrin e biteve në paraqitjen binare). Veprimi kryesor i algoritmit është
shumëzimi, 7 numrin e ekzekutimeve të të cilit po e shënojmë me Sh(n). Duke qenë se
funksioni F(n) llogaritet në përputhje me formulën

F(n) = F(n – 1) ∙ n për n ≥ 1,

numri i shumëzimeve Sh(n) i nevojshëm për të llogaritur atë duhet të kënaqë barazimin

Sh(n) = ���������������
Sh(n − 1) + �����������������
1 për n ≥ 1
pë r të llogaritur F(n−1) pë r të pë rgatitur n∙F(n−1)

Me të vërtetë, Sh(n – 1) shumëzime shpenzohen për të llogaritur F(n – 1), dhe 1 shumëzim
më shumë i nevojshëm për të shumëzuar rezultatin me n.

Ekuacioni i fundit përcakton vargun Sh(n) që duhet të gjejmë. Ky ekuacion përcakton Sh(n)
në mënyrë të pashtjellur, domethënë, jo si një funksion i n, por si funksion i vlerës së tij në
një pikë tjetër, pikërisht n – 1. Ekuacione të tilla quhen relacione rekurrenciale ose
shkurtimisht rekurrencë. Relacionet rekurrencialë luajnë një rol të rëndësishëm jo vetëm në
analizën e algoritmeve por gjithashtu edhe në disa fusha të tjera të matematikës së zbatuar.
Qëllimi ynë tani është të zgjidhim relacionin rekurrencial Sh(n) = Sh(n – 1) + 1, domethënë,
të gjejmë një formulë të shtjellur për Sh(n) vetëm në funksion të n.

Vëmë në dukje, se ekziston jo vetëm një zgjidhje por një pafundësi zgjidhjesh. (A mund të
jepni për shembull dy të tilla?) Për të përcaktuar një zgjidhje të vetme, ne kemi nevojë për një
konditë fillestare që na jep vlerën me të cilën fillon vargu. Këtë vlerë mund ta përftojmë duke
kontrolluar kushtin, që e bën algoritmin të ndalojë thirrjet e tij rekursive:
if n = 0 then return 1
Kjo na thotë dy gjëra. Së pari, përderisa thirrjet përfundojnë kur n = 0, që është vlera më e
vogël e n për të cilën algoritmi do të ekzekutohet, pra Sh(n) e përcaktuar është 0. Së dyti,
duke kqyrur rreshtin përkatës në pseudokod, shohim se kur n = 0, algoritmi nuk kryen
shumëzime. Si rrjedhim, kondita fillestare që kërkojmë është

7
Ne mënyrë alternative mund të numërojmë numrin e herëve që ekzekutohet krahasimi n = 0, që është njëlloj si
numërimi i numrit të përgjithshëm të thirrjeve të bëra nga algoritmi.
5 Analiza e efektshmërisë së algoritmeve | 100

Sh(0) = 0

thirrjet përfundojnë kur n = 0 numri i veprimeve kur n = 0

Në këtë mënyrë arritëm të ndërtojmë relacionin e rekurrencës dhe konditën fillestare për
numrin e shumëzimeve të algoritmit, Sh(n):

Sh(n) = Sh(n – 1) + 1, për n ≥ 1 (5.2)


Sh(0) = 0, për n = 0

Para se të fillojmë nga diskutimet për zgjidhjen e kësaj rekurrence le të rikujtojmë edhe një
herë një çështje të rëndësishme. Deri tani kemi të bëjmë me dy funksione të përcaktuara në
mënyrë rekursive. I pari është vetë funksioni faktorial F(n), që përcaktohet nga rekurrenca

F(n) = F(n – 1)∙n , për çdo n ≥ 1


F(0) = 0, për n = 0

I dyti është numri i shumëzimeve Sh(n) i nevojshëm për llogaritjen e F(n) me anë të
algoritmit rekursiv, pseudokodi i të cilit u paraqit në fillim të seksionit. Sapo pamë, që Sh(n)
është përcaktuar nga rekurrenca (5.2). Dhe zgjidhja e rekurrencës është ajo që kërkojmë të
gjejmë. Më poshtë do të paraqesim dy metoda për zgjidhjen e ekuacioneve rekurrencialë.

Metoda e zëvendësimit
Edhe pse nuk është e vështirë që të hamendësojmë këtu një zgjidhje (cili është ai varg që
fillon me 0, kur n = 0, dhe rritet çdo hap me 1?), do të ishte më e dobishme që të mbrijmë tek
zgjidhja nëpërmjet një mënyre sistematike. Ndërmjet metodave të ndryshme për zgjidhjen e
ekuacioneve rekurrenciale po përdorim atë që quhet Metoda e zëvendësimeve (Method of
Backward Substitutions). Idea e metodës, nga ku rrjedh edhe emri i saj, bëhet menjëherë e
qartë nga mënyra e përdorimit të saj për të zgjidhur këtë rekurrencë:

Sh(n) = Sh(n – 1) +1 //zëvendësohet Sh(n – 1) me Sh(n – 2) + 1


= [ Sh(n – 2) +1] +1 = Sh(n – 2) + 2 //zëvendësohet Sh(n – 2) me Sh(n – 3) + 1
= [Sh(n – 3) +1] +2 = Sh(n – 3) + 3.

Pas kqyrjes së tre rreshtave të parë, shohim se shfaqet një trajtë, e cila bën të mundur jo
vetëm parashikimin e rreshtit pasardhës por gjithashtu edhe një formulë të përgjithshme në
trajtën: Sh(n) = Sh(n – i) + i. Nëse flasim me rigorozitet, korrektësia e kësaj formule duhet të
provohet me anë të induksionit matematik, por është më lehtë të përftohet zgjidhja si më
poshtë dhe pastaj të verifikohet korrektësia e saj.

Ajo çfarë mbetet për t’u kryer është që të përfitohet nga kondita fillestare e dhënë. Përderisa
ajo është specifikuar për n = 0, duhet të zëvendësojmë i = n tek formula për të marrë
rezultatin e fundit të zëvendësimeve:

Sh(n) = Sh(n–1) + 1 = ••• = Sh(n–i) + i = ••• = Sh(n–n) + n = n.

Nuk duhet të jini të shqetësuar, që pas kaq përpjekjesh, morëm një zgjidhje që ishte e qartë.
Të mirat e metodës të ilustruar në këtë shembull të thjeshtë do të bëhen të qarta shumë shpejt,
kur të kemi për të zgjidhur rekurrenca më të vështira. Vëmë në dukje gjithashtu se algoritmi i
thjeshtë iterativ që grumbullonte produktin e n numrave të plotë të njëpasnjëshëm kërkonte të
5 Analiza e efektshmërisë së algoritmeve | 101

njëjtin numër shumëzimesh, dhe këtë e bënte pa një kohë dhe hapsirë shtesë të nevojshme për
të mbajtur strukturat e rekursionit.

Metoda e pemës së rekursionit


Metoda e zëvendësimit mund të shihet edhe nën një këndvështrim tjetër, atë të pemës së
rekursionit. Një pemë rekursioni për një rekurrence është një paraqitje pamore (vizuale) dhe
konceptuale e procesit të përsëritjes së rekurrencës. Ne vizatojmë pemën e rekursionit me
koston e një thirrjeje në çdo nyje të saj dhe koha e ekzekutimit është shumatorja e të gjitha
nyjeve. Është mjaft e dobishme që të kemi një interpretim “algoritmik” të rekurrencës.
Prandaj, gjatë paraqitjes, do të përpiqemi të tregojmë se pse zgjidhja ekuacionit të
rekurrencës mund të modelohet me anë të pemës së rekursionit.

Për shembull, duke lënë mënjanë për momentin rastin bazë, ne mund ta interpretojmë
rekurrencën

Sh(n) = Sh(n – 1 ) + 1, (5.3)

në trajtën: “në mënyrë që të zgjidhim një problem të përmasës n, ne duhet të zgjidhim një
problem të përmasës n – 1 dhe të kryejmë 1 njësi punë shtesë (koha për ekzekutimin e një
shumëzimi në rastin e llogaritjes së faktorialit).”

Në figurën 5.4 po paraqesim fillimin e punës për ndërtimin e diagramës të rekursionit për
rekurrencen (5.2). Një diagramë e pemës së rekursionit përbëhet nga 4 pjesë. Në krahun e
majtë ne mbajmë nivelin, djathtas saj mbahet përmasa e problemit, më djathtas akoma
vizatohet pema e thirrjeve, dhe në fund numri i veprimeve ose e thënë ndryshe kosto.
Diagrama vizatohet me nivele, çdo nivel i diagramës paraqet një nivel thirrjeje të rekursionit.
Në mënyrë të njëvlershme, çdo nivel i diagramës paraqet një nivel të përsëritjes së
rekurrencës. Kështu, për të filluar, pemën e rekursionit për (5.2) ne tregojmë që, në nivelin 0
në të majtë, kemi një problem të përmasës n. Pastaj vizatojmë të ashtuquajturën rrënjë me një
brinjë që del nga ajo. Në të djathtë tregojmë që kemi për të kryer një punë shtesë me kosto 1
njësi (kosto e një shumëzimi) pavarësisht se çdo do të bëhet me problemin e ri të krijuar. Në
mënyrë që diagrama të përmbajë informacion të vlefshëm plotësojmë një pjesë të nivelit të
parë. Vizatojmë një kulm që paraqet problemin që rrjedh nga problemi kryesor dhe tregojmë
se problemi e ka përmasën n – 1.

Niveli Madhësia e problemit Pema e rekursionit Kosto shtesë (shumëzime)


0 n 1
1 n -1

Figura 5.4 Faza fillestare e vizatimit të pemës të rekursionit

Tani mund të shihet si është reflektuar rekurrenca në nivelet 0 dhe 1 të pemës së rekursionit.
Kulmi i sipërm i pemës paraqet Sh(n), në nivelin pasues kemi një problem të madhësisë n – 1,
që na jep termin rekursiv Sh(n – 1) të rekurrencës. Pastaj, pasi zgjidhim këtë problem
kthehemi në nivelin 0 të pemës dhe kryejmë një punë shtesë prej 1 njësi për termin jorekursiv
të rekurrencës.

Në figurën 5.5 paraqiten katër nivele të pemës së rekursionit.


5 Analiza e efektshmërisë së algoritmeve | 102

Niveli Madhësia e problemit Pema e rekursionit Kosto shtesë (shumëzime)


0 n 1
1 n -1 1
2 n -2 1
3 n -3
Figura 5.5 Katër nivele të një peme rekursioni

Le të përmbledhin atë ç’ka na thotë diagrama. Në nivelin zero (në krye), është kryer 1 njësi
punë shtesë. Shohim që në secilin nivel pasues, kemi zvogëluar madhësinë e problemit me 1.
Gjithashtu shohim që në nivelin 1, kërkohet një njësi kosto shtesë. Në mënyrë të ngjashme
niveli 2 ka një problem të përmasës n-2 dhe kryhet një njësi punë shtesë dhe në nivelin e tretë
ka një problem të përmasës n-3 dhe kryhet puna prej një njësi.

Më tej për këtë problem, në nivelin i, kemi një problem të përmasës n-i dhe një njësi kosto
shtesë.

Tani kemi grumbulluar informacion në sasi të mjaftueshme për të përshkruar diagramën e


pemës së rekursionit në përgjithsi. Për të bërë këtë është e nevojshme të përcaktojmë për çdo
nivel tri gjëra: (i) numrin e problemeve, (ii) madhësinë e çdo problemi dhe (iii) punën e
përgjithshme të kryer. Gjithashtu është e nevojshme të dimë se prej sa nivelesh përbëhet
pema e rekursionit.

Për të llogaritur numrin e niveleve të pemës së rekursionit, mjafton të vëmë në dukje që në


çdo nivel madhësia e problemit zvogëlohet me 1 dhe pema mbyllet kur madhësia e problemit
bëhet 0. Prandaj pema përbëhet nga n + 1 nivele. E gjithë pema është paraqitur në figurën 5.6.

Niveli Madhësia e problemit Pema e rekursionit Kosto shtesë (shumëzime)


0 n 1
1 n-1 1
2 n-2 1
3 n-3 1
… … …
n -1 1 1
n 0 0
Figura 5.6 Një diagramë e përfunduar e një peme rekursioni

Niveli më i poshtëm është i ndryshëm nga nivelet e tjerë, në të cilat puna përshkruhej nga
pjesa rekursive e rekurrencës, Sh(n) = Sh(n – 1) + 1. Në nivelin më të poshtëm, puna
përcaktohet nga rasti bazë. Në këtë nivel ne kemi një problem dhe puna shtesë e kryer në
nivelin bazë, me supozimin që Sh(0) = 0, është 0 (nuk kemi shumëzime).

Sapo të njohim me saktësi numrin e niveleve dhe punën që kryhet në çdo nivel atëherë mund
të shumojmë punën e kryer në të gjithë nivelet duke përftuar në këtë mënyrë zgjidhjen e
rekurrencës.

Në rastin në shqyrtim, kemi n + 1 nivele, dhe në çdo nivel puna e kryer është 1 njësi me
përjashtin të nivelit n, ku puna e kryer është 0. Pra arrijmë në përfundimin se e gjithë puna që
duhet për të zgjidhur problemin e përshkruar nga rekurrenca (5.3), me kusht fillestar Sh(0) =
5 Analiza e efektshmërisë së algoritmeve | 103

0 është e barabartë Sh(n) = n. Puna gjithsej e kryer nëpërmjet pemës është zgjidhja e
rekurrencës në shqyrtim mbasi pema thjesht modelon procesin e përsëritjes të rekurrencës.

Plani i përgjithshëm për analizën efektshmërisë kohore të algoritmeve rekursive


Duke përgjithësuar eksperiencën tonë të kqyrjes së algoritmit rekursiv për llogaritjen e n!, ne
mund të skicojmë një plan të përgjithshëm për analizën e algoritmeve rekursivë.

1. Të përcaktohet parametri që tregon përmasën e të dhënave fillestare


2. Të identifikohet veprimi kryesor i algoritmit.
3. Të kontrollohet nëse numri i herëve që ekzekutohet veprimi kryesor mund të ndryshojë
për të dhëna të ndryshme të të njëjtës përmasë; nëse ndodh atëherë rasti më i keq, më i
mirë dhe ai mesatar duhet të shqyrtohen ndarazi.
4. Të ndërtohet një relacion rekurrencial, me kondita fillestare të përshtatshme, për numrin
e herëve që ekzekutohet veprimi kryesor.
5. Të zgjidhet rekurrenca, ose të paktën të përcaktohet rendi i rritjes i zgjidhjes së saj.

Problemi i kullave të Hanoit 8. Jepen 3 boshte të emërtuar Burim, Ndërmjetës dhe


Destinacion si dhe n disqe me diametër të ndryshëm, që mund të futen në këta boshte, figura
5.7. Në fillim të gjithë disqet janë futur në boshtin Burim, të renditur nga më i madhi (në
bazë) deri tek më i vogli (në krye). Detyra është që të zhvendoset stiva e disqeve në boshtin
Destinacion duke përdorur boshtin Ndërmjetës si ndihmës.

Figura 5.7 Zgjidhja rekursive e problemit të kullave të Hanoit

Për të realizuar zhvendosjen lejohet vetëm që në çdo hap mund të zhvendoset një dhe vetëm
një disk dhe nuk lejohet që një disk me diametër më të vogël të vihet poshtë një disku me
diametër më të madh.

Le ta fillojmë zgjidhjen e problemit në rastin më të thjeshtë. Supozojmë se kemi vetëm një


disk për të zhvendosur. Lëvizja është e qartë: zhvendoset disku nga boshti Burim tek boshti
Destinacion. Gjithsej një zhvendosje.

Tani le të kemi dy disqe. Përsëri problemi është i thjeshtë. Për të krijuar hapsirë për të
zhvendosur diskun e madh, zhvendosim fillimisht diskun e vogël të sipërm nga boshti Burim
tek boshti Ndërmjetës. Me pas zhvendosim diskun e madh nga boshti Burim tek boshti

8
Towers of Hanoi (Quhet edhe Towers of Brahmas). Origjina e lojës nga matematicieni francez Edourd Lucas
në vitin 1883
5 Analiza e efektshmërisë së algoritmeve | 104

Destinacion dhe në fund zhvendosim diskun e vogël nga boshti Ndërmjetës tek boshti
Destinacion. Në këtë mënyrë problemi i kullës me dy disqe realizohet me 3 zhvendosje.

Le të mendojmë procesin me tre disqe. Për të zhvendosur diskun e madh tek boshti
Destinacion duhet që së pari të largojmë pengesën, dy disqet e sipërm. Këta dy disqe
formojnë një problem të kullës me dy disqe. Duke zbatuar procesin për dy disqe, mund t’i
zhvendosim ata në boshtin Ndërmjetës dhe kështu të çlirojmë diskun më të madh për ta
zhvendosur në boshtin Destinacion. Në këtë mënyrë zgjidhja e problemit me tre disqe
reduktohet në tre hapa:

1. Zgjidh problemin e përmasës dy nga boshti Burim tek boshti Ndërmjetës


2. Zhvendos diskun e madh nga boshti Burim tek boshti Destinacion
3. Zgjidh problemin e përmasës dy nga boshti Ndërmjetës tek boshti Destinacion.

Hapi i parë dhe hapi i tretë kërkojnë zgjidhjen e një problemi të përmasës dy. Këtë proces
dimë ta trajtojmë. Janë probleme të përmasës me dy disqe me theksim të faktit që për njërin
do ta zhvendosim nga boshti Burim tek boshti Ndërmjetës duke përdorur boshtin Destinacion
si bosht ndërmjetës dhe për tjetrin do të kryejmë zhvendosjet nga boshti Ndërmjetës tek
boshti Destinacion duke përdorur boshtin Burim si bosht ndërmjetës.

Në këtë mënyrë kemi skicuar idenë e një algoritmi të thjeshtë rekursiv për problemin e
përgjithshëm të kullës me çfardo lloj përmase nga një bosht në tjetrin në formën:

1. Zgjidh problemin me (n – 1) disqe nga boshti Burim në boshtin Ndërmjetës


2. Zhvendos diskun n nga boshti Burim në boshtin Destinacion
3. Zgjidh problemin me (n – 1) disqe nga boshti Ndërmjetës në boshtin Destinacion.

Cili do të ishte rasti bazë për këtë proces rekursiv? Le të vëmë në dukje se si një zhvendosje
prej n disqesh reduktohet në dy zhvendosje prej (n – 1) disqesh. Përderisa reduktojmë
përmasën me nga një çdo herë, do të ndodhë që përmasa e kullës do të bëhet një. Një kullë
me përmasë një mund të zhvendoset drejtpërsëdrejti si një disk i vetëm, domethënë, nuk kemi
nevojë për thirrje rekursive për të zhvendosur atë.

//ALGORITMI 5.7 Problemi i kullave të Hanoi


//Të dhëna: n disqe të emërtuar 1, 2,…,n, nga më i vogli tek më i madhi dhe tre
// boshte të emërtuar Burim, Ndërmjetës dhe Destinacion
//Rezultate: Afishimi i listës së zhvendosjeve

Hanoi( n, Burim, Destinacion, Ndërmjetës) {


if n = 1
then print(“Lëviz diskun”, n,”nga boshti”, Burim, “tek boshti”, Destinacion)
else { Hanoi (n – 1, Burim, Ndërmjetës, Destinacion);
print (“Lëviz diskun”, n, “nga boshti ”, Burim, “tek boshti ”, Destinacion);
Hanoi(n – 1, Ndërmjetës, Destinacion, Burim)
}
}

Thirrja e funksionit në algoritmin kryesor është Hanoi(n, Burim, Destinacion, Ndërmjetës).


5 Analiza e efektshmërisë së algoritmeve | 105

Le të zbatojmë planin e përgjithshëm për analizën e efektshmërisë për problemin e kullave të


Hanoi. Është e qartë se si përmasë e të dhënave shërben numri i disqeve, n, ndërsa veprimi
bazë i algoritmit është lëvizja e një disku. Është e qartë se numri i lëvizjeve L(n) varet vetëm
nga n dhe ne përftojmë rekurrencën e mëposhtme për të:

L(n) = L(n – 1) + 1 + L(n – 1) për n > 1

Me kushtin fillestar të qartë L(1) = 1, kemi relacionin rekurrencial të mëposhtëm për numrin
e lëvizjeve të L(n):

L(n) = 2L(n – 1) + 1 për n > 1, (5.4)


L(1) = 1

Le ta zgjidhim rekurrencën me metodën e zëvendësimit:

L(n) = 2L(n – 1) + 1
= 2 [2L(n – 2) + 1 ] + 1 = 22L(n – 2) + 2 +1
= 22[2L(n – 3) +1 ] + 2 + 1 = 23L(n – 3) + 22 + 2 + 1

Trajta e tri shumave të para na sugjeron që pasardhësja do të jetë: 24L(n – 4) + 23 + 22 + 21 +


1, dhe në përgjithësi pas i zëvendësimesh, do të kemi

L(n) = 2iL(n – i) + 2i – 1 + 2i – 2 + ∙∙∙ + 1 = 2iL(n – i) + 2i – 1

Meqënëse kondita fillestare është specifikuar për n = 1, atëherë do të përftojmë formulën e


mëposhtme për zgjidhjen e rekurrencës (5.4):
L(n) = 2n – 1L(n – (n – 1)) + 2n – 1 – 1
= 2n – 1L(1) + 2n – 1 – 1 = 2n – 1 + 2n – 1 – 1 = 2n – 1

Sigurisht që të njëjtin rezultat do të marrim nëse zbatojmë metodën e pemës së rekursionit,


figura 5.8. (Këtu kosto shtesë është numri i lëvizjeve për klimin nga një nivel i pemës në
tjetrin.)

Niveli Madhësia e problemit Pema Kosto shtesë


0
0 n 1=2
1
1 n-1 1 + 1= 2
2
2 n-2 1+1+1+1 = 2
3
3 n-3 1+1+1+1+1+1+1+1 = 2
. . . . . . . . . . . . . . . . . . . . . . .
n -1 1 . . . 2
n-1

Figura 5.8 Pema e rekursionit për problemin e kullave të Hanoit

Duke vërejtur se vlerat e lëvizjeve të disqeve në çdo nivel formojnë një progresion
gjeometrik, atëherë shuma e tyre është L(n) = 2n - 1. Ky pohim mund të verifikohet
drejtpërsëdrejti me anë të induksionit matematik.
5 Analiza e efektshmërisë së algoritmeve | 106

Për zgjidhjen e problemit të kullave të Hanoit, krijuam një algoritëm me efektshmëri


eksponenciale, i cili shpenzon shumë kohë, bile edhe për vlera të moderuara të n. Kjo nuk
rrjedh nga fakti që ky algoritëm i veçantë është i keq; në të vërtetë provohet që ky është
algoritmi më i efektshëm i mundshëm për këtë problem. Është ndërlikueshmëria e problemit
që i bën llogaritjet të vështira. Për më tepër, ky shembull na vë në dukje një çështje të
rëndësishme:
Duhet të kemi kujdes me algoritmet rekursivë mbasi eleganca e tyre mund të fshehë
paefektshmërinë.
Megjithëse problemi i Kullave të Hanoit është relativisht i lehtë për t’u shprehur në algoritëm
ai futet në klasën e problemeve që quhen të patrajtueshëm (intractable problems). Këta janë
probleme që kërkojnë shumë kohë llogaritëse për t’u zgjidhur në praktikë, me përjashtim të
rasteve të thjeshta.

Shënim
Legjenda, që ka lindur rreth këtij problemi, thotë se botës do t’i vijë fundi atëherë kur një grup
murgjish të një farë tempulli në Indi, do të arrijnë të zgjidhin problemin për të zhvendosur 64 disqe të
artë në boshte diamanti. Nëse murgjit do të zhvendosnin disqet me shpejtësi 1 sekondë për një disk
atëherë do të duheshin 585,000 miliard vjet për të përfunduar zhvendosjen (mosha e universit
vlerësohet në rreth 15 miliard vjet!).

Problemi. Le të diskutojmë versionin rekursiv të algoritmit bitCount, të paraqitur në fund të


seksionit 5.3, llogaritja e numrit të shifrave në paraqitjen binare të një numri të plotë n.

Algoritmi rekursiv, për llogaritjen e sasisë së shifrave, që do të ketë paraqitja binare e një
numri të plotë n, të dhënë në sistemin dhjetor, bazohet thjeshtë në idenë e pjesëtimit të
vazhdueshëm me 2 dhe në çdo hap shtohet vlera 1 (një mbledhje).

// Algoritmi 5.8 Llogaritja e shifrave në paraqitjen binare të një numri të plotë


// Të dhëna: një numër i plotë, n > 0
// Rezultate: Sasia e shifrave binare

BitCountRec(n) {
if (n = 1)
then return 1
else return BitCountRec (floor(n /2)) + 1
}

Le të ndërtojmë një rekurrencë dhe konditën fillestare për numrin e mbledhjeve të kryera nga
algoritmi. Numri i mbledhjeve të kryera nga algoritmi është M(floor(n/2)), plus një mbledhje
për të zmadhuar vlerën e kthyer me 1. Kjo shpie në rekurrencën,

M(n) = M(floor(n/2)) + 1 për n > 1

Meqenëse thirrjet rekursive përfundojnë kur n bëhet e barabartë me 1 dhe nuk ka më


mbledhje për t’u bërë, atëherë kondita fillestare do të jetë

M(1) = 0
5 Analiza e efektshmërisë së algoritmeve | 107

Prania e funksionit floor(n/2) në argumentin e funksionit, bëhet pengesë për zbatimin e


metodës së zëvendësimit për vlera të n që nuk janë fuqi të 2it. Prandaj, rruga standarde për të
zgjidhur një rekurrencë të tillë zbatohet vetëm për n = 2k . Por, duke përfituar nga teorema e
quajtur rregulli i lëmimit (smoothness rule), që thotë se nën hipoteza shumë të gjëra rendi i
rritjes i vrojtuar për n = 2k jep një përgjigje korrekte mbi rendin e rritjes të të gjitha vlerave të
n. Kështu që le të zbatojmë këtë recetë për rekurrencën tonë, e cila për n = 2k transformohet
në formën

M(2k) = M(2k-1) + 1 për k > 0


M(20) = 0 për k = 0

Tashmë zëvendësimet kryhen pa probleme:

M(2k) = M(2k - 1) + 1 (zëvendësohet M(2k- 1) = M(2k - 2) + 1)


= [M(2k - 2) + 1] + 1 = M(2k - 2) + 2 (zëvendësohet M(2k - 2) = M(2k - 3) + 1)
= [M(2k - 3) + 1] + 2 = M(2k - 3) + 3
•••
k–i
= M(2 )+i
•••
k-k
= M(2 ) + k.
Kështu përfundojmë me

M(2k) = M(1) + k = k,

ose pas kthimit në variablin origjinal, n = 2k dhe meqë k = log 2 n, do të kemi,


M(n) = log 2 n ∈ Θ(log n)

Problemi. Le të rikonsiderojmë përsëri numrat e Fibonaçit, përbërësit e vargut të famshëm


0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …, të cilët përcaktohen nga rekurrenca e mëposhtme,

F(n) = F(n – 1) + F(n – 2), për n > 1 (5.5)

dhe dy konditave fillestare

F(0) = 0, F(1) = 1.

Llogaritja e drejtëpërdrejtë e numrave të Fibonaçit


Nëse përpiqemi që të zbatojmë metodën e zëvendësimit për të zgjidhur rekurrencën bazë të
Fibonaçit për të llogaritur numrat e Fibonaçit, do të dështojmë që të marrim një formulë
lehtësisht të zgjidhshme.

Ndërkaq, vëmë në dukje se ekziston një formulë e drejpërdrejtë për llogaritjen e numrave të
Fibonaçit duke përdorur mjetet që ofron matematika për zgjidhjen e rekurrencave lineare
homogjene të rendit të dytë me koeficientë konstantë të formës

ax(n) + bx(n – 1) + cx(n – 2) = 0, (5.6)

ku a, b dhe c janë numra reale ( a ≠ 0) të quajtur koeficientë të rekurrencës dhe x(n) është
termi i përgjithshëm i një vargu të pafundëm që duhet të gjendet. Duke zbatuar këtë teoremë
5 Analiza e efektshmërisë së algoritmeve | 108

për vargun Fibonaçi (a = 1, b = –1, c = –1), me konditat fillestare të dhëna, përftohet formula
për llogaritjen drejtpërsëdrejti të numrave të Fibonaçit

𝟏 � 𝐧 �,
𝐅(𝐧) = �∅𝐧 − ∅ (5.7)
√𝟓

𝟏
� = − ≈ −𝟎. 𝟔𝟔𝟔𝟔𝟔. 9
ku ∅ = �𝟏 + √𝟓�/𝟐 ≈ 𝟏. 𝟔𝟔𝟔𝟔𝟔 dhe ∅ ∅

Është e vështirë të besohet që formula (5.7), që përmban një fuqi të plotë të çfardoshme të një
numri irracional, të prodhojë të gjithë elementet e vargut Fibonaçi (5.5), por ja që ndodh.

Një nga të përfitimet e formulës (5.7) është se ajo tregon që numrat e Fibonaçit F(n) rriten në
mënyrë eksponenciale domethënë, F(n) ∈ Θ(∅𝒏 ). Kjo rrjedh nga fakti që termi i dytë i
formulës (5.7) shkon në zero kur n bëhet pambarimisht e madhe. Në të vërtetë, mund të
provohet që ndikimi i termit të dytë në vlerën e F(n) mund të përftohet nga rrumbullakimi i
vlerës të termit të parë tek numri i plotë më i afërt. Me fjalë të tjera, për çdo numër të plotë
jonegativ
𝟏
𝐅(𝐧) = (∅𝐧 ), e rrumbullakosur tek numri i plotë më i afërt.
√𝟓

Koha e ekzekutimit për llogaritjen e numrave te Fibonaçit


Le të llogaritimin tani kohën e ekzekutimit të algoritmit për llogaritjen e numrave të Fibonaçit
të dhënë më poshtë. Për hir të thjeshtësisë, do të konsiderojmë se veprime të tilla si mbledhja
dhe shumëzimi e kanë koston 1 njësi. (Në fakt, e para në konsideratë duhej marrë madhësia e
numrave të Fibonaçit dhe jo gjetja e një metode llogaritëse më të efektshme.)

Algoritmi rekursiv i mëposhtëm për llogaritjen e numrave te Fibonaçit rrjedh drejtpërdrejt


nga rekurrenca (5.5) dhe dy kushtet fillestare,

// Algoritmi 5.9 Llogaritja e numrave të Fibonaçit


// Të dhëna: një numër i plotë pozitiv, n
// Rezultate: kthen termin e n t e të vargut Fibonaçi

F(n) {
if (n = 0 or n = 1)
then return n
else return F(n – 1) + F(n – 2)
}

Duke qenë se numri i thirrjeve varet nga numri n, atëherë funksionin që do të shërbejë për të
llogaritur kohën e ekzekutimit të algoritmit në fjalë do ta shprehim si një funksion të n.
Veprimi kryesor i algoritmit është mbledhja, prandaj le të shënojmë me M(n) numrin e
mbledhjeve që kryen algoritmi për të llogaritur vargun Fibonaçi. Numrat e veprimeve të
mbledhjes të nevojshme për llogaritjen e F(n – 1) dhe F(n – 2) janë respektivisht M(n – 1)
dhe M(n – 2), plus një mbledhje tjetër për të llogaritur shumën e tyre. Kështu që përftojmë
rekurrencën që pason për numrin e përgjithshëm të mbledhjeve, M(n)

9
Konstantja ∅ njihet me emrin Prerja e artë. Në antikitet ka qenë konsideruar si raporti më i pëlqyeshëm
për brinjët e një drejtkëndëshi dhe është përdour shumë nga skulptorët dhe arkitektët.
5 Analiza e efektshmërisë së algoritmeve | 109

M(n) = M(n – 1) + M(n – 2) + 1 për n > 1 (5.8)


M(0) = 0, M(1) = 0.

Ekuacioni i mësipërm është i ngjashëm me ekuacionin rekurrencial (5.6) por me ndryshimin


se krahu i djathtë nuk është i barabartë me 0. Një rekurrencë e tillë quhet johomogjene.
Zgjidhja me mjetet që ofron matematika jep

𝟏 � 𝐧+𝟏 � − 𝟏
M(n) = �∅𝐧+𝟏 − ∅
√𝟓
Pra, M(n) ∈ Θ(∅𝒏 ), dhe nëse e shprehim përmasën e n me anë të numrit të bit-eve b = ⌊log2
n⌋+1 në paraqitjen e tij binare, klasa e efektshmërisë do të jetë edhe më e keqe, pikërisht,
𝒃
eksponencial i dyfishtë: M(b) ∈Θ(∅𝟐 ).

Klasa shumë e keqe e efektshmërisë të algoritmit është e përcaktuar nga formula llogaritëse
(5.8). Me të vërtetë, algoritmi përbëhet nga dy thirrje rekursive me përmasa më të vogla dhe
që përsëriten.

Në këtë seksion paraqitëm një hyrje për analizën e algoritmeve rekursivë. Këto teknika do të
përdoren në kapitujt e tjerë si dhe do të pasurohen kur të jetë e nevojshme me teknika të tjera.

5.5 Analiza empirike e efektshmërisë së algoritmeve

Në seksionet e mëparshme pamë mënyrën se si algoritmet iterativë dhe rekursivë mund të


analizoheshin matematikisht. Megjithse këto teknika mund të zbatohen me sukses ndaj shumë
algoritmeve të thjeshtë, fuqia e matematikës, bile edhe kur ajo zgjerohet me teknika më të
përparuara, nuk është e pashtershme. Me të vërtetë, bile edhe për disa algoritme në dukje të
thjeshtë, është provuar se ka qenë e vështirë të analizohen matematikisht me saktësi dhe
siguri.

Si një qasje alternative për vlerësimin e efektshmërisë së algoritmeve është kryerja e


eksperimenteve kompjuterike, e quajtur ndryshe edhe analiza empirike e efektshmerise. Kjo
qasje do të paraqitet më poshtë.

Plani i përgjithshëm për analizën empirike të efektshmërisë kohore të algoritmit


1. Të kuptohen qëllimet e eksperimentit.
2. Të përcaktohet se cila metrikë (mënyrë matje) do të përdoret, numri i veprimeve
elementare apo koha fizike e ekzekutimit si dhe njësia matëse në rastin e fundit.
3. Të përcaktohen karakteristikat e kampionit të të dhënave (përmasa, shtrirja, etj).
4. Të përgatitet një program për zbatimin e algoritmit (ose algoritmeve) për eksperimentim.
5. Të prodhohet (krijohet) një kampion i të dhënave.
6. Të ekzekutohet algoritmi (ose algoritmet) me kampionin e krijuar të të dhënave dhe të
regjistrohen rezultatet.
7. Të analizohen të dhënat e përftuara.

Le t’i diskutojmë një nga një hapat e mësipërm.


5 Analiza e efektshmërisë së algoritmeve | 110

Ka disa qëllime të ndryshme që mund të ndiqen me anë të analizës empirike të algoritmeve.


Ato përfshijnë:

• kontrollin e vërtetësisë të një pohimi teorik mbi efektshmërinë e algoritmit;


• krahasimin e efektshmërisë së disa algoritmeve për zgjidhjen e të njëjtit problem zbatime
të ndryshme të të njëjtit algoritëm;
• zhvillimin e ndonjë hipoteze mbi klasën e efektshmërisë të algoritmit;
• verifikimin e efektshmërisë se algoritmit nëpërmjet programit të zbatuar në një kompjuter
të caktuar.

Është e qartë se një skicim i eksperimentit duhet të varet nga çështja se çfarë dëshëron
eksperimentuesi që të marrë si përgjigje. Në veçanti, qëllimi i eksperimentit duhet të
influencojë, nëse nuk e dikton atë, në mënyrën se si do të matet efektshmëria e algoritmit.

Alternativa e parë, numri i veprimeve, është që të përdoret një numërator (disa numëratorë)
në programin që zbaton algoritmin për të numëruar numrin e herëve që do të ekzekutohet
veprimi elementar kryesor i algoritmit. Në përgjithsi ky është një veprim i drejtpërdrejtë, por
në vëmendje duhet patur vetëm mundësia, që veprimi elementar kryesor mund të shfaqet në
disa vende në program dhe që duhet të numërohen të gjitha ekzekutimet e tij. Duhet të
testohet gjithmonë drejtpërsëdrejti se a punon korrekt si për problemin që zgjidh ashtu edhe
për numërimin që duhet të kryejë.

Alternativa e dytë është që të matet koha e ekzekutimit të programit që zbaton algoritmin. Si


alternativë mund të matet koha e ekzekutimit të programit duke kërkuar kohën e sistemit
tamam para se të fillojë fragmenti algoritmik (tfillim) dhe kohën në mbyllje të fragmentit
(tfund) dhe pastaj të llogaritet diferenca (tfillim - tfund). Për shembull, në një program të hartuar
me gjuhën Java, mund të përdoret metoda e quajtur currentTimeMillis() që jep kohën në
milisekonda ose System.nanoTime () që jep kohën në nanosekonda dhe që bëjnë pjesë në
System class.

Ndërkaq është e rëndësishme që të kihen në vëmendje disa fakte. Së pari, koha e sistemit
tipikisht nuk është shumë e saktë, dhe ndonjëherë mund të përftohen rezultate të ndryshme në
ekzekutime të ndryshme të programit me të njëjtat të dhëna fillestare. Zgjidhja, në këtë rast,
është që të kryhen shumë ekzekutime dhe të llogaritet koha mesatare (ose mediana). Së dyti,
duke patur parasysh shpejtësinë e lartë të kompjuterave të sotëm, koha e ekzekutimit mund të
mos fiksohet dhe të raportohet si zero. Truku standard në këto raste është që ekzekutohet
programi shumë herë me cikle shtesë, të matet koha e përgjithshme e ekzekutimit, dhe pastaj
të pjesëtohet me numrin e herëve të përsëritjes së cikleve.

Kështu, matja e kohës fizike ka të meta themelore (varësia nga një makinë konkrete është më
e rëndësishmja) dhe teknike, gjë që nuk ndodh kur numërohet ekzekutimi i veprimit
elementar kryesor. Nga ana tjetër, koha fizike e ekzekutimit furnizon një informacion shumë
specifik mbi performancat e algoritmit në një mjedis llogaritës të veçantë, e cila mund të ketë
më pak rëndësi për eksperimentuesin, të themi sesa klasa asimptotike e efektshmërisë. Përveç
kësaj, duke matur kohën e shpenzuar në segmente të ndryshme të programit, mund krijohet
një keqësim (pinpoint a bottleneck) në performancat e programit, që mund të shpjerë në një
keqinterpretim të veprimit elementare të algoritmit. Marrja e të dhënave të tilla, të quajtura
profilizim (profiling), është një burim i rëndësishëm në analizën empirike të kohës së
ekzekutimit të algoritmit; të dhënat në fjalë mund të përftohen nga mjete të sistemit që janë të
mundshme në shumë mjedise llogaritëse.
5 Analiza e efektshmërisë së algoritmeve | 111

Pavarësisht se cila mënyrë do të zgjidhet për të matur efektshmërinë, ajo e numërimit të


veprimit kryesor apo ajo e matjes së kohës, është e rëndësishme të përcaktohet kampioni i të
dhënave fillestare me anë të të cilit do të eksperimentohet. Shpesh, qëllimi është që të
përdoret një kampion që përfaqson një rast “tipik” të dhënave fillestare; kështu që mbetet të
përcaktohet se çfarë do të thotë “tipike”. Për një klasë algoritmesh, si për shembull algoritmet
për problemin e tregtarit shëtitës, kërkuesit kanë përcaktuar një bashkësi rastesh, që ata i
përdorin si kampion (benchmark). Por ka shumë algoritme të tjerë që nuk kanë të tilla të
dhëna dhe është vetë eksperimentuesi që duhet të vendosë. Po ashtu duhet të përcaktohet (i)
përmasa e kampionit (do të ishte më mirë të fillohej me një përmasë relativisht të vogël dhe
të rritej më pas nëse është e domosdoshme); (ii) fusha e vlerave të të dhënave (tipikisht as
shumë të vogla dhe as shumë të mëdha) dhe (iii) një procedurë për prodhimin e të dhënave
fillestare brenda fushës së zgjedhur. Përmasa e të dhënave mund të ndjekë një farë modeli
(për shembull, 1000, 2000, 3000, …, 10000 ose 500, 1000, 2000, 4000, …, 128000) ose të
prodhohet automatikisht brenda një kufiri të përcaktuar.

Përfitimi kryesor nga ndryshimi i përmasës në përputhje me një model të caktuar është që
ndikimi i saj është më i lehtë për t’u analizuar. Për shembull, nëse përmasa e kampionit
prodhohet duke e dyfishuar atë, mund të llogariten raportet T(2n) / T(n) të metrikës së
vrojtuar T (numri i veprimeve apo koha) dhe të kqyret nëse raporti shfaq ndonjë tendence
sjelljeje tipike për ndonjë nga klasat e efektshmërisë.

Paefektshmëria kryesore e një përmase jo të rastit është mundësia e shfaqes së një sjelljeje
jotipike e algoritmit në lidhje me kampionin e zgjedhur. Për shembull, nëse të gjitha përmasat
në kampionim janë çift dhe algoritmi në shqyrtim do të ekzekutohej shumë më ngadalë nëse
ato do të ishin tek, analiza empirike mund të na shpinte lehtësisht në keqinterpretime.

Një çështje tjetër e rëndësishme përsa i përket përmasës së kampionit në një eksperiment
është nëse do të përfshihen disa raste të dhënash fillestare me të njëjtën përmasë. Nëse pritet
që metrika e vrojtuar të ndryshojë në mënyrë të ndjeshme në varësi të rasteve me të njëjtën
përmasë, do të ishte e dobishme të përfshihen disa raste për çdo përmasë të të dhënës në
kampionim. (Ekzistojnë metoda mjaft të përpunuara në statistikë që ndihmojnë një
eksperimentues të marrë vendime të tilla.) Natyrisht nëse në proces përfshihen raste të
ndryshme të së njëjtës përmasë atëherë duhet të llogaritet mesatarja apo mediana e vlerave të
matura për çdo përmasë dhe të kqyret ajo në vend të vlerësimeve në pika të veçanta.

Një analizë empirike e efektshmërisë së algoritmit, në shumicën e rasteve, kërkon gjithashtu


prodhimin e numrave të rastit (random number). Bile edhe kur vendoset që të dhënat
fillestare do të kenë një formë të caktuar përsa i përket madhësisë së kampionit, prodhimi i të
dhënave do të bëhet në mënyrë të rastit. Është e njohur se prodhimi i numrave të rastit në një
kompjuter digjital, në parim, paraqet një problem të vështirë, problemi mund të zgjidhet
vetëm në mënyrë të përafërt. Kjo është arsyeja që shkencëtarët e kompjuterave preferojnë të
përdorin ata që quhen numra pothuajse të rastit ose numra të pseudorastësishëm
(pseudorandom number). Në aspektin praktik, rruga më e lehtë dhe më e natyrshme për të
përfituar këta numra është që të përdoret një prodhues i numrave të rastit që ndodhet në
librarinë e programeve standard të çdo gjuhe programimi. Gjuha e programimit Java përmban
një metode të quajtur Math.random() që prodhon numra të rastit të shpërndara uniformisht në
gjysëm segmentin [0, 1).
5 Analiza e efektshmërisë së algoritmeve | 112

Rezulatet e përftuara nga një eksperiment duhet të regjistrohen dhe pastaj të paraqiten për
analizë. Rezultatet mund të paraqiten numerikisht në një tabelë ose grafikisht në një sistem
koordinativ kartezian. Është ide e mirë që të përdoren të dyja metodat mbasi secila prej tyre
ka të mira dhe të meta.

E mira e paraqitjes së rezultateve në trajtë tabelare lidhet me mundësinë që ato përpunohen


lehtë. Për shembull mund të llogaritet raporti T(n)/g(n), ku g(n) është një kandidat i
mundshëm për të paraqitur klasën e efektshmërisë të algoritmit në analizë. Nëse algoritmi bën
pjesë në klasën Θ(g(n)), ka shumë mundësi që raportet e lartpërmendura të konvergjojnë në
një vlerë konstante kur n rritet shumë. (Vëmë në dukje se kur themi konstante nuk
nënkuptohet vlera 1). Ose mund të llogaritet raporti T(2n)/T(n) dhe të shihet se si reagon koha
e ekzekutimit kur dyfishohet përmasa e të dhënave. Në këtë rast, raporte të tilla ndryshojnë
shumë pak për algoritme të tipit logaritmik dhe me shpesh konvergjojnë tek 2, 4 dhe 8 për
algoritme linearë, kuadratikë dhe kubikë, respektivisht, për të emërtuar rastet më të qarta dhe
më të zakonshme.

Nga ana tjetër, paraqitja në trajtë grafike, mund të shërbejë për të ndihmuar në zgjedhjen e
klasës së efektshmërisë. Për algoritmet logaritmikë, shpërndarja e pikave ngjan me një formë
të lugët (konkave), figura 5.9(a). Ky fakt e dallon atë nga të gjitha klasat e tjera themelore.
Për algoritmet linearë, pikat kanë tendencë të grumbullohen rreth një vije të drejtë ose më
saktë të përfshihen ndërmjet dy vijave të drejta, figura 5.9(b). Pikat e grafikëve të
funksioneve Θ(n lg n) dhe Θ(n2) do të kenë një formë të mysët (konvekse), figura 5.9(c),
duke u bërë e vështirë për tu dalluar. Një pamje grafike e një algoritmi kubik do të ketë
gjithashtu një pamje të mysët, por ai do të ketë një ngritje më të shpejtë të vlerave të tij. Për të
paraqitur një algoritëm eksponencial, është më e përshtatshme që boshti vertikal të ketë një
shkallëzim logaritmik, në të cilin do të paraqiten vlerat e logaT(n) dhe jo ato të T(n) (baza e
logaritmit zakonisht është 2 dhe jo 10). Në një sistem koordinativ të tillë paraqitjeje, një
grafik i një algoritmi të vërtetë logaritmik do të ngjajë me një funksion linear mbasi T(n) ≈
can ka si rrjedhojë që logb T(n) ≈ logb c + n logb a dhe anasjelltas.

Një nga zbatimet e mundshme të analizës empirike është që të parashikojë performancën e


algoritmit për rastet që nuk përfshihen në ndonjë nga kampionet e eksperimentuara. Për
shembull, nëse vërejmë që raportet T(n) / g(n) janë afër një vlere konstante c, për të dhënat e
kampionit, mund ta përafrojmë T(n) me anë të produktit c g(n) për raste të tjera. Megjithatë
kjo rrugë është e ndjeshme prandaj duhet që të përdoret me kujdes, në veçanti për vlera të n
jashtë fushës së kampionit. (Në matematikë këto quhen ekstrapolime, në kundërvënie me
interpolimin, i cili bën fjalë për të dhëna brenda kampionit). Natyrisht, që ne mund t’i
përdorim teknikat standarde të statistikës për analizë të dhënash dhe të parashikimit. Por
theksojmë që pjesa më e madhe e këtyre teknikave bazohet në hipoteza që mund të jenë ose
që mund të mos jenë të vlefshme për të dhënat eksperimentale në fjalë.

Figura 5.9 Grafikë tipikë: (a) logaritmik; (b) linear; (c) një funksion konveks
5 Analiza e efektshmërisë së algoritmeve | 113

Për ta përfunduar këtë paragraf, është me vlerë të theksojmë ndryshimet ndërmjet një
analize empirike dhe matematike të algoritmit. Forca kryesore e analizës matematikore është
pavarësia e saj nga të dhënat e veçanta; dobësia kryesore është zbatueshmëria e kufizuar,
veçanërisht për shqyrtimin e efektshmërisë mesatare. Forca kryesore e analizës empirike
lidhet me zbatueshmërinë për çdo algoritëm, por rezultatet e saj mund të varen nga kampione
të veçantë të të dhënave fillestare dhe nga kompjuteri ku kryhet eksperimenti.

5.6 Përmbledhje

• Ndër efektshmëritë e algoritmeve dallojmë efektshmërinë kohore dhe efektshmërinë


hapsinore. Efektshmëria kohore tregon sesa shpejt ekzekutohet algoritmi; efektshmëria
hapsinore ka të bëjë me kujtesën shtesë që kërkohet.
• Efektshmëria kohore e një algoritmi matet kryesisht si funksion i përmasës të të dhënave
fillestare me anë të llogaritjes të numrit të herëve që ekzekutohet veprimi kryesor. Veprim
elementar kryesor është veprimi që konsumon më shumë kohë ekzekutimi. Zakonisht
veprimi që konsumon më shumë kohë ndodhet në ciklin më të brendshëm të algoritmit.
• Efektshmëria hapsinore matet me anë të numrit të njësive shtesë për kujtesë të algoritmit.
• Efektshmëria e disa algoritmeve mund të ndryshojë në mënyrë të ndjeshme për të dhëna
fillestare të të njëjtës përmasë. Për algoritme të tillë është e nevojshme që të bëjmë dallim
ndërmjet rastit më të keq, rastit mesatar dhe rastit më të mirë të efektshmërisë.
• Kuadri i përcaktuar për të analizuar efektshmërinë kohore të algoritmit bazohet kryesisht
në rendin e rritjes të kohës së ekzekutimit të algoritmit (ose të kujtesës shtesë të
konsumuar) kur përmasa e të dhënave fillestare tenton në infinit.
• Shënimet asimptotike O, Ω dhe Θ përdoren për të treguar dhe krahasuar rendin asimptotik
të rritjes së funksioneve që shprehin efektshmëritë e algoritmeve.
• Efektshmëritë e një numri shumë të madh algoritmesh ndahen në pak klasa: konstante,
logaritmike, lineare, pothuajse lineare, kuadratike, kubike dhe eksponenciale.
• Mjeti kryesor për të analizuar efektshmërinë kohore të një algoritmi iterativ është
përcaktimi i një funksioni që shpreh numrin e ekzekutimeve të veprimit kryesor dhe gjetja
e rendit të rritjes të funksionit.
• Mjeti kryesor për të analizuar efektshmërinë kohore të një algoritmi rekursiv është
përcaktimi i një relacioni rekurrence që shpreh numrin e herëve që ekzekutohet veprimi
kryesor dhe gjetja e rendit të rritjes së zgjidhjes të relacionit.
• Zgjidhja e relacionit rekurrencial mund të bëhet me metoda të ndryshme. Dy prej tyre janë
metoda e zëvendësimit për së prapi dhe metoda e ekuacionit karakteristik.
• Qartësia e një algoritmi rekursiv mund të maskojë paefektshmërinë e tij.
• Numrat e Fibonaçit janë një varg i rëndësishëm i numrave të plotë në të cilin çdo element
është i barabartë me shumën e dy elementeve fqinjë të mëparshëm. Ekzistojnë disa
algoritme për llogaritjen e numrave të Fibonacit me efektshmëri krejtësisht të ndryshme
• Analiza empirike e një algoritmi kryhet duke ekzekutuar një program që zbaton algoritmin
me kampionë të dhënash për të analizuar rezultatet e matura (me anë të numërimit të
numrit të veprimit elementare apo kohën fizike të ekzekutimit). Ky process shpesh
përfshin prodhimin e numrave të rastit. Zbatueshmëria për çdo algoritëm është fuqia e
kësaj metode; varësia nga një kompjuter i veçantë dhe nga rastet e zgjedhura si kampion
janë dobësia kryesore.
5 Analiza e efektshmërisë së algoritmeve | 114

5.7 Formula të dobishme për analizën e efektshmërisë së algoritmeve

a) Formula të shumimit për analizën e algoritmeve

1. ∑li=k 1 = ���������
1 + 1 + ⋯+ 1 = l − k + 1 ku, l, k të plotë dhe k ≤ l
l−k+1 here
n(n+1) 1
2. ∑ni=1 i = 1 + 2 + ⋯ + n = ≈ n2
2 2
n(n+1)(2n+1) 1
3. ∑ni=1 i2 = 12 + 22 + ⋯ + n2 = ≈ n3
6 3
1
4. ∑ni=1 ik = 1k + 2k + ⋯ + nk ≈ nk+1
k+1
5. ∑ni=0 2i = 2n+1 − 1
an+1 −1
6. ∑ni=0 ai = 1 + a + ⋯ + an = (a ≠ 1)
a−1
7. ∑ni=1 i 2i = 1 ∙ 2 + 2 ∙ 22 + ⋯ + n ∙ 2n = (n − 1)2n+1 + 2
1 1 1
8. ∑ni=1 = 1 + + ⋯ + ≈ ln n + γ, ku γ ≈ 0.5772 ⋯ , konstantja e Eulerit
i 2 n
9. ∑ni=1 log i ≈ nlog n (Nëse n është fuqi e dyshit formula është e saktë)

b) Rregulla të manipulimit të shumave

1. ∑li=k cai = c ∑li=k ai


2. ∑li=k(ai ± bi ) = ∑li=k ai ± ∑li=k bi )
3. ∑li=k ai = ∑li=k ai + ∑li=k ai , k≤m≤l
4. ∑li=k( ai − ai−1 ) = al − ak

c) Formula në lidhje me funksionet Floor dhe Ceiling

Nëse n është një numër i plotë dhe x numër real atëherë:


1. x – 1 < floor(x) ≤ x ≤ ceiling(x) < x + 1
2. floor(x + n) = floor(x) + n , ku x numër real dhe n numër i plotë
3. ceiling(x + n) = ceiling(x) + n , ku x numër real dhe n numër i plotë
4. floor(n/2) + ceiling(n/2) = n
5. ceiling (lg(n+1)) = floor(lg n) +1

d) Formula nga kombinatorika

1. Numri i permutacioneve të një bashkësie me n elementë; P(n) = n!


𝑛!
2. Numri i kombinacioneve të klasës k të n elementeve: 𝐶𝑛𝑘 =
𝑘!(𝑛−𝑘)!

e) Numri i nënbashkësive që formohen nga një bashkësi me n elementë: 2𝑛

5.8 Ushtrime për Kapitullin 5

1. Si e dhënë fillestare e një problemi shërben një numër i plotë n. Cila është përmasa e të
dhënës fillestare e shprehur në bit?
5 Analiza e efektshmërisë së algoritmeve | 115

2. Le të supozojmë se të dhënat fillestare të një problemi përbëhen nga n numra të plotë,


secili prej të cilëve zë 2 Bytes në kujtesë. Cila është përmasa e të dhënave fillestare sipas
modelit RAM? Po e shprehur në bit?

3. Për të dhëna fillestare të përmasës n një algoritëm (për shembull, Insertion sort) kryhen
8n2 veprime elementare, ndërsa një algoritëm tjetër (për shembull, Merge sort) kryhen 64
n log n veprime. Për cilat vlera të n-së algoritmi Insertion sort është “më i mirë” sesa
algoritmi Mergesort?

4. Për secilin nga algoritmet e mëposhtëm tregoni: i) se cili mund të shërbejë si parametër i
natyrshëm i madhësisë së të dhënave fillestare; ii) veprimin elementar më të
rëndësishëm të tij (më të kushtueshëm në kohë ose hapsirë); iii) nëse numërimi i
veprimit elementar mund të jetë i ndryshëm për të dhëna fillestare të së njëjtës përmasë.
a. Llogaritja e shumës së n numrave
b. Llogaritja e n!
c. Gjetja e vlerës më të madhe në një listë prej n vlerash
d. Algoritmi i përdorur për shumëzimin e dy numrave me laps-dhe-letër

5. Le të konsiderojmë algoritmet e mbledhjes dhe shumëzimit të dy matricave kuadratike të


rendit n, mbështetur në përkufizimin e tyre. Cili është veprimi elementar më i
kushtueshëm për secilin nga algoritmet? Sa herë kryhet ai në funksion të rendit n të
matricës? Po si funksion i numrit të përgjithshëm të elementeve të matricave të të
dhënave?

6. Le të konsiderojmë një variacion të mundshëm të algoritmit të kërkimit linear që skanon


listën e elementeve dhe kthen numrin e shfaqjeve të një elementi të kërkuar në listën e
dhënë. A do të ndryshojë efektshmëria e tij nga efektshmëria e algoritmit klasik të
kërkimit linear?

7. Në një sirtar janë vendosur 22 doreza: 5 palë janë të kuqe, 4 palë janë të verdha dhe 2 palë
janë jeshile. Dorezat mund të zgjidhen në errësirë dhe ngjyra e saj mund të kontrollet
vetëm pasi të jetë zgjedhur. Cili është numri i dorezave të nevojshme që duhet të
merren për të patur të paktën një palë me të njëjtën ngjyrë në rastin më të mirë? Po në
rastin më të keq?

8. Eliminimi i Gausit, algoritmi klasik për zgjidhjen e sistemeve të n ekuacioneve linearë


1 3
me n të panjohura, kërkon afërsisht n shumëzime, që është edhe veprimi më i
3
kushtueshëm i algoritmit.
a. Sa herë më shumë duhet që të punojë algoritmi i eliminimit të Gausit për një sistem
prej 1000 ekuacionesh ndaj një sistemi me 500 ekuacione?
b. Nëse duam të blejmë një kompjuter 1000 herë më të shpejtë sesa ai që kemi
aktualisht, sa herë do të rriten përmasat e sistemit që do të zgjidhen nëse shpenzojnë të
njëjtën sasi kohe?

9. Për secilin nga funksionet e mëposhtëm tregoni se si do të ndryshojë vlera e funksionit


kur vlera e variablit do të katërfishohet.
a. log2n b. √𝑛 c. n d. n2 e. n3 f. 2n
5 Analiza e efektshmërisë së algoritmeve | 116

10. Tregoni nëse funksioni i parë i secilit prej çifteve të mëposhtëm ka një rend rritjeje
më të vogël, të njëjtin ose më të madh (me afërsinë e një shumëzuesi konstant) se
funksioni i dytë.
a. n(n + 1) dhe 2000n2 b. 100n2 dhe 0.01n3
c. log2 n dhe ln n d. (log2n)2 dhe log2n2
e. 2n-1 dhe 2n f. (n – 1)! dhe n!

11. Të vlerësohet numri i përsëritjeve të ciklit do/while , në fragmentet algoritmikë të


mëposhtëm, për rastin më të keq të organizimit të të dhënave. Çfarë i dallon këta dy
fragmente në lidhje me cilësitë e algoritmeve?

Fragmenti i parë Fragmenti i dytë


n←5; input (n) ;
do do
input (m); input (m);
n ← n – 1; n ← n – 1;
while (m = 0 or n = 0); while (m = 0 or n = 0);

12. Të vlerësohet koha e ekzekutimit të fragmentit algoritmik të mëposhtëm në funksion


të variablit n me anë të tre modeleve të vlerësimit të kohës së ekzekutimit.
i ← 1;
while (i ≤ n) do {
i ← i + 1;
j ← j * 3 + 42;
}

13. Të vlerësohet koha e ekzekutimit të fragmentit algoritmik të mëposhtëm në funksion të


variablit n me anë të tre modeleve të vlerësimit të kohës së ekzekutimit.
i ← 1;
while (i ≤ 2*n) do{
i ← i + 1;
j ← j*3 + 4367;
}

14. Të vlerësohet koha e ekzekutimit të fragmentit algoritmik të mëposhtëm në funksion të n


me anë të tre modeleve të vlerësimit të kohës së ekzekutimit.
i ← 1;
while (i ≤ n) do {
for j ← 1 to n do
s ← s + 1;
i ← i + 1;
}

15. Të vlerësohet koha e ekzekutimit të fragmentit algoritmik të mëposhtëm në funksion


të n me anë të tre modeleve të vlerësimit të kohës së ekzekutimit.
i ← 1;
while (i * i ≤ n) do
i ← i + 1;
5 Analiza e efektshmërisë së algoritmeve | 117

16. Jepet fragmenti algoritmik i mëposhtëm. Të vlerësohet koha e ekzekutimit të


fragmentit në funksion të n me anë të modelit të numërimit të veprimit kryesor.
i ← 0;
while ( i < n) do {
for j ← 0 to n – 1 do
if ( i < j)
then s ← s + 1
else s ← s – 1;
i ←i + 1;
}

17. Jepet fragmenti algoritmik i mëposhtëm. Të vlerësohet koha e ekzekutimit të


fragmentit në funksion të n me anë të modelit të numërimit të veprimit kryesor.
k ← 1;
while k ≤ n do{
j ← 1;
while j ≤ k do{
shuma ← shuma + 1;
j ← j +1;
}
k ← 2 * k;
}

18. Jepet fragmenti algoritmik i mëposhtëm. Të vlerësohet koha e ekzekutimit të


fragmentit në varësi të n me anë të modelit të numërimit të veprimit kryesor.
j ← 1;
i ← 2;
while (i ≤ n) do{
b[j] ← a[i];
j ← j + 1;
i ← i * i;
}

19. Për fragmentin algoritmik të mëposhtëm të vlerësohet koha e ekzekutimit në funksion


të variablit n në lidhje me veprimin kryesor.
i ← 1;
while i ≤ n do{
j ← 1;
while j ≤ n do {
k ← 1;
while k ≤ n do{
shuma ← shuma + 1;
k ← k * 3;
}
j ← j * 2;
}
i ← i + 1;
}
5 Analiza e efektshmërisë së algoritmeve | 118

20. Le të jenë dhënë algoritmet e mëposhtëm ProcA dhe ProcB (ProcB thirret nga ProcA). Të
përcaktohet numri i veprimeve sipas modelit të numërimit të veprimit kryesor për ProcA
në lidhje me të dhënën fillestare n, numër natyral.

Algoritmi ProcA(n){ Algoritmi ProcB(m) {


s ← 0; s ← 0;
for i ← 1 to n do for i ← 1 to m do
s ← s + ProcB(i); s ← s + i;
return s return s
} }

21. Supozohet se një algoritëm për të llogaritur produktin e dy matricave të rendit n me


numra të plotë kërkon 3n3 veprime elementare. Për thjeshtësi do të supozohet se të gjitha
veprimet elementare kërkojnë 10 ns (1 ns = 10-9 sek) në një platformë të caktuar
Hardware/Software. Cila do të jetë koha e ekzekutimit në platformën në fjalë kur a) n =
100 dhe b) n = 1000. Cili është raporti i ekzekutimit të dy rasteve shprehur në kohë
ekzekutimi?

22. Le të supozojmë se kemi për të zgjidhur të njëjtin problem me shtatë algoritme me


kompleksitete të ndryshme në të njëjtin kompjuter, në të cilin një veprim kryesor
ekzekutohet në një mikrosekondë (1 ms = 10-6 sek). Të plotësohet tabela e mëposhtme
për kohën e ekzekutimit (shprehur në sekonda apo njësi tjetër matjeje të përshtatshme)
për rastet e zgjedhura të të dhënave fillestare dhe të komentohen rezultatet.

Kompleksiteti n = 10 n = 10 2 n = 10 3 n = 10 6
1. √n
2. n + 5
3. 2*n
4. n2
5. n2 + n
6. n3
7. 2n

23. Cili nga shënimet Ο, Θ 𝑑ℎ𝑒 Ω është më i përshtatshëm për të treguar klasën e
efektshmërisë së algoritmit të Kërkimit linear.
a. për rastin më të keq
b. për rastin më të mirë

24. Duke përdorur shënimet joformale për Ο, Θ 𝑑ℎ𝑒 Ω përcaktoni nëse pohimet e
mëposhtme janë të vërteta ose të rreme.
𝐚. 𝑛(𝑛 + 1)/2 ∈ Ο(𝑛3 ) 𝐛. 𝑛(𝑛 + 1)/2 ∈ Ο(𝑛2 )
𝐜. 𝑛(𝑛 + 1)/2 ∈ Θ(𝑛3 ) 𝐝. 𝑛(𝑛 + 1)/2 ∈ Ω(𝑛)

25. Tregoni klasën Θ(𝑔(𝑛)) në të cilën ndodhet secili nga funksionet e mëposhtëm.
𝐚. (n2 + 1)10 𝐛. √10n2 + 7n + 3
n
𝐜. 2n log( n + 2)2 + (n + 2)2 log 𝐝. 2n+1 + 3n−1
2

26. Duke përdorur përkufizimin e shënimit O e madhe të provohet që:


a. f(n) = 100n + 6 ∈ O(n)
5 Analiza e efektshmërisë së algoritmeve | 119

b. f(n) = 10n2 + 4n + 2 ∈ O(n2)


c. f(n) = 3n2 – 100n + 6 ∉ O(n)
d. f(n) = 5n + 10 ∈ O(n2).
e. f(n) = (n² + 2n + 3) / (n + 1) ∈ O(n).
f. f(n) = 2n ∈O(3n) por f(n) = 3n ∉ O(2n).

27. Duke përdorur përkufizimin e shënimit Ω e madhe të provohet që:


a. f(n) = 3n + 2 ∈ Ω(n)
b. f(n) = 10n2 + 4n + 2 ∈ Ω(n2)
c. f(n) = 3n2 – 100n +6 ∉ Ω(n3)
d. f(n) = 3n2 – 3n – 5 ∈ Ω( n2 ).
e. f(n) = 5n – 1000 √n ∈ Ω(n)

28. Duke përdorur përkufizimin e shënimit Θ e madhe të provohet që:


a. f(n) = n2/2 – 3n ∈ Θ(n2).
b. f(n) = 3n2 + 7n – 5 ∈ Θ(n2).
c. f(n) = 1/2 n2 – 3n ∈ Θ(n2).

a. Në tabelën e mëposhtme paraqiten vlerat e disa funksioneve që shfaqen shpesh në


analizën e algoritmeve. Këto vlera të thonë që funksionet: log n, n, nlog n, n2, n3, 2n,
n!, janë vendosur në shtyllat e tabelës sipas rendit rritës të rritjes së tyre. A e provojnë
këto vlera këtë fakt me siguri matematike?

n log2n n nlog2n n2 n3 2n n!
10 3.3 101 3.3·101 102 103 103 3.6·106
102 6.6 102 6.6·102 104 106 1.3·1030 9.3·10157
103 10 103 1.0·104 106 109
104 13 104 1.3·105 108 1012
105 17 105 1.7·106 1010 1015
106 20 106 2.0·107 1012 1018

29. Të renditen sipas rendit të rritjes funksionet e mëposhtme:

n, √n, n1.5, n2, nlog n, nlog(log n), n log2 n, n log n2, 2/n, 2n, 2n/2, 37, n2log n, n3

Vërtetimi nuk kërkohet por duhet të njihet përdorimi i rregullit të L’Hopitalit për të
provuar renditjen.

30. Të vërtetohet që nëse f(n) ∈ Ο(g(n)) atëherë f(n)k ∈ O(g(n)k) për çdo k ≥1.

31. Jepni një shembull për një funksion rritës pozitiv f(n), për të cilin 𝑓(𝑐𝑐) ∉ Θ(𝑓(𝑛)) për
ndonjë konstante pozitive c.

32. Jepni një shembull për një funksion rritës pozitiv f(n), për të cilin 𝑓(𝑐𝑐) ∈ Θ(𝑓(𝑛)) për
çdo konstante pozitive c.
5 Analiza e efektshmërisë së algoritmeve | 120

33. Gjeni numrin e plotë më të vogël k të tillë që f(n) është O(nk) për secilin nga funksionet e
mëposhtme:
a. f(n) = 2n2 + n3log n
b. f(n) = 3n5 + (log n)4
c. f(n) = (n4 + n2 +1) / (n4 + 1)
d. f(n) = (n3 + 5logn) / (n4 + 1)

34. Të përcaktohet klasa e kompleksitetit e fragmentit algoritmik të mëposhtëm me anë të


shënimit O e madhe.
shuma ← 0;
for k ← 0 to n – 1 do
for j ← 0 to k – 1 do
shuma ← shuma + 1;
for k ← 0 to n – 1 do
for j ← 0 to k – 1 do
for p ← 0 to j – 1 do
shuma ← shuma + 1;

36. Provoni formalisht, duke përdorur përkufizimin e shënimit Θ, pohimin se për çdo
konstante k > 0 dhe funksion f(n) është e vërtetë që 𝑘𝑘(𝑛) ∈ Θ(𝑓(𝑛).

37. Janë dhënë dy funksione: f(n) = n log n + n dhe g(n) =n2/2. Cili pohim është i vërtetë:
𝑓(𝑛) ∈ O�𝑔(𝑛)� apo 𝑔(𝑛) ∈ O(𝑓(𝑛))?

38. Janë dhënë funksionet 𝑙𝑙𝑙 𝑛 dhe 𝑙𝑙𝑙√𝑛. Cili pohim është e vërtetë: 𝑙𝑙𝑙√𝑛 ∈
𝑂(log 𝑛) apo log 𝑛 ∈ 𝑂(log √𝑛)?

39. Krahasoni rendet e rritjes të funksioneve 2n dhe 2n+1.

40. Llogaritni shumat e mëposhtme


a. 1 + 3 + 5 + 7 + ⋯ + 999
b. 2 + 4 + 8 + 16 + ⋯ + 1024
c. ∑ni=3 1
d. ∑n+1
i=3 i
e. ∑n−1
i=0 i(i + 1)
n
f. ∑j=1 3j+1
g. ∑ni=1 ∑nj=1 ij
1
h. ∑ni=1
i(i+1)
41. Gjeni rendin e rritjes së shumave të mëposhtme.
a. ∑n−1 2
i=0 (i + 1)
2

b. ∑n−1
i=2 log i
2

c. ∑ni=1(i + 1) 2i−1
d. ∑n−1 i−1
i=0 ∑j=0(i + j)

42. Varianca e një vargu prej n matjesh 𝑥1 , 𝑥2 , ⋯ , 𝑥𝑛 mund të llogaritet si më poshtë:


5 Analiza e efektshmërisë së algoritmeve | 121

𝟐
∑𝑛
𝑖=1(𝑥𝑖 −𝑥)
2 ∑𝑛
𝑖=1 𝑥𝑖 ∑𝒏 𝟐 𝒏
𝒊=𝟏 𝒙𝒊 −�∑𝒊=𝟏 𝒙𝒊 � /𝒏
𝑘𝑘 𝑥 = ose
𝑛−1 𝑛 𝒏−𝟏

Gjeni dhe krahasoni numrin e pjesëtimeve, shumëzimeve dhe mbledhje/zbritjeve


(mbledhjet dhe zbritjet zakonisht llogariten sëbashku) që kërkohen për të llogaritur
variancën e secilës prej këtyre formulave.

43. Më poshtë jepet një version i algoritmit të kërkimin linear për të gjetur pozicionin e një
vlere a të dhënë, në një tabelë. Algoritmi kthen pozicionin e vlerës së kërkuar nëse ajo
gjendet në tabelë përndryshe kthen vlerën -1. Të llogaritet numri i veprimeve elementare
për kërkimin e vlerave 8 dhe 6 në tabelën t = {3, -2, 0, 7, 5, 4, 0, 8, - 3, -1, 9, 12, 20, 5}.

// Algoritmi Kërkimi një pozicionit te një vlere në një tabelë


// Të dhëna: Një tabelë t[0:13] dhe një vlere a

kerkoPozicion(14, t, a)
i←0;
while i ≤ n – 1 do {
if (t[i] = a)
then return i ;
i ← i +1;
}
return -1

44. Le të konsiderojmë algoritmin e mëposhtëm.


// Algoritmi i panjohur
// Të dhëna fillestare: n, një numër i plotë jonegativ
// Rezultate: ?
Mister(n) {
s ← 0;
for i ← 1 to n do
s ← s + i * i;
return s
}

a. Çfarë llogarit algoritmi?


b. Cili është veprimi i tij kryesor?
c. Sa herë ekzekutohet veprimi kryesor?
d. Cila është klasa e efektshmërisë kohore të algoritmit?
e. Propozoni një përmirësim apo një algoritëm krejtësisht më të mirë dhe tregoni klasën
e efektshmërisë së tij. Nëse mendoni se nuk mund të bëhet atëherë përpiquni ta
provoni këtë.

45. Le të konsiderojmë algoritmin e mëposhtëm.


// Algoritmi i panjohur
// Të dhëna: n, numra reale të vendosur në një tabelë A[0..n-1]
// Rezultate: ?
mister(n, a) {
vlera1 ← a[0];
vlera2 ← a[0];
5 Analiza e efektshmërisë së algoritmeve | 122

for i←1 to n-1 do{


if a[i] < vlera1
then vlera1 ← a[i];
if a[i] > vlera2
then vlera2 ← a[i];
{
return vlera2 – vlera1
}

a. Çfarë llogarit algoritmi?


b. Cili është veprimi i tij kryesor?
c. Sa herë ekzekutohet veprimi kryesor}
d. Cila është klasa e efektshmërisë kohore të algoritmit?
e. Propozoni një përmirësim apo një algoritëm krejtësisht më të mirë dhe tregoni klasën
e efektshmërisë së tij. Nëse mendoni se nuk mund të bëhet atëherë përpiquni ta
provoni këtë.

46. Le të konsiderojmë algoritmin e mëposhtëm.


//Algoritmi i panjohur
// Të dhëna: n, numra reale të vendosur në një tabelë a[0..n-1,0..n-1]
// Rezultate: ?
enigma(n, a){
for i ← 1 to n – 2 do
for j ← i + 1 to n – 1 do
if a[i, j] ≠ a[j, i]
then return false;
return true
}

a. Çfarë llogarit algoritmi?


b. Cili është veprimi i tij më i kushtueshëm?
c. Sa herë ekzekutohet veprimi më i kushtueshëm?
d. Cila është klasa e efektshmërisë kohore të algoritmit?
e. Propozoni një përmirësim apo një algoritëm krejtësisht më të mirë dhe tregoni klasën
e efektshmërisë së tij. Nëse mendoni se nuk mund të bëhet atëherë përpiquni ta
provoni këtë.

47. Përmirësoni zbatimin e algoritmit të shumëzimit të matricave të dhënë në leksion duke


reduktuar numrin e mbledhjeve të bëra nga algoritmi. Çfarë efekti do të kenë këto
ndryshime në efektshmërinë e algoritmit?

48. Vërtetoni formulën


𝑛 𝑛(𝑛 + 1)
� 𝑖 = 1 + 2 + ⋯+ 𝑛 =
𝑖=1 2
ose nëpërmjet induksionit matematik ose duke ndjekur rrugën e nxënësit 10 vjeçar Karl
Friedrich Gauss 10, i cili kur u rrit u bë një nga matematikanët më të mëdhenj të të gjitha
kohërave.

10
Carl Friedrich Gauss (1777 – 1855), gjigand në fushën e matematikës ku ka dhënë kontribute në matematikën
teorike dhe llogaritëse.
5 Analiza e efektshmërisë së algoritmeve | 123

49. Le të konsiderojmë versionin e mëposhtëm të një algoritmi të rëndësishëm që do të


studjohet më vonë.
// Algoritmi eleminimi i Gausit
// Të dhëna: një tabelë dy-dimensionale a[0..n-1, 0..n+1] me numra realë
// Rezultati : ?
eleminimiGaus(n, A) {
for i ← 0 to n – 2 do
for j ← i + 1 to n – 1 do
for k ← i to n do
A[j, k] ← A[j, k] – A[i, k] * A[j, i] / A[i, i];
}

a. Përcaktoni klasën e efektshmërisë së algoritmit.


b. Cila është paefektshmëria e dukshme e këtij fragmenti dhe në çfarë mënyre mund të
eliminohet për të përshpejtuar algoritmin ?

50. Fqinjësia von Newmann. Sa katrorë njësi prodhohen nga algoritmi që fillon me një
katror të vetëm dhe në secilin nga n iteracionet e tij shton katrorë të rinj në të gjitha
drejtimet nga jashtë ? (Në teorinë e automatëve celularë, përgjigja paraqet numrin e
qelizave në fqinjësinë von Newmann të rendit n.) Rezultatet për n = 0, 1 dhe 2
ilustrohen më poshtë.

51. Të zgjidhen relacionet rekurrenciale të mëposhtëm


a. x(n) = x(n - 1) + 5 për n > 1, x(1) = 0
b. x(n) = 3x(n - 1) për n > 1, x(1) = 4
c. x(n) = x(n - 1) + n për n > 0, x(0) = 0
d. x(n) = x(n / 2) + n për n > 1, x(1) = 1 (të zgjidhet për n = 2k)
e. x(n) = x(n / 3) + 1 për n > 1, x(1) = 1 (të zgjidhet për n = 3k)

52. Ndërtoni dhe zgjidhni një relacion rekurrencial për numrin e thirrjeve të kryer nga
fakt(n), algoritmi rekursiv për llogaritjen e n!

53. Le të konsiderojmë algoritmin e mëposhtëm rekursiv për llogaritjen e shumës së n


numrave kubikë të parë : S(n) =13 + 23 + … + n3.
// Algoritmi Llogaritja e shumës së kubeve të numrave natyrore
// Të dhëna: n, një numër i plotë pozitiv
// Rezultati: Shuma e n numrave kubikë të parë

shumaKubike(n) {
if n = 1
then return 1
else return S(n – 1) + n*n*n
}
5 Analiza e efektshmërisë së algoritmeve | 124

a. Ndërtoni dhe zgjidhni një relacion rekurrencial për numrin e herëve që ekzekutohet
veprimi kryesor algoritmit.
b. Krahasoheni këtë algoritëm me algoritmin iterativ për llogaritjen e të njëjtës shumë.

54. Le të konsiderojmë algoritmin rekursiv të mëposhtëm:


// Algoritmi i panjohur
// Të dhëna: n, një numër i plotë pozitiv
// Rezultati: ?
enigma(n) {
if n = 1
then return 1
else return enigma (n - 1) + 2*n – 1
}

a. Çfarë llogarit ky algoritëm?


b. Ndërtoni një relacion rekurrencial për numrin e shumëzimeve të kryer nga ky
algoritëm dhe zgjidheni atë.
c. Ndërtoni një relacion rekurrencial për numrin e përgjithshëm të mbledhjeve dhe të
zbritjeve të kryera nga ky algoritëm dhe zgjidheni atë.

55. Hartoni një algoritëm për të llogaritur 2n për çdo numër të plotë jonegativ n që mbështetet
në formulën
2n = 2n - 1 + 2n - 1
a. Ndërtoni një relacion rekurrencial për numrin e mbledhjeve të kryera nga algoritmi
dhe zgjidheni atë.
b. Ndërtoni një diagramë të thirrjeve rekursive për këtë algoritëm dhe numëroni
numrin e thirrjeve të kryera nga algoritmi.
c. A është ky një algoritëm i mirë për të zgjidhur këtë problem?

56. Le të konsiderojmë algoritmin rekursiv të mëposhtëm


// Algoritmi i panjohur
// Të dhëna: n numra reale të vendosur në një tabelë a[0..n – 1]
// Rezultati: ?

mister(n, a){
if n = 1
then return a[0]
else { temp ← mister(n – 1, a);
if temp ≤ a[n – 1]
then return temp
else return a[n – 1];
}
}

a. Çfarë llogarit algoritmi?


b. Ndërtoni një relacion rekurrencial për numërimin e veprimit kryesor të algoritmit dhe
zgjidheni atë.

57. Le të konsiderojmë një algoritëm tjetër për të zgjidhur problemin e ushtrimit 56, i cili në
mënyrë rekursive e ndan tabelën në dy pjesë.
5 Analiza e efektshmërisë së algoritmeve | 125

// Algoritmi i panjohur
// Të dhëna: n numra reale të vendosur në një tabelë A[0..n-1]
// Rezultati: ?
mister(m, d, A]){
if m = d
then return A[m]
else {mes ←(m + d) div 2;
temp1 ← mister(m, mes, A);
temp2 ← mister(mes+1, d, A);
if temp1 ≤ temp2
then return temp1
else return temp2;
}
}

a. Ndërtoni një relacion rekurrencial për numrin e veprimeve të veprimit kryesor dhe
zgjidheni atë.
b. Cili nga algoritmet min1 dhe min2 është më i shpejtë? A mund të sugjeroni ndonjë
algoritëm për problemin që ata zgjidhin që mund të jetë më i efektshëm sesa këta të
dy?

58. Përcaktori i një matrice kuadratike të rendit n

𝑎11 ⋯ 𝑎1𝑛
A=� ⋮ ⋱ ⋮ �,
𝑎𝑛1 ⋯ 𝑎𝑛𝑛

shënuar me det A, mund të përcaktohet si a11 për n = 1 dhe për n > 1 me anë të
formulës rekursive
𝑛
det 𝐴 = � 𝑠𝑗 𝑎1𝑗 det 𝐴𝑗,
𝑗=1

ku sj është +1 nëse j është tek dhe -1 nëse j është çift, a1j është elementi në rreshtin 1
dhe shtyllën j, dhe Aj është një matricë kuadratike e rendit n - 1 e përftuar nga matrica
A me anë të fshirjes së rreshtit të saj i dhe shtyllës j.
a. Ndërtoni një ekuacion rekurrencial për numrin e shumëzimeve të bëra nga algoritmi
duke përdorur përkufizimin rekursiv.
b. Pa zgjidhur ekuacionin e rekurrencës, a mund të thoni rendin e rritjes së zgjidhjes
duke e krahasuar me n!?
6 Teknika e forcës brutale dhe kërkimi shterues | 126

6 Teknika e forcës brutale dhe kërkimi shterues

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të kuptojë teknikën e forcës brutale në konceptimin e algoritmeve;


• të njohë algoritmin e renditjes selection sort dhe ta zbatojë atë;
• të njohë algoritmin e renditjes bubble sort dhe ta zbatojë atë;
• të njohë algoritmin e renditjes insertion sort dhe ta zbatojë atë;
• të njohë algoritmin e mbështetur në teknikën e forcës brutale për kërkimin e motivit;
• të njohë algortimin e mbështetur në teknikën e forcës brutale për gjetjen çiftit më të afërt
të pikave në plan;
• të njihet me problemet e patrajtueshme të kërkimit shterues

Pasi paraqitëm kuadrin dhe metodat për analizën e algoritmeve tani jemi gati për të filluar
diskutimet mbi teknikat e dizajnit (konceptimit) të algoritmeve. Secili nga leksionet pasuese i
dedikohet një teknike të veçantë. Subjekti i këtij leksioni është forca brutale (brute force), më
e thjeshta nga të gjitha teknikat e dizajnit të algoritmeve. Ajo mund të përshkruhet si më
poshtë:

Forca brutale është një rrugë e drejtpërdrejtë për të zgjidhur një problem, e mbështetur
zakonisht në formulimin e problemit dhe në përkufizimet e koncepteve të përfshira në të.

“Forca” e nënkuptuar në përkufizimin e teknikës është ajo e kompjuterit dhe jo e dikujt me


intelekt. “Thjesht bëje” është mënyra tjetër e të shprehurit të përcaktimit të rrugës së forcës
brutale. Dhe shpesh, teknika e forcës brutale, është me të vërtetë më e lehta për ta zbatuar.

Deri tani kemi paraqitur një sërë problemesh, algoritmet e të cilëve kryesisht janë bazuar në
metodën e forcës brutale por pa i cilësuar si të tillë. Përmendim problemin e llogaritjes
iterative të n-faktorialit, kërkimin linear, gjetjen e vlerës më të madhe apo më të vogël në një
tabelë, shumëzimin e dy matricave, etj.

Megjithëse ndodh rrallë që të jetë burim i algoritmeve të mprehtë apo të efektshëm, teknika e
forcës brutale shihet si një teknike e rëndësishme e hartimit të algoritmeve për arsyet e
mëposhtme:

• Në ndryshim nga teknikat e tjera, forca brutale zbatohet në një klasë shumë të gjerë
problemesh. (Në fakt, duket që të jetë i vetmi përafrim i përgjithshëm për të cilin është e
vështirë të gjesh probleme që mund ta shmangin).
• Për disa probleme të rëndësishme (për shembull, renditjeja, kërkimi, shumëzimi i
matricave, gjetja e motivit), teknika e forcës brutale furnizon algoritme të aryeshëm të
paktën për disa vlera praktike pa u kufizuar në përmasat e problemit.
• Shpenzimi për të hartuar një algoritëm më të efektshëm mund të jetë i pajustifikuar nëse
është e nevojshme të zgjidhen pak raste dhe algoritmi i forcës brutale mund t’i zgjidhë
këto raste me një shpejtësi të pranueshme.
• Edhe pse në përgjithësi i paefektshëm, algoritmi i forcës brutale mund të jetë i dobishëm
për të zgjidhur probleme me përmasa të vogla.
6 Teknika e forcës brutale dhe kërkimi shterues | 127

• Algoritmi i forcës brutale mund të shërbejë për qëllime të rëndësishme teorike apo
mësimore, domethënë, si një masë me anë të së cilës do të gjykohen alternativa më të
efektshme për zgjidhjen e një problemi.

6.1 Renditja

Renditja është padyshim një nga problemet më themelore të algoritmikës. Sipas studimeve të
kryera, një pjesë mjaft e madhe e kohës së procesorit shpenzohet për problemet e renditjes.
Nëse të dhënat janë të renditura algoritmet e shumë problemeve realizohen në mënyrë të
efektshme. Përmendim me këtë rast problemet e kërkimit (Searching), çiftit të pikave më të
afërta (closest pair problem), uniciteti i elementeve (element uniqueness), shpërndarjes së
dëndurive (frequency distribution), përzgjedhjes (selection), etj.

Duke shënuar me a[i], i = 0,1, ..., n – 1, elementet e një tabele dhe duke marrë në konsideratë
konceptet e krahasimit të përfaqësuara me anë të shenjave >, ≥, <, ≤, si për të dhëna numerike
ashtu dhe për të dhëna të tipeve të tjera, renditja është ripozicionimi i vlerave të elementeve
në mënyrë të tillë që të plotësojnë një nga kushtet e mëposhtme:

a. a[0] < a[2] < ... < a[n – 2] < a[n – 1], tabela është renditur në rendin rritës
b. a[0] ≤ a[2] ≤ ... ≤ a[n – 2] ≤ a[n – 1], tabela është renditur në rendin jozbritës
c. a[0] > a[2] > ... > a[n – 2] > a[n – 1], tabela është renditur në rendin zbritës
d. a[0] ≥ a[2] ≥ ... ≥ a[n – 2] ≥ a[n – 1], tabela është renditur në rendin jorritës

Të dhënat që renditen mund të jenë numerike, alfabetike ose alfabetiko-numerike. Kur të


dhënat janë numerike renditja quhet numerike dhe renditja bazohet në vlerën e numrave. Kur
të dhënat janë alfabetike renditja quhet alfabetike dhe renditja bazohet ne rendin alfabetik.
Ndërsa kur të dhënat janë alfabetiko-numerike renditja kryhet sipas rendit leksikografik, ku
secilës shenjë i është dhënë një vlere numerike (p.sh., sipas kodit ASCII).

Në këtë leksion do të paraqesim dhe do të analizojmë tre nga algoritmet më të thjeshtë të


renditjes që mbështeten në krahasimin e elementeve. Ka shumë arsye se pse trajtohen në
fillim algoritmet e thjeshta.

Së pari ata shërbejnë për të dhënë terminologjinë dhe mekanizmat bazë tipikë duke furnizuar
një minimun dijesh të domosdoshme për të studjuar algoritme më të ndërlikuar.

Së dyti në disa zbatime është më e përshtatshme që të përdoren algoritmet e thjeshtë në


krahasim me algoritmet e “fuqishëm”.

Së fundmi disa nga këta algoritme të thjeshtë mund të shtrihen në metoda më të përgjithshme
ose mund të përdoren për të përmirësuar efektshmërinë e algoritmeve më të fuqishëm.

Struktura e algoritmeve të renditjes


Nga pikpamja e strukturës algoritmet e renditjes ndahen në dy grupe: algoritme për renditje
të bazuara në krahasimin e elementeve (comparison sort) dhe algoritme që nuk bazohen në
krahasime të elementeve por në cilësitë e veçanta të të dhënave. Në algoritmet e klasës së
parë futen algoritmet me kompleksitet kohor pothuajse linear dhe kuadratik ndërsa në klasën
e dytë algoritmet me kompleksitet linear.
6 Teknika e forcës brutale dhe kërkimi shterues | 128

Qëndrueshmëria e algoritmit
Nëse të dhënat janë organizuar në disa tabela korresponduese nga ana kuptimore dhe renditja
kryhet sipas elementeve të njërës prej tyre, thuhet se një algoritëm renditjeje është i
qëndrueshëm (stable) nëse ai e ruan “pozicionin” relativ të elementeve të tabelave të tjera
kur ka elemente me të njëjtën vlerë në tabelën që drejton renditjen.
Le të kemi tabelën t= {4; 1; 3; 4} me numra dhe tabelën e={alfa, beta, bama, delta} me emra
të supozojmë se duam t’i renditim sipas rendit alfabetik të vlera të tabelës t.

(a) Të dhënat (b) Renditje e qendrueshme (c) Renditje e paqendrueshme


[t] [e] [t] [e] [t] [e]
4 alfa 1 beta 1 beta
1 beta 3 gama 3 gama
3 gama 4 alfa 4 delta
4 delta 4 delta 4 alfa

Në renditjen e qendrueshme (b) elementi i parë “alfa” dhe elementi i katërt “delta” kanë të
njëjtin pozicion relativ ndërsa në renditjen e paqendrueshme, renditja (c) është e
paqendrueshme mbasi nuk ruhet pozicioni relativ fillestar i elementeve.

Renditja në vend
Në qoftë se për një algoritëm nuk përdoret kujtesë shtesë që të ndihmojë renditjen atëherë
thuhet se algorimi ekzekutohet në vend (in place).

6.1.1 Algoritmi i renditjes Selection sort

Algoritmi i renditjes me përzgjedhje (Selection sort) është njëri nga algoritmet më të thjeshtë
të renditjes. Le të konsiderojmë se duam të kryejmë renditjen e elementeve në rendin
jozbritës.

Algoritmi fillon të skanojë të gjithë tabelën për të gjetur elementin më të vogël dhe ta
përkëmbejë atë me të parin, duke vendosur më të voglin në pozicionin përfundimtar në
tabelën e renditur. Pastaj skanohet tabela duke filluar nga elementi i dytë për të gjetur më të
voglin ndër n – 1 elementet e mbetur dhe përkëmbehet me elementin e dytë, duke vendosur
në këtë mënyrë të dytin më të vogël me pozicionin e tij përfundimtar. Në përgjithësi, në
kalimin e itë nëpër tabelë, me vlera nga 1 tek n – 1, algoritmi kërkon elementin më të vogël
ndër n – i elementet e mbetur dhe e përkëmben atë me elementin a[i]:

𝑎 0 ≤ 𝑎1 ≤ ⋯ ≤ 𝑎𝑖−1
������������� ∥ 𝑎 𝑖 , ⋯ , 𝑎𝑚𝑚𝑚 , ⋯ , 𝑎𝑛−1
�������������
𝑖 𝑒𝑒𝑒𝑒𝑒𝑒𝑒𝑒 𝑛𝑒̈ 𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝 𝑝𝑒̈ 𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟 𝑛−𝑖 𝑒𝑒𝑒𝑒𝑒𝑒𝑒𝑒 𝑝𝑒̈ 𝑟 𝑡 ′ 𝑢 𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟

Pas n – 1 hapash tabela është e renditur. Ky algoritëm quhet algoritmi i përzgjedhjes sepse
vazhdimisht përzgjedh elementin më të vogël ndër ata që kanë mbetur.

// Algoritmi 6.1 Algoritmi i renditjes Selection sort


// Të dhëna: tabela a[0:n – 1] me n elemente që mund të krahasohen
// Rezultate: tabela me elemente në rendin jozbritës

SelectionSort (n, a) {
6 Teknika e forcës brutale dhe kërkimi shterues | 129

for i ← 0 to n – 2 do
{ min ← i; // min, indeksi i elementit me vlerën më të vogël
for j ← i + 1 to n – 1 do
if (a[j] ≤ a[min])
then min ← j; // indeksi i elementit më të vogël nga ai aktual
swap (a[i], a[min]); // gjen elementin më të vogël në hapin e i-të
}
return
}

Në tabelën 6.1 paraqitet dinamika ekzekutimit të algoritmit Selection sort, pas çdo hapi të
ciklit të jashtëm, për vlerat fillestare {89, 45, 68, 90, 29, 34, 17}. Elementi në bold tregon
elementin më të vogël të gjetur, ndërsa elementet në të majtë të vijës vertikale janë në
pozicionin e tyre përfundimar dhe nuk konsiderohen më në ciklet pasardhëse. Cikli i jashtëm
ekzekutohet 7 herë.

Tabela 6.1 Dinamika e ekzekutimit të algoritmit Selection sort

Veprimi Elementet e tabelës


a[0] a[1] a[2] a[3] a[4] a[5] a[6]
Të dhënat fillestare 89 45 68 90 29 34 17
Pas hapit 1 (i = 0) 17 45 68 90 29 34 89
Pas hapit 2 (i = 1) 17 29 68 90 45 34 89
Pas hapit 3 (i = 2) 17 29 34 90 45 68 89
Pas hapit 4 (i = 3) 17 29 34 45 90 68 89
Pas hapit 5 (i = 4) 17 29 34 45 68 90 89
Pas hapit 6 (i = 5) 17 29 34 45 68 89 90

Analiza kohore e algoritmit është e thjeshtë. Si parametër për të vlerësuar algoritmin do të


shërbejë përmasa e tabelës, n. Struktura e algoritmit përbëhet nga dy cikle të përfshirë, ku
cikli i brendshëm përsëritet një numër të ndryshueshëm herësh që varet nga cikli i jashtëm.
Veprimi elementar kryesor është krahasimi i elementeve a[j] < a[min]. Numri i herëve që ai
ekzekutohet varet nga madhësia e tabelës dhe jepet nga shuma e mëposhtme:

(n−1)n
K(n) = ∑n−2 n−1 n−2 n−2
i=0 ∑j=i+1 1 = ∑i=0 [(n − 1) − (i + 1) + 1] = ∑i=0 (n − 1 − i) = 2

Ky rezultat na jep të drejtë të themi që algoritmi Selection sort bën pjesë në klasën e
efektshmërisë Θ(n2).

Një karakteristikë e algoritmit SelectionSort është se koha e tij ekzekutimit varet shumë pak
nga shkalla e renditjes së të dhënave fillestare. Kërkimi i vlerës më të vogël gjatë një skanimi
të të dhënave nuk jep informacion rreth pozicionit të elementit pasues më të vogël. Kush do
të kryejë eksperimente me këtë algoritëm mund të habitet nga verifikimi se koha e
ekzekutimit është pak a shumë e njëjtë si në një tabelë të renditur, si në një tabelë me të gjithë
elementet e barabartë ashtu në një tabelë me elemente të çfardoshëm. Si përfundim algoritmi
Selection sort, bën pjesë në klasën Θ(n2), për të gjitha llojet e të dhënave fillestare. Vemë në
dukje që numri i përkëmbimeve është vetëm Θ(n) dhe kjo cilësi e dallon në mënyrë pozitive
këtë algoritëm nga shumë algoritme të tjerë të renditjes.
6 Teknika e forcës brutale dhe kërkimi shterues | 130

Algoritmi SelectionSort i paraqitur me lart nuk kërkon kujtesë shtesë dhe është i
qëndrueshëm.

6.1.2 Algoritmi i renditjes Bubble sort

Algoritmi Bubble sort, është një zbatim tjetër i teknikës të forcës brutale për problemet e
renditjes, që krahason elementet fqinjë në tabelë dhe i përkëmben ata nëse janë jashtë
renditjes së kërkuar. Duke e kryer këtë në mënyrë të përsëritur, e shtyjmë elementin më të
madh në pozicionin e fundit në tabelë (me indeksin me të madh). Hapi i dytë shtyn elementin
e dytë më të madh që ka mbetur dhe kështu derisa pas n – 1 hapash të ciklit të jashtëm, tabela
është e renditur. Hapi i itë (0 ≤ i ≤ n – 2) i algoritmit Bubble sort mund të paraqitet në formën
e diagramës së mëposhtme:

?
𝑎0 , ⋯ , 𝑎𝑗 ⇔ 𝑎𝑗+1 , ⋯ , 𝑎𝑛−𝑖−1 ∥ 𝑎𝑛−𝑖 ≤ ⋯ ≤ 𝑎𝑛−1
�����������
𝑛𝑒̈ 𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝 𝑒 𝑡𝑡𝑡𝑡 𝑝𝑒̈ 𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟

// Algoritmi 6.2 Algoritmi i renditjes Bubble sort


// Të dhëna: tabela a[0:n – 1] me n elemente që mund të krahasohen
// Rezultate: tabela a e renditur në rendin jozbritës

bubbleSort (n, a) {
for i ← 0 to n – 2 do
for j ← 0 to n – 2 – i do
if (a[j] > a[j + 1])
then swap(a[j], a[j + 1]);
return
}

Vlerësimi i kohës së ekzekutimit është krejt i ngjashëm me algoritmin Selection sort. Numri i
krahasimeve të elementeve, në versionin e dhënë më lart, është i njëjtë për të gjitha tabelat e
rendit n. Ai përftohet nga një shumim, shumë i ngjashëm me atë të Selection sort:
(n−1)n
K(n) =∑n−2 n−2−i
i=0 ∑j=0 1 = ∑n−2 n−2
i=0 [n − 2 − i − 0 + 1] = ∑i=0 (n − 1 − i) = 2
∈ Θ(n2 )

Ndërkaq, numri i përkëmbimeve të elementeve P(n) varet nga të dhënat. Në rastin më të keq,
kur tabela e dhënë është e renditur në rendin zbritës, numri përkëmbimeve të elementeve
është i njëjtë me numrin e krahasimit të elementeve:

(𝑛−1)𝑛
Pkeq(n) = K(n) = ∈ Θ(𝑛2 )
2
Në figurën 6.1, paraqiten dy hapat e para të algoritmit ndaj vargut me të dhëna {89, 45, 68,
90, 29, 34, 17}. Pas përkëmbimit të dy vlerave është paraqitur një rresht i ri. Elementet në
krahun e djathtë të vijës vertikale janë në pozicionin e duhur dhe nuk merren në konsideratë
në hapat pasues.
6 Teknika e forcës brutale dhe kërkimi shterues | 131

?
89 45 ? 68 90 29 34 17
45 89 68 90 29 34 17
? ?
45 68 89 90 29 34 17
?
45 68 89 29 90 34 17
?
45 68 89 29 34 90 17
45 68 89 29 34 17 90
? ? ?
45 68 89 29 34 17 90
?
45 68 29 89 34 17 90
?
45 68 29 34 89 17 90
45 68 29 34 17 89 90
etj

Figura 6.1 Pamje e pjesëshme e gjurmës së ekzekutimit të algoritmit Bubble sort

Është e mundur që algoritmi Bubble sort të modifikohet në mënyrë të tillë, që procesi të


përfundojë sapo tabela të jetë renditur. Kjo tregon që koha e llogaritjes varet nga organizimi i
të dhënave fillestare. Mjafton vetëm një kalim i ciklit të jashtëm në rast se tabela është
tashmë e renditur, ndërkaq kalimi i i-të kërkon (n – i) krahasime dhe përkëmbime në rast se
ai është i renditur në rendin e kundërt. Megjithë përmirësimet e mundshme që mund t’i
bëhen, algoritmi mbetet përsëri në klasën Θ(n2). Edhe ndërmjet algoritmeve elementare të
renditjes ai është më i keqi dhe vetëm emri i tij nuk mjafton për ta bërë tërheqës. Mësimi më i
rëndësishëm në këtë rast është se edhe kur realizohet një algoritëm me teknikën e forcës
brutale, ai edhe mund të përmirësohet.

Algoritmi i mësipërm Bubble sort nuk përdor kujtesë shtesë dhe është algoritëm i
qëndrueshëm.

6.1.3 Algoritmi i renditjes Insertion sort

Duke ndjekur idenë themelore të teknikës, le të supozojmë që problemi më i vogël i renditjes


së tabelës a[0..n – 2] tashmë është zgjidhur për të dhënë një tabelë të renditur të përmasës n –
1: a[0] ≤ a[1] ≤ • • • ≤ a[n – 2]. Si do të përfitonim nga kjo zgjidhje e problemit të vogël për të
përftuar zgjidhjen e problemit origjinal duke marrë në konsideratë elementin a[n – 1]? Është
e qartë se ne duhet të gjejmë një pozicion të përshtatshëm për a[n – 1] ndër elementet e
renditur dhe ta futim aty.

Zakonisht kjo bëhet duke lëvizur në nëntabelën e renditur, nga e djathta në të majtë, derisa të
takohet elementi i parë më i vogël ose i barabartë me a[n – 1] për të futur a[n – 1] tamam pas
këtij elementi. Algoritmi që rezulton quhet Insertion sort.

Megjithëse algortimi Insertion sort bazohet në një ide krejtësisht rekursive, është më e
efektshme që të zbatohet në formën nga poshtë-lart, domethënë, iterative. Siç tregohet në
figurën 6.2 duke filluar me a[1] dhe duke përfunduar me a[n – 1], a[i] futet në vendin e
përshtatshëm ndër i elementet e parë të tabelës që tashmë janë renditur (në ndryshim nga
Selection sort, ku në përgjithësi nuk janë në pozicionet e tyre përfundimtare).

𝑎[0] ≤ ⋯ ≤ 𝑎[𝑗] <


����������� 𝑎[𝑗 + 1] ≤ ⋯ ≤ 𝑎[𝑖 − 1] ∥ 𝑎[𝑖 ] ⋯ 𝑎[𝑛 − 1]
�����������������
𝑚𝑒̈ 𝑡𝑒̈ 𝑣𝑣𝑣𝑣𝑒̈ 𝑙 𝑜𝑜𝑜 𝑡𝑒̈ 𝑏𝑏𝑏𝑏𝑏𝑏𝑏𝑏𝑒̈ 𝑚𝑚 𝐴[𝑖] 𝑚𝑒̈ 𝑡𝑒̈ 𝑚𝑒̈ 𝑑ℎ𝑒𝑒𝑒 𝑠𝑠 𝐴[𝑖]
6 Teknika e forcës brutale dhe kërkimi shterues | 132

Figura 6.2 Një iteracion i Insertion sort: a[i] futet në pozicionin e duhur ndër
elementet e mëparshëm tashmë të renditur

Idea e algoritmit Insertion sort, për renditjen e një vargu vlerash, është frymëzuar nga loja me
letra bixhozi. Lojtari përpiqet që letrat që ka në dorë t’i grupojë sipas luleve dhe sipas
vlerave. Procedura qëndron në faktin që letra e radhës futet në vendin e duhur, ndërmjet
letrave të tjera, tashmë të renditura. Futja e një letre në vendin e duhur realizohet duke i hapur
një vend nëpërmjet zhvendosjes majtas apo djathtas të letrave të tjera.

Algoritmi 6.3 InsertionSort, paraqet një zbatim të metodës, ndoshta jo më të mirin por atë që
është dhënë në versionin fillestar (kur është krijuar për herë të parë).

Algoritmi 6.3 Algoritmi i renditjes Insertion sort


// Të dhëna: n vlera të vendosura në një tabelë a[0:n–1]
// Rezultate: tabela a[0:n–1] e renditur në rendin jozbritës

insertionSort(n, a){
for i ← 1 to n – 1 do
{ v ← a[i]; // rezervohet vlera e rradhës që të mos fshihet
j ← i – 1; // indeks për të bredhje nga e djathta në të majtë
while (j ≥ 0 and a[j] > v) do // cikël për të gjetur elementin e parë më të vogël
{ a[j + 1] ← a[j];
j ← j – 1;
}
a[j + 1] ← v
}
return
}

Në tabelën 6.2 ilustrohet dinamika e renditjes të vargut {89, 45, 68, 90, 29, 34, 17}. Një vijëz
e vogël vertikale ndan pjesën e renditur (jo përfundimtare) të tabelës nga elementet që mbeten
ndërsa elementi që ka radhën për t’u futur, është kuadratuar.

Tabela 6.2 Gjendja e tabelës pas përfundimit të çdo cikli të jashtëm


Gjendja Elementet e tabelës
a[0] a[1] a[2] a[3] a[4] a[5] a[6]
Të dhënatfillestare 89 45 68 90 29 34 17
Pas hapit 1 45 89 68 90 29 34 17
Pas hapit 2 45 68 89 90 29 34 17
Pas hapit 3 45 68 89 90 29 34 17
Pas hapit 4 29 45 68 89 90 34 17
Pas hapit 5 29 34 45 68 89 90 17
Pas hapit 6 17 29 34 45 68 89 90

Veprimi elementar kryesor i algoritmit është krahasimi i elementeve a[j] > v. (Por pse të mos
jetë j ≥ 0, kur ky, me siguri, është më i shpejtë se i mëparshmi në një zbatim në kompjuterat e
sotshëm. Për arsyen e thjeshtë se ky kontroll mund të eleminohet në një version me
sentinelë.)
6 Teknika e forcës brutale dhe kërkimi shterues | 133

Është e qartë se numri i krahasimeve të elementeve në këtë algoritëm varet nga natyra e të
dhënave. Në rastin më të keq, të dhënat fillestare të renditura në rendin zbritës, krahasimi
a[j] > v ekzekutohet numrin më të madh të herëve, domethënë, për çdo j = i – 1 , ..., 0.
Ndërkaq a[j + 1] ← a[j], ndodh atëherë dhe vetëm atëherë kur a[j] > a[i] për j = i – 1, ..., 0.
(Vëmë në dukje se jemi duke përdorur faktin që në iteracionin e itë të Insertion sort të gjithë
elementet që paraprijnë a[i], janë i elementet e parë të të dhënave, megjithëse të renditur.)
Kështu, për rastin më të keq të të dhënave fillestare do të kemi a[0] > a[1] (për i = 1), a[1] >
a[2] (për i = 2), ..., a[n – 2] > a[n – 1] (për i = n – 1). Me fjalë të tjera rasti më i keq i të
dhënave fillestare është një tabelë me vlera rigorozisht zbritëse. Numri i krahasimeve të
elementeve për të dhëna të tilla do të jetë:

(n−1)n
Ckeq (n) = ∑n−1 i−1 n
i=1 ∑j=0 1 = ∑i=1 i = ∈ Θ(n2 ).
2

Pra, në rastin më të keq, algortimi Insertion sort kryen të njëjtin numër krahasimesh si
algoritmi Selection sort.

Në rastin më të mirë, krahasimi a[j] > v ekzekutohet vetëm një herë në çdo përsëritje të ciklit
të jashtëm. Ai ndodh atëherë dhe vetëm atëherë kur a[i – 1] ≤ a[i] për çdo i = 1, ..., n – 1,
d.m.th të dhënat fillestare janë tashmë të renditura në rendin jozbritës. (Megjithëse duket “e
logjikëshme” që rasti më i mirë i një algoritmi të ndodhë kur problemi është tashmë i
zgjidhur, nuk ndodh gjithmonë kështu, siç do ta shihni në kapitullin pasardhës, në algoritmin
Quicksort). Kështu, për tabelat e renditura, numri i krahasimeve të elementeve do të jetë,

T(n) = ∑n−1
i=1 1 = n − 1 ∈ Θ(n)

Duket sikur kjo performancë shumë e mirë, në rastin e tabelave të renditura, nuk është shumë
dobishme në vetvete, mbasi nuk mund të presim të dhëna kaq të përshtatshme. Por
megjithatë, skedarë pothuajse të renditur, takohen në shumë zbatime reale, dhe Insertion sort
ka një performancë të shkëlqyer për raste të tilla.

Së fundmi, vëmë në dukje që algoritmi Insertion sort nuk ka nevojë për kujtesë shtesë
prandaj është një algoritëm që ekzekutohet në vend si dhe është një algoritëm i qëndrueshëm.

6.2 Kërkimi i motivit

Problemi i gjetjes së motivit (String matching problem) takohet gjatë përpunimit të teksteve
me anë të Editorëve të tekstit (Word Editor), në njohjen e gjuhëve (language recognition), në
bioinformatikë (bio-computing), në përpunimin e figurave (image processing), gjatë kërkimit
në Internet me anë të motorëve të kërkimit (Google, Yahoo, etj), gjatë kërkimit në një fjalor
elektronik, etj.

Ne do të përdorim termin motiv (pattern) për t’iu referuar një vargu shenjash në një alfabet të
dhënë, Σ. Në varësi të problemit, alfabeti Σ mund të jetë i formuar nga 26 shkronjat e vogla të
alfabetit anglez, ose nga 128 shenjat e standardit ASCII të paraqitjes së shenjave, ose nga
numrat e plotë nga 0 deri në 255 (që paraqet të gjitha vlerat e kombinimeve të mundshme në
një byte), etj.

Zakonisht motivi që kërkohet është shumë i shkurtër apo i shkurtër (ndoshta 5-20 karaktere)
ndërsa teksti ku kërkohet është shumë më i gjatë (mijra apo më shumë karaktere). Duke qenë
6 Teknika e forcës brutale dhe kërkimi shterues | 134

se ky është një veprim mjaft i zakonshëm në informatikë, janë hartuar shumë algoritme të
efektshëm kërkimi. Në këtë kapitull do të paraqitet një algoritëm i thjeshtë, i hartuar me
teknikën e forcës brutale.
Shtrimi i problemit: jepet një varg i formuar nga n shenja i quajtur tekst (text) dhe një varg i
formuar nga m shenja (m ≤ n) i quajtur motiv ose mostër. Të gjendet se në cilin pozicion të
tekstit fillon motivi, nëse ka të tillë.

Duke shënuar me {p1,...,pm} motivin dhe {t1,…,tn} tekstin, nga algoritmi kërkojmë që të gjejë
indeksin i, që është indeksi i shenjës më të majtë në përputhjen e parë të motivit në tekst, e
tillë që ti = p1, …, ti + j = pj, …, ti + m - 1 = pm :

t1 … ti … ti+j-1 … ti+m-1 … tn teksti T

p1 … pj … pm motivi P

Një algoritëm i tipit të forcës brutale për problemin e kërkimit të motivit është shumë i qartë:
radhit motivin përkundrejt m shenjave të para të tekstit dhe fillon kontrollin e përputhjes së
çifteve të shenjave nga e majta në të djathtë derisa të m çiftet e shenjave të jenë përputhur
(atëherë ndalon algoritmi) ose të takohet një mospërputhje. Në rastin e fundit, zhvendoset
motivi një pozicion djathtas dhe rifillohet krahasimi i shenjave, duke filluar përsëri me
shenjën e parë të motivit dhe homologut të tij në tekst.Vemë në dukje se, pozicioni i fundit në
tekst, në cilin mund të fillojë një kërkim i ri është n – m + 1 (pozicionet e tekstit janë
indeksuar nga 1 deri tek n). Tej këtij pozicioni nuk ka mjaftueshëm shenja, që të mund të
krahasohet i gjithë motivi; prandaj nuk ka kuptim që algoritmi të kryejë më krahasime).

// Algoritmi 6.4 Kërkimi i motivit në versionin zhvendosje nga e majta në të djathtë


// Të dhëna: n,numri i karaktereve të tekstit të vendosura në tabelën t[1:n],
// m, numri i karaktereve motivit të vendosura në tabelën p[1:m]
// Rezultate: i, pozicioni i parë në tekst ku fillon përputhja
// ose – 1 nëse kërkimi është i pasuksesshëm

StringMatchingBF(n, t, m, p) {
for i ← 1 to n – m + 1 do {
j ← 1;
while (j ≤ m ) and t[i + j – 1] = p[j]) do
j ← j + 1;
if j > m
then return i;
}
return –1
}

Një diagramë e veprimeve të algoritmit paraqitet në figurën 6.3. Shenjat e motivit që


krahasohen me homologët e tyre në tekst janë theksuar me bold.

Vëmë re që për këtë rast, algoritmi e zhvendos motivin pothuajse gjithmonë pas një
krahasimi të vetëm. Por, rasti më i keq është akoma më i keq: algoritmit mund t’i duhen që t’i
bëjë të m krahasimet (të gjithë karakteret e motivit)) para se të kryejë zhvendosjen dhe kjo
mund të ndodhë për secilën nga n – m + 1 përpjekjet. Kështu që në rastin më të keq algoritmi
6 Teknika e forcës brutale dhe kërkimi shterues | 135

bën pjesë në klasën Θ(nm). Për një kërkim tipik fjalësh në një tekst të shkruar me një gjuhë
natyrale, duhet të presim që zhvendosjet më të shumta të ndodhin pas një numri të pakët
krahasimesh, (shih përsëri shembullin). Është vënë re që për kërkime në tekste të rastësishme
efektshmëria të ketë qenë lineare, domethënë Θ(n + m) = Θ(n). Janë krijuar disa algoritme më
të sofistikuar dhe më të efektshëm për kërkimin e motivit. Më i njohuri prej tyre është krijuar
nga R. Boyer dhe J. Moore dhe më tej i përmirësuar nga R. Horspool.

Pozicioni 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
Teksti A S K U S H N U K V U R I R E
N U K
N U K
N U K
Zhvendosjet
N U K
e
N U K
motivit
N U K
N U K
N U K
Figura 6.3 Diagrama e kërkimit të një motivi në një tekst

Në qoftë se problemi kërkon që të gjenden të gjitha përputhjet, algoritmi i kërkimit duhet të


vazhdojë deri sa të kontrollohet i gjithë teksti. Le të supozojmë se duam të gjejmë të gjitha
shfaqjet e motivit ABAB në tekstin ABABABCCABAB. Siç shihet motivi shfaqet në
pozicionet 1, 3 dhe 9.

Shënim. Është e mundshme që kërkimi i gjetjes së motivit në një tekst të fillojë nga e djathta
në të majtë.

6.3 Problemi i çiftit të pikave më të afërta dhe i mbështjellëses konvekse

Në këtë seksion do të konsiderojmë rrugën e drejtpërdrejtë për dy probleme të mirënjohura që


kanë të bëjnë me një bashkësi pikash të fundme në plan. Këta probleme, krahas interesit të
tyre teorik, shtrihen edhe në dy fusha të rëndësishme të zbatuara: gjeometria llogaritëse dhe
kërkimi operacional.

Problemi i çiftit të pikave më të afërta në plan


Problemi i çiftit më të afërt të pikave (Closest Pair Problem) është një problem interesant. Për
herë të parë ai është marrë në shqyrtim në fushën e gjeometrisë llogaritëse (computational
geometry) herët në vitin 1970 nga M.I. Shamos dhe D. Hoey. Ky problem ka gjetur zbatime
në fusha të tilla si grafika (graphics), shikimi me anë të kompjuterave (computer vision),
sistemet informative gjeografike (Geographic Information System - GIS), në aviacion dhe
navigim detar për kontrollin e trafikut, etj. Për momentin ne do të shqyrtojmë vetëm rastin
plan, siç thuhet problemi në hapsirën 2-D. Problemi mund të përgjithësohet për hapësira më
të larta.

Problemi. Të gjendet distanca ndërmjet dy pikave më të afërta në një bashkësi pikash në


plan. Pikat specifikohen me anë të koordinatave të tyre karteziane (x, y). Distanca ndërmjet
dy pikave Pi(xi,yi) dhe Pj(xj,yj) është distanca euklidiane e llogaritur sipas formulës:

d�Pi , Pj � = �(xi − xj )2 + (yi − yj )2


6 Teknika e forcës brutale dhe kërkimi shterues | 136

Idea e algoritmit, sipas teknikës së forcës brutale, është e thjeshtë: të llogaritet distanca
ndërmjet çdo çifti të mundshëm (gjithsej n(n–1)/2 çifte) dhe në çdo hap, nëse gjendet një
distancë më e shkurtër se ajo e mëparshmja atëherë ajo rezervohet si distanca më e shkurtër
e çastit. Algoritmi closestPair paraqet një algoritëm të hartuar sipas teknikës të forcës brutale
për llogaritjen e distancës së pikave më të afërta në plan.

// Algoritmi 6.5 Gjen distancën ndërmjet dy pikave më të afërta në plan


// Të dhëna: Për n pika (n ≥ 2), tabela e absisave x[1:n] dhe ordinatave y[1: n]
// Rezultate: dmin, distanca e çiftit më të afërt

closestPair(n, x , y) {
1. dmin ← + ∞ ; // vlera më e madhe në një kompjuter të caktuar
2. for i ← 1 to n 1 do
3. for j ← i + 1 to n do
4. d ←min(d, sqr((x[i] – x[j])2 + (y[i] – y[j])2));
5. return d

Në fakt, llogaritja e rrënjës katrore nuk është aq e thjeshtë sa duket. Për shumë numra të plotë
rrënja katrore është numër irracional. Për të rritur shpejtësinë e algoritmit mund t’i
shmangemi llogaritjes së rrënjës katrore duke krahasuar (xi – xj)2 + (yi – yj)2 drejtpërsëdrejti.
Veprimi bazë atëherë do të jetë ngritja në katror e një numri dhe jo rrënja katrore.

Përderisa llogaritja e distancës ndërmjet dy pikave kërkon një kohë konstante dhe gjithsej
janë n(n–1)/2 çifte të mundshme pikash atëherë algoritmi bën pjesë në klasën Θ(n2) .

Problemi i mbështjellëses konvekse


Problemi i mbështjellëses konvekse (Convex Hull Problem) i një bashkësie me pika është një
problem tjetër interesant në gjeometrinë llogaritëse. Ai shërben si një nga elementet kryesorë
për zbatime të ndryshme si:

 Zbulimi i përplasjeve në video game


 Zbulimi i formave vizuale të objekteve që përputhen
 Kartografi (mapping)
 Përcaktimi i rrugës së lëvizjes së robotit, etj.

Për shembull, roboti lëvizës në planetin Mars, pajiset me një hartë rrugësh për të lëvizur
rrotull një shkëmbi. Mbështjellësja konvekse përdoret për të gjetur rrugën më të shkurtër për
të kaluar pengesën, duke përdorur hartën dhe duke përcaktuar pikat ku guri puqet me truallin.

Le të japim në fillim përkufizimin e një bashkësie konvekse.

Përkufizimi i konveksit. Një bashkësi pikash (e fundme apo e pafundme) në plan quhet
konveks (convex) nëse për çdo dy pika P dhe R të kësaj bashkësisë, i gjithë segmenti i
drejtëzës me skaje në pikat P dhe R i përket bashkësisë.
6 Teknika e forcës brutale dhe kërkimi shterues | 137

Bashkësia e paraqitur në figurën 6.4.a është konvekse, e tillë është edhe një drejtëz, një
trekëndësh, një katërkëndësh dhe në përgjithësi çdo shumëkëndësh konveks, 11 një rreth dhe i
gjithë plani. Nga ana tjetër bashkësia e paraqitur në figurën 6.4.b dhe çdo bashkësi e fundme
prej dy apo më shumë pikash të dallueshme, kufiri i çdo shumëkëndëshi konveks dhe
perimetri i rrethit nuk janë bashkësi konvekse.
(a) (b)

Figura 6.4 (a) Bashkësi konvekse. (b) Bashkësi jokonvekse

Tashmë jemi gati për të dhënë kuptimin e mbështjellëses konvekse. Në mënyrë intuitive,
mbështjellësja konvekse e një bashkësie prej n pikash në plan është shumëkëndëshi konveks
më i vogël që përmban të gjitha ato (qofshin në brendësi apo në kufijt e tyre). Me humor, D.
Harel, e shtroi si problemi i n tigrave në gjumë të cilët duhet të rrethohen me një gardh me
perimetrin më të vogël. Një interpretim tjetër më pak “i frikshëm” është që disa gozhdë të
ngulura në një kompesato mjaft të gjerë të qarkohen me një fije elastike të tendosur në
maksimum. Mbështjellësja konvekse është zona e rrethuar nga fija elastike, figura 6.5.

Figura 6.5 Interpretimi i mbështjellëses konvekse si një fije elastike

Një përkufizim formal i mbështjellëses konvekse që është i zbatueshëm për një bashkësi
arbitrare, duke përfshirë edhe bashkësinë e pikave që mund të shtrihen në të njëjtën drejtëz,
vijon më poshtë:

Përkufizim. Mbështjellësja konvekse e një bashkësie S prej n pikash në plan është bashkësia
konvekse më e vogël që përmban S. (Kërkesa “më e vogël” nënkupton që mbështjellësja
konvekse e S duhet të jetë një nënbashkësi e çdo bashkësie konvekse që përmban S.)

Në qoftë se S është konveks, është e qartë se mbështjellësja e tij konvekse është ajo vetë. Në
qoftë se S është bashkësi prej dy pikash, mbështjellësja e tij konvekse është segmenti që
bashkon ato dy pika. Në qoftë se S është një bashkësi prej tre pikash që nuk shtrihen në të
njëjtën drejtëz, mbështjellësja e tyre konvekse është trekëndëshi me kulme në tre pikat e
dhëna; në qoftë se tre pikat shtrihen në të njëjtën drejtëz, mbështjellësja konvekse është
segmenti që bashkon pikat, me skaje në dy pikat më të larguara. Për shembull, mbështjellësja
e një bashkësie prej disa pikash paraqitet në figurën 6.6.

11
Me trekëndësh, katërkëndësh dhe në përgjithsi me shumëkëndësh konveks nënkuptojmë një zonë, domethënë
bashkësinë e pikave brenda dhe në kufi të figurës në fjalë.
6 Teknika e forcës brutale dhe kërkimi shterues | 138

P6

P4 P2 P5

P3 P1

Figura 6.6 Mbështjellësja konvekse e një bashkësie prej gjashtë pikash

Duke kqyrur me kujdes shembujt e mësipërm mbrijmë në teoremën e mëposhtme.

Teoremë. Mbështjellësja e një bashkësie S prej n > 2 pikash (jo të gjitha në të njëjtën drejtëz)
është shumëkëndëshi konveks me kulme në disa nga pikat e bashkësisë S. (Nëse të gjitha
pikat shtrihen në të njëjtën drejtëz, shumëkëndëshi degjeneron në një segment por gjithmonë
me dy pika fundore të bashkësisë S.)

Problemi i mbështjellëses konvekse është problemi i ndërtimit të mbështjellëses konvekse për


një bashkësi prej n pikash të dhëna. Për të zgjidhur atë, kemi nevojë të gjejmë pikat që do të
shërbejnë si kulme të shumëkëndëshit në fjalë. Matematikanët i quajnë “pika skajore” kulmet
e tilla të shumëkëndëshit. Me përkufizim, një pikë skajore (extreme point) e një bashkësie
konvekse është pika e kësaj bashkësie që nuk është pikë mesi e asnjë segmenti me pika
fundore në bashkësi. Për shembull, pika skajore të një trekëndëshi janë të tre kulmet e tij,
pika skajore të rrethit janë të gjitha pikat e perimetrit, dhe pika skajore të mbështjellëses
konvekse të bashkësisë prej gjashtë pikash të figurës 6.6 janë pikat P1, P5, P6, P4 dhe P3.

Për të zgjidhur problemin, e kemi të nevojshme të dimë diçka më shumë se cilat nga n pikat e
bashkësisë së dhënë janë pika skajore të bashkësisë të mbështjellëses konvekse: është e
nevojshme të njohim se cilat janë çiftet e pikave që duhet të lidhen për të formuar kufirin e
mbështjellëses konvekse. Këtë çështje mund ta trajtojmë gjithashtu si listim i pikave skajore
në drejtimin orar apo antiorar.

Teknika e forcës brutale mbështetet në faktin që një segment drejtëze, që bashkon dy pika Pi
dhe Pj, është pjesë e kufirit të mbështjellëses konvekse atëherë dhe vetëm atëherë kur të
gjitha pikat e tjera të bashkësisë ndodhen në të njëjtën anë të drejtëzës që bashkon këto dy
pika. Duke përsëritur këtë procedurë për çdo çift pikash përftohet një listë e segmenteve që
përbëjnë kufirin e mbështjellëses konvekse.

Shtrohet pyetja se si do të përcaktohet që një bashkësi pikash është e gjitha në të njëjtën anë
të segmentit? Këtu na vjen në ndihmë matematika dhe pikërisht ekuacioni i drejtëzës që kalon
nëpër dy pika të dhëna Pi(xi, yi) dhe Pj(xj, yj):

y – yi = ((yi – yj) / (xi – xj)) ∙ (x – xi) (6.1)

Me veprime të thjeshta matematike ekuacioni (6.1) transformohet në ekuacionin e


përgjithshëm të drejtëzës që kalon nëpër dy pika të dhëna në formën:

ax + by = c, (6.2)

ku, a = yj – yi, b = xi – xj dhe c = xiyj – yixj


6 Teknika e forcës brutale dhe kërkimi shterues | 139

Drejtëza me ekuacion (6.2) e ndan planin në dy gjysmë plane. Në njërin prej tyre të gjitha
pikat plotësojnë mosbarazimin ax + by > c; në tjetrin të gjitha pikat plotësojnë mosbarazimin
ax + by < c; ndërsa pikat që shtrihen mbi drejtëz kënaqin barazimin ax + by = c . Prandaj,
nëse të gjitha pikat, të ndryshme nga i dhe j, prodhojnë të njëjtën shenjë në shprehjen ax + by
– c, atëherë ato janë në të njëjtën anë të segmentit PiPj. Vemë në dukje që janë (n – 2) pika
jashtë drejtëzës të formuar nga segmenti PiPj. Algoritmi i mëposhtëm llogarit mbështjellësen
konvekse të një bashkësie prej n pikash në plan.

// Algoritmi 6.6 Përcaktimi i mbështjellëses konvekse


// Të dhëna: Tabela e absisave X[1:n] dhe e ordinatave Y[1:n] për n pika
// Rezultate: Tabela C [1:n] jep informacion mbi kulmet e mbështjellëses ku vlera 1
// tregon se është kulm i mbështjellëses dhe 0 kur nuk është kulm i mbështjellëses

convexHull (n, x, y,c) {


for i ← 1 to n do // inicializimi i tabelës së kulmeve të mbështjellëses
c[i] ← 0;
for i ← 1 to n – 1 do // për çdo çift pikash (i, j)
for j ← i + 1 to n do {
a ← y[j] – y[i]; // llogarit koeficientët e vijës
b ← x[i] – x[j];
c ← x[i] * y[j] – y[i] * x[j];
mbiVijë ← 0; // numri i pikave që shtrihen sipër vijës
njëraAnë ← 0; // numri i pikave që shtrihen nga njëra anë e vijës
anaTjetër ← 0; // numri i pikave që shtrihen nga ana tjetër e vijës

// llogarit numrin e pikave mbi vijë, nga njëra anë e vijës dhe nga ana tjetër e vijës
for k ← 1 to n do {
if (k ≠ i) and (k ≠ j) // përjashton kulmet i dhe j
then { p ← a * x[k] + b * y[k] – c;
if (p = 0)
then mbiVijë ← mbiVijë + 1
else if (p > 0)
then njëraAnë ← njëraAnë + 1
else anaTjetër ← anaTjetër + 1;
}
};
// kontrollon nëse pikat i dhe j janë pika skajore
if ((n–2 – mbiVijë = njëraAnë) or (n –2 – mbiVijë = anaTjetër))
then { c[i] ← 1; c[j] ← 1;}
}
return
}

Efektshmëria e algoritmit. Për secilen nga n(n–1)/2 pikat e planit duhet të llogaritet shenja
e shprehjes ax + by – c për secilën nga n –2 pikat e tjera, prandaj ky algoritëm i teknikës të
forcës brutale bën pjesë në klasën O(n3).
6 Teknika e forcës brutale dhe kërkimi shterues | 140

6.4 Kërkimi shterues

Shumë probleme të rëndësishme kërkojnë gjetjen e një ndonjë elementi, që zotëron ndonjë
cilësi të veçantë, në një mjedis që rritet në mënyrë eksponenciale ose edhe më shpejt, për një
përmasë të caktuar. Në mënyrë tipike, probleme të tilla krijohen në situata që kërkojnë, në
mënyrë të dukshme apo të padukshme, objekte kombinatorike të tilla si permutacionet,
kombinacionet dhe nënbashkësitë e një bashkësie të dhënë. Shumë nga këta probleme janë
edhe probleme optimizimi: kërkohet të gjendet një element që maksimizon apo minimizon
disa karakteristika të dëshëruara si për shembull gjatësinë e rrugës të përshkrur.

Kërkimi shterues (Exhaustive search) është thjesht një forcë brutale për zgjidhjen e
problemeve kombinatorike. Procedura që zbatohet është që në një mënyrë sistematike, të
ndërtohen të gjitha zgjidhjet e mundshme për problemin në fjalë, në mënyrë që të gjitha
zgjidhjet të reflektohen dhe të mos ketë përsëritje të zgjidhjeve. Vlerësohen zgjidhjet një nga
një, ndoshta duke skualifikuar ato që nuk mund të realizohen, dhe duke mbajtur shënim për
më të mirën të zgjedhur deri në atë çast. Pasi kërkimi të përfundojë shpallet fituesi.

6.4.1 Problemi i tregtarit shëtitës

Problemi i tregtarit shëtitës (Traveling Salesman Problem – TSP) ka intriguar kërkuesit për
mbi 150 vjet me anë të formulimit të tij tepër të thjeshtë në dukje, zbatimeve të rëndësishme,
dhe lidhjeve interesante me probleme të tjera. Në terma të thjeshta, problemi kërkon të
gjendet rruga më e shkurtër, në një bashkësi prej n qytetesh, me kusht që të kalohet në të
gjithë qytetet vetëm nga një herë, para se të kthehet në pikën e nisjes. Problemi mund të
modelohet në forma të ndryshme, njëra prej të cilave është ajo e grafit të peshuar, ku kulmet e
grafit paraqesin qytetet dhe pesha e brinjëve paraqet distancat. Pastaj problemi mund të
formulohet si problemi i gjetjes të qarkut më të shkurtër Hamiltonian të grafit. (Një qark
Hamiltonian përcaktohet si një cikël që kalon në të gjitha kulmet e grafit ekzaktësisht vetëm
një herë. Ai e ka marre emrin nga matematikani irlandez Sir William Rowan Hamilton (1805
- 1865), që interesohej për cikle të tillë në zbulimet e veta algjebrike).

Në figurën 6.7 paraqitet një rast i vogël i problemit dhe zgjidhja e tij me metodën e
përshkruar.
2 b
a
8
7 3
5

d
c 1

Nr Udhëtimi Gjatësia e udhëtimit Rezultati


1 a-b-c-d-a L = 2 + 8 + 1 + 7 = 18
2 a-b-d-c-a L = 2 + 3 + 1 + 5 = 11
3 a-c-b-d-a L = 5 + 8 + 3 + 7 = 23 optimale
4 a-c-d-b-a L = 5 + 1 + 3 + 2 = 11
5 a-d-b-c-a L = 7 + 3 + 8 + 5 = 23 optimale
6 a-d-c-b-a L = 7 + 1 + 8 + 2 = 18
Figura 6.7 Zgjidhja e një problemi me përmasa të vogla të tregtarit shëtitës
6 Teknika e forcës brutale dhe kërkimi shterues | 141

Duke mbajtur të fiksuar një qytet, prodhojmë të gjitha permutacionet e formuara nga n – 1
qytetet e tjerë; llogaritim gjatësinë e çdo rruge dhe gjejmë më të shkurtrën prej tyre.
Algoritmi i mbështetur në kërkimin shterues bën pjesë në klasën O((n–1)!).
6.4.2 Problemi i çantës së shpinës

Problemi i çantës së shpinës (knapsack problem) është një problem tjetër i njohur në
algoritmikë. Jepen n artikuj, me pesha të njohura w1, …,wn dhe vlera të njohura v1, …, vn si
dhe një çantë shpine me kapacitet W. Cilët artikuj duhet të futen në çantë në mënyrë që vlera
e ngarkuar të jetë maksimale. Probleme të tilla takohen në praktikë në ngarkimin e
kontenierëve, anijeve dhe avionëve të transportit të mallrave.

Kërkimi shterues për këtë problem sugjeron prodhimin e çdo nënbashkësie të mundshme prej
bashkësisë prej n elementesh, duke llogaritur peshën e përgjithëshme për çdo nënbashkësi për
të identifikuar të pranueshmet (domethënë ato për të cilat pesha e përgjithshme nuk e tejkalon
kapacitetin e çantës), dhe për të gjetur më të madhen në vlerë prej tyre. Përderisa numri i
nënbashkësive të formuara nga një bashkësi prej n-elementesh është 2n – 1, kërkimi shterues
është një algoritëm i klasës Ω(2n) dhe nuk është fare e rëndësishme se sa i efektshëm është
procesi i formimit të nënbashkësive të mundshme.

Një rast zgjidhjeje jepet në tabelën 6.3 me një çantë me kapacitet W = 16.

Kështu për të dy problemet, si për problemin e tregtarit shëtitës ashtu për problemin e çantës
së shpinës, kërkimi shterues rezulton në algoritme që janë ekstremalisht të paefektshëm për
çdo të dhënë fillestare. Me të vërtetë, këta dy probleme janë shembujt më të mirë, të njohur
për të ashtuquajturit problemet e forta NP (NP-hard problem). Nuk njihet ndonjë algoritëm i
klasës polinomiale për problemet NP. Për më tepër shkencëtarët e kompjuterave besojnë se
algoritme të tillë nuk ekzistojnë, megjithëse ky supozim shumë i rëndësishëm nuk është
provuar kurrë. Metoda më të sofistikuara si backtracking dhe branch-and-bound lejojnë që të
zgjidhen disa por jo të gjitha rastet e këtyre problemeve apo të problemeve të ngjashëm me
to në një kohë më të vogël se ajo eksponenciale. Gjithashtu, për zgjidhjen e tyre, mund të
përdoren si alternativa edhe algoritme të përafërt.

Tabela 6.3 Një rast i problemit të çantës së shpinës me kapacitet, W=16


Të dhëna fillestare Kombinimi Pesha Vlera Koment
Artikulli Pesha Vlera {1} 2 20
1 2 20 {2} 5 30
2 5 30 {3} 10 50
3 10 50 {4} 5 10
4 5 10 {1, 2} 7 50
{1,3} 12 70
{1,4} 7 30
{2,3} 15 80 Vlera maksimale
{2,4} 10 40
{3,4} 15 60
{1,2,3} 17 E papranueshme
6 Teknika e forcës brutale dhe kërkimi shterues | 142

{1,2,4} 12 60
{1,3,4} 17 E papranueshme
{2,3,4} 20 E papranueshme
{1,2,3,4} 22 E papranueshme

6.4.3 Problemi i caktimit të detyrave

Shembulli i tretë i një problemi që mund të zgjidhet me anë të kërkimit shterues është ai i
caktimit të detyrave (assignment problem). Kjo nënkupton që çdo personi i caktohet një dhe
vetëm një detyrë dhe çdo detyrë kryhet nga një dhe vetëm nga një person. Ndërkaq njihet
kosto C[i, j] kur personit i, i caktohet detyra j për çdo çift i, j = 1, 2, …, n. Problemi qëndron
në gjetjen e një shpërndarjeje të tillë të personave dhe detyrave në mënyrë që kosto e
përgjithshme të jetë minimale. Një problem i përmasave të vogla (kostot) paraqitet në tabelën
6.4.

Tabela 6.4 Kostot e shpërndarjes për një rast me katër detyra të ndryshme
detyra_1 detyra_2 detyra_3 detyra_4
personi_1 9 2 7 8
personi_2 6 4 3 7
personi_3 5 8 1 8
personi_4 7 6 9 4

Është lehtësisht e dukshme që një rast i problemit të caktimit të detyrave është i përcaktuar
tërësisht nga matrica e tij e kostos. Në funksion të kësaj matrice, problemi kërkon
përzgjedhjen e një elementi në çdo rresht të matricës, kështu që të gjithë elementet janë në
shtylla të ndryshme dhe shuma e përgjithshme e elementeve të zgjedhur është më e vogla e
mundshme.

Vëmë në dukje që nuk ka ndonjë strategji qartësisht të dukshme për gjetjen e një zgjidhjeje që
të funksionojë. Për shembull nuk mund të zgjedhim elementin më të vogël në çdo rresht
mbasi elementet më të vegjël mund të jenë në të njëjtën kollonë. Po ashtu, elementi më i
vogël në të gjithë matricën mund të mos bëjë pjëse në zgjidhjen optimale. Kështu që kërkimi
shterues mund të duket si një e keqe e pashmangshme.

Ne mund t’i përshkruajmë zgjidhjet e mundshme të problemit të caktimit të detyrave si një


varg prej n-elementesh (n-tuple) 〈𝑗1 , ⋯ , 𝑗𝑛 〉, në të cilin komponentja e itë, për i = 1, …, n,
tregon kollonën e elementit të zgjedhur në rreshtin e itë (domethënë, numri i detyrës i caktuar
për personin e itë). Për shembull, për matricën e kostos të dhënë më sipër, vargu 〈2, 3,4, 1〉
tregon një caktim të mundshëm të personi_1 për detyra_2, të personi_2 për detyra_3, të
personi_3 për detyra_4 dhe personi_4 për detyra_1.

Nga kërkesa e problemit të caktimit të detyrave rrjedh se ka një korrespondencë një me një
(biunivoke) ndërmjet caktimeve të pranueshme dhe permutacioneve të n numrave të plotë të
parë. Prandaj, rruga e kërkimit shterues për problemin e caktimit duhet të kërkojë prodhimin
e të gjithë permutacioneve të numrave të plotë 1, 2, …, n, duke llogaritur koston e
përgjithshme të çdo caktimi me anë të shumimit të elementeve korrespondues të matricës së
kostos, dhe së fundmi duke zgjedhur atë më të vogël.
6 Teknika e forcës brutale dhe kërkimi shterues | 143

Hapat e parë të zbatimit të metodës për shembullin e lartpërmendur janë paraqitur në figurën
6.8.
< 1, 2, 3, 4 > kosto = 9 + 4 + 1 + 4 = 18
9 2 7 8 < 1, 2, 4, 3 > kosto = 9 + 4 + 8 + 9 = 30
6 4 3 7 < 1, 3, 2, 4 > kosto = 9 + 3 + 8 + 4 = 24 etj
C= 5 8 1 8 < 1, 3, 4, 2 > kosto = 9 + 3 + 8 + 6 = 26
7 6 9 4 < 1, 4, 2, 3 > kosto = 9 + 7 + 8 + 9 = 33
< 1, 4, 3, 2 > kosto = 9 + 7 + 1 + 6 = 23

Figura 6.8 Iteracionet e para për zgjidhjen e një problemi të përcaktimit të punëve

Meqenëse numri i permutacioneve që duhet të merren në konsideratë për rastin e


përgjithshëm të problemit të caktimit të detyrave është n!, kërkimi shterues nuk është praktik
për të gjitha përmasat e problemit. Për fat të mirë që është zbuluar një algoritëm i efektshëm
për këtë problem, i quajtur metoda hungareze për nder të matematikanëve hungarezë König
dhe Egerváry, të cilët kanë përpunuar metodën.

Ky është një lajm i mirë: fakti që zona e problemit rritet eksponencialisht (ose më shpejt) nuk
do të thotë medoemos që nuk do të ketë algoritme të efektshëm për t’i zgjidhur. Megjithatë
raste të tilla janë më tepër një përjashtim nga rregulli. Për më tepër, nuk njihen algoritme me
kohë ekzekutimi polinomiale për probleme, zona e të cilëve rritet shumë shpejt me rritjen e
përmasave të problemit.

6.5 Përmbledhje

• Teknika e forcës brutale është një metodë e drejpërdrejtë për zgjidhjen e një problemi, e
bazuar kryesisht në formulimin e problemit, në përkufizimet dhe në konceptet që
përfshihen në të.
• Forca e metodës qëndron në faktin se ajo mund të përdoret gjerësisht dhe thjeshtësisht;
dobësia e saj kryesore është se ajo nuk është e efektshme për shumë algoritme.
• Një zbatim i parë i metodës së forcës brutale shpesh mund të përmirësohet me përpjekje
të vogla
• Kërkimi shterues është një forcë brutale për problemet kombinatorike. Ai sugjeron
prodhimin e çdo objekti kombinatorik të problemit, duke zgjedhur atë që kënaq të gjitha
kushtet dhe duke zgjedhur objektin e dëshëruar
• Algoritmi Insertion sort është një zbatim me teknikën zvogëlo me një konstante për
probleme renditjeje. Ai bën pjesë në klasën Θ(n2) si në rastin me të keq ashtu edhe në
rastin mesatar të të dhënave fillestare por është dy herë më i shpejtë në rastin mesatar sesa
në rastin më të keq. Përfitimi më i mirë nga algoritmi është se ai ka performanca shumë të
mira për tabelat që janë në pjesën më të madhe të renditura.
• Problemi i tregtarit shëtitës, problemi i çantës së shpinës, dhe problemi caktimit të
detyrave janë shembuj tipikë të problemeve që mund të zgjidhen, të paktën teorikisht, me
algoritmet e kërkimit shterues
• Kërkimi shterues është përgjithësisht i pazbatueshëm por mund të përdoret vetëm në
probleme të përmasave të vogla.

6.6 Ushtrime për kapitullin 6

1. Përgjigjuni pyetjeve të mëposhtme


6 Teknika e forcës brutale dhe kërkimi shterues | 144

a. Jepni një shembull të një algoritmi që nuk duhet të konsiderohet si një zbatim i
teknikës së forcës brutale.
b. Mendoni se ka probleme që nuk mund të zgjidhen me saktësi me anë të ndonjë
algoritmi të forcës brutale? Në rast se po, a mund të tregoni ndonjë të tillë?

2. Cila është efektshmëria e algoritmit të forcës brutale për llogaritjen e an si një funksion i
n? Po si funksion i numrit të biteve në paraqitjen binare të numrit n?

3. Për secilin nga algoritmet për problemet e mëposhtme thoni nëse algoritmi mbështetet
apo jo në teknikën e forcës brutale.
a. Llogaritja e ∑𝑛𝑖=1 𝑖 2
b. Llogaritja e diferencës ndërmjet vlerës më të madhe dhe vlerës më të vogël në një
tabelë (i quajtur rangu i tabelës)
c. Të kontrollohet nëse një matricë kuadratike është simetrike

4. Për polinomin e rendit n


Pn(x) = anxn + an-1xn-1 + ··· + a1x + a0
a. Hartoni një algoritëm të teknikës së forcës brutale për llogaritjen e vlerës së polinomit
në një pikë të dhënë x0 dhe vlerësoni kohën e ekzekutimit për rastin më të keq në lidhje
me veprimin më të kushtueshëm.
b. Nëse algoritmi i hartuar bën pjesë në klasën Θ(𝑛2 ), hartoni një algoritëm linear për
këtë problem.
c. A është e mundur të hartohet ndonjë algoritëm me efektshmëri më të mirë se ajo
lineare për këtë problem?

5. Renditni vargun e shkronjave {S, H, E, M, B, U, L, L} sipas rendit alfabetik duke


zbatuar algoritmin Selection sort, duke paraqitur gjendjen pas çdo përfundimi të të çdo
hapi të ciklit të jashtëm. Sa krahasime elementesh dhe sa përkëmbime kryhen algoritmi në
këtë rast?

6. A është algoritmi Selection sort i paraqitur në leksion një algoritëm renditjeje i


qëndrueshëm? Nëse jo, jepni një shembull.

7. Renditni vargun e shkronjave {S, H, E, M, B, U, L, L} sipas rendit alfabetik duke


zbatuar algoritmin Bubble sort, duke paraqitur duke paraqitur gjendjen pas çdo
perfundimi të të çdo hapi të ciklit të jashtëm. Sa krahasime dhe sa përkëmbime
elementesh kryhen algoritmi në këtë rast?

8. Në lidhje me algoritmin Bubble sort


a. Provoni që nëse algoritmi nuk kryhen përkëmbime nëpërmjet kalimit në të dhënat
atëherë tabela është e renditur dhe algoritmi mund të ndalojë.
b. Hartoni një algoritëm që merr parasysh këtë përmirësim.
c. Provoni që efektshmëria e versionit të përmirësuar, për rastin më të keq të organizimit
të të dhënave fillestare, është kuadratike.

9. A është algoritmi Bubble sort i paraqitur në leksion një algoritëm renditjeje i


qëndrueshëm dhe nëse po, pse është i tillë?

10. Në një rresht të drejtë janë vendosur 2n disqe, n prej të cilëve me ngjyrë të zezë dhe n me
ngjyrë të bardhë. Në vendosjen fillestare ata janë vendosur në mënyrë të alternuar: disk i
6 Teknika e forcës brutale dhe kërkimi shterues | 145

zi, disk i bardhë, disk i zi, disk i bardhë, e kështu me rradhë. Detyra është që të gjithë
disqet e bardhë të vendosen në anën e majtë dhe disqet e zinj të vendosen ne anën e
djathtë. E vetmja lëvizje që lejohet për të kryer këtë proces është përkëmbimi i disqeve që
ndodhen në dy pozicione fqinjë. Të hartohet një algoritëm që zgjidh këtë lojë dhe të
përcaktohet numri i lëvizjeve të nevojshme.

11. Paraqitni gjurmën e ekzekutimit të algoritmit Insertion Sort për të renditur vargun e
karaktereve {I, N, S, E, R, T, I, O, N, S, O, R, T} sipas rendit alfabetik.

12. Krijoni një rast të të dhënave fillestare prej n elementesh të tillë që Insertion sort të
kërkojë me siguri Θ(n2) krahasime.

13. Referuar algoritmit Insertion sort, paraqitur në tekst:


a. Në rreshtin 4, për të shmangur kalimin tej kufirit të poshtëm të tabelës së të dhënave
në ciklin e brendshëm, kontrollohet vlera e variablit j në çdo hap të ciklit. Ky kontroll
zmadhon koston kohore të algoritmit. Të ndiqet ekzekutimi i algoritmit me anë të një
rasti me supozimin se kushti j ≥ 0 nuk ekziston. Çfarë ndodh?
b. Ndërkaq, kushti j ≥ 0 mund të hiqet dhe roli i tij mund të zëvendësohet me sentinelë
që duhet të vihet para elementit të parë të tabelës që do të renditet. Çfarë
karakteristike duhet të zotërojë sentinel-a në mënyrë që të kemi një algoritëm korrekt?
c. A do të ketë versioni me sentinelë të njëjtën klasë efektshmërie si versioni origjinal?

14. Për versionin e mëposhtëm të algoritmit Insertion sort përcaktoni efektshmërinë kohore dhe
bëni një krahasim me versionin e dhënë në tekst duke marrë në konsideratë veprimet e
vlerëdhënies dhe të indeksimit.

// Algoritmi
// Të dhëna: një tabelë a[0..n–1]
// Rezultate: tabela a[0..n–1] e renditur sipas rendit rritës

InsertionSort2 (a, n) {
for i ← 1 to n – 1 do {
j ← i – 1;
while (j ≥ 0 and a[j] > a[j + 1]) do{
swap(a[j], a[j + 1);
}
j←j–1
}
}

15. Në cilat raste të të dhënave fillestare mund të ndërtohet një algoritëm më i shpejtë i
bazuar në teknikën e forcës brutale për problemin e çiftit më të afërt të n pikave
x1,···, xn të shtrira në një bosht të numrave reale? Ndërtoni një algoritëm të tillë dhe
vlerësoni kompleksitetin e tij.

16. Le të jenë x1 < x2 < ··· < xn numra reale që paraqesin koordinatat e n fshatrave të
vendosura në një rrugë të drejtë. Është e nevojshme që të ndërtohet një zyrë postare në një
prej këtyre fshatrave.
a. Hartoni një algoritëm të efektshëm që të gjejë se ku duhet të ndërtohet zyra e postës
për të minimizuar distancën mesatare ndërmjet fshatrave dhe zyrës së postës.
6 Teknika e forcës brutale dhe kërkimi shterues | 146

b. Hartoni një algoritëm të efektshëm që të gjejë se ku duhet të ndërtohet zyra e postës


për të minimizuar distancën maksimale nga një fshat tek zyra e postës. (Udhëzim.
Zgjidhni problemin për n=2 dhe n=3 dhe kjo do t’ju shpjerë në zgjidhjen e problemit)

17. Problemi i çiftit të pikave më të afërta mund të shtrohet edhe për një hapsirë k-
dimensionale në të cilën distanca ndërmjet dy pikave 𝑃′ = (𝑥1′ , ⋯ , 𝑥𝑘′ ) dhe 𝑃" =
(𝑥1′′ , ⋯ , 𝑥𝑘′′ ) përcaktohet si:

k
d(P ′ , P") = �� (xs′ − xs′′ )2 .
s=1

Cila do të jetë klasa e efektshmërisë së algoritmit të forcës brutale për problemin k-


dimensional të çiftit të pikave më të afërta.

18. Gjeni mbështjellëset konvekse të bashkësive të mëposhtme dhe identifikoni pikat e tyre
skajore (nëse kanë pika të tilla).
a. një segment
b. një katror
c. kufiri i një katrori
d. një vijë e drejtë

19. Skiconi një algoritëm me efektshmëri kohore lineare për të përcaktuar dy pikat
skajore të një mbështjellëseje konvekse të një bashkësie prej n pikash në plan (n > 1).

20. Cili është modifikimi i nevojshëm për t’u bërë në algoritmin e forcës brutale për
problemin e mbështjellëses konvekse për të trajtuar më tepër se dy pika në të njëjtën
drejtëz?

21. Të modifikohet algoritmi i kërkimit linear duke përdorur si kusht për përfundimin e
kërkimit vetëm vlerën e kërkuar, të vendosur në mënyrë të përshtatshme në tabelën e të
dhënave.

22. Të gjendet numri i krahasimeve të kryera nga algoritmi i kërkimit linear në versionin
sentinel për rastin më të keq të organizimit të të dhënave fillestare.

23. Përcaktoni numrin e krahasimeve të karakterit që do të bëhen nga algoritmi i forcës


brutale për kërkimin e motivit GANDHI në tekstin

THERE_IS_MORE_TO_LIFE_THAN_INCREASING_ITS_SPEED

(Supozohet se gjatësia e tekstit prej 47 karaktere dihet para se të fillojë kërkimi i


motivit)

24. Sa krahasime (të suksesshme dhe të pasuksesshme) do të bëhen nga algoritmi i forcës
brutale gjatë kërkimit të secilit prej motiveve të mëposhtme në një tekst të përbërë nga
1000 (një mijë) zero?
a. 00001 b. 10000 c. 01010
6 Teknika e forcës brutale dhe kërkimi shterues | 147

25. Jepni një rast të një teksti me gjatësi n dhe një motivi me gjatësi m që përbën rastin më të
keq të të dhënave për algoritmin e kërkimit të motivit. Saktësisht sa krahasime të
karaktereve do të kryhen nga algoritmi i kërkimit të motivit hartuar sipas teknikës së
forcës brutale për të dhëna të tilla?

26. Në zgjidhjen e problemit të gjetjes së motivit, a do të ketë ndonjë përparësi nëse


krahasimi i karaktereve të motivit me tekstin të bëhet nga e djathta në të majtë në vend të
krahasimit nga e majta në të djathtë?

27. Le të konsiderojmë problemin e numërimit, në një tekst të dhënë, të numrit të


motiveve që fillojnë me A dhe përfundojnë me B. (Për shembull në tekstin
CABAAXBYA gjenden katër të tillë)
a. Hartoni një algoritëm të forcës brutale për këtë problem dhe përcaktoni klasën e
efektshmërisë.
b. Hartoni një algoritëm më të efektshëm për këtë problem.

28. Cila do të ishte pamja të një teksti të përbërë nga n karaktere, në të cilin do të kërkojmë
me një motivi të formës AAAH për të patur kohën më të keqe të kërkimit me anë të një
algoritmi të hartuar me teknikën e forcës brutale. Sa do të ishte numri i krahasimeve në
këtë rast dhe cili do të ishte kompleksiteti për rastin më të keq?

29. Në lidhje me problemin e tregtarit shëtitës:


a. Duke supozuar se çdo rrugëtim mund të prodhohet brenda një kohe konstante, cila do
të ishte klasa e efektshmërisë së algoritmit të kërkimit shterues të paraqitur në tekstin
e leksioneve?
b. Nëse algoritmi programohet në një kompjuter që kryen 1 miliard mbledhje në
sekondë, vlerësoni numrin maksimal të qyteteve për të cilat problemi mund të
zgjidhet:
i. në një orë ii. në 24 orë iii. në një vit iv. në një shekull

30. Duke zbatuar teknikën e forcës brutale zgjidhni problemin e tregtarit shëtitës duke u nisur
nga qyteti C.
13 B
A
8
5 10
14
D C
15

31. Të zgjidhet problemi i çantës së shpinës me të dhënat vijuese: pesha e artikujve P = {9,
12, 6, 5, 3, 2); vlera e artikujve V = {30, 36, 15, 11, 5, 3} dhe kapaciteti çantës K = 19.

32. Duke i u referuar problemit të përcaktimit të punëve të paraqitur në leksion, kryeni të


gjitha hapat e nevojshme për zgjidhjen e problemit.

33. Jepni një rast të problemit të caktimit të detyrave zgjidhja optimale e të cilit nuk
përmban elementin më të vogël të matricës së tij të kostos.

34. Le të konsiderojmë një problem ndarjeje: jepen n numra të plotë pozitivë, të ndahen ata
në dy nënbashkësi shuma e elementeve të secilave të jetë e njëjtë. (Natyrisht që ky
6 Teknika e forcës brutale dhe kërkimi shterues | 148

problem nuk ka gjithmonë zgjidhje.) Hartoni një algoritëm të bazuar në kërkimin shterues
për këtë problem. Përpiquni të minimizoni numrin e nënbashkësive që janë të nevojshme
të prodhohen.

35. A mund të përdoret kërkimi shterues për problemin e renditjes dhe nëse po përcaktoni
klasën e efektshmërisë të një algoritmi të tillë
7 Teknika zvogëlo-dhe-sundo | 149

7 Teknika zvogëlo-dhe-sundo

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të njohë teknikën e zvogëlo-dhe-sundo dhe versionet e saj për konceptimin e algoritmeve;


• të përdorë reduktimin me një faktor për kërkimin me anë të përgjysmimit;
• të përdorë zvogelimin me një madhësi variabël për problemin e përzgjedhjes dhe
llogaritja e medianës

Teknika zvogëlo-dhe-sundo (Decrease-and-Conquer) bazohet në shfrytëzimin e lidhjes që


ekziston ndërmjet zgjidhjes të një rasti (instance) të problemit dhe zgjidhjes të një rasti më
të vogël. Pas përcaktimit të një lidhjeje të tillë, ajo mund të shfrytëzohet me të ashtuquajturat
teknika nga lart-poshtë (top-down) ose poshtë-lart (bottom-up). Teknika nga lart-poshtë shpie
natyrshëm në një zbatim rekursiv megjithëse në disa raste mund edhe të bëhet edhe zbatim
iterativ. Versioni poshtë-lart zakonisht zbatohet në mënyrë iterative duke filluar me një
zgjidhje të rastit me të vogël të problemit; ndonjëherë ai quhet edhe përafrimi rritës
(incremental approach).

Ekzistojnë dy variante të teknikës zvogëlo-dhe-sundo: (i) redukto me një faktor konstant


(decrease-by-a-constant-factor) dhe (ii) zvogëlo me një madhësi variabël (variable-size-
decrease).

Redukto me një faktor konstant. Në këtë rast, teknika sugjeron reduktimin e rastit të një
problemi me të njëjtin faktor në çdo përsëritje të problemit. Në pjesën më të madhe të
zbatimeve ky faktor reduktues konstant është i barabartë me 2.

Zvogëlo me një madhësi të ndryshueshme. Në këtë rast madhësia e zvogëlimit të përmasës


të problemit ndryshon nga një iteracion të algoritmit në tjetrin. Algoritmi i Euklidit, për
llogaritjen e plotpjesëtuesit më të madh të përbashkët, është shembulli më i mirë për këtë rast.

7.1 Redukto me një faktor konstant

Siç përmendëm në fillim të kapitullit, redukto me një faktor konstant është një nga versionet
teknikës zvogëlo-dhe-sundo. Probleme tipike të kësaj kategorie janë kërkimi binar dhe
ngritja në fuqi me anë të përgjysmimit. Si probleme të tjera mund të përmendim problemin e
monedhës difektoze (fake-coin problem), shumëzimin me metodën e fshatarit rus (Russian
peasant method), problemin e Josephus-it (Josephus problem). Në këtë kategori nuk ka
shumë probleme, ndoshta nga që algoritmet e kësaj klase janë me efektshmëri logaritmike, që
përfundojnë shumë shpejt, gjë që nuk ndodh aq shpesh. Për më tepër, një reduktim me ndonjë
faktor të ndryshëm nga dy, ndodh veçanërisht rrallë.

Ngritja në fuqi
Le të shohim problemin e ngritjes në fuqi. Nëse rasti i përmasës n është që të llogaritet an,
rasti i gjysmës së përmasës është të llogaritet an/2, me një lidhje të qartë ndërmjet dy rasteve:
an = (an/2)2. Por, përderisa po konsiderojmë vetëm ngritjet në fuqi të plota formula e
mësipërme nuk funksionon për n tek. Në qoftë se n është tek atëhere duhet të llogaritim an-1
7 Teknika zvogëlo-dhe-sundo | 150

duke zbatuar rregullin e eksponentit çift dhe pastaj ta shumëzojmë rezultatin me a. Duke
përmbledhur, do të kemi formulën:

(an/2 )2 në se n ë shtë çift dhe pozitive


𝑛
𝑎 = � (a(n−1)/2 )2 ∙ a në se n ë shtë tek (7.1)
1 në se n = 0

Nëse llogaritim an në mënyrë rekursive sipas formulës (7.1) dhe vlerësojmë efektshmërinë e
algoritmit duke konsideruar shumëzimin si veprim kryesor, duhet të presim një efektshmëri të
klasës Θ(log n) mbasi, në çdo përsëritje, përmasa reduktohet përgjysëm duke shpenzuar një
ose dy shumëzime.

Kërkimi binar
Kërkimi binar (binary search) është padyshim një algoritëm jashtëzakonisht i efektshëm për
të kërkuar në tabela të renditura. Ai punon duke krahasuar një vlerë k, (shpesh e quajtur
target ose çelës), me elementin e mesit të tabelës A[mes]. Nëse vlera përputhet algoritmi
ndalon; përndryshe i njëjti operacion përsëritet rekursivisht për gjysmën e parë të tabelës nëse
k < A[mes] ose për gjysmën tjetër nëse k > A[mes]:
K


[0] ⋯
a �����������������
a[mes − 1] �����
a[mes] a[mes + 1] ⋯ a[n − 1]
���������������
kë tu kë rkohet në se k <a[mes] kë tu kë rkohet në se k >a[mes]

Le të zbatojmë kërkimin binar për kërkimin e k = 70 në shembullin e dhënë në tabelën e


mëposhtme:

3 14 27 31 39 42 55 70 74 81 85 93 98

Iteracionet e algoritmit paraqiten në tabelën e mëposhtme:

indeksi 0 1 2 3 4 5 6 7 8 9 10 11 12
vlera 3 14 27 31 39 42 55 70 74 81 85 93 98

iteracioni 1 m mes d

iteracioni 2 m mes d

iteracioni 3 m,mes d

Megjithëse kërkimi binar bazohet në ide plotësisht rekursive, ai mund të zbatohet lehtësisht si
algoritëm iterativ. Më poshtë paraqitet një nga zbatimet e mundshme në versionin iterativ.

Algoritmi 7.1 Algoritmi i Kërkimit binar në versionin iterativ


// Të dhëna: një tabelë e renditur a[m..d] dhe një vlerë target k
// Rezultate: mes, indeksi i elementit të tabelës i barabartë me k ose –1 nëse nuk
// ekziston

kërkimBinarIterativ(m, d, a, k) {
while (m ≤ d) do {
7 Teknika zvogëlo-dhe-sundo | 151

mes ← (m + d) div 2;
if k = a[mes]
then return mes
else if (k < a[mes])
then d ← mes – 1
else m ← mes + 1;
}

return –1
}

Një rrugë për të analizuar efektshmërinë e kërkimit binar është që të numërohet numri i
herëve që çelsi i kërkimit krahasohet me një element të tabelës. Megjithatë për hir të
thjeshtësisë do të numërojmë të ashtuquajturin krahasim me tre-dalje 12. Kjo supozon që pas
një krahasimi të k-së me a[mes], algoritmi mund të përcaktojë nëse k është më e vogël, e
barabartë ose më e madhe se a[mes] (secila veç).

Sa krahasime të tilla duhet të bëjë algoritmi në një tabelë me n elemente? Është e qartë se
përgjigja varet jo vetëm nga përmasa n e rastit por edhe nga cilësitë e një rasti të problemit.
Le të gjejmë numrin e krahasimeve të çelsit në rastin më të keq Kkeq(n). Rasti më i keq i të
dhënave përfshin të gjitha tabelat që nuk e përmbajnë vlerën që kërkohet si edhe kërkime të
suksesshme (kur është e fundit). Përderisa pas një krahasimi algoritmi ndodhet përballë të
njëjtës situatë por në një tabelë me gjysmën e madhësisë, atëherë do të përftonim ekuacionin
e mëposhtëm të rekurrencës për Kkeq(n):
Kkeq(n) = Kkeq (floor(n/2)) + 1 për n > 1, Kkeq(1) = 1 (7.2)
Siç kemi diskutuar më parë rruga standarde për të zgjidhur një rekurrencë të tillë është të
supozojmë që n = 2k dhe të zgjidhet rekurrenca e formuar me anë të metodës së zëvendësimit
ose me ndonjë metodë tjetër. Rekurrencën (7.2) e kemi zgjidhur në një nga leksionet e
kaluara por me tjetër konditë fillestare. Me konditën fillestare Kkeq(1) = 1, përftojmë
zgjidhjen
Kkeq(2k) = k + 1 = log 2 n + 1 (7.3)
Për më tepër, mund të provohet që zgjidhja e dhënë nga formula (7.3) për n = 2k mund të
përgjithësohet për të përftuar një zgjidhje të vlefshme për një numër të plotë pozitiv të
çfardoshëm n:

T(n) = floor(log 2 n) + 1 = ceiling(log2 (n + 1)) (7.4)


Formula (7.4) duhet parë me vëmendje. Së pari ajo thotë që klasa e efektshmërisë në rastin
më të keq është Θ(log n). Së dyti është një përgjigje që pritej: përderisa algoritmi thjesht
redukton përmasën e tabelës që mbetet përgjysmë në çdo iteracion, numri i iteracioneve të
tilla të nevojshme për reduktuar përmasën fillestare nga n tek 1 është rreth log2n. Së treti,
funksioni logaritmik rritet aq avash saqë vlera e tij mbetet shumë e vogël edhe për vlera
shumë të mëdha të n. Për shembull, në përputhje me formulën (7.4), nuk do të duhen më
shumë se floor(log 2 103) + 1 = 10 krahasime me 3-dalje për të gjetur një element me një
vlerë të dhënë (ose për të provuar që një element i tillë nuk ekziston) në një tabelë të renditur
prej 1000 elementesh, dhe nuk do të duheshin më shumë se floor(log 2 106)+1 = 20 krahasime
për të bërë të njëjtën gjë në një tabelë me 1 milion elemente!

12
Ne gjuhën e programimit Fortran është parashikuar kusht kontrolli me tre-dalje
7 Teknika zvogëlo-dhe-sundo | 152

7.2 Zvogëlo me një madhësi variabël

Llogaritja e pjestuesit më të madh të përbashkët


Kujtojmë që, pjesëtuesi më i madh i përbashkët (great common divisor) i dy numrave të
plotë jonegativë m dhe n, të cilët nuk janë njëkohësisht të barabartë me zero, shënuar me
simbolin pmp(m, n), përcaktohet si numri i plotë më i madh që plotpjesëton njëkohësisht
edhe m edhe n domethënë, që jep mbetjen zero. Kujtojmë që ky algoritëm bazohet në
formulën pmp(m, n) = pmp(n, m mod n). Megjithëse vlera e argumentit të dytë, është
gjithmonë më e vogël në krahun e djathtë sesa në krahun e majtë, ajo nuk zvogëlohet as me
një madhësi konstante e as reduktohet me ndonjë faktor konstant.

Euklidi i Aleksandrisë (shekulli i tretë p.e.r), e ka përshkruar një algoritëm për zgjidhjen e
këtij problemi në volumin e tij më të famshëm, Elementet.

//ALGORITMI 7.2 Llogarit pjestuesin më të madh te përbashkët sipas metodës së Euklidit


//Të dhëna: m, n numra të plotë, jonjëkohësisht 0
// Rezultati: m, pjestuesi më i madh i përbashkët

llogaritPMP(m, n) {
while (n ≠ 0) do {
mbetja ← mod (m,n);
m ← n;
n ← mbetja;
{
return m
}

Problemi i përzgjedhjes dhe llogaritja e medianës


Problemi i përzgjedhjes është problemi i gjetjes së elementit të ktë më të vogël në një listë prej
n numrash. Në statistikë, këta numra quhen statistika e rendit të ktë (kth order statistics).
Sigurisht, për k = 1 kemi të bëjmë me elementin më të vogël në vlerë dhe kur k = n kemi të
bëjmë me elementin me vlerën më të madhe. Gjetja e tyre është e thjeshtë, mjafton të
bredhim në listën e dhënë për të gjetur elementin më të vogël ose elementin më të madh. Një
nga rastet më interesante është ai i gjetjes të elementit që ndodhet në mes të listës, k =
ceilling(n/2) , që kërkon gjetjen e një elementi që është më i madh se secili element i gjysmës
së parë të listës dhe më i vogël se secili element i gjysmës së dytë të listës. Në statistikë kjo
vlerë quhet mesore (median) dhe është një nga kuptimet më të rëndësishme të saj. Po ashtu të
rëndësishëm janë edhe kuartilet, kuantilet, decilet dhe percentilet.

Mënyra më e thjeshtë për të gjetur elementin e ktë më të vogël në një tabelë është renditja e
tabelës në rendin rritës dhe pastaj gjendet elementi në pozicionin e ktë në tabelën e renditur. E
qartë që efektshmëria e këtij algoritmi varet tërësisht nga efektshmëria e algoritmit të
renditjes. Kështu me një algoritëm të mirë renditjeje si Mergesort ose Quicksort (që do të
diskutohen në leksionin vijues), efektshmëria do të ishte O(n lg n).

Por, në përgjithësi, renditja e tabelës është e kushtueshme nëse kërkohet vetëm elementi i ktë
më i vogël. Prandaj nisemi nga një ide tjetër më frytdhënëse. Ne mund të përfitojmë nga idea
7 Teknika zvogëlo-dhe-sundo | 153

e rigrupimit me anë të zhvendosjes së të dhënave të tabelës duke u bazuar në një vlerë p të një
elementi të saj, le të themi elementit të saj të parë. Në përgjithësi ky është një rigrupim i
elementeve i tillë që pjesa e majtë të përmbajë të gjithë elementet më të vegjël ose të
barabartë me p, pasuar nga vetë elementi p (i quajtur edhe bosht (pivot) i ndarjes), pasuar me
pas nga të gjithë elementet më të mëdhenj ose e barabartë me p si më poshtë

gjendja fillestare gjendja pas rigrupimit me bosht vlerën p


p → elementet ≤ p p elementet ≥ p

Ndër alternativat e mundshme për rigrupimin e elementeve të një tabele do të përdorim


algoritmin e ndarjes Lomuto (Lomuto Partition). Për të kuptuar idenë në të cilën mbështetet
grupimi i Lomuto-s është e dobishme të mendojmë një tabelë, ose në mënyrë më të
përgjithshme, një nëntabelë a[m..d] (0 ≤ m ≤ d ≤ n − 1), sikur ajo përbëhet nga tre segmente
të vazhdueshëm. Listuar sipas kësaj rradhe në lidhje me vlerën e elementit bosht p: një
segment me elemente që dihet që janë më të vegjël se p, segmenti i elementeve që dihet që
janë më të mëdhenj ose të barabartë me p dhe së fundmi segmenti i elementeve ende për t’u
krahasuar në lidhje me p (shih figura 7.1a). Vëmë në dukje se segmentet mund të mos kenë
elemente; për shembull e tillë është gjithmonë gjendja fillestare kur algoritmi nuk ka filluar të
funksionojë dhe dy segmentet e parë janë pa elemente.

gjendja (a)
Segmenti 1 Segmenti 2 Segmenti 3
vlerat më të vogla vlerat jo më të vogla vlerat e panjohura
m s i d
p <p ≥p ?

gjendja (b)
m s d
p <p ≥p

gjendja (c)
m s d
<p p ≥p

Figura 7.1 Ilustrimi i ndarjes në grupe sipas Lomuto-s

Duke filluar me i = m + 1, algoritmi skanon nëntabelën a[m..d] nga e majta në të djathtë,


duke mbajtur këtë strukture derisa të përfundojë një ndarje. Në çdo iteracion, ai krahason
elementin e parë në segmentin e panjohur (shënjuar me anë të indeksit i në figurën 7.1a) me
boshtin p. Nëse a[i] ≥ p, i thjesht zmadhohet për të shtrirë segmentin e elementeve më të
mëdhenj ose të barabartë me p, duke ngushtuar ndërkaq segmentin e paprocesuar. Nëse a[i] ≤
p, është segmenti i elementeve më të vegjël se p që ka nevojë të zgjerohet. Kjo bëhet me anë
të zmadhimit të s, indeksi i elementit të fundit në segmentin e parë, duke përkëmbyer a[i] me
a[s], dhe duke zmadhuar i për të shënjuar elementin e parë të ri në segmentin e papërpunuar
të ngushtuar. Pasi nuk mbeten më elemente për t’u përpunuar (Figura 7.1b), algoritmi
përkëmben boshtin p me a[s] për të përfunduar ndarjen e kërkuar (Figure 7.1c).

Më poshtë është zbatimi në pseudokod i kësaj procedure grupimi.


7 Teknika zvogëlo-dhe-sundo | 154

// Algoritmi 7.3 Grupimi i elementeve me anë të algoritmit Lomuto


// Të dhëna: Një nëntabelë a[m..d] e tabelës a[0..n − 1], e përcaktuar nga indekset e saj
// të majtë dhe të djathtë, m and d (m ≤ d) respektivisht
// Rezultate: Ndarja e a[m..d] dhe s, pozicioni ri i boshtit

LomutoPartition(m, d, a) {
p ←a[m]; // elementi bosht
s ← m; // pozicioni i elementit bosht
for i ←m + 1 to d do
if a[i] < p
then{s ←s + 1;
swap(a[s], a[i]);
}
swap(a[m], a[s]);
return s
}

Si mund të përfitojmë nga procesi i ndarjes së një tabele për të gjetur elementin e ktë më të
vogël në të? Le të supozojmë se tabela është indeksuar duke filluar me 0, dhe le të jetë s
pozicioni i elementit ndarës, domethënë, indeksi i elementit bosht të tabelës të pas ndarjes.
Nëse s = k – 1 atëherë boshti p është vetë elementi i ktë më i vogël, i cili zgjidh problemin.
Nëse s > k – 1 atëherë elementi i ktë më i vogël i gjithë tabelës mund të gjendet si elementi i
ktë më i vogël në anën e majtë të tabelës së ndarë. Dhe nëse s < k – 1 ai mund të gjendet si
elementi i (k – s)të më i vogël në anën e tij të djathtë. Kështu, nëse nuk e zgjidhim menjëherë
problemin, ne zvogëlojmë përmasën e tij me një, e cila mund të zgjidhet me të njëjtën mënyrë
rekursivisht. Ky algoritëm quhet Quickselect. Për të gjetur elementin e ktë më të vogël në
tabelën a[0..n – 1] me anë të këtij algoritmi, ne kryejmë thirrjen QuickSelect(0, 1, a, k) në
algoritmin kryesor.

// Algoritmi 7.4 Gjetja e elementit të k-të më të vogël në mënyrë rekursive


// Të dhëna: Një nëntabelë a[m:d] e tabelës A[0:n – 1] dhe
// një numër pozitiv k (1 ≤ k ≤ d – m +1),
// Rezultate: vlera e elementit të k-të me të vogël në nëntabelën a[m:d]

QuickSelect (m, d, a, k) {
if (m ≤ d)
then { s ← LomutoPartition(m, d, a);
if (s = k – 1)
then return a[s]
else if s > k − 1
then QuickSelect (m, s - 1, a, k)
else QuickSelect (s + 1, d, a, k)
}
else return
}

Në fakt, e njëjta ide mund të zbatohet po ashtu edhe në mënyrë iterative. Për versionin
iterativ, bile nuk është e nevojshme as përshtatja e vlerës së k por thjesht të vazhdohet derisa s
= k − 1.
7 Teknika zvogëlo-dhe-sundo | 155

Shembull. Të zbatohet algoritmi i mbështetur në ndarje për të gjetur medianën e vargut të


mëposhtëm të numrave {4, 1, 10, 8, 7, 12, 9, 2, 15} të cilët janë vendosur në një tabelë.

Në këtë rast k = ceiling(9/2) = 5 dhe detyra jonë është që të gjejmë elementin e 5-të më të
vogël. Për lehtësi shënimesh do të supozojmë që elementet e tabelës janë indeksuar nga 0 deri
tek 8.

Do të përdorim versionin e mësipërm për ndarjen e tabelës, duke kuadratuar elementin bosht.

0 1 2 3 4 5 6 7 8
s i
4 1 10 8 7 12 9 2 15
s i
4 1 10 8 7 12 9 2 15
s i
4 1 10 8 7 12 9 2 15
s i
4 1 2 8 7 12 9 10 15
s i
4 1 2 8 7 12 9 10 15
2 1 4 8 7 12 9 10 15

Meqenëse s = 2 është më e vogël se k – 1 = 4, më tej do të vazhdojmë të përpunojmë anën e


djathtë të tabelës:

0 1 2 3 4 5 6 7 8
s i
8 7 12 9 10 15
s i
8 7 12 9 10 15
s i
8 7 12 9 10 15
7 8 12 9 10 15

Tani s = k – 1 = 4, dhe prandaj këtu mund të ndalojmë : mediana e gjetur është 8, e cila është
më e madhe se 2, 1, 4, dhe 7 por është më e vogël se 12, 9, 10, dhe 14.

Sa i efektshëm është algoritmi Quickselect? Ndarja e një tabele me n elemente kërkon


gjithmonë n – 1 krahasime të çelsit. Nëse ai prodhon ndarjen që zgjidh problemin e
përzgjedhjes pa kërkuar iteracione të tjera atëherë për këtë rast, më i miri, ne përftojmë
Kmir (n) = n – 1∈ Θ(n). Për fat të keq, algoritmi mund të prodhojë një ndarje skajisht të
pabalancuar për një tabelë të dhënë, me një pjesë që është e zbrazët dhe një tjetër që përmban
n – 1 elemente. Në rastin më të keq kjo mund të ndodhë për secilin nga n – 1 iteracionet (Si
një rast i veçantë për rastin më të keq të të dhënave, le të konsiderojmë rastin kur k = n dhe
një tabelë rigorozisht rritëse.) Kjo sjell që
K keq = (n − 1) + (n − 2) + . . . + 1= (n − 1) n/2 ∈ Θ(n2 ),
7 Teknika zvogëlo-dhe-sundo | 156

e cila as që nuk mund të krahasohet me metodën e drejtpërdrejtë, bazuar në renditje, që u


përmend në fillim të seksionit. Si rrjedhim dobishmëria e algoritmit të bazuar në ndarjen
varet nga efektshmëria në rastin mesatar. Për fat të mirë, një analizë matematikore e
kujdesshme ka treguar se efektshmëria në rastin mesatar është lineare. Ndërkaq është zbuluar
nga shkencëtarët e informatikës një mënyrë më e sofistikuar për zgjedhjen e boshtit në
QuickSelect që garanton një kohë lineare bile edhe në rastin më të keq por që është tepër e
ndërlikuar për ta rekomanduar për zbatime praktike.

Është e vlefshme gjithashtu të vëmë në dukje që algoritmi QuickSelect jo vetëm që përcakton


elementin e ktë më të vogël por ai tregon edhe k elementet më të vegjël dhe n − k elementet
më të mëdhenj.

Kërkimi me interpolim
Si shembull tjetër i një algoritmi në këtë kategori le të konsiderojmë një algoritëm për
kërkimin në një tabelë të renditur të quajtur kërkimi me interpolim (interpolation search). Në
ndryshim nga kërkimi binar, i cili krahason gjithmonë një çelës kërkimi me vlerën e mesit në
një tabelë të dhënë (dhe pra redukton përmasën e problemit përgjysëm), kërkimi me
interpolim, merr parasysh vlerën e çelsit të kërkimit për të gjetur elementin e tabelës që do të
krahasohet me çelsin e kërkimit. Në një farë kuptimi, algoritmi imiton mënyrën se si ne
kërkojmë në një numërator telefonik: nëse kërkojmë për dikë që quhet Agim, ne e hapin
numëratorin jo në mes por në fillim, në ndryshim nga mënyra kur kërkojmë për dikë që quhet
Sajmir.

Më saktësisht, në përsëritjen që trajton pjesën e tabelës ndërmjet elementit më të majtë a[m]


dhe elementit më të djathtë a[d], algoritmi supozon se vlerat e tabelës rriten linearisht,
domethënë, gjatë një vije të drejtë ndërmjet dy pikave (m, a[m]) dhe (d, a[d]). Prandaj, vlera e
çelsit të kërkimit v krahasohet me elementin, indeksi i të cilit llogaritet (i rrumbullakosur) si
koordinata x e pikës në vijën e drejtë që bashkon pikat (m, a[m]) dhe (d, a[d]), koordinata y e
të cilës është e barabartë me vlerën që kërkohet v, (figura 7.2).

vlera

A[d]

A[m]

indeksi
m x d

Figura 7.2 Llogaritja e indeksit në kërkimin me interpolim

Duke shkruar ekuacionin standard për vijën e drejtë që kalon nëpër dy pika të dhëna, duke
zëvendësuar v për y, dhe duke e zgjidhur në lidhje me x do të kemi formulën pasuese:

(υ−a[m])(d−m)
x = m + floor( ) (7.5)
a[d]−a[m]

Llogjika që qëndron pas këtij përafrimi është e thjeshtë. Ne dimë që vlerat e tabelës janë në
rritje (me saktësisht nuk janë në zbritje) nga a[m] deri në a[d], por nuk e dimë se si ecin.
Duke patur vlerat e tabelës në rritje lineare, që është mënyra më e thjeshtë e mundshme,
indeksi i llogaritur nga formula (7.5) duhet të jetë pozicioni i elementit të tabelës me vlerë të
7 Teknika zvogëlo-dhe-sundo | 157

barabartë me v. E qartë që nëse v nuk është ndërmjet a[m] dhe a[d] formula (7.5) nuk mund të
zbatohet (pse?).

Pas krahasimit të v me a[x] algoritmi ose ndalon (nëse ato janë të barabartë) ose vazhdon të
kërkojë në të njëjtën mënyrë ndërmjet elementeve të indeksuar ose ndërmjet m dhe x – 1 ose
ndërmjet x + 1 dhe d, në varësi të faktit nëse a[x] është më e vogël apo më e madhe se v.
Kështu, përmasa e rastit të problemit reduktohet, por paraprakisht nuk e dimë se në çfarë
madhësie.

// Algoritmi 7.5 Kërkimi me anë të interpolimit


// Të dhëna: Një vlerë v dhe një tabelë e renditur a[1:n] në rendin rritës
// Rezultate: x, indeksi i kërkuar ose – 1 në qoftë se vlera nuk gjendet në tabelë

kerkoMeInterpolim (v, n, a) {
i ← 1;
j ← n;
am ← a[i];
ad ← a[j];
while (i < j) do {
x ← ( i + (j – i) * (v – am) div (ad – am));
mes ← a[x];
if (v = mes)
then return x;
if (v > mes)
then{ i ← x + 1;
am ← mes}
else{ j ← x;
ad ← mes };
}
return –1
}

Analiza e efektshmërisë së algoritmit tregon që kërkimi me interpolim shpenzon më pak se


log2log2 n + 1 krahasime të çelsit në rastin mesatar të të dhënave kur kërkon në një listë me
çelsa të shpërndarë në mënyrë të rastit. Ky funksion rritet aq ngadalë saqë numri i
krahasimeve do të jetë një konstante shumë e vogël për të gjitha problemet praktike të
mundshme. Por në rastin më të keq kërkimi me interpolim është vetëm linear që mund të
konsiderohet e keqe (pse?). Si një thënie të fundit për rëndësinë e kërkimit me interpolim ndaj
kërkimit binar, mund të përmendim një pohim të R. Sedgewick-ut sipas të cilit kërkimi binar
është probabilisht më i miri për skedarë të vegjël por kërkimi me interpolim është më i mirë
për skedarë të mëdhenj dhe për zbatime ku krahasimet janë në veçanti të shtrenjta ose kostot
e aksesit janë shumë të larta.

7.3 Përmbledhje

• Zvogëlo-dhe-Sundo është një teknikë e përgjithshme për hartimin e algoritmeve, e bazuar


në shfrytëzimin e një lidhjeje ndërmjet një rasti të dhënë të problemit dhe një rasti më të
7 Teknika zvogëlo-dhe-sundo | 158

vogël të të njëjtit problem. Sapo një lidhje e tillë të përcaktohet, ajo mund të shfrytëzohet
nga lart-poshtë (rekursivisht) ose nga poshtë-lart (pa rekursion)
• Ekzistojnë dy variante të teknikës zvogëlo-dhe-sundo:
 redukto me një faktor konstat, më shpesh dy
 zvogëlo me një madhësi variabël
• Kërkimi binar dhe ngritja në fuqi janë dy shembuj të zbatimit të reduktimit me një faktor
konstant.
• Për disa algoritme të bazuar në versionin zvogëlo me një madhësi variabël, madhësia e
reduktimit ndryshon nga një iteracion në tjetrin. Shembuj algoritmesh të realizuar me këtë
version janë algoritmi i Euklidit, problemi i përzgjedhjes, kërkimi me interpolim, etj.

7.4 Ushtrime për Kapitullin 7

1. Jepet tabela vijuese me numra të plotë {3, 14, 27, 31, 39, 42, 55, 70, 74, 81, 85, 93, 98}
a. Cili është numri më i madh i krahasimeve të elementeve të kryera nga algoritmi i
kërkimit binar për kërkimin e një elementi në tabelën e mëposhtme?
b. Të gjenden të gjithë elementet e kësaj tabele që do të kërkonin numrin më të madh
të krahasimeve kur kërkohet një element me anë të algoritmit të kërkimit binar.

2. Zgjidhni rekurencën 𝑇(𝑛) = 𝑇(⌊𝑛/2⌋) + 1 për n >1, T(1) = 1, për n = 2k me anë të


zëvendësimit.

3. Përgjigjuni pyetjeve të mëposhtme.


a. Provoni barazimin ⌊log 2 𝑛⌋ + 1 = ⌈log 2 (𝑛 + 1)⌉ për n ≥ 1.
b. Provoni që 𝑇(𝑛) = ⌊log 2 𝑛⌋ + 1 kënaq ekuacionin (7.3) për çdo numër tek të plotë
pozitiv n.

4. Përshkruani se si mund të përdoret kërkimi binar për të ashtuquajturin kërkim i rangut,


domethënë, për të përcaktuar segmentin (pozicionet) se ku gjenden të gjithë ata elemente
të një tabele të dhënë të renditur me n elemente, të përfshirë ndërmjet dy vlerave të dhëna
P dhe S (P ≤ S), përfshirë kufijtë.

5. Hartoni një algoritëm rekursiv për kërkimin binar.

6. Hartoni një version të algoritmit të kërkimit binar që për krahasimin e elementit me


vlerën e kërkuar përdor vetëm krahasime të formës ≤ dhe =. Algoritmin kontrollojeni me
kujdes mbasi në raste të tilla gabimet janë shumë të mundshme.

7. Analizoni efektshmërinë kohore të versionit të ushtrimit 6 për rastin më të keq dhe për
rastin më i mirë të organizimit të të dhënave.

8. Duke argumentuar përgjigjen, të tregohet nëse janë të vërteta apo nuk janë të vërteta
pohimet e mëposhtme.
a. Një kërkim binar funksionon për një tabelë ta parenditur por ai është më i shpejtë në
një tabelë të renditur.
b. Përmasa e tabelës nuk është e vetmja konsideratë në zgjedhjen ndërmjet kërkimit
linear dhe kërkimit binar.
c. Një kërkim binar në një tabelë presupozon që tabela të jetë e renditur.
7 Teknika zvogëlo-dhe-sundo | 159

d. Një kërkim binar mund të përdoret vetëm në tabela, elementet e të cilave janë numra të
plotë.
e. Një kërkim binar gjithmonë mund të përdoret në vend të një kërkimi linear.

9. Sa iteracione, nga vlerat e paraqitura më poshtë, do të duhet të kryhen me anë të kërkimit


binar për të gjetur një vlerë në një tabelë me 512 elemente për rastin më të keq të
organizimit të të dhënave?
a. 512 b. 256 c. 9 d. 3

10. Të llogaritet koha e ekzekutimit të kërkimit binar në versioni rekursiv në funksion të


numrit të krahasimeve të elementeve me elementin e kërkuar.

11. Supozojmë se jepet një tabelë e renditur me n numra të plotë të ndryshëm. Karshi kësaj
tabele zbatohet një rrotullim (zhvendosje) k pozicione, (1≤ k < n). Për shembull tabela {1,
2, 3, 4, 5} me anë të një rrotullimi me k = 3 kthehet në trajtën {4, 5, 1, 2, 3}. Paraqisni një
algoritëm që gjen elementin më të madh të tabelës së zhvendosur. Vlerësoni
kompleksitetin kohor të algoritmit të propozuar. Algoritmi i hartuar duhet ta ketë kohën e
ekzekutimit më të vogël se lineare.

12. Problemi i pikës fikse. Jepet një tabelë a me n numra të plotë të ndryshëm dhe e renditur
në rendin rritës. Të hartohet një algoritëm që të jetë në gjendje të përcaktojë nëse ekziston
një indeks i, i tillë që a[i] = i në një kohë O(log n).

13. Hartoni një algoritëm që llogarit numrin e shifrave të një numri të plotë pozitiv të dhënë.
Përcaktoni kohën e ekzekutimit në funksion të numrit të mbledhjeve.

14. Hartoni një algoritëm që llogarit shumën e shifrave të një numri të plotë pozitiv të dhënë.

15. Transporti i ushtarëve. Një repart me n ushtarë duhet të kalojë një lumë të gjerë dhe të
thellë ku nuk dallohet asnjë urë. Ata vunë re dy djem 12 vjeçarë që luanin me një barkë
në breg të lumit. Barka ishte aq e ngushtë sa mund të mbante vetëm 2 djemtë ose vetëm
një ushtar. Si mund të kalonin ushtarët lumin dhe t’u linin fëmijve barkën e tyre fëmijve?
Sa herë duhet që barka të kalonte nga bregu në breg?

16. Alternimi i gotave. Janë vendosur 2n gota në një vijë të drejtë njëra pas tjetrës, ku n gotat
e para janë mbushur me lëng fruti ndërsa n gotat e tjera janë bosh. Vendosini gotat në
mënyrë të alternuar në renditjen “e mbushur − e zbrazur − e mbushur − e zbrazur… ”
duke kryer një numër minimal lëvizje gotash.

17. Le të jetë a[0..n –1] një tabelë me n elemente që mund të renditen. (Për thjeshtësi do të
supozohet se të gjithë elementet janë të ndryshëm.) Thuhet se një çift elementesh (a[i],
a[j]) formon një inversion nëse kur i < j atëherë a[i] > a[j]. Cila nga tabelat me përmasë n
ka numrin më të madh të inversioneve dhe cili është ky numër? Përgjigjuni të njëjtave
pyetje për numrin më të vogël të inversioneve.

18. Në lidhje me algoritmin e ndarjes Lomuto:


a. Zbatoni algoritmin për të gjetur medianën e vargut të numrave {9, 12, 5, 17, 20}.
b. Provoni që efektshmëria e algoritmit, për rastin më të keq të organizimit të të
dhënave, për problemin e përzgjedhjes është kuadratike.
7 Teknika zvogëlo-dhe-sundo | 160

19. Të hartohet një algoritëm jo rekursiv sipas teknikës zvogëlo-dhe-sundo, që realizon


pjesëtimin e plotë të dy numrave të plotë m dhe n (funksioni DIV).

20. Hartoni një algoritëm për llogaritjen e pjestuesit më të madh të përbashkët të dy numrave
mbështetur në përkufizimin e PMP-se dhe krahasoheni me algoritmin e Euklidit.

21. Zbatoni algoritmin e Euklidit:


a. Për të gjetur pmp(31415, 14142).
b. Vlerësoni se sa herë më shpejt do të gjendet pmp(31415, 14142) duke e llogaritur me
algoritmin e Euklidit në krahasim me algoritmin e bazuar në kërkimin e numrave të
plotë të njëpasnjëshëm që nga min(m, n) deri tek pmp(m, n).

22. Çfarë bën algoritmi i Euklidit për një çift numrash kur i pari është më i vogël se i dyti? Sa
është numri më i madh i herëve që kjo mund të ndodhë gjatë ekzekutimit të algoritmit me
një të dhënë fillestare të tillë?

23. Në lidhje me algoritmin e Euklidit:


a. Cili është numri më i vogël i pjesëtimeve të kryera nga algoritmi i Euklidit për të
gjitha të dhënat fillestare që plotësojnë kushtin 1≤ m, n ≤ 10?
b. Cili është numri më i madh i pjesëtimeve të kryera nga algoritmi i Euklidit për të
gjitha të dhënat fillestare që plotësojnë kushtin 1≤ m, n ≤ 10?

24. Algoritmi i Euklidit, siç paraqitet në traktatin e Euklidit, përdor zbritjet në vend të
pjesëtimit të plotë. Shkruani një algoritëm për këtë version të algoritmit të Euklidit.
8 Teknika ndaj-dhe-sundo | 161

8 Teknika ndaj-dhe-sundo

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të njohë teknikën e ndaj-dhe-sundo për konceptimin e algoritmeve;


• të njohë algoritmin e renditjes mergesort dhe ta zbatojë atë;
• të njohë algoritmin e renditjes quicksort dhe ta zbatojë atë;
• të kuptojë dallimet ndërmjet mergesort dhe quicksort;
• të njohë algorimin e shumëzimit të numrave të mëdhenj me anë të teknikës ndaj-dhe-
sundo;
• të njohë algortimin e shumëzimit të matricave me anë të teknikes ndaj-dhe-sundo;
• të zbatojë teknikën ndaj-dhe-sundo për problemin e çiftit më të afërt të pikave në plan;

Ndaj-dhe-sundo (divide-and-conquer) është një nga teknikat më të njohura të dizajnit të


algoritmeve. Ndoshta fama mund të ketë lidhje me emrin intrigues, të cilin e meriton
plotësisht. Shumë algoritme të efektshëm, për probleme të rëndësishme, janë përftuar nga
zbatimi i kësaj metode. Algoritmet ndaj-dhe-sundo e kryejnë punën sipas planit të
përgjithshëm të mëposhtëm:

1. Një problem ndahet në disa nënprobleme të të njëjtit tip, mundësisht me të njëjtën


përmasë.
2. Zgjidhen nënproblemet (zakonisht në mënyrë rekursive, edhe pse ndonjëherë përdoret
ndonjë algoritëm i ndryshëm, veçanërisht kur nënproblemet bëhen mjaft të vegjël).
3. Nëse është e domosdoshme, zgjidhjet e nënproblemeve kombinohen për të përftuar
zgjidhjen e problemit origjinal.

Teknika ndaj-dhe-sundo paraqitet në trajtë diagrame në figurën 8.1, në të cilën përshkruhet


rasti i ndarjes së problemit në dy nënprobleme më të vegjël, rasti që ndodh më shpesh (të
paktën për algoritme ndaj-dhe-sundo të hartuar për t’u ekzekutuar në kompjutera me një
procesor të vetëm).

problem i përmasës n

nënproblemi 1 nënproblemi 2
i përmasës n / 2 i përmasës n / 2

zgjidhja e nënproblemit 1 zgjidhja e nënproblemit 2

zgjidhja e problemit origjinal

Figura 8.1 Teknika ndaj-dhe-sundo (rasti tipik)

Si një shembull, le të konsiderojmë problemin e llogaritjes së shumës të n numrave a1, a2, …,


an. Në qoftë se n > 1, ne mund ta ndajmë problemin në dy raste të të njëjtit problem: të
8 Teknika ndaj-dhe-sundo | 162

llogaritet shuma e ⌊𝑛/2⌋ numrave të parë dhe pastaj të llogaritet shuma e ⌈𝑛/2⌉ numrave që
mbesin:
a1 + ⋅⋅⋅ + an = (a1 + ⋅⋅⋅ + a⌊n/2⌋) + (a⌊n/2⌋+1 + ⋅⋅⋅ + an)

Natyrisht që në qoftë se n = 1, ateherë pergjigja është thjesht a1. Sapo secila nga këto shuma
të llogaritet, duke zbatuar të njëjtën metodë, domethënë, rekursivisht), ne mund të mbledhim
vlerat e tyre për të përftuar shumën e përgjithshme.

Në algoritmet e hartuara sipas teknikës ndaj-dhe-sundo është shumë e përshtatshme që kufijtë


e nëntabelave, që përfaqësojnë të dhënat e nënproblemeve, të emërtohen me anë dy
variablave, të quajtur për shembull: majtas, djathtas ose m dhe d. Duke marrë në
konsideratë këtë shënim, për problemin e shumës së elementeve të tabelës do të kemi majtas
= 1, djathtas = n.

//Algoritmi 8.1 Llogaritja e shumës së elementeve të një tabele


// Të dhëna: a[majtas:djathtas], një tabelë me (djathtas - majtas + 1) elemente
// Rezultate: shuma e elementeve të tabelës

shumaTab (majtas, djathtas, a) {


if (m ≥ d)
then return a[majtas]
else { mes = (majtas + djathtas) div 2;
return shumaTab(majtas, mes, a) + shumaTab(mes+1,djathas, a)
}
}

A është kjo një rrugë më e efektshme për të llogaritur shumën e n numrave sesa teknika e
forcës brutale? Mjafton një shembull i thjeshtë shumimi, le të themi, katër numra për të
treguar numri i mbledhjeve është i njëjtë për të dy algoritmet.

Kështu, jo çdo algoritëm i hartuar me teknikën ndaj-dhe-sundo është domosdoshmërisht më i


efektshëm sesa një zgjidhje me anë të teknikës së forcës brutale. Por ka raste që ndodh edhe
e kundërta. Koha e shpenzuar për ekzekutimin e planit ndaj-dhe-sundo bëhet më e vogël sesa
zgjidhja e problemit me ndonjë qasje tjetër. Në të vërtetë, teknika ndaj-dhe-sundo, prodhon
disa nga algoritmet me të rëndësishëm dhe më të efektshëm në informatikë. Disa nga
problemet më klasikë do të paraqiten në këtë leksion.

Shënim
Ka shumë autorë që i konsiderojnë algoritmet si për shembull, kërkimin binar, si raste të
degjeneruara të ndaj-dhe-sundo, ku vetëm një nga dy nënproblemet me gjysmën e përmasës
është e nevojshme të zgjidhet.

8.1 Metoda të zgjidhjes së rekurrencave

Sipas teknikës ndaj-dhe-sundo, në mënyrë më të përgjithshme, një problem i përmasës n


mund të ndahet në a nënprobleme të përmasave n/b secili, ku është e nevojshme të zgjidhen
të a nënproblemet e formuar. (Këtu a dhe b janë konstante që plotësojnë kushtet, a ≥ 1 dhe b
> 1). Për të thjeshtuar analizën kohore, duke supozuar se n është fuqi e b, përftojmë
rekurrencën pasuese për të shprehur kohën e ekzekutimit T(n):
8 Teknika ndaj-dhe-sundo | 163

T(n) = aT(n/b) + f(n), (8.1)


ku, f(n) është një funksion jorekursiv që llogarit kohën e shpenzuar për ndarjen e problemit të
përmasës n në nënprobleme të madhësisë n/b dhe/ose kombinimin e zgjidhjeve të tyre (Për
problemin e mësipërm të llogaritjes të shumës kemi a = b = 2 dhe f(n) = 1.) Rekurrenca (8.1)
quhet rekurrenca e përgjithshme ndaj-dhe-sundo (general divide-and-conquer recurrence).
Është e qartë se rendi i rritjes të zgjidhjes së saj T(n), varet nga konstantet a dhe b si dhe nga
rendi i rritjes të funksionit f(n).

Metoda e përgjithshme e zgjidhjes së rekurrencës ndaj-dhe-sundo


Zgjidhja e rekurrencave të trajtës (8.1) thjeshtohet në mënyrë të konsiderueshme nga teorema
e mëposhtme:

Teorema e themelore: Në qoftë se f(n) ∈ Θ(nd) dhe d ≥ 0 në ekuacionin rekurrencial (8.1)


atëherë për zgjidhjen T(n) ka vend pohimi:
Θ�nd � në se a < bd
d
T(n) ∈ �Θ(n log n) në se a = bd (8.2)
Θ�nlogb a � në se a > bd
Rezultate të ngjashme me 8.2, vlejnë edhe për shënimet O dhe Ω.

Për shembull, rekurrenca për numrin e mbledhjeve M(n) të kryera nga algoritmi i mësipërm
për llogaritjen e shumës për të dhëna të përmasës n është:

M(n) = 2 M(n/2) + 1

Duke zbatuar teoremën themelore, për këtë rast, me parametra a = 2, b = 2 dhe d = 0 dhe
meqenëse a > bd do të kemi:

M(n) ∈ Θ(nlogb a ) ≡ Θ(nlog2 2 ) ≡ Θ(n)

Vëmë ne dukje që me anë të teoremës themelore gjendet vetëm klasa e efektshmërisë dhe jo
zgjidhja e ekuacionit rekurrencial që do të prodhonte një përgjigje të saktë (të paktën kur n
është fuqi e b).

Metoda e pemës së rekursionit


Le të përdorim metodën e pemës së rekursionit për zgjidhjen e ekuacionit (8.1) në rastin e
veçantë kur f(n) = n. Duke lënë mënjanë për momentin rastin bazë, ne mund ta interpretojmë
rekurrencën

T(n) = 2T(n/2) + n, (8.3)

në trajtën: “në mënyrë që të zgjidhim një problem të përmasës n, ne duhet të zgjidhim 2


probleme të përmasës n/2 dhe të kryejmë n njësi pune shtesë.”

Në figurën 8.2 po paraqesim fillimin e punës për ndërtimin e diagramës të rekursionit për
rekurrencen (8.3). Për thjeshtësi le të supozojmë që n është një fuqi e 2-it. Kështu, për të
filluar, pemën e rekursionit për (8.3) ne tregojmë që, në nivelin 0 në të majtë, kemi një
8 Teknika ndaj-dhe-sundo | 164

problem të përmasës n. Pastaj vizatojmë të ashtuquajturën rrënjë me dy brinjë që dalin nga


ajo për të treguar që ndahet në dy probleme. Në të djathtë tregojmë që kemi për të kryer një
punë shtesë me kosto n njësi pavarësisht se çdo do të bëhet me dy problemet e rinj të krijuar.
Në mënyrë që diagrama të përmbajë informacion të vlefshëm plotësojmë një pjesë të nivelit
të parë. Vizatojmë dy kulme në mes që paraqesin dy problemet në të cilat është ndarë
problemi kryesor dhe tregojmë në anën e majtë që secili nga këta probleme e ka përmasën
n/2.

Niveli Madhësia e problemit Pema Kosto shtesë


0 n n

1 n/2
Figura 8.2 Faza fillestare e vizatimit të pemës të rekursionit

Tani mund të shihet se si rekurrenca është reflektuar në nivelet 0 dhe 1 të pemës së


rekursionit. Kulmi i sipërm i pemës paraqet T(n), në nivelin pasues kemi dy probleme të
madhësisë n/2, që na japin termin rekursiv 2T(n/2) të rekurrencës. Pastaj pasi zgjidhim këto
dy probleme kthehemi në nivelin 0 të pemës dhe kryejmë një punë shtesë prej n njësish për
termin jorekursiv të rekurrencës.

Tani mund të vazhdojmë vizatimin e mëtejshëm të pemës. Plotësimi i asaj që ka mbetur nga
niveli 1 dhe shtimi i disa niveleve të tjera paraqitet në figurën 8.3.

Niveli Madhësia e problemit Pema Kosto shtesë


0 n n

1 n/2 n / 2 + n / 2= n

2 n/4 n/4+n/4+n/4+n/4=n

3 n/8 8 (n / 8) = n

Figura 8.3 Katër nivele të një peme rekursioni

Le të përmbledhin atë ç’ka na thotë diagrama. Në nivelin zero (në krye), janë kryer n njësi
punë shtesë. Shohim që në secilin nivel pasues, kemi përgjysmuar madhësinë e problemit dhe
dyfishuar numrin e nënproblemeve. Gjithashtu shohim që në nivelin 1, secili nga dy
nënproblemet kërkon n/2 njësi kosto shtesë, dhe se gjithsej janë kryer n njësi punë shtesë në
këtë nivel. Në mënyrë të ngjashme niveli 2 ka 4 nënprobleme të përmasës n/4 secili dhe
prandaj kryhet 4(n/4) = n punë shtesë.

Shohim që për këtë problem, në nivelin i, kemi 2i nënprobleme të përmasës n/2i. Më tej,
përderisa një problem i madhësisë 2i kërkon 2i njësi punë shtesë, do të kemi (2i)[n/(2i)] = n
njësi pune shtesë për nivel.

Për të llogaritur numrin e niveleve të pemës së rekursionit, mjafton të vëmë në dukje që në


çdo nivel madhësia e problemit përgjysmohet dhe pema mbyllet kur madhësia e problemit
bëhet 1. Prandaj pema përbëhet nga log n + 1 nivele. E gjithë pema është paraqitur në figurën
8.4.
8 Teknika ndaj-dhe-sundo | 165

Niveli Madhësia e problemit Pema Kosto shtesë


0 n n

1 n/2 n / 2 + n / 2= n

2 n/4 n/4+n/4+n/4+n/4=n

3 n/8 8 (n / 8) = n
. . . . . . . . . . . . . . . . . . . . . . .
lg n 1 . . . n.1

Figura 8.4 Një diagramë e përfunduar e një peme rekursioni

Niveli më i poshtëm është i ndryshëm nga nivelet e tjerë, në të cilët puna përshkruhej nga
pjesa rekursive e rekurrencës (në rastin në shqyrtim nga T(n) = 2T(n) + n). Në nivelin më të
poshtëm, puna përcaktohet nga rasti bazë. Kështu ne duhet të llogaritim numrin e problemeve
të madhësisë 1 (duke supozuar se kemi vetëm një rast bazë) dhe ta shumëzojmë pastaj këtë
vlerë me T(1). Në këtë nivel ne kemi n probleme dhe puna shtesë e kryer ne nivelin bazë, me
supozimin që T(1) = 1, është n.

Sapo të njohim me saktësi se sa nivele janë dhe se sa punë kryhet në çdo nivel atëherë mund
të shumojmë punën e kryer në të gjithë nivelet duke përftuar në këtë mënyrë zgjidhjen e
rekurrencës.

Në rastin në shqyrtim, kemi log n + 1 nivele, dhe në çdo nivel puna e kryer është n njësi. Pra
arrijmë në përfundimin se e gjithë puna që duhet për të zgjidhur problemin e përshkruar nga
rekurrenca (8.3), me kusht fillestar T(1) = 1 është e barabartë me n(log n + 1). Puna gjithsej e
kryer nëpërmjet pemës është zgjidhja e rekurrencës në shqyrtim mbasi pema thjesht modelon
procesin e përsëritjes të rekurrencës.

8.2 Algoritmi i renditjes Mergesort

Algoritmi Mergesort 13 është shembulli tipik i një zbatimi të suksesshëm të teknikës ndaj-dhe-
sundo. Ai rendit një tabelë të dhënë a[0..n – 1] duke e ndarë atë në dy gjysma a[0..⌊n/2⌋ - 1]
dhe a[⌊n/2⌋.. n - 1], duke e renditur secilën prej tyre në mënyrë rekursive dhe pastaj shkrin dy
tabelat më të vogla të renditura për të prodhuar një tabelë të vetme të renditur.

Algoritmi Mergesort mbështetet në dy ide themelore:

 Nëse një tabelë e kemi coptuar në dy nëntabela të renditura atëherë renditja e dy


nëntabelave të renditura kryhet më shpejt sesa renditja e tabelës fillestare të parenditur
(me ndonjë algoritëm). Ky proces shpenzon një kohë proporcionale me madhësinë e
tabelës fillestare dhe realizohet me anë të të ashtuquajturit proces i shkrirjes (merge) të
tabelave.
13
Shpikur nga John von Neumann në 1945
8 Teknika ndaj-dhe-sundo | 166

 Coptimi i tabelës fillestare deri në elemente të veçantë kryhet me anë të përgjysmimit të


vazhdueshëm. Ky proces shpenzon një kohë sa logaritmi i madhësisë së tabelës dhe
realizohet në mënyre rekursive.

Shkrirja e tabelave
Para se të japim algoritmin Mergesort, le të paraqesim në fillim procesin e shkrirjes së
tabelave. Le të supozojmë se një tabelë e dhënë është dypjesëshe e renditur. Një tabelë
quhet dypjesëshe e renditur atëherë kur pjesa e parë, që nga elementi i parë deri tek elementi i
mesit është e renditur, dhe po ashtu pjesa e dytë, që nga elementi pas mesit deri tek i fundit
është e renditur. Nëse një tabelë e ka cilësinë e të qënurit dypjesëshe e renditur ajo mund të
renditet me lehtësi me anë të procesit të shkrirjes. Kjo lehtësi në kohë penalizohet në
shpenzim kujtese mbasi për të realizuar shkrirjen, do të përdoret një tabelë ndihmëse me të
njëjtën madhësi sa tabela e të dhënave fillestare.

Procesi i shkrirjes realizohet në dy faza:

Në fazën e parë tabela e dhënë a kopjohet në një tabelë ndihmëse c në mënyrë të


drejtëpërdrejtë nëpërmjet një cikli.

Në fazën e dytë në tabelën a kthehen rradhazi elementet nga tabela a, duke marrë herë nga
pjesa e parë dhe herë nga pjesa e dytë sipas parimit merret elementi më i vogël që ka mbetur
në tabelën c. Kur të jetë marrë edhe elementi i fundit nga tabela c, atëherë tabela a do të jetë e
renditur (Figura 8.5) .

Indekset 1 2 3 4 5
Tabela A (2-pjesëshe e renditur) 2 4 6 1 3
Kopjim i drejtpërdrejtë
Tabela C (2-pjesëshe e renditur) 2 4 6 1 3
shkrirje
Tabela A (e renditur) C(4) C(1) C(5) C(2) C(3)
Figura 8.5 Procesi i shkrirjes së një tabele dypjesëshe të renditur

Algoritmi i mëposhtëm, Merge, realizon të ashtuquajturën shkrirje në vend mbasi rezultati


vendoset drejtpërsëdrejti në tabelën e të dhënave fillestare.

// Algoritmi 8.2 Shkrirja e drejtëpërdrejtë në vend e dy tabelave


// Të dhëna: një tabelë a[majtas:djathtas], dypjesëshe e renditur; mes, mesi i tabelës
// Rezultate: tabela a, e renditur

Merge(majtas, djathtas, a, mes) {


for i← majtas to djathtas do // kopjimi i të dhënave fillestare në një tabelë të re
c[i] ← a[i];
i ← majtas ; // indeks për të bredhur në pjesën e parë
j ← mes + 1; // indeks për të bredhur në pjesën e dytë
k ← majtas ; // indeks i bredhjes për të dhënat rezultat

// shkrin elementet duke marrë nga të dy pjesët e tabelës c


8 Teknika ndaj-dhe-sundo | 167

while (i ≤ mes) and (j ≤ djathtas) do {


if c[i] < c[j]
then //elementi rradhës furnizohet nga pjesa e parë e tabelës, c
{ a[k] ← c[i];
i ← i + 1;
k←k+1
}
else //elementi rradhës furnizohet nga pjesa e dytë e tabelës, c
{ a[k] ← c[j];
j ← j + 1;
k ← k + 1};
}
//Nëse kanë mbetur elemente nga pjesa e parë e tabelës
while (i ≤ mes) do {
a[k] ← c[i];
i ← i + 1;
k ← k + 1;
}
}

Algoritmi përbëhet nga disa blloqe. Në bllokun e parë, (rreshtat 1, 2) kopjohet tabela e të
dhënave fillestare në një tabelë ndihmëse me të njëjtën përmasë. Në bllokun e dytë, në
rreshtat 3 deri 5 realizohet inicializimi i indekseve shënjues për lëvizjen në tabela. Në
bllokun e tretë, në rreshtat 6 deri 15, kryhet zgjedhja e elementit më të vogël nga pjesa e parë
apo nga pjesa e dytë e tabelës c deri sa të ketë elemente të pa prekur në këto dy pjesë. Si
kusht i përfundimit këtij cikli shërben kontrolli i njëkohshëm i dy indekseve të bredhjes nga
ana e majtë dhe ana e djathtë. Në bllokun e katërt, rreshtat 16 deri 19, realizohet kalimi e
elementeve që kanë mbetur nga pjesa e majtë (nëse kanë mbetur). Nëse kanë mbetur
elemente nga pjesa e djathtë ata janë në vendin e tyre në tabelën fillestare a dhe nuk është e
domosdoshme të rikopjohen.

Algoritmi i renditjes MergeSort


Duke patur ndihmën e algoritmit të shkrirjes, për të renditur një tabelë të dhënë mjafton që
ajo të ndahet në dy pjesë, të renditen në mënyrë rekursive të dyja pjesët dhe më pas të
shkrihen të dy pjesët në një të vetme por tashmë të renditur. Më poshtë vijon algoritmi
Mergesort (teknikisht është proces rekursiv), i cili thirret për punë në algoritmin kryesor në
formën: MergeSort(a, 0, n – 1).

// Algoritmi 8.3 Algoritmi i renditjes MergeSort


// Të dhëna: një tabelë a[majtas .. djathtas],
// Rezultate: tabela a, e renditur

MergeSort (a, majtas , djathtas) {


if (majtas = djathtas)
then return
else { mes ← (djathtas + majtas ) div 2;
MergeSort(a, majtas , mes);
MergeSort(a, mes+1, djathtas);
Merge(majtas, djathtas, a, mes,);
8 Teknika ndaj-dhe-sundo | 168

}
}

Algoritmi Mergesort rendit tabelën a[majtas…djathtas] duke e ndarë në dy nëntabela ose


pjesë: a[majtas…mes] dhe a[mes+1…djathtas], duke i renditur në mënyrë të pavarur, me
thirrje rekursive dhe pastaj duke i shkrirë të dy nëntabelat e renditura.

Dinamika e ekzekutimit të algoritmit për të dhënat {8, 3, 2, 9, 7, 1, 5,4} paraqitet në figurën


8.6.
8 3 2 9 7 1 5 4

8 3 2 9 7 1 5 4

8 3 2 9 7 1 5 4
ndarja
8 3 2 9 7 1 5 4

3 8 2 9 1 7 4 5
shkrirja

2 3 8 9 1 4 5 7

1 2 3 4 5 7 8 9

Figura 8.6 Dinamika e ekzekutimit të algoritmit MergeSort

Sa i efektshëm është algoritmi Mergesort? Duke supozuar për thjeshtësi se n është fuqi e 2,
relacioni rekurrencial për numrin e krahasimeve të elementeve K(n) do të jetë

K(n) = 2K(n/2) + Kmerge (n) për n > 1, K(1) = 0

Le të analizojmë funksioin Kmerge (n) , që llogarit numrin e krahasimeve të elementeve të


kryera gjatë fazës të shkrirjes. Në çdo hap, kryhet saktësisht një krahasim, pas të cilit numri
përgjithshëm i elementeve në të dy tabelat që janë ende e nevojshme të procesohen,
zvogëlohet me 1. Në rastin më të keq të të dhënave, asnjë nga të dy tabelat nuk zbrazet para
se tjetra të mbajë tamam një element (domethënë, elementet më të vegjël të mund të vijnë nga
tabela tjetër). Prandaj, për rastin më të keq, Kmerge(n) = n – 1, dhe do të kemi rekurrencën

Kkeq (n) = 2 Kkeq (n/2) + n – 1 për n > 1, Kkeq (1) = 0.

Prandaj, në përputhje me teoremën themelore, Kkeq (n) ∈ Θ(n log n). Në fakt, është e lehtë të
gjendet zgjidhja e saktë për rastin më të keq të rekurrencës për n = 2k:

Kkeq (n)= n logn – n + 1.

Koha e ekzekutimit të algoritmit Mergesort nuk varet nga renditja fillestare e të dhënave; si
në rastin më të keq, më të mirë dhe mesatar koha e ekzekutimit është e rendit n lg n.

E vetmja e metë e algoritmit Mergesort është se ai kërkon një kujtesë shtesë të rendit n për
procesin e shkrirjes. Është e mundur që të realizohet një algoritëm shkrirjeje në vend pa
8 Teknika ndaj-dhe-sundo | 169

kujtesë shtesë, por algoritmi do të ishte shumë i ndërlikuar dhe në të do të shfaqej një faktor
shumëzues shumë i madh. Prandaj një Mergesort i tillë paraqet vetëm interes teorik.
Algoritmi Mergesort është një algoritëm i qëndrueshëm nëse shkrirja mbi të cilën bazohet
është e qëndrueshme. Në të vërtetë sa më i madh të jetë ndërlikimi i algoritmit aq më e madhe
do të jetë tendenca e tij për të shqetësuar qëndrueshmërinë.

Si rrjedhim, kjo metodë është mirë të zbatohet në rastet kur shpejtësia e zgjidhjes së detyrës
është e rëndësishme si dhe nëse kihet në dispozicion një sasi e mjaftueshme kujtese.

8.3 Algoritmi i renditjes Quicksort

Algoritmi Quicksort, është një algoritëm tjetër i rëndësishëm që bazohet në teknikën ndaj-
dhe-sundo, në ndarjen e të dhënave në grupe, të cilët renditen në mënyrë të pavarur. Në
ndryshim nga algoritmi Mergesort, i cili i ndan të dhënat në lidhje me pozicionin që kanë ato
në tabelë, algoritmi Quicksort i ndan të dhënat në përputhje me vlerën e tyre. Me idenë e
ndarjes jemi njohur në algoritmin Lomuto. Një ndarje është një ripozicionim i elementeve të
një tabele në mënyrë të tillë që të gjithë elementet në të majtë të një elementi të caktuar a[s]
të jenë më të vegjël ose të barabartë me a[s] dhe të gjithë elementet në të djathtë të a[s] të
jenë më të mëdhenj ose të barabartë me të:

a[0]
�� ��⋯ a[s��
��� −��
1] a[s] �������������
a[s + 1] ⋯ a[n − 1]
Kë ta elemente janë ≤ A[s] Kë ta elemente janë ≥ A[s]

Është e qartë, që pasi të jetë kryer një ndarje, elementi a[s] do të jetë në pozicionin e tij
përfundimtar në tabelën e renditur dhe ne mund të vazhdojmë duke renditur të dy nëntabelat
në të majtë dhe në të djathtë të elementit a[s] në mënyrë të pavarur (domethënë, me anë të së
njëjtës metodë).

Vëmë në dukje ndryshimin me algoritmin Mergesort: tek ky i fundit, ndarja në dy


nënprobleme është e menjëhershme dhe e gjithë puna ndodh në kombinimin e zgjidhjeve të
tyre; tek algoritmi Quicksort e gjithë puna ndodh në fazën e ndarjes dhe nuk kryhet punë për
të kombinuar zgjidhjet e nënproblemeve.

Më poshtë vijon algoritmi Quicksort (realizuar si procedurë rekursive), i cili thirret për punë
në algoritmin kryesor në formën: Quicksort(a, 0, n – 1).

// Algoritmi 8.4 Renditja e një nëntabele me anë të algoritmit Quicksort


// Të dhëna: një nëntabelë a[m..d] e tabelës a[0..n – 1]
// Rezultate: tabela a, e renditur në rendin jozbritës

Quicksort(m, d, a) {
if (m < d)
then
{ s ← HoarePartition(m, d, a) ; //s, është pozicioni i ndarjes
Quicksort(a, m, s - 1);
Quicksort(a, s + 1, d)
}
else return
}
8 Teknika ndaj-dhe-sundo | 170

Si algoritëm për ndarjen, sigurisht që mund të përdorim algoritmin e Lomuto-s të diskutuar


në leksionin e kaluar. Por do të veprojmë ndryshe, do ta ndajmë tabelën a[0..n – 1] dhe në
përgjithësi nëntabelën e tyre a[m..d] (0 ≤ m <d ≤ n − 1) me anë të një metodë më të
sofistikuar të propozuar nga C.A.R. Hoare, një shkencëtar i shquar britanik që ka shpikur
Quicksort 14.

Si më parë, fillojmë me zgjedhjen e elementit bosht, në lidhje me vlerën e të cilit do të bëhet


ndarja e nëntabelave. Ekzistojnë strategji të ndryshme për zgjedhjen e boshtit, çështje që do
t’i rikthehemi kur të studjojmë efektshmërinë e algoritmit. Për momentin, për zgjedhjen e tij,
do të përdorim një nga strategjitë më të thjeshta, si bosht do të jetë elementi i parë i
nëntabelës, vlerën e të cilit po e shënojmë me p = a[m].

Në ndryshim nga algoritmi i Lomuto-s, nëntabela do të skanohet nga të dy skajet, duke


krahasuar elementet e nëntabelës me boshtin. Skanimi nga e majta në të djathtë, i drejtuar nga
indeksi i, fillon me elementin e dytë. Meqenëse ne duam, që elementet më të vegjël se boshti,
të jenë në pjesën e majtë të nëntabelës, ky skanim i kapërcen elementet që janë më të vegjël
se boshti dhe ndalon me të takuar elementin e parë më të madh ose të barabartë me boshtin.
Skanimi nga e djathta në të majtë, i drejtuar nga indeksi j, fillon me elementin e fundit të
nëntabelës. Meqenëse ne duam që elementet që janë më të mëdhenj se boshti të jenë në
pjesën e djathtë të tabelës, ky skanim i kapërcen elementet më të mëdhenj se boshti dhe
ndalon kur takon elementin e parë më të vogël ose të barabartë me elementin bosht.

Pasi të dy skanimet ndalojnë mund të ndodhemi para tri situatave, në varësi të faktit nëse
indekset e skanimit janë apo nuk janë kryqëzuar.

Situata e parë. Në qoftë se indekset i dhe j nuk janë kryqëzuar, domethënë, i < j, atëherë
thjesht përkëmbejmë a[i] me a[j] dhe rifillojmë skanimin duke zmadhuar i dhe duke
zvogëluar j, respektivisht:

i→ ←j
p të gjithë ≤ p ≥p ∙∙∙ ≤p të gjithë ≥ p

Situata e dytë. Nëse indekset e skanimit janë kryqëzuar, domethënë, i > j, ne e kemi ndarë
tabelën pas përkëmbimit të boshtit me elementin a[j]:

←j i→
p të gjithë ≤ p ≤p ≥p të gjithë ≥ p

14
C.A.R. Hoare, e shpiku algoritmin në vitin 1960, në moshën 26 vjeçare, duke u përpjekur të
rendiste fjalët për një projekt përkthimi nëpërmjet kompjuterit, nga gjuha ruse në atë angleze. Njëzet
vjet më vonë ai fitoi çmimin Turing për “kontributin themelor për përcaktimin dhe hartimin e gjuhëve
të programimit”; në vitin 1980 është fisnikëruar nga mbretëresha e Anglise, për shërbimet e tij në
arsim dhe në shkencën kompjuterike.
8 Teknika ndaj-dhe-sundo | 171

Situata e tretë. Nëse indekset e skanimit kanë ndaluar duke treguar të njëjtin element,
domethënë, i = j, vlera mbi të cilën po shënjojnë është e barabartë me p (pse?). Kështu që
tabela është ndarë, me pozicion ndarjeje s = i = j:

← j = i→
p të gjithë ≤ p =p të gjithë ≥ p

Janë krijuar shumë versione të algoritmit të ndarjes sipas metodës së Hoare-it duke përdorur
cikle for, while…do ose do…while. Në algoritmin e mëposhtëm HoarePartition (realizuar si
funksion) është zbatuar cikli while…do nga i cili dilet kur kryqëzohen të dy indekset.

// Algoritmi 8.5 Ndarja e një nëntabele me algoritmin Hoare, si bosht elementi i majtë
// Të dhëna: një tabelë a[m .. d]
// Rezultate: tabela e ndarë a[m..d] dhe j, pozicioni i elementit bosht

HoarePartition (m, d, a) {
pivot ← a[m];
i ← m;
j ← d;
while i < j do {
// skanimi nga e majta në të djathtë
while a[i] ≤ pivot do
i ← i + 1;
// skanimi nga e djathta në të majtë
while a[j] > pivot do
j ← j – 1;
if i < j
then swap(a[i], a[j]); // përkëmbim i brendshëm
}
swap(a[m], a[j]); // përkëmbimi me elementin bosht
return j // kthimi i pozicionit të elementit bosht
}

Vëmë në dukje se gjatë zbatimit të metodës së ndarjes, indeksi i mund të dalë jashtë kufijve të
nëntabelës. Pasojat negative që mund të krijojë ky fakt mund të shmagen ose duke shtuar një
kontroll indeksi çdo herë që zmadhohet indeksi i, ose duke shtuar një sentinelë në fund të të
dhënave, në mënyrë që të mos lejohet kalimi i indeksit tej pozicionit n. Ndërkaq, vëmë në
dukje se janë krijuar metoda më të mira të zgjedhjes se boshtit të cila i bëjnë të panevojshme
këto marifete.

Në figurën 8.7 paraqitet dinamika e zbatimit të algoritmit të Quicksort ndaj tabelës me të


dhëna: {5, 3, 1, 9, 8, 2, 4, 7}. Si element bosht, për fillimin e procesit të ndarjes, është
zgjedhur elementi që ndodhet në skajin e majtë të tabelës, domethënë vlera 5. Në figurë,
elementet bosht janë të rrethuar me vijë të nxirë.
8 Teknika ndaj-dhe-sundo | 172

0 1 2 3 4 5 6 7
Ndarja e tabelës a[0..7]
i j
5 3 1 9 8 2 4 7
3 1 4 8 2 9 7
i j
5 3 1 4 8 2 9 7
i j
5 3 1 4 2 8 9 7
j i
5 3 1 4 2 8 9 7
2 3 1 4 5 8 9 7
Ndarja e nëntabelës a[0..3]
2 3 1 4
i j
2 3 1 4
2 1 3 4
j i
2 1 3 4
1 2 3 4
Ndarja e nëntabelës a[2..3]
i, j
3 4
j i
3 4
4
Ndarja e nëntabelës a[5..7]
i j
8 9 7
8 7 9
j i
8 7 9
7 8 9
7
9

Figura 8.7 Një shembull i ekzekutimit të algoritmit Quicksort

Në figurën 8.8 paraqitet pema e thirrjeve rekursive të Quicksort me vlerat m dhe d të kufijve
të nëntabelave dhe pozicioni i ndarjes s të përftuar nga ndarja.
8 Teknika ndaj-dhe-sundo | 173

m = 0, d = 7

s =4
m = 0, d = 3 m = 5, d = 7

s =6
m= 0, d = 0 m = 2, d = 3 m = 5, d = 5 m = 7, d = 7

s =2

m = 2, r = 1 m = 3, d = 3

Figura 8.8 Pema e thirrjeve rekursive

Analiza e algoritmit
Fillojmë diskutimin për efektshmërinë e algoritmit Quicksort, duke vënë në dukje që numri i
krahasimeve të elementeve të kryera para se një ndarje të përfundojë është n + 1, në qoftë se
indekset e skanimit kryqëzohen dhe n nëse barazohen. Nëse të gjitha ndarjet ndodhin në
mesin e nëntabelave korresponduese, do të ndodhemi në rastin më të mirë. Numri i
krahasimeve të elementeve për rastin më të mirë të të dhënave fillestare kënaq rekurrencën

Kmire (n) = 2Kmire (n/2) + n për n > 1, Kmire (1) = 0.

Duke zbatuar teoremën themelore do të kemi: Kmire (n) ∈Θ(n log2 n).

Duke e zgjidhur rekurrencën në mënyrë të saktë për n = 2k do të kemi Kmire (n) = n log2 n.

Në rastin më të keq, të gjitha ndarjet do të “shtrembërohen” në ekstrem: një nga nëntabelat do


të jetë bosh dhe përmasa e pjesës tjetër do të jetë tamam 1 më e vogël se përmasa e
nëntabelës që do të ndahet. Kjo situatë e pafat do të ndodhë, në veçanti, atëherë kur tabelat
janë të renditura në rendin rritës, domethënë, kur tabelat e të dhënave janë ashtu siç i
kërkojmë të jenë. Me të vërtetë, nëse a[0..n – 1] është një tabelë rigorozisht rritëse dhe do të
përdorim a[0] si bosht, skanimi nga e majta në të djathtë do të ndalojë tek elementi a[1],
ndërsa skanimi nga e djathta në të majtë do të shkojë deri sa të takojë a[0], duke treguar
ndarjen në pozicionin 0:

j← →i
a[0] a[1] ∙∙∙ a[n – 1 ]

Kështu, pas kryerjes të n + 1 krahasimeve për të përftuar këtë ndarje dhe përkëmbimin e
boshtit a[0] me vetveten, algoritmi do të lërë një tabelë rigorozisht rritëse a[1..n – 1] për të
renditur. Kjo renditje e tabelave rigorozisht rritëse e përmasave që zvogëlohen do të vazhdojë
derisa të përpunohet edhe e fundit prej tyre a[n – 2 , n – 1]. Numri i përgjithshëm i
krahasimeve të elementeve do të jetë i barabartë me

Kkeq (n) = (n + 1) + n + … + 3 = (n +1)(n + 2)/2 – 3 ∈ Θ(n2 ).

Kështu, çështja mbi dobishmërinë e algoritmi Quicksort qëndron në sjelljen e tij në rastin
mesatar. Teorikisht provohet që në rastin mesatar ai jep rezultatin më të mirë duke u vlerësuar
në:
Kmes (n) ≈ 2n ln n ≈ 1.39n log2 n.
8 Teknika ndaj-dhe-sundo | 174

Kështu në rastin mesatar, Quicksort bën vetëm 39% me shumë krahasime sesa në rastin më të
mirë. Ndërkaq, cikli i tij i brendshëm është aq i efektshëm saqë ekzekutohet më shpejt se
algoritmet Mergesort dhe Heapsort (algoritëm që bazohet në strukturën e tipit pemë), në të
dhëna të rastit të përmasave të mëdha. Kjo justifikon edhe emrin që i ka vënë shpikësi i tij.

Përshkak të rëndësisë së tij, janë bërë përpjekje të vazhdueshme gjatë viteve për të
përmirësuar algorimin bazë. Ndër gjetjet më të rëndësishme për përmirësim, përmendim:

• Metoda për një përcaktim më të mirë të elementit bosht si për shembull Quicksort i
randomizuar që përdor një element të rastit si bosht ose metoda e Medianës së tre
elementeve, elementit të parë, të fundit dhe të mesit;
• Krijimi i Quicksort hibrid, ku në fillim punon Quicksort dhe kur përmasat e ndarjeve
bëhen të vogla i lihet rradha Insertion sort për sjellje të mira me tabelat pothuajse të
rendituara.
• Modifikimi i algoritmit të Ndarjes, nëpërmjet ndarjes në tri pjesë të nëntabelave;

Proçesi i Ndarjes nuk është i qëndrueshëm, mbasi në vijim të një përkëmbimi, mund të
ndodhë që një element mund të zhvendoset tej një numri të pacaktuar elementesh që kanë të
njëjtën vlerë dhe që nuk janë marrë akoma në shqyrtim nga ndarja. Nuk ka ende ndonjë
metodë të thjeshtë për të transformuar Quicksort në një algoritëm renditjeje të qëndrueshëm.

Meqënëse të gjitha nëntabelat renditen në vend nuk është e domosdoshme asnjë punë shtesë
për të kryer kombinimin e tyre.

Quicksort është një nga algoritmet e renditjes më të përdorur. Ky algoritëm është mjaft
popullor sepse zbatohet lehtë dhe është një algoritëm i mirë për qëllime të përgjithshme
(general-purpose). Ai ka një sjellje të mirë në një varg situatash dhe në shumë raste kërkon
më pak burime sesa algoritmet e tjerë të renditjes.

Algoritmi Quicksort zë një vend të shquar në infrastrukturën llogaritëse botërore dhe është
nderuar duke u klasifikuar ndër 10 top algoritmet më të mirë të shekullit të 20të në shkencë
dhe në inxhinieri.

Algoritmi Quicksort i është nënështruar një analizë matematike shumë të thellë dhe mjaft të
saktë saqë shërbimet e tij janë kuptuar thellësisht dhe sjellja e tij është përshkruar në mënyrë
shumë të sigurtë. Rezultatet e përftuara në fazën e analizës janë verifikuar në mënyrë
eksperimentale në mënyrë të gjerë dhe algoritmi bazë është përmirësuar deri në atë pikë sa të
bëhet metoda ideale për një numër të madh të zbatimesh praktike. Kjo është arsyeja se pse
përshkrimi i mënyrës se si të zbatohet në mënyrë të efektshme algoritmi bëhet më i
hollësishëm për Quicksort sesa për algoritmet e tjerë të renditjes.

Një version i Quicksort i zbatuar me kujdes, probabilisht do të jetë shumë më i shpejtë se çdo
metodë tjetër renditjeje në pjesën më të madhe të kompjuterave. Ndërkaq, Quicksort përdoret
gjerësisht si procedurë renditjeje në libraritë softuerike dhe në zbatimet në të cilat renditja
është një çështje kritike. Për shembull, në librarinë standard të C++ paraqitet funksioni qsort,
i cili tipikisht është zbatuar duke përdorur një algoritëm të tipit Quicksort.
8 Teknika ndaj-dhe-sundo | 175

Koha e llogaritjeve të Quicksort varet nga natyra e të dhënave fillestare dhe mund të
ndryshojë, që nga pak më shumë se lineare deri në kuadratike, në varësi të numrit të
elementeve që do të renditen.
Duke bërë një përmbledhje mund të themi që:

Të mirat e Quicksort janë:


 vepron në mënyrë të drejtpërdrejtë në tabelën që do të renditet (duke përdorur vetëm një
stivë të vogël),
 kërkon mesatarisht vetëm n lgn veprime për të renditur n elemente,
 ka një cikël të brendshëm shumë të shkurtër.

Të metat e Quicksort janë:


 nuk është një algoritëm renditjeje i qëndrueshëm,
 në rastin më të keq ka një sjellje kuadratike,
 është shumë delikat në zbatim (një gabim i vogël në zbatimin e tij, që mund të kalojë pa u
vënë re, mund të shkaktojë në disa situata një keqësim shumë të ndjeshëm të shërbimit që
ai ofron).

8.4 Shumëzimi i numrave të plotë të mëdhenj dhe shumëzimi i matricave

Në këtë seksion do të trajtojmë dy algoritme surprizues për detyra dukshëm të drejtpërdrejta:


shumëzimi i dy numrave të plotë të mëdhenj dhe shumëzimi i dy matricave kuadratike. Të dy
algoritmet zvogëlojnë numrin e shumëzimeve dhe rritin pak numrin e mbledhjeve. Tek të dy
algoritmet shfrytëzohet idea ndaj-dhe-sundo.

Shumëzimi i numrave të plotë të mëdhenj


Shumë zbatime, sidomos ato të kriptologjisë moderne, kërkojnë manipulimin e numrave të
plotë që kalojnë 100 shifrat. Meqënëse numra të tillë janë shumë të mëdhenj që të futen në
një fjalë (word) të vetme të kompjuterave të sotëm, ata kërkojnë një trajtim të veçantë. Në
këtë seksion do të skicojmë një algoritëm interesant për shumëzimin e numrave të tillë. Është
e qartë që, nëse përdorimin algoritmin klasik me laps-e-letër për shumëzimin e dy numrave
të plotë n-shifrorë, secila nga n shifrat e numrit të parë do të shumëzohet me secilën nga
shifrat e numrit të dytë, duke kryer gjithsej n2 shumëzime shifrash. (Nëse një nga numrat, ka
më pak shifra sesa tjetri, ne mund të plotësojmë numrin më të vogël me zero në fillim.)
Megjithëse mund të duket se është e pamundur që të hartohet një algoritëm që të shpenzojë
më pak se n2 shumëzime shifrash, në fakt nuk është kështu. Mrekullia e ndaj-dhe-sundo vjen
në ndihmë në këtë rast.

Për të demostruar idenë bazë të algoritmit, le të fillojmë me një rast me numra të plotë dy-
shifrorë, për shembull, 23 me 14. Numrat mund të paraqiten në trajtën polinomiale si më
poshtë:

23 = 2⋅101 + 3⋅100 dhe 14 = 1⋅101 + 4⋅100.


Le t’i shumëzojmë ata:

23 * 14 = (2⋅101 + 3⋅100 ) ⋅ (1⋅101 + 4⋅100 )


= (2 * 1) 102 + (2 * 4 + 3 * 1) 101 + (3 * 4)100
8 Teknika ndaj-dhe-sundo | 176

Formula e fundit prodhon rezultatin korrekt 322, por ajo përdor të katër shumëzimet një
shifrore si algoritmi me laps-e-letër. Për fat të mirë ne mund të llogarisim termin e mesit
vetëm me anë të një shumëzimi një shifror duke përfituar nga fakti që produkti 2*1 dhe 3*4
mund të llogariten drejtpërsëdrejti:

2 * 4 + 3 * 1 = (2 + 3) * (1 + 4) – 2 * 1 – 3*4

Natyrisht nuk ka asgjë të veçantë mbi numrat që sapo shumëzuam. Për çdo çift numrash të
plotë dy shifrorë a = a1a0 dhe b = b1b0, produkti i tyre c mund të llogaritet me anë të
formulës:

c = a * b = c2102 + c1101 + c0

ku
c2 = a1 * b1 // është produkti i shifrave të para të tyre,
c0 = a0 * b0 // është produkti i shifrave të dyta të tyre,

// është produkti i shumës së shifrave të a-së dhe shumës së shifrave të b-së minus
// shumën e c2 dhe c0.
c1 = (a1 + a0 ) * (b1 + b0 ) – (c2 + c0)

Le të zbatojmë tani këtë marifet për të shumëzuar dy numra të plotë n-shifrorë a dhe b ku n
është një numër i plotë pozitiv çift. Meqenëse duam të përfitojmë nga teknika ndaj-dhe-
sundo, le t’i ndajmë numrat në mes. Le të shënojmë gjysmën e parë të numrit a me a1 dhe
gjysmën e dytë me a0; për numrin b shënimet janë b1 dhe b0 respektivisht. Me këto shënime,
a = a1a0 shkruhet a = a110n/2 + a0 dhe b = b1b0 shkruhet b = b110n/2 + b0. Më tej, duke
përfituar nga marifeti që përdorëm me numrat dy shifrorë do të kemi:

c = a * b = (a110n/2 + a0) * (b110n/2 + b0)


= (a1 + b1) 10n + (a1 * b0 + a0 * b1) 10n/2 + (a0 * b0) = c210n + c110n/2 + c0,

ku
c2 = a1 * b1 // është produkti i gjysmave të para
c0 = a0 * b0 // është produkti i gjysmave të dyta
// është produkti i shumës së dy gjysmave të a-së dhe shumës së dy gjysmave të b-së
// minus shumën e c2 dhe c0.
c1 = (a1+a0) * (b1+b0) – (c2+c0)

Nëse n/2 është çift ne mund të zbatojmë të njëjtën metodë për të llogaritur produktet c2, c0
dhe c1. Kështu në qoftë se n është fuqi e 2, ne do të kemi një algoritëm rekursiv për të
llogaritur produktin e dy numrave të plotë n-shifrorë. Në formën e tij të pastër, rekursioni do
të ndalojë kur n bëhet 1. Gjithashtu, ai mund të ndalohet kur ne jemi të mendimit që n është
mjaft e vogël sa që mund t’i shumëzojmë numrat drejtpërsëdrejti.

Sa shumëzime shifrash kryen algoritmi? Meqenëse shumëzimi i numrave n-shifrorë kërkon


tre shumëzime të numrave n/2-shifrorë, rekurrenca për numrin e shumëzimeve Sh(n) do të
jetë

Sh(n) = 3Sh(n/2) për n > 1, Sh(1) = 1 për n = 1.


8 Teknika ndaj-dhe-sundo | 177

Duke zgjidhur ekuacionin e mësipërm të rekurrencës me anën e zëvendësimit për së prapi për
n = 2k do të kemi:

Sh(2k) = 3Sh(2k - 1) = 3[3Sh(2k - 2)] = 32Sh(2k - 2)


= • • • = 3iSh(2k - i) = • • • = 3kSh(2k - k) = 3k.
Meqenëse k = log2n,

Sh(n) = 3log2 n = nlog2 3 ≈ n1.585

(në hapin e fundit shfrytëzuam cilësinë e mëposhtme të logaritmeve: 𝑎log𝑏 𝑐 = 𝑐 log𝑏 𝑎 )

Po në lidhje me mbledhjet dhe zbritjet? Mos valle kemi ulur numrin e shumëzimeve duke
kërkuar më shumë mbledhje dhe zbritje. Provohet që numri i tyre ka të njëjtën rritje
asimptotike si edhe shumëzimet.

Vëmë në dukje se për shumëzimin e numrave të plotë me një madhësi të moderuar, ky


algoritëm mund të kërkojë më shumë kohë se algoritmi klasik. Brassard dhe Bratley, në
publikimin e tyre për eksperimentet e kryera me metodën ndaj-dhe-sundo pohojnë se metoda
e më sipërme e kalon metodën me laps-e-letër nëse numrat e plotë kanë mbi 600 shifra. Në
gjuhët e orientuara objekt si Java, C++ ose Smalltalk ekzistojnë tipe të dhënash dhe funksione
që trajtojnë veprimet me numra të plotë të mëdhenj.

Shumëzimi i matricave sipas Strassen


Tashmë që pamë se teknika ndaj-dhe-sundo mund të reduktojë numrin e shumëzimeve një-
shifrore kur shumëzojmë dy numra të plotë, nuk duhet të habitemi që një gjë e ngjashme
mund të ndodhë kur shumëzojmë matricat. Një algoritëm i tillë është publikuar nga V.
Strassen në vitin 1969. Thelbi i algoritmit duket në faktin që shumëzimi i dy matricave
kuadratike dhe të rendit të dytë kërkon 7 shumëzime kundrejt 8 të tillave që kërkonte
algoritmi i forcës brutale:

𝑐00 𝑐01 𝑎00 𝑎01 𝑏00 𝑏01 𝑚 + 𝑚4 − 𝑚5 + 𝑚7 𝑚3 + 𝑚5


�𝑐 𝑐11 � = � 𝑎10 𝑎11 � ∗ �𝑏10 �=� 1 �
10 𝑏11 𝑚2 + 𝑚4 𝑚1 + 𝑚3 − 𝑚2 + 𝑚6

ku
m1 = (a00 + a11) ⋅ (b00 + b11)
m2 = (a10 + a11) ⋅ b00
m3 = a00 ⋅ (b01 – b11)
m4 = a11 ⋅ (b10 – b00) (8.4)
m5 = (a00 + a01) ⋅ b11
m6 = (a10 – a00) ⋅ (b00 + b01)
m7 = (a01 – a11) ⋅ (b10 + b11).

Kështu, për të shumëzuar dy matrica të rendit të dytë, algoritmi i Strassen-it kryen 7


shumëzime dhe 18 mbledhje/zbritje, ndërsa algoritmi i forcës brutale kërkon 8 shumëzime
dhe 4 mbledhje. Sigurisht që këto vleresime nuk na ngrohin të përdorim algoritmin e
Strassen-it për shumëzimin e matricave të rendit të dytë. Rëndësia e tyre është që të shohim
prej tyre superioritetin asimptotik kur rendi i matricës n, shkon në infinit.
8 Teknika ndaj-dhe-sundo | 178

Le të jenë a dhe b dy matrica kuadratike të rendit n dhe n është fuqi e dyshit. (Nëse n nuk
është e fuqi e dyshit, matricat mund të mbushen me rreshta dhe shtylla me zero.) Ne mund ta
ndajmë a dhe b dhe produktin e tyre c në 4 nënmatrica të rendit n/2 si më poshtë:

|𝑐00 | |𝑐01 | |𝑎 | |𝑎01 | |𝑏 | |𝑏01 |


� � = � 00 � ∗ � 00 �
|𝑐10 | |𝑐11 | |𝑎10 | |𝑎11 | |𝑏10 | |𝑏11 |

Nuk është e vështirë të verifikohet që duke i trajtuar këto nënmatrica si numra përftohen
produktet e rregullta. Për shembull, c00 mund të llogaritet ose si a00 ⋅ b00 + a01 ⋅ b10 ose si m1 +
m4 – m5 + m7 ku m1, m4, m5, dhe m7 që gjenden nga formulat Strassen, (8.4), në ku
zëvendësohen me anë të matricave korresponduese. Nëse të 7 produktet e matricave
kuadratike të rendit n/2 llogariten rekursivisht me anë të së njëjtës metodë, përftojmë
algoritmin e Strassen-it për shumëzimin e matricave.

Le të vlerësojmë efektshmërinë kohore të algoritmit. Në qoftë se Sh(n) është numri i


shumëzimeve të kryera nga algoritmi i Strassen-it në shumëzimin e dy matricave të rendit n
(ku n është një fuqi e 2-it), ne përftojmë formulën e mëposhtme rekurrente:

Sh(n) = 7Sh(n/2) për n > 1, Sh(1) =1.

Meqenëse n = 2k ,

Sh(2k) = 7Sh(2k - 1) = 7[7Sh(2k - 2)] = 72Sh(2k - 2) = • • • = 7iSh(2k - i) • • • = 7kSh(2k - k) = 7k.

Meqenëse k = log2n,

Sh(n) = 7log2 n = nlog2 7 ≈ n2.807

e cila është më e vogël se n3 që, kërkon algoritmi i forcës brutale.

Meqenëse zvogëlimi i numrit të shumëzimeve është kryer duke u rritur numri i mbledhjeve,
ne duhet të kontrollojmë numrin e mbledhjeve M(n) të kryera nga algoritmi i Strassen-it. Për
të shumëzuar dy matrica të rendit n > 1, algoritmi ka nevojë të kryejë shumëzimin e 7
matricave të rendit n/2 dhe 18 mbledhje të matricave të rendit n/2; kur n = 1, nuk kryhen
mbledhje mbasi dy numrat thjesht shumëzohen. Këto vrojtime na shpien në relacion
rekurrencial të mëposhtëm:

M(n) = 7M(n/2) + 18(n/2)2 për n > 1, M(1) = 0.

Megjithëse është e mundur që të përftohet një zgjidhje në trajtë formule e ekuacionit


rekurrencial, do të mjaftohemi vetëm me rendin e rritjes. Duke u mbështetur në teoremën
themelore do të kemi që 𝑀(𝑛) ∈ Θ�𝑛log2 7 �. Me fjalë të tjera numri i mbledhjeve ka të njëjtin
rend rritjeje si numri i shumëzimeve. Kjo e fut algoritmin Strassen-it në klasën e
efektshmërisë Θ(𝑛log2 7 ), i cila është një klasë më e mirë efektshmërie sesa Θ(𝑛3 ) e
algoritmit të forcës brutale.

Që nga koha kur u zbulua algoritmi i Strassen-it, janë shpikur disa algoritme të tjerë për
shumëzimin e dy matricave me numra reale të rendit n në kohën O(nα) me kontante α gjithnjë
e më të vogla. Algoritmi më i shpejtë është ai i Coopersmith-it dhe Winograd-it me
efektshmëri O(n2.376). Ulja e vlerës së eksponentit ka ardhur si rezultat i ndërlikueshmërisë së
8 Teknika ndaj-dhe-sundo | 179

algoritmit. Për shkak të konstanteve të mëdha shumëzuese asnjë prej tyre nuk ka vlerë
praktike. Megjithatë, ata janë interesante nga pikpamja teorike. Megjithëse këta algoritme
kapin kohë gjithnjë e më të vogla drejt kufirit teorik më të ulët, që për shumëzimin e
matricave është 2, për momentin diferenca ndërmjet këtij dhe rezultatit të arritur ka mbetur e
pazgjidhshme.

8.5 Problemi i çiftit të pikave më të afërta dhe i mbështjellëses konvekse

Në leksionin për teknikën e forcës brutale, trajtuam zgjidhjen e dy problemeve klasikë të


gjeometrisë llogaritëse: problemin e çiftit të pikave më të afërta dhe problemin e
mbështjellëses konvekse. Thamë që versionet dy-dimensionale të këtyre problemeve mund të
zgjidhen me algoritme në kohë Θ(n2) dhe Θ(n3) respektivisht. Në këtë seksion do të
diskutojmë për algoritme më të ndërlikuar dhe asimptotikisht më të efektshëm për këto
probleme.

Problemi i çiftit të pikave më të afërta


Le të jetë P një bashkësi prej n > 1 pikash në një plan kartezian. Për thjeshtësi do të
supozojmë që pikat janë të ndyshme. Gjithashtu do të supozojmë që pikat janë të renditura në
rendin jozbritës të koordinatave x të tyre. ((Nëse nuk janë të tilla mjafton t’i renditim me një
algoritëm me kompleksitet O(n log n), si për shembull Mergesort). Gjithashtu do të ishte e
përshtatshme të kemi pikat e renditura në rendin jozbritës të koordinatave të tyre y në një listë
tjetër, që po e shënojmë Q.

Nëse 2 ≤ n ≤ 3, problemi mund të zgjidhet me anë të algoritmit të qartë të forcës brutale.


Nëse n > 3, ne mund t’i ndajmë pikat në dy nënbashkësi Pm dhe Pd me ⌈n/2⌉ dhe ⌊n/2⌋pika
respektivisht, me anë të vizatimit të një vijë vertikale tek mediana m e koordinatave x në
mënyrë të tillë që ⌈n/2⌉ pika të shtrihen në të majtë ose mbi vetë vijën dhe ⌊n/2⌋ pika të
shtrihen në të djathtë të saj ose mbi të. Atëherë mund të zgjidhim problemin e çiftit të pikave
më të afërta në mënyrë rekursive për nënbashkësitë Pm dhe Pd. Le të jenë dm dhe dd distancat
më të vogla ndërmjet çiftit të pikave në Pm dhe Pd, respektivisht dhe le të jetë d = min{dm,
dd}.

Vëmë në dukje që d nuk është medoemos distanca më e vogël ndërmjet të gjitha çifteve të
pikave mbasi pikat e një çifti më të afërt mund të shtrihet në anë të kundërta të vijës ndarëse.
Prandaj si një hap që kombinon zgjidhjet e problemeve më të vegjël ne duhet të shqyrtojmë
edhe pikat e tilla. Është e qartë, se ne mund ta përqëndrojmë vëmendjen ndër pikat brenda një
brezi vertikal me gjerësi 2d rreth vijës ndarëse, përderisa distanca ndërmjet çdo çifti tjetër
është të paktën d (Figura 8.9a).
8 Teknika ndaj-dhe-sundo | 180

a) b)
x=m
x=m

dm
d d
dd
d min

d d

Figura 8.9 a. Idea ndaj-dhe-sundo për problemin e pikave më të afërta


b. Drejtkëndëshi mund të përmbajë pika më afër se dmin tek pika P.

Le të jetë S lista e pikave brenda brezit me gjerësi 2d përreth vijës ndarëse, e përftuar nga Q
dhe si rrjedhojë të renditura në rendin jozbritës të ordinatës së tyre y. Ne do të kontrollojmë
këtë listë, duke përditësuar informacionin mbi dmin, distancën më të vogël të gjetur deri tani,
nëse takojmë një çift pikash më të afërta. Fillimisht dmin = d, dhe si rrjedhim dmin ≤ d. Le të
jetë p(x, y) një pikë e kësaj liste. Për një pikë p(x, y) për të patur shprese që të jetë me afër p
sesa dmin, pika duhet të pasojë p në S dhe diferenca ndërmjet ordinatës së saj y duhet të jetë
më e vogël se dmin. Gjeometrikisht, kjo nënkupton që pika p duhet t’i takojë drejtkëndëshit të
paraqitur në figurën 8.9b. Veçoria kryesore që do të shfrytëzojë algoritmi është vrojtimi që
drejtkëndëshi mund të përmbajë një numër të vogël pikash të tilla, mbasi pikat në çdo gjysmë
(të majtë dhe të djathtë) të drejtkëndëshit duhet të jenë larg të paktën në distancën d. Provohet
që numri i përgjithshëm i pikave të tilla në drejtkëndësh, përfshirë p, nuk është më shumë se
6. Kështu algoritmi do të konsiderojë jo më shumë se 5 pika që pasojnë pikën p në listën S,
para se të kalojë në pikën pasuese.

Algoritmi shpenzon një kohë lineare për ndarjen e problemit në dy nënprobleme me gjysmën
e përmasës dhe kombinimin e zgjidhjeve. Prandaj, duke supozuar si zakonisht se n është një
fuqi e 2, do të kemi rekurrencën pasuese për kohën e ekzekutimit të algoritmit:

T(n) = 2 T(n/2) + f(n)

ku f(n) ∈ Θ(n). Duke zbatuar teoremën themelore (me a = 2, b = 2 dhe d = 1) do të përftojmë


që T(n) ∈ Θ(n log n). Domosdoshmëria e pararenditjes të pikave fillestare nuk e ndryshon
klasën e efektshmërisë së përgjithshme të problemit nëse renditja kryhet me algoritëm me
efektshmëri O(n log n), për shembull Mergesort.

Problemi i mbështjellëses konvekse

Le të rishohim problemin e mbështjellëses konvekse të shtruar në një nga leksionet e


mëparshme: të gjendet shumëkëndëshi më i vogël që përfshin n pika të dhëna në një plan. Do
të trajtojmë një algoritëm të quajtur Quickhull për shkak të ngjashmërisë së tij me Quicksort.

Le të jetë P1=(x1, y1), …, Pn(xn, yn) një bashkësi S prej n > 1 pikash në plan. Do të supozojmë
që pikat janë të renditura në rendin rritës të absisës x. Nuk është e vështirë të provohet
gjeometrikisht fakti i qartë se pika më e majtë P1 dhe pika më e djathtë Pn janë dy pikat
skajore të dallueshme të mbështjellëses konvekse (Figura 8.10). Le të jetë ��������⃗
𝑃1 𝑃𝑛 vija e drejtë
që bashkon pikat P1 dhe P2 në drejtimin nga P1 në P2. Kjo vijë ndan pikat e bashkësisë S në
dy bashkësi: S1 është bashkësia e pikave në të majtë ose mbi këtë vijë dhe S2 është bashkësia
8 Teknika ndaj-dhe-sundo | 181

e pikave në të djathtë ose mbi këtë vijë. (Themi që pika P3 është në të majtë të vijës 𝑃��������⃗
1 𝑃2 e
drejtuar nga pika P1 në pikën P2 në qoftë se P1P2P3 formon një cikël antiorar. Më tej do të
përmendim një rregull analitik për të kontrolluar këtë kusht bazuar në kontrollin e shenjës të
një përcaktori të formuar nga koordinatat e tri pikave). Pikat e S mbi vijën ��������⃗
𝑃1 𝑃𝑛 të ndryshme
nga P1 dhe P2 nuk mund të jenë pika skajore të mbështjellëses konvekse dhe prandaj
përjashtohen nga konsiderimi i mëtejshëm.

P1 Pn

Figura 8.10 Mbështjellëset e sipërme dhe të poshtme të një bashkësie pikash

Kufiri i mbështjellëses konvekse të S përbëhet nga nga dy vargje shumëkëndore: një kufi i
“sipërm” dhe një kufi i “poshtëm”. Kufiri i “sipërm” i quajtur mbështjellësja e sipërme (upper
hull) është vargu i segmenteve vijëdrejtë me kulme në P1, dhe disa nga pikat e S1 (në qoftë se
S1 nuk është bosh), dhe Pn. Kufiri i “poshtëm” , i quajtur mbështjellësja e poshtme (lower
hull), është vargu i segmenteve vijëdrejtë me kulme në P1 dhe disa nga pikat e S2 (në qoftë se
S2 nuk është bosh) dhe Pn.

Fakti që mbështjellësja konvekse e gjithë bashkësisë S përbëhet nga mbështjellësja e sipërme


dhe mbështjellësja e poshtme, të cilët mund të ndërtohen jo vetëm në mënyrë të pavarur por
edhe në mënyrë të ngjashme është një premisë shumë e vlefshme që është shfrytëzuar nga
shumë algoritme për këtë problem.

Për të qenë konkret le të diskutojmë se si duhet të procedojmë për të ndërtuar mbështjellësen


e sipërme; mbështjellësja e poshtme mund të ndërtohet në të njëjtën mënyrë. Në qoftë se S1
është bosh, mbështjellësja e sipërme është thjesht segmenti me skaje fundore pikat P1 dhe Pn.
Në qoftë se S1 nuk është bosh, algoritmi identifikon kulmin Pmax në S1, i cili është më i
larguari nga vija ��������⃗
𝑃1 𝑃𝑛 (figura 8.11). Në qoftë se ajo është një kryqëzim, pika që, maksimizon
këndin ∠PmaxP1Pn mund të zgjidhet. (Vëmë në dukje që pika Pmax maksimizon sipërfaqen e
trekëndëshit me dy kulmet P1 dhe Pn dhe me kulm të tretë një pikë tjetër e S1). Pastaj
��������������⃗
algoritmi identifikon të gjitha pikat e bashkësisë S1 që janë në të majtë në vijës 𝑃 1 𝑃𝑚𝑚𝑚 ; këto
janë pikat që së bashku me P1 dhe Pmax do të formojnë bashkësinë S1,1. Pikat e S1 në të majtë
��������������⃗
të vijës 𝑃 𝑚𝑚𝑚 𝑃1 do të formojnë së bashku me Pm dhe Pn, bashkësinë S1,2. Nuk është e vështirë
të provohet që:

• Pmax është një kulm i mbështjellëses së sipërme


• Pikat brenda ∆P1PmaxPn nuk mund të jenë kulme të mbështjellëses së sipërme (dhe
prandaj do të eliminohen nga shqyrtimi i mëtejshëm), dhe
• Nuk ka pika në të majtë të dy vijave ��������������⃗
𝑃1 𝑃𝑚𝑚𝑚 dhe ��������������⃗
𝑃𝑚𝑚𝑚 𝑃𝑛 .

P max

P1 Pn
8 Teknika ndaj-dhe-sundo | 182

Figura 8.11 Idea e Quickhull për mbështjellësen konvekse

Prandaj, algoritmi mund të vazhdojë me ndërtimin e mbështjellëses së sipërme të


𝑃1 ⋃ 𝑆1,1 ⋃ 𝑃𝑚𝑚𝑚 dhe 𝑃𝑚𝑚𝑚 ⋃ 𝑆1,2 ⋃ 𝑃𝑛 rekursivisht dhe pastaj thjesht t’i bashkojë ato për të
formuar mbështjellësen e sipërme të të gjithë bashkësisë 𝑃1 ⋃ 𝑆1 ⋃ 𝑃𝑛 .

Le të ilustrojmë tani se si mund të zbatohen veprimet gjeometrike. Për fat, mund të përfitojmë
nga një fakt shumë i dobishëm i gjeometrisë analitike: në qoftë së p1 = (x1, y1), p2 = (x2, y2)
dhe p3 = (x3, y3) janë tri pika të çfardoshme në një plan kartezian atëherë sipërfaqja e
trekëndëshit ∆p1p2p3 është e barabartë me gjysmën e madhësisë së përcaktorit,

𝑥1 𝑦1 1
�𝑥2 𝑦2 1� = 𝑥1 𝑦2 + 𝑥3 𝑦1 + 𝑥2 𝑦3 − 𝑥3 𝑦2 − 𝑥2 𝑦1 − 𝑥1 𝑦3 ,
𝑥3 𝑦3 1

Shenja e këtij përcaktori është pozitive atëherë dhe vetëm atëherë kur pika p3 = (x3, y3) është
në të majtë të vijës 𝑃��������⃗
1 𝑃2 . Duke përdorur këtë formulë, mund të kontrollojmë në kohë
konstante nëse një pikë ndodhet në të majtë të vijës të përcaktuar nga dy pikat e tjera si dhe të
gjejmë largesën nga pika tek vija.

Quickhull ka të njëjtën efektshmëri Θ(n2) në rastin më të keq ashtu si Quicksort . Për rastin
mesatar duhet të presim një performancë më të mirë. Së pari, algoritmi duhet të përfitojë nga
kursimet e ngjashme me Quicksort për ndarjet e balancuara të problemit në nënprobleme. Së
dyti, një pjesë e ndjeshme e pikave, dhe pikërisht ato që ndodhen brenda trekëndëshit
∆P1PmaxPn (shih figurën 8.11), eleminohen nga trajtimi i mëtejshëm. Nën supozimin normal
që pikat e dhëna janë zgjedhur në mënyrë të rastit nga një shpërndarje uniforme në një zonë
konvekse (për shembull, një rreth apo katërkëndësh), rasti mesatar i efektshmërisë së
Quickhull mund të jetë edhe linear.

8.6 Përmbledhje

• Teknika ndaj-dhe-sundo është një teknikë e përgjithshme për hartimin e algoritmeve, që


zgjidh një rast të problemit duke e ndarë atë në disa raste më të vegjël, (ideale do të ishte
nëse përmasat do të ishin të njëjta), ku secili prej tyre zgjidhet në mënyrë rekursive, dhe
pastaj kombinohet zgjidhja e tyre për të përftuar zgjidhjen e problemit fillestar. Shumë
prej algoritmeve të efektshëm bazohen në këtë teknike, megjithëse ajo mund të jetë e
pazbatueshme dhe më e dobët për zgjidhjen e disa problemeve më të thjeshtë.
• Koha e ekzekutimit T(n) e shumë prej algoritmeve ndaj-dhe-sundo shprehet me anë të
rekurrencës T(n) = aT(n / b) + f(n). Teorema themelore jep rrendin e rritjes të zgjidhjeve
të tyre.
• Metoda e pemës rekursionit është një metodë që jep zgjidhjen e ekuacionit të rekurrencës
në formë grafike
• Mergesort është një algoritëm renditjeje i tipit ndaj-dhe-sundo. Ai ka si parim ndarjen e
një tabele me të dhëna fillestare në dy gjysma, të cilat i rendit rekursivisht dhe pastaj i
shkrin të dy gjysmat e renditura për të përftuar tabelën fillestare të renditur. Koha e
efektshmërisë së algoritmit është Θ(n log n) në të gjitha rastet dhe numri i krahasimeve të
elementit është shumë afër minimumit teorik. E meta e tij kryesore është nevoja për
kujtesë shtesë e rendit n
8 Teknika ndaj-dhe-sundo | 183

• Quicksort është një algoritëm renditjeje i tipit ndaj-dhe-sundo. Ai ka si parim grupimin e


të dhënave fillestare në përputhje me vlerën e tyre në lidhje me ndonjë element të
parazgjedhur. Quicksort dallohet për efektshmërinë e tij të lartë ndër algoritmet e klasës
n log n për renditjen e tabelave me të dhëna të shpërndara në mënyrë të rastit por
gjithashtu për efektshmërinë e tij kuadratike për rastin më të keq.
• Ekziston një algoritëm për shumëzimin e dy numrave të plotë n-shifrorë me rreth n1.585
shumëzime.
• Algoritmi i Strassen-it shpenzon vetëm 7 shumëzime për të shumëzuar 2 matrica
kuadratike por kërkon më shumë mbledhje sesa algoritmi i bazuar në përkufizimin e
shumëzimit. Duke përdorur teknikën ndaj-dhe-sundo, ky algoritëm mund të shumëzojë dy
matrica të rendit n me rreth n2.807 shumëzime.
• Teknika ndaj-dhe-sundo mund të përdoret me sukses për dy probleme të rëndësishme të
gjeometrisë llogaritëse: problemin e çiftit të pikave më të afërta dhe problemin e
mbështjellëses konvekse.

8.7 Ushtrime për Kapitullin 8

1. Jepet një tabelë me n numra të plotë.


a. Hartoni një algoritëm të bazuar në teknikën ndaj-dhe-sundo për gjetjen e pozicionit
të elementit më të madh.
b. Cili do të jetë pozicioni i gjetur për tabela që përmbajnë disa elemente me
vlerën më të madhe?
c. Ndërtoni dhe zgjidhni një ekuacion rekurrencial për numrin e krahasimeve të
elementeve të kryera nga algoritmi?
d. Krahasoni këtë algoritëm me algoritmin e forcës brutale për të njëjtin problem.

2. Jepet një tabelë me n numra të plotë.


a. Hartoni një algoritëm të bazuar në teknikën ndaj-dhe-sundo për gjetjen e
elementit më të madh në një tabelë me n numra.
b. Ndërtoni dhe zgjidhni një ekuacion rekurrencial për numrin e krahasimeve të
elementeve të kryera nga algoritmi?

3. Jepet një tabelë me n numra të plotë.


a. Shkruani një algoritëm të bazuar në teknikën ndaj-dhe-sundo për gjetjen njëkohësisht
të elementit më të vogël dhe më të madh.
b. Ndërtoni dhe zgjidhni (për n = 2k) një ekuacion rekurrencial për numrin e
krahasimeve të elementit që kryen algoritmi.
c. Krahasoni këtë algoritëm me algoritmin e forcës brutale për të njëjtin problem?

4. Jepet një tabelë me n numra të plotë si dhe një numër i plotë k.


a. Të hartohet një algoritëm që llogarit numrin e herëve që shfaqet k në a.
b. Ndërtoni dhe zgjidhni një ekuacion rekurrencial për numrin e veprimeve bazë të
kryera nga algoritmi?

5. Është dhënë një tabelë e përbërë nga n numra të plotë të ndryshëm ndërmjet tyre. Hartoni
një algoritëm rekursiv të mbështetur në teknikën ndaj-dhe-sundo që llogarit numrin e
minimumeve lokalë të elementeve. Përjashtohen nga numërimi skajet a[0] dhe a[n – 1]).
Përcaktohet si minimum lokal një element që është më i vogël se të dy fqinjët e tij. Për
8 Teknika ndaj-dhe-sundo | 184

shembull, tabela a = {3, 4, 2, 8, 7, 10, 12, 15, 14, 20} ka tre minimume lokalë, që janë
vlerat 2, 7 dhe 14. Të diskutohet kompleksiteti kohor dhe hapsinor.

6. Le të supozojmë se jepet një tabelë me n numra të plotë, n > 1. Të hartohet një algoritëm
bazuar në teknikën ndaj-dhe-sundo që llogarit shprehjen: v[1]*1 + v[2]*2 + … + v[n]*n.
Më pas shkruani ekuacionin e rekurrencës që shpreh kohën e llogaritjeve dhe zgjidheni
atë me një nga metodat e mësuara.

7. Llogaritja e fuqisë.
a. Të hartohet një algoritëm, mbështetur në teknikën ndaj-dhe-sundo, për llogaritjen e
fuqisë an, kur a > 0 dhe n është një numër i plotë pozitiv.
b. Ndërtoni dhe zgjidhni një ekuacion rekurrencial për numrin e shumëzimeve të kryera
nga algoritmi.
c. Krahasoni këtë algoritëm me algoritmin e forcës brutale për të njëjtin problem?

8. Në analizën e efektshmërisë së një algoritmi është thënë që bazat e logaritmit nuk kanë
influence të ndjeshme. A është kjo e vërtetë për dy pohimet e Teoremës themelore që
përfshijnë logaritme.

9. Zbatoni teoremën themelore për rekurrencat e mëposhtme .


a. T(n) = 4T(n/2) + n, për n > 1 dhe T(1) = 1
b. T(n) = 4T(n/2) + n2, për n > 1 dhe T(1) = 1
c. T(n) = 4T(n/2) + n3, për n > 1 dhe T(1) = 1

10. Zbatoni algoritmin Mergesort për të renditur vargun e karaktereve M, E, R, G, E, S, O, R,


T (pa përfshirë presjet) në rendin alfabetik. Të tregohen të gjithë hapat e përdorur nga
algoritmi.

11. A është i qëndrueshëm algoritmi Mergesort?

12. Sa krahasime të elementeve janë të nevojshme për të shkrirë tabelat e mëposhtme


dypjesëshe të renditura.
a. a={1, 3, 5, 7, 9, 2, 4, 6, 8, 10}
b. a={1, 2, 3, 4, 6, 7, 8, 9}
c. a={1, 5, 6, 7, 8, 2, 3, 4, 9, 10}

13. Zbatoni algoritmin Quicksort për të renditur listën Q,U,I,C,K,S,O,R,T në rendin


alfabetik. Të tregohen të gjithë hapat e përdorur nga algoritmi.

14. A është algoritmi Quicksort i paraqitur në leksion një algoritëm i qëndrueshëm?

15. Të paraqiten hapat për renditjen në rendin rritës (jozbritës) të tabelave të mëposhtme me
anë të algoritmit Quicksort dhe të llogaritet për çdo rast numri i krahasimeve të
elementeve.
a. {1, 2, 3, 4, 5, 6, 7, 8, 9}
b. {9, 8, 7, 6, 5, 4, 3, 2, 1}
c. {5, 5, 5, 5, 5, 5, 5, 5, 5}
d. {3, 5, 4, 2, 1, 9, 8, 7, 6}
8 Teknika ndaj-dhe-sundo | 185

16. Cilat do të ishin modifikime kryesore në algoritmin HoarePartition në mënyrë që


algoritmi Quicksort të realizojë renditjen e elementeve të një tabele në rendin zbritës.

17. Duke qenë se algoritmi Quicksort ka kohë ekzekutimi Θ(n2) kur të gjithë elementet janë
të barabartë, si mund t’i shmangemi përdorimit të algoritmit në një rast të tillë?

18. Jepet një tabelë a me numra të plotë, pozitivë dhe negativë. Të hartohet një algoritëm i
efektshëm në kohë dhe hapsirë që vendos të gjithë elementet negativë para gjithë
elementeve pozitivë. Të llogaritet kompleksiteti kohor dhe hapsinor i algoritmit të
projektuar.

19. Të llogaritet 2101*1130 duke zbatuar algoritmin e shumëzimit të numrave të mëdhenj të


skicuar në tekst.

20. a. Provoni vërtetësinë e barazimit 𝑎log𝑏 𝑐 = 𝑐 log𝑏 𝑎 që është përdorur në tekst.


b. Pse shprehja 𝑛log2 3 është më e preferueshme sesa shprehja 3log2 𝑛 si formulë për
shprehjen e numrit të shumëzimeve Sh(n), në algoritmin ndaj-dhe-sundo të
shumëzimit të numrave të mëdhenj?

21. Shumëzimi i numrave të mëdhenj.


a. Përse në numërimin e shumëzimeve për vlerësimin e kohës së ekzekutimit të
kryera nga algoritmi nuk janë përfshirë shumëzimet me 10n?
b. Përveç supozimit që n është fuqi e 2-it, për hir të thjeshtësisë është bërë edhe një
supozim tjetër në hartimin e relacionit të rekurrencës për numrin e
shumëzimeve Sh(n) i cili nuk është gjithmonë i vërtetë (që megjithatë nuk
ndryshon përgjigjen përfundimtare). Cili është ky supozim?

22. Sa mbledhje një-shifrore kryhen nga algoritmi që përdorim ne për shumëzimin e dy


numrave n shifrorë me laps-dhe-letër? (Në llogaritje të mos merren parasysh
mbartjet e mundshme).

23. Verifikoni formulat themelore të algoritmit i Strassen-it për shumëzimin e një matricë të
rendit të dytë.

24. Zbatoni algoritmin e Strassen-it për të llogaritur produktin e matricave të mëposhtme dhe
të dilet nga rekursioni kur n = 2 domethënë, llogaritja e produktit të matricave të rendit
të dytë të bëhet nga algoritmi i forcës brutale.

1 0 2 1 0 1 0 1
4 1 1 0 2 1 0 4
*
0 1 3 0 2 0 1 1
5 0 2 1 1 3 5 0

25. Zgjidhni rekurrencën e ndërtuar për numrin e mbledhjeve të kërkuara nga algoritmi i
Strassen-it. (Të supozohet se n është fuqi e 2-it.)

26. Hartoni një algoritëm mbështetur në teknikën ndaj-dhe-sundo për version 1-


dimensional të problemit të çiftit të pikave më të afërta, domethënë, problemin e gjetjes e
dy pikave më të afërta në një bashkësi të dhënë të n numrave reale dhe përcaktoni klasën
e efektshmërisë. A është ai një algoritëm i mirë për këtë problem?
8 Teknika ndaj-dhe-sundo | 186

27. Shpjegoni se si mund të gjendet pika Pmax në mënyrë analitike në algoritmin Quickhull?

28. Cila është efektshmëria e rastit më të mirë për algoritmin Quickhull?

29. Jepni një rast të veçantë të të dhënave për të cilat algoritmi Quickhull ekzekutohet në
kohë kuadratike.
9 Teknika transformo-dhe-sundo | 187

9 Teknika transformo-dhe-sundo

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të njohë teknikën e transformo-dhe-sundo dhe versionet e saj për konceptimin e


algoritmeve;
• të përdorë pararenditjen për problemet elementeve unikë, llogaritjes së modës dhe të
bëjë krahasimin me metodën e forcës brutale;
• të përdorë shperthimin LU për zgjidhjen e sistemeve të ekuacioneve lineare, llogaritjen e
matricës inverse dhe percaktorit;
• të përdorë ndryshimin e paraqitjes për llogaritjen e fuqisë së një numri.

Në këtë leksion do të trajtohen një grup metodash të hartimit të algoritmeve që bazohen në


idenë e tranformimit. Këto metoda quhen me një emër të vetëm dhe pikërisht me emrin
transformo-dhe-sundo (transform-and-conquer). Ky emërtim mbështetet në faktin që këto
metoda realizohen si procedura me dy faza. Faza e parë është faza e transformimit ku
problemi transformohet në një gjendje të re për të lehtësuar zgjidhjen. Ndërsa në fazën e dytë
ndodh sundimi, zgjidhja e problemit.

Ekzistojnë tre variante të kësaj ideje që ndryshojnë nga mënyra se si transformohet një rast i
dhënë:
• Transformimi në një rast më të thjeshtë ose më të përshtatshëm të të njëjtit problem, dhe
ky variant quhet thjeshtim i rastit (instance simplification)
• Transformimi në një paraqitje tjetër të të njëjtit rast, dhe ky variant quhet ndryshimi i
paraqitjes (representation change)
• Transformimi në një rast të një problemi të ndryshëm për të cilin ekziston tashmë një
algoritëm vlefshëm, dhe ky variant quhet reduktimi i problemit (problem reduction)

9.1 Thjeshtimi i rastit

Një nga idetë më të frytshme të kësaj kategorie është renditja paraprake ose ndryshe
pararenditja (presorting) e të dhënave fillestare. Shumë problemeve mbi tabelat, është më e
lehtë t’u jepet përgjigje nëse tabelat janë të renditura. Natyrisht që përfitimet në kohë, nga
fakti i të qenurit të renditur, duhet që të jenë më të shumta sesa koha që shpenzohet për
renditje; përndryshe do të ishte më mirë të punohet me një tabelë të parenditur.

Një ide tjetër që përdoret gjerësisht në algoritmet më të rëndësishëm të matematikës së


zbatuar është eliminimi i Gausit. Ky algoritëm përdoret për zgjidhjen e një sistemi të
ekuacioneve lineare me anë të transformimit paraprak të tij në një sistem tjetër, me një cilësi
speciale, që e bën zgjidhjen e tij shumë më të lehtë.

9.1.1 Pararenditja

Është e qartë që efektshmëria kohore e zbatimit kur përfshihet edhe renditja, në një masë të
madhe varet nga tipi i algoritmit të renditjes të përdorur. Deri tani janë paraqitur tre algoritme
elementarë renditjeje: Selection sort, Bubble sort dhe Insertion sort që janë kuadratikë për
rastin më të keq dhe atë mesatar si dhe dy algoritme më të mirë, Mergesort që është
9 Teknika transformo-dhe-sundo | 188

gjithmonë O(n lg n) dhe QuickSort që është O(n lg n) në rastin mesatar dhe kuadratik për
rastin më të keq. A janë këta algoritmet më të shpejtë të renditjes? Është provuar se ndër
algoritmet që mbështeten në krahasimet nuk ka algoritme me efektshmëri më të mirë se n lg n
për rastin më të keq dhe atë mesatar. Më poshtë pasojnë tre probleme që ilustrojnë idenë e
pararenditjes.

Problemi i elementeve unikë


Kemi parë për këtë problem një algoritëm të realizuar me teknikën e forcës brutale, me anë të
të cilit kontrollonim nëse elementet e një tabele janë të gjithë të ndryshëm ndërmjet tyre.
Algoritmi i forcës brutale, nëpërmjet dy cikleve krahasonte çiftet e elementeve derisa të
takohen dy elemente të barabartë ose të mos ketë më çifte elementesh për t’u krahasuar. Në
rastin më të keq të organizimit të dhënave fillestare, algoritmi bënte pjesë në klasën Θ(n2) të
efektshmërisë kohore.

Një përafrim tjetër është që së pari të renditet tabela dhe pastaj nëpërmjet një cikli të vetëm,
të kontrollohen çiftet e elementeve fqinjë: nëse tabela ka elemente të barabartë atëherë një çift
i tillë elementesh do të pasojë njëri-tjetrin

// Algoritmi 9.1 A janë të ndryshëm elementet e një tabele


// Të dhëna: një tabelë a[0..n-1] me n-elemente
// Rezultate: Kthen true nëse të gjithë elementet janë të njëjtë, përndryshe false

janëUnikë2 (n, a) {
renditTabele (a[0..n–1]); // rendit tabelën a
for i ← 0 to n – 2 do
if a[i] = a[i + 1]
then return false;
return true
}

Koha e ekzekutimit të këtij algoritmi është e barabartë me shumën e kohës të shpenzuar për
renditje plus kohën e shpenzuar për krahasimin e elementeve të njëpasnjëshëm. Përderisa
renditja kryen të paktën n log n krahasime dhe e dyta nuk kërkon më shumë se (n–1)
krahasime atëherë është renditja ajo që ifluencon në kohën e përgjithshme të ekzekutimit.
Kështu që nëse përdorim për renditje një algoritëm kuadratik i gjithë algoritmi nuk mund të
jetë më i efektshëm sesa algoritmi i tipit të forcës brutale. Nëse përdorim një algoritëm të
mirë renditjeje si për shembull Mergesort që në rastin më të keq është Θ(n log n) atëherë
efektshmëria në rastin më të keq e gjithë procesit të kërkimit do të jetë gjithashtu Θ(n log n):

T(n) = Trenditje(n) + Tkerkim(n) ∈ Θ(n log n) + Θ(n) ∈ Θ(n log n)

Për shkak të efektshmërisë së mirë të algoritmit ai përdoret gjerësisht në shumë zbatime reale.
Për shembull identifikimi i semundjeve që shkaktojnë përhapjen e një numri elementesh të
ngjashëm si viruse, bakterie, etj në fusha të veçanta. Kjo rritje uniforme homogjene duhet të
identifikohet me anë të algoritmit dhe të investigohet me tej. Algoritmi gjithashtu mund të
përdoret në kërkime gjenomike për të identifikuar dallueshmërinë e nuklotideve për të
gjurmuar dhe kuptuar më mirë trashëgimin, ndryshimet gjenetike ose mutacionet.
9 Teknika transformo-dhe-sundo | 189

Llogaritja e modës
Një modë është një vlerë që shfaqet më shpesh në një tabelë të dhënë. Për shembull, në
tabelën {5, 1, 5, 7, 6, 5, 7} moda është vlera 5, e cila shfaqet tri herë. (Në qoftë se vlera të
ndryshme shfaqen në të njëjtin numër herësh atëherë secila prej tyre mund të konsiderohet si
modë). Një përafrim, sipas teknikës të forcës brutale, llogarit një modë duke bredhur në
tabelë dhe numëruar denduritë e të gjitha vlerave të ndryshme të saj dhe më pas gjen
dendurinë më të madhe. Për të zbatuar këtë ide rezervohen vlerat tashmë të takuara si dhe
denduritë e tyre në një tabelë të re. Në çdo iteracion, elementi i ite i tabelës burim krahasohet
me vlerat tashmë të takuara gjatë kalimit në tabelën ndihmëse. Nëse gjendet një vlerë e njëjtë
atëherë denduria e çastit e saj zmadhohet me një përndryshe elementi vijues regjistrohet në
tabelën ndihmëse dhe si denduri vendoset 1.

Nuk është e vështirë të kuptohet që rasti më i keq i organizimit të të dhënave fillestare është
ai i një tabele me të gjithë elementet e ndryshëm ndërmjet tyre. Për një tabelë të tillë,
elementi i itë i saj krahasohet me (i - 1) elementet e tabelës ndihmëse me elemente tashmë të
ndryshëm, që janë trajtuar deri tani me denduri 1. Si rezultat, numri i krahasimeve të
elementeve në rastin më të keq, i kryer gjatë krijimit të tabelës se dendurive nga algoritmi
është
𝑛 (𝑛 − 1)𝑛
𝐾(𝑛) = � (𝑖 − 1) = 0 + 1 + ⋯ + (𝑛 − 1) = ∈ Θ(𝑛2 )
𝑖=1 2

Krahasimet shtesë të rendit (n - 1) të nevojshme për të gjetur më të madhen e dendurive në


tabelën ndihmëse nuk e ndryshojnë klasën e efektshmërisë të algoritmit kuadratik të përftuar.

Një përafrim tjetër për zgjidhjen e problemit është renditja e tabelës me të dhëna. Pas
renditjes, vlerat e barabarta do të jenë fqinje të njëra tjetrës. Për të llogaritur modën mjafton të
gjendet vargu më i gjatë i vlerave të njëjta të elementeve fqinjë në tabelën e renditur.

// Algoritmi 9.2 Llogaritja e modës


// Të dhëna: një tabelë a[0..n–1] me n elemente të renditshëm
// Rezultate: moda, moda e të dhënave

llogaritMode(n, a) {
Rendit (a[0..n–1]); // Renditet tabela a
freqmoda ← 0;
i ←0;
while (i ≤ n–1) do {
përsëritja ← 1;
vleraRradhës ← a[i];
while ((i + përsëritja) ≤ (n–1)) and (a[i + përsëritja] = vleraRradhës) do
përsëritja ← përsëritja + 1;
if përsëritja > freqmoda
then { freqmoda ← përsëritja;
moda ← vleraRradhës;
}
i ← i + përsëritja;
}
return moda
}
9 Teknika transformo-dhe-sundo | 190

Analiza kohore është e ngjashme me problemin e elementeve unikë. Koha e ekzekutimit të


algoritmit do të zotërohet nga koha e shpenzuar për renditje ndërsa pjesa që mbetet e
algoritmit do të kërkojë një kohë lineare. Si rrjedhim me një renditje të klasës Θ(n log n), ky
algoritëm në rastin më të keq do të jetë në një klasë asimptotike më të mirë se në rastin më të
keq të një algoritmi të forcës brutale.

Problemi i kërkimit
Le të shqyrtojmë përsëri problemin e kërkimit për një vlerë të dhënë k në një tabelë të dhënë
të renditur me n elemente. Zgjidhja e tipit forcë brutale është një kërkim sekuencial që ka
nevojë për n krahasime në rastin më të keq. Nëse tabela fillimisht renditet atëherë mund të
zbatohet kërkimi binar që siç e dimë kërkon vetëm (⌊log n⌋ + 1) krahasime në rastin më të
keq. Duke supozuar se algoritmi i renditjes bën pjesë në klasën Θ(n log n), atëherë koha e
përgjithshme e ekzekutimit të një algoritmi të tillë kërkimi do të jetë:

T(n) = Trenditje(n) + Tkërkimi(n) ∈Θ(n log n) + Θ(log n) ∈ Θ(n log n)

Kjo kohë, është më e keqe se koha e kërkimit sekuencial. E njëjta gjë është e vërtetë edhe për
efektshmërinë për rastin mesatar. Natyrisht, vetëm nëse do të kërkohet në të njëjtën tabelë
shumë herë atëherë koha e shpenzuar për renditje mund të jetë e justifikueshme.

Para se të përfundojmë me pararenditjen, kujtojmë se shumë probleme gjeometrike, në mos


më të shumtët, që kanë të bëjnë me një bashkësi pikash përdorin pararenditjen në një mënyrë
ose tjetër. Pikat mund të renditen sipas ndonjerës prej koordinatave ose nga largesa e tyre
ndaj ndonjë vije të veçantë ose ndaj ndonjë këndi, etj. Për shembull pararenditja përdoret në
algoritmet e gjetjes së çiftit të pikave më të afërta në plan ose të mbështjellëses konvekse, me
teknikën ndaj-dhe-sundo.

9.1.2 Eliminimi i Gausit


Le të paraqesim një sistem me dy ekuacione dhe dy të panjohura:

𝑎 𝑥 + 𝑎12 𝑦 = 𝑏1
� 11
𝑎21 𝑥 + 𝑎22 𝑦 = 𝑏2

Kujtojmë që në rast se koeficientët e një ekuacioni nuk janë proporcionalë me koeficientët e


ekuacionit tjetër, sistemi i ekuacioneve ka një zgjidhje të vetme. Metoda standarde (metoda e
zëvendësimit) për gjetjen e zgjidhjes është që në njërin nga ekuacionet të shprehet një
variabël në funksionit të variablit tjetër dhe pastaj të zëvendësohet në ekuacionin tjetër. Në
këtë mënyrë përftohet një ekuacion me një variabël, zgjidhja e të cilit është e thjeshtë dhe
zgjidhja e tij shërben më pas për të gjetur vlerën e të panjohurës tjetër.

Në shumë zbatime është e domosdoshme të zgjidhet një sistem me një numër të më të madh
ekuacionesh, le të themi n ekuacione me n të panjohura:

𝑎11 𝑥1 + 𝑎12 𝑥2 + ⋯ + 𝑎1𝑛 𝑥𝑛 = 𝑏1


� ⋮
𝑎𝑛1 𝑥1 + 𝑎𝑛2 𝑥2 + ⋯ + 𝑎𝑛𝑛 𝑥𝑛 = 𝑏𝑛
9 Teknika transformo-dhe-sundo | 191

Teorikisht një sistem i tillë mund të zgjidhet duke përgjithësuar metodën e zëvendësimit për
zgjidhjen e një sistemi linear me dy ekuacione dhe dy të panjohura. Megjithatë algoritmi që
do të rezultonte do të ishte shumë i pavolitshëm.

Për fat të mirë ekziston një algoritëm më elegant për zgjidhjen e sistemeve të ekuacioneve
linearë që quhet metoda e eliminimit të Gausit (Gaussian elimination). Idea e eliminimit të
Gausit qëndron në transformimin e një sistemi linear me n ekuacione dhe n të panjohura në
një sistem të njëvlershëm (domethënë, në një sistem me të njëjtën zgjidhje si ai origjinali) me
një matricë trekëndëshe të sipërme (një matricë me të gjitha vlerat zero poshtë diagonales
kryesore)

𝑎11 𝑥1 + 𝑎12 𝑥2 + ⋯ + 𝑎1𝑛 𝑥𝑛 = 𝑏1 𝑎′11 𝑥1 + 𝑎′12 𝑥2 + ⋯ + 𝑎′1𝑛 𝑥𝑛 = 𝑏′1


𝑎 𝑥 + 𝑎22 𝑥2 + ⋯ + 𝑎2𝑛 𝑥𝑛 = 𝑏2 𝑎′22 𝑥2 + ⋯ + 𝑎′2𝑛 𝑥𝑛 = 𝑏′2
� 21 2 ⇒�
⋮ ⋱
𝑎𝑛1 𝑥𝑛 + 𝑎𝑛2 𝑥2 + ⋯ + 𝑎2𝑛 𝑥𝑛 = 𝑏𝑛 𝑎′𝑛𝑛 𝑥𝑛 = 𝑏′𝑛

Në trajtë matricore ekuacionet e mësipërme mund të shkruhen:

𝑎𝑎 = 𝑏 ⇒ 𝑎′ 𝑥 = 𝑏′

ku,

𝑎11 𝑎12 ⋯ 𝑎1𝑛 𝑏1 𝑎′11 𝑎′12 ⋯ 𝑎′1𝑛 𝑏′1


𝑎12 𝑎22 ⋯ 𝑎2𝑛 𝑏 0 𝑎′22 ⋯ 𝑎′2𝑛 𝑏′
𝑎=� ⋮ ⋮ ⋱ ⋮ �, 𝑏 = � 2� , 𝑎′ = � � , 𝑏′ = � 2 �
⋮ ⋮ ⋮ ⋱ ⋮ ⋮
𝑎1𝑛 𝑎2𝑛 ⋯ 𝑎𝑛𝑛 𝑏𝑛 0 0 ⋯ 𝑎′𝑛𝑛 𝑏′𝑛

(Tek elementet e matricës së krahut të djathtë dhe në elementet e termit të lirë janë vendosur
shënjat prim për të theksuar faktin që vlerat e tyre ndryshojnë nga homologët e tyre në
sistemin original).

Pse sistemi me matricë kryesore trekëndëshe të sipërme është “më i mirë" se sistemi me një
matricë koeficientësh të çfardoshëm? Mbasi është më e lehtë të zgjidhet sistemi me një
matricë trekëndëshe të sipërme të koeficientëve me anë të zëvendësimeve të njëpasnjëshme si
më poshtë. Së pari, në mënyrë të drejtëpërdrejtë përcaktohet vlera xn nga ekuacioni i fundit;
pastaj mund të zëvendësohet kjo vlerë tek ekuacioni i parafundit për të përftuar xn - 1, dhe
kështu me radhë derisa të mbrijmë në zëvendësimin e vlerave të (n - 1) variablave të fundit në
ekuacionin e parë prej të cilit gjejmë vlerën e x1.

Por si mundet atëherë që të përftohet nga një sistem me matricë me koeficienta të


çfardoshëm një sistem i njëvlershëm me një matricë trekëndëshe të sipërme a’? Kjo mund të
kryhet me anë të një serie veprimesh, të quajtura veprime elementare:

• përkëmbimi i dy ekuacioneve të sistemit;


• zëvendësimi i një ekuacioni me shumëfishat e tij jozero;
• zëvendësimi i një ekuacioni me shumën (diferencën) e një ekuacioni tjetër të shumëzuar
(pjesëtuar) me një faktor konstant.

Çdo sistem që përftohet nëpërmjet një serie të tillë veprimesh do të ketë të njëjtën zgjidhje si
sistemi fillestar. Le të shohim tani se si të përftohet një sistem me matricë trekëndëshe të
9 Teknika transformo-dhe-sundo | 192

sipërme. Së pari le të përdorim elementin a11 si bosht (pivot) për të transformuar në vlerën
zero të gjithë koeficientët pranë variablit x1 në të gjithë ekuacionet e tjerë poshtë të parit. Në
mënyrë të veçantë, ne zëvendësojmë ekuacionin e dytë me diferencën ndërmjet tij dhe
ekuacionit të parë të shumëzuar me a21/a11 për të përftuar një ekuacion me një koeficient zero
për x1. Duke kryer të njëjtin veprim për ekuacionin e tretë, të katërt dhe në fund të n-tin, me
shumëzuesit respektive a31/a11, a41/a11,…, an1/a11 të ekuacionit të parë, transformohen të
gjithë koeficientët pranë x1 në ekuacionet poshtë të parit në zero.
Pastaj veprojmë për koeficientët pranë x2 në mënyrë të ngjashme duke zbritur nga ekuacioni i
dytë të shumëzuar me një faktor të gjithë ekuacionet poshtë ekuacionit të dytë. Duke
përsëritur këtë veprim për secilin prej n–1 variablave, përftohet një sistem me një matricë
koeficientësh të tipit trekëndësh i sipërm.

Më parë se të japim algoritmin, le të shohim një shembull të përdorimit të metodës së


eliminimit të Gausit, duke e zmadhuar paraprakisht matricën e koeficientëve edhe me termin
e lirë (duke shtuar kollonën e (n + 1)të.

Shëmbull. Të zgjidhet me metodën e eliminimit të Gausit sistemi i ekuacioneve

2𝑥1 − 𝑥2 + 𝑥3 = 1
�4𝑥1 + 𝑥2 − 𝑥3 = 5
1𝑥1 + 𝑥2 + 𝑥3 = 0

Matrica e zgjeruar e sistemit do të jetë:

2 −1 1 1
�4 1 −1 5� = 𝑟𝑟𝑟𝑟ℎ𝑡𝑡 2 − (4/2) 𝑟𝑟𝑟𝑟ℎ𝑡𝑡 1
1 1 1 0 = 𝑟𝑟𝑟𝑟ℎ𝑡𝑡 3 − (1/2) 𝑟𝑟𝑟𝑟ℎ𝑡𝑡 1

2 −1 1 1
�0 3
3
−3
1
3�
1
0 − = 𝑟𝑟𝑟𝑟ℎ𝑡𝑡 3 − (1/2) 𝑟𝑟𝑟𝑟ℎ𝑡𝑡 2
2 2 2

2 −1 1 1
�0 3 −3 3�
0 0 2 −2

Pasi të kryhen transformimet e mësipërme mund të përftohet zgjidhja me anë të


zëvendësimeve nga poshtë-lart:

x3 = (−2) / 2 = − 1; x2 = (3− (−3)x3) / 3 = 0; dhe x1 = (1− x3 − (−1)x2) / 2 = 1.

Më poshtë, paraqitet faza e eliminimit të metodës së Gausit.

//Algoritmi 9.3 Skema e eliminimit të Gausit


// Të dhëna: matrica katrore a[1..n,1..n] dhe vektori shtyllë b[1..n]
// Rezultate: një matricë trekëndëshe e sipërme e njëvlershme e vendosur
// në matricën a[1..n, 1..n+1] ku kollona n+1 përmban krahun e djathtë
eliminimiGaus(n, a, b) {
for i ← 1 to n do
a[i, n+1] ← b[i]; // zgjerimi i matricës
9 Teknika transformo-dhe-sundo | 193

for i ← 1 to n - 1 do
for j ← i + 1 to n do {
pivot ← a[j, i]/a[i, i];
for k ← i to n + 1 do
a[j, k] ← a[j, k] – pivot *a[i, k];
}
}

Është me vend të bëjmë një vërejtje të rëndësishme në lidhje me algoritmin, të cilat duhet të
merren në konsideratë gjatë zbatimit të algoritmit të eliminimit të Gausit.

Në trajtën e paraqitur, eliminimi i Gausit nuk është gjithmonë korrekt. Nëse vlera bosht a[i, i]
= 0, atëherë me të nuk mund të pjesëtojmë dhe nuk mund të përdorim rreshtin e itë si bosht
për iteracionin e itë të algoritmit. Në një rast të tillë duhet të përfitojmë nga veprimi i parë
elementar dhe të përkëmbejmë rreshtin e itë me ndonjë rresht tjetër më poshtë tij që e ka
jozero koeficientin në shtyllën e itë. (Nëse sistemi ka një zgjidhje të vetme një rresht i tillë
ekziston gjithmonë).

Përderisa duhet të përgatitemi për mundësinë e përkëmbimit të rreshtave, atëherë duhet të


kemi kujdesemi edhe për një vështirësi tjetër të mundëshme: mundësia që vlera e koeficientit
a[i, i] të jetë shumë e vogël dhe atëherë faktori shkallëzues a[j, i]/a[i, i] mund të jetë shumë i
madh dhe vlera e re e a[j, k]-së mund të deformohet për shkak të gabimeve të rrumbullakimit
(round-off error) të shkaktuara nga zbritja e dy numrave me madhësi shumë të ndryshme. Për
të shmangur këtë problem gjithmonë mund të kërkojmë për një rresht me vlerën absolute më
të madhe të koeficientëve në shtyllën e itë, dhe të përkëmbehet ai me rreshtin e itë dhe të
përdoret elementi i ri a[i, i] si bosht në iteracionin e itë. Ky modifikim i algoritmit, i quajtur,
boshtëzim i pjesshëm (partial pivoting), siguron që madhësia e faktorit shkallëzues të mos ta
kalojë asnjëherë vlerën 1.

Algoritmi i mëposhtëm paraqet një zbatim të përmirësuar të metodës të eliminimit të Gausit.

// Algoritmi 9.4
// Eliminimi i përmirësuar i Gausit
// Të dhëna: matrica a[1..n,1..n] dhe vektori shtyllë b[1..n]
// Rezultate: një matricë trekëndëshe e sipërme e njëvlershme e vendosur
// në matricën a[1..n, 1..n+1], ku kollona n+1 përmban krahun e
// djathtë të sistemit

eliminimiPërmirësuarGaus(n, a, b) {
for i←1 to n do
a[i, n+1] ← b[i]; // zgjerimi i matricës
for i ← 1 to n – 1 do{
pivotrow ← i;
for j ← i + 1 to n do
if abs(a[j, i]) > abs(a[pivotrow, i]) //gjetja e boshtit të ri
then pivotrow ← j ;
for k ← i to n + 1 do
swap(a[i, k], a[pivotrow, k]);
for j ← i + 1 to n do { // eliminimi i Gausit
9 Teknika transformo-dhe-sundo | 194

temp ← a[j, i]/a[i, i];


for k ← i to n + 1 do
a[j, k] ← a[j, k] - a[i, k] * temp
}
}
}

Le të përmendim shkurtimisht efektshmërinë kohore të algoritmit. Trupi i ciklit më të


brendshëm përbëhet nga një rresht i vetëm (rreshti 13):

A[j, k] ← A[j, k] − A[i, k] * temp,

që përmban një shumëzim, një zbritje dhe tre indeksime. Në pjesën më të madhe të
kompjuterave shumëzimi është më i shtrenjtë se mbledhje/zbritja dhe prandaj për llogaritjen e
efektshmërisë do të mbështetemi vetëm tek shumëzimi. Duke u mbështetur tek ky fakt,
llogaritjet e klasifikojnë algoritmin e përmirësuar të Gausit në klasën Θ(n3).

Përderisa faza e dytë e eliminimit të Gausit, zëvendësimi nga poshtë-lart, (backward


substitution) bën pjesë në klasën Θ(n2), koha e përgjithshme zotërohet nga faza e eliminimit,
kështu që i gjithë algoritmi është një algoritëm kubik.

Teorikisht, eliminimi i Gausit ose gjithmonë prodhon rezultate të sakta për një sistem
ekuacionesh kur sistemi ka një zgjidhje të vetme ose zbulon se nuk ekziston një zgjidhje e
tillë. Në praktikë, zgjidhja e sistemeve me përmasa të mëdha nuk është kaq e pëlqyeshme të
kryhet me anë të kësaj metode, për shkak të grumbullimit të gabimeve të rrumbullakimit.

9.1.3 Shpërthimi LU dhe zbatime të tij

Eliminimi i Gausit ka një nënprodukt interesant dhe mjaft të vlefshëm që quhet shpërthimi
LU (LU-decomposition). Në fakt, zbatimet tregtare të eliminimit të Gausit mbështeten më
tepër në një shpërthim të tillë sesa në metodat që paraqitëm më lart.

Shëmbull. Le t’i kthehemi shembullit të dhënë në fillim të leksionit ku u zbatua eliminimi i


Gausit për matricën:

2 −1 1
A = �4 1 −1�
1 1 1

Le të konsiderojmë matricën trekëndëshe të poshtme L të përbërë nga vlera 1 në diagonalen


kryesore dhe në rreshtat poshtë diagonales kryesore nga shumëzuesit e përdorur në procesin e
eliminimit të Gausit:

1 0 0
2 1 0
L = �1 1 �
1
2 2

dhe matricën trekëndëshe të sipërme U që është rezultat i eliminimit:


9 Teknika transformo-dhe-sundo | 195

2 −1 1
U = �0 3 −3�
0 0 2

Rezulton se produkti i këtyre dy matricave është i barabartë me matricën A. (Ky rast i


veçantë mund të provohet me anë të shumëzimit të drejtëpërdrejtë ndërsa rasti i përgjithshëm
ka nevojë për një vërtetim që nuk do ta trajtojmë).
Prandaj zgjidhja e sistemit Ax = b është e njëvlershme me zgjidhjen e sistemit LUx = b.
Sistemi i fundit mund të zgjidhet si më poshtë. Shënojmë me y = Ux dhe pastaj Ly = b.
Zgjidhim së pari sistemin Ly = b, gjë që është lehtësisht e realizueshme mbasi L është
matricë trekëndëshe e poshtme dhe pastaj zgjidhim sistemin Ux = y me matricën trekëndëshe
të sipërme për të gjetur zgjidhjen x. Kështu për sistemin e fillimit të leksionit ne zgjidhim së
pari Ly = b (nga lart–poshtë):

1 0 0 𝑦1 1
2 1 0
�1 1 � �𝑦2 � = �5�
1 𝑦3
2 2 0

1 1
Zgjidhja e tij është: y1 = 1, y2 = 5 − 2y1 = 3, y3 = 0 – y1 – y2 = −2
2 2
Zgjidhja e Ux = y nënkupton zgjidhjen e sistemit

2 −1 1 𝑥1 1
�0 3 𝑥
−3� � 2 � = � 3 �
0 0 2 𝑥3 −2

dhe zgjidhja e tij (nga poshtë-lart) do të jetë:

x3 = (−2)/2 = −1, x2= (3 − (−3) x3 / 3 = 0, x1 = (1 − x3 − (−1) x2) / 2 = 1

Vëmë në dukje se sapo të kemi shpërthimin LU të matricës A ne mund të zgjidhim sistemin


Ax = b, me shumë krahë të djathtë b në të njëjtën kohë. Kjo është një përparësi e dukshme
ndaj eliminimit klasik të Gausit. Gjithashtu vëmë në dukje se shpërthimi LU nuk kërkon
kujtesë shtesë mbasi pjesën jozero të matricës U e regjistron në pjesën e sipërme të matricës
A (duke përshirë diagonalen kryesore) dhe pjesën jo të dukshme të L e rezervon poshtë
diagonales kryesore të A.

Llogaritja e matricës së anasjelltë


Eliminimi i Gausit është një algoritëm shumë i dobishëm mbasi prek një nga problemet më të
rëndësishme të matematikës së zbatuar: zgjidhjen e sistemeve të ekuacioneve të linearë. Në
fakt eliminimi i Gausit mund të zbatohet edhe në disa probleme të tjera të algjebrës lineare, si
për shëmbull llogaritja e matricës së anasjelltë (matrix inverse). E anasjellta e një matrice
kuadratike A e rendit n është përsëri një matricë kuadratike, po e rendit n, që shënohet A-1 e
tillë që:
AA-1 = I,

ku me I është shënuar matrica njësi (të gjitha 1 në diagonalen kryesore dhe kudo zero në
pjesën tjetër). Jo çdo matricë ka matricë të anasjelltë por kur ajo ekziston atëherë është e
9 Teknika transformo-dhe-sundo | 196

vetme. Në qoftë se matrica A nuk ka matricë të anasjelltë ajo quhet matricë singulare
(singular). Mund të provohet që një matricë është singulare atëherë dhe vetëm atëherë kur një
nga rreshtat e saj është kombinim linear i rreshtave të tjerë. Një rrugë e përshtatshme për të
provuar mos-singularitetin e matricës është përdorimi i eliminimit të Gausit. Nëse algoritmi
prodhon një matricë trekëndëshe të sipërme me të gjitha vlerat jozero në diagonalen kryesore
atëherë matrica është josingulare përndryshe ajo është singulare. Kështu të qenurit singular
është një situatë shumë e veçantë dhe shumë matrica katrore kanë të anasjelltat e tyre.
Teorikisht matricat e anasjellta janë shumë të rëndësishme mbasi ato luajne rolin e
reciprokuesit në algjebrën matricore duke kapërcyer mungesën e veprimit të pjesëtimit të
dukshëm për matricat. Për shëmbull me një ngjashmëri të plotë me një ekuacion linear me një
të panjohur Ax = b zgjidhja e të cilit mund të shkruhet x = A-1b (në qoftë se a nuk është zero)
ne mund të shprehim një zgjidhje të një sistemi me n ekuacione dhe n të panjohura Ax = b si
x = A-1b (nëse A nuk është singulare) ku b natyrisht është një vektor dhe jo një numur.

Në përputhje me përkufizimin e matricës së anasjelltë për një matricë josingulare të rendit n,


për ta llogaritur atë, është e nevojshme të gjenden n2 numrat xij, 1≤ i, j≤ n të tillë që:

𝑎11 𝑎12 ⋯ 𝑎1𝑛 𝑥11 𝑥12 ⋯ 𝑥1𝑛 1 0 ⋯ 0


𝑎12 𝑎22 ⋯ 𝑎2𝑛 𝑥12 𝑥22 ⋯ 𝑥2𝑛 0 1 ⋯ 0
� ⋮ ⋮ ⋱ ⋮ � � ⋮ ⋮ ⋱ ⋮ � = �⋮ �
⋮ ⋱ ⋮
𝑎1𝑛 𝑎2𝑛 ⋯ 𝑎𝑛𝑛 𝑥1𝑛 𝑥2𝑛 ⋯ 𝑥𝑛𝑛 0 0 ⋯ 1

Të panjohurat mund të gjenden duke zgjidhur një sistem prej n ekuacionesh lineare që kanë të
njëjtën matricë koeficientësh A, vektor të panjohurave xj që është kollona e jtë e matricës së
anasjelltë dhe vektor të krahut të djathtë ej që është kollona e jtë e matricës njësi (1≤ j ≤ n):

Axj = ej

Ne mund t’i zgjidhim këta sisteme duke zbatuar eliminimin e Gausit tek matrica A e zgjeruar
me një matricë njësi të rendit n. Akoma më mirë ne mund të përdorim eliminimin e Gausit
për të gjetur shpërthimin LU të A dhe pastaj zgjidhim sistemet
LU xj = ej, j = 1,2 …, n, siç u shpjeguar më parë.

Llogaritja e përcaktorit
Një problem tjetër që mund të zgjidhet me anë të eliminimit të Gausit është llogaritja e
përcaktorit (determinant). Përcaktori i një matrice të rendit n, që shënohet me det A ose |𝐴|,
është një numur vlera e të cilit mund të përcaktohet në mënyrë rekursive si më poshtë. Në
qoftë se n = 1 domethënë, në qoftë se A përbëhet nga një element i vetëm a11, atëherë det A
është e barabartë me a11; për n > 1, det A llogaritet me formulën rekursive:
n
det A = � sj a1j detAj
j=1

ku sj është +1 në qoftë se j është tek dhe −1 në qoftë se j është çift, a1j është elementi në
rreshtin e parë dhe kollonën jtë, dhe Aj është një matricë e rendit (n - 1) e përftuar nga matrica
A duke fshirë prej saj rreshtin e parë dhe kollonën e j-të.

Në veçanti për një matricë të rendit 2, përkufizimi na shpie në një formulë që mbahet mend
lehtë:
9 Teknika transformo-dhe-sundo | 197

a11 a12
a a
det �a
21 a22 � = a11 det[ 22 ] − a12 det[ 21 ] = a11 a22 − a12 a21

Me fjalë të tjera, përcaktori i një matrice të rendit të dytë është thjesht i barabartë me
diferencën e produkteve të elementeve në diagonalet e tyre.

Në mënyrë të ngjashme për një matricë kuadratike të rendit 3 duke zbatuar përkufizimin ne
përftojmë
𝑎11 𝑎12 𝑎13
𝑎 𝑎23 𝑎21 𝑎23 𝑎21 𝑎22
𝑑𝑒𝑒 �𝑎21 𝑎22 𝑎23 � = 𝑎11 𝑑𝑑𝑑 � 22
𝑎32 𝑎33 � − 𝑎12 𝑑𝑑𝑑 �𝑎31 𝑎33 � + 𝑎13 𝑑𝑑𝑑 �𝑎31 𝑎32 �
𝑎31 𝑎32 𝑎33

= a11a22a33 + a12a23a31 + a21a32a13 − a31a22a13 − a21a12a33 − a32a23a11.

Me që ra fjala, kjo formulë është shumë e përshtatshme në zbatime të ndryshme. Në veçanti


ajo takohet në algoritmin Quickhull.

Në mënyrë të natyrshme lind pyetja: “Po nëse dëshërojmë të llogaritim përcaktorin e një
matrice të një rendi shumë të lartë”? (Megjithëse kjo detyrë ndeshet rallë në praktike ja vlen
ta diskutojmë si problem.) Përdorimi i përkufizimit rekursiv na jep pak mundësi sepse kërkon
llogaritjen e një shume të përbërë nga n! terma. Por në këtë rast na vjen në ndihmë eliminimi
i Gausit. Thelbi i çështjes qëndron në faktin se përcaktori i një matrice trekëndëshe të sipërme
është i barabartë me produktin e elementeve në diagonalen kryesore. Si rezultat ne mund ta
llogaritim përcaktorin e një matrice kuadratike të rendit n në një kohë kubike me anë të
eliminimit të Gausit.

Përcaktori luan një rol të rëndësishëm në teorinë e sistemeve të ekuacioneve linearë. Në


mënyrë të veçantë, një sistem prej n ekuacionesh linearë me n të panjohura Ax = b ka një
zgjidhje të vetme atëherë dhe vetëm atëherë kur përcaktori i matricës së koeficientëve të tij,
det A është i ndryshëm nga zero. Për më tepër kjo zgjidhje mund të gjendet me anë të
formulave të quajtura rregulli i Kramerit (Cramer 15)

detA1 detAj det An


x1 = , ⋯ , xj = , ⋯ , xn =
det A det A det A

ku, det Aj

është përcaktori i një matricë të përftuar nga zëvendësimi i kollonës së jtë të A me kollonën e
termave të lira. Por a është rregulli i Kramerit një algoritëm i mirë për të zgjidhur një sistem
ekuacionesh linearë?

9.2 Ndryshimi i paraqitjes

Në këtë seksion do të shqyrtojmë edhe njëherë problemin e llogaritjes së vlerës së një


polinomi

Pn(x) = anxn + an-1xn - 1 + ··· + a1x1+ a0, (9.1)

15
Gabriel Cramer, 1704 – 1752, matematikan zviceran
9 Teknika transformo-dhe-sundo | 198

në një pikë të dhënë x dhe rastin e tij të veçantë, llogaritjes e xn. Polinomet përbëjnë rastin më
të rëndësishëm të funksioneve mbasi nga njëra anë ata zotërojnë një tërësi cilësish të mira dhe
nga ana tjetër mund të përdoren për përafrimin e funksioneve të tjerë. Problemi i përpunimit
të polinomeve me efektshmëri ka qenë i rëndësishëm për shekuj me rradhë; zbulime të reja
janë kryer edhe këto 50 vitet e fundit. Nga më të rëndësishmit është Transformimi i shpejtë
Furie (Fast Fourier Transform, FFT). Rëndësia praktike e këtij algoritmi të shquar, i cili
bazohet në paraqitjen e një polinomi me anë të vlerave të tij në pika të zgjedhura në mënyrë
të veçantë, është e tillë që shumë njerëz e quajnë atë si një nga zbulimet algoritmike më të
rëndësishme në të gjitha kohët. Algoritmi FFT, për shkak të vështirësive relative të tij nuk do
të trajtohet në këtë cikël leksionesh.

9.2.1 Rregulli i Hornerit

Rregulli i Hornerit ose e quajtur edhe skema e Hornerit, është një algoritëm i vjetër por tepër
elegant dhe i efektshëm për vlerësimin e një polinomi në një pikë të caktuar. Këtij rregulli i
është dhënë emri i publikuesit të tij në shekullin e 19të, matematikanit anglez W.G. Horner.
Por sipas D. Knuth-it metoda është përdorur nga Isaac Newton-i 150 vjet para Hornerit.

Rregulli i Hornerit është një shëmbull i mirë për të paraqitur versionin e ndryshimit të
paraqitjes të teknikës transformo-dhe-sundo përderisa ai mbështetet në paraqitjen në një
formë tjetër të ndryshme nga ajo e përgjithshmja (9.1) për paraqitjen e polinomit. Kjo formë e
re përftohet nga (9.1) me anë të vënies të x-it si faktor i përbashkët në polinomet e shkallëve
më të ulta:

Pn (x) = (⋯ (an x + an−1 )x + an−2 )x + ⋯ + a1 )x + a0 (9.2)

Për shëmbull për polinomin e shkallës së katërt P4(x) = 2x4 – x3 + 3x2 + x – 5, do të kemi

P4(x) = 2x4 – x3 + 3x2 + x – 5


= (2x3 – x2 + 3x + 1)x – 5
= ((2x2 – x + 3)x + 1)x – 5
= (((2x – 1)x + 3)x + 1)x – 5 (9.3)

Pikërisht në formulën (9.2) do të bëhet zëvendësimi i një vlere x për të përftuar vlerën e
polinomit. Duket e vështirë të besohet se në këtë mënyrë përftohet një algoritëm i efektshëm,
por pamja e formulës (9.2) është thjesht një pamje dhe asgjë më shumë. Siç do të shohim nuk
është e nevojshme që të zbatojmë në trajtë të dukshme nëpërmjet transformimeve që të shpien
tek ajo: mjafton vetëm të kemi listën me koeficientët e polinomit.

Llogaritjet me laps-dhe-letër mund të organizohen në mënyrë të përshtatshme me anë të një


tabele me dy rreshta. Në rreshtin e parë vendosen koeficientët e polinomit (duke përfshirë
edhe koeficientët zero nëse ka të tillë) nga më i madhi an, në më të voglin a0. Në rreshtin e
dytë, me përjashtim të vlerës së parë që është an, vlerat e tjera plotësohen nga e majta në të
djathtë si më poshtë: vlera pasuese është sa vlera e x-it shumëzuar me vlerën e mëparshme të
llogaritur dhe shtuar koeficienti korrespondues në rreshtin e parë. Vlera e fundit e llogaritur
në këtë mënyrë jep vlerën e dëshëruar.

Shembull 1. Të llogarit P4(x) = 2x4 – x3 + 3x2 + x – 5 në pikën x = 3


9 Teknika transformo-dhe-sundo | 199

koeficientët 2 –1 3 1 –5
x=3 2 3∙2 + (–1)=5 3∙5+3=18 3∙18+1=55 3∙55+(-5)=160

Pra, P4(3) = 160. (Duke krahasuar llogaritjet e mësipërme me formulën (9.3), do të shihet se:
3 ∙ 2 + (-1) = 5 është vlera e 2x - 1 në pikën x = 3; 3∙5 +3 = 18 është vlera e x(2x - 1) + 3 në
pikën x = 3; 3∙18+1 = 55 është vlera e x(x(2x - 1) + 3) +1 në pikën x = 3; 3∙55 +(-5) = 160
është vlera e x(x(x(2x - 1) + 3) + 1) - 5 = P4(x) në pikën x = 3.)

Algoritmi është më i shkurtëri që mund të imagjinohet për një formulë jo aq të qartë:

Algoritmi 9.5 Llogaritja e vlerës së një polinomi me anë të rregullit të Hornerit


// Të dhëna: një tabelë a[0..n] me koeficientët e një polinomi, të rezervuar
// nga më i ulti në më të lartin dhe një vlerë numerike x
// Rezultate: pol, vlera e polinomit në pikën x

horner(n, a, x) {
pol ← a[n];
for i ← n – 1 to 0 step - 1 do
pol ← pol * x + a[i];
return pol
}

Algortimi i mbështetur në formulën e Hornerit e ka efektshmërinë kohore Θ(n).

Rregulli i Hornerit ka edhe disa produkte të tjera shumë të dobishme. Numrat ndërmjetës të
prodhuar nga algoritmi në procesin e vlerësimit të P(x) në një pikë x0, japin koeficientët e
pjesëtimit të P(x) me (x – x0), ndërsa rezultati përfundimtar përveç të qenurit P(x0), është i
barabartë me mbetjen e këtij pjesëtimi. Kështu në shëmbullin e dhënë më lart, herësi dhe
mbetja e pjesëtimit të 2x4 – x3 + 3x2 + x – 5 me (x – 3) do të jenë 2x3 + 5x2 + 18x + 55 dhe
160. Ky algoritëm pjesëtimi, i njohur me emrin pjesëtim sintetik (synthetic division), është
më i përshtatshëm se i ashtuquajturi pjesëtimi i gjatë (long division). (Megjithatë në ndryshim
nga pjesëtimi i gjatë ai është i përdorshëm vetëm për pjesëtime të formës (x – c), ku c është
një vlerë konstante.)

9.2.2 Llogaritja e fuqisë së një numri

Efektshmëria habitëse e rregullit të Hornerit zbehet nëse ajo zbatohet në llogaritjen vetëm të
an, që është vlera e polinomit Pn(x) = xn në pikën x = a. Me të vërtetë, ai degjeneron në
shumëzimet e vazhdueshme të vlerës a me vetveten dhe me një grumbull të kotë mbledhjesh
të zerove ndërmjetëse.

Përderisa llogaritja e an (në fakt llogaritja e an mod m) është një veprim thelbësor në disa
metoda të rëndësishme të kontrollit të të qenurit numur i thjeshtë dhe të kriptimit të
informacionit, do të paraqiten dy algoritme për llogaritjen e an që bazohen në idenë e
ndryshimit të paraqitjes. Të dyja ato shfrytëzojnë paraqitjen në trajtë binare të eksponentit n
por njëra prej tyre e përpunon vargun e biteve nga e majta në të djathtë ndërsa e tjetra nga e
djathta në të majtë.
9 Teknika transformo-dhe-sundo | 200

Fuqia binare nga e majta në të djathtë


Le të jetë n = bk∙∙∙bi∙∙∙b0 paraqitja si një varg bit-ësh e një numri të plotë pozitiv n në
sistemin binar të numërimit. Kjo do të thotë që vlera e n mund të llogaritet si vlera e
polinomit

P(2) = bk2k + ∙∙∙ + bi2i + ∙∙∙ + b020 (9.4)

Për shëmbull, nëse n = 13, paraqitja e tij binare është 1101, dhe paraqitja e numrit në trajtë
polinomiale është

13 = 1∙ 23 + 1∙ 22 + 0∙21 + 1∙20

Le të llogaritim vlerën e këtij polinomi duke zbatuar rregullin e Hornerit dhe të shohim pastaj
se çfarë sjellin veprimet e rregullit për llogaritjen e fuqisë binare:
k +⋯+b i +⋯+b
an = aP(2) = abk2 i2 0

a. Rregulli i Hornerit për polinomin binar P(2) b. Implikimi për an = ap(2)


p←1 // shifra e kreit është gjithmonë 1 për n ≥ 1 ap ← a1
for i ← k – 1 to 0 step -1 do for i ← k – 1 to 0 step -1 do
p ←2p + bi ap ← a 2p+bi

Por,
(ap )2 në se bi = 0
a2p+bi = a2p ∙ abi = (ap )2 ∙ abi = � p 2
(a ) ∙ a, në se bi = 1

Në këtë mënyrë duke inicializuar akumulatorin me vlerën a, ne mund të skanojmë vargun e


biteve të paraqitjes binare të eksponentit duke ngritur në fuqi të dytë gjithmonë vlerën e
fundit të akumulatorit, dhe nëse shifra e rradhës e paraqitjes binare është 1, e shumëzojmë atë
edhe me një a. Këto vrojtime na shpien në algoritmin e fuqisë binare nga-e-majta-në-të-
djathtë (left-to-right binary exponentiation) për llogaritjen e an .

Algoritmi 9.6 Llogaritja e fuqisë me anë të LRB-së


// Të dhëna: k, rendi më i lartë në shpërthimin binar dhe tabela e koeficientëve
// të shpërthimit binar b[k…0] dhe një numër a
// Rezultate: vlera e an

LRBinExp (a, k, b) {
fuqi ← a;
for i ← k – 1 to 0 step -1 do {
fuqi ← fuqi * fuqi;
if b[i] = 1
then fuqi ← fuqi * a;
}
return fuqi
}

Shembull 2. Të llogaritet a13 me anë të algoritmit të fuqisë binare nga-e-majta-në-të-djathtë.


9 Teknika transformo-dhe-sundo | 201

Llogaritim në fillim paraqitjen në sistemin binar të fuqisë, n = 13 = 11012 dhe pastaj


plotësojmë tabelën nga e majta në të djathtë sipas kërkesës së algoritmit:

Shifrat binare të n-së 1 1 0 1


Akumulatori produktit a a ∙a = a3
2
(a ) = a6
3 2
(a ) ∙a = a13
6 2

Në lidhje me kohën e llogaritjeve mund të vëmë re që algoritmi kryen një ose dy shumëzime
në çdo përsëritje të ciklit të tij të vetëm. Në këtë mënyrë numri i përgjithshëm i shumëzimeve
Sh(n) i kryer prej tij për llogaritjen e an do të jetë

(k - 1) ≤ Sh(n) ≤ 2(k - 1),

ku, k është sasia e shifrave në paraqitjen binare të eksponentit n. Duke patur parasysh që
𝑘 − 1 = ⌊log 𝑛⌋ mund të arijmë në përfundimin që efektshmëria e algoritmit të Fuqisë binare
nga-e-majta-në-të-djathtë është logaritmike.

Nëse a = 2, d.m.th. duam që të llogaritet 213 atëherë do të kishim:

Shifrat binare të n-së 1 1 0 1


Akumulatori produktit 2 2 ∙2 = 8
2 2
8 = 64 64 ∙2 = 8192
2

Fuqia binare nga e djathta në të majtë


Krahas algoritmit të mësipërm është krijuar një algoritëm tjetër i quajtur fuqia binare nga-e-
djathta-në-të-majtë (right-to-left binary exponentiation) që përdor të njëjtin polinom binar
P(2) (shih (9.4)) për të përftuar vlerën e n. Por ai nuk përdor rregullin e Hornerit si metoda e
mëparshme por ai e shfrytëzon ndryshe:
k +⋯+b i +⋯+b k i
an = aP(2) = abk2 i2 0 = ab k 2 ∗ ⋯ ∗ a b i 2 ∗ ⋯ ∗ a b 0

Kështu, an mund të llogaritet si produkt i termave

2𝑖
𝑎𝑏𝑖 2 = �𝑎 𝑛𝑒̈ 𝑠𝑠 𝑏𝑖 = 1
𝑖

1 𝑛𝑒̈ 𝑠𝑠 𝑏𝑖 = 0
𝑖
,domethënë, produkt i termave të njëpasnjëshëm 𝑎2 duke kapërcyer ata për të cilët
𝑖
koeficienti binar bi është zero. Për më tepër, ne mund të llogaritim 𝑎2 thjesht me anë të
ngritjes në katror të të njëjtit term të llogaritur nga vlera e mëparshme e i-së përderisa
𝑖 𝑖−1
𝑎2 = (𝑎2 )2 . Kështu ne llogaritim të gjitha fuqitë e a, nga më vogla tek më e madhja (nga
e djathta në të majtë), por ama përfshijmë në akumulatorin e produktit vetëm ata për të cilët
shifra binare është 1.

//Algoritmi 9.7 Llogaritja e fuqise me anë të RL-së


// Të dhëna: k >0, rendi më i lartë në shpërthimin binar dhe tabela e koeficientëve
// të shpërthimit binar b[k..0] dhe një numër a
// Rezultate: vlera e an
9 Teknika transformo-dhe-sundo | 202

RLBinExp (a, k, b) {
term ← a;
if b[0] = 1
then fuqi ← a
else fuqi ← 1;
for i ← 1 to k do {
term ← term * term;
if b[i] = 1
then fuqi ← fuqi *term;
}
return fuqi
}

Shëmbull 3. Të llogaritet a13 me anë të algoritmit të fuqisë binare nga-e-djathta-në-të-majtë.

Llogaritim në fillim paraqitjen në sistemin binar të fuqisë, n = 13 = 11012 dhe pastaj


plotësojmë tabelën nga e djathta në të majtë sipas kërkesës së algoritmit:

1 1 0 1 Shifrat binare të n-së


a8 a4 a2 a Termat (a2)i
a5a8 = a13 a∙a4 = a5 a a Akumulatori produktit

Është e qartë se efektshmëria i algoritmit është gjithashtu logaritmike për të njëjtën arsye si
algoritmi i fuqisë binare nga e majta-në-të-djathtë. Dobishmëria e të dy algoritmeve në trajtën
e paraqitur zbehet pak nga fakti që të dy kërkojnë shpërthimin binar në trajtë të dukshme të
eksponentit.

9.3 Reduktimi i problemit

Reduktimi i problemit është më radikali nga të tre versionet: një problem transformohet në
një problem krejtësisht tjetër për të cilin dihet se si zgjidhet, figura 9.1.

reduktimi Problemi 2 Algoritmi A


Problemi 1
(i zgjidhshëm nga Zgjidhja e problemit 2
(për t'u zgjidhur)
algoritmi A)

Figura 9.1 Strategjia e reduktimit të problemit

Zbatimi i strategjisë së reduktimit për të hartuar algoritme praktike nuk është aq i dukshëm.
Së pari duhet që të identifikojmë problemin e ri në të cilin duhet të transformohet problemi i
dhënë. Pastaj duhet të sigurohemi që algoritmi i transformimit, si dhe algoritmi i përdorur për
problemin e ri janë më të efektshëm në krahasim me alternativat e tjera.

Vëmë në dukje që ne e kemi takuar këtë teknikë më parë. Një nga shembujt është ai i të
ashtuquajturit pjesëtim sintetik kryer me anë të zbatimit të rregullit të Hornerit për vlerësimin
e polinomeve. Gjithashtu nga gjeometria analitike kemi përdorur faktin e mëposhtëm: nëse
p1=(x1, y1), p2=(x2, y2) dhe p3=(x3, y3) janë tri pika arbitrare në plan atëherë përcaktori
9 Teknika transformo-dhe-sundo | 203

x1 y1 1
�x2 y2 1� = x1 y2 + x3 y1 + x2 y3 − x3 y2 − x2 y1 − x1 y3
x3 y3 1

është pozitiv atëherë dhe vetëm atëherë në qoftë se pika p3 është në të majtë të vijës së
drejtuar 𝑝��������⃗
1 𝑝2 nga pika p1 në pikën p2. Me fjalë të tjera, ne reduktojmë një çështje
gjeometrike për vendosjen relative të tri pikave në çështjen e shënjës së një përcaktori. Me të
vërtetë, e gjithë idea e gjeometrisë analitike është e bazuar në reduktimin e problemeve
gjeometrike në probleme algjebrike. Një pjesë e madhe e problemeve gjeometrike i kushtohet
mprehtësisë së René Descartes 16.

Llogaritja e shumëfishit më të vogël të përbashkët


Kujtojmë që shumëfishi më i vogël i përbashkët (least common multiple) i dy numrave të
plotë pozitivë m dhe n, shënuar si shmvp(m, n) përkufizohet si numri i plotë më i vogël që
plotpjesëtohet nga m dhe n. Për shëmbull, shmvp(24, 60) = 120 dhe shmvp(11, 5) = 55.
Shumëfishi më i vogël i përbashkët është një nga konceptet më të rëndësishme të aritmetikës
elementare dhe algjebrës. Në shkollë të mesme një nga rrugët për llogaritjen e tij është ajo e
faktorizimit të m-së dhe n-së me anë të numrave të thjeshtë dhe pastaj shmvp(m, n) mund të
llogaritet si produkt i të gjithë faktorëve të thjeshtë të përbashkët të m dhe n shumëzuar me
faktorët e thjeshtë të m që nuk janë në numrin n si dhe shumëzuar me faktorët e thjeshtë të n
që nuk janë në numrin m. Për shëmbull,

24 = 2⋅2⋅2⋅3
60 = 2⋅2⋅3⋅5
shmvp(24, 60) = (2⋅2⋅3) ⋅2⋅5=120

Si procedurë llogaritëse kjo mënyrë ka të njëjtat të meta si dhe algoritmi i llogaritjes se


plotpjesëtuesit më të madh të përbashkët. Ai është i paefektshëm dhe kërkon një listë të
numrave të thjeshtë të njëpasnjëshëm.

Mund të hartohet një algoritëm më i efektshëm për llogaritjen e shumëfishit më të madh të


përbashkët të dy numrave të plotë pozitivë duke përdorur versionin e reduktimit të problemit.
Para se gjithash ka një algoritëm shumë të efektshëm (algoritmi i Euklidit) për llogaritjen
pjesëtuesit më të madh të përbashkët. A ka ndonjë formulë që lidh shmvp(m, n) dhe pmp(m,
n)? Nuk është e vështirë të shihet se produkti i shmvp(m, n) dhe pmp(m, n) përfshin çdo
faktor të m-së dhe n-së një herë dhe vetëm një herë kështu që është thjesht produkti i m dhe n.
Ky vrojtim na shpie tek formula

m∙n
shmvp(m, n) =
pmp(m, n)

ku, pmp(m, n) mund të llogaritet me shumë efektshmëri me anë të algoritmit të Euklidit.

Reduktimi i problemeve të optimizimit


Një klasë tjetër problemesh që zgjidhen me anë të versionit të reduktimit të problemit janë
problemet e optimizimit. Nëse një problem kërkon gjetjen e maksimumit të një funksioni ai

16
René Descartes (1596-1650) filozof, matematikan, fizikan dhe shkrimtar francez.
9 Teknika transformo-dhe-sundo | 204

quhet problem maksimizimi (maximization problem); nëse kërkohet të gjendet minimumi


atëherë ai quhet problem minimizimi (minimization problem). Le të supozojmë se kemi për të
gjetur minimumin e një funksioni f(x) dhe ne njohim një algoritëm për gjetjen e maksimumit
të një funksioni. Si mund të përfitojme nga ky fakt? Përgjigja gjendet në formulën e thjeshtë

min f(x) = –max [–f(x)]


Me fjalë të tjera, për të minimizuar një funksion ne mund të maksimizojmë negativin e tij dhe
për të përftuar një vlerë të drejtë minimale të vetë funksionit ndryshojmë shënjën e përgjigjes.
Kjo cilësi ilustrohet për një funksion të një variabli real në figurën 9.2.

Sigurisht, formula
max f(x) = –min [–f(x)]
është gjithashtu e vërtetë; ajo tregon se si një problem maksimizimi mund të reduktohet në
një problem të njëvlershëm minimizimi.

Figura 9.2 Lidhja ndërmjet problemeve të maksimizimit dhe minimizimit

Kjo lidhje ndërmjet problemeve të maksimizimit dhe minimizimit është shumë e


përgjithshme: ajo është e vlefshme për funksionet e përcaktuar në ndonjë zonë D. Në veçanti
ajo mund të zbatohet në funksionet me disa variabla që i nënështrohen disa kushteve shtesë.
Një klasë e tillë problemesh janë problemet e të ashtuquajturit programimi linear (linear
programming).

Duke qenë akoma në fushën e optimizimit të funksioneve është vendi të përmendim që


procedurat standarde të gjetjes se pikave skajore të një funksioni bazohen në fakt në një
reduktim problemi. Me të vërtetë ato sugjerojnë gjetjen e funksionit f ’(x) dhe zgjidhjen e
ekuacionit f’(x) = 0 për të gjetur pikat kritike. Me fjalë të tjera problemi i optimizimit
reduktohet në zgjidhjen e një ekuacioni si pjesa qendrore e gjetjes së pikave skajore.
Theksojmë këtu që ne nuk i quajmë procedurat e llogaritjeve një algoritëm përderisa ato nuk
janë përcaktuar qartësisht. Me të vërtetë, nuk ka një metodë të përgjithshme për zgjidhjen e
ekuacioneve. Një sekret i vogël nga tekstet mësimore të llogaritjeve është se problemet janë
zgjedhur me aq kujdes sa pikat skajore mund të gjenden pa vështirësi. Kjo e bën me të lehtë
jetën e studentit dhe të pedagogut por nga ana tjetër mund të krijojë përshtypje të gabuara në
mendjen e studentëve.

9.4 Përmbledhje

• Transformo-dhe-sundo është strategjia e katërt për hartimin e algoritmeve (zgjidhjen e


problemeve) që po diskutojmë në këtë cikël leksionesh. Ajo bazohet në idenë e
transformimit të problemit në një problem që mund të zgjidhet me lehtë.
9 Teknika transformo-dhe-sundo | 205

• Ekzistojnë tre versione kryesore të strategjisë transformo-dhe-sundo: thjeshtimi i rastit,


ndryshimi i paraqitjes dhe reduktimi i problemit.
• Thjeshtimi i rastit është një teknikë e transformimit të një rasti të një problemi në një rast
të të njëjtit problem por me disa cilësi të veçanta që e bëjnë problemin më të lehtë për ta
zgjidhur. Pararenditja dhe eliminimi i Gausit janë shembuj të mirë të kësaj teknike.
• Ndryshimi i paraqitjes nënkupton ndryshimin e një paraqitjeje të një rasti të një problemi,
në një paraqitje tjetër të të njëjtit rast. Si shembuj mund të përmendim rregullin e Hornerit
për vlerësimin e polinomit dhe algoritmet e fuqisë binare.
• Reduktimi i problemit kërkon transformimin e një problemi të dhënë në një problem tjetër
që mund të zgjidhet me ndonjë algoritëm të njohur. Ndër shembujt që zgjidhen me këtë
teknikë përmendim reduktimin në programimin linear.
• Disa nga shembuj e përdorur për ilustrimin e teknikës transformo-dhe-sundo janë
algoritme të rëndësishme si për shembull eliminimi i Gausit dhe skema e Hornerit.
• Eliminimi i Gausit, një algoritëm për zgjidhjen e sistemeve të ekuacioneve linearë, është
algoritmi kryesor i algjebrës lineare. Ai zgjidh një sistem, me anë të transformimit të tij,
në një sistem të njëvlershëm me një matricë trekëndëshe të sipërme, i cili mund të
zgjidhet lehtësisht me anë të zëvendësimeve së prapthi. Eliminimi i Gausit shpenzon rreth
1 3
𝑛 shumëzime.
3
• Rregulli i Hornerit është një algoritëm optimal për vlerësimin e një polinomi pa
parapërpunimin e koeficentëve. Ai kërkon vetëm n shumëzime dhe n mbledhje për të
vlerësuar një polinom të shkallës n në një pikë të dhënë. Rregulli i Hornerit ka gjithashtu
disa produkte të rëndësishme si për shembull algoritmi i pjesëtimit sintetik.
• Dy algoritmet e fuqisë binare për llogaritjen e fuqisë an përdorin paraqitjen binare të
eksponentit n, por procesojnë në drejtime të kundërta: nga e djathta në të majtë dhe nga e
majta në të djathtë.

9.5 Ushtrime për kapitullin 9

1. Kujtojmë që vlera mesore e një liste prej n numrash përcaktohet si elementi i ⌈n/2⌉ më i
vogël. Hartoni një algoritëm që llogarit vlerën mesore me një kohë më të mirë sesa ajo
kuadratike dhe përcaktoni klasën e efektshmërisë.

2. Konsideroni problemin për gjetjes e distancës ndërmjet dy numrave më të afërt në një


tabelë e përbërë nga n numra. Distanca ndërmjet dy numrave x dhe y llogaritet si ∣x – y∣.
a. Hartoni një algoritëm të bazuar në pararenditje për zgjidhjen e këtij problemi dhe
përcaktoni klasën e efektshmërisë së tij.
b. Krahasoni efektshmërinë e tij me algoritmin e bazuar në teknikën e forcës brutale.

3. Le të jenë a = {a1, …, an} dhe b = {b1,…, bm} dy tabela me numra të plotë.


Konsideroni problemin për gjetjen e bashkësisë së vlerave që janë si në a ashtu edhe në
b (prerjen e tyre). Për thjeshtësi do të supozojmë që secila tabele nuk ka elemente të
përsëritur në vetvete.
a. Hartoni një algoritëm të mbështetur në teknikën e forcës brutale dhe përcaktoni
klasën e tij të efektshmërisë.
b. Hartoni një algoritëm të mbështetur në teknikën tranformo-dhe-sundo, dhe
përcaktoni klasën e efektshmërisë
9 Teknika transformo-dhe-sundo | 206

4. Konsideroni problemin e gjetjes së elementit më të madh dhe më të vogël në një tabelë të


përbërë nga n numra.
a. Hartoni një algoritëm të bazuar në teknikën transformo-dhe-sundo për zgjidhjen e
këtij problem dhe përcaktoni klasën e efektshmërisë së tij.
b. Krahasoni efektshmërinë e tre algoritmeve: (i) algoritmit të forcës brutale, (ii) të
algoritmit të mbështetur në teknikën transformo-dhe-sundo dhe (iii) algoritmit të
mbështetur në teknikën ndaj-dhe- sundo.

5. Të renditet apo të mos renditet? Hartoni një algoritëm me efektshmëri të mirë për
zgjidhjen e secilit nga problemet e mëposhtme dhe përcaktoni klasën e efektshmëri së tij.
a. Janë dhënë n fatura telefonike dhe m çeqe (mënyrë pagese) për të paguar faturat (n ≥
m). Duke supozuar se numri i telefonit është shkruar në çek, gjeni ata klientë që nuk
kanë paguar faturën. (Për thjeshtësi, do të supozohet se për një faturë është dërguar
vetëm një çek dhe se shuma e shkruar mbulon plotësisht vlerën e faturës.)
b. Në tri tabela ndodhen përkatësisht kodi i studentit, emri i tij dhe qarku ku banon.
Gjeni numrin e studentëve për secilin nga 12 qarqet e vendit.

6. Të zgjidhet sistemi i mëposhtëm i ekuacioneve me anë të eliminimit të Gausit.

𝑥1 + 𝑥2 + 𝑥3 = 2
2𝑥1 + 𝑥2 + 𝑥3 = 3
𝑥1 − 𝑥2 + 3𝑥3 = 8

7. (a) Të zgjidhet sistemi i dhënë në ushtrimin 6 me anë të shpërthimit LU. (b) Parë nga
pikpamja e teknikave të përgjithshme të hartimit të algoritmeve, si do ta klasifikonit
metodën e shpërthimit LU?

8. Zgjidhni sistemin e problemit 6 me anë të llogaritjes të matricës inverse të


koeficentëve të tij dhe shumëzimit të saj më pas, me vektorin e krahut të djathtë.

9. A do të ishte korrekte që të përftohet klasa e efektshmërisë të fazës së eliminimit të


Gausit, llogaritur në lidhje me veprimin e shumëzimit në ciklin më të brendshëm, si më
poshtë?
T(n) = ∑ni=1 ∑nj=i+1 ∑n+1 n−1 n−1
k=i 1 = ∑i=1 (n + 2 − i)(n − i) = ∑i=1 [(n + 2)n − i(2n + 2 +
i2 ] = ∑n−1 n−1 n−1 2
i=1 (n + 2)n − ∑i=1 (2n + 2)i + ∑i=1 i

Meqenëse t1 (n) = ∑n−1 3


i=1 (n + 2)n ∈ Θ(n ), t 2 (n) = ∑n−1 3
i=1 (2n + 2)i ∈ Θ(n ), dhe
t 3 (n) = ∑n−1 2
i=1 i , atëherë t1 (n) − t 2 (n) + t 3 (n) ∈ Θ(n )
3

10. Shkruani me pseudokod fazën e kthimit nga pas të eliminimit të Gausit dhe vlerësoni
kohën e ekzekutimit nëpërmjet veprimit të shumëzimit.

11. Duke supozuar se pjesëtimi i dy numrave zgjat tri herë më shumë se mbledhja e tyre,
vlerësoni se sa më i shpejtë do të jetë eliminimi i përmirësuar i Gausit nga eliminimi i
Gausit.

12. Në lidhje me metodën e eliminimit të Gausit


a. Jepni një shembull të një sistemi me dy ekuacione linearë dhe dy të panjohura që ka
një zgjidhje të vetme dhe zgjidheni atë.
9 Teknika transformo-dhe-sundo | 207

b. Jepni një shembull të një sistemi me dy ekuacione linearë dhe dy të panjohura që nuk
ka zgjidhje dhe dhe zgjidheni atë.
c. Jepni një shembull të një sistemi me dy ekuacione linearë dhe dy të panjohura
që ka një pafundësi zgjidhjesh dhe zgjidheni atë.

13. Një sistem Ax = b me n ekuacione dhe n të panjohura ka një zgjidhje të vetme atëherë
dhe vetëm atëherë kur det (A) ≠ 0. A do të ishte një ide e mirë që të kontrollohet ky kusht
para se të zbatohej metoda e eliminimit të Gausit për zgjidhjen e tij?

14. a. Zbatoni rregullin e Kramerit për të zgjidhur ushtrimin 6.


b. Vlerësoni se sa më gjatë do të vazhdojë zgjidhja e një sistemi me n ekuacione dhe n të
panjohura me rregullin e Kramerit sesa me eliminimin e Gausit (sigurisht që do të
supozohet se përcaktorët në rregullin e Kramerit janë llogaritur pavarësisht metodës
së eliminimit të Gausit).

15. Le të konsiderojmë algoritmin e mëposhtëm të bazuar në teknikën e forcës brutale për


llogaritjen e vlerës së një polinomi në një pikë të caktuar.

// Algoritmi Llogaritja e vlerës së polinomit


// Të dhëna: Tabela P[0..n] përmban n koeficientët e polinomit të regjistruar
// nga fuqia më e ulët deri në fuqinë më të lartë dhe një numër x
// Rezultate: pol, vlera e polinomit në pikën x.
BFPolVlerësim (P, x) {
pol ← 0;
for i ← n to 0 step – 1 do {
exp ← 1;
for j ← to i do
exp ← exp * x;
pol ← pol + p[i]* exp;
}
return pol
}

Gjeni numrin e përgjithshëm të shumëzimeve dhe numrin e përgjithshëm të


mbledhjeve të kryera nga ky algoritëm.

16. Hartoni një algoritëm të bazuar në teknikën e forcës brutale për të llogaritur vlerën e një
polinomi në një pikë të caktuar x duke e filluar vlerësimin nga termi me fuqi më të ulët në
termin me fuqi më të lartë P(x) = a0 + a1x + ….+ an xn. Përcaktoni numrin e shumëzimeve
dhe numrin e mbledhjeve të kryera nga ky algoritëm.

17. Në lidhje me rregullin e Hornerit:


a. Vlerësoni se sa më i shpejtë është algoritmi i rregullit të Hornerit në krahasim me
algoritmin e forcës brutale të hartuar në ushtrimin 15 nëse (i) koha e një
shumëzimi është ndjeshëm më e madhe sesa koha e një mbledhjeje; (ii) koha e një
shumëzimi është afërsisht e njëjtë me kohën e një mbledhjeje?
b. A është algoritmi i rregullit Horner më i efektshëm në kohë mbasi ai kërkon më pak
hapsirë sesa algoritmi i forcës brutale?

18. Në lidhje me rregullin e Hornerit:


9 Teknika transformo-dhe-sundo | 208

a. Të zbatohet rregulli i Hornerit për të llogaritur vlerën e polinomit


P(x) = 3x4 – x3 + 2x + 5 në pikën x = – 2.
b. Përdor rezultatet e zbatimit të rregullit Horner për shembullin e mësipërm për të gjetur
herësin dhe mbetjen e pjesëtimit të P(x) me x + 2.

19. Në lidhje me algoritmin e fuqisë binare nga e majta në të djathtë.


a. Zbatoni algoritmin për të llogaritur 217.
b. A është e mundur të zgjerohet algoritmi për çdo eksponent të plotë jonegativ?

20. Zbatoni algoritmin e Fuqisë binare nga e djathta në të majtë për të llogaritur 217.

21. Hartoni një algoritëm jorekursiv për llogaritjen e an që imiton fuqinë binare nga e djathta
në të majtë por që nuk përdor në mënyrë të shtjellur paraqitjen binare të n.

22. A është një ide e mirë që për vlerësimin e polinomeve të trajtës P(x) = xn +xn-1 + …+ x+ 1
të përdoren algoritme me qëllime të gjithanshme siç është rregulli Horner?

23. Le të supozojmë se jepet një numër i plotë pozitiv n. Të gjendet çifti numrave të plotë
shuma të cilëve është n dhe produkti i tyre të jetë sa më i madh që të jetë i mundur.
Hartoni një algoritëm të efektshëm për të zgjidhur këtë problem dhe tregoni klasën e
efektshmërisë së tij.
10 Kompromisi hapsirë-kohë | 209

10 Kompromisi hapsirë−kohë

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të njohë teknikën e kompromisit hapsirë-kohë dhe ta përdorë për problemin e renditjes;


• të dallojë kushtet e përdorimit të kompromisit hapsirë-kohë;
• të përdorë kompromisin hapsirë-kohë në problemin e kërkimit të motivit;
• të dallojë ndryshimet ndërmjet teknikës së forcës brutale dhe kompromisit hapsirë-kohë
për zgjidhjen e problemit të kërkimit të motivit.

Kompromisi ndërmjet hapsirës për memorizimin e dhënave dhe kohës së ekzekutimit (space
and time tradeoffs) në hartimin e algoritmeve njihet mjaft mirë si nga teoricienët ashtu edhe
nga prakticienët e informatikës. Le të konsiderojmë si një shembull problemin e llogaritjes së
vlerave të një funksioni në shumë pika brenda fushës së tij të përcaktimit. Në qoftë se na
duhet që këtë punë ta bëjmë sa më shpejt (koha është e vlefshme) atëherë ne mund të
llogaritim paraprakisht vlerat e funksionit në një bashkësi diskrete brenda fushës së
përcaktimit dhe i rezervojmë ato në një tabelë. Kjo ishte edhe rruga që ndiqte njeriu para se të
shpikeshin kompjuterat duke mbushur bibliotekat me volume librash me tabela matematike.
Megjithëse tabela të tilla tashmë e kanë humbur joshjen përshkak të përdorimit të gjerë të
kompjuterave, idea në të cilat u mbështet krijimi i tyre ishte mjaft e dobishme për zhvillimin
e disa algoritmeve të rëndësishëm për probleme të tjera.

Në formë të përgjithshme idea është që të parapërpunohen të dhënat fillestare, të tëra ose një
pjesë e tyre, dhe të regjistrohet informacioni shtesë i përfituar për të shpejtuar zgjidhjen më
pas të problemit. Kjo mënyrë veprimi quhet shtimi i të dhënave fillestare 17 (input
enhancement) dhe për ta mbështetur atë do të paraqitim algoritmet e mëposhtme:

• metodat e renditjes me anë të numërimit (counting methods for sorting).


• algoritmi i Horspool-it për problemin e kërkimit të motivit

Vemë ne dukje që dy burimet, hapsira dhe koha, nuk hyjnë në konkurrencë me njëra tjetrën
në asnjë nga teknikat e hartimit të algoritmeve. Në të vërtetë ato tentojnë të sjellin një
zgjidhje algoritmike që minimizon si kohën e ekzekutimit ashtu edhe hapsirën e përdorur.
Situata të tilla ndodhin, në veçanti, atëherë kur një algoritëm përdor një strukturë të të
dhënave të efektshme për të paraqitur të dhënat fillestare e cila nga ana e saj të shpie në një
algoritëm më të thjeshtë. Për shembull, kjo situatë ndodh kur manipulojmë matrica të ralla
dhe polinome të rallë, në të cilët pjesa më e madhe e elementeve është zero. Në qoftë se
përqindja e zerove është mjaftueshmërisht e lartë ne mund të kursejmë si hapsirën ashtu edhe
kohën duke mos marrë parasysh vlerat zero në rezervimin e të dhënave dhe në përpunimin e
tyre.

10.1 Renditja me anë të numërimit


17
Terma të tjerë që përdoren për të emërtuar këtë teknikë janë edhe parapërpunim ose parakushtëzim. Në
mënyrë që të mos krijohet konfuzion me para përpunimin që kemi parë në teknikën e transformimit ku nuk
përdoret idea e hapsirës shtesë, këtu do ta quajmë “shtim i të dhënave fillestare” për të dhënë idenë e përdorimit
të hapsirës shtesë.
10 Kompromisi hapsirë-kohë | 210

Problemi i parë që do të trajtojmë, mbështetur në idenë e shtimit të të dhënave fillestare është


problemi tashmë i njohur i renditjes. Vlerat janë numra të plotë. Idea qëndrore e algoritmit
është që, për çdo element të llogaritet sasia e elementeve më të vegjël se ai. Këta numra do të
shërbejnë për të treguar pozicionin e elementeve në tabelën e renditur. Për shembull në qoftë
se për ndonjë element të caktuar ky numër është 10, atëherë ai duhet të ndodhet ne pozicionin
e 11të (në pozicionin 10, nëse tabela është indeksuar nga 0) në tabelën e renditur. Kështu që
do të jemi në gjendje të renditim tabelën e të dhënave fillestare thjesht duke kopjuar
elementet e saj në një pozicion të përshtatshëm në një tabelë tjetër. Ky algoritëm quhet
renditja me anë të numërimit të krahasimeve (comparison counting sort),

Indekset
Pozicionet 0 1 2 3 4 5
Të dhënat A[0:5] 62 31 84 96 19 47
tabela Count
fillimi 0 0 0 0 0 0
Pas hapit i = 0 3 0 1 1 0 0
Pas hapit i = 1 1 2 2 0 1
Pas hapit i = 2 4 3 0 1
Pas hapit i = 3 5 0 1
Pas hapit i = 4 0 2
Gjendja e fundit 3 1 4 5 0 2

Tabela R[0..5] 19 31 47 62 84 96
Figura 10.1 Shembull i ekzekutimit të algoritmit të renditjes me anë të numërimit të
krahasimeve

// Algoritmi 10.1 Renditja me anë të numërimit të krahasimeve


// Të dhëna: një tabelë a[0..n - 1] me n numra të plotë
// Rezultate: një tabelë r[0..n - 1] me elementet e A por e renditur

countingSort (a, r, n){


// inicializimi i tabelës numrator të krahasimeve
for i ← 0 to n–1 do
count[i] ← 0;
// numërimi i krahasimeve
for i ← 0 to n – 2 do
for j ← i +1 to n – 1 do
if (a[i] < a[j])
then count[j] ← count[j] + 1
else count[i] ← count[i] + 1;
// përftimi i tabelës së renditur, r
for i ← 0 to n – 1 do
r[count[i]] ← a[i];
return
}

Cila është efektshmëria kohore e këtij algoritmi? Duhet të jetë kuadratike sepse algoritmi
merr në konsideratë të gjitha çiftet dyshe të një tabelë me n-elemente. Më formalisht, numri i
herëve që ekzekutohet veprimi i tij themelor, krahasimi a[i] < a[j], përcaktohet nga formula:
10 Kompromisi hapsirë-kohë | 211

n−2 n−1 n−2 n−2 (n − 1)n


K(n) = � � 1= � [(n − 1) − (i + 1) + 1] = � (n − 1 + i) =
i=0 j=i+1 i=0 i=0 2

Përderisa algoritmi kryen të njëjtin numër krahasimesh të elementeve si algoritmi Selection


sort dhe veç kësaj, ai përdor një hapsirë shtesë të rendit n është e vështirë se mund të
përdoret për qëllime praktike.

Por idea e numërimit funksionon me efektshmëri në situatat kur vlerat që do të renditen


shtrihen në një segment të ngushtë. Le të supozojmë, për shembull, se vlerat janë 1 dhe 2. Më
mirë se të zbatojmë një algoritëm të përgjithshëm, ne duhet të përfitojmë nga informacioni
shtesë që kemi. Me të vërtetë, mund të skanojmë listën e elementeve dhe të numërojmë sa
vlera janë 1 dhe sa vlera janë 2. Pastaj mbushim tabelën e të dhënave me aq vlera 1 sa janë
dhe pjesën që mbetet e mbushim me vlerën 2.

Akoma më shumë, nëse elementet e tabelës së të dhënave janë numra të plotë te


njëpasnjëshëm në segmentin [m, d], ne mund të llogaritim shpeshtësinë e secilës vlerë dhe e
rezervojmë në një tabelë F[0.. d – m]. Pastaj, F[0] pozicionet e para mbushen me vlerën m,
më tej F[1] pozicionet pasardhëse mbushen me vlerën m+1, e kështu me radhë. Natyrisht, e
gjithë kjo mund të kryhet vetëm nëse mbishkruajmë mbi elementet e dhënë.

Le të shohim tani një situatë më reale për renditjen e një tabele duke patur edhe një
informacion shtesë për vlerat, dhe pikërisht atë që elementet janë numra të plotë të përfshirë
në segmentin [m, d] dhe ndërkaq nuk do mbishkruajmë mbi tabelën e të dhënave. Ne mund të
llogaritim shpeshtësinë e secilës prej vlerave dhe ta rezervojmë në një tabelë F[0, d – m]. Më
pas llogariten shpeshtësitë e grumbulluara (kumulative) të çdo elementi që është e barabartë
me shpeshtësinë grumbulluese të elementit të mëparshëm shtuar me shpeshtësinë e elementit
të radhës. Tabela rezultat, R[0..n – 1] do të formohet me ndihmën e tabelës së shpeshtësive të
grumbulluara. Vlerat e shpeshtësive të grumbulluara tregojnë pozicionin e vërtetë për
përsëritjen e fundit të elementeve të saj në tabelën përfundimtare të renditur. Nëse tabela e të
dhënave është e indeksuar nga 0 tek n – 1, shpeshtësitë e grumbulluara duhet të zvogëlohen
me 1 për të përftuar pozicionin korrespondues të elementit në tabelën e re të renditur.
Përderisa shuma të tilla të grumbulluara të shpeshtësive në statistikë quhen shpërndarje
(distribution), metoda vetë njihet si algoritmi i renditjes me anë të llogaritjes së shpërndarjes
(distribution counting 18).

Shembull. Le të konsiderojmë renditjen e vlerave {13, 11, 12, 13, 12, 12} të rezervuara në
një tabelë.

Vihet re që elementet e tabelës janë të formuar nga numrat {11, 12, 13}. Shpeshtësitë dhe
shpeshtësitë e grumbulluara janë si më poshtë:

Vlerat e mundshme në tabelë 11 12 13


Shpeshtësitë 1 3 2
Shpeshtësitë e grumbulluara 1 4 6

Vemë në dukje që shpeshtësitë e grumbulluara tregojnë pozicionin e saktë të shfaqjes së


fundit të elementeve në tabelën e renditur. Nëse e indeksojmë tabelën nga 0 në n – 1,

18
Algoritmi është krijuar nga Harold H. Seward in 1954
10 Kompromisi hapsirë-kohë | 212

Shpeshtësia e grumbulluar duhet të zvogëlohet me 1 për të përftuar pozicionin e elementit


korrespondues.

Do të ishte më e përshtatshme të fillojmë përpunimin e tabelës së të dhënave nga e djathta në


të majtë (nga elementi i fundit). Për shembull, elementi i fundit është 12 dhe përderisa vlera e
tij e shpërndarjes është 4 ne e vendosim këtë vlerë 12 në pozicionin 4 – 1 = 3 të tabelës r që
do të mbajë vlerat e renditura. Pastaj zvogëlojmë me 1 shpeshtësinë e grumbulluar që u
përpunua (vlera e 12të e shpërndarjes) dhe procedojmë me elementin tjetër (nga e djathta) të
tabelës së dhënë. Dinamika e përpunimit të këtij shembulli paraqitet në figurën 10.2.

Shpeshtësitë Tabela rezultat


Të dhënat FG[0] FG[1] FG[2] R[0] R[1] R[2] R[3] R[4] R[5]
A[5] = 12 1 4 6 12
A[4] = 12 1 3 6 12
A[3] = 13 1 2 6 13
A[2] = 12 1 2 5 12
A[1] = 11 1 1 5 11
A[0] = 13 0 1 5 13
Figura 10.2 Shembull i renditjes me anë të llogaritjes së shpërndarjes

Më poshtë paraqitet algoritmi i renditjes me anë të llogaritjes së shpërndarjes.

Algoritmi 10.2 Algoritmi i renditjes me anë të llogaritjes së shpërndarjes


// Të dhëna: një tabelë a[0..n – 1] me numra të plotë ndërmjet m dhe d (m ≤ d)
// Rezultate: një tabelë r[0..n – 1] e renditur, me elementet e tabelës a

distributionCounting(n, a, r, m, d) {

// inicializim i tabelës së shpeshtësive të grumbulluara


for j ← 0 to d – m do
fg[j] ← 0;

// llogaritja e shpeshtësive të çdo vlere


for i ← 0 to n – 1 do
fg[a[i] – m] ← fg[a[i] – m] + 1;

// llogaritja e shpeshtësive të grumbulluara


for j← 1 to d – m do
fg[j] ← fg[j – 1] + fg[j];

// shpërndarja e vlerave në tabelën rezultat


for i ← n – 1 to 0 step –1 do
{j ← a[i] – m; // indeksi i llogaritur
r[fg[j] – 1] ← a[i];
fg[j] ← fg[j] – 1;}
return
}
10 Kompromisi hapsirë-kohë | 213

Në bazë të supozimit që fushështrirja e vlerave është e vogël por edhe e fiksuar, është e qartë
se kemi të bëjmë me një algoritëm me kohë ekzekutimi lineare. Kjo është koha më e mirë për
renditje në krahasim me algoritmet që kemi parë deri tani. Por të mos harojmë se kjo
efektshmëri, përveç kompromisit hapsirë - kohë, është pasojë e shfrytëzimit të cilësive të
veçanta të listës së të dhënave mbi të cilat punon algoritmi.

10.2 Problemi i kërkimit të motivit

Në këtë seksion do të shohim se si teknika e shtimit të të dhënave mund të zbatohet në


problemin e kërkimit të motivit. Kujtojmë që problemi i kërkimit të motivit ka të bëjë me
gjetjen e një stringu të dhënë prej m karakteresh, motivi (pattern), në një string të gjatë prej n
karakteresh, të quajtur tekst (text).

Ne kemi diskutuar algoritmin e forcës brutale për këtë problem: ai thjesht krahason çiftet
korresponduese të karaktereve të motivit dhe të tekstit nga e majta në të djathtë dhe nëse
ndodh një mospërputhje motivi zhvendoset një karakter në të djathtë të tekstit për provën
tjetër. Meqenëse numri maksimal i provave të tilla është n – m + 1 dhe në rastin më të keq,
duhen m krahasime për secilën prej këtyre provave, atëherë për rastin më të keq numri i
krahasimeve të karaktereve është m(n – m +1). Kjo e vendos performancën e rastit më të keq
të algoritmit të forcës brutale në klasën Θ(𝑛𝑛). Megjithatë, në rastin e kërkimit në tekste të
shkruar me gjuhët natyrale, efektshmëria në rastin mesatar përfshihet në klasën Θ(𝑛).

Efektshmëria e mirë e algoritmit të forcës brutale në rastin mesatar mund të quhet edhe lajm i
mirë edhe lajm i keq. Ai është lajm i mirë nga pikëpamja praktike mbasi ai e bën zgjidhjen e
forcës brutale një kandidat të pranueshen për zbatime praktike (në mënyrë të veçantë për
motive të shkurtër). Ai është një lajm i keq për një teoricien që do të shpikë një algoritëm më
të shpejtë. Megjithatë janë shpikur disa algoritme më të mirë. Shumë prej tyre përdorin idenë
e shtimit të të dhënave fillestare: një parapërpunim i motivit për të përftuar një informacion
mbi të, rezervim në një tabelë i këtij informacioni dhe pastaj përdorimi i këtij informacioni
gjatë kërkimit të motivit në një tekst të dhënë. Kjo është edhe idea që qëndron pas dy
algoritmeve më të mirë që njihen për këtë tip problemi: algoritmi i Knuth-Morris-Pratt-it dhe
algoritmi i Boyer-Moore-it.

Megjithëse idea që qëndron në bazë të algoritmit të Boyer-Moore-it është e thjeshtë, zbatimi i


saj ne një program nuk është i tillë. Prandaj ne do të trajtojmë një version të thjeshtuar të
algoritmit të Boyer-Moore-it të propozuar nga R. Horspool. Përveç se është më i thjeshtë,
algoritmi i Horspool-it nuk është më pak i efektshëm sesa algoritmi i Boyer-Moore-it për të
dhëna të rastit.

Algoritmi i Horspool-it

Le të konsiderojmë si shembull kërkimin e motivit (fjalë në këtë rast) PEMA në një tekst:

t0 … c … tn-1
P E M A

Duke filluar nga shkronja e fundit, A, e motivit dhe duke u zhvendosur nga e djathta në të
majtë, ne krahasojmë çiftet korresponduese të karaktereve të motivit dhe të tekstit. Në qoftë
se të gjithë karakteret e motivit përputhen atëherë ky motiv gjendet. (Në varësi të problemit,
kërkimi ose mund të ndalohet ose mund të vazhdojë nëse duam të gjejmë një shfaqje tjetër të
10 Kompromisi hapsirë-kohë | 214

motivit.) Ndërkaq, në qoftë se takohet një mospërputhje, ne duhet të zhvendosim motivin në


të djathtë. E qartë se do të dëshëronim të bënim një zhvendosje aq të “madhe” pa patur frikë
se mund të humbasim një gjetje të motivit në tekst. Pikërisht, algoritmi i Horspool-it
përcakton madhësinë e një zhvendosjeje të tillë duke vështruar në karakterin c të tekstit që
është radhitur përkundrejt karakterit të fundit të motivit. Mund të ndodhin katër rastet e
mëposhtme:

Rasti 1. Në motiv nuk ekziston karakteri i kërkuar. Për shembull, shkronja T nuk ndodhet në
motivin PEMA. Prandaj në këtë rast motivin me siguri, mund ta zhvendosim sa gjithë gjatësia
e tij për të kërkuar një përputhje

t0 … T … tn-1
P E M A rradhitja aktuale
P E M A rradhitja pas zhvendosjes

Rasti 2. Në motiv ekziton karakteri i kërkuar, ndoshta me tepër se 1 herë, por ai nuk është i
fundit prej tyre, për shembull, shkronja P në motivin PLEPI. Në këtë rast motivin duhet ta
zhvendosim në mënyrë të tillë që shfaqja më e djathtë e karakterit P në motiv të jetë nën
karakterin e kërkuar të tekstit:

t0 … P … tn-1
P L E P I rradhitja aktuale
P L E P I rradhitja pas zhvendosjes

Rasti 3. Në motiv ekziston karakteri i kërkuar, është i vetëm dhe është i fundit, për shembull,
shkronja A në motivin PEMA. Në këtë rast motivin e zhvendosim si Rasti 1, domethënë, sa
është numri i karaktereve të tij:

t0 … T M A … tn-1
P E M A rradhitja aktuale
P E M A rradhitja pas zhvendosjes

Rasti 4. Në motiv ekziston më tepër se një karakter i kerkuar dhe ndërkaq njëri prej tyre është
i fundit, për shembull, shkronja I në motivin LISI. Në këtë rast zhvendosja duhet të jetë e
ngjashme me Rasti 2: shfaqja më e djathtë e karakterit pa përfshirë shfaqjen e fundit duhet të
radhitet me tekstin në I:

t0 … K I … tn-1
L I S I rradhitja aktuale
L I S I rradhitja pas zhvendosjes

Këta shembuj ndihmojnë për të kuptuar se krahasimi i karaktereve nga e djathta në të majtë
mund të shpjerë në zhvendosje më të largëta të motivit sesa ato vetëm me një pozicion të
algoritmit të forcës brutale. Megjithatë, nëse një algoritëm i tillë do të duhet të kontrollojë të
gjithë karakteret e motivit në çdo provë, ai duhet të humbasë shumë nga superioriteti i tij. Për
fat të mirë, idea e shtimit të të dhënave fillestare i bën të panevojshme krahasimet e
përsëritura. Për një motiv të caktuar, ne mund të llogaritim në mënyrë paraprake madhësitë e
zhvendosjeve dhe t‘i vendosim ato në një tabelë të një lloji të veçantë, të quajtur tabela e
zhvendosjeve të motivit.
10 Kompromisi hapsirë-kohë | 215

Në ndryshim nga tabelat që kemi parë deri tani, ku si indeks shërbejnë numra të plotë, në këtë
tabelë si indeks shërbejnë të gjithë karakteret individuale që mund të takohen në një tekst,
duke përfshirë për tekstet e gjuhës natyrale edhe karakterin hapsirë (space), shenjat e
pikësimit, dhe karakteret e tjera të veçanta. Përmasa e kësaj tabele është aq sa ç’është sasia e
karaktereve që përbëjnë alfabetin. Vemë në dukje se informacioni për zhvendosjet është i
mjaftueshëm për të zgjidhur problemin. Vlerat e elementeve të tabelës së zhvendosjeve, që do
të tregojnë përmasat e zhvendosjeve, llogariten me anë të formulës:

𝑔𝑔𝑔𝑔ë𝑠𝑠𝑠 𝑚 𝑒 𝑚𝑚𝑚𝑚𝑚𝑚𝑚 𝑛𝑒̈ 𝑠𝑠 𝑐 𝑛𝑛𝑛 𝑒̈ 𝑠ℎ𝑡𝑒̈ 𝑛𝑛𝑒̈ 𝑟



⎪ 𝑚 − 1 𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘 𝑒 𝑝𝑝𝑝𝑒̈ 𝑡𝑒̈ 𝑡𝑡𝑡
Z(c)= 𝑙𝑙𝑙𝑙𝑙𝑙𝑙 𝑞𝑒̈ 𝑘𝑘 𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘 𝑐 𝑚𝑒̈ 𝑖 𝑑𝑑𝑑𝑑ℎ𝑡𝑒̈ (10.1)
⎨ 𝑛𝑒̈ 𝑠𝑠 𝑐 𝑒̈ 𝑠ℎ𝑡𝑒̈ 𝑛𝑛𝑒̈ 𝑟 𝑚 − 1
⎪ 𝑛𝑛𝑒̈ 𝑟 𝑚 − 1 𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘 𝑒 𝑝𝑝𝑝𝑒̈ 𝑡𝑒̈ 𝑚𝑚𝑚𝑚𝑚𝑚𝑚 𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘 𝑒 𝑝𝑝𝑝𝑒̈ 𝑡𝑒̈ 𝑚𝑚𝑚𝑚𝑚𝑚𝑚
⎩ 𝑞𝑒̈ 𝑛𝑛𝑛 𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘𝑘 𝑖 𝑓𝑓𝑓𝑓𝑓𝑓 𝑖 𝑡𝑡𝑡

Për shembull, për motivin BAOBAB, të gjithë elementet e tabelës të zhvendosjeve do të jenë
të barabartë me 6, me përjashtim të vlerave për karakteret A, B dhe O që do të jenë
respektivisht 1, 2, 3 si më poshtë:

Karakteri c A B C ∙∙∙ O ∙∙∙


Madhësia e zhvendosjes Z(c) 1 2 6 6 3 6

Më poshtë paraqitet një algoritëm i thjeshtë që shërben për të llogaritur vlerat e elementeve të
tabelës së zhvendosjes. Në fillim inicializohen të gjithë elementet e tabelës së zhvendosjeve
me madhësinë e motivit, m dhe skanohet motivi nga e majta në të djathtë duke përsëritur këtë
hap m – 1 herë: për karakterin e jtë të motivit (0 ≤ j ≤ m – 2), të mbishkruhet vlera në tabelë
me m – 1 – j, e cila është largesa e karakterit nga skaji i djathtë i motivit. Vemë në dukje se
meqë algoritmi skanon motivin nga e majta në të djathtë, mbishkrimi i fundit do të ndodhë
për një karakter të shfaqjes më të djathtë, tamam siç do të dëshëronim të ndodhte.

Algoritmi 10.3 Krijimi i tabelës së zhvendosjeve


// Të dhëna: Motivi p[0..m − 1] dhe z, një alfabet me karakteret e mundshme
// Rezultate: z[0..size −1 ], tabela e zhvendosjeve, e indeksuar me karakteret e alfabetit
// dhe e mbushur me madhësitë e zhvendosjeve të llogaritura me anë të
// formulës (10.1)

tabelaZhvendosjes(m, p, z) {
Inicializimi i të gjithë elementeve të tabelës z me vlerë m, sa gjatësia e motivit
for j ← 0 to m − 2 do
z[p[j]] ← m −1 − j
return
}

Tani jemi në gjendje që të skicojmë algoritmin e Horspool-it si më poshtë.

// Algoritmi 10.4 Algoritmi Horspool për kërkim motivi


// Të dhëna: Motivi p[0.. m - 1] dhe teksti t[0..n-1]
// Rezultate: Indeksi i skajit të majtë në tekst ku fillon motivi ose -1 nëse nuk ka përputhje

horspool(m, p, n, t) {
tabelaZhvendosjes ( m, p, z); // krijohet tabela e zhvendosjeve
10 Kompromisi hapsirë-kohë | 216

i ← m – 1;
while i ≤ n – m +1 do {
k ← 0; //k, numri i karaktereve që përputhen
while (k ≤ m – 1 and p[m – 1 – k] = t[i – k]) do
k ← k + 1;
if k = m
then return i – m + 1
else i ← i + z[t[i]] };
return –1
}

Shembull. Si një shembull të një zbatimi të plotë të algoritmit të Horspool-it, le të


konsiderojmë kërkimin e motivit BAOBAB në tekstin PEMA BAOBAB RRITET NE AFRIKE,
(Shenja e hapsirës për qartësi përfaqësohet nga shenja minus — ).

Tabela e zhvendosjeve paraqitet më poshtë:

Karakteri i alfabetit A B C ∙∙∙ O ∙∙∙ —


Madhësia e zhvendosjes Z(c) 1 2 6 6 3 6 6

Në figurën 10.3 paraqiten hapat e kërkimit. Përputhja ndodh në zhvendosjen e dytë të motivit.

P E M A — B A O B A B — R R I T E T — N E — A F R I K E

B A O B A B
B A O B A B
B A O B A B
Figura 10.3 Shembull i një kërkimi me algoritmin Horspool

Efektshmëria e algoritmit të Horspool-it, për rastin më të keq të organizimit të të dhënave


fillestare, është Θ(𝑛𝑛). Por për tekste të rastit, ajo është Θ(n), njëlloj algoritmi i forcës
brutale. Siç u përmend më lart ai shpesh është të paktën po aq i efektshëm sa paraardhësi i tij
i zbuluar nga R. Boyer dhe J. Moore.

10.3 Përmbledhje

• Kompromisi ndërmjet hapsirës dhe kohës në hartimin e algoritmeve është një çështje që
njihet mirë si nga teoricienët ashtu edhe nga prakticienët e informatikës. Si një teknikë
për hartimin e algoritmeve, kompromisi hapsirë-kohë anon nga ana e hapsirës.
• Shtimi i të dhënave fillestare është një nga versionet kryesore të kompromisit hapsirë për
kohë. Idea e tij është që të parapërpunohet problemi i të dhënave fillestare, tërësisht apo
pjesërisht, dhe të rezervohet informacioni i përftuar në mënyrë që të përshpejtohet
zgjidhja e problemit më pas. Renditja me anë të llogaritjes së shpërndarjes dhe disa
algoritme të rëndësishme për kërkimin e motivit janë shembuj të algoritmeve të
mbështetura në këtë teknikë.
• Llogaritja e shpërndarjes është një metodë e veçantë për të renditur një listë elementesh
me vlera nga një bashkësi e vogël.
10 Kompromisi hapsirë-kohë | 217

• Algoritmi i Horspool-it për problemin e kërkimit të motivit mund të konsiderohet si një


version i thjeshtuar i algoritmit të Boyer-Moore-it. Algoritmi mbeshtet në idenë e shtimit
të të dhënave fillestare duke krijuar tabelën e zhvendosjeve dhe në krahasimin nga e
djathta në të majtë të karaktereve të një motivi.

10.4 Ushtrime për kapitullin 10

1. A është e mundshme të përkëmbehen vlerat numerike të dy variablave, le të themi, u dhe


v, pa përdorur variabël ndërmjetës?

2. A do të funksionojë në mënyrë korrekte algoritmi i renditjes me anë të numërimit të


krahasimeve (Comparison counting) për tabela me vlera të njëjta?

3. Duke supozuar se bashkësia e vlerave të mundshme është bashkësia {a, b, c, d}, të


renditet lista e mëposhtme në rendin alfabetik me anë të algoritmit të llogaritjes së
shpërndarjes (Distribution Counting):

b, c, d, c, b, a, a, b

4. A është i qëndrueshëm algoritmi i llogaritjes së shpërndarjes?

5. Jepet një tabele me n numra të plotë, vlerat e elemeteve të të cilës janë numrat nga 1 deri
ne n. Hartoni një algoritëm të përbërë nga një cikël i vetëm që rendit tabelën.

6. Jepet një tabele A me n numra plotë të ndryshëm nga zero. Të hartohet një algoritëm, i
ndryshëm nga Distribution Counting, i efektshëm në kohë që vendos të gjithë elementet
negativë para atyre pozitivë. Llogaritni kompleksitetin kohor dhe hapsinor të algoritmit të
projektuar.

7. Zbatoni algoritmin e Horspool-it për të kërkuar motivin BAOBAB në tekstin


BESS—KNEW—ABOUT—BAOBABS

8. Le të konsiderojmë problemin e kërkimit për gjenet në një sekuencë të ADN-së duke


përdorur algoritmin e Horspool-it. Një ADN paraqitet si një tekst në alfabetin {A, C, G,
T} dhe gjeni apo segmenti i gjenit është motivi.
a. Të ndërtohet tabela e zhvendosjeve për segmentin e mëposhtëm të gjenit për
kromozomin 10: TCCTATTCTT.
b. Të zbatohet algoritmi Horspool për të lokalizuar motivin e mësipërm në sekuencën
e mëposhtme të ADN-së:
TTATAGATCTCGTATTCTTTTATAGATCTCCTATTCTT
9. Sa krahasime të karaktereve do të kruhen nga algoritmi i Horspool-it për kërkimin e
secilit nga motivet e mëposhtme në një tekst të përbërë nga një mijë zero?
a. 00001 b. 10000 c. 01010

10. Për kërkimin në një tekst me gjatësi n të një motivi me gjatësi m (n ≥ m) me


algoritmin e Horspool-it, jepni një shembull për të dhëna për (i) rastin më të keq dhe (ii)
për rastin më të mirë.
10 Kompromisi hapsirë-kohë | 218

11. A është e mundshme që algoritmi i Horspool-it të bëjë më shumë krahasime të


shenjave se algoritmi i forcës brutale për kërkimin e të njëjtit motiv në të njëjtin tekst?

12. Nëse algoritmi i Horspool-it zbulon një motiv që përputhet, sa e madhe duhet të jetë
zhvendosja që duhet të bëhet për të kërkuar një përputhje tjetër?
11 Programimi dinamik | 219

11 Programimi dinamik

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të kuptojë kushtet e përdorimit te teknikes se programimit dinamik;


• të përdorë teknikën e programimit dinamik për disa probleme optimizimi;
• të bëjë dallimin ndërmjet teknikes ndaj-dhe-sundo dhe programimit dinamik;
• të përdorë teknikën e programimit dinamik për problemin e çantës së shpinës.

Programimi dinamik (Dynamic programming) është një teknikë e përgjithëshme për hartimin
e algoritmeve me një histori mjaft interesante. Ajo është shpikur nga matematikani i njohur
amerikan Richard Bellman në vitin 1950, si një metodë e përgjithëshme për optimizimin e
proceseve të vendimarrjes me shumë faza. Fjala “programming” në emërtimin e teknikës
nënkupton planifikim (“planning”) dhe nuk i referohet programimit në kompjuter. Pas
sprovës që kaloi si një mjet i fuqishëm në matematikën e zbatuar, programimi dinamik
konsiderohet, të paktën në qarqet e informatikës, si një metodë e përgjithëshme për hartimin e
algoritmeve për problemet e optimizimit, pjesa më e madhe e të cilave janë probleme të
patrajtueshëm (me kompleksitet eksponencial). Ndërkaq, zbatimi nuk kufizohet vetëm në
problemet e optimizimit.

Programimi dinamik shërben për zgjidhjen e problemeve me nënprobleme të mbivendosura,


optimizues ose jo-optimizues. Në mënyrë tipike, nënprobleme të tilla krijohen nga një
rekurrencë që lidh një zgjidhje të një problemi të dhënë me zgjidhjet e nënproblemeve të tij
më të vegjël. Në vend që të zgjidhë nënproblemet që mbivendosen përsëri dhe përsëri,
programimi dinamik sugjeron që secilin nga problemet më të vegjël ta zgjidhë vetëm një herë
dhe t’i regjistrojë rezultatet në një tabelë, prej së cilës mund të përftohet zgjidhja e problemit
origjinal.

Për të ilustruar teknikën le të rishohim edhe një herë llogaritjen e termit të ntë të vargut
Fibonaçi:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ⋯

të cilët mund të përcaktohen nëpërmjet rekurrencës së thjeshtë

F(n) = F(n - 1) + F(n - 2) për n > 1 (11.1)

dhe dy konditave fillestare

F(0) = 0, F(1) = 1. (11.2)

Nëse përpiqemi të përdorim rekurrencën (11.1) në mënyrë të drejtpërdrejtë për të llogaritur


numrin e ntë të Fibonaçit F(n), duhet të rillogarisim të njëjtat vlera të këtij funksioni shumë
herë. Vëmë në dukje se llogaritja e F(n) shprehet si funksion i nënproblemeve të tij më të
vegjël dhe të mbivendosur F(n - 1) dhe F(n - 2).

Si një rrugë alternative, mund të mbushim me elementet tashmë të llogaritur, nëpërmjet një
cikli shumë të thjeshtë, një tabelë një dimensionale me n + 1 vlerat e njëpasnjëshme të F(n),
11 Programimi dinamik | 220

duke filluar me konditat e dhëna fillestare (11.2) dhe përdorimin e ekuacionit (11.1) për
prodhimin e vlerave të tjera. Është e qartë se elementi i fundit i kësaj tabele do të përmbajë
vlerën F(n).

Algoritmi 11.1 Llogaritja e numrave të Fibonaçit, (version iterativ me tabelë)


// Të dhëna: n, një numër i plotë jonegativ
// Rezultate: f[0..n] një tabelë që përmban numrat e Fibonaçit

llogaritFibonaciMeTabele (n, f) {
f[0] ← 0;
f[1] ← 1;
for i ← 2 to n do
f[i] ← f[i − 1] + f[i − 2];
return
}

Vëmë në dukje se mundemi, që për llogaritjen e numrit të ntë të Fibonaçit, të shmangim


përdorimin edhe të tabelës shtesë duke mbajtur vetëm gjurmën e dy elementeve të fundit të
vargut. Ky fenomen nuk është i rrallë dhe takohet edhe në disa probleme të tjera të
programimit dinamik. Kështu, megjithëse një zbatim i drejtëpërdrejtë i programimit dinamik
mund të interpretohet si një version i veçantë i kopromisit hapsirë−kohë, një algoritëm i
teknikës së programimit dinamik shpesh herë mund të lëmohet për të shmangur përdorimin e
hapsirës shtesë.

Algoritmi 11.2 Llogaritja e termit të ntë të vargut Fibonaçi (version pa tabelë)


// Të dhëna: n ≥ 0
// Rezultate: final, termi i ntë i vargut Fibonaçi

llogaritFibonaciPaTabele(n) {
fibPara ← 0;
fibPas ← 1;
fibFinal ← 1;
for i ← 3 to n do {
fibPara ← fibPas;
FibPas ← fibFinal;
fibFinal ← fibPara + fibPas;
}
return fibFinal
}

Përveç sa më sipër, ekzistojnë edhe disa algoritme që mbështeten në metoda të tjera, që


llogarisin elementin e ntë të vargut Fibonaçit pa llogaritur të gjithë elementet e mëparshëm të
vargut. Por është tipike që në një algoritëm që bazohet në metodën klasike nga poshtë-lart të
programimit dinamik, të zgjidhen të gjithë nënproblemet më të vegjël të një problemi të
dhënë. Një version i teknikës së programimit dinamik përpiqet të shmangë zgjidhjen e
nënproblemeve të panevojshëm. Kjo teknikë, përdor të ashtuquajturit funksione memorizues
(memory function) dhe mund të konsiderohet si versioni nga lart-poshtë i programimit
dinamik.
11 Programimi dinamik | 221

Qoftë kur përdorim versionin klasik iterativ nga poshtë-lart të programimit dinamik qoftë kur
përdorim versionin rekursiv nga lart-poshtë, hapi më i rëndësishëm në konceptimin e një
algoritmi të programimit dinamik është i njëjti: dhe pikërisht nxjerrja e një relacioni
rekurrencial që lidh zgjidhjen e një problemi me nënproblemet e tij më të vogla. Rastet, kur
një ekuacion i tillë është i gatshëm si për numrat e Fibonaçit, janë të rralla.

Është e vështirë të jepet një përkufizim më specifik i programimit dinamik. Aspekti më i


vështirë në ndërtimin e algoritmit të programimit dinamik për një problem të caktuar është
analiza e strukturës së brendshme të problemit dhe reduktimi në nënprobleme. Kjo fazë është
shumë delikate mbasi një coptim i tepërt mund të shpjerë në një numër shumë të madh
nënproblemesh, por nga ana tjetër pa një numër të mjaftueshëm të nënzgjidhjeve nuk mund të
ndërtohet ajo e dëshëruara. Pasi të jetë kryer kjo analizë, do të ndërtohet një tabelë që
përmban në çdo element të saj një nënzgjidhje (ose të dhëna të mjaftueshme për ta
rindërtuar). Sapo të mbushet tabela është e lehtë të përftohet zgjidhja e problemit fillestar.

Meqenëse një pjesë e madhe e zbatimeve të programimit dinamik ka të bëjë me probleme


optimizimi, është e nevojshme të përmendim një parim të përgjithshëm mbi të cilin
mbështeten zbatime të tilla. Richard Bellman e ka quajtur atë parimi i optimalitetit (priciple
of optimality). Në një formulim pak ndryshe nga origjinali, thotë që zgjidhja optimale e një
problemi optimizimi përbëhet nga zgjidhjet optimale të nënproblemeve të tij.

11.1 Tre probleme bazë

Qëllimi i këtij seksioni është që të paraqesë programimin dinamik nëpërmjet tre problemeve
tipike.

11.1.1 Problemi i rreshtit me monedha

Në një rresht janë vendosur n monedha me vlera respektive c1, c2, …, cn. Vlerat e monedhave
janë numra pozitivë jomedoemos të ndryshëm ndërmjet tyre. Qëllimi është që të përftohet
shuma më e madhe e monedhave me kusht që mos merren dy monedha fqinjë nga renditja
fillestare e monedhave.

Le të jetë F(n) shuma më e madhe që mund të merret nga një rresht me n monedha. Për të
nxjerrë një rekurrencë për F(n), ndajmë të gjitha zgjedhjet e lejuara të monedhave në dy
grupe: ata të cilët përfshijnë monedhën e fundit dhe ata që nuk e përfshijnë atë. Shuma më e
madhe që mund të merret nga grupi i parë është e barabartë me cn + F(n – 2), vlera e
monedhës së ntë plus maksimumi i shumës që mund të merret nga n – 2 monedhat e para.
Shuma më e madhe që mund të merret nga grupi i dytë është F(n – 1) në bazë të përkufizimit
të F(n). Kështu, do të kemi rekurrencën e mëposhtme nënështruar dy konditave fillestare të
qarta:

F(n) = max{cn + F(n – 2), F(n – 1)} për n > 1, (11.3)


F(0) = 0, F(1) = c1

Ne mund ta llogaritim F(n) duke mbushur një tabelë nga e majta në të djathtë në mënyrë të
ngjashme siç u bë për numrin e ntë të Fibonaçit në algoritmin llogaritFibonaciPaTabele(n) më
sipër.
11 Programimi dinamik | 222

// Algoritmi 11.3 gjetja e shumës më të madhe me monedha


// Të dhëna: tabela c[1..n], me n numra të plotë që tregojnë vlerën e monedhave
// Rezultate: f(n), vlera më e madhe që mund të përftohet

rreshtiMeMonedha(n, c) {
f[0] ← 0;
f[1] ← c[1];
for i ← 2 to n do
F[i] ← max(c[i] + f[i – 2], f[i – 1]);
return f[n];
}

Zbatimi i algoritmit për rreshtin me monedha 5, 1, 2, 10, 6, 2 tregohet në figurën 11.1, e cila
prodhon një shumë 17. Vemë në dukje se në këtë mënyrë, në fakt, kemi zgjidhur gjithashtu
problemin për i monedhat e para në rresht, për çdo 1 ≤ i ≤ 6. Për shembull, për i = 3, vlera më
e madhe e monedhave është F(3) = 7.

indeksi 0 1 2 3 4 5 6
c 5 1 2 10 6 2
f[0] = 0, f[1] = c1 = 5 f 0 5

indeksi 0 1 2 3 4 5 6
c 5 1 2 10 6 2
f[2] = max{1 + 0, 5}= 5 f 0 5 5

indeksi 0 1 2 3 4 5 6
c 5 1 2 10 6 2
f[3] = max{2 + 5, 5}= 7 f 0 5 5 7

indeksi 0 1 2 3 4 5 6
c 5 1 2 10 6 2
f[4] = max{10 + 5, 7}= 15 f 0 5 5 7 15

indeksi 0 1 2 3 4 5 6
c 5 1 2 10 6 2
f[5] = max{6 + 7, 15}= 15 f 0 5 5 7 15 15

indeksi 0 1 2 3 4 5 6
c 5 1 2 10 6 2
f[6] = max{2 + 15, 15}= 17 f 0 5 5 7 15 15 17
Figura 11.1 Diagrama e zgjidhjes së problemit të rreshtit të monedhave me anë të programimit
dinamik.

Për të gjetur monedhat për të cilat përftohet vlera më e madhe, duhet t’i kthehemi mbrapsh
llogaritjeve për të parë se cila nga dy mundësitë, cn + F(n – 2) ose F(n – 1), prodhon vlerën
më të madhe në formulën (11.3). Në zbatimin e fundit të formulës, ishte c6 + F(4), e cila
nënkupton që monedha c6 = 2 është pjesë e zgjidhjes optimale. Duke shkuar tek llogaritja e
F(4), vlera më madhe është prodhuar nga c4 + F(2), që nënkupton se monedha c4 = 10 është
11 Programimi dinamik | 223

pjesë e zgjidhjes optimale. Më tej, vlera më e madhe në llogaritjen e F(2) është prodhuar nga
F(1), duke patur si rrjedhim që monedha c2 të mos jetë pjesë e zgjidhjes optimale dhe së
fundmi monedha c1 = 5 është pjesë e zgjidhjes optimale. Kështu, zgjidhja optimale është {c1,
c4, c6}. Për të shmangur përsëritjen e të njëjtave llogaritje gjatë kthimit pas, informacioni për
secilin nga dy termat e (11.3) mund të regjistrohet në një tabele shtesë, kur vlerat e F
llogariten.

Duke përdorur algoritmin rreshtiMeMonedha për të gjetur F(n), shuma më e madhe e


monedhave që mund të kapet, si dhe monedhat që përbëjnë bashkësinë optimale, është e qartë
se kërkojnë Θ(n) kohë dhe Θ(n) hapsirë. Kjo është shumë superiore në krahasim me
alternativat: zbatimi nga lart-poshtë i rekurrencës (11.3) dhe zgjidhja e problemit me anë të
kërkimit shterues.

11.1.2 Problemi i kusurit të monedhave


Le të konsiderojmë problemin e përgjithshëm të problemit pasues të mirënjohur. Të kthehet
kusuri për vlerën n duke përdorur një numër minimal të monedhave metalike të serisë d1 < d2
< ∙∙ ∙ < dm. Le të konsiderojmë një algoritëm të programimit dinamik për rastin e
përgjithshëm, duke supozuar se disponohen sasi të pakufizuara të monedhave për secilën nga
m llojet e serisë d1, d2, …, dm. Seria e monedhave plotëson kushtet d1 < d2 < ∙ ∙ ∙ < dm dhe d1
= 1.

Le të jetë F(n), sasia më e vogël e monedhave të mjaftueshme për të përftuar vlerën n;


ndërkaq është e përshtatshme të përcaktohet F(0) = 0. Vlera n mund të përftohet vetëm me
anë të shtimit të ndonjë prej monedhave të prerjes dj tek vlera n – dj për j = 1, 2, ∙ ∙ ∙ , m e tillë
që n ≥ dj. Prandaj, mund të konsiderojmë të gjitha prerjet e tilla dhe të zgjedhim atë që
minimizon F(n – dj) + 1. Përderisa 1 është një konstante, ne mund, sigurisht, të gjejmë
F(n – dj) së pari dhe pastaj të shtojmë 1 tek ajo. Prandaj, kemi rekurrencën e mëposhtme për
F(n):

F(n) = minj:n≥dj �F�n − dj �� + 1 për n > 0, (11.4)


F(0) = 0.

Ne mund të llogaritim F(i) duke mbushur një tabelë me një rresht nga e majta në të djathtë në
mënyrë të ngjashme siç u bë më lart për problemin e rreshtit të monedhave, por llogaritja e
elementeve të tabelës këtu kërkon gjetjen e minimumit të deri m numrave.

// Algoritmi 11.4 Gjetja e numrit më të vogël të monedhave të serisë d1 < d2 < ∙ ∙ ∙ < dm ,
// ku d1 = 1 që duhet të shtohen për të dhënë vlerën n
// Të dhëna: n, vlera që duhet të kthehet dhe tabela d[1..m], me vlerat e serisë së
// monedhave në rendin rritës
// Rezultate: numri më i vogël i monedhave që duhet të japin vlerën n.

kusurMeMonedha (n, d) {
f[0] ← 0;
for i ←1 to n do{
temp ← ∞;
j ← 1;
while (j ≤ m and i ≥ d[j]) do {
11 Programimi dinamik | 224

temp ← min(f[i – d[j]], temp);


j ← j + 1;}
f[i] ← temp + 1;
}
return f[n]
}

Zbatimi i algortimit për vlerën n = 6 dhe prerjet 1, 3, 4 paraqitet në figurën 11.2. Përgjigja që
ai prodhon është dy monedha. Efektshmëria në kohë dhe në hapsirë e algoritmit është O(nm)
dhe Θ(n), respektivisht.

indeksi 0 1 2 3 4 5 6
f[0] = 0 f 0

indeksi 0 1 2 3 4 5 6
f[1] = min {f[1 – 1]} + 1 = 1 f 0 1

indeksi 0 1 2 3 4 5 6
f[2] = min {f[2 – 1]} + 1 = 2 f 0 1 2

indeksi 0 1 2 3 4 5 6
f[3] = min {f[3 – 1], f[3 – 3]} + 1 = 1 f 0 1 2 1

indeksi 0 1 2 3 4 5 6
f[4] = min {f[4 – 1], f[4 – 3], f[4 – 4] } + 1 = 1 f 0 1 2 1 1

indeksi 0 1 2 3 4 5 6
f[5] = min {f[5 – 1], f[5 – 3], f[5 – 4] } + 1 = 2 f 0 1 2 1 1 2

indeksi 0 1 2 3 4 5 6
f[6] = min {f[6 – 1], f[6 – 3], f[6 – 4] } + 1 = 2 f 0 1 2 1 1 2 2
Figura 11.2 Zbatimi i algoritmit kusurMeMonedha për vlerën n = 6 dhe monedha të serisë 1, 3
dhe 4.

Për të gjetur monedhat e një zgjidhjeje optimale, duhet të ecim mbrapsh në llogaritje, për të
parë se cilat monedha prodhojnë minimumin në formulën (11.4). Për rastin e trajtuar, zbatimi
i fundit i formulës (për n = 6), minimumi është prodhuar nga d2 = 3. Minimumi i dytë (për n
= 6 – 3) gjithashtu është prodhuar për monedhën e po kësaj madhësie. Kështu, bashkësia që
formon minimumin për n = 6 është e përbërë nga dy monedha me vlerë 3.

11.1.3 Problemi i grumbullimit të monedhave


Disa monedha janë vendosur në qelizat e një tabele n x m, por jo më shumë se një monedhë
në një qelizë. Një robot, i vendosur fillimisht në qelizën e qoshes lart-majtas, duhet të
mbledhë sa më shumë monedha që të jetë e mundur dhe t’i sjellë ato në qelizën e qoshes në
fund-djathtas. Në çdo lëvizje roboti mund të shkojë një qelizë djathtas ose të zbresë një qelizë
poshtë që nga pozicioni i tij aktual. Kur roboti viziton një qelizë me një monedhë atëherë ai e
merr monedhën. Hartoni një algoritëm për të gjetur maksimumin e numrit të monedhave që
mund të mbledhë roboti dhe rrugën që duhet të përshkruajë për të arritur këtë.
11 Programimi dinamik | 225

Le të jetë F(i, j) numri më i madh i monedhave që roboti mund të grumbullojë dhe të sjellë
në qelizën (i, j) në rreshtin e itë dhe shtyllën e jtë të tabelës. Ai mund të mbrijë në këtë qelizë
ose nga qeliza fqinjë (i – 1, j) sipër saj ose nga qeliza fqinjë (i, j – 1) nga e majta e saja.
Numrat më të mëdhenj të monedhave që mund të sjellë në këto qeliza janë F(i – 1, j) dhe F(i,
j – 1), respektivisht. Sigurisht, që nuk ka qeliza fqinjë sipër qelizave të rreshtit të parë, dhe
nuk ka qeliza fqinjë në të majtë të qelizave të shtyllës së parë. Për këto qeliza do të supozohet
se F(i – 1, j) dhe F(i, j – 1) janë të barabarta me 0 për fqinjët e tyre që nuk ekzistojnë.
Prandaj, numri më i madh i monedhave që roboti mund të sjellë në qelizën (i, j) është sa më i
madhi i këtyre dy numrave plus një monedhë të mundshme të vetë qelizës (i, j). Me fjalë të
tjera, do të kemi formulën e mëposhtme për F(i, j):

F(i, j) = max {F(i – 1, j), F(i, j – 1)} +ci,j, për 1 ≤ i ≤ n, 1 ≤ j ≤ m (11.5)


F(0, j) për 1 ≤ j ≤ m dhe F(i, 0) = 0 për 1 ≤ i ≤ n,

ku, cij = 1 nëse ka një monedhë në qelizën (i, j), përndryshe ci,j = 0.

Duke përdorur këto formula, ne mund ta mbushim tabelën n ∙ m me vlerat e F(i, j) ose rresht
pas rreshti ose shtylle pas shtylle, si një algoritëm tipik i programimit dinamik në të cilin
përfshihen tabelat.

// Algoritmi 11.5 Gjetja e numrit më të mad te monedhave qe mbledh roboti


// Të dhëna: tabela c[1..n, 1..m], elementet e së cilës janë ose 1 ose 0 për qeliza
// me dhe pa monedhë, respektivisht
// Rezultate: f[n,m], numri më i madh i monedhave që roboti do të sjellë në qelizën (n, m)

koleksionMonedhaMeRobot (n, m, c) {
f[1, 1] ← c[1, 1];
for j ←2 to m do
f[1, j] ← f[1, j – 1] + c[1, j];
for i ←2 to n do {
f[i, 1] ← f[i – 1, 1] + c[i, 1]
for j = 2 to m do
f[i, j] ← max(f[i – 1, j], f[i, j – 1]) + c[i, j];
}
return f[n, m]
}

Algoritmi ilustrohet në figurën 11.3b për gjendjen fillestare të monedhave të dhënë në figurën
11.3a. Meqenëse llogaritja e vlerës F[i, j] me anë të formulës (11.5) për çdo qelizë të tabelës
shpenzon një kohë konstante, efektshmëria kohore e algoritmit është Θ(nm). Efektshmëria e
tij hapsinore, është e qartë që është gjithashtu Θ(nm).
11 Programimi dinamik | 226

1 2 3 4 5 6 1 2 3 4 5 6 1 2 3 4 5 6
1 ○ 0 0 0 0 1 1 ■ ○
2 ○ ○ 0 1 1 2 2 2 ○ ○
3 ○ ○ 0 1 1 3 3 4 ○ ○
4 ○ ○ 0 1 2 3 3 5 ○ ○
5 ○ ○ 1 1 2 3 4 5 ○ ○ ■
(a) (b) (c)

Figura 11.3 (a) Monedhat që do të mblidhen. (b) rezultati i algoritmit të programimit dinamik.
(c) Dy rrugë për të mbledhur 5 monedha, numri më i madh i mundshëm i monedhave.

Për të gjetur një rrugë optimale duhet t’u kthehemi llogaritjeve mbrapsh: nëse F[i – 1, j] >
F[i, j – 1], atëherë një rrugë optimale për tek qeliza (i, j) duhet të vijë nga qeliza fqinjë sipër
saj; nëse F(i – 1, j] < F[i, j – 1], një rrugë optimale tek qeliza (i, j) duhet të vijë nga qeliza
fqinjë majtas saj; dhe nëse F[i – 1, j] = F[i, j – 1], qeliza (i, j) mund të mbrihet nga secili
drejtim. Kjo prodhon dy rrugë optimale për rastin e figurës 11.3a, të cilët paraqiten në figurën
11.3c. Nëqoftëse kryqëzimet nuk merren parasysh, një rrugëtim optimal mund të përftohet në
kohën Θ(n + m).

11.2 Problemi i çantës së shpinës

E fillojmë seksionin për hartimin e një algoritmi të programimit dinamik për një problem
optimizimi të njohur: problemi i çantës së shpinës: jepen n artikuj me pesha të njohura p1, p2,
…, pn dhe vlera respektive v1, v2, …, vn si dhe kapaciteti maksimal i çantës P. Të gjendet
bashkësia më e vlershme e artikujve që mund të futet në çantë (problemin e kemi shtruar në
leksionin për teknikën e forcës brutale ku kemi diskutuar për zgjidhjen e tij me anë të një
algoritmi të kërkimit shterues). Do të supozojmë se të gjitha peshat dhe kapaciteti i çantës
janë numra të plotë pozitivë; ndërsa vlerat e artikujve edhe mund të mos jenë numra të plotë.

Për të hartuar algoritmin sipas teknikës të programimit dinamik është e nevojshme që të


nxjerrim një relacion rekurrence që të shprehë një zgjidhje të një rasti të problemit të çantës
së shpinës në funksion të zgjidhjeve të nënrasteve të tij më të vegjël. Le të konsiderojmë një
rast të përcaktuar nga i artikujt e parë, 1 ≤ i ≤ n me pesha respektive p1, …, pi, vlera v1,…, vi
dhe kapacitet të çantës j, 1 ≤ j ≤ P. Le të jetë F[i, j] vlera e një zgjidhjeje optimale të këtij
rasti, domethënë, vlera e nënbashkësisë më të vlershme të i artikujve të parë që futen në
çantën me kapacitet j. Ne mund t’i ndajmë të gjitha nënbashkësitë e i artikujve të parë që
futen në çantën me kapacitet j në dy kategori: ato në të cilat përfshihet artikulli i dhe ato në të
cilët nuk përfshihet artikulli i. Vëmë në dukje sa më poshtë:

1. Ndër nënbashkësitë që nuk përfshijnë artikullin e itë, vlera e një nënbashkësie optimale
nga përkufizimi është F[i – 1, j].
2. Ndër nënbashkësitë që përfshijnë artikullin e itë ( pra plotsojnë kushtin, j – pi ≥ 0), një
nënbashkësi optimale prodhohet nga ky artikull dhe një nënbashkësi optimale e i – 1
artikujve që futen në çantë me kapacitet j – pi. Vlera e një bashkësie të tillë optimale është
vi + F[i – 1, j – pi].

Kështu vlera e një zgjidhjeje optimale ndërmjet të gjitha zgjidhjeve të lejuara të i artikujve të
parë është sa maksimumi i këtyre dy vlerave. Natyrisht, nëse artikulli i itë nuk futet dot në
çantë, vlera e një nënbashkësie të zgjedhur nga i artikujt e parë është e njëjtë si vlera e një
11 Programimi dinamik | 227

nënbashkësie optimale të zgjedhur nga i – 1 artikujt e parë. Këto vërejtje na shpien në


rekurrencën e mëposhtme:

max{F[i − 1, j], vi + F[i − 1, j − pi ]}, në se j − pi ≥ 0


F[i, j] = � (11.6)
F[i − 1, j], në se j − pi < 0

Është e përshtatshme që të përcaktojmë kushtet fillestare si më poshtë:

F[0, j] = 0 për j ≥ 0 dhe F[i, 0] = 0 për i ≥ 0 (11.7)

Qëllimi ynë është të gjejmë F[n, P], vlerën maksimale të një nënbashkësie prej n artikujsh të
dhënë që mund të futen në çantën me kapacitet P dhe një nënbashkësi të artikujve që
përcakton këtë vlerë maksimale.

Tabela 11.1 ilustron vlerat e përfshira në ekuacionet 11.6 dhe 11.7. Për çdo i, j > 0, për të
llogaritur elementin në rreshtin e itë dhe shtyllën e jtë, F[i, j], duhet të gjejmë vlerën më të
madhe ndërmjet vlerës që ndodhet në qelizën e rreshtit e mëparshëm dhe në njëjtën shtyllë
dhe shumës të formuar nga vlera vi me vlerën e qelizës në rreshtin e mëparshëm dhe pi
shtylla në të majtë. Tabela mund të plotësohet ose rresht për rresht ose shtyllë për shtyllë.

Tabela 11.1 Zgjidhja e problemit të çantës së shpinës me anë të programimit dinamik

kapaciteti j
0 j – pi j P
0 0 0 0 0

i–1 0 F[i – 1, j – pi] F[i – 1, j]


pi, vi i 0 F[i, j]

n 0 qëllimi

Shembull 1. Le të zgjidhim problemin e çantës së shpinës me të dhënat fillestare të


mëposhtme: kapaciteti P = 5; peshat p={2, 1, 3, 2} dhe vlerat v={12, 10, 20, 15}.

Në tabelën 11.2 paraqitet dinamika e zgjidhjes së problemit me anë të teknikës së


programimit dinamik, duke zbatuar formulat 11.6 dhe 11.7.

Tabela 11.2 Shembull i zgjidhjes të një rasti të problemit të çantës së shpinës me anë
të algoritmit të programimit dinamik

kapaciteti j
artikulli i 0 1 2 3 4 5
0 0 0 0 0 0 0
p1 = 2 v1 = 12 1 0 0 12 12 12 12
p2 = 1 v2 = 10 2 0 10 12 22 22 22
p3 = 3 v3 = 20 3 0 10 12 30 30 32
p4 = 2 v4 = 15 4 0 10 15 30 30 37
11 Programimi dinamik | 228

Kështu, vlera me e madhe është F[4, 5] = 37. Ne mund ta gjejmë përbërjen e një
nënbashkësie optimale duke gjurmuar në rrugën e kundërt të mbushjes së tabelës. Përderisa
F[4,5] > F[3, 5], artikulli 4 do të përfshihet në një zgjidhje optimale gjatë kërkimit të një
nënbashkësie optimale për mbushjen e 5 – 2 = 3 njësive të kapacitetit të mbetur të çantës.
Artikulli tjetër kërkohet në elementin F[3, 3]. Përderisa F[3, 3] = F[2, 3], artikulli 3 nuk është
pjesë e një nënbashkësie optimale. Artikulli tjetër kërkohet në elementin F[2, 3]. Përderisa
F[2, 3] > F[1, 3], artikulli 2 është përfshirë në një zgjidhje optimale gjatë kërkimit të një
nënbashkësie optimale për mbushjen e 3 – 1 njësive të kapacitetit të mbetur të çantës.
Artikulli tjetër kërkohet në elementin F[1, 2]. Përderisa F[1, 2] > F[0, 2], artikulli 1 është
pjesa e fundit e zgjidhjes optimale të përbërë nga treshja {artikulli 1, artikulli 2, artikulli 4}.

Efektshmëritë kohore dhe hapsinore të këtij algoritmit janë që të dyja Θ(nP). Ndërsa koha që
duhet për të gjetur përbërjen e një zgjidhjeje optimale është O(n).

Në praktikë ka shumë probleme, që mund të trajtohen si problem i çantës së shpinës


(Knapsack problem 0-1). Për shembull, një kompani transporti mund të dojë të njohë
mënyrën më të mirë për të ngarkuar një kamion ose një kontenier me mallra për ta
transportuar. Ndërkaq mund të paraqiten variante të ndryshme të problemit si Për shembull,,
mund të ketë një numër të kufizuar mallrash për çdo lloj, etj. Shumë nga këto probleme mund
të zgjidhen në mënyrë të ngjashme me anë të teknikës së programimit dinamik.

Për rastet kur përmasat e artikujve nuk janë numra të plotë përdoren algoritme të tjerë të
Programimit dinamik.

11.3 Funksionet memorizues

Siç u diskutua në fillim të kapitullit, programimi dinamik ka të bëjë me probleme, zgjidhja e


të cilëve kënaq një ekuacion rekurrencial me nënprobleme të mbi- vendosuara. Versioni i
drejtpërdrejtë nga lart-poshtë për gjetjen e një zgjidhjeje të një rekurrence të tillë të shpie në
një algoritëm që zgjidh nënproblemet e përbashkëta më tepër se një herë dhe prandaj është i
paefektshëm (në mënyrë tipike me kohë eksponenciale ose më keq). Nga ana tjetër, versioni
klasik i programimit dinamik, nga poshtë-lart: mbush një tabelë me zgjidhjet e të gjitha
nënproblemeve të vogla, dhe secili prej tyre zgjidhet vetëm një herë. Një aspekt i
papëlqyeshëm i këtij versioni është se zgjidhjet e nënproblemeve më të vegjël nuk janë të
domosdoshme për të përftuar zgjidhjen e problemit. Përderisa kjo e metë nuk shfaqet në
versionin lart-poshtë, është e natyrshme që të përpiqemi të kombinojmë përparësitë e
versioneve lart-poshtë dhe poshtë-lart. Qëllimi është që të përftohet një metodë që zgjidh
vetëm nënproblemet të cilat janë të domosdoshme dhe trajtohen vetëm një herë. Një metodë e
tillë ekziston dhe mbështetet në përdorimin e funksioneve memorizues (memory functions).

Kjo metodë zgjidh një problem të dhënë në mënyrën lart-poshtë, por përveç kësaj mban dhe
një tabelë të gjërave që do të përdoren nga algoritmi nga poshtë-lart. Në fillim të gjitha vlerat
e tabelës inicializohen me një simbol “null” të veçantë që tregon se ato nuk janë llogaritur
akoma. Pas kësaj, nëse një vlerë e re është e nevojshme që të llogaritet, metoda kontrollon së
pari vlerën në tabelë: nëse vlera nuk është “null”, ajo thjesht merr vlerën nga tabela;
përndryshe vlera llogaritet me një thirrje rekursive, dhe vendoset në tabelë.

Algoritmi pasues zbaton këtë ide për problemin e çantës së shpinës. Pas inicializimit të
tabelës, funksioni rekursiv është i domosdoshëm të thirret me i = m (numri i artikujve) dhe j =
P (kapaciteti i çantës).
11 Programimi dinamik | 229

Algoritmi 11.6 Metoda e funksioneve memorizues për problemin e çantës së shpinës


// Të dhëna: Një numër i plotë jonegativ i, që tregon numrin e artikullit të parë
// që do të merret në konsideratë dhe numër i plotë jonegativ j
// që tregon kapacitetin e çantës të shpinës
// Rezultate: Vlera e një nënbashkësie të lejuar optimale e i artikujve të parë
// Shënim: Algoritmi shfrytëzon tabelat pesha[1..n], vlera[1..n] dhe
// tabelën f[0..n, 0..P] vlerat e së cilës janë inicializuar me –1, me përjashtim
// të rreshtit 0 dhe shtyllës 0 që janë inicializuar me vlerën 0

knapsackMF (i, j) {
if f[i, j] < 0
then { if j < pesha[i]
then vl ← knapsackMF(i – 1, j)
else vl ← max(knapsackMF(i – 1, j),
vlera[i] + knapsack(i – 1, j – pesha[i]));
f[i, j] ← vl};
return f[i, j]
}

Shembull 2. Le të zbatojmë metodën e funksionit memorizues për të dhënat fillestare të


konsideruara në shembullin 1. Në tabelën 11.3 jepen rezultatet. Vetëm 11 nga 20 llogariten
efektivisht (përjashtuar ato të rrjeshtit 0 dhe shtyllës 0). Vetëm një vlerë V[1, 2] mbetet 0. Për
të dhëna fillestare të rendit më të lartë proporcioni i vlerave të tilla që nuk llogariten është
shumë më i madh.

Tabela 11.3 Shembull i zgjidhjes të një rasti të problemit të çantës së shpinës me anë të
algoritmit të funksionit memorizues

Në përgjithësi, ne nuk mund të presim një përfitim më të madh sesa një faktor konstant, duke
përdorur metodën e funksionit memorizues për problemin e çantës së shpinës mbasi klasa e
efektshmërisë kohore të tij është e njëjtë me atë të algoritmit nga poshtë-lart. Një përmirësim
më i ndjeshëm mund të pritet për algoritme të programimit dinamik në të cilin llogaritja e një
vlere shpenzon më tepër se një kohë konstante. Gjithashtu duhet të mbani mend se metoda e
funksionit memorizues mund të jetë më pak e efektshme në hapsirë sesa një version i
efektshëm në hapsirë për një algoritëm nga poshtë-lart.

11.4 Përmbledhje

• Programimi dinamik është një teknikë për zgjidhjen e problemeve me nënprobleme që


mbivendosen. Në mënyrë tipike të tillë probleme rrjedhin nga një relacion recurrence që
lidh zgjidhjen e një problemi të dhënë me zgjidhjet e nënproblemeve të tij më të vegjël të
të njëjtit tip. Programimi dinamik sugjeron zgjidhjen e çdo problemi më të vogël vetëm
njëherë dhe regjistrimin e rezultatit në një tabelë nëpërmjet të cilës mund të përftohet
zgjidhja e problemit original.
• Zbatimi i programimit dinamik në një problem optimizimi kërkon që problemi të kënaqë
principin e optimalitetit: një zgjidhje optimale në çdo hap duhet të përbëhet nga zgjidhjet
optimale të nënhapave të tij.
11 Programimi dinamik | 230

• Zgjidhja e problemit të çantës së shpinës me anë të një algoritmi të programimit dinamik


ilustron zbatimin e kësaj teknike në problemet e vështirë të optimizimit kombinatorik.
• Teknika e funksionit memorizues përpiqet të kombinojë fuqinë e përafrimit nga lart-
poshtë me fuqinë e përafrimit nga poshtë-lart për të zgjidhur probleme me nënprobleme të
mbivendosura. Ajo e bën këtë duke zbatuar, rrugën nga lart-poshtë vetëm njëherë sa për
të formuar nënproblemet e nevojshme të problemit të dhënë dhe regjistrimin e tyre në një
tabelë.

11.5 Ushtrime për kapitullin 11

1. Çfarë ka të përbashkët teknika e programimit dinamik dhe teknika ndaj-dhe- sundo? Cili
është ndryshimi kryesor ndërmjet këtyre dy teknikave?

2. Të zgjidhet rasti {5, 1, 2, 10, 6} i problemit të rreshtit me monedha

3. a. Provoni që efektshmëria kohore e zgjidhjes se problemit të rreshtit me


monedha me anë të zbatimit të drejtpërdrejtë të rekurrencës (11.3) është
eksponencial.
b. Provoni që efektshmëria kohore e zgjidhjes se problemit të rreshtit me monedha me
anë të kërkimit shterues është të paktën eksponenciale.

4. Zbatoni algoritmin e programimit dinamik për të gjetur të gjitha zgjidhjet e problemit të


kusurit të monedhave për serinë {1, 3, 5} dhe vlerën n = 9.

5. Hartoni një algoritëm për llogaritjen e koeficientit binomial C(n, k) duke përdorur
teknikën e programimit dinamik mbështetur në formulat e mëposhtme C(n, k) = C(n – 1, k
- 1) + C(n - 1, k) për n > k > 0 dhe C(n, 0) = C(n, n) = 1. Cila është efektshmëria
kohore në lidhje me veprimin e mbledhjes?

6. a. Zbatoni algoritmin e programimit dinamik nga poshtë-lart të paraqitur në


leksion për të zgjidhur rastin e mëposhtëm të problemit të çantës së shpinës
me kapacitet të përgjithshëm P=6, pesha = {3, 2, 1, 4, 5} dhe vlera = {25, 20,
15, 40, 50)
b. Sa nënbashkësi optimale të ndryshme ka rasti i paraqitur?
c. Në përgjithësi si mund ta përdorim tabelën e gjeneruar nga algoritmi
programimit dinamik që të themi nëse ka më shumë se një nënbashkësi
optimale për rastin e problemit të çantës së shpinës?

7. Zbatoni funksionet memorizues për rastin e problemit të çantës së shpinës të dhënë në


ushtrimin 6. Tregoni ata elemente të tabelës së programimit dinamik nga poshtë-lart: (i)
që nuk llogariten asnjëherë nëpërmjet metodës së funksioneve memorizues për këtë rast;
(ii) që merren (kapen) pa rillogaritje.
Teknika lakmitare | 231

12 Teknika lakmitare

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të kuptojë kushtet e përdorimit të teknikës lakmitare;


• të përdorë teknikën lakmitare për zgjidhjen e problemit të kusurit të monedhave;
• të përdorë teknikën lakmitare për problemin e zgjedhjes se aktivitetit;
• të bëjë dallimin ndërmjet teknikës lakmitare dhe programimit dinamik.

Teknika lakmitare (greedy technique) përdoret për të zgjidhur probleme optimizimi, kërkimi
i një vlere më të vogël ose më të madhe. Shkencëtarët e informatikës e konsiderojnë
teknikën lakmitare si teknikë të përgjithshme, pavarësisht se ajo mund të përdoret vetëm në
problemet e optimizimit. Rruga lakmitare sugjeron ndërtimin e një zgjidhjeje nëpërmjet një
vargu hapash, në secilin prej të cilëve zgjerohet zgjidhja e pjesëshme e përftuar deri në atë
hap, deri sa të arrihet zgjidhja e plotë e problemit. Në çdo hap, dhe ky është thelbi i kësaj
teknike, zgjedhja e kryer, duhet të jetë:

• E lejuar (feasible), domethënë, ajo duhet të kënaqë kushtet e problemit


• Optimale lokalisht (locally optimal), domethënë, duhet të jetë zgjedhja më e mirë lokale
ndër të gjitha zgjedhjet e lejuara të vlefshme në atë hap
• E pakthyeshme (irrevocable), domethënë, pasi të kryet, ajo nuk mund të ndryshohet në
hapat pasues të algoritmit.

Këto kërkesa shpjegojnë edhe emrin e teknikës: në çdo hap, ajo sugjeron një kapje lakmitare
të alternativës më të mirë të mundshme, me shpresën që një varg i tillë zgjidhjesh optimale
lokale do të prodhojë një zgjidhje optimale (globale) të të gjithë problemit. Këtu po i
shmangemi diskutimit filozofik nëse lakmia është e mirë apo e keqe.

Nga perspektiva algoritmike, çështja është nëse strategjia lakmitare ecën apo jo. Në
përgjithësi, ka probleme për të cilat një varg zgjidhjesh lokalisht optimale prodhon një
zgjidhje optimale për çdo rast të të dhënave fillestare të problemit. Por ka edhe probleme të
tjerë, që nuk prodhohet zgjidhje optimale: për probleme të tilla, zgjidhja lakmitare vlen nëse
ne interesohemi që të kemi një zgjidhje të përafërt të problemit. Si rregull algoritmet
lakmitare janë si joshës ashtu edhe të thjeshtë. Me gjithë dukjen e thjeshtë, prapa tyre
qëndron një teori e sofistikuar, që bazohet në struktura kombinatorike abstrakte të quajtura
“matroide”, të cilat nuk do t’i diskutojmë.

Përmendim disa probleme nga jeta e përditshme që mund të zgjidhen me teknikën lakmitare:
Problemi i planifikimit të punëve apo zgjedhjes së aktiviteteve (scheduling problem, activity
selection problem), probleme të trafikut rrugor (traffic problem), problemi i paketimit (bin
packing problem), problemi i ngjeshjes së të dhënave (file compression), problemi i kusurit të
monedhave (making-change problem), etj. Në përgjithsi teknika lakmitare është më e
përshtatshme për t’u përdorur me të dhëna të organizuara në struktura të tjera si pemët apo
grafet. Më poshtë do të trajtojmë vetëm dy probleme: problemin e kusurit të monedhave dhe
problemin e zgjedhjes së aktiviteteve.
Teknika lakmitare | 232

12.1 Problemi i kusurit të monedhave

Në të gjithë botën, arkëtaret ndeshen me problemin e kusurit të monedhave metalike, gjatë


një pagese me para në dorë i cili në formë të përgjithshme formulohet: të kthehet kusuri për
një vlerë të caktur n, me numrin më të vogël të monedhave të serisë d1 > d2 > ⋯ > dm, që
përdoren në atë vend.
Për shembull, në Shtetet e Bashkuara të Amerikës, seria e monedhave metalike që shprehin
qindarka (cents) përbëhet nga: d1 = 25 cents (quarter), d2 = 10 cents (dime), d3 = 5 cents
(nickel) dhe d4 = 1 cent (penny). Le të supozojmë se arkëtarja duhet të kthejë 48 cents. Nëse
përgjigja e propozuar është: 1 quarter, 2 dimes, dhe 3 pennies atëherë është ndjekur në
mënyrë të ndërgjegjshme ose jo një strategji logjike që përbën zgjedhjen më të mirë ndër
zgjedhjet e mundshme. Në të vërtetë, në hapin e parë, mund të kthehet secila nga të katër
monedhat e serisë. Por mendimi lakmitar të shpie në idenë që të kthehet 1 quarter sepse me
një monedhë të vetme, reduktohet në maksimum shuma që mbetet, domethënë, 23 cents. Në
hap të dytë nuk mund të kthehet më 1 quarter sepse shkel kushtin kufizues të problemit.
Prandaj kthehet 1 dime dhe mbeten për t’u kthyer edhe 13 cents. Duke kthyer përsëri edhe 1
dime tjetër mbeten 3 cents dhe për këtë të fundit mjaftojnë 3 pennies. Gjithsej kthimi i
mbetjes u realizua me 6 monedha.

A është optimale zgjidhja e më sipërme për problemin e kthimit të monedhave për sistemin
amerikan? Po, ajo është. Në fakt, është e mundur të provohet, që teknika lakmitare shpie në
një zgjedhje optimale, për çdo vlerë të shprehur me anë të një numri të plotë pozitiv, me
monedha metalike të serisë së lartpërmendur të monedhave.

Në të njëjtën kohë mund të ndërtohen shembuj të serive të monedhave, për shembull, d1 = 25,
d2 = 20, d3 = 10, d4 = 5 dhe d5 = 1, që në rast se zbatohet procedura e mësipërme, nuk japin
dot një zgjidhje optimale për disa shuma parash të caktuara,. Për shembull, për vlerën 40,
procedura e zbatuar më lart jep një zgjidhje që përbëhet nga tri monedha: një monedhë 25,
një monedhë 10 dhe një monedhë 5. Ndërkaq zgjidhja optimale përbëhet nga 2 monedha me
vlerë 20.

Në një sistem tjetër të përbërë nga seria bazë: d1 = 25, d2 = 10 dhe d3 = 4, për këmbimin e
sasisë 41, kthimi i mbetjes me idenë e lartpërmendur do të prodhojë një zgjidhje prej 3
monedhash: një monedhë 25, një monedhë 10 dhe një monedhë 4, që prodhojnë një rezultat
39 që nuk përputhet me atë që dëshërohet. Ndërkaq zgjidhja optimale përbëhet nga 5
monedha: një monedhë 25 dhe katër monedha me vlerë 4.

Një alternativë për zgjidhjen e këtyre problemeve është programimi dinamik që funksionon
për çdo lloj serie monedhash që ose kthen zgjidhjen optimale ose thotë që zgjidhja nuk
ekziston.

Në trajtë të përgjithshme problemi i kthimit të monedhave shtrohet si më poshtë: në qoftë se


supozohet se sistemi monetar përbëhet nga seria e monedhave d1, d2, …, dk, atëherë cili do të
ishte numri minimal i monedhave të nevojshme për të këmbyer një sasi monetare prej M
njësish? Matematikisht problemi shtrohet që të gjendet
𝑘
𝑚𝑚𝑚 = � 𝑛𝑖
𝑖=1
me kusht që : ∑𝑘𝑖=1 𝑑𝑖 𝑛𝑖 = 𝑀
Teknika lakmitare | 233

Supozimi bazë është që në këtë sistem duhet të ekzistojë monedha prej një njësi që të
sigurohet ekzistenca e zgjidhjes. Algoritmi kthimMonedhe realizon zgjidhjen e problemit
duke u mbështetur në teknikën Greedy e cila sugjeron që në çdo hap të merret një vendim që
është optimal për këtë hap (përdor monedhën më të madhe të disponueshme) pa marrë
parasysh pasojat në të ardhmen.

Algoritmi 12.1 Kthimi i monedhave


// Të dhëna : m, shuma që do të këmbehet dhe p[1..k] vlerat numerike të serisë të
// monedhave të sistemit të renditura në rendin zbritës
// Rezultate: s[1..k], tabela që përmban numrin e monedhave për çdo prerje që do të
// kthehet

kthimMonedhe (m, k, p, s) {
mbetja ← m;
for i ← 1 to k do {
s[i] ← div(mbetja, p[i]);
mbetja ← mbetja − s[i] * p[i];
}
return
}

12.2 Problemi i zgjedhjes së aktivitetit

Jepet një bashkësi aktivitetesh S={A1, A2, … ,An} që duan të përdorin të njëjtin burim, i cili
mund të përdoret vetëm nga një aktivitet në një çast kohë. Çdo aktivitet përcaktohet nga një
çift të dhënash që janë koha e fillimit si dhe koha e përfundimit fi që plotësojnë kushtin 0 ≤ si
< fi < +∞ . Një aktivitet i zgjedhur Ai zhvillohet gjatë gjithë gjysëm intervalit [si, fi). Dy
aktivitete Ai dhe Aj janë të pajtueshëm në qoftë se si ≥ fj ose sj ≥ fi. Problemi i zgjedhjes se
aktivitetit (activity selection) kërkon që të zgjidhet numri më i madh i aktiviteteve
reciprokisht të pajtueshme.

Algoritmi 12.2 zgjedhja e aktivitetit


// Të dhëna: n, numri i aktiviteteve; tabela s[1..n] kohët e fillimit të aktiviteteve;
// tabela f[1..n], kohët e përfundimit të aktiviteteve
// Rezultate: Kodi i çdo aktiviteti të zgjedhur

zgjedhjeAktiviteti(n, s, f)
1. rendit (f, n) // rendit në rendin jozbrritës kohët e përfundimit të aktiviteteve
2. j ← 1;
3. print(‘Aktivitet i zgjedhur’, j )
4. for i ← 2 to n do
5. if (s[i] ≥ f[j])
6. then { print( 'Aktivitet i zgjedhur ', i);
7. j ← i;}
8. return

Megjithëse algoritmi i bazuar në teknikën lakmitare për problemin e zgjedhjes së aktiviteteve


përbëhet nga një cikël i thjeshtë linear, ai vetë bën pjesë në klasën Θ(n log n), për shkak të
renditjes së të dhënave fillestare.
Teknika lakmitare | 234

Shembull. Le të supozojmë se kemi në dispozicion një ambjent në të cilin kërkohet të


zhvillohen 11 aktivitete reciprokisht të zëvendësueshme. Orët e fillimit dhe përfundimit të
aktiviteteve janë: A1=(12, 14), A2=(3, 5), A3=(8, 11), A4=(5, 7), A5=(5, 9), A6=(3, 8), A7=(6,
10), A8=(0, 6), A9=(8, 12), A10=(2, 13), A11=(1, 4). Të zgjidhen sa më shumë aktivitete
reciprokisht të zëvendësueshëm.

Në figurën 12.1(a), paraqitet shpërndarja fillestare e aktiviteteve ndërsa në figurën 12.1(b)


paraqitet shpërndarja e aktiviteteve e renditur në rendin rritës të kohës së përfundimit të
aktivitetit. Në bazë të algoritmit 12.2 numri më i madh i aktiviteteve reciprokisht të
zëvendësueshëm rezulton të jetë 4 ndërsa aktivitetet e zgjedhura nga algoritmi lakmitar janë :
A11, A4, A3, dhe A1.

A1 A11 zgjedhur
A2 A2
A3 A8
A4 A4 zgjedhur
A5 A6
A6 A5
A7 A7
A8 A3 zgjedhur
A9 A9
A10 A10
A11 A1 zgjedhur

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

a) Diagrama fillestare e aktiviteteve b) Diagrama e renditur sipas kohës së përfundimit

Figura 12.1 Shembull i zgjedhjes së aktivitetit

12.3 Teknika lakmitare dhe programimi dinamik

Në vështrim të parë duket sikur të dy teknikat janë të ngjashme. Të dyja janë teknika
optimizimi dhe që të dyja e ndërtojnë zgjidhjen nga një bashkësi zgjedhjesh të elementeve
individualë. Teknika lakmitare i kryen zgjidhjet e saj duke ecur gjithmonë përpara, duke mos
parë asnjëherë pas ose të revizionojë zgjidhjet tashmë të kryera. Programimi dinamik i kryen
zgjidhjet duke u nisur nga poshtë-lart, duke i sintetizuar ato prej nënproblemeve më të vogla
dhe duke provuar shumë mundësi dhe zgjedhje para se të arrijë në bashkësinë optimale. A
priori nuk ka asnjë indikator që të na thotë se teknika lakmitare do të na shpjerë në një
zgjidhje optimale. Ndërsa për programimin dinamik ka një të tillë që është parimi i
optimalitetit.

Teknika lakmitare, kur jep rezultat, zakonisht është më e efektshme se teknika e programimit
dinamik. Për të përdorur programimin dinamik mjafton të tregojmë se parimi i optimalitetit
mund të aplikohet për problemin në fjalë.

12.4 Përmbledhje

• Teknika lakmitare sugjeron ndërtimin e një zgjidhjeje për një problem optimizimi
nëpërmjet një vargu hapash, secili prej të cilëve zgjeron një zgjidhje të pjesëshme të
ndërtuar deri në atë hap, deri sa të arrihet një zgjidhje e plotë e problemit.
13 Kufizimet e fuqisë së algoritmeve | 235

13 Kufizimet e fuqisë së algoritmeve

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të kuptojë edhe pafuqinë e algoritmeve në zgjidhjen e disa klasave me probleme ;


• të kuptojë metodat për përftimin e kufijve më të ulët të vlerësimit të efektshmërisë së
algoritmeve;
• të njohë pemët e vendimit;
• të njohë vështirësitë në algortimet numerikë.

Në leksionet e mëparshme, janë paraqitur me dhjetra algoritme për zgjidhjen e problemeve të


tipeve të ndryshme. Një vlerësim i drejtë i algoritmeve si mjete për zgjidhjen e problemeve
është i pashmangshëm: ata janë instrumente shumë të fuqishëm, në veçanti kur ekzekutohen
në formë programesh në kompjuterat modernë. Por fuqia e algoritmeve nuk është e
pakufizuar. Disa probleme nuk mund të zgjidhen nga asnjë algoritëm. Probleme të tjerë mund
të zgjidhen në mënyrë algoritmike por jo në kohë polinomiale (kohë të pranueshme). Bile
edhe kur një problem mund të zgjidhet në kohë polinomiale nga disa algoritme, ata zakonisht
janë në kufijtë më të ulët të efektshmërisë së tyre, domethënë, nuk mund të përmirësohet më
efektshmëria e tyre.

Në seksionin e parë të leksionit do të trajtojmë metodat për përftimin e kufijve më të ulët, të


cilat janë vlerësime për një sasi minimale të pune të nevojshme për të zgjidhur një problem.
Në përgjithësi, për të fituar një kufi më të ulët jo i qartë bile edhe për një problem që duket i
lehtë, është një punë shumë e vështirë. Në kundërvënie me përcaktimin e efektshmërisë të një
algoritmi të veçantë, këtu puna qëndron në përcaktimin e një kufiri për efektshmërinë e të
gjithë algoritmeve, të njohur apo të panjohur. Gjithashtu kjo kërkon një përshkrim të
kujdesshëm të veprimeve, që algoritmeve të tillë, u lejohet të kryejnë. Nëse gabojmë në
përcaktimin me kujdes të “rregullave të lojës” (“rules of the game”), me që ra fjala,
pretendimi ynë mund të përfundojë në koshin e madh të plehrave të pamundësive të
shprehura në mënyrë të ngjashme, si për shembull, e thëna e fizikanit të madh britanik Lord
Kelvin në vitin 1895: “Heavier-than-air flying machines are impossible.”

Më pas, në seksionin e dytë, do të paraqiten pemët e vendimit. Kjo teknikë do të na lejojë, që


të përcaktojmë kufijtë më të ulët në lidhje me efektshmërinë kohore të atyre algoritmeve, ku
veprimi më i kushtueshëm është krahasimi, në veçanti algoritmet e renditjes dhe kërkimit në
tabela të renditura. Si rrjedhim, ne do të jemi të aftë t’u përgjigjemi pyetjejeve të tilla se a
është e mundur të shpiket një algoritëm renditjeje më i shpejtë se algoritmi Mergesort dhe a
është Kërkimi binar, algoritmi më i shpejtë për kërkim në një tabelë të renditur. (Po ashtu,
pemët e vendimit shërbejnë si një udhërrëfyes për zgjidhjen e disa gjë-a-gjëzave (puzzles.)

Në seksionin e fundit do të merremi me analizën numerike. Kjo degë e shkencës së


kompjuterave i përket algoritmeve për zgjidhjen e problemeve të matematikës së “vazhduar”,
si zgjidhja e ekuacioneve dhe sistemeve të ekuacioneve, vlerësimi i funksioneve të tillë si sin
x dhe ln x, llogaritja e integraleve të caktuar, etj. Natyra e problemeve të tilla krijon dy lloj
kufizimesh. Së pari, shumë prej tyre nuk mund të zgjidhen saktësisht. Së dyti, zgjidhja e tyre
edhe me përafërsi ka të bëjë me numra që mund të paraqiten në kompjuterat dixhitalë vetëm
me një nivel saktësie të kufizuar. Veprimi me numra të përafërt pa kujdesin e duhur mund të
13 Kufizimet e fuqisë së algoritmeve | 236

të shpjerë në rezultate shumë të pasakta. Ne do të shohim që edhe zgjidhja e një ekuacioni


themelor siç është ai i ekuacionit kuadratik paraqet vështirësi domethënëse, të cilat kërkojnë
një modifikim të formulës bazë për llogaritjen e rrënjëve të ekuacionit.

13.1 Argumentat e kufirit më të ulët

Ne mund ta shohim efektshmërinë e një algoritmi në dy këndvështrime. Sipas këndvështrimit


të parë (këndvështrimi absolut) ne mund të përcaktojmë klasën asimptotike të efektshmërisë
(le të themi, për rastin më të keq) dhe të shohim se ku ndodhet kjo klasë në hierarkinë e
klasave të efektshmërisë. Për shembull, selection sort, efektshmëria e të cilit është kuadratike,
është një algoritëm mjaft i shpejtë, ndërsa algoritmi për problemin e kullës së Hanoit është
shumë i ngadaltë mbasi efektshmëria e tij është eksponenciale. Megjithatë, dikush mund të
kundërshtojë duke thënë se ky krahasim është i ngjashëm me atë të krahasimit të mollës me
portokallin mbasi të dy algoritmet zgjidhin probleme të ndryshme. Prandaj këndvështrimi
alternativ (këndvështrimi relativ) dhe “më i ndershëm” është që të shihet se sa i efektshëm
është një algoritëm i veçantë në krahasim me algoritmet e tjerë që zgjidhin të njëjtin
problem. Parë me këtë sy, algoritmi Selection sort është i ngadaltë, mbasi ekzistojnë
algoritme renditjeje të klasës O(n log n); ndërsa algoritmi i kullës së Hanoit nga ana tjetër,
del që është më i shpejti i mundshëm për problemin që zgjidh.

Kur ne duam të vëmë në dukje efektshmërinë e një algoritmi në lidhje me algoritmet e tjerë
për të njëjtin problem, do të ishte e dëshërueshme të njohim efektshmërinë më të mirë të
mundshme të ndonjë algoritmi, që zgjidh këtë problem. Njohja e një kufiri të tillë, të quajtur
kufiri më i ulët (lower bound), mund të na tregojë se sa përmirësime mund të shpresojmë të
kryejmë për të gjetur një algoritëm më të mirë për problemin në fjalë. Nëse një kufi i tillë
është i ngushtë (tight) domethënë,, njohim tashmë një algoritëm me të njëjtën klasë
efektshmërie si kufiri më i ulët, atëherë e shumta që mund të shpresojmë është ndonjë
përmirësim me një faktor konstant.

Në këtë seksion do të paraqesim disa metoda për përcaktimin e kufijve më të ulët dhe do t’i
ilustrojmë me shembuj të veçantë.

13.1.1 Kufijt më të ulët të qartë


Mënyra më e thjeshtë për të përfituar një klasë të kufijve më të ulët bazohet në numërimin e
sasisë së të dhënave fillestare që duhet të përpunohen si dhe në numrimin e rezultateve që
duhet të shtypen. Përderisa një algoritëm duhet të “lexojë” të paktën të gjithë elementet që
janë të nevojshëm për t’u përpunuar si dhe të “shtypë” të gjithë rezultatet e nevojshme, një
llogaritje e tillë prodhon automatikisht një kufi më të ulët të qartë (trivial lower bound). Për
shembull, çdo algoritëm për prodhimin e të gjithë përkëmbimeve të n elementeve të
dallueshëm duhet të jetë në klasën Ω(n!) mbasi përmasa e rezultateve është n!. Dhe për më
tepër një kufi i tillë është i ngushtë mbasi algoritmet e mirë për prodhimin e përkëmbimeve
shpenzojnë një kohë konstante për secilin prej tyre, me përjashtim të atij të fillimit.

Si një shembull tjetër, le të konsiderojmë problemin e vlerësimit të një polinomi të shkallës n

Pn (x) = an x n + an−1 x n−1 + ⋯ + a0

në një pikë të dhënë x, kur jepen koeficientat an, an - 1, …, a0. Shihet lehtë se të gjithë
koeficientët do të përpunohen nga çdo algoritëm i vlerësimit të polinomit. Me të vërtetë, nëse
13 Kufizimet e fuqisë së algoritmeve | 237

nuk do të ishte kështu, atëherë ne do të mund të ndryshonim vlerën e një koeficienti të


papërpunuar, por kjo do të ndryshonte vlerën e polinomit në një pikë jozero. Kjo nënkupton
që një algoritëm i tillë duhet të jetë në klasën Ω(n). Ky kufi më i ulët është edhe i ngushtë
mbasi si algoritmi i vlerësimit nga e djathta në të majtë ashtu edhe skema Horner janë që të
dy linearë.
Në mënyrë të ngjashme, një kufi më i ulët i qartë për llogaritjen e produktit të dy matricave të
rendit n është në klasën Ω(n2) mbasi një algoritëm i tillë ka për të procesuar të 2n2 elementet
e matricave hyrëse dhe ka për të prodhuar n2 elemente në dalje. Por ende nuk dihet nëse ai
është një kufi i ngushtë.

Ndodh që kufijtë më të ulët toe qartë të jenë shumë të ulët për të qenë të dobishëm. Për
shembull, kufiri i ulët i qartë për problemin e tregtarit shëtitës është Ω(n2), mbasi si të dhëna
fillestare shërbejnë n(n − 1)/2 distancat ndërqytetëse dhe si rezultat i prodhuar është lista e (n
+ 1) qyteteve që përbëjnë turin optimal. Por ky kufi është pothuajse i padobishëm, mbasi nuk
njihet ndonjë algoritëm për këtë problem, koha e ekzekutimit të të cilit, të jetë një funksion
polinomial i ndonjë rendi.

Ka edhe një pengesë tjetër për të nxjerrë me anë të kësaj metode një kufi të ulët që të jetë i
vlefshëm. Ajo lidhet me faktin se cila pjesë e të dhënave fillestare duhet të procesohet nga
ndonjë algoritëm për zgjidhjen e problemit në fjalë. Për shembull, kërkimi për një element të
dhënë në një tabele të renditur nuk ka nevojë të procesojë të gjithë elementet.

13.1.2 Argumentat e informacionit teorik


Ndërsa metoda e skicuar më lart merr parasysh përmasën e rezultateve të problemit, metoda e
informacionit teorik (information-theoretical approach) mbështetet në teorinë e informacionit
dhe kërkon të përcaktojë një kufi më të ulët bazuar në sasinë e informacionit (sipas kuptimit
që i jep teoria e informacionit kësaj shprehjeje) që ka për të prodhuar algoritmi.

Le të konsiderojmë si shembull lojën e njohur të zbulimit të një numri të plotë pozitiv


ndërmjet 1 dhe n, të menduar nga dikush dhe që do të gjendet nga dikush tjetër, me anë të
disa pyetjeve, përgjigja e të cilave është e formës: Po ose Jo. Në thelb ky është një problem
kodimi. Nga teoria e informacionit dihet që sasia e pasigurisë (entropia) e çdo algoritmi
zgjidhës për këtë problem, është e barabartë me⌈log 2 𝑛⌉, që është e barabartë me numrin e
biteve të nevojshëm për të koduar një numër të veçantë në sistemin binar. Ne mund të
mendojmë sikur çdo pyetje (ose për të qenë më të saktë përgjigja e një pyetjeje) prodhon të
shumtën 1 bit informacion mbi rezultatin e algoritmit, domethënë,, mbi numrin e zgjedhur në
rastin tonë. Si rrjedhim, një algoritëm i tillë do të ketë nevojë për të paktën ceiling(log2n)
hapa të tillë, para se të përcaktohet rezultati i tij në rastin më të keq. Për shembull, për të
gjetur numrin 8, të koduar në formë binare do të mund të kryhet vargu i mëposhtëm i
pyetjeve:

Pyetjet Përgjigja Vlera


1. Është biti i parë zero? Jo 1
2. Është biti i dytë zero? Po 0
3. Është biti i tretë zero? Po 0
4. Është biti i katërt zero? Po 0

Metoda e sapo skicuar quhet argumenti i informacionit teorik (information-theoretic


argument) për shkak të lidhjes së saj me teorinë e informacionit. Është provuar që ajo ka qenë
mjaft e dobishme për të gjetur të ashtuquajturit kufijt më të ulët të informacionit teorik për
13 Kufizimet e fuqisë së algoritmeve | 238

shumë probleme, që bazohen në krahasim, duke përfshirë problemet e renditjes dhe kërkimit.
Idea që qëndron në bazë të saj mund të realizohet më saktësisht nëpërmjet mekanizmit të
pemëve të vendimit (decisions tree), të cilit për shkak të rëndësisë së teknikës të përdorur, do
t’i kushtojmë një seksion të veçantë më pas.

13.1.3 Argumentat e kundërshtarit


Le t’i rikthehemi lojës së zbulimit të numrit, që u përdor për të paraqitur idenë e argumentit të
informacionit teorik. Ne mund të provojmë, që çdo algoritëm që zgjidh këtë problem duhet të
kryejë të paktën ceiling(log2n) pyetje në rastin e tij më të keq, duke luajtur rolin e një
kundërshtari “armiqësor”, që dëshëron që algoritmi të bëjë sa më shumë pyetje që të jetë e
mundur. Kundërshtari e fillon lojën me idenë që çdo numër ndërmjet 1 dhe n është
potencialisht i mundur që të zgjidhet. (Natyrisht që kjo është një hile, por që ne nuk kemi
asnjë argument që ta provojmë). Pas çdo pyetjeje, kundërshtari jep një përgjigje që i lë atij në
dispozicion bashkësinë më të madhe të numrave, por që është e pajtueshme me të gjitha
përgjigjet e dhëna më parë. (Kjo strategji e lë atë me të paktën me gjysmën e numrave që ai
ka patur para se të përgjigjet). Nëse algoritmi e gjen numrin para se përmasa e bashkësisë të
reduktohet në 1, kundërshtari ndërkaq mund të mendojë një numër tjetër, që mund të jetë një
e dhënë fillestare legjitime, që algoritmi dështon ta identifikojë. Është çështje thjesht teknike
për të treguar se janë të nevojshme ⌈log 2 𝑛⌉ iteracione për të reduktuar një bashkësi prej n
elementesh në një bashkësi me 1 element me anë të përgjysmimit dhe rrumbullakimit nga
sipër të përmasës të bashkësisë që mbetet. Pra, nevojitet që të bëhen të paktën ceiling(log2n)
pyetje nga një algoritëm në rastin më të keq.

Ky shembull ilustron metodën e kundërshtarit (adversary method) për të përcaktuar kufijt më


të ulët. Ajo bazohet në një logjikë keqdashëse të një kundërshtari të ndershëm: keqdashja e
bën atë që të shtyjë algoritmin poshtë, në rrugën që shpenzon sa më shumë kohë, ndërsa
ndershmëria e bën atë që të jetë i pajtueshëm me zgjedhjet e bëra më parë. Kufiri më i ulët
përftohet atëherë nga matja e sasisë së punës të nevojshme për të reduktuar një bashkësi me
një potencial të caktuar të të dhënave fillestare, në një të dhënë të vetme fillestare nëpërmjet
rrugës që harxhon më shumë kohë.

13.1.4 Reduktimi i problemit


Ne e kemi takuar problemin e reduktimit në teknikën transformo-dhe-sundo. Kemi diskutuar
për përftimin e një algoritmi për problemin P me anë të reduktimit të tij në një problem tjetër
Q të zgjidhshëm me anë të ndonjë algoritmi të njohur. Një ide e ngjashme reduktimi mund të
përdoret për të gjetur një kufi të ulët. Për të treguar që problemi P është të paktën po aq i fortë
sa një problem tjetër Q me një kufi më të ulët të njohur, ne duhet të reduktojmë Q tek P ( jo P
tek Q). Me fjalë të tjera, ne duhet të tregojmë se një rast i çfardoshëm i problemit Q mund të
transformohet (në një mënyrë të efektshme) në një rast të problemit P, kështu që çdo
algoritëm që zgjidh P duhet që po ashtu të zgjidhë Q. Pastaj kufiri më i ulët për Q do të jetë
më i ulët për P. Tabela 13.1 përmban një listë të problemeve të rëndësishëm që përdoren
shpesh për këtë qëllim.

Tabela 13.1 Probleme të përdoruara shpesh për përcaktimin e kufirit të ulët me anë të
reduktimit

Problemi Kufiri më i poshtëm Të qënurit i ngushtë


Renditja Ω(n log n) Po
Kërkimi në një tabelë të renditur Ω(log n) Po
13 Kufizimet e fuqisë së algoritmeve | 239

Problemi i unicitetit të elementeve Ω(n log n) Po


Shumëzimi i numrave të plotë të mëdhenj Ω(n) E panjohur
Shumëzimi i matricave kuadratike Ω(n2 ) E panjohur

13.2 Pemët e vendimit

Shumë algoritme të rëndësishëm, në veçanti ata të krahasimit dhe të renditjes, kanë në bazë
krahasimin e elementeve të të dhënave fillestare. Performanca e algoritmeve të tillë studjohet
me anë të një modeli të quajtur pema e vendimit (decision tree). Pema e vendimit përbëhet
nga nyje të paraqitura me anë të simbolit grafik të rombit dhe nga gjethe të paraqitura me anë
të simbolit grafik të drejtkëndëshit. Si shembull, figura 13.1 paraqet pemën e vendimit për një
algoritëm për gjetjen e më të voglit ndër tre numra të dhënë {a, b, c}. (Për hir të thjeshtësisë
do të supozojmë që, të gjithë elementet e të dhënave fillestare janë të ndryshëm.) Gjetja e
elementit më të vogël bazohet në veprimin e krahasimit. Pema e vendimit e figurës 13.1,
quhet pemë vendimi binare mbasi nga çdo nyje dalin të shumtën dy degëzime.
po jo
a<b

po jo po jo
a<c b<c

a c b c
Figura 13.1 Pema e vendimit për gjetjen e minimumit të tre numrave

Çdo nyje e brendshme e një peme vendimi binare paraqet një krahasim që specifikohet
brenda nyjes në formën 𝑘 < 𝑘′. Nyjet e nënpemës të majtë përmbajnë informacion në lidhje
me krahasimet që kryhen kur krahasimi 𝑘 < 𝑘′ është i vërtetë, ndërsa nënpema e djathtë bën
të njëjtën gjë atëherë kur është i vërtetë krahasimi 𝑘 > 𝑘′. Çdo gjethe paraqet një rezultat të
mundshëm të ekzekutimit të algoritmit për të dhëna të përmasës 3. Vëmë në dukje që numri i
gjetheve mund të jetë më i madh se numri i rezultateve, mbasi për disa algoritme, i njëjti
rezultat mund të arrihet nëpërmjet një vargu të ndryshëm krahasimesh. (Kjo ka ndodhur në
pemën e vendimit të dhënë në figurën 13.1.) Një fakt i rëndësishëm është që numri i gjetheve
duhet të jetë të paktën po aq i madh sa ç'është numri i rezultateve të mundshme.

Në përgjithësi puna e algoritmit ndaj një grupi të caktuar të dhënash fillestare të përmasës n,
mund të ndiqet me anë të një rruge që nga rrënja deri tek një gjethe e pemës së vendimit, dhe
numri i krahasimeve të kryera nga algoritmi në një ekzekutim të tillë është i barabartë me
numrin e brinjëve në këtë rrugë. Pra, numri i krahasimeve, në rastin më të keq të organizimit
të të dhënave fillestare, është i barabartë me lartësinë e pemës së vendimit të algoritmit.

Idea qëndrore e këtij modeli mbështetet në vëzhgimin sipas të cilit një pemë me një numër të
dhënë gjethesh, që është diktuar nga numri i rezultateve të mundshme, duhet të jetë
mjaftueshmërisht e lartë për të patur kaq gjethe. Ndërkaq provohet që për çdo pemë binare
me l gjethe dhe me lartësi h (lartësia e një peme është gjatësia e rrugës së thjeshtë më të gjatë
nga rrënja tek gjethja dhe rrënja e ka lartësinë 0), është i vërtetë mosbarazimi

ℎ ≥ ⌈log 2 𝑙⌉ (13.1)
Me të vërtetë, një pemë binare me lartësi h me numrin më të madh të gjetheve, i ka të gjitha
gjethet e saj në nivelin e fundit. Pra, numri më i madh i gjetheve në një pemë të tillë është 2h.
Me fjalë të tjera, 2h ≥ l, nga e cila rrjedh menjëherë (13.1).
13 Kufizimet e fuqisë së algoritmeve | 240

Mosbarazimi (13.1), përcakton një kufi më të ulët për lartësinë e pemëve binare të vendimit
dhe si rrjedhim, numrin e krahasimeve të kryera në rastin më të keq të algoritmeve të bazuara
në krahasime (numri i veprimeve nuk mund të bëhet më i vogël se aq). Një kufi i tillë u quajt
më lart kufiri më i ulët i informacionit teorik (information-theoretic lower bound). Këtë
teknikë do ta ilustrojmë më poshtë për problemin e renditjes së një tabele.

Pemët e vendimit për algoritmet e renditjes


Pjesa më e madhe e algoritmeve të renditjes janë algoritme që bazohen në krahasim,
domethënë,, krahasimi i dy elementeve është veprimi bazë i algoritmeve të tillë. Prandaj, me
anë të studimit të cilësive të pemëve të vendimit për algoritmet e renditjes me bazë
krahasimin, ne mund të nxjerrim kufijtë më të ulët të efektshmërisë kohore të algoritmeve të
tillë.

Për fat të keq, pemët e vendimit nuk kanë ndonjë mënyrë që të përshkruajnë lëvizjen e të
dhënave, prandaj problemi perifrazohet pak më ndryshe. Ne mund të interpretojmë një
rezultat të një algoritmi të renditjes si gjetjen e një përkëmbimi të indekseve të elementeve të
listës së të dhënave fillestare që vendos listën e elementeve në rendin rritës. Për shembull, për
rezultatin a < c < b të përftuar nga lista a, b, c (figura 13.2), përkëmbimi në fjalë është 1, 3, 2.

Një treshe shkronjash mbi nyje tregon gjendjen e tabelës që do të renditet. Vëmë në dukje
gjithashtu dy krahasime të tepërta b < a vetëm me një rezultat të mundshëm për shkak të
rezultateve të disa krahasimeve të bëra me parë.
abc
po jo
a<b

abc abc
po jo po jo
a<c b<c

abc cba bac cba


po jo jo po jo po
b<c b<a a<c b<a

a<b<c a<c<b c<a<b b<a<c b<c<a c<b<a

Figura 13.2 Pema e vendimit për Selection sort me tre elemente

Përderisa numri i përkëmbimeve të mundshme të indekseve është n! atëherë çdo pemë


vendimi për renditjen do të ketë të paktën n! gjethe. Mosbarazimi (13.1) sjell si rrjedhim që
lartësia e një peme vendimi binare për çdo algoritëm renditje të bazuar në krahasim dhe pra
numri i krahasimeve në rastin më të keq të kryer nga një algoritëm i tillë nuk mund të jetë më
i vogël se ℎ ≥ ⌈log 2 𝑙⌉

𝑇𝑘𝑘𝑘 (𝑛) ≥ ⌈log 2 𝑛!⌉ (13.2)

Duke përdorur formulën Stirling për llogaritjen e n! do të përftojmë:

log 2 𝑛 log 2 2𝜋
⌈log 2 𝑛 !⌉ ≈ log 2 √2𝜋𝜋(𝑛/𝑒)𝑛 = 𝑛 log 2 𝑛 − 𝑛 log 2 𝑒 + + ≈ 𝑛 log 2 𝑛
2 2

Me fjalë të tjera, për të renditur një tabelë me n-elemente me anë të një algoritmi që bazohet
në krahasime, janë të domosdoshme të paktën n log2n krahasime. Kujtojmë që algoritmi
13 Kufizimet e fuqisë së algoritmeve | 241

Mergesort kryen po këtë numër krahasimesh në rastin më të keq, pra ai është asimptotikisht
optimal. Si rrjedhim kufiri më i ulët n log2n është i ngushtë dhe nuk mund të përmirësohet në
mënyrë esenciale.

13.3 Vështirësitë e algoritmeve numerike

Analiza numerike, zakonisht përshkruhet si një degë së informatikës, që merret me algoritmet


për zgjidhjen e problemeve matematike. Ky përshkrim ka nevojë për një sqarim të
rëndësishëm: problemet në fjalë janë probleme të matematikës së “vazhduar”, zgjidhjes së
ekuacioneve dhe sistemeve të ekuacioneve, vlerësimi i funksioneve të tilla si sin(x) dhe ln(x),
llogaritja e integraleve, etj, si kundërta e problemeve të matematikës diskrete që kanë të bëjnë
me struktura të tilla si grafet, pemët, përkëmbimet dhe kombinacionet. Interesimi ynë për
algoritme të efektshëm për probleme matematike rrjedh nga fakti që këta probleme përftohen
si modele të shumë fenomeneve të jetës reale si të botës natyrale ashtu edhe të shkencave
shoqërore.

Në fakt, analiza numerike përdorej si fusha kryesore e kërkimit, studimit dhe zbatimit të
informatikës. Me depërtimin e shpejtë të kompjuterave në zbatime të biznesit dhe në jetën e
përditshme, që kryesisht kanë të bëjnë me rezervimin dhe marrjen e informacionit, rëndësia
relative e analizës numerike është ngushtuar këta 30 vitet e fundit. Megjithatë, zbatimet e saj
të zgjeruara nga fuqia e kompjuterave moderne, vazhdojnë të shtrihen në të gjitha fushat e
kërkimit themelor dhe të teknologjisë. Kështu, pavarësisht se interesat e saj lidhen me botën e
gjerë të njehsimit modern, është e rëndësishme që të kihen të paktën disa nocione të sfidave
të veçanta që shtrojnë problemet e matematikës të vazhduar.

Ne nuk do të diskutojnë vështirësitë e ndryshme që shfaqen gjatë modelimit të punëve që


përshkruajnë fenomenet e jetës reale në terma matematike. Me supozimin se kjo punë tashmë
është e kryer, cilat janë atëherë vështirësitë kryesore me të cilat përballemi për të zgjidhur një
problem matematike? Vështirësia e parë kryesore është në fakt që pjesa më e madhe e
problemeve të analizës numerike nuk mund të zgjidhet me saktësi 19. Ata mund të zgjidhen
vetëm me përafërsi.

13.3.1 Gabimet e këputjes


Për shembull, vlera e ex në një pikë të dhënë mund të llogaritet me anë të përafrimit të serisë
së tij të pafundme të Tejlorit në pikën x = 0 me anë të një shume të fundme të termave të tij të
parë të quajtur polinomi i Taylor-it (Taylor polynomial) i shkallës n:

𝑥2 𝑥𝑛
𝑒𝑥 ≈ 1 + 𝑥 + + ⋯+ (13.3)
2! 𝑛!

Le të japim një shembull tjetër, integrali i caktur i një funksioni mund të përafrohet me anë të
një numri të fundëm të shumave të peshuara të vlerave të tij, i ashtuquajturi rregulli i trapezit
të përbërë (composite trapezoidal rule)

𝑏 ℎ
∫𝑎 𝑓(𝑥)𝑑𝑑 ≈ 2 [𝑓(𝑎) + 2 ∑𝑛𝑖=1 𝑓(𝑥𝑖 ) + 𝑓(𝑏)] (13.4)

19
Zgjidhja e sistemeve të ekuacioneve linearë dhe vlerësimi i polinomeve që kemi parë në leksionet e kaluara
janë përjashtime të rralla nga ky rregull.
13 Kufizimet e fuqisë së algoritmeve | 242

ku, h = (b – a) / n, xi = a + ih për i = 0, 1, …, n

Figura 13.3 Rregulli i trapezit për llogaritjen e integralit të caktuar

Gabimet e përafrimeve të tilla quhen gabime të këputjes (truncation errors). Një nga detyrat
kryesore të analizës numerike është që të vlerësojë madhësinë e gabimeve të këputjes. Kjo
realizohet me anë të përdorimit të mjeteve të njehsimit diferencial dhe integral që nga më të
thjeshtat deri tek ato më të ndërlikuarat. Për shembull, për përafrimin (13.3) do të kemi

𝑥2 𝑥𝑛 𝑀
�𝑒 𝑥 − �1 + 𝑥 + +⋯+ �� ≤ (𝑛+1)! |𝑥|𝑛+1 (13.5)
2! 𝑛!

ku 𝑀 = max 𝑒 𝜉 në segmentin me pika fundore 0 dhe x. Kjo formulë bën të mundur që të


përcaktohet rendi e polinomit të Taylor-it, i nevojshëm për të siguruar një nivel saktësie të
paracaktuar për përafrimin (13.3).

Për shembull nëse duam ta llogaritin e0.5 me anë të formulës (13.5) dhe të garantojmë që
gabimi i këputjes do të jetë më i vogël se 10−4, ne duhet të procedojmë si më poshtë. Së pari
të vlerësojmë M me anë të formulës (13.5):

𝑀 = max eξ ≤ 𝑒 0.5 < 2.


0≤ξ<0.5

Duke përdorur këtë kufi dhe nivelin e dëshëruar të saktësisë prej 10−4 (gabimi i këputjes), ne
përftojmë nga formula (13.5)

𝑀 2
|0.5|𝑛+1 < 0.5𝑛+1 < 10−4
(𝑛 + 1)! (𝑛 + 1)!

Për të zgjidhur inekuacionin e fundit ne mund të llogaritim disa nga vlerat e para të barazimit

2 2−𝑛
0.5𝑛+1 =
(𝑛 + 1)! (𝑛 + 1)!

për të vërejtur se vlera më e vogël e n për të cilën mosbarazimi është i vërtetë është 5.

Në mënyrë të ngjashme, për përafrimin (13.4), kufiri standard i gabimit të këputjes jepet nga
mosbarazimi

𝑏 ℎ (𝑏−𝑎)ℎ2
�∫𝑎 𝑓(𝑥)𝑑𝑑 − [𝑓(𝑎) + 2 ∑𝑛−1
𝑖=1 𝑓(𝑥𝑖 ) + 𝑓(𝑏)]� ≤ 𝑀2 , (13.6)
2 12

ku, 𝑀2 = 𝑚𝑚𝑚|𝑓 ′′ (𝑥)| 𝑛𝑛 𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠𝑠 𝑎 ≤ 𝑥 ≤ 𝑏 .


13 Kufizimet e fuqisë së algoritmeve | 243

13.3.2 Gabimet e rrumbullakimit

Lloji tjetër i gabimeve, i quajtur gabimet e rrumbullakimit (round off errors), shkaktohet nga
saktësia e kufizuar me anë të së cilës ne paraqesim numrat reale në kompjuterat digjitalë.
Këto gabime nuk ndodhin vetëm për të gjithë numrat irracionalë (të cilët nga përkufizimi,
kërkojnë një numër të pafundëm shifrash për paraqitjen e tyre të saktë) por gjithashtu edhe
për shumë numra racionalë. Në pjesën dërmuese të rasteve, numrat realë paraqiten në
kujtesën e kompjuterit si numra me pikë notuese (floating-point),

±𝑑1 𝑑2 ⋯ 𝑑𝑝 ∙ 𝐵𝐸 (13.7)

ku B është baza e paraqitjes, zakonisht 2 ose 16 (ose për llogaritësa të pandërlikuar 10); d1,
d2, …, dp, janë shifrat (0 ≤ di < B për i =1, 2, …, p dhe d1 > 0 edhe në se numri është 0) që
paraqesin së bashku pjesën fraksionale të numrit dhe që quhet mantisa (mantissa) e tij; E
është një eksponent (exponent) i plotë.

Saktësia e paraqitjes me pikë notuese varet nga numri i shifrave të rëndësishme p në


paraqitjen (13.7). Pjesa më e madhe e kompjuterave lejojnë dy ose tre nivele saktësie: saktësi
e njëfishtë (single precision) (në mënyrë tipike e njëvlershme me 6 apo 7 shifra dhjetore të
vlefshme), saktësia e dyfishtë (double precision) (13 apo 14 shifra dhjetore me vlerë) dhe
saktësia e zgjeruar (extended precision) (19 apo 20 shifra dhjetore të vlefshme). Përdorimi i
aritmetikës me saktësi tepër të lartë ngadalson llogaritjet por mund të ndihmojë për të
shmangur disa nga problemet që shkakton rrumbullakimi. Saktësia më e lartë mund të duhet
të përdoret vetëm në një hap të veçantë të algoritmit në fjalë.

Ashtu si në çdo lloj përafrimi, është e rëndësishme të bëjmë dallimin ndërmjet gabimit
absolut (absolute error) dhe gabimit relativ (relative error) të paraqitjes së një numri 𝛼 ∗ me
anë të përafrimit të tij 𝛼:

gabimi absolut = |𝛼 − 𝑎∗ | (13.8)

|𝑎−𝑎∗ |
gabimi relativ = (13.9)
|𝑎∗ |

(Gabimi relativ është i papërcaktuar nëse 𝛼 ∗ = 0. )

Numrat shumë të mëdhenj dhe numrat shumë të vegjël nuk mund të paraqiten me anë të
aritmetikës me pikë notuese për shkak të fenomenit të quajtur derdhje nga sipër (overflow)
dhe derdhje nga poshtë (underflow), respektivisht. Një derdhje nga sipër ndodh atëherë kur
një veprim aritmetik prodhon një rezultat jashtë kufijve të caktura për numrat me pikë
notuese. Shembull tipik derdhjeje nga sipër është shumëzimi i numrave të mëdhenj ose
pjesëtimi me numra shumë të vegjël. Ndonjëherë ne mund ta eliminojmë këtë problem duke
bërë thjesht një ndryshim në rradhën e vlerësimit të një shprehjeje (për shembull, (1029 ⋅ 1130)
/ (1230) = 1029 ⋅ (11/12)30) ose duke e zëvendësuar një shprehje të dhënë me një shprehje tjetër
të njëjtë (për shembull, llogaritja e kombinacionit �100
2
� të mos kryhet si 100!/(2!(100 ─ 2)!)
por si (100⋅99)/2) ose me llogaritjen e logaritmit të një shprehjeje në vend të shprehjes vetë.
13 Kufizimet e fuqisë së algoritmeve | 244

Një derdhje nga poshtë ndodh kur rezultati i një veprimi është një fraksion jozero i një
madhësie shumë të vogël që nuk mund të paraqitet si një numër jozero me pikë notuese.
Zakonisht numra të tillë zëvendësohen automatikisht me zero dhe një sinjal i veçantë
prodhohet nga hardueri për të treguar se ka ndodhur një ngjarje e tillë.

Po ashtu, është e rëndësishme të kujtojmë që përveç paraqitjes jo të saktë, veprimet aritmetike


të kryera në një kompjuter nuk janë gjithmonë të sakta. Në veçanti, zbritja e dy numrave
shumë të afërt, të paraqitur me pikë notuese, mund të shkaktojë një rritje të madhe të gabimit
relativ. Ky fenomen quhet anullimi i zbritjes (subtractive cancellation).

Shembull 1. Le të konsiderojmë dy numrat irracionalë

α*= π = 3.14159265··· dhe β* = π – 6⋅10-7 = 3.14159205···

të paraqitur me numra me pikë notuese si α= 0.3141593⋅101 dhe β = 0.3141592⋅101.

Gabimet relative të këtyre përafrimeve janë të vegjël:

|𝛼 − 𝛼 ∗ | 0.0000003 ⋯ 4 −7
= < 10
|𝑎∗ | 𝜋 3
dhe
|𝛽 − 𝛼 ∗ | 0.00000005 ⋯ 1 −7
= < 10
|𝛽 ∗ | 𝜋 − 6 ∙ 10−7 3

respektivisht. Për diferencën e dy numrave γ = α - β, gabimi relativ i diferencës γ* = α* - β*


është

|𝛾 − 𝛾 ∗ | 10−6 − 6 ∙ 10−7 2
= =
|𝛾 ∗ | 6 ∙ 10−7 3

që është shumë i madh për një gabim relativ, pavarësisht përafrimit shumë të saktë si për 𝛼
ashtu edhe për β.

Vëmë në dukje se mund të arrihet në një zmadhim të ndjeshëm të gabimit të rrumbullakimit


nëse një diferencë e tillë, me saktësi të vogël, do të përdoret si pjesëtues. (Këtë problem e
kemi takuar në leksionin ku kemi trajtuar eleminimin e Gausit.) Shumë algoritme përfshijnë
me mijra ose miliona veprime aritmetike për të dhëna fillestare tipike. Për disa algoritme të
tillë, përhapja e gabimeve të rrumbullakimit bëhet një problem i madh si nga këndvështrimi
praktik ashtu edhe nga ai teorik. Për disa algoritme, gabimet e rrumbullakimit mund të
përhapen nëpërmjet veprimeve të algoritmit me një efekt rritës. Kjo cilësi shumë e
padëshërueshme për algoritmet numerikë quhet paqëndrueshmëri (instability). Disa nga
problemet shfaqin një ndjeshmëri shumë të lartë ndaj ndryshimit të të dhënave fillestare dhe
për ta është e pamundur që të hartohet një algoritëm zgjidhës i qëndrueshëm. Probleme të
tilla quhen të keq-kushtëzuar (ill-conditioned).

Shembull 2. Le të konsiderojmë sistemin e mëposhtëm me dy ekuacione dhe dy të


panjohura:

1.001𝑥 + 0.999𝑦 = 2

0.999𝑥 + 1.001𝑦 = 2
13 Kufizimet e fuqisë së algoritmeve | 245

Zgjidhja e tij e vetme është x = 1, y = 1. Për të parë se sa i ndjeshëm është ky sistem ndaj
ndryshimeve të vogla të krahut të djathtë, le të konsiderojmë sistemin me të njëjtët
koeficientë por me krah të djathtë të ndryshuar pak fare,

1.001𝑥 + 0.999𝑦 = 2.002



0.999𝑥 + 1.001𝑦 = 1.998

Zgjidhja e vetme e këtij sistemi është x = 2, y = 0, e cila është tepër larg nga zgjidhja e
sistemit të mëparshëm. Vëmë në dukje që matrica kryesore e sistemit është shumë pranë të
qenurit singulare. Prandaj, një ndryshim i vogël në koeficientët e tij mund të prodhojë ose një
sistem pa zgjidhje ose me një pafundësi zgjidhjesh në varësi të vlerave të krahut të djathtë.

Le ta përfundojmë diskutimin me problemin e mirënjohur të gjetjes të rrënjëve të ekuacionit


kuadratik

ax2 + bx + c = 0 (13.10)

me koeficienta reale a, b dhe c (𝑎 ≠ 0). Në përputhje me algjebrën e shkollës së mesme,


ekuacioni (13.10) ka rrënjë reale atëherë dhe vetëm atëherë kur dallori D = b2 − 4ac është
jonegativ dhe rrënjët llogariten me anë të formulës:

−𝑏±√𝑏2 −4𝑎𝑎
𝑥1,2 = (13.11)
2∙𝑎

Megjithëse formula (13.11) furnizon një zgjidhje të plotë të problemit të shtruar në aspektin
matematik, ajo është larg të qenurit një zgjidhje e plotë për një hartues algoritmesh. Pengesa e
parë kryesore është në vlerësimin e rrënjës katrore. Bile për pjesën më të madhe të numrave
të plotë D, √D është një numër irracional që mund të llogaritet vetëm me përafërsi. Ekziston
një metodë për llogaritjen e rrënjës katrore që është shumë më e mirë sesa ajo e mësuar në
shkollën e mesme. Është një rrjedhim i metodës së Njutonit (Newton’s method) për zgjidhjen
e ekuacioneve. Kjo metodë prodhon vargun {xn} të përafrimeve të √D , ku D është një numër
i dhënë jonegativ, sipas formulës

1 𝐷
𝑥𝑛+1 = �𝑥𝑛 + � 𝑝𝑒̈ 𝑟 𝑛 = 1,2, … (13.12)
2 𝑥𝑛

ku përafrimi fillestar x0 mund të zgjidhet në mënyra të ndryshme, ku njëra prej tyre është x0
= (1 +D) /2. Nuk është e vështirë që të provohet që vargu (13.12) është një varg rigorozisht
zbritës (në se D ≠ 1) dhe konvergjon tek √D. Ne mund ta ndalojmë prodhimin e termave ose
kur diferenca ndërmjet dy termave të njëpasnjëshëm është më e vogël sesa gabimi i lejuar i
paracaktuar ε > 0,

xn – xn+1 < ε
2
ose kur 𝑥𝑛+1 është mjaftueshmërisht afër D. Në veçanti, në 0.25 ≤ D < 1 atëherë jo më
shumë se katër iteracione janë të mjaftueshëm që të garantojnë se,

�𝑥𝑛 − √𝐷� < 4 ∙ 10−15 ,


13 Kufizimet e fuqisë së algoritmeve | 246

dhe ne gjithmonë mund ta shkallëzojmë një vlerë të dhënë d në një interval [0.25, 1) me anë
të formulës 𝑑 = 𝐷 ∙ 2𝑝 , ku p është një numër i plotë çift.

Shembull 3. Le të zbatojmë algoritmin e Njutonit për të llogaritur √2 (për thjeshtësi nuk do


të zbatojmë shkallëzimin e përafrimit fillestar). Veprimet do të kryen të rrumbullakosura me
gjashtë shifra pas pikës dhjetore dhe për të treguar rrumbullakimin do të përdoret simboli
standard i analizës numerike =̇ .

1
𝑥0 = (1 + 2) = 1.500000
2
1 2
𝑥1 = �𝑥0 + � =̇ 1.416667
2 𝑥0

1 2
𝑥2 = �𝑥1 + � =̇ 1.414216
2 𝑥1

1 2
𝑥3 = �𝑥2 + � =̇ 1.414214
2 𝑥2

1 2
𝑥4 = �𝑥3 + � =̇ 1.414214
2 𝑥4

Në këtë iteracion do të ndalojmë mbasi𝑥4 = 𝑥3 =̇ 1.414214, dhe të gjitha përafrimet e tjera


do të jenë të njëjta. Vlera e saktë e √2 është 1.41421356···. Duke lënë mënjanë çështjen e
llogaritjes së rrënjës katrore, a jemi të lirë të shkruajmë një program të bazuar në formulën
(13.11)? Përgjigja do të jetë “Jo” për shkak të ndikimit të mundshëm të gabimeve të
rrumbullakimit. Ndër pengesat e tjera, këtu përballemi me kërcënimin e anullimit të zbritjes
(subtractive cancellation). Në se b2 do të jetë shumë më e madhe se 4ac, atëherë √𝑏 2 − 4𝑎𝑎
do të jetë shumë afër │b│dhe rrënja e llogaritur me anë të formulës (13.11) do të ketë një
gabim relativ të madh.

Shembull 4. Le të shohim tani një problem të trajtuar nga George Forsythe 20. Le të jetë
dhënë ekuacioni x2 – 105x + 1 = 0.

Rrënjët e tij të vërteta me 11 shifra të vlershme janë

𝑥1∗ =̇ 99999.999990
𝑥2∗ =̇ 0.000010000000001.

Nëse përdorim formulën (13.11) dhe kryejmë llogaritjet me aritmetikën e pikës notuese le të
themi me shtatë shifra me vlerë, ne do të përftojmë

(−𝑏 2 ) = 0.1000000 ∙ 1011


4𝑎𝑎 = 0.4000000 ∙ 101

20
George E. Forsythe (1917-1972), kërkues në analizën numerike, që ka lojtur një rol kryesor në vendosjen e
shkencës së kompjuterave si një disiplinë akademike e veçantë në Sh.B.A.
13 Kufizimet e fuqisë së algoritmeve | 247

𝐷 =̇ 0.1000000 ∙ 1011
√𝐷 =̇ 0.1000000 ∙ 106
−𝑏 + √𝐷
𝑥1 =̇ =̇ 0.1000000 ∙ 106
2∙𝑎
−𝑏 − √𝐷
𝑥2 =̇ =̇ 0.
2∙𝑎

Dhe ndërsa gabimi relativ i përafrimit të 𝑥1∗ me anë të x1 është shumë i vogël, për rrënjën e
dytë është shumë i madh:

𝑥2 − 𝑥2∗
� � = 1 (𝑑. 𝑚. 𝑡ℎ. 100%)
𝑥2∗

Për të shmangur mundësinë e anullimit të zbritjes (subtractive cancellation) në formulën


(13.11) ne mund të përdorim në vend të saj një formulë tjetër, të përftuar si më poshtë:

−𝑏 + √𝑏 2 − 4𝑎𝑎 �−𝑏 + √𝑏 2 − 4𝑎𝑎�(−𝑏 − √𝑏 2 − 4𝑎𝑎) 2𝑐


𝑥1 = = =
2𝑎 2𝑎(−𝑏 − �𝑏 2 − 4𝑎𝑎) −𝑏 − √𝑏 2 − 4𝑎𝑎

tashmë pa rrezikun e anullimit të zbritjes në emërues nëse b > 0. Ndërsa x2 mund të llogaritet
nga formula standarde:

−𝑏 − √𝑏 2 − 4𝑎𝑎
𝑥2 =
2𝑎

pa rrezikun e anullimit, gjithashtu, për një vlerë pozitive të b.

Rasti b < 0 është simetrik dhe mund të përdorim formulat

−𝑏 + √𝑏 2 − 4𝑎𝑎
𝑥1 =
2𝑎
dhe
2𝑐
𝑥2 =
−𝑏 + √𝑏 2 − 𝑎𝑎
.
Rasti i b = 0 mund të konsiderohet me anë të secilit prej dy rasteve.

Ka edhe disa pengesa të tjera ne zbatimin e formulës (13.11), të cilat lidhen me kufizimet e
aritmetikës me pikë notuese. Nëse a është shumë e vogël, pjesëtimi me a mund të shkaktojë
derdhje. Këtu nuk shihet të ketë ndonjë mënyrë për të shmangur anullimin e zbritjes në
llogaritjen e b2 - 4ac përveç se llogaritjes së saj me saktësi të dyfishtë; etj. Këto probleme janë
trajtuar nga William Kahan i Universitetit të Torontos dhe algoritmi i tij konsiderohet si një
arritje në historinë e analizës numerike. Në leksionin pasardhës do të paraqesim edhe një
çështje më tepër: tri metoda klasike për zgjidhjen e ekuacioneve me një të panjohur.
13 Kufizimet e fuqisë së algoritmeve | 248

13.4 Përmbledhje

• Në një klasë të dhënë të algoritmeve për të zgjidhur një problem të veçantë, një kufi më i
ulët tregon efektshmërinë më të mirë të mundshme për çdo algoritëm të kësaj klase.
• Një kufi më i ulët i qartë bazohet në llogaritjen e numrit të të dhënave fillestare dhe në
numrin e rezultateve që janë të nevojshme të prodhohen
• Një kufi më i ulët i informacionit teorik zakonisht përfitohet me anë të mekanizmit të
pemëve të vendimit. Kjo teknikë është veçanërisht e dobishme për algoritmet e renditjes
dhe të kërkimit të bazuara në veprimin e krahasimit. Në veçanti:
çdo algoritëm i përgjithshëm renditjeje i bazuar në veprimin e krahasimit duhet të
kryejë të paktën n log n krahasime në rastin më të keq.
• Metoda e kundërshtarit për përcaktimin e kufirit më të ulët bazohet në ndjekjen e logjikës
të një kundërshtari të “pabesë” që shtyn algoritmin në rrugën që shpenzon më shumë
kohë.
• Një kufi më i ulët mund të përcaktohet edhe me anë të reduktimit, domethënë,, me anë të
reduktimit të një problemi me kufi më të ulët të njohur në problemin në fjalë.
• Analiza numerike është një degë e informatikës që trajton zgjidhjen e problemeve
matematike të vazhdueshëm. Në zgjidhjen e një pjese të madhe të problemeve të tillë
shfaqen dy lloje gabimesh: gabimet e rrumbullakimit dhe gabimet e këputjes. Gabimet e
këputjes rrjedhin nga zëvendësimi i objekteve të pafundëm me anë të përafrimeve të
fundme të tyre. Gabimet e rrumbullakimit shkaktohen nga pasaktësia e paraqitjes se
numrave në një kompjuter dixhital.
• Anullimi i zbritjes ndodh si rezultat i zbritjes së dy numrave me pikë notuese shumë të
afërt. Kjo mund të shpjerë në një rritje të fortë të gabimit relativ në rrumbullakim dhe
prandaj duhet të shmanget (ose me anë të ndryshimit të shprehjes llogaritëse ose me anë
të përdorimit të një saktësie më të lartë në llogaritje)
• Të shkrojturit e një programi të përgjithshëm për zgjidhjen e ekuacionit kuadratik ax2 +
bx + c = 0 është një punë e vështirë. Problemi i llogaritjes së rrënjës katrore mund të
zgjidhet me anë të metodës së Njutonit; problemi i anullimit të zbritjes mund të shmanget
duke përdorur formula të ndryshme në varësi të faktit që koeficienti b është pozitiv apo
negativ dhe me anë të llogaritjes së dallorit b2 – 4 ac më saktësi të dyfishtë.
14 Përballjame kufizimet e fuqisë së algoritmve | 249

14 Përballja me kufizimet e fuqisë së algoritmeve

Rezultatet e të mësuarit

Në fund të këtij kapitulli studenti duhet të jetë i aftë:

• të njohë metodën Backtracking për shmangien e pafuqisë së algoritmeve për zgjidhjen e


disa problemeve;
• të njohë mdetoden Branch-and-bound për shmangien e pafuqisë së algoritmeve për
zgjidhjen e disa problemeve;
• të njohë disa algoritme për zgjidhjen e ekuacioneve jolinearë.

Siç thamë në leksionin e kaluar, ka probleme që janë të vështirë për t’u zgjidhur
algoritmikisht. Në të njëjtën kohë, disa prej këtyre problemeve janë aq të rëndësishëm saqë
nuk mund të anashkalohen. Në këtë leksion do të skicohen disa mënyra për t’ja dalë mbanë
këtyre problemeve të vështirë.

Në seksionet 14.1 dhe 14.2 do të paraqiten dy teknika të reja për hartimin e algoritmeve, të
quajtura kthimi mbrapa (Backtracking) dhe degëzo-dhe-kufizo (Branch-and-bound), që
shpesh bëjnë të mundur të zgjidhen të paktën disa nga rastet e problemeve të vështirë
kombinatorikë, kur numri i të dhënave fillestare është i madh. Të dy teknikat mund të
konsiderohen si një përmirësim që i bëhet kërkimit shterues të diskutuar në një nga leksionet
e kaluara. Në ndryshim nga kërkimi tërësor, ato i ndërtojnë zgjidhjet kandidate duke
shqyrtuar një komponente në një kohë dhe vlerësojnë zgjidhjet e pjesshme të ndërtuara: nëse
komponentet që mbeten nuk janë potencialisht të vlerësueshme atëherë ato nuk prodhohen
fare. Kjo rrugë bën të mundur që të zgjidhen raste të përmasave të mëdha të problemeve të
vështira kombinatorike, megjithëse, në rastin më të keq, përsëri përballemi me të njëjtin
problem të shpërthimit eksponencial, që na rrethon në kërkimin tërësor.

Të dyja metodat, backtracking dhe branch-and-bound mbështeten në ndërtimin e të


ashtuquajturës pema “gjendje-hapsirë” (state-space tree), nyjet e së cilës reflektojnë zgjedhjet
e veçanta të bëra për një komponente të zgjidhjes. Të dyja teknikat heqin dorë nga një nyje
sapo që sigurohen që nuk mund të përftohet zgjidhja e problemit nëse konsiderohen nyjet
pasardhëse (descendants node’s). Teknikat ndryshojnë vetëm në natyrën e problemeve që
zgjidhin. Branch-and-bound përdoret vetëm për problemet e optimizimit për shkak se ajo
mbështetet në llogaritjen e një kufiri për vlerat e mundshme të funksionit qëllim të problemit.
Backtracking nuk kufizohet nga kjo kërkesë, por më shpesh, ajo përdoret në probleme
jooptimizimi. Ndryshimi tjetër ndërmjet këtyre dy metodave qendron në rradhën në mënyrën
e prodhimit toe nyjeve të pemës “gjendje-hapsirë”. Për backtracking, kjo pemë zhvillohet së
pari në thellësi. Branch-and-bound mund të prodhojë nyjet në përputhje me disa rregulla; më
i natyrshmi prej tyre është i ashtuquajturi rregulli i pari-më i mirë që do të paraqitet në
seksioni 14.2.

Seksioni 14.4 i kushtohet algoritmeve për zgjidhjen e ekuacioneve jolinearë. Pas një
diskutimi të shkurtër mbi këtë klasë problemesh shumë të rëndësishme, do të shqyrtojmë tri
metoda klasike për gjetjen e përafërt të rrënjëve: metoda e përgjysmimit, metoda e pozicionit
të gabuar dhe metoda e Njutonit.
14 Përballjame kufizimet e fuqisë së algoritmve | 250

14.1 Metoda Backtracking

Gjatë këtij cikli leksionesh ne kemi takuar probleme që kërkojnë gjetjen e një elementi që
gëzon një cilësi të veçantë në një fushë që rritet me shpejtësi eksponenciale apo edhe më
shpejt kur rritet numri i të dhënave fillestare: për shembull nënbashkësia më e vlershme e
artikujve për problemin e çantës së shpinës. Kemi theksuar se shumë nga këta probleme nuk
mund të zgjidhen në kohë polinomiale. Gjithashtu kemi diskutuar se disa nga këta probleme,
të paktën në parim, mund të zgjidhen me anë të kërkimit tërësor. Teknika e kërkimit tërësor
propozon prodhimin e të gjitha zgjidhjeve kandidate dhe identifikon njerën ose disa prej tyre
që gëzojnë cilësinë e kërkuar.

Metoda backtracking është një variant më inteligjent i kësaj qasjeje. Idea themelore është
ndërtimi i zgjidhjes fillon me një komponente të pjesshme të zgjidhjes cila vlerësohet si më
poshtë. Nëse komponentja e pjesshme e ndërtuar mund të zhvillohet më tej pa kundërshtuar
kushtet e problemit, ajo qendron si mundësia e parë e ligjshme për të përftuar komponenten
tjetër. Nëse kjo nuk është mundësi e ligjshme për komponenten pasuese, atëherë nuk ka
alternativë për asnjë nga komponentet që mbeten për t’u marrë në konsideratë. Në këtë rast,
algoritmi kthehet mbrapsh për të zëvendësuar komponenten e fundit të zgjidhjes së pjesshme
të ndërtuar me një mundësi tjetër.

Siç thamë, zbatimi i kësaj mënyre përpunimi kryhet me anë të ndërtimit të pemës “gjendje-
hapsirë”. Rrënja e saj paraqet një gjendje fillestare para se të ketë filluar kërkimi për një
zgjidhje. Nyjet e nivelit të parë në pemë paraqesin zgjedhjet e kryera për komponenten e parë
të një zgjidhjeje, nyjet e nivelit të dytë paraqesin zgjedhjet për komponenten e dytë, dhe
kështu me rradhë. Një nyje në pemën “gjendje-hapsirë” thuhet se është premtuese
(promising) nëse ajo i korrespondon një zgjidhjeje të pjesshme që mund të shpjerë në një
zgjidhje të plotë; përndryshe, ajo quhet jopremtuese (nonpromising). Gjethet paraqesin ose
funde jo-premtuese të vdekura (të thara) ose zgjidhjet e plota të gjetura nga algoritmi. Në
pjesën më të madhe të rasteve, një pemë “gjendje-hapsirë” për një algoritëm të tipit
backtracking ndërtohet me anë të kërkimit në thellësi. Nëse nyja e rradhës është premtuese,
fëmija e saj prodhohet me anë të shtimit të mundësisë së parë të ligjshme që ka mbetur për
komponenten tjetër të një zgjidhjeje dhe përpunimi shkon tek ky fëmijë. Nëse nyja e rradhës
del që nuk është premtuese algoritmi kthehet pas tek nyja prind për të shqyrtuar mundësinë
tjetër për komponenten e tij të fundit; nëse një mundësi e tillë nuk ekziston, ajo kthehet pas
një nivel më lart në pemë dhe kështu më tej. Së fundmi, nëse algoritmi arrin një zgjidhje të
plotë të problemit ai ose ndalon (nëse kërkohet vetëm një zgjidhje) ose vazhdon për kërkimin
e zgjidhjeve të tjera të mundshme.

14.1.1 Problemi i n-mbretëreshave

Si shembull i parë, kemi zgjedhur problemin e n-mbretëreshave (n-queens problem), një


shembull klasik në këtë kategori. Problemi kërkon vendosjen e n mbretëreshave në një fushë
shahu të përmasës n-herë-n në mënyrë të tillë që dy mbretëresha të mos kenë mundësi të
kërcënojnë njëra tjetrën drejtpërsëdrejti duke mos qenë në të njëjtën horizontale, vertikale apo
diagonale. Për n = 1 problemi ka zgjidhje të dukshme, dhe duket lehtë se nuk ka zgjidhje për
n = 2 dhe n = 3. Le të konsiderojmë tani problemin e 4-mbretëreshave dhe të përpiqemi ta
zgjidhim problemin me anë të teknikës backtracking. Meqenëse secila nga të katër
mbretëreshat duhet të vendoset në horizontalen e vet, gjithçka që duam të bëjmë është që të
caktojmë një vertikale për çdo mbretëreshë në fushën e paraqitur në figurën 14.1.
14 Përballjame kufizimet e fuqisë së algoritmve | 251

Figura 14.1 Fusha e shahut për problemin e 4-mbretëreshave

Vendosjet fillojnë në një fushë bosh dhe mbretëresha-1 vendoset në katrorin (1, 1). Pastaj
vendoset mbretëresha-2 në kuadratin (2, 3) mbasi nuk mund të vendoset as në pozicionin (2,
1) dhe as në pozicionin (2, 2) (mbasi shkelen kushtet e problemit). Por ky pozicion duket që
të jetë një fund i vdekur mbasi nuk do të jetë pozicion i pranueshëm për mbretëresha-3.
Kështu algoritmi kthehet mbrapa dhe vendos mbretëresha-2 në pozicionin tjetër të mundshëm
në kuadrantin (2, 4). Pastaj mbretëresha-3 vendoset në (3, 2) e cila përsëri rezulton të jetë një
fund i vdekur. Pastaj algoritmi kthehet pas për tek mbretëresha-1 dhe e zhvendos atë në
kuadratin (1, 2). Mbretëresha-2 kalon në (2, 4), mbretëresha-3 kalon në (3, 1) dhe
mbretëresha-4 në (4, 3) që është një zgjidhje e lejuar e problemit. Pema “gjendje-hapsirë” e
këtij kërkimi paraqitet në figurën 14.2. Në këtë figurë me X është shënuar një përpjekje e
pasuksesshme për të vendosur një mbretëreshë në shtyllën e treguar. Numri sipër nyjes tregon
rendin në të cilën janë prodhuar nyjet.

Figura 14.2 Pema “gjendje-hapsirë” për zgjidhjen e problemit të 4-mbretëreshave

Në qoftë se kërkohet që të gjendet një zgjidhje tjetër (dhe të tilla ka për problemin e 4-
mbretëreshave) atëherë algoritmi thjesht rifillon në gjethen në të cilën u ndalua. Në mënyrë
tjetër për gjetjen e zgjidhjeve të tjera është përdorimi i simetrisë së fushës.
14 Përballjame kufizimet e fuqisë së algoritmve | 252

14.1.2 Problemi i nënbashkësisë së shumave

Problemi i nënbashkësisë së shumave (subset-sum problem) shtrohet si më poshtë: Jepet një


bashkësi e n numrave natyralë S = {s1, … , sn} dhe një numër i plotë pozitiv d. Të gjendet një
nënbashkësi e S, shuma e elementeve të së cilës është e barabartë me d. ( Natyrisht që disa
raste të këtij problemi mund të mos kenë zgjidhje.)

Për shembull, për S = {1, 2, 5, 6, 8} dhe d = 9 janë dy zgjidhje: {1, 2, 6} dhe {1, 8}.

Për të zgjidhur problemin është më e përshtatshme që të renditen elementet e bashkësisë në


rendin rritës. Kështu do të supozojmë që

s1 ≤ s2 ≤ … ≤ sn .

Pema “gjendje-hapsirë” mund të ndërtohet si një pemë binare e ngjashme me atë të figurës
14.3 për rastin S = {3, 5, 6, 7} dhe d = 15. Numri brenda nyjes paraqet shumën e elementeve
tashmë të përfshirë nënbashkësinë e paraqitur nga nyja. Mosbarazimi poshtë një gjetheje
tregon arsyen e përfundimit. Rrënja e pemës paraqet pikën e fillimit dhe asnjë vendim për
elementet e dhënë nuk është marrë. Gjethja e majtë dhe e djathtë paraqesin respektivisht
përfshirjen (me s1) dhe përjashtimin e s1 (pa s1) në bashkësinë që po kërkohet. Në mënyrë të
ngjashme, të ecurit në të majtë në nivelin e parë i korrespondon përfshirjes së s2, ndërsa ecja
në të djathtë i korrespondon përjashtimit të saj, e kështu më tej. Kështu, një rrugë që nga
rrënja deri tek një nyje në nivelin e itë të pemës tregon se cili nga i numrat e parë është
përfshirë në nënbashkësinë e paraqitur nga kjo nyje.

0
me 3 pa 3

3 0
me 5 pa 5 me 5 pa 5

8 3 5 0
me 6 pa 6 me 6 pa 6 me 6 pa 6
0 + 13 < 15

14 8 9 3 11 5
me 7 pa 7
14 + 7 > 15 9 + 7 > 15 3 + 7 < 15 11 + 7 > 15 5 + 7 < 15

15 8

zgjidhja 8 < 15

Figura 14.3 Pema “gjendje-hapsirë” për problemin e nënbashkësisë së shumave

Regjistrojmë në nyje vlerën 𝑠′, të shumës së këtyre numrave. Në qoftë se 𝑠′ është e barabartë
me d, atëherë ne kemi zgjidhjen e problemit. Në këtë moment ose mund ta raportojmë këtë
rezultat dhe të ndalojmë ose mund të vazhdojmë me kthimin mbrapa që nga nyja prindrore
nëse kërkohen të gjitha zgjidhjet. Në qoftë se 𝑠 ′ nuk është e barabartë me d atëherë mund ta
braktisim nyjen si jopremtuese nëse ka vend një nga dy mosbarazimet e mëposhtme:

𝑠 ′ + 𝑠𝑖+1 > 𝑑 (shuma 𝑠′ është më e madhe)



𝑠 + ∑𝑛𝑗=𝑖+1 𝑠𝑗 <𝑑 (shuma 𝑠′ është më e vogël)
14 Përballjame kufizimet e fuqisë së algoritmve | 253

14.1.3 Shënime të përgjithshme

Nga shembujt e paraqitur duket sikur teknika backtracking është shumë e suksesshme, por në
të vërtetë nuk është gjithmonë kështu. Në rastin më të keq, ajo mund të prodhojë të gjithë
kandidatët në një pemë “gjendje-hapsirë” që rritet eksponencialisht (ose më shpejt) për
problemin në shqyrtim. Natyrisht që dëshira është që algoritmi backtracking të jetë i aftë të
shkurtojë mjaft degë të pemës “gjendje-hapsirë” para se të ekzekutohet jashtë kohe dhe
hapsire ose që të dyjave. Njihet se suksesi i kësaj strategjie ndryshon shumë, jo vetëm nga
problemi në problem por edhe nga një rast në tjetrin për të njëjtin problem.

Ekzistojnë disa dredhi që mund të ndihmojnë në reduktimin e përmasës së pemës “gjendje-


hapsirë”. Njëra prej tyre shfrytëzon simetrinë që paraqitet shpesh në problemet
kombinatorike. Për shembull, fusha e shahut për n-mbretëreshat ka disa simetri kështu që disa
nga zgjidhjet mund të merren me anë të reflektimit apo rrotullimit. Kjo sjell si rrjedhim, në
veçanti, që nuk është e nevojshme të konsiderojmë vendosjen e mbretëreshës së parë në
shtyllën e fundit ⌊𝑛/2⌋, mbasi çdo zgjidhje me mbretëreshën e parë në katrorin (1, i), ku
⌈𝑛/2⌉ ≤ 𝑖 ≤ 𝑛 , mund të përftohet me anë të reflektimit nga një zgjidhje me mbretëreshën e
parë në katrorin (1, n – i + 1). Ky vrojtim zvogëlon përmasën e pemës gati përgjysëm. Një
dredhi tjetër është për shembull, pararenditja e të dhënave si në problemin e nënshumave.

Do të ishte tepër e dëshërueshme që të jemi të aftë të vlerësojmë përmasën e pemës “gjendje-


hapsirë” në algoritmin backtracking. Si rregull, kjo është shumë e vështirë për ta bërë në
mënyrë analitike. Vlerësime probabilitare të rastit mesatar ka bërë D. Knuth.

Në përfundim tre gjëra mund të themi për sjelljen e algoritmit backtracking.

Së pari, ai në mënyrë tipike është i përdorshëm për problemet e vështira kombinatorike për të
cilët nuk ekziston algoritëm i efektshëm për të gjetur zgjidhjen e saktë.

Së dyti, në ndryshim nga kërkimi shterues, i cili është i dënuar që të jetë ekstremisht i
ngadaltë për të gjitha rastet e një problemi, kthimi mbrapa të paktën të mban me shpresë për
zgjidhjen e disa rasteve të përmasave jo të thjeshta në një sasi kohe të pranueshme. Kjo është
veçanërisht e vërtetë për problemet e optimizimit, për të cilët idea e kthimit mbrapa mund të
zgjerohet më tej me anë të vlerësimit të cilësisë të zgjidhjeve të pjesshme të ndërtuara. Në
seksionin pasues do të shpjegohet se si realizohet ky proces.

Së treti, madje edhe kur kthimi mbrapa nuk eleminon ndonjë nga elementet e pemës
“gjendje-hapsirë” të problemit dhe përfundon duke prodhuar të gjithë elementet e tij, ai
furnizon një teknikë të veçantë për bërjen e gjërave, e cila ka vlerë në vetvete.

14.2 Branch-and-bound

Kujtojmë që idea qendrore e kthimit mbrapa, diskutuar në seksionin e mëparshëm, ishte që


mund të ndërpritet një degëzim i pemës “gjendje-hapsirë” të problemit sapo të arrihet në
përfundimin se ajo nuk të shpie në zgjidhje. Kjo ide mund të përmirësohet më tej në qoftë se
kemi të bëjmë me një problem optimizimi, i cili kërkon të minimizojë apo të maksimizojë një
funksion qëllim, nënshtruar zakonisht disa kushteve (për shembull, gjatësia e një rrugë, vlera
e artikujve të zgjedhur, kosto e një detyre, e.t.j të ngjashme). Vëmë në dukje se në
terminologjinë standarde të problemeve të optimizimit, një zgjidhje e lejuar (feasible
14 Përballjame kufizimet e fuqisë së algoritmve | 254

solution) është një pikë në hapsirën e kërkimit të problemit që kënaq të gjitha kushtet e
problemit, ndërsa një zgjidhje optimale (optimal solution) është një zgjidhje e lejuar me
vlerën më të mirë të funksionit qëllim.

Krahasuar me teknikën bactracking, teknika branch-and-bound kërkon dy gjëra:

1. Një mënyrë për të furnizuar për çdo nyje të pemës “gjendje-hapsirë”, një kufi për vlerën
më të mirë të funksionit qëllim 21 në lidhje me çdo zgjidhje që mund të përftohet me anë të
shtimit të komponenteve të tjera të zgjidhjes së pjesshme të ndërtuar të paraqitur nga nyja.
2. Vlerën e zgjidhjes më të mirë deri në atë çast

Në qoftë se ky informacion është i mundshëm, ne mund të krahasojmë një vlerë kufi të nyjes
me vlerën e zgjidhjes më të mirë deri në atë çast: nëse vlera kufi nuk është më e mirë se
zgjidhja më e mirë e arritur deri në atë çast, domethënë, jo më e vogël për problemet e
minimizimit dhe jo më e madhe për problemet e maksimizimit, nyja nuk është e pranueshme
dhe mund të përfundohet (dega “krasitet”) mbasi nuk ka zgjidhje të përftuar prej saj që të
prodhojë një zgjedhje më të mirë sesa ajo që është tashmë. Kjo është idea kryesore e teknikës
branch-and-bound.

Në përgjithësi, ne përfundojmë një rrugë kërkimi në nyjen e rradhës të pemës “gjendje-


hapsirë” të algoritmit branch-and-bound për ndonjë nga arsyet e mëposhtme:

• Vlera e kufirit të nyjes nuk është më e mirë sesa vlera e zgjidhjes më të mirë të
deritanishme.
• Nyja paraqet një zgjidhje të palejuar për shkak se janë shkelur kushtet e problemit.
• Nënbashkësia e zgjidhjes së lejuar e paraqitur nga nyja përbëhet nga një pikë e vetme
(dhe pra nuk ka më zgjedhje për të bërë), në këtë rast ne krahasojmë vlerën e funksionit
qëllim për këtë zgjidhje të lejuar me atë të zgjidhjes më të mirë të deritanishme dhe të
përditësohet kjo e fundit me të mëparshmen nëse zgjidhja e re është më e mirë.

Problemi i çantës së shpinës


Le të diskutojmë tani se si mund të zbatohet teknika branch-and-bound për të zgjidhur
problemin e çantës së shpinës. Ky problem është paraqitur në leksionet e kaluara: jepen n
artikuj për secilin nga të cilët njihet pesha wi dhe vlera vi, për i =1, 2, n si dhe një çantë me
kapacitet W. Të gjendet kombinimi më me vlerë i artikujve që mund të futen në çantën e
dhënë. Do të ishte e përshtatshme të renditen artikujt e një rasti të dhënë në rendin zbritës të
raporteve të tyre vlerë ndaj peshës. Atëherë artikulli i parë jep fitimin më të mirë për njësi
peshe dhe i fundit jep fitimin më të keq për njësi peshe:

v1/w1 ≥ v2/w2 ≥ ⋯≥ vn/wn

Është e natyrshme që të strukturojmë pemën “gjendje-hapsirë” për këtë problem si një pemë
binare të ndërtuar si më poshtë (Figura 14.4). Çdo nyje në nivelin e itë të kësaj peme, 0 ≤ i ≤
n, paraqet të gjitha nënbashkësitë e n artikujve që përmban një zgjedhje e veçantë e bërë nga i
artikujt e renditur. Kjo zgjedhje e veçantë përcaktohet në mënyrë të vetme nga rruga që nga
rrënja deri tek nyja: një degë që shkon majtas tregon përfshirjen e artikullit pasardhës, ndërsa
një degë që shkon djathtas tregon përjashtimin e saj. Ne regjistrojmë peshën e përgjithshme w

21
Kufiri duhet të jetë një kufi më i poshtëm për një problem minimizimi dhe një kufi më i sipërm për një
problem maksimizimi
14 Përballjame kufizimet e fuqisë së algoritmve | 255

dhe vlerën e përgjithshme v të kësaj zgjedhjeje në nyje, së bashku me një kufi të sipërm ks
(upper bound) në vlerën e çdo nënbashkësie që mund të përftohet me anë të shtimit të zero
ose më shumë elementeve në këtë zgjedhje.

Një mënyrë e thjeshtë për të llogaritur kufirin e sipërm ks, është që të shtohet tek v, vlera e
përgjithshme e artikujve tashmë të zgjedhur, prodhimi i kapacitetit të mbetur në çantë P – p
me peshën njësi më të mirë të artikujve të mbetur, që është vi+1/pi+1:

ks = v + (W – w)( vi+1/wi+1) (14.1)


0
w = 0, v = 0
me 1 pa 1
ks = 100
1 2
w = 4, v = 40 w = 0, v = 0
me 2 pa 2

3 ks = 76 4 ks = 60
w = 11 w = 4, v = 40 më e vogël se nyja 8
me 3 pa 3
ks = 70
5 6
e palejuar w = 9, v = 65 w = 4, v = 40
me 4 pa 4
ks = 69 ks = 64
7 8
më e vogël se nyja 8
w = 12 w = 9, v = 65

ks = 65
e palejuar zgjidhja optimale

Figura 14.4 Pema “gjendje-hapsirë” për problemin e çantës së shpinës

Si një shembull, le të zbatojmë algoritmin branch-and-bound në rastin e një çante me


kapacitet W=10, në të cilën duam të futin katër artikuj me pesha {4, 7, 5, 3} dhe vlera
respektive {40, 42, 25, 12}

Në rrënjën e pemës “gjendje-hapsirë” (Figura 14.4), ende nuk janë zgjedhur artikuj. Pra, si
pesha e përgjithshme e artikujve tashmë të zgjedhur w ashtu edhe vlera e tyre e përgjithshme
v janë të barabarta me 0. Vlera e kufirit të sipërm e llogaritur nga formula (14.1) është 100.
Nyja 1, fëmija e majtë e rrënjës, paraqet nënbashkësinë që përfshin artikullin 1. Pesha e
përgjithshme dhe vlera e artikujve tashmë të përfshirë është 4 dhe 40, respektivisht; vlera e
kufirit të sipërm është 40 + (10 - 4) * 6 = 76. Nyja 2 paraqet nënbashkësitë që nuk përfshijnë
artikullin 1. Si rrjedhim, w = 0, v = 0, dhe ks = 0 + (10 - 0) * 6 = 60. Përderisa nyja 1 ka kufi
të sipërm më të madh sesa kufiri i sipërm i nyjes 2, ajo është më e premtuese për këtë
problem maksimizimi dhe ne degëzohemi së pari që nga nyja 1. Bijtë e saj, nyjet 3 dhe 4
paraqesin nënbashkësitë me artikullin 1 dhe pa artikullin 2, respektivisht. Përderisa pesha e
përgjithshme w e çdo nënbashkësie të paraqitur nga nyja 3 tejkalon kapacitetin e çantës, nyja
3 mund të quhet e përfunduar. Nyja 4 ka të njëjtën vlerë të w dhe v si prindi i saj; kufiri i
sipërm është ks i barabartë me 40 + (10 - 4) * 5 = 70. Duke zgjedhur nyjen 4 ndaj nyjes 2 për
degëzimin tjetër (pse?), ne marrim nyjet 5 dhe 6 respektivisht duke përfshirë dhe duke
përjashtuar artikullin 3. Pesha e përgjithshme dhe vlera po ashtu edhe kufijt e sipërm të kësaj
nyje janë llogaritur në të njëjtën mënyrë si për nyjet e mëparshme. Degëzimi nga nyja 5
prodhon nyjen 7, e cila paraqet një zgjidhje të palejuar dhe nyjen 8 që paraqet një
nënbashkësi të thjeshtë {1, 3}. (Edhe këtu nuk ka artikuj të tjerë për të marrë në konsideratë,
kufiri i sipërm për nyjen 7 është thjesht i barabartë me vlerën e përgjithshme të këtyre dy
artikujve.) Nyjet e mbetura të gjalla 2 dhe 6 kanë kufi më të ulët të sipërm sesa vlera e
zgjidhjes e paraqitur nga nyja 8. Pra, të dyja mund të përfundojnë duke e bërë nënbashkësinë
{1, 3} të nyjes 8, zgjidhjen optimale të problemit.
14 Përballjame kufizimet e fuqisë së algoritmve | 256

Zgjidhja e problemit të çantës së shpinës me anë të algoritmit branch-and-bound ka disa


karakteristika të pazakonshme. Në mënyrë tipike, nyjet e brendshme të pemës “gjendje-
hapsirë” nuk përcaktojnë një pikë të brendshme të hapsirës së kërkimit, mbasi disa nga
komponentet e zgjidhjes mbeten të papërcaktuara. Për problemin e çantës së shpinës,
megjithatë, çdo nyje e pemës paraqet një nënbashkësi të artikujve të dhënë. Ne mund të
përdorim këtë fakt për të përditësuar informacionin mbi nënbashkësinë më të mirë të
prodhuar deri në këtë çast për çdo nyje të re të pemës. Në qoftë se e kemi bërë këtë për rastin
e shqyrtuar më sipër, Atëherë do të kemi përfunduar me nyjet 2 dhe 6 para se të jetë prodhuar
nyja 8, mbasi të dyja ato janë më të vogla se nënbashkësia e vlerës 65 të nyjes 5.

14.3 Algoritme për zgjidhjen e ekuacioneve jolineare

Në këtë seksion do të paraqiten disa algoritme për zgjidhjen e ekuacioneve jolineare me një
të panjohur,

f(x) = 0. (14.2)

Ka disa arsye për këtë zgjedhje ndër nënfushat e analizës numerike. Para se gjithash ky është
një problem shumë i rëndësishëm si nga ana praktike ashtu edhe teorike. Tek ai mbrijmë si
rezultat i modelimit matematik të fenomeneve të shumta në shkencë dhe inxhinieri, në
mënyrë të drejtpërdrejtë apo tërthorazi. (Kujtojmë për shembull se gjetja e ekstremumeve të
një funksioni f(x) bazohet në gjetjen e pikave kritike që janë rrënjët e ekuacionit f ’(x) = 0.) Së
dyti ai paraqet çështjet më të kapshme të analizës numerike dhe në të njëjtën kohë paraqet
instrumentet dhe problemet më tipike të saj. Së treti, disa nga metodat për zgjidhjen e
ekuacioneve janë të lidhura ngushtë me algoritmet e tipit paralel për kërkim në tabela dhe
prandaj furnizojnë shembuj për zbatimin e teknikave të përgjithshme të hartimit të
algoritmeve në problemet e matematikës së vazhduar.

Le të fillojmë me shmangien e një keqkuptimi që ndoshta mund të jetë krijuar nga zgjidhja e
ekuacioneve. Eksperienca e juaj për zgjidhjen e ekuacioneve nga shkolla e mesme mund t’u
ketë shpënë në mendimin se ekuacionet zgjidhen me anë të faktorizimeve ose duke zbatuar
formula të gatshme. Por kjo ka ndodhur vetëm se ekuacionet janë zgjidhur në mënyrë të tillë
që zbatimi i formulave të jetë i lehtë. Por në përgjithësi, ne nuk mund të zgjidhim ekuacionet
me saktësi dhe prandaj janë të domosdoshëm algoritmet e përafrimit për ta kryer këtë detyrë.

Kjo është e vërtetë madje edhe për zgjidhjen e ekuacionit të shkallës së dytë

ax2 + bx + c = 0

mbasi formula standarde për rrënjët e tij

−𝑏 ± √𝑏 2 − 4𝑎𝑎
𝑥1,2 =
2𝑎

kërkon llogaritjen e rrënjës katrore, e cila mund të bëhet vetëm me përafërsi për pjesën më të
madhe të numrave pozitivë. Përveç kësaj, siç u diskutua në leksionin e mëparshëm kjo
formulë kanonike ka nevojë të modifikohet për të shmangur mundësinë e rezultateve me
saktësi të ulët.
14 Përballjame kufizimet e fuqisë së algoritmve | 257

Por atëherë çfarë mund të themi për rrënjët e polinomeve të shkallëve më të larta se dy?
Formula të tilla për polinomet e shkallës dytë dhe të tretë ekzistojnë por ato janë të
pavolitshme për të patur vlerë praktike. Për polinomet e shkallës më të lartë se katër mund të
mos ketë formula të përgjithshme për gjetjen e rrënjëve të tyre të cilat të përmbajnë
koeficientët e polinomit, veprime aritmetike dhe nxjerrjen e rrënjëve. Ky rezultat i shënuar
është publikuar për herë të parë nga matematikani italian Paolo Ruffini (1765-1822) në vitin
1799 dhe rizbuluar një çerek shekulli më pas matematikani norvegjez Niels Abel (1802-
1829); më tej, ai u përpunua nga matematikani francez Evariste Galois (1811-1832) 22.

Ne mund t’i interpretojmë zgjidhjet e ekuacionit (14.2) si pika në të cilën grafiku i funksionit
f(x) pret boshtin x. Tre algoritmet që do të diskutojmë në këtë seksion përfitojnë nga ky
interpretim. Natyrisht që grafiku i f(x) mund të ndërpresë boshtin x në një pikë të vetme (për
shembull, x3 = 0), në disa pika ose në një pafundësi pikash ( sin(x) = 0), ose mund të mos ta
ndërpresë fare ( ex + 1 = 0). Equacioni (14.2) mund të ketë atëherë një rrënjë të vetme, disa
rrënjë ose mund të mos ketë rrënjë, respektivisht. Është një gjë e mirë që të bëhet një skicë e
grafikut para se të filloje procesi i gjetjes së rrënjëve të përafërta të tij. Ajo mund të ndihmojë
në përcaktimin e numrit të rrënjëve si dhe në lokalizimin e përafërt të tyre. Në përgjithsi një
ide e mirë është izolimi i rrënjëve të tij, domethënë, të identifikohen intervalet që përmbajnë
nga një rrënjë të vetme të tij.

14.3.1 Metoda e përgjysmimit


Ky algoritëm mbështetet në vrojtimin që grafiku i një funksioni të vazhduar duhet të
ndërpritet me boshtin x ndërmjet dy pikave a dhe b të paktën njëherë në qoftë se vlerat e
funksionit kanë shenjë të kundërt në këto dy pika (figura 14.5).

Vërtetësia e këtij vrojtimi provohet në analizën matematikë. Ai shërben si bazë e algoritmit të


mëposhtëm të quajtur metoda e përgjysmimit (bisection method), për zgjidhjen e ekuacionit
(14.2). Duke filluar me një segment [a, b] në skajet e të cilit f(x) ka vlera me shenja të
kundërta, algoritmi llogarit pikën e mesit xmes = (a + b) / 2. Në qoftë se f(xmes) = 0, atëherë një
rrënjë u gjet dhe algoritmi ndalon. Përndryshe ai vazhdon kërkimin e rrënjës ose në [a, xmes]
ose në [xmes, b] në varësi të faktit se në cilin nga segmentet e mësipërme, funksioni f(x) ka
shenja të kundërta në skaje.

Figura 14.5 Iteracioni i parë i metodës së përgjysmimit

Përderisa nuk mund të presim që algoritmi i përgjysmimit të “godasë” në vlerën e saktë të


rrënjës së ekuacionit dhe të ndalojë, është i nevojshëm një kriter tjetër për të ndaluar

22
Zbulimi i Ruffinit u injorua nga pjesa më e madhe e matematikanëve të kohës. Abeli vdiq i ri pas një jete të
vështirë në varfëri. Galois u vra në një duel kur ishte vetëm 21 vjeç. Rezultatet e tyre për zgjidhjen e
ekuacioneve të shkallës së lartë quhen si arritje të rëndësishme në historinë e matematikës.
14 Përballjame kufizimet e fuqisë së algoritmve | 258

algoritmin. Mund të ndalojmë algoritmin pasi segmenti [an, bn] që qarkon një rrënjë x* të
bëhet aq i vogël sa të mund të garantojmë që gabimi absolut i përafrimit të x* me anë të xn,
pika e mesit të këtij intervali, të jetë më i vogël se një numër ε > 0, i dhënë që më parë.
Përderisa xn është pika e mesit e [an, bn] dhe x* shtrihet brenda këtij intervali do të kemi
𝑏𝑛 −𝑎𝑛
|𝑥𝑛 − 𝑥 ∗ | ≤ (14.3)
2

Pra, mund ta ndalojmë algoritmin sapo (bn – an) < ε ose e njëvlershmja me të

xn – an < ε. (14.4)

Nuk është e vështirë të provohet që


𝑏1 −𝑎1
|𝑥𝑛 − 𝑥 ∗ | ≤ për n =1, 2, (14.5)
2𝑛

Ky inekuacion tregon që vargu i përafrimeve {xn} mund të shkojë aq afër rrënjës x* sa më e


madhe të jetë vlera e n. Me fjalë të tjera ne mund të themi që {xn} konvergjon tek rrënja x*.
Vëmë në dukje se meqenëse në çdo kompjuter dixhital vlerat shumë të vogla paraqiten si
zero, pohimi i konvergjences është i vërtetë teorikisht por jo medoemos në praktikë. Me të
vërtetë, nëse zgjedhim ε më të vogël se zero e kompjuterit atëherë algoritmi mund të mos
ndalojë kurrë! Një burim tjetër ndërlikimi mund të jenë gabimet e rrumbullakimit në
vlerësimin e vlerës së funksionit. Prandaj është një gjë e mirë që në programin që zbaton
algoritmin e metodës së përgjysmimit të vihet një kufi për numrin e iteracioneve që i lejohet
algoritmit të kryejë, (shih algoritmin 14.1).

// Algoritmi 14.1 Metoda e përgjysmimit për gjetjen e rrënjës së ekuacionit


// Të dhëna : Dy numra reale a, b;
// një funksion i vazhduar F(x) në [a, b], ku F(a)·F(b) < 0;
// eps, një kufi i sipër për gabimin
// nit, numri maksimal i iteracioneve të lejuara
// Rezultate: x, një vlerë e përafërt (ose e saktë) e rrënjës në (a, b) ose një mesazh
// për arritjen e numrit maksimal të iteracioneve pa zgjidhur problemin

bisection( f(x), a, b, eps, nit) {


it ← 1; // numëratori i iteracioneve
while (it ≤ nit) do {
x ← (a + b) / 2;
if (x – a < eps)
then return x
fval ← F(x);
if fval = 0
then return x;
if fval * F(a) < 0
then b ← x
else a ← x;
it ← it + 1
};
print “U arrit numri maksimal i iteracioneve të lejuara!”
return
14 Përballjame kufizimet e fuqisë së algoritmve | 259

Vëmë në dukje që mund të përdorim inekuacionin (14.5) për të gjetur paraprakisht numrin e
iteracioneve të mjaftueshëm, të paktën teorikisht, për të arritur vlerën e parazgjedhur të
saktësisë. Kjo realizohet duke zgjedhur n mjaft të madhe që të kënaqë mosbarazimin (b1 -
a1)/2n < ε, d.m.th
𝑏1 −𝑎1
𝑛 > log 2 (14.6)
𝜀

Shembull 1. Le të konsiderojmë ekuacionin

x3 – x – 1 = 0 (14.7)

Ai ka një rrënjë reale. (Shih figurën 14.6 për grafikun e f(x) = x3 – x – 1.) Për derisa f(0) < 0
dhe f(2) > 0, rrënja duhet të shtrihet në intervalin (0, 2). Nëse zgjedhim si nivel gabimi ε = 10-
2
, inekuacioni 14.5 na jep numrin e iteracioneve të barabartë me n > log2(2/10-2) ose n ≥ 8
iteracione.

3
Figura 14.6 Grafiku i funksionit f(x) = x – x – 1

Në tabelën 14.1 paraqitet gjurma e tetë iteracioneve të para të algoritmit të përgjysmimit të


zbatuar për ekuacionin 14.9 (Shenjat pas numrave në shtyllën e dytë dhe të tretë tregojnë
shenjën e f(x) = x3 – x – 1 në pikën korresponduese fundore të intervalit).
Kështu përfitojmë x8 = 1.3203125 si një përafrim për rrënjën x* të ekuacionit (14.7) dhe
garantojmë që

|1.3203125 − 𝑥 ∗ | < 10−2

Për më tepër, nëse marim në konsideratë shenjat në anën e majtë të ekuacionit (14.7) për a8,
b8 dhe x8 mund të pohojmë se rrënja shtrihet në segmentin [1.3203125, 1.328125].

E meta kryesore e metodës së përgjysmimit, si një algoritëm i përgjithshëm për zgjidhjen e


ekuacioneve, është shpejtësia e ulët e konvergjencës në krahasim me metodat e tjera të
njohura. Për këtë arsye kjo metodë përdoret rrallë. Gjithashtu ajo nuk mund të shtrihet për
zgjidhjen e ekuacioneve më të përgjithshëm dhe të sistemeve të ekuacioneve. Por nga ana
tjetër metoda ka disa pika të forta. Ajo gjithmonë konvergjon tek rrënja nëse fillojmë me një
interval, cilësitë e të cilit mund të kontrollohen lehtë. Gjithashtu metoda nuk përdor derivatet
e funksionit siç bëjnë disa metoda të shpejta.

Cilit algoritëm të rëndësishëm i ngjan metoda e përgjysmimit? Ajo i ngjan kërkimit binar. Që
të dy metodat zgjidhin probleme kërkimi dhe që të dyja janë algoritme të përgjysmimit.
Ndryshimi kryesor ndërmjet tyre qendron në fushën ku zbatohen algoritmet: diskrete për
14 Përballjame kufizimet e fuqisë së algoritmve | 260

kërkimin binar dhe e vazhduar për metodën e përgjysmimit. Gjithashtu vëmë në dukje se
ndërsa kërkimi binar kërkon që tabela e vlerave të jetë e renditur, metoda e përgjysmimit nuk
kërkon që funksioni të jetë jo rritës apo jozbritës. Së fundmi, ndërsa kërkimi binar është
shumë i shpejtë, metoda e përgjysmimit është e ngadaltë.

Tabela 14.1 Gjurma e metodës së Përgjysmimit për ekuacionin 14.7.

n an bn xn f(xn)
1 0.0- 2.0+ 1.0 -1.000000
2 1.0- 2.0+ 1.5 0.875000
3 1.0- 1.5+ 1.25 -0.296875
4 1.25- 1.5+ 1.375 0.224609
5 1.25- 1.375+ 1.3125 -0.051514
6 1.3125- 1.375+ 1.34375 0.082611
7 1.3125- 1.34375+ 1.328125 0.014576
8 1.3125- 1.328125+ 1.3203125 -0.018711

14.3.2 Metoda e pozicionit fals


Metoda e pozicionit fals (e njohur edhe me emrin latin si regula falsi) ashtu si metoda e
përgjysmimit, përdor një interval [an, bn] që përmban rrënjën e një funksioni të vazhduar f(x),
që ka shenja të kundërta në skajet an dhe bn. Në ndryshim nga metoda e përgjysmimit,
përafrimi pasues nuk llogaritet si mesi i segmentit [an, bn] por si ndërprerja e boshtit x nga
vija e drejtë që kalon në pikat (an, f(an)) dhe (bn, f(bn)) (figura 14.7).

Formula, që llogarit pikën e ndërprerjes me boshtin x, është

𝑎𝑛 𝑓(𝑏𝑛 )−𝑏𝑛 𝑓(𝑎𝑛 )


𝑥𝑛 = (14.8)
𝑓(𝑏𝑛 )−𝑓(𝑎𝑛 )

Figura 14.7 Iteracioni i metodës së Pozicionit fals

Tabela 14.2 Gjurma e metodës së pozicionit fals për ekuacionin (14.7).

n an bn xn f(xn)
1 0.000000- 2.0+ 0.333333 -1.296296
2 0.333333- 2.0+ 0.676471 -1.366909
3 0.676471- 2.0+ 0.960619 -1.074171
4 0.960619- 2.0+ 1.144425 -0.645561
5 1.144425- 2.0+ 1.242259 -0.325196
6 1.242259- 2.0+ 1.288532 -0.149163
14 Përballjame kufizimet e fuqisë së algoritmve | 261

7 1.288532- 2.0+ 1.309142 -0.065464


8 1.309142- 2.0+ 1.318071 -0.028173

Shembull 2. Tabela e mësipërme, 14.2, përmban gjurmën e tetë iteracioneve të para të kësaj
metode për zgjidhjen e ekuacionit (14.7).

Vëmë në dukje se për këtë shembull, metoda e pozicionit fals nuk performon po aq mirë sa
metoda e përgjysmimit, ndërsa për shumë shembuj të tjerë ajo prodhon vargje që
konvergjojnë më shpejt.

14.3.3 Metoda e Njutonit


Metoda e Njutonit, e quajtur edhe metoda Njuton-Rafson (Newton-Raphson method), është
një nga algoritmet e përgjithshëm më të rëndësishëm për zgjidhjen e ekuacioneve jolinearë. E
zbatuar për ekuacioni (14.2) me një të panjohur, ajo ilustrohet si në figurën 14.8. Elementi
pasardhës xn+1, i vargut të përafrimeve të metodës, përftohet si ndërprerja e tangentes të
grafikut të funksionit f(x) në pikën xn me boshtin x.

Elementet e vargut të përafrimeve llogariten me anë të formulës së mëposhtme

𝑓(𝑥𝑛 )
𝑥𝑛+1 = 𝑥𝑛 − n = 0, 1, 2, (14.9)
𝑓′ (𝑥𝑛 )

Në pjesën më të madhe të rasteve, algoritmi i Njutonit, garanton konvergjencën e vargut


(14.9) në qoftë se përafrimi fillestar x0 zgjidhet mjaft afër rrënjës. (Mënyra e zgjedhjes së
përafrimit fillestar trajtohet në librat e analizës numerike) Ai mund të konvergjojë edhe për
përafrime fillestare që janë shumë larg rrënjës por kjo nuk është gjithmonë e vërtetë.

Figura 14.8 Iteracioni i metodës së Njutonit

Shembull 3. Llogaritja e √𝑎 për a ≥ 0 mund të bëhet me anë të gjetjes së rrënjëve të


ekuacionit x2 – a = 0. Në qoftë se përdorim formulën (14.9) për rastin e x2 – a = 0 dhe
𝑓 ′ (𝑥) = 2𝑥 do të përftojmë

𝑓(𝑥𝑛 ) 𝑥𝑛2 − 𝑎 𝑥𝑛2 + 𝑎 1 𝑎


𝑥𝑛+1 = 𝑥𝑛 − ′
= 𝑥𝑛 − = = (𝑥𝑛 + )
𝑓 (𝑥𝑛 ) 2𝑥𝑛 2𝑥𝑛 2 𝑥𝑛

e cila është ekzaktësisht formula që përdoret për llogaritjen e rrënjës katrore.


14 Përballjame kufizimet e fuqisë së algoritmve | 262

Shembull 4. Le të përdorim metodën e Njutonit për ekuacionin (14.7), që e zgjidhëm me


metodën e përgjysmimit dhe metodën e pozicionit fals. Formula (14.9) në këtë rast
transformohet në

𝑥𝑛3 − 𝑥𝑛 − 1
𝑥𝑛+1 = 𝑥𝑛 −
3𝑥𝑛2 − 1

Si një përafrim fillestar për vargun e përafrimeve le të marrim x0 = 2. Tabela 14.3 paraqet
gjurmën e pesë iteracioneve të para të realizuara me metodën e Njutonit.

Tabela 14.3 Gjurma e metodës së Njutonit për ekuacionin (14.7)


n xn xn+1 f(xn+1)
0 2.000000 1.545455 1.145755
1 1.545455 1.359615 0.153705
2 1.359615 1.325801 0.004625
3 1.325801 1.324719 4.70 ∙10-6
4 1.324719 1.324718 5.00∙10-12

Menjëherë vihet re se sa shpejt konvergjon vargu i përafrimeve të Njutonit tek rrënja, në


krahasim me metodën e përgjysmimit dhe metodën e pozicionit fals. Kjo konvergjencë e
shpejtë është tipike nëse përafrimi fillestar është afër rrënjës. Vëmë në dukje gjithashtu se çdo
iteracion i kësaj metode ka nevojë të llogariten vlerat e reja e funksionit dhe të derivatit,
ndërsa metodat e mëparshme kërkonin vetëm vlerat e funksionit. Gjithashtu metoda e
Njutonit nuk mbërthen një rrënjë siç bënin të dy metodat e tjera. Po ashtu, për një funksion të
çfardoshëm dhe një përafrim fillestar të zgjedhur në mënyrë të çfardoshme, vargu i saj i
përafrimeve edhe mund të divergjojë. Dhe meqenëse formula (14.9) përmban funksionin
derivat në emërues metoda mund të ndërpritet nëse vlera e tij është zero. Në të vërtetë,
metoda e Njutonit është shumë efektive në rast se 𝑓 ′ (𝑥) është i kufizuar nga një vlerë që
është larg zeros në një zonë afër rrënjës x*. Në veçanti në qoftë se

|𝑓 ′ (𝑥)| ≥ 𝑚1 > 0

në intervalin ndërmjet xn dhe x*, ne mund të vlerësojmë distancën ndërmjet xn dhe x* duke
përdorur teoremën e vlerës mesatare (Mean Value Theorem) të njehsimit diferencial si më
poshtë:

𝑓(𝑥𝑛 ) − 𝑓(𝑥 ∗ ) = 𝑓 ′ (𝑐)(𝑥𝑛 − 𝑥 ∗ )

ku c është është një pikë ndërmjet xn dhe x*. Përderisa f(x*) = 0 dhe |𝑓 ′ (𝑐)| ≥ 𝑚1 do të
përfitojmë

|𝑓(𝑥𝑛 )|
|𝑥𝑛 − 𝑥 ∗ | ≤ (14.10)
𝑚1

Formula (14.10) mund të përdoret si kriter për të ndaluar algoritmin e Njutonit kur krahu i
djathtë bëhet më i vogël së një nivel saktësie i zgjedhur që më parë ε. Kritere të tjera të
mundshme për të ndaluar algoritmin shërbejnë
|𝑥𝑛 − 𝑥𝑛−1 | < 𝜀

dhe
14 Përballjame kufizimet e fuqisë së algoritmve | 263

|𝑓(𝑥𝑛 )| < 𝜀

ku ε është një numër i vogël pozitiv. Përderisa dy kriteret e fundit nuk kërkojnë medoemos
afërsinë e xn tek rrënja x*, ato mund të konsiderohen kritere më të dobëta se ai që bazohet tek
(14.10).

Të metat e metodës së Njutonit nuk mund të zbehin forcën kryesore të saj: konvergjencen e
shpejtë kur përafrimi fillestar është zgjedhur mirë dhe zbatimin e saj në klasa të gjerë të
ekuacioneve dhe sistemeve të ekuacioneve jolinearë.

14.4 Përmbledhje

• Backtracking dhe branch-and-bound janë dy teknika të hartimit të algoritmeve për


zgjidhjen e problemeve në të cilat numri i zgjedhjeve rritet të paktën në mënyrë
eksponenciale në funksion të përmasës së të dhënave fillestare. Që të dyja teknikat e
ndërtojnë zgjedhjen sipas parimit një komponente në një etape, duke u përpjekur të
mbyllin procesin sapo të sigurohemi që nuk mund të përftohet zgjidhja si rezultat i
zgjedhjeve tashmë të bëra.
• Si metoda backtracking ashtu edhe branch-and-bound përdorin si mekanizëm kryesor një
pemë të ashtuquajtur “gjendje-hapsirë” nyjet e të cilës paraqesin zgjidhjen e pjesëshme të
ndërtuar për problemin në fjalë. Të dyja teknikat e mbyllin me një nyje sapo të sigurohen
që nuk ka zgjidhje të problemit nëse do të konsiderohen zgjedhjet e nyjeve pasuese.
• Metoda backtracking e ndërton pemën “gjendje-hapsirë” sipas një kërkimi së pari në
thellësi për pjesën më të madhe të problemeve. Në qoftë se vargu i zgjedhjeve i paraqitur
nga nyja e rradhës në pemën “gjendje-hapsirë” mund të zhvillohet më tej pa shkelur
kushtet e problemit, ai kryhet duke marrë në konsideratë alternativën e parë të ligjshme të
mbetur për komponenten tjetër. Përndryshe, metoda kthehet pas duke e zhbërë
komponenten e fundit të zgjidhjes së pjesëshme të ndërtuar dhe e zëvendëson atë me
alternativën tjetër.
• Branch-and-bound është një teknike për hartimin e algoritmeve që zgjeron idenë e
prodhimit të pemës “gjendje-hapsirë” me idenë e vlerësimit të vlerës më të mirë të
përftuar nga nyja e rradhës të pemës së vendimit: nëse një vlerësim i tillë nuk është më i
lartë se zgjidhja më e mirë e ndërtuar deri tani në këtë përpunim, nyja nuk merret më në
konsideratë.
• Zgjidhja e ekuacioneve jolineare është një nga fushat më të rëndësishme të analizës
numerike. Përderisa nuk ekzistojnë formula për gjetjen e rrënjëve të ekuacioneve
jolineare (më pak përjashtime), disa algoritme mund t’i gjejnë ato në mënyrë të përafërt.
• Metoda e përgjysmimit dhe metoda e pozicionit fals janë të ngjashmit e metodave të
kërkimit binar dhe kërkimit me interpolim, respektivisht. Përparësia e tyre kryesore është
rrethimi i rrënjës në çdo iteracion të algoritmit
• Metoda e Njutonit prodhon një varg përafrues tek rrënja që është i formuar nga
ndërprerjet e boshtit x me tangentet e grafikut të funksionit. Nëse përafrimi fillestar
zgjidhet mirë Atëherë zakonisht mjaftojnë pak iteracione për të përftuar rrënjën me një
shkallë të lartë saktësie.

You might also like