Zadanie
W dzisiejszym zadaniu należy prześledzić ścieżkę strażnika w 2 wymiarowej przestrzeni, wyglądającej tak:
..#..
.#..#
.....
#.^..
Początkowa pozycja strażnika oznaczona jest jako ^
, z kolei #
oznaczają przeszkody,
ścieżka strażnika wygląda tak, że idzie on cały czas do przodu, aż stanie bezpośrednio
przed przeszkodą, wtedy skręca w prawo o 90 stopni.
Naszym zadaniem jest prześledzić jego ścieżę aż do momentu kiedy wyjdzie on poza znaną nam przestrzeń, wtedy należy policzyć ilość unikalnych pól na których stanął on przec całą swoją podróż. Dla powyższego przykładu ścieżka wygląda tak:
..#..
.#oo#
..||.
#.^|.
A wynik wynosi 6.
Rozwiązanie
Parsowanie
Zaczniemy od parsowania wejścia, na pewno potrzebne będą nam pozycja początkowa strażnika jak i pozycje wszystkich przeszków. Dobrze też otrzymać wielkośc planszy żebyśmy mogli stwierdzić w którym momencie strażnik ją opuścił.
Poniższy kod wyciąga wszystkie te wartości:
# part1.awk
BEGIN {FS = ""}
{
for (i = 1; i <= NF; i++) {
if ($i == "#") {
obstacles[NR][i] = 1
}
if ($i == "^") {
guardx = i
guardy = NR
}
}
} END {
printf "Guard pos: (%d, %d)\n", guardx, guardy
print "Obstacles: "
for (row in obstacles) {
for (col in obstacles[row]) {
print row, col
}
}
print "Max row: ", NR
print "Max col: ", NF
Śledzenie ścieżki
zmienne dx
, dy
są wystarczające by trzymać aktualny kierunek w który zwrócony jest
strażnik:
dx = 0; dy = -1 # zwrócony w górę
while(guardx >= 1 && guardy >= 1 && guardx <= NF && guardy <= NR) {
visited[guardy][guardx] = 1
newx = guardx + dx
newy = guardy + dy
if (obstacles[newy][newx]) { # napotkano przeszkodę
# zmiana kierunku (aktualizacja dx, dy)
...
} else {
# zmiana pozycji (aktualizacja guardx, guardy)
guardx = newx
guardy = newy
}
}
Aktualizacja kierunku
Kiedy napotkamy przeszkodę zmieniamy kierunek żeby iśc w prawo poniższy kod to osiąga:
...
# zmiana kierunku (aktualizacja dx, dy)
if (dx == 0) {
if (dy == -1) dx = 1
else dx = -1
dy = 0
} else {
if (dx == 1) dy = 1
else dy = -1
dx = 0
}
...
Liczenie odwiedzonych pól
Na końcu bez problemu możemy zliczyć ilość odwiedzonych pól
...
# count number of visited tiles
for (row in visited) {
for (col in visited[row]) {
res += visited[row][col]
}
}
print res
Ostateczny kod
# part1.awk
BEGIN {FS = ""}
{
for (i = 1; i <= NF; i++) {
if ($i == "#") obstacles[NR][i] = 1
if ($i == "^") { guardx = i; guardy = NR }
}
} END {
dx = 0; dy = -1 # zwrócony w górę
while(guardx >= 1 && guardy >= 1 && guardx <= NF && guardy <= NR) {
visited[guardy][guardx] = 1
newx = guardx + dx; newy = guardy + dy
if (obstacles[newy][newx]) { # napotkano przeszkodę
if (dx == 0) {
if (dy == -1) dx = 1
else dx = -1
dy = 0
} else {
if (dx == 1) dy = 1
else dy = -1
dx = 0
}
} else { guardx = newx; guardy = newy }
}
# count number of visited tiles
for (row in visited) {
for (col in visited[row]) res += visited[row][col]
}
print res
}
Uruchamiamy:
awk -f part1.awk input.txt
I widzimy poprawny wynik!
Część druga
Żeby zrobić drugą gwiazdkę musimyt się bardziej postarać. Tym razem możemy dołożyć jedną swoją przeszkodę na dowolne pole (oprócz pola startowego strażnika), mamy wykryć czy strażnik wpadnie w nieskończoną pętle z której nigdy nie wyjdzie. Naszym zadaniem jest policzyć ile jest możliwych pozycji na nową przeszkodę które doprowadzą do takiej sytuacji.
Pomysł
Na początku po przeczytaniu treści zadania, wydawało mi się ono dosyć trudne, ale wydaje mi się że wiem jak to rozwiązać w miarę wydajnie. Kluczem jest znaleźć sposób na wykrycie czy jesteśmy już w pętli.
Wygląda na to że jedyny stan jaki musimy śledzić to pozycja i kierunek naszego strażnika, kolejne ruchy są zależne jedynie od tych wartości, więc jeśli drugi raz znajdziemy się w tym samym stanie, wiemy że jesteśmy w pętli.
Kod
Do naszej pętli ścieżki musimy dodać kilka linijek
while(guardx >= 1 && guardy >= 1 && guardx <= NF && guardy <= NR) {
visited[guardy][guardx] = 1
# sprawdź czy byliśmy już w tej sytuacji
if (dxs[guardy][guardx][dx] && dys[guardy][guardx][dy]) {
cycles += 1
break
}
# Zapisz że w tym polu byliśmy już skierowani w tym kierunku
dxs[guardy][guardx][dx] = 1
dys[guardy][guardx][dy] = 1
...
To wystarczy żeby wykryć cykl i go podliczyć
Główna pętla
W głównej pętli będziemy iterować po wszystkich możliwych nowych pozycjach przeszkód:
for (ox = 1; ox <= NF; ox++) {
for (oy = 1; oy <= NR; oy++) {
# # pomińmy pola na których już jest przeszkoda
if (obstacles[oy][ox]) continue
obstacles[oy][ox] = 1
dx = 0; dy = -1
guardx = original_guardx
guardy = original_guardy
delete visited
delete dxs
delete dys
while(guardx >= 1 && guardy >= 1 && guardx <= NF && guardy <= NR) {
...
Po złączeniu tego wszystkiego w całość odpalamy, kod i…
Czekamy…
…
….
Czekamy …..
2 i pół minuty później
Otrzymujemy poprawny wynik! Już myślałem że nasz program się zawiesił. Normalnie zaliczyłbym moje rozwiązanie ale czuję że jesteśmy w stanie poprawić ten czas.
Pomysł na optymalizację
Wydaje mi się że wiele pól nie musi zostać sprawdzonych gdyż strażnik nigdy ich nie odwiedza w oryginalnej ścieżce. Dlatego spróbujemy rozważać tylko te pola które zostały odwiedzone w częsci pierwszej zadania.
Końcowy kod
# solution.awk
BEGIN {FS = ""}
{
for (i = 1; i <= NF; i++) {
if ($i == "#") {
obstacles[NR][i] = 1
}
if ($i == "^") {
original_guardx = i
original_guardy = NR
}
}
}
END {
dx = 0; dy = -1
guardx = original_guardx
guardy = original_guardy
while(guardx >= 1 && guardy >= 1 && guardx <= NF && guardy <= NR) {
visited_p1[guardy][guardx] = 1
newx = guardx + dx; newy = guardy + dy
if (obstacles[newy][newx]) {
if (dx == 0) {
if (dy == -1) dx = 1
else dx = -1
dy = 0
} else {
if (dx == 1) dy = 1
else dy = -1
dx = 0
}
} else { guardx = newx; guardy = newy }
}
for (row in visited_p1) {
for (col in visited_p1[row]) res += visited_p1[row][col]
}
print "part 1", res
for (ox = 1; ox <= NF; ox++) {
for (oy = 1; oy <= NR; oy++) {
if (!visited_p1[oy][ox]) continue
obstacles[oy][ox] = 1
dx = 0; dy = -1
guardx = original_guardx
guardy = original_guardy
delete visited
delete dxs
delete dys
while(guardx >= 1 && guardy >= 1 && guardx <= NF && guardy <= NR) {
visited[guardy][guardx] = 1
if (dxs[guardy][guardx][dx] && dys[guardy][guardx][dy]) {
cycles += 1
break
}
dxs[guardy][guardx][dx] = 1
dys[guardy][guardx][dy] = 1
newx = guardx + dx
newy = guardy + dy
if (obstacles[newy][newx]) {
if (dx == 0) {
if (dy == -1) dx = 1
else dx = -1
dy = 0
} else {
if (dx == 1) dy = 1
else dy = -1
dx = 0
}
} else {
guardx = newx
guardy = newy
}
}
obstacles[oy][ox] = 0
}
}
print "Part 2", cycles
}
Ostatecznie otrzymujemy te same, poprawne rozwiązania w 28 sekund, więc pięciokrotnie przyśpieszyliśmy działanie programu.
Podsumowanie
Rozwiązanie dzisiejszego zadania nie sprawiło dużych problemów, jednak nie mam pomysłu jak zoptymalizować go do bardziej akceptowalnego czasu. Na pewno rozważamy dużo redundantnych ścieżek.
Kod dzisiejszego rozwiązania też jest już dużo mniej czytelny niż w poprzednich dniach,
powoli używanie awk
, może stawać się większym problemem niż plusem. Mimo wszystko nawet
dzisiaj używanie tego narzędzia nie było złym doświadczeniem.