Evo nekih od najpoznatijih i najzanimljivijih (pokušajte da na internetu pronađete još ovakvih primera):
“Internet crv”1 Moris (engl. Morris) raširio se, koristeći propuste i greške u nekoliko sistemskih programa, putem interneta 1988. godine (kao jedan od prvih takvih programa) i privukao pažnju mnogih svetskih medija. Autor crva, student Robert Moris, nije nameravao da crv bude destruktivan, već samo da se replicira i širi preko mreže, ali stepen replikacije bio je takav da su se računari “inficirali” mnogo puta, do nivoa da više nisu mogli da funkcionišu. Na taj način, nekoliko hiljada računara prestalo je sa normalnim funkionisanjem i bilo je neoperativno nekoliko dana. Ukupna šteta je u to vreme procenjena na nekoliko miliona dolara. Suđenje autoru crva bilo je jedno od prvih takvih, zatvorska kazna je na kraju bila uslovna, uz novčanu kaznu i društveno koristan rad.
Eksplozija rakete Ariane (fr. Ariane 5) 1996. uzrokovana konverzijom broja iz šezdesetčetvorobitnog realnog u šesnaestobitni celobrojni zapis koja je dovela do prekoračenja.
Greška u Patriot raketnom sistemu tokom Zalivskog rata 1991. bila je posledica akumulacije numeričke greške u proračunu vremena koristeći pokretni zarez ograničene preciznosti. Interni sat sistema merio je vreme u desetinkama sekunde, a pri konverziji u sekunde dolazilo je do malog zaokruživanja koje se vremenom sabiralo. Nakon dugotrajnog rada sistema bez restartovanja, ova greška je postala dovoljno velika da uzrokuje značajno odstupanje u predviđanju položaja cilja. Kao posledica, sistem nije uspeo da presretne dolazeći projektil, što je dovelo do ljudskih žrtava. Ovaj incident jasno ilustruje kako i male numeričke greške mogu imati ozbiljne posledice u sistemima koji rade dugo i u realnom vremenu.
Greška u numeričkom koprocesoru procesora Pentium 1994.
uzrokovana pogrešnim indeksima u for petlji u okviru softvera koji je radio dizajn čipa,
Pad orbitera poslatog na Mars 1999. uzrokovan činjenicom da je deo softvera koristio metričke, a deo softvera engleske jedinice.
U slučaju Therac-25, medicinskog uređaja za terapiju zračenjem, softverska greška imala je fatalne posledice po pacijente. Dve konkurentne rutine upravljale su istim delovima memorije bez odgovarajuće sinhronizacije. U određenim okolnostima sistem bi pogrešno procenio konfiguraciju uređaja i isporučio višestruko veću dozu zračenja nego što je predviđeno. Pošto su ranije verzije sistema imale hardverske zaštite koje su uklonjene u ovoj verziji, softver je ostao jedina linija odbrane. Kombinacija lošeg dizajna, nedovoljnog testiranja i izostanka bezbednosnih mehanizama dovela je do više ozbiljnih povreda i smrtnih ishoda.
U Los Anđelesu je 14. septembra 2004. godine više od četiristo aviona u blizini aerodroma istovremeno izgubilo vezu sa kontrolom leta. Na sreću, zahvaljujući rezervnoj opremi unutar samih aviona, do nesreće ipak nije došlo. Uzrok gubitka veze bila je greška prekoračenja u brojaču milisekundi u okviru sistema za komunikaciju sa avionima. Da ironija bude veća, ova greška je bila otkrivena ranije, ali pošto je do otkrića došlo kada je već sistem bio isporučen i instaliran na nekoliko aerodroma, njegova jednostavna popravka i zamena nije bila moguća. Umesto toga, preporučeno je da se sistem resetuje svakih 30 dana kako do prekoračenja ne bi došlo. Procedura nije ispoštovana i greška se javila posle tačno \(2^{32}\) milisekundi, odnosno 49.7 dana od uključivanja sistema.
Pad satelita Kriosat (engl. Cryosat)} 2005. godine koštao je Evropsku Uniju oko 135 miliona evra. Pad je uzrokovan greškom u softveru zbog koje nije na vreme došlo do razdvajanja satelita i rakete koja ga je nosila.
Ranljivost poznata kao Heartbleed nastala je u implementaciji proširenja u biblioteci OpenSSL, gde nije adekvatno proveravana dužina podataka koje klijent prijavljuje da šalje. Napadač je mogao da pošalje mali paket uz lažno veliku deklarisanu dužinu, nakon čega bi server u odgovoru vratio i deo svoje radne memorije. Time su mogli biti otkriveni osetljivi podaci poput korisničkih lozinki, sesijskih kolačića, pa čak i privatnih kriptografskih ključeva. Problem je bio posebno opasan jer nije ostavljao tragove u logovima i zahvatio je veliki broj servera širom interneta, što je dovelo do masovnog resetovanja lozinki i zamene sertifikata.
Više od pet procenata penzionera i primalaca socijalne pomoći u Nemačkoj je privremeno ostalo bez svog novca kada je 2005. godine uveden novi računarski sistem. Greška je nastala zbog toga što je sistem, koji je zahtevao desetocifreni zapis svih brojeva računa, kod starijih računa koji su imali osam ili devet cifara brojeve dopunjavao nulama, ali sa desne umesto sa leve strane kako je trebalo.
Incident sa XZ Utils 2024. godine predstavlja jedan od najozbiljnijih primera napada na lanac razvoja otvorenog softvera. Napadač je tokom dužeg vremenskog perioda stekao poverenje održavaoca projekta i postepeno ubacio zlonameran kôd u biblioteku liblzma, koja se široko koristi u Linux sistemima. Taj kôd je bio pažljivo prikriven i aktivirao se samo u specifičnim uslovima, omogućavajući potencijalno neautorizovan pristup sistemima preko SSH servera, što bi, imajući u vidu široku rasprostranjenost Linux servera imalo katastrofalne posledice. Ranljivost je otkrivena relativno rano, pre nego što je dospela u stabilne verzije većine distribucija, zahvaljujući neobičnom ponašanju performansi koje je privuklo pažnju istraživača. Ovaj slučaj je pokazao koliko su projekti otvorenog koda ranjivi i koliko je važno pažljivo upravljanje doprinosima i revizija koda.
Kompanije Dell i Apple morale su tokom 2006. godine da korisnicima zamene više od pet miliona laptop računara zbog greške u dizajnu baterije kompanije Sony koja je uzrokovala da se nekoliko računara zapali.
Ne naročito opasan, ali veoma zanimljiv primer greške je greška u programu Microsoft Excell 2007 koji, zbog greške u algoritmu formatiranja brojeva pre prikazivanja, rezultat izračunavanja izraza \(77.1 \cdot 850\) prikazuje kao \(100,000\) (iako je interno korektno sačuvan).
U poslednje vreme veoma su popularne kriptovalute, zasnovane na tzv. blokčejn tehnologiji (npr. Bitcoin, Ethereum). S obzirom na ogromna finansijska sredstva koja kontroliše ovaj softver (procenjuje se da je krajem 2024. godine vrednost svih kritpovaluta bila oko \(3\cdot 10^{12}\) američkih dolara), svaka greška može dovesti do ogromnih finansijskih gubitaka, Na primer, u čuvenom napadu na investicioni fond DAO tokom 2018. godine ukradeno je oko 150 miliona američkih dolara (u to doba oko 14% ukupnog iznosa kriptovalute ETH na mreži Ethereum na kojoj je taj fond radio). Sredstva su vraćena tako što su sporne transakcije poništene, što je u suprotnosti sa filozofijom kritpovaluta i dovelo je do cepanja mreže (tzv. hard fork). Procenjuje se da je do kraja 2024. ukupan iznos kriptovaluta koje su prenete na osnovu softverskih grešaka oko 7 milijardi dolara.
Američka kompanija CrowdStrike je 19. jula 2024. godine distribuirala neispravnu verziju svog bezbednosnog softvera Falcon Sensor za računare sa sistemom Microsoft Windows, što je dovelo do pada oko 8,5 miliona računara širom sveta, dovelo do otkazivanja preko 5000 letova, poremetilo živote miliona ljudi i uzrokovalo ogromnu materijalnu štetu.
U ovom poglavlju prikazaćemo još nekoliko primera dokaza korektnosti, što rekurzivnih, što iterativnih funkcija.
Problem: Definisati funkciju koja određuje minimum nepraznog niza brojeva i dokazati njenu korektnost.
Algoritam se veoma jednostavno konstruiše induktivno-rekurzivnom konstrukcijom. Dimenzija problema u ovom primeru je broj elemenata niza.
Ako niz ima samo jedan element, tada je taj element ujedno i minimum.
U suprotnom, pretpostavimo da nekako umemo da rešimo problem za manju dimenziju i na osnovu toga pokušajmo da dobijemo rešenje za ceo niz. Dakle, pretpostavimo da je dužina niza \(n > 1\) i da umemo da nađemo broj \(m\) koji predstavlja minimum prvih \(n-1\) elemenata niza. Minimum celog niza dužine \(n\) je manji od brojeva \(m\) i preostalog, \(n\)-tog elementa niza (ako brojanje kreće od \(0\), to je element \(a_{n-1}\)).
Na osnovu ovoga možemo definisati rekurzivnu funkciju.
int minNiza(int a[], int n) {
if (n == 1)
return a[0];
else {
int m = minNiza(a, n-1);
return min(m, a[n-1]);
}
}
int main() {
int a[] = {3, 5, 4, 1, 6, 2, 7};
int n = 7;
cout << minNiza(a, n) << endl;
}Korektnost prethodnog algoritma se može formulisati u obliku sledeće teoreme.
Teorema 1.A.1. Za svaki neprazan niz \(a\) (niz za koji je dužina \(|a| \geq 1\)) i za svako \(1 \leq n \leq |a|\) poziv minNiza(a, n) vraća najmanji među prvih \(n\) elementa niza \(a\) (sa \(a\) i \(n\) su obeležene vrednosti niza a i promenljive n, a sa \(|a|\) dužina niza a).
Dokaz. Teoremu možemo dokazati indukcijom.
Bazu indukcije predstavlja slučaj \(n=1\), tj. poziv minNiza(a, 1). Na osnovu definicije funkcije minNiza rezultat je a[0] tj. prvi član niza \(a_0\) i tada tvrđenje trivijalno važi (jer je on ujedno najmanji među prvih \(1\) elemenata niza).
Induktivna hipoteza je tvrđenje: ako važi \(1 \leq n - 1 < |a|\), tada poziv minNiza(a, n-1) vraća najmanji od prvih \(n-1\) elemenata niza \(a\). Iz te pretpostavke potrebno je da dokažemo da poziv minNiza(a, n) vraća najmanji od prvih \(n\) elemenata niza \(a\) (pri tom je \(a\) neprazan niz). Na osnovu definicije funkcije minNiza, poziv minNiza(a, n) će vratiti minimum brojeva \(m\) (koji predstavlja rezultat poziva minNiza(a, n-1)) i \(a_{n-1}\). Pošto su uslovi induktivne hipoteze zadovoljeni, na osnovu induktivne hipoteze znamo da će \(m\) biti najmanji među prvih \(n-1\) elemenata niza \(a\). Zato će minimum broja \(m\) i \(n\)-tog elementa niza (elementa \(a_{n-1}\)) biti najmanji među prvih \(n\) elemenata niza \(a\). \(\Box\)
Dokažimo sada korektnost sledeće iterativne implementacije, korišćenjem tehnike invarijante petlje.
#include <iostream>
#include <algorithm>
using namespace std;
int minNiza(int[] a, int n) {
int m = a[0];
for (int i = 1; i < n; i++)
m = min(m, a[i]);
return m;
}
int main() {
int a[] = {3, 5, 4, 1, 6, 2, 7};
int n = 7;
cout << minNiza(a, n) << endl;
}U svakom koraku petlje, deo niza čiji minimum znamo postaje duži za po jedan element. Algoritam kreće od prefiksa niza dužine \(1\) i postavlja promenljivu m na vrednost prvog elementa niza \(a_0\). U svakom koraku petlje, pretpostavljamo da promenljiva m sadrži vrednost minimuma prvih \(i\) elemenata niza, a onda u telu petlje obrađeni deo niza proširujemo dodajući \(i+1\)-vi element niza, na poziciji \(i\). Minimum proširenog niza se izračunava kao minimum minimuma prvih \(i\) elemenata niza (čija je vrednost smeštena u promenljivoj m) i dodatnog elementa niza \(a_i\). Nakon izvršavanja tela petlje, deo niza čiji minimum je poznat je proširen na \(i+1\) element. Na kraju petlje je \(i\) jednako dužini niza, pa promenljiva m sadrži minimum celog niza.
Pre nego što pređemo na precizan dokaz prethodog razmatranja, skrenimo još jednom pažnju na to da imenovane veličine u matematici (tačnije algebri) i u programiranju imaju različite osobine. Naime, imenovane veličine u matematici (parametri, nepoznate) označavaju jednu vrednost, dok u (imperativnom) programiranju imenovane veličine imaju dinamički karakter i menjaju svoje vrednosti tokom izvršavanja programa po pravilima zadatim samim programom. Na primer, brojačka promenljiva i u nekoj petlji može redom imati vrednosti 1, 2 i 3. Prirodno je da tekuća vrednost promenljive i bude označena prosto sa \(i\). Međutim, nekada je važno da razlikujemo staru i novu vrednost promenljive i (vrednost pre i vrednost posle izvršavanja nekog koda), i tada ćemo koristiti oznake \(i'\) i \(i''\). Ako želimo da naglasimo da je promenljiva redom uzimala neku seriju vrednosti, koristićemo oznake \(i_0\) (početna vrednost promenljive i), \(i_1\), \(i_2\), … Vrednosti promenljivih koje koje se ne menjaju tokom izvršavanja programa ćemo označavati prosto imenom promenljive (npr. vrednost promenljive n iz prethodnog programa ćemo uvek označavati sa \(n\), a elemente niza a sa \(a_0\), \(a_1\), …, \(a_{n-1}\)).
Sledeću teoremu možemo strogo dokazati.
Teorema 1.A.2. Ako je niz \(a\) dužine \(n \geq 1\), neposredno pre početka petlje, u svakom koraku petlje (i na njenom početku, neposredno nakon provere uslova, ali i na njenom kraju, neposredno nakon izvršavanja tela), kao i nakon izvršavanja cele petlje važi da je \(1 \leq i \leq n\) i da je \(m\) minimum prvih \(i\) elemenata niza (gde je \(i\) tekuća vrednost promenljive i, a \(m\) tekuća vrednost promenljive m).
Dokaz. Ovo tvrđenje možemo dokazati indukcijom i to po broju izvršavanja tela petlje (obeležimo taj broj sa \(k\)). Napomenimo samo da ćemo petlju for smatrati samo skraćenicom za petlju while, tako da ćemo inicijalizaciju petlje smatrati za kôd koji se izvršava pre petlje, dok ćemo korak petlje smatrati kao poslednju naredbu tela petlje.
int m = a[0];
int i = 1;
while (i < n) {
m = min(m, a[i]);
i++;
}Takođe, implicitno ćemo podrazumevati da se tokom izvršavanja petlje niz ni u jednom trenutku ne menja (i to se eksplicitno može dokazati indukcijom). Ni promenljiva n ne menja svoju vrednost.
Obeležimo sa \(m_0, m_1, \ldots, m_k, \ldots\) vrednosti promenljive m, a sa \(i_0, i_1, \ldots, i_k, \ldots\) vrednost promenljive i nakon \(0, 1, \ldots, k, \ldots\) izvršavanja tela petlje. Pošto promenljiva n ne menja svoju vrednost, njenu vrednost ćemo označiti sa \(n\).
Bazu indukcije čini slučaj \(k=0\) tj. slučaj kada se telo petlje nije još izvršilo. Pre ulaska u petlju promenljiva i se inicijalizuje na \(1\) (važi \(i_0 = 1\)). Pošto pretpostavljamo da je niz neprazan, važi da je \(1 \leq i = i_0 = 1 \leq n\). Promenljiva m se inicijalizuje na vrednost a[0] (važi \(m_0 = a_0\)), što je zaista minimum jednočlanog prefiksa niza \(a\). Dakle, uslovi su zadovoljeni pre prvog izvršavanja tela petlje.
Pretpostavimo sada kao induktivnu hipotezu da tvrđenje važi nakon \(k\) izvršavanja tela petlje. Dakle, pretpostavljamo da uslovi teoreme važe za vrednosti \(m_k\) i \(i_k\) tj. da je \(1 \leq i_k \leq n\) i da je \(m_k\) jednako minimumu prvih \(i_k\) elemenata niza (sa \(i_k\) i \(m_k\) obeležavamo vrednosti promenljivih nakon \(k\) izvršavanja tela petlje). Ako je uslov petlje ispunjen, to će ujedno biti i vrednosti promenljivih na početku tela petlje, pre njenog \(k+1\)-vog izvršavanja. Nakon \(k\) izvršavanja tela petlje važi da je \(i_k = k+1\), jer je promenljiva i imala početnu vrednost \(1\) i tačno \(k\) puta je uvećana za 1 (i ovo bi se formalno moglo dokazati indukcijom).
Iz induktivne hipoteze i pretpostavke da je uslov petlje i < n ispunjen (tj. da je \(i_k < n\)) dokažimo da nakon \(k+1\) izvršavanja tela petlje uslovi teoreme važe i za vrednosti \(m_{k+1}\) i \(i_{k+1}\) (sa \(m_{k+1}\) i \(i_{k+1}\) obeležavamo vrednosti promenljivih nakon \(k+1\) izvršavanja tela petlje). Vrednosti \(m_{k+1}\) i \(i_{k+1}\) se mogu lako odrediti na osnovu vrednosti \(m_k\) i \(i_k\), analizom jednog izvršavanja tela petlje. Važi da je \(i_{k+1} = i_{k} + 1 = k+2\). Zato, pošto je \(1 \leq i_k = k+1 < n\), važi i da je \(1 \leq i_{k+1} = k+2 \leq n\), pa je uslov koji se odnosi na raspon vrednosti promenljive i očuvan. Dokažimo i da je \(m_{k+1}\) minimum prvih \(i_{k+1}\) elementa niza. Važi da je \(m_{k+1}\) minimum vrednosti \(m_k\) i elementa \(a_{i_k}\), tj. \(a_{k+1}\). Na osnovu induktivne hipoteze znamo da je \(m_k\) minimum prvih \(i_k = k+1\) elemenata niza. Zato će \(m_{k+1}\) biti minimum prvih \(k+2\) elemenata niza (zaključno sa elementom \(a_{k+1}\)), što je tačno \(i_{k+1}\) elemenata niza, pa i drugi uslov ostaje očuvan.
Neka su \(i\) i \(m\) vrednosti promenljivih i i m nakon izvršavanja petlje. Na osnovu dokazanog tvrđenja znamo da uslovi navedeni u njemu važe i nakon završetka petlje. Kada se petlja završi, važi da je \(i = n\) (jer na osnovu prvog uslova znamo da je \(1 \leq i \leq n\), a uslov petlje i < n nije ispunjen). Na osnovu drugog uslova znamo da je \(m\) minimum \(n\) članova niza (što je zapravo ceo niz, jer je \(n\) njegova dužina), tj. da promenljiva m sadrži traženu vrednost, čime je dokazana parcijalna korektnost. Zaustavljanje se dokazuje jednostavno tako što se dokaže da se u svakom koraku petlje nenegativna vrednost \(n-i\) smanjuje za po 1, dok ne postane 0. \(\Box\)
Izolujmo ključne delove prethodnog dokaza i prikažimo ih u formatu koji ćemo i ubuduće koristiti prilikom dokazivanja invarijanti petlji (indukcija će u tim dokazima biti samo implicitna).
Lema 1.A.1. Ako je niz \(a\) dužine \(n \geq 1\), uslov da je \(1 \leq i \leq n\) i da je \(m\) minimum prvih \(i\) elemenata niza je invarijanta petlje (gde sa \(i\) obeležavamo tekuću vrednost promenljive i, a sa \(m\) tekuću vrednost promenljive m).
Dokaz. Dokažimo da invarijanta važi nakon inicijalizacije, a zatim i da se održava nakon izvršenja tela petlje.
Pre ulaska u petlju promenljiva i se inicijalizuje na \(1\) (važi \(i = 1\)). Pošto pretpostavljamo da je niz neprazan, važi da je \(1 \leq i \leq n\). Promenljiva m se inicijalizuje na vrednost a[0] (važi \(m = a_0\)), što je zaista minimum jednočlanog prefiksa niza \(a\).
Obeležimo sa \(i'\) i \(m'\) vrednosti promenljivih i i m pre, sa \(i''\) i \(m''\) vrednosti posle izvršavanja tela i koraka petlje.
Pretpostavimo da invarijanta važi nakon ulaska u petlju tj. da je vrednost \(m'\) promenljive m jednaka minimumu prvih \(i'\) članova niza, da je \(1 \leq i' \leq n\), kao i da je uslov petlje ispunjen tj. da je \(i' < n\).
Pošto je nakon izvršavanja tela petlje vrednost promenljive i uvećana za jedan, važi da je \(i'' = i' + 1\). Pošto je važi da je \(i' < n\) i \(1 \leq i' \leq n\), nakon izvršavanja tela petlje, važiće da je \(1 \leq i'' \leq n\).
Nova vrednost \(m''\) promenljive m biće jednaka manjoj od vrednosti \(m'\) i \(a_i\). Na osnovu pretpostavke važi da je \(m'\) jednako minimumu prvih \(i\) elemenata niza, tj. minimumu brojeva \(a_0\), …, \(a_{i-1}\), pa je \(m''\) jednako minimumu brojeva \(a_0\), …, \(a_i\), što je upravo minimum prvih \(i+1\) elemenata niza, pa je zaista \(m''\) minimum prvih \(i''\) elemenata niza. \(\Box\)
Teorema 1.A.3. Nakon izvršavanja petlje, promenljiva m sadrži minimum celog niza.
Dokaz. Na osnovu invarijante važi da je \(1 \leq i \leq n\), a pošto po završetku petlje njen uslov nije ispunjen (ne važi \(i < n\)), važi da je \(i = n\). Na osnovu invarijante važi i da promenljiva m sadrži minimum prvih \(i\) elemenata niza, a pošto je \(i = n\), gde je \(n\) broj članova niza, to je zapravo minimum celog niza. \(\Box\)
Napiši program koji na osnovu neoznačenog celog broja \(n\) formira i ispisuje njegov 32-bitni binarni zapis.
Sa standardnog ulaza se unosi broj \(n\) (\(0 \leq n \leq 2^{32}-1\)).
Na standardni izlaz ispisati 32-bitni binarni zapis broja \(n\).
123456
00000000000000011110001001000000
16777215
00000000111111111111111111111111
Neka je niz od 32 logičke vrednosti popunjen vrednošću false. Binarni zapis određujemo tako što određujemo jednu po jednu binarnu cifru broja, zdesna nalevo. U svakom koraku petlje određujemo ostatak pri deljenju broja \(n\) sa \(2\), i na naredno mesto u nizu (u koraku \(i\) na mesto \(i\)) upisujemo true ako je taj ostatak jednak 1. Na kraju petlje, ispisujemo sadržaj niza unazad.
Primer 1.A.1. Prikažimo izvršavanje algoritma na primeru prevođenja broja 38 u binarni zapis. Prikazaćemo samo korake koji se izvode dok \(n\) ne postane 0 (od tog trenutka nadalje niz se samo popunjava nulama).
n niz b 38 19 0 9 10 4 110 2 0110 1 00110 0 100110
Implementacija se može napraviti na sledeći način.
// broj koji se prevodi
unsigned long n;
cin >> n;
// niz binarnih cifara, redom, od cifre najmanje do
// cifre najvece tezine
bool binarneCifre[32] = {false};
// prevodjenje
for (int i = 0; n > 0; i++, n /= 2)
binarneCifre[i] = n % 2;
// ispisujemo rezultat (od cifre najmanje tezine
for (int i = 31; i >= 0; i--)
cout << (binarneCifre[i] ? '1' : '0');
cout << endl;#include <iostream>
using namespace std;
int main() {
// broj koji se prevodi
unsigned long n;
cin >> n;
// niz binarnih cifara, redom, od cifre najmanje do
// cifre najvece tezine
bool binarneCifre[32] = {false};
// prevodjenje
for (int i = 0; n > 0; i++, n /= 2)
binarneCifre[i] = n % 2;
// ispisujemo rezultat (od cifre najmanje tezine
for (int i = 31; i >= 0; i--)
cout << (binarneCifre[i] ? '1' : '0');
cout << endl;
return 0;
}Dokažimo korektnost ovog algoritma.
Da bismo lakše odredili invarijantu proširimo primer izvršavanja programa vrednošću binarnog broja trenutno zapisanog u nizu \(b\) i odgovarajućim stepenom dvojke.
n niz b b 2^i 38 0 1 19 0 0 2 9 10 2 4 4 110 6 8 2 0110 6 16 1 00110 6 32 0 100110 38 64
Sada se lako može primetiti da u svakom redu važi da je \(2^i\cdot n + b = 38\) (zaista, važi da je \(1\cdot 38 + 0\) = \(2 \cdot 19 + 0\) = \(4\cdot 9 + 2\) = \(8\cdot 4 + 6\) = \(16 \cdot 2 + 6\) = \(32 \cdot 1 + 6\) = \(64 \cdot 0 + 38 = 38\).
Lema 1.A.2. Uslov \(2^i \cdot n + b = n_0\) je invarijanta petlje, gde je \(b\) broj trenutno kodiran nizom binarnih cifara (ako logička vrednost na poziciji \(k\) u nizu odgovara cifri \(b_k\), neka je \(b = \sum_{k=0}^{31} b_k 2^k\)), gde je \(i\) tekuća vrednost promenljive i, dok je \(n_0\) početna, a \(n\) tekuća vrednost neoznačenog broja n.
Dokaz. Dokažimo da je navedeni uslov invarijanta.
Zaista na početku je \(n = n_0\), \(i = 0\) i \(b = 0\) pa tvrđenje važi.
Pretpostavimo da tvrđenje važi pri ulasku u petlju. Promenljive se tokom izvršavanja tela i koraka petlje menjaju na sledeći način. \(n' = n \,\mathrm{div}\,2\), \(b' = b + 2^i\cdot (n \,\mathrm{mod}\,2)\) i \(i' = i+1\). Tada je \(2^{i'} \cdot n' + b'\) = \(2^{i+1} \cdot (n \,\mathrm{div}\,2) + b + 2^i \cdot n \,\mathrm{mod}\,2\) = \(2^i\cdot(2\cdot (n \,\mathrm{div}\,2) + n \,\mathrm{mod}\,2) + b\). Na osnovu definicije celobrojnog deljenja važi da je \(2 \cdot (n \,\mathrm{div}\,2) + n \,\mathrm{mod}\,2 = n\), pa je vrednost prethodnog izraza jednaka \(2^i\cdot n + b\), a na osnovu pretpostavke o tome da invarijanta važi na ulasku u telo petlje znamo da je to jednako \(n_0\). \(\Box\)
Teorema 1.A.4. Po završetku algoritma niz sadrži binarni zapis neoznačenog broja n.
Dokaz. Kako je po izlasku iz petlje \(n=0\), na osnovu invarijante važi da je \(b = n_0\) tj. da niz sadrži binarni zapis polaznog broja. \(\Box\)
U narednom apletu možete videti korake u izvršavanju ovog algoritma i uveriti se da u svakom koraku invarijanta važi.
U prodavnici se nalazi \(n\) poklona, poređanih po ceni u rastućem redosledu. Svaki kupac želi da kupi što vredniji poklon, ali tako da ne prekorači svoj budžet.
Za svakog kupca potrebno je odrediti cenu najskupljeg poklona koji može da priušti.
Za svakog kupca ispisati po jedan ceo broj u zasebnom redu – cenu najskupljeg poklona koji je manji ili jednak od \(x_i\).
Ukoliko takav poklon ne postoji, ispisati \(-1\).
5 20 10 70 35 50 3 5 40 70
-1 35 70
Nakon što se niz cena sortira, zadatak se veoma efikasno može rešiti binarnom pretragom, primenjenu iznova na svaki budžet \(b_i\). Krenimo od sledeće invarijante. Tokom petlje održavaćemo dva pokazivača l i d, tako da su sve cene levo od pokazivača l takve da se poklon može kupiti za dati budžet, a sve cene desno od pokazivača d takve da se poklon ne može kupiti za dati budžet, dok su na pozicijama iz intervala \([l, d]\) cene koje algoritam još nije ispitao. Dakle, pretpostavimo da je stanje niza sledeće:
| l | d | |||||||||
| \(\leq\) | \(\leq\) | \(\leq\) | ? | ? | ? | ? | ? | > | > | > |
U početku su sve cene nepregledane, pa interval \([l, d]\) treba da se poklopi sa celim nizom, tj. \(l\) treba inicijalizovati na \(0\), a \(d\) na \(n-1\).
U svakom koraku petlje određujemo središnju poziciju \(s\) u intervalu \([l, d]\). Da bi se smanjila mogućnost prekoračenja, tu poziciju umesto pomoću \(\left\lfloor{\frac{l+d}{2}}\right\rfloor\), možemo izračunati pomoću \(l + \left\lfloor{\frac{d-l}{2}}\right\rfloor\).
Ako se element na poziciji \(s\) uklapa u dati budžet tj. ako je cena \(c_s \leq b_i\), tada se, zbog sortiranosti, mogu priuštiti i svi pokloni na pozicijama levo od \(s\), pa invarijanta ostaje na snazi ako se \(l\) pomeri na poziciju \(s+1\).
U suprotnom, ako je \(b_i < c_s\), tada se, zbog sortiranosti, ne može prišutiti ni jedan poklon na pozicijama desno od \(s\), pa invarijanta ostaje na snazi ako se \(l\) pomeri na poziciju \(s-1\).
Petlja se izvršava dok god ima nepregledanih cena, tj. dok je interval \([l, d]\) neprazan. Kada se pretraga završi, stanje u nizu je sledeće.
| d | l | |||||||||
| \(\leq\) | \(\leq\) | \(\leq\) | \(\leq\) | \(\leq\) | > | > | > | > | > | > |
Na osnovu invarijante znamo da se svaki element levo od \(l\) može priuštiti, a da se ni jedan desno od \(d\) ne može, pa se stoga traženi najvredniji poklon nalazi na poziciji \(d=l-1\). Ako su svi pokloni preskupi, krajnje vrednosti će biti \(d=-1\) i \(l=0\), pa se vraćanjem vrednosti \(d\) i u tom slučaju dobija korektan rezultat.
Funkcija se zaustavlja jer se u svakom koraku širina intervala \([l, d]\) striktno smanjuje (zahvaljujući zaokruživanju naniže prilikom određivanja središnje pozicije \(s\)).
// vraca poziciju u sortiranom nizu cene najvrednijeg poklona
// koji se moze kupiti za dati budzet ili
// -1 ako su svi pokloni preskupi
int najvredniji_poklon(const vector<int>& cene, int budzet) {
int n = cene.size();
int l = 0, d = n - 1;
while (l <= d) {
int s = l + (d - l) / 2;
if (cene[s] <= budzet)
l = s + 1;
else
d = s - 1;
}
return d;
}#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// vraca poziciju u sortiranom nizu cene najvrednijeg poklona
// koji se moze kupiti za dati budzet ili
// -1 ako su svi pokloni preskupi
int najvredniji_poklon(const vector<int>& cene, int budzet) {
int n = cene.size();
int l = 0, d = n - 1;
while (l <= d) {
int s = l + (d - l) / 2;
if (cene[s] <= budzet)
l = s + 1;
else
d = s - 1;
}
return d;
}
int main() {
int n;
cin >> n;
vector<int> cene(n);
for (int i = 0; i < n; i++)
cin >> cene[i];
sort(begin(cene), end(cene));
int q;
cin >> q;
for (int i = 0; i < q; i++) {
int budzet;
cin >> budzet;
int k = najvredniji_poklon(cene, budzet);
if (k == -1)
cout << -1 << endl;
else
cout << cene[k] << endl;
}
return 0;
}Formalna specifikacija programa može se zadati u obliku Horove trojke. U tom slučaju preduslov opisuje uslove koji važe za ulazne promenljive, dok postuslov opisuje uslove koje bi trebalo da zadovolje rezultati izračunavanja. Na primer, program P za množenje brojeva \(x\) i \(y\), koji rezultat smešta u promenljivu \(z\) bi trebalo da zadovolji trojku \(\{\top\}\)P\(\{z = x\cdot y\}\) (\(\top\) označava iskaznu konstantu tačno, i ovom primeru znači da nema preduslova koje vrednosti \(x\) i \(y\) treba da zadovoljavaju). Ako se zadovoljimo time da program može da množi samo nenegativne brojeve, specifikacija se može oslabiti u \(\{x \geq 0\ \wedge\ y \geq 0\}\)P\(\{z = x\cdot y\}\).
Jedno od ključnih pitanja za verifikaciju je pitanje da li je neka Horova trojka tačna (tj. da li program zadovoljava datu specifikaciju). Hor je dao formalni sistem (aksiome i pravila izvođenja) koji omogućava da se tačnost Horovih trojki dokaže na formalan način (slika @fig:horov_sistem). Za svaku sintaksnu konstrukciju programskog jezika koji se razmatra formiraju se aksiome i pravila izvođenja koji daju rigorozan opis semantike odgovarajućeg konstrukta programskog jezika.2
Opis aksiome i pravila Horove logike:
Aksioma dodele Ova aksioma definiše semantiku naredbe dodele. Izraz \(\varphi[x \rightarrow E]\) označava logičku formulu koja se dobije kada se u formuli \(\varphi\) sva slobodna pojavljivanja promenljive \(x\) zamene izrazom \(E\). Na primer, jedna od instanci ove sheme je i \(\{x + 1 = 2\}\mathtt{y := x + 1}\{y = 2\}\). Zaista, preduslov \(x + 1 = 2\) se može dobiti tako što se sva pojavljivanja promenljive kojoj se dodeljuje (u ovom slučaju \(y\)) u izrazu postuslova \(y = 2\) zamene izrazom koji se dodeljuje (u ovom slučaju \(x+1\)). Nakon primene pravila posledice, moguće je izvesti trojku \(\{x = 1\}\mathtt{y := x + 1}\{y = 2\}\). Potrebno je naglasiti da se podrazumeva da izvršavanje dodele i sračunavanje izraza na desnoj strani ne proizvodi nikakve propratne efekte do samog efekta dodele (tj. izmene vrednosti promenljive sa leve strane dodele) koji je implicitno i opisan navedenom aksiomom.
Pravilo posledice Ovo pravilo govori da je moguće ojačati preduslov i oslabiti postuslov svake trojke. Na primer, od tačne trojke \(\{x=1\}\mathtt{y:=x}\{y=1\}\), moguće je dobiti tačnu trojku \(\{x=1\}\mathtt{y:=x}\{y > 0\}\). Slično, na primer, ako program P zadovoljava \(\{\top\}\mathtt{P}\{z = x\cdot y\}\), tada će zadovoljavati i trojku \(\{x \geq 0 \wedge y \geq 0\}\mathtt{P}\{z = x\cdot y\}\).
Pravilo kompozicije Pravilom kompozicije opisuje se semantika sekvencijalnog izvršavanja dve naredbe. Kako bi trojka \(\{\varphi\}{P_1; P_2}\{\psi\}\) bila tačna, dovoljno je da postoji formula \(\mu\) koja je postuslov programa \(P_1\) za preduslov \(\varphi\) i preduslov programa \(P_2\) za postuslov \(\psi\). Na primer, iz trojki \(\{x=1\}\mathtt{y := x + 1}\{y=2\}\) i \(\{y=2\}\mathtt{z := y + 2}\{z=4\}\), može da se zaključi \(\{x=1\}\mathtt{y := x + 1; z := y + 2}\{z=4\}\).
Pravilo grananja Pravilom grananja definiše se semantika if-then-else naredbe. Korektnost ove naredbe se svodi na ispitivanje korektnosti njene then grane (uz mogućnost korišćenja uslova grananja u okviru preduslova) i korektnosti njene else grane (uz mogućnost korišćenja negiranog uslova grananja u okviru preduslova). Opravdanje za ovo, naravno, dolazi iz činjenice da ukoliko se izvršava then grana uslov grananja je ispunjen, dok, ukoliko se izvršava else grana, uslov grananja nije ispunjen.
Pravilo petlje Pravilom petlje definiše se semantika while naredbe. Uslov \(\varphi\) u okviru pravila predstavlja invarijantu petlje. Kako bi se dokazalo da je invarijanta zadovoljena nakon izvršavanja petlje (pod pretpostavkom da se petlja zaustavlja), dovoljno je pokazati da telo petlje održava invarijantu (uz mogućnost korišćenja uslova ulaska u petlju u okviru preduslova). Opravdanje za ovo je, naravno, činjenica da se telo petlje izvršava samo ako je uslov ulaska u petlju ispunjen.
Dokazi ispravnosti programa u okviru Horove logike koriste instance aksioma (aksiome primenjene na neke konkretne naredbe) i iz njih, primenom pravila, izvode nove zaključke. Dokazi se mogu pogodno prikazati u vidu stabla, u čijem su listovima primene aksioma.
Dokažimo korektnost algoritma stepenovanja. Potrebno je dokazati sledeću trojku.
\[ \begin{array}{l} \{x > 0 \wedge k \geq 0\}\\ \mathtt{i:=0;\ s:=1;\ while\ (i < k)\ do\ begin\ s := s * x;\ i := i+1\ end}\\ \{s = x^k\} \end{array} \]
Na osnovu pravila dodele primenjenog na dodele i:=0 i m:=0 i dva puta primenjenog pravila kompozicije važi trojka
\[\{x > 0 \wedge k \geq 0\}\mathtt{i:=0;\ s:=1}\{x > 0 \wedge k \geq 0 \wedge i = 0 \wedge s = 1\}\]
Dovoljno je, dakle, da dokažemo trojku
\[ \begin{array}{l} \{x > 0 \wedge k \geq 0 \wedge i = 0 \wedge s = 1\}\\ \mathtt{while\ (i < k)\ do\ begin\ s := s * x;\ i := i+1\ end}\\ \{s = x^k\} \end{array} \]
Primenimo pravilo petlje na ovu petlju uz invarijantu \(\varphi \equiv x > 0 \wedge k \geq 0 \wedge 0 \leq i \leq k \wedge m = x^i\). Potrebno je dokazati da telo petlje čuva invarijantu tj. dokazati trojku:
\[ \begin{array}{l} \{x > 0 \wedge k \geq 0 \wedge 0 \leq i \leq k \wedge m = x^i \wedge i < k\}\\ \mathtt{m := m * x;\ i := i+1}\\ \{x > 0 \wedge k \geq 0 \wedge 0 \leq i \leq k \wedge m = x^i\} \end{array} \]
tj. \[ \begin{array}{l} \{x > 0 \wedge k \geq 0 \wedge 0 \leq i < k \wedge m = x^i\}\\ \mathtt{m := m * x;\ i := i+1}\\ \{x > 0 \wedge k \geq 0 \wedge 0 \leq i \leq k \wedge m = x^i\} \end{array} \]
Nakon prve dodele, na osnovu pravila dodele, važi trojka
\[ \begin{array}{l} \{x > 0 \wedge k \geq 0 \wedge 0 \leq i < k \wedge m = x^i\}\\ \mathtt{m := m * x}\\ \{x > 0 \wedge k \geq 0 \wedge 0 \leq i < k \wedge m = x^{i+1}\} \end{array} \]
Zaista, zamenom promenljive \(m\) izrazom \(m\cdot x\) u postuslovu i skraćivanjem vrednosti \(x\) (što je dopušteno, jer važi \(x > 0\)) dobija se tačno preduslov.
Nakon druge dodele, na osnovu pravila dodele važi trojka
\[ \begin{array}{l} \{x > 0 \wedge k \geq 0 \wedge 0 \leq i < k \wedge m = x^{i+1}\}\\ \mathtt{i := i + 1}\\ \{x > 0 \wedge k \geq 0 \wedge 0 \leq i \leq k \wedge m = x^i\} \end{array} \]
Zaista, zamenom vrednosti \(i\) sa \(i+1\) u postuslovu dobija se uslov \(x > 0 \wedge k \geq 0 \wedge 0 \leq i+1 \leq k \wedge m = x^{i+1}\), koji je logička posledica preduslova. Dakle, kombinovanjem pravila posledice i pravila kompozicije dobija se trojka koja garantuje da telo petlje zadovoljava invarijantu, odakle, na osnovu pravila petlje, sledi trojka
\[ \begin{array}{l} \{x > 0 \wedge k \geq 0 \wedge 0 \leq i \leq k \wedge m = x^i\}\\ \mathtt{while\ (i < k)\ do\ begin\ m := m * x;\ i := i+1\ end}\\ \{x > 0 \wedge k \geq 0 \wedge 0 \leq i \leq k \wedge m = x^i \wedge i \geq k\} \end{array} \]
Postuslov implicira \(i=k\), pa zato i željeni uslov \(m = x^k\). Ostalo je još da se prethodna trojka spoji sa trojkom
\[ \{x > 0 \wedge k \geq 0\} \mathtt{i:=0;\ m:=1} \{x > 0 \wedge k \geq 0 \wedge i = 0 \wedge m = 1\} \]
koju smo ranije dokazali. Za to je dovoljno primetiti da uslov \(x > 0 \wedge k \geq 0 \wedge i = 0 \wedge m = 1\) povlači uslov invarijante \(x > 0 \wedge k \geq 0 \wedge 0 \leq i \leq k \wedge m = x^i\). Zato se trojka koja zaključak pravila petlje, na osnovu pravila posledice, može zameniti željenom trojkom:
\[ \begin{array}{l} \{x > 0 \wedge k \geq 0 \wedge i = 0 \wedge m = 1\}\\ \mathtt{while\ (i < k)\ do\ begin\ m := m * x;\ i := i+1\ end}\\ \{m = x^k\} \end{array} \]
Sada na osnovu pravila kompozicije sledi korektnost celog algoritma tj. trojka:
\[ \begin{array}{l} \{x > 0 \wedge k \geq 0\}\\ \mathtt{i:=0;\ m:=1;\ while\ (i < k)\ do\ begin\ m := m * x;\ i := i+1\ end}\\ \{m = x^k\} \end{array} \]
Verifikacija softvera je razvijena oblast i postoji određen broj softverskih sistema koji se koriste za verifikaciju softvera. Samo ilustrativno, da bi se stekao neki osećaj kako se ovaj alat koristi navešćemo dva primera.
Softver Dafny (https://dafny.org/) je razvijen u kompaniji Microsoft. Program se piše na posebnom jeziku koji pored naredbi sadrži i specifikaciju (preduslove, postuslove, invarijante). Ako automatski dokazivač uspe da dokaže sve uslove koje je programer naveo, moguće je izvesti izvorni kod u nekom od klasičnih programskih jezika (Java, C#, …).
U narednom kodu je prikazana funkcija za izračunavanje maksimuma niza. Kao preduslov (klauzula requires) navedeno je da niz mora biti neprazan, a kao postusplov (klauzule ensures) navedeno je da je povratna vrednost max zaista maksimum niza (veća je ili jednaka od svih članova niza i javlja se u nizu). Uz petlju je navedena i invarijanta koja tvrdi da se brojač i petlje uvek nalazi u granicama \([1, n]\), gde je \(n\) dužina niza i da u svakom koraku petlje promenljiva max sadrži maksimum prvih i članova niza. Da bi se automatski moglo dokazati zaustavljanje, formulisana je i varijanta koja kaže da se u svakom koraku petlje vrednost \(n-i\) smanjuje. Ova specifikacija je dovoljna da bi softver Dafny dokazao korektnost algoritma određivanja maksimuma niza i njegove implementacije.
// izračunava najveći element datog nepraznog niza arr
method Max(arr: array<int>) returns (max: int)
// maksimum računamo samo za neprazne nizove
requires arr.Length > 0
// max je najveći element niza
ensures forall j :: 0 <= j < arr.Length ==> max >= arr[j]
ensures exists j :: 0 <= j < arr.Length && max == arr[j]
{
max := arr[0];
var i: int := 1;
while (i < arr.Length)
// granice promenljive i
// (nakon petlje i će biti jednako dužini niza arr)
invariant 1 <= i <= arr.Length
// promenljiva max sadrži najveću vrednost prvih i
// elemenata niza
invariant forall j :: j >= 0 && j < i ==> max >= arr[j]
invariant exists j :: j >= 0 && j < i && max == arr[j]
// naredna varijanta obezbeđuje zaustavljanje
// (ova razlika se smanjuje sve dok ne dođe do nule)
decreases arr.Length - i
{
if (arr[i] >= max)
{
max := arr[i];
}
i := i + 1;
}
}Osnovna razlika između računarskog virusa i računarskog crva je u tome što virus mora biti aktiviran nekakvom akcijom na računaru na kojem se nalazi. S druge strane, crv je samostalni program koji može da se replicira i širi čim dopre do nekog računarskog sistema.↩︎
U svom originalnom radu, Hor je razmatrao veoma jednostavan programski jezik (koji ima samo naredbu dodele, naredbu grananja, jednu petlju i sekvencijalnu kompoziciju naredbi), ali u nizu radova drugih autora Horov originalni sistem je proširen pravilima za složenije konstrukte programskih jezika (funkcije, pokazivače, itd.).↩︎