Zajęcia 8: jak to napisać?

Zadanie 1: spacer króla po szachownicy

Po szachownoci n na m przechadza się król, ma ustalone pewne pole startowe i docelowe. Pewne pola są zabronione (znak '#'). Należy znaleźć jak najkrótszą ścieżkę dla króla.

Widać, że w zadaniu należy tylko wykonać algorytm BFS (przeszukiwanie wszerz). Problem polega na tym, jak rozsądnie reprezentować graf, a w szczególności krawędzie. Poniżej wszystkie możliwe przejścia obliczamy łatwo i przyjemnie, korzystając z tablic przesunięć dx, dy w możliwych ruchach króla, a także z funkcji sprawdzającej, czy po wykonaniu ruchu król nadal mieści się w danym wymiarze na planszy. Doświadczenie (moje) pokazuje, że tak najłatwiej uniknąć błędów wynikających z copy-paste'a lub na przykład z pomylenia wymiarów n i m. Dodatkowo, kolejka w algorytmie BFS jest reprezentowana w zwykłym wektorze, co także upraszcza kod (choć czasem może być nieefektywne pamięciowo).

int n,m;
char t[M][M];

vector<pair<int, int>> kol;  // "kolejka"
int odl[1010][1010];  // wyniki

int dx[] = {1,1,1,0,0,-1,-1,-1};
int dy[] = {-1,0,1,-1,1,-1,0,1};
inline bool ins(int g, int h) { return g >= 0 && g < h; }

int bfs(pair<int, int> pocz, pair<int, int> kon) {
  kol.push_back(pocz);
  for (int i = 0; i < n; ++i)
    for (int j = 0; j < m; ++j)
      odl[i][j] = INFTY;
  odl[pocz.first][pocz.second] = 0;

  for (int i = 0; i < (int)kol.size(); ++i) {
    pair<int, int> q = kol[i];
    for (int z = 0; z < 8; ++z) {
      int a = q.first + dx[z], b = q.second + dy[z];
      if (ins(a, n) && ins(b, m) && odl[a][b] >= INFTY && t[a][b] != '#') {
        odl[a][b] = odl[q.first][q.second] + 1;
        kol.push_back(make_pair(a, b));
      }
    }
  }
  return odl[kon.first][kon.second];
}

Zamiast tablic dx i dy można też użyć innej metody: przechodzimy pętlami po wszystkich możliwych parach przesunięć i piszemy warunek na to, kiedy dana para przesunięć jest dobra. Ta metoda dobrze uogólnia się na więcej wymiarów. Ale uważam tę metodę za błędogenną: swego czasu dzięki napisaniu niepoprawnego warunku udało mi się odpaść z Google Code Jam Europe 2005.

  for (int i = 0; i < (int)kol.size(); ++i) {
    pair<int, int> q = kol[i];
    for (int dx = -1; dx <= 1; ++dx)
      for (int dy = -1; dy <= 1; ++dy)
        if (dx || dy) {
          int a = q.first + dx, b = q.second + dy;
          if (ins(a, n) && ins(b, m) && odl[a][b] >= INFTY && t[a][b] != '#') {
            odl[a][b] = odl[q.first][q.second] + 1;
            kol.push_back(make_pair(a, b));
          }
        }
  }

Zadanie 2: Rudy osioł

Rozważamy łamigłówkę Rudy osioł (inna nazwa: Klotski), której zasady są opisane np. w tym artykule z Delty. Chcemy w jak najmniejszej liczbie ruchów dojść ze stanu początkowego do końcowego.

Zastosujemy znów algorytm BFS. Przede wszystkim należy przekonać się, że będzie odpowiednio niewiele stanów do przejrzenia. W podanym artykule znajdujemy oszacowanie, że będzie ich kilka milionów, czyli nie za dużo. Stany będziemy przenumerowywać, począwszy od jedynki, np. z użyciem mapy. Pozostaje pytanie, jakiej struktury użyć do reprezentacji stanu, tak aby można było łatwo przesuwać klocki.

Możemy ponumerować pola planszy (np. wierszami) liczbami od 0 do 19. Wtedy położenie danego klocka możemy zapisać jako maskę bitową pól, które on zajmuje (mask). Pamiętamy także wszystkie pola zajmowane przez jakikolwiek klocek (Mask). Wtedy przesunięcia klocka wykonujemy następująco. Lewo:

int maska_lewa = (1 << 4) * ((1 << 5) - 1);
if (!(mask & (maska_lewa)) && !((Mask ^ mask) & (mask >> 1))) mask >>= 1;

Dół:

int maska_dolna = (1 << 20) - (1 << 16);
if (!(mask & (maska_dolna)) && !((Mask ^ mask) & (mask << 4))) mask <<= 4;

Pozostałe kierunki podobnie. Musimy jeszcze zadbać o to, żeby cztery klocki o wymiarach 1 x 2 i cztery klocki 1 x 1 traktować jako nierozróżnialne. Jednym z możliwych sposobów jest pamiętanie masek bitowych wszystkich pól zajmowanych przez te dwa typy klocków. Warto zauważyć, że taką maskę możemy zawsze jednoznacznie odkodować. Czyli cała struktura może wyglądać np. tak:

struct stan {
  int mask21, mask22;  // pojedyncze klocki
  int mask12, mask11;  // wszystkie pola zajmowane przez te dwa typy klocków
};

Zadanie 3: Kratka i skojarzenia

Na prostokątnej planszy o wymiarach n na m nany pewne pola dostępne, a pewne zabronione. Chcemy pokryć tę planszę klockami o wymiarach 2 na 1 lub 1 na 2. Na ile sposobów możemy to zrobić? Znów, interesuje nas reszta z dzielenia wyniku przez jakąś niedużą liczbę całkowitą. Zakładamy, że n ≤ 15, m ≤ 100.

Zadanie 4: Kratka i cykle Hamiltona

Mamy taką samą planszę jak w poprzednim zadaniu. Interesują nas cykle przechodzące przez wszystkie pole dostępne (przejść możemy tylko pomiędzy polami sąsiadującymi w pionie lub w poziomie). Znów chcemy wiedzieć, ile jest różnych takich cykli.

Interesuje nas szkic rozwiązania działającego w czasie O(2n * n2). Wskazówka: poprawne wyrażenia nawiasowe i liczby Catalana.

Zadanie domowe

Zadanie domowe: zadanie Skoczek na ASD-SIO.


Jakub Radoszewski