Dzisiaj nadal obyło się bez większych problemów, choć zacząłem czuć że używanie AWKa zaczyna mi bardziej przeszkadzać niż pomagać.
Zadanie
Dzisiaj musimy zliczyć wystąpienia słowa XMAS
w dwuwymiarowej tablicy liter. Słowa
mogą występować w liniach pionowych, poziomych i ukośnych, w poniższym przykładzie słwo
występuje 3 razy. (kropki są dodane dla ułatwienia ale w ostatecznie w inpucie będą tam
inne losowe litery).
XMAS
..AA
.M.M
X..X
Rozwiązanie
Jeśli wypiszemy sobie wszystkie wiersze, kolumny i linie ukośne, mógłbym za pomocą grepa
znaleźć wystąpienia słowa XMAS
i jego odwrotności SAMX
.
Wypisanie lini
Do tego celu użyję języka AWK. O ile wypisanie liń poziomych jest banalnie proste (w każdej
lini wypisujemy $0
), to będzie to trudniejsze dla kolumn i przekątnych. Będziemy mieć
dwuwymiarowe listy trzymające dla każdej lini litery w niej występujące.
W awku możemy wygodnie iterować po znakach w linijce jeśli ustawimy sobie separator fieldów
na pustego stringa (""
), w ten sposób poszczególne fieldy ($1, $2, ..., $NF)
,
będą kolejnymi znakami, można to ustawić w command linie jako argument -F
, ale ja ustawię
go w naszym programie w kodzie od BEGIN żeby użytkownik nie musiał o tym pamiętać:
BEGIN {FS = ""}
Nasz dalszy kod będzie wyglądał w ten sposób:
{
for (i = 1; i <= NF; i++) {
cols[i][NR] = $i # dodaj aktualny znak do kolumny
diags[i - NR][NR] = $i # dodaj znak do przekątnej (prawo-dół)
diags2[NF - i - NR + 1][NR] = $i # dodaj znak do przekątnej (lewo-dół)
}
print $0 # wypisz linię poziomą
}
END {
print_2d_arr(cols); # wypisz linie pionowe
print_2d_arr(diags); # wypisz przekątne (prawo-dół)
print_2d_arr(diags2); # wypisz przekątne (lewo-dół)
}
Funkcja print_2d
została zdefiniowana wcześniej jako funkcja pomocnicza:
function print_arr(arr) {
for (i in arr) { printf arr[i] }
printf "\n"
}
function print_2d_arr(arr) { for (i in arr) print_arr(arr[i]) }
Po uruchomieniu kodu otrzmujemy taki output:
$ awk -f part1.awk input.txt > lines
$ cat lines
XMAS
..AA
# ... celowo obcięte
SAMX
A..
M.
X
Zauważmy że nie wszystkie przekątne mają tyle znaków co linie poziome czy pionowe.
Zliczanie wystąpień XMAS
Kolejny raz przyda nam się grep -o "pattern" [plik]
, szukałem jak złapać wystąpienia
słowa XMAS
do przodu i do tyłu, ale nie udało mi się to. Niestety poniższy kod nie działa:
grep -o "XMAS\|SAMX" lines
Z tego powodu że nie łapie on obu słów jeśli nachodzą na siebie XMASAMX
, wypisze tylko raz.
Żeby to naprawić użyję po prostu komendy dwa razy. Połączę wyniki obu komend narzędziem
cat
(które służy do konkatenacji dwóch plików), i policzę liczbę lini poleceniem wc -l
cat <(grep -o "XMAS" lines) <(grep -o "SAMX" lines) | wc -l
Wynik jest poprawny!
Cały kod
# part1.awk
function print_arr(arr) {
for (i in arr) { printf arr[i] }
printf "\n"
}
function print_2d_arr(arr) { for (i in arr) print_arr(arr[i]) }
BEGIN {FS = ""}
{
for (i = 1; i <= NF; i++) {
cols[i][NR] = $i
diags[i - NR][NR] = $i
diags2[NF - i - NR + 1][NR] = $i
}
print $0
}
END {
print_2d_arr(cols);
print_2d_arr(diags);
print_2d_arr(diags2);
}
Uruchamiamy całość komendą:
awk -f part1.awk input.txt > lines; cat <(grep -o "XMAS" lines) <(grep -o "SAMX" lines) | wc -l
Część 2
Żeby zdobyć drugą gwiazdę musimy się jeszcze trochę namęczyć, okazuje się że nie mamy
szukać wystąpień słowa XMAS
a X-MAS
, a zapisane jest ono w taki sposób że dwa słowa
MAS
się ze sobą przecinają w taki sposób że tworzą dużego X
.
M.S
.A.
M.S
Rozwiązanie
Podejdziemy do rozwiązania podobnie, tym razem jednak zamiast wypisywać linie wypiszemy
wszystkie możliwe kwadraty 3x3
z pliku (każdy we własnej lini). I użyjemy grepa do wyfiltrowania
tylko tych kwadratów które spełniają jedno z czterech możliwych wzorców. każde MAS
ma
dwie możliwości kierunku co daje w sumie cztery.
Kod wypisujący kwadraty
Nie jestem szczególnie dumny z tego kodu i nie będę tłumaczył co robi każda linijka, powiem tylko że zarówno w tej jak i poprzedniej części bardzo przydatna okazała się asocjacyjna natura macierzy w AWK, nie ma żadnych problemów z przypisaniem liczby do negatywnego indeksu w macierzy, więc nie muszę się tu przejmować edge case’ami. W końcowej pętli po prostu ignoruję kwadraty które wychodzą poza nasz obszar (negatywne indeksy, albo zaczynające się dwa lub jeden od końca macierzy).
# part2.awk
BEGIN {FS = ""}
{
offset = NF * (NR - 1)
for (i = 1; i <= NF; i++) {
si = offset + i - 1
for (x = 0; x < 3; x++) {
for (y = 0; y < 3; y++) {
pos = si - (x + NF * y)
squares[pos][x + 3 * y] = $i
}
}
}
} END {
for (i = 0; i < NR - 2; i++) {
for (j = 0; j < NF - 2; j++) {
pos = i * NF + j
for (v in squares[pos]) {
printf squares[pos][v]
}
printf "\n"
}
}
}
Output dla wcześniejszego przykładu:
$ awk -f part2.awk input.txt
XMA..A.M.
MAS.AAM.M
..A.M.X..
.AAM.M..X
Jak widać zwracam nasze kwadraty w spłaszczonej formie, gdzie pierwsze trzy znaki odpowiadają za pierwszą linię, drugie trzy znaki za drugą linię i tak dalej.
Liczenie wysąpień X-MAS
Stworzyłem 4 wzorce które obejmują wszystkie możliwości wystąpienia X-MAS
, zamiast kropki
w regexie może wejść jaki kolwiek inny znak
Końcowy kod:
awk -f part2.awk input.txt | grep -e "M.S.A.M.S" -e "M.M.A.S.S" -e "S.M.A.S.M" -e "S.S.A.M.M" | wc -l
Podsumowanie
Cieszę się że kolejny raz udało się zadanie rozwiązać w AWKu, jednak powoli wchodzimy w
terytorium gdzie mam wrażenie że będziemy potrzebować bardziej wydajnych metod,
wypisywanie wszystkich kwadratów 3x3 brzmi bardzo redundantnie, no ale póki co wydajnościowo
i tak nieźle sobie radzimy. Porównałem później parę rozwiązań z reddita r/adventofcode
i nadal biję czasowo większość rozwiązań w pythonie.