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)