Poprawność algorytmu - ćwiczenia 2 Definicja 1. Algorytm jest częściowo poprawny, gdy dla każdego zestawu danych X ze zbioru dopuszczalnych danych wejściowych jeżeli algorytm sie zatrzyma, to relacja R pomiędzy zestawem danych X, a otrzymanym zestawem wyników jest zachowana. Warunek stopu Algorytm zatrzymuje sie dla dozwolonego zbioru danych wejściowych jeśli zatrzymuje sie (kończy) dla każdego zestawu danych z tego zbioru. Definicja 2. Algorytm jest całkowicie poprawny, gdy dla każdego zestawu danych X ze zbioru dopuszczalnych danych wejściowych algorytm zatrzymuje się i jest częściowo poprawny. algorytm całkowicie poprawny = algorytm częściowo poprawny + warunek stopu Dowodzenie częściowej poprawności algorytmu - metoda niezmienników Floyda: Wybieramy w algorytmie punkty kontrolne. Dla każdego z punktów kontrolnych określamy asercje (funkcja logiczna reprezentująca przypuszczenie co do stanu algorytmu; niezmiennik). Pokazujemy, że z prawdziwości jednej asercji wynika prawdziwość następnej asercji (a to pociąga za sobą prawdziwość ostatniej asercji). Asercja początkowa (w pierwszym punkcie kontrolnym) - warunki nakładane na dopuszczalne dane wejściowe. Asercja końcowa (w ostatnim punkcie kontrolnym) - opisuje relacje wyników i danych wejściowych. Niezmiennik pętli (iteracji) - własność (formuła), która jeśli jest prawdziwa na początku wykonania pętli, to jest również prawdziwa po wykonaniu pętli. Dowodzenie całkowitej poprawności algorytmu: metoda niezmienników, pokazanie zbieżności Zbieżnik - wielkość zależna od zmiennych i danych; wykazujemy, ze zbieżnik zmniejsza się w czasie wykonania algorytmu, lecz istnieje granica poniżej której nie zejdzie, np. zbieżnikiem dla algorytmu sortowania jest liczba elementów, które pozostały jeszcze nieposortowane. Zad 1. Rozważmy problem znalezienia elementu największego w danym ciągu liczb naturalnych x0,..., xn-1. Algorytm rozwiązujący ten problem możemy zapisać w postaci: max_el = x0; i = 1; while (i < n) { if (xi > max_el) max_el= xi; i = i+1; } a) Wykaż częściową poprawność tego algorytmu korzystając z metody niezmienników. b) Wykaż całkowitą poprawność algorytmu Rozwiązanie: Aby wykazać częściową poprawność algorytmu należy pokazać, że: w pierwszym kroku iteracji prawdziwość asercji 1 implikuje prawdziwość asercji 2 w kolejnych przebiegach iteracji: prawdziwość asercji 2 implikuje prawdziwość tej asercji w następnym kroku (niezmiennik iteracji) w ostatnim kroku iteracji prawdziwość asercji 2 implikuje prawdziwość asercji 3 Asercja 1: Naturalnym warunkiem początkowym będzie założenie niepustości ciągu: nie można znaleźć elementu największego w zbiorze pustym. Możemy ją zapisać w postaci: n>0. Asercja 2: W i-tym kroku (1<=i<n) element maksymalny max_eli = max{max_eli-1, xi} Asercja 3: W max_el mamy element maksymalny całego ciągu tj. max_el = xk dla pewnego k < n oraz dla wszystkich i<n, xi <= max_el. //Asercja 1 max_el = x0; i = 1; while (i < n) //Asercja 2 { if (xi > max_el) max_el = xi; i = i+1; } // Asercja 3 Algorytm max zatrzymuje się dla dowolnych danych w strukturze liczb naturalnych. Rzeczywiście, zmienna i, kontrolująca pętlę, przyjmuje jako swoje wartości kolejne liczby naturalne, a parametr n też jest liczbą naturalną. Zatem po skończonej liczbie kroków zmienna i przyjmie wartość n. Co więcej, po wykonaniu algorytmu spełniony jest warunek (xi <= max_el dla wszystkich i): W pętli "while" każdy element ciągu jest porównywany z elementem max_el. Jeśli po przejrzeniu i-pierwszych elementów ciągu, max_el ma wartość największego z nich, to w następnym kroku, max_el przyjmie wartość największego elementu wśród pierwszych (i+1) elementów: max{max{x1,..., xi}, xi+1} = max{x1,..., xi+1}. Ponieważ, dla ciągu o długości 1, wartością zmiennej max_el jest element największy (ten jedyny element ciągu), to, na mocy zasady indukcji matematycznej, dla ciągu dowolnej długości n, po zakończeniu pętli "while", wartością max_el będzie element największy ciągu x1,...,xn. Badanie poprawności metodą niezmienników możemy również realizować bezpośrednio w programie korzystając (w C++) z makra assert(...): #include <cassert> #include <iostream> using namespace std; //funkcja pomocnicza do asercji 3 bool testmaxtab(int n, int x[], int max_el) { for (int i=0; i<n; i++) if (x[i] > max_el) return false; return true; } int maxtab(int n, int x[]) { //Asercja 1 assert(n > 0); int max_el = x[0]; int i = 1; while (i < n) { int max_el_i_1 = max_el; if (x[i] > max_el) max_el = x[i]; //Asercja 2 assert(max_el == (max_el_i_1 > x[i]?max_el_i_1 : x[i])); i = i+1; } // Asercja 3 assert(testmaxtab(n, x, max_el)); return max_el; } int main(int argc, char* argv[]) { int tab[10] = {3,56,-3,4,67,31,11,55,9,0}; cout << maxtab(10, tab) << endl; system("pause"); return 0; } Uwaga: Ze względu na to, że asercja jest nienormalnym przerwaniem programu, to aby zobaczyć błędy zgłaszane przez assert w programie konsolowym należy ten program uruchomić z wiersza poleceń/konsoli. Aby wyłączyć wszystkie asercje użyte w danym kodzie należy przed linijką #include <cassert> umieścić definicję: #define NDEBUG Przetestuj działanie asercji generując błędy np. test asercji 1: cout << maxtab(0, tab) << endl; test asercji 2: zmieniamy wyrażenie na przeciwne : if (x[i] < max_el) Zad 2. Rozważmy algorytm znajdowania NWD dwóch liczb naturalnych x i y // jeżeli jedna z liczb x, y jest równa zeru, to największy wspólny //dzielnik tych liczb jest równy drugiej z nich if (x*y == 0) nwd = x+y; else { while (!(x == y)) { if (x > y) x = x - y; else y = y - x; }// jeżeli liczby są równe, to ich największy wspólny dzielnik jest równy ich wspólnej wartości } nwd = y; Wykaż całkowitą poprawność tego algorytmu.