Leksione Te Algoritmikes 2020-2021
Leksione Te Algoritmikes 2020-2021
Leksione Te Algoritmikes 2020-2021
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
1 Hyrje
Rezultatet e të mësuarit
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.
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
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.
Problemi
Algoritmi
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
Hartimi i algoritmit
Programimi i algoritmit
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.
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
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.
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.
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.
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ë 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}.
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.
Ë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ë.
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.
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;
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!
import java.util.Scanner;
#include<stdio.h>
long faktorial(int);
main()
{
int n;
long fact = 1;
return 0;
}
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.
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.
Si rregull, një algoritëm i mirë është një rezultat i përpjekjeve të përsëritura dhe
ripunimit.
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.
Strukturat e të dhënave
1.4 Instruksionet
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).
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:
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.
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. 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)?
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.
7. Çfarë mund te thoni për veprimin e mëposhtëm kur a dhe b dy numra të çfardoshëm:
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
Elementet bazë të gjuhës algoritmike janë: shenjat themelore, fjalët, instruksionet, komentet,
algoritmi.
Shenjat themelore
• 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.
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.
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.
• 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.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ë.
• 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ë:
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
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.
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
a[i + j] ← a[i + j] + 1;
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.
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.
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.
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.
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.
Për shembull, nëse variabli count ka si vlerë aktuale 5, pas ekzekutimit të instruksionit
count ← count + 1
Për shembull, nëse variabli count ka si vlerë aktuale 5, pas ekzekutimit të instruksionit
count ← count – 1
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
Ky instruksion kërkon nga përdoruesi dy vlera dhe i rezervon ata respektivisht në variablat
distanca dhe shpejtesia.
print (shprehje)
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.
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ë.
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.
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ë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
Kuptimi matematik i vlerës absolute të një numri negativ është mohimi aritmetik i këtij
numri, përndryshe është vetë numri.
vleraAbsolute () {
input (x); // jep numrin
if x < 0 then // kontrollon shenjën e numrit
x ← – x;
print (x); // afishon rezultatin
}
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).
maksimum() {
then max ← n1
else max ← n2;
print(max);
}
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ç.
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);
}
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 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.
true false
kushti
Trupi i 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.
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.
verifikimFjaleKalimi() {
input (siguria); // inicializimi i drejtuesit të ciklit
2 Përshkrimi i algoritmeve | 29
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.
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
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
}
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
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".
verifikim() {
do
print “Fjalëkalimi?”
input (siguria);
while (siguria ≠ "nuk_e_gjen_dot");
}
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ë).
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)
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.
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
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.
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.
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 .
Shembull.
for i ← 1 to 10 do {
if (i = 5)
then break;
print(i);
}
i ←0;
while i < n – 1 do {
// instruksione të tjera
i ← i + 1;
}
i ←0;
while i ≤ n do {
// instruksione të tjera
i ← i + 1;
}
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;
}
Në tabelën 2.7 paraqitet një pamje e tre cikleve përsa i përket tipit të kontrollit dhe
vendndodhjes së kushtit.
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
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).
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
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).
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.
minimum1(a, b) {
if a > b
then return b
else return a
}
2 Përshkrimi i algoritmeve | 38
Ekzekutimi i modulit për një rast të caktuar në funksionin kryesor mund të kryhet si më
poshtë.
main() {
input (a, b); // marrja e të dhënave fillestare
print (minimum1(a, b)); // thirrja e funksionit
return // mbyllja e algoritmit kryesor
}
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).
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.
main() {
input (a, b) // marrja e të dhënave fillestare
minimum2(a, b, min) // thirrja e funksionit
print (min) // afishimi i rezultatit
return
}
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.
• 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
2.7.6 Përmbledhje
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.
4. Jepet një numër i plotë. Të verifikohet nëse numri është çift apo tek?
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?
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;
25. Shkruni një algoritëm që realizon hyrjen në mënyrë interaktive të n vlerave në një tabelë.
mister(n, T) {
for i ← 1 to div(n, 2) do {
if (T[i] ≠ T[n+1 – i])
then return false;
}
return true
}
Rezultatet e të mësuarit
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ë.
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.
Duke u mbështetur në formulën (3.1) algoritmi i mëposhtëm llogarit faktorialin e një numri n
në mënyrë iterative.
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.
llogaritjaFuqise(x, n) {
fuqia ← 1.0; // inicializimi i fuqisë
for i ← 1 to n do
fuqia ← fuqia * a; // llogaritja e a i
return fuqia
}
𝑥 𝑥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.
ekspo(x, n) {
exp ← 1; // inicializimi i rezultatit
for i ← 1 to n do
exp ← exp + fuqia(x,n)/faktIterativ(n); //
return exp
}
duke filluar me një përafrim fillestar të pëlqyeshëm. Llogaritjet vazhdojnë ndërkohë që nuk
është arritur saktësia e kërkuar.
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
}
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
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
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.
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.
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ë.
shumaTabele (n, a) {
3 Probleme me algoritme iterativë | 49
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 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.
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ë:
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
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 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.
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
}
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
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ë.
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.
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.
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 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
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
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ë.
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ë.
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:
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:
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ë:
Rezultatet e të mësuarit
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.
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.
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)
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.
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ë)
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.
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
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.
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.
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:
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.
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.
P1. Shumëzimi i 6 me 2
P2. Zmadhimi me 6 i rezultatit të problemit P1
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.
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.
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.
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
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(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
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
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].
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:
llogaritShumaTabelë1D(n, a) {
if n = 1
then return a[0]
else return a[n–1] + shumaTabelë1D(n – 1, a)
}
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 .
if (n = 0) {
then return false
else {if (A[n–1] = target)
then return true
else return kerkoVlereRekursiv(n – 1, A, target)
}
}
Ka probleme për të cilët mund të hartohen algoritme rekursive me funksione që nuk kthejenë
vlerë por kryejnë një përpunim.
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.
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
}
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)
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:
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
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?
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
Rezultatet e të mësuarit
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.
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.
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.
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:
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
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ë.
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ë.
Ndër modelet e krijuara për llogaritjem e kohës së ekzekutimit në këtë cikël të leksioneve do
të përqendrohemi
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.
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:
T(n) = 1 + 1 + (n + 1) + 2n + 2n = 5n + 3
Llogaritjet për të njëjtin fragment iterativ por të realizuar me instruksionin for në mënyrë të
përmbledhur paraqiten më poshtë:
M(n) = ∑𝑛𝑖=1 1 = n
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ë.
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ë.
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),
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.
// 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ë.
Ë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.
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.
• 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.
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.
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!).
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
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.
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.
(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.
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,
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ë
(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ë”.)
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ë
(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ë”.)
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))
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
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
𝑔(𝑛) 𝑛→∞ 𝑔′ (𝑛)
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).
a ←b
Meqenëse instruksioni i vlerëdhënies konsumon një kohë konstante T(n) = 1, atëherë ai është
në Θ(1).
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).
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
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).
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 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).
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:
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.
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ë.
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
n(n+1) 1
∑ni=0 i = ∑ni=1 i = 1 + 2 + ⋯ + n = ≈ n2 ∈ Θ(n2 ) (S2)
2 2
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.
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
}
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
(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.
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
∑𝑛−1
𝑘=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.
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:
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.
Problemi. Llogaritja e numrit të shifrave në paraqitjen binare të një numri të plotë të dhënë
në sistemin me bazë dhjetë.
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)
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);
}
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
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):
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
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ë:
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:
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.
Për shembull, duke lënë mënjanë për momentin rastin bazë, ne mund ta interpretojmë
rekurrencën
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.
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.
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ë.
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.
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.
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:
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:
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ë.
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
= 2 [2L(n – 2) + 1 ] + 1 = 22L(n – 2) + 2 +1
= 22[2L(n – 3) +1 ] + 2 + 1 = 23L(n – 3) + 22 + 2 + 1
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
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!).
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).
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(1) = 0
5 Analiza e efektshmërisë së algoritmeve | 107
M(2k) = M(1) + k = k,
F(0) = 0, F(1) = 1.
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
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.
√𝟓
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) = �∅𝐧+𝟏 − ∅
√𝟓
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.
Ë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ë.
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
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.
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.
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.
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
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ë)
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
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
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?
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!
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.
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
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
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)
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 √𝑛)?
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)
𝟐
∑𝑛
𝑖=1(𝑥𝑖 −𝑥)
2 ∑𝑛
𝑖=1 𝑥𝑖 ∑𝒏 𝟐 𝒏
𝒊=𝟏 𝒙𝒊 −�∑𝒊=𝟏 𝒙𝒊 � /𝒏
𝑘𝑘 𝑥 = ose
𝑛−1 𝑛 𝒏−𝟏
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}.
kerkoPozicion(14, t, a)
i←0;
while i ≤ n – 1 do {
if (t[i] = a)
then return i ;
i ← i +1;
}
return -1
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
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ë.
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!
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ë.
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?
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];
}
}
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?
𝑎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
Rezultatet e të mësuarit
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ë.
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
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ë 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.
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.
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).
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.
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ë.
(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.
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
�����������
𝑛𝑒̈ 𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝𝑝 𝑒 𝑡𝑡𝑡𝑡 𝑝𝑒̈ 𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟𝑟
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
Algoritmi i mësipërm Bubble sort nuk përdor kujtesë shtesë dhe është algoritëm i
qëndrueshëm.
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).
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ë).
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.
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.
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 :
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).
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
}
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
Shënim. Është e mundshme që kërkimi i gjetjes së motivit në një tekst të fillojë nga e djathta
në të majtë.
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.
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) .
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.
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)
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.
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
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.)
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):
ax + by = c, (6.2)
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.
// 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
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.
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
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.
{1,2,4} 12 60
{1,3,4} 17 E papranueshme
{2,3,4} 20 E papranueshme
{1,2,3,4} 22 E papranueshme
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.
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
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.
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
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.
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
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
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.
THERE_IS_MORE_TO_LIFE_THAN_INCREASING_ITS_SPEED
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?
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?
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.
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
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.
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:
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]
3 14 27 31 39 42 55 70 74 81 85 93 98
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.
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:
12
Ne gjuhën e programimit Fortran është parashikuar kusht kontrolli me tre-dalje
7 Teknika zvogëlo-dhe-sundo | 152
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.
llogaritPMP(m, n) {
while (n ≠ 0) do {
mbetja ← mod (m,n);
m ← n;
n ← mbetja;
{
return m
}
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 (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
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.
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
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
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.
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.
vlera
A[d]
A[m]
indeksi
m x d
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.
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
}
7.3 Përmbledhje
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.
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.
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.
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.
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.
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ë?
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
problem i përmasës n
nënproblemi 1 nënproblemi 2
i përmasës n / 2 i përmasës n / 2
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.
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.
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.
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:
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).
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
1 n/2
Figura 8.2 Faza fillestare e vizatimit të pemës të rekursionit
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.
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
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.
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
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.
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.
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.
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 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.
}
}
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
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ë
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:
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.
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ë).
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).
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
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.
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
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
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
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.
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
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ë:
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ë:
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:
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.
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:
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.
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).
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ë:
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.
Meqenëse n = 2k ,
Meqenëse k = log2n,
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:
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.
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
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:
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
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.
P max
P1 Pn
8 Teknika ndaj-dhe-sundo | 182
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
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.
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
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.
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.)
27. Shpjegoni se si mund të gjendet pika Pmax në mënyrë analitike në 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
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)
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.
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.
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
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):
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
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.
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
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ë:
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.
𝑎 𝑥 + 𝑎12 𝑦 = 𝑏1
� 11
𝑎21 𝑥 + 𝑎22 𝑦 = 𝑏2
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:
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)
𝑎𝑎 = 𝑏 ⇒ 𝑎′ 𝑥 = 𝑏′
ku,
(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.
Ç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.
2𝑥1 − 𝑥2 + 𝑥3 = 1
�4𝑥1 + 𝑥2 − 𝑥3 = 5
1𝑥1 + 𝑥2 + 𝑥3 = 0
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
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ë).
// 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
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).
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.
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.
2 −1 1
A = �4 1 −1�
1 1 1
1 0 0
2 1 0
L = �1 1 �
1
2 2
2 −1 1
U = �0 3 −3�
0 0 2
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
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.
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
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.
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ë?
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.
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:
Për shëmbull për polinomin e shkallës së katërt P4(x) = 2x4 – x3 + 3x2 + x – 5, do të kemi
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.
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.)
horner(n, a, x) {
pol ← a[n];
for i ← n – 1 to 0 step - 1 do
pol ← pol * x + a[i];
return pol
}
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.)
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
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
Por,
(ap )2 në se bi = 0
a2p+bi = a2p ∙ abi = (ap )2 ∙ abi = � p 2
(a ) ∙ a, në se bi = 1
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
}
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ë
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.
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.
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
}
Ë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.
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.
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.
24 = 2⋅2⋅2⋅3
60 = 2⋅2⋅3⋅5
shmvp(24, 60) = (2⋅2⋅3) ⋅2⋅5=120
m∙n
shmvp(m, n) =
pmp(m, n)
16
René Descartes (1596-1650) filozof, matematikan, fizikan dhe shkrimtar francez.
9 Teknika transformo-dhe-sundo | 204
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.
9.4 Përmbledhje
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ë.
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.
𝑥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?
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.
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?
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.
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
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:
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.
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
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
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ë:
18
Algoritmi është krijuar nga Harold H. Seward in 1954
10 Kompromisi hapsirë-kohë | 212
distributionCounting(n, a, r, m, d) {
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.
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.
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
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:
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ë:
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.
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
}
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
}
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
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
b, c, d, c, b, a, a, b
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.
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
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.
Për të ilustruar teknikën le të rishohim edhe një herë llogaritjen e termit të ntë të vargut
Fibonaçi:
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).
llogaritFibonaciMeTabele (n, f) {
f[0] ← 0;
f[1] ← 1;
for i ← 2 to n do
f[i] ← f[i − 1] + f[i − 2];
return
}
llogaritFibonaciPaTabele(n) {
fibPara ← 0;
fibPas ← 1;
fibFinal ← 1;
for i ← 3 to n do {
fibPara ← fibPas;
FibPas ← fibFinal;
fibFinal ← fibPara + fibPas;
}
return fibFinal
}
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.
Qëllimi i këtij seksioni është që të paraqesë programimin dinamik nëpërmjet tre problemeve
tipike.
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:
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
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.
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
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.
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):
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.
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).
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ë.
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
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ë.
kapaciteti j
0 j – pi j P
0 0 0 0 0
n 0 qëllimi
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).
Për rastet kur përmasat e artikujve nuk janë numra të plotë përdoren algoritme të tjerë të
Programimit dinamik.
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
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]
}
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
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?
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?
12 Teknika lakmitare
Rezultatet e të mësuarit
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ë:
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
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.
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.
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
}
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.
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
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
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
Rezultatet e të mësuarit
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ë.
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
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.
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.
Tabela 13.1 Probleme të përdoruara shpesh për përcaktimin e kufirit të ulët me anë të
reduktimit
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.
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
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.
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.
𝑥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
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! 𝑛!
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):
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
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ë.
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 relativ = (13.9)
|𝑎∗ |
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ë.
|𝛼 − 𝛼 ∗ | 0.0000003 ⋯ 4 −7
= < 10
|𝑎∗ | 𝜋 3
dhe
|𝛽 − 𝛼 ∗ | 0.00000005 ⋯ 1 −7
= < 10
|𝛽 ∗ | 𝜋 − 6 ∙ 10−7 3
|𝛾 − 𝛾 ∗ | 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 β.
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,
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ë.
ax2 + bx + c = 0 (13.10)
−𝑏±√𝑏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,
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.
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
Shembull 4. Le të shohim tani një problem të trajtuar nga George Forsythe 20. Le të jetë
dhënë ekuacioni x2 – 105x + 1 = 0.
𝑥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ë
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∗
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𝑎
−𝑏 + √𝑏 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
Rezultatet e të mësuarit
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.
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
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.
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.
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
Për shembull, për S = {1, 2, 5, 6, 8} dhe d = 9 janë dy zgjidhje: {1, 2, 6} dhe {1, 8}.
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
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:
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.
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
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.
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.
• 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ë.
Ë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:
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
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
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
−𝑏 ± √𝑏 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.
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)
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)
𝜀
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
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].
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ë.
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
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
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.
𝑓(𝑥𝑛 )
𝑥𝑛+1 = 𝑥𝑛 − n = 0, 1, 2, (14.9)
𝑓′ (𝑥𝑛 )
𝑥𝑛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.
|𝑓 ′ (𝑥)| ≥ 𝑚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