Rozwiązania kilku zadań z WDI
Transkrypt
Rozwiązania kilku zadań z WDI
Rozwiązania kilku zadań z WDI Antoni Kościelski 25 listopada 2015 1 Zadanie 6 z listy 1 Rozwiązując to zadanie należy w pierwszym rzędzie przedstawić algorytm znajdujący największą wspólną wielokrotność dwóch danych dodatnich liczb naturalnych. Dodatkowo należy omówić w tym przypadku kwestię rozmiaru danych i ocenić złożoność czasową przedstawionego algorytmu. Oto algorytm zaproponowany przez jednego z uczestników ćwiczeń: read(a) (1) read(b) (2) x ← a (3) y ← b (4) dopóki x 6= y wykonuj (5) jeżeli x < y (6) to x ← x + a, (7) a w przeciwnym razie y ← y + b (8) pisz(x) (9) Algorytm ten ma kilka niezmienników. Podczas wykonywania algorytmu niemal stale są prawdziwe następujące stwierdzenia: 1) wartość zmiennej x jest wielokrotnością a (pierwszej z danych liczb), 2) wartość zmiennej y jest wielokrotnością b (drugiej z danych liczb), 3) wartości zmiennych x i y spełniają nierówności x ¬ W oraz y ¬ W dla dowolnej ustalonej wielokrotności W danych liczb. Z tych niezmienników łatwo wynika poprawność algorytmu. Po wykonaniu pętli dopóki zmienne x i y mają tę samą wartość i jest ona podzielna zarówno przez pierwszą, jak i przez drugą daną, jest wieć wspólną wielokrotnością danych liczb. Ponadto ta wspólna wartość nie przekracza jakiejkolwiek innej wspólnej wielokrotności danych. Oznacza to, że jest najmniejszą wspólną wielokrotścią danych liczb. Również bez trudu ustalamy czas wykonania podanego algorytmu rozumiany jako liczba wykonanych czynności podstawowych, czyli podstawień, testów i wyświetleń. Jest oczywiste, że zmiennych x i y można używać jako liczników liczby wykonań pętli (np. w odpowiednich jednostkach). Widać, że po uruchomieniu algorytmu z danymi m i n pętla dopóki zostanie wykonana N W W (m, n) N W W (m, n) + −2 m n 1 razy. Stąd otrzymujemy, że czas pracy algorytmu (rozumiany jak wyżej) wyraża się wzorem N W W (m, n) N W W (m, n) T (m, n) = 3 · + m n ! (w pętli za każdym razem wykonujemy 3 czynności, w tym test, a poza pętlą – 5 czynności i test kończący wykonanie pętli). Pojęcie złożoności czasowej oczywiście zależy od pojęcia rozmiaru danych (patrz też uwagi w rozwiązaniu zadania 2 z listy 3). W przypadku rozważanego zadania daną jest para liczb naturalnych. Powstaje pytanie, jak liczyć rozmiar pary liczb. Czasem przyjmuje się, że rozmiar pary danych jest równy większemu z rozmiarów danych liczb. Zgodnie z bardzo naturalną definicją rozmiar pary jest sumą rozmiarów poszczególnych danych: podając dwie liczby trzeba napisać przedstawienia obu. Rozmiar liczby też możemy definiować na kilka sposobów: jako dana liczba lub jako długość jej jakiegoś przedstawienia. Przyjmijmy, że 1) t1 oznacza złożoność podanego algorytmu dla rozmiaru rozumianego jako maksimum danych liczb, 2) t2 to złożoność dla rozmiaru rozumianego jako maksimum długości przedstawień dwójkowych oraz 3) t3 to złożoności w sytuacji, gdy rozmiar danych to suma długości przedstawień dwójkowych. Wtedy na przykład 3(k + 1) ¬ t1 (k) = max{T (m, n) : m, n ¬ k} ¬ max{3(m + n) : m, n ¬ k} ¬ 6 · k. Pierwsza nierówność wynika z wzoru T (1, k) = 3(k +1)), druga – z oszacowania T (m, n) ¬ 3(m+n) będącego konsekwencją nierówności N W W (m, n) ¬ m · n. Wydaje mi się, że w analogiczny sposób uzyskujemy nierówności 3 · 2k ¬ t2 (k) = max{T (m, n) : | m |2 , | n |2 ¬ k} ¬ max{3(m + n) : | m |2 , | n |2 ¬ k} ¬ 6 · 2k − 6. (| n |2 oznacza tu długość przedstawienia dwójkowego liczby n), a także 3 3 k · 2 ¬ t3 (k) = max{T (m, n) : | m |2 + | n |2 = k} ¬ max{3(m + n) : | m |2 + | n |2 = k} ¬ · 2k . 2 2 2 Zadanie 6 z listy 2 Zadanie. Podaj schemat blokowy i program w kodzie RAM dla zadania opisanego następującą specyfikacją: Wejście: liczba naturalna n > 1 i n elementowy ciąg liczb x1 , x2 , . . . , xn , Wyjście: 1, jeżeli istnieje liczba i taka, że 0 < i ¬ n, x1 < x2 < . . . < xi oraz xi > xi+1 > . . . > xn , a w przeciwnym razie – 0. Ciągi spełniające warunek podany w treści zadania będziemy teraz nazywać rosnąco-malejącymi. Ciągi rosnące oraz malejące są szczególnymi przypadkami ciągów rosnąco-malejących. Zauważmy też, że przytoczona treść zadania odbiega trochę od oryginalnej. Dla uproszczenia sytuacji w specyfikacji zadania żądamy, aby analizowany ciąg miał przynajmniej dwa elementy. wieloktrotnością danych na początku liczb. Zamiast schematu blokowego niżej podaję opis algorytmu rozwiązującego to zadanie: czytaj(dlg) (1) czytaj(p ost) (2) czytaj(ost) (3) dlg ← dlg - 2 (4) dopóki 0 < dlg oraz p ost < ost wykonuj (5) p ost ← ost; (6) Read(ost); (7) dlg ← dlg - 1 (8) (9) jeżeli p ost < ost, to pisz(1) a w przeciwnym razie, dopóki 0 < dlg oraz ost < p ost wykonuj (10) (11) p ost ← ost Read(ost) (12) dlg ← dlg - 1 (13) jeżeli p ost > ost, to pisz(1) (14) w przeciwnym razie pisz(0) (15) Algorytm został napisany całkowicie bez skoków1 i realizuje następującą ideę: najpierw szukamy możliwie długiego fragmentu danego ciągu, który jest rosnący, a następnie możliwie długiego fragmentu malejącego. Jeżeli cały ciąg składa się z tych dwóch fragmentów, to jest on rosnąco-malejący. Algorytm wykorzystuje trzy zmienne: dlg, p ost oraz ost. Z każdą z tych zmiennymi wiąże się pewien niezmienniki programu i jest ona wykorzystywana w określony sposób. W zasadzie przez cały czas wykonywania algorytmu zmienna dlg pamięta liczbę wyrazów danego ciągu, które jeszcze nie zostały przeczytane, zmienna p ost – przedostatni wyraz przeczytanego fragmentu danego ciągu, a zmienna ost – ostatni wyraz tego fragmentu. Symbolem N będziemy dalej oznaczać długość danego ciągu, czyli początkową wartość zmiennej dlg. Zauważmy teraz, że do momentu wykonania pierwszej pętli programu jest niezmiennie prawdziwe następujące stwierdzenie: ciąg x1 , x2 , . . . , xN −dlg−1 jest rosnący (inaczej: N − dlg − 1 pierwszych wyrazów danego ciągu tworzy ciąg rosnący). Po zakończeniu wykonywania pierwszej instrukcji dopóki nie zachodzi warunek sprawdzany przed wejściem do pętli, czyli albo zmienna dlg przyjmuje wartość 0, albo nie zachodzi nierówność p ost < ost. Ponadto zachodzi wspomniany niezmiennik. Jeżeli w tej sytuacji mimo wszystko ma miejsce nierówność p ost < ost, to cały wczytany fragment ciągu jest rosnący, a także dlg = 0, został wczytany cały ciąg danych i w konsekwencji cały ciąg danych jest rosnący (także rosnącomalejący). Tak więc wypisując 1 algorytm przekazuje nam informację zgodną ze stanem faktycznym. Co więcej, ta informacja musi zostać przekazana w tym momencie, gdyż w analogicznej sytuacji, po wejściu do drugiej pętli i jej wykonaniu, algorytm powinien wypisać 0. Druga instrukcja dopóki też ma niezmiennik i jest nim stwierdzenie ciąg x1 , x2 , . . . , xN −dlg−1 jest rosnąco-malejący. Przed rozpoczęciem wykonywania wspomnianej instrukcji ta własność jest prawdziwa, podczas wykonywania instrukcji do ciągu są dopisywane coraz mniejsze elementy. Własność ta zachodzi więc także po wykonaniu instrukcji w całości. Dalsza analiza taka, jak przeprowadzona dla poprzedniego dopóki, pozwala wykazać poprawność podanego algorytmu. Poniżej mamy ten sam algorytm zapisany w kodzie maszyny RAM. 1 Chodzi tu o skoki w języku wyższego rzędu. Skoki są ukryte w instrukcji dopóki. READ 1 READ 2 READ 3 LOAD 1 SUB =2 STORE 1 e1:JZERO e2 LOAD 3 SUB 2 JGTZ s1 JUMP e3 s1:LOAD 3 STORE 2 READ 3 LOAD 1 SUB =1 STORE 1 JUMP e1 e2:LOAD 3 SUB 2 JGTZ p1 e3:STORE 1 e4:JZERO e5 LOAD 2 SUB 3 JGTZ s2 JUMP p0 s2:LOAD 3 STORE 2 READ ∧ 3 LOAD 1 SUB =1 STORE 1 JUMP e4 e4:LOAD 2 SUB 3 JGTZ p1 p0:WRITE =0 JUMP end p1:WRITE =1 end: (1) (2) (3) (4) wczytanie długości danego ciągu wczytanie pierwszego i drugiego wyrazu ciągu zmniejszenie liczby wyrazów do przeczytania (5) przygotowanie do testu, czy dlg > 0 wyjście z pętli, jeżeli dlg = 0 początek testu, czy p ost < ost kontynuacja po pozytywnym wyniku testu wyjście z pętli, wtedy ost ¬ p ost (6) (7) (8) zmniejszamy dlg o 1 (9) powrót na początek pętli, w akumulatorze wartość dlg test z instrukcji jeżeli skok do instrukcji pisania 1 (10) przygotowanie do testu, czy dlg > 0 wyjście z pętli, jeżeli dlg = 0 wyjście z pętli, wtedy p ost ¬ ost (11) (12) (13) zmniejszamy dlg o 1 powrót na początek pętli, w akumulatorze dlg (14) (15) Zadanie to można rozwiązać też nieco inaczej, zgodnie z następującym schematem: czytaj(dlg) czytaj(p ost) dlg ← dlg - 1 dopóki 0 < dlg wykonuj Read(ost); ... jeżeli p ost = ost, to pisz(0) i zakończ wykonywanie algorytmu jeżeli p ost > ost, to zakończ pętlę ... ... używając skoków, a nawet innej pętli niż dopóki. Niżej jest podana lista czynności takiego programu, która daje się łatwo wyrazić za pomocą schematu blokowego: czytaj(dlg); czytaj(p ost); dlg ← dlg - 1; jeżeli dlg = 0, to pisz(1) i zakończ działanie programu czytaj(ost); jeżeli ost < p ost, to przejdź do punktu (10) jeżeli ost = p ost, to pisz(0) i zakończ działanie programu p ost ← ost; przejdź do punktu (3) dlg ← dlg - 1 jeżeli dlg = 0, to pisz(1) i zakończ działanie programu czytaj(ost); jeżeli p ost < ost, to pisz(0) i zakończ działanie programu jeżeli ost = p ost, to pisz(0) i zakończ działanie programu p ost ← ost; przejdź do punktu (10) (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14) (15) (16) Na koniec podaję kod programu maszyny RAM realizującej wyżej podany algorytm. READ 1 READ 2 e1:LOAD 1 SUB =1 STORE 1 JZERO p1 READ 3 LOAD 2 SUB 3 JGTZ e2 JZERO p0 LOAD 3 STORE 2 JUMP e1 e2:LOAD 1 SUB =1 STORE 1 JZERO p1 READ 3 LOAD 3 SUB 2 JGTZ p0 JUMP p0 LOAD 3 STORE 2 JUMP e2 p0:WRITE =0 JUMP end p1:WRITE =1 end: (1) (2) (3) wczytanie długości danego ciągu wczytanie pierwszego wyrazu ciągu zmniejszenie wartości zmiennej dlg o 1 (4) (5) przygotowanie do testu, czy dlg > 0 wyjście z pętli, jeżeli dlg = 0 wczytanie kolejnego wyrazu ciągu początek testu, liczymy p ost - ost (6) (7) (8) wczytany wyraz okaza si mniejszy od poprzedniego wyjście z pętli, mamy ost = p ost ostatni zapamiętujemy w miejscu przedostatniego (9) powrót na początek pętli, w akumulatorze wartość dlg zmniejszamy dlg o 1 wyjście z pętli, jeżeli dlg = 0 (10) liczymy ost - p ost (11) (12) wyjście z pętli, wtedy p ost ¬ ost (11) powrót na początek pętli, w akumulatorze dlg (15)