Zajęcia 4: Trochę wolniej ale łatwiej, cz. 1

Zachęta

Czasem chcemy coś zaimplementować bardzo efektywnie. Innym razem może nam zależeć na szybkości samego procesu implementacji, przy zachowaniu rozsądnej efektywności kodu. Jeśli chcemy posortować 5 liczb i nie mamy do dyspozycji wbudowanego algorytmu sortowania, możemy to zrobić np. tak:

void sort(int* a, int n) {
  for (int i = 0; i < n; ++i)
    for (int j = i; j < n; ++j)
      if (a[i] > a[j])
        swap(a[i], a[i]);
}

Chyba prościej się nie da. Dodatkowo, nie ma gdzie się tu pomylić – czy zaczniemy od j = i, czy od j = i+1, i tak będzie dobrze. Podczas tych zajęć pokażemy kilka przykładów na to, jak kosztem czynnika logarytmicznego lub pierwiastkowego można uzyskać dużo prostszy algorytm.

Kiedyś pojawią się tu notatki z prawdziwego zdarzenia. Na razie jest sporo odnośników do łatwo dostępnych źródeł.

Potęgowanie funkcji

Patrz metoda 2c w opisie rozwiązania zadania Żabka z III etapu XVII Olimpiady Informatycznej (Niebieska książeczka).

Zadanie Koleje

Patrz ostatni przykład z lekcji o sztuczkach algorytmicznych na MAIN-ie.

Zadanie o szalonych/szczęśliwych liczbach

Patrz przedostatni przykład z lekcji o sztuczkach algorytmicznych na MAIN-ie oraz jedno z zadań domowych do tej lekcji.

Ukryte słowniki

Słownik to struktura danych, która pozwala wykonywać operacje: find(x) – test na występowanie elementu w strukturze, ewentualnie zwraca iterator na ten element; insert(x) – wstawienie elementu; delete(x) – usunięcie elementu. Jest wiele klasycznych słowników, umożliwiających wykonywanie powyższych operacji w czasie O(log(n)), przy czym n to liczba operacji wykonywanych na strukturze: np. w bibliotece STL mamy set i map, są to zrównoważone drzewa binarne, takie jak drzewa czerwono-czarne, AVL, są też drzewa Splay, B-drzewa itp. Można także używać haszowania. Tym razem pokażemy, jak zastosować tablice Younga do implementacji operacji słownikowych w czasie O(sqrt(n)).

Tablica Younga to tablica kwadratowa t[N][N] wypełniona elementami słownika i INFTY (nieskończoność), jeśli żadnego elementu na danej pozycji nie ma. Dodatkowo, zarówno wiersze, jak i kolumny tablicy są uporządkowane niemalejąco. To pozwala wykonać operację find(x) za pomocą jednego przejścia w dół i w lewo, w czasie O(N):

pair<int, int> find(int x) {
  int i = 0, j = N - 1;
  while (i < N) {
    while (j ≥ 0 && t[i][j] > x)
      --j;
    if (t[i][j] == x) return make_pair(i, j);
    ++i;
  }
  return make_pair(-1, -1);  // brak
}

W operacji insert(x) musimy przede wszystkim zamienić jakoś nieskończoność na x. Najłatwiej to zrobić w polu t[N-1][N-1], bo tam na pewno znajduje się nieskończoność, jeśli w ogóle tablica nie jest całkowicie zapełniona. Potem tylko musimy poprawić uporządkowanie, patrząc na dwa sąsiednie pola. Po tej poprawce możemy być zmuszeni poprawiać tak dalej, idąc w górę i w lewo. Poniższy kod jest jakiś paskudny, ale ma trywialną strukturę.

void insert(int x) {
  int i = N - 1, j = N - 1;
  t[i][j] = x;
  while (1) {
    if ((!i || t[i-1][j] ≤ x) && (!j || t[i][j-1] ≤ x)) break;
    if (i && j) {
      if (t[i-1][j] > t[i][j-1]) {
        swap(t[i-1][j], t[i][j]);
        --i;
      } else {
        swap(t[i][j-1], t[i][j]);
        --j;
      }
    } else if (i) {
      swap(t[i-1][j], t[i][j]);
      --i;
    } else {
      swap(t[i][j-1], t[i][j]);
      --j;
    }
  }
}

Wreszcie w operacji delete(x) najpierw znajdujemy element x za pomocą find(x), ustawiamy go na INFTY, po czym poprawiamy tablicę lokalnie podobnie jak powyżej, tylko idąc w dół i w prawo.

Koszt każdej operacji słownikowej to O(N) = O(sqrt(n)).

Sortowanie przez odwracanie

Omówiliśmy zadanie domowe. Notatki w tym IKO z Delty.

Zadanie domowe

Zadanie domowe: zadanie Robot sortujący na ASD-SIO.


Jakub Radoszewski