Advent of Code Day 4 in Golang: Finding XMAS and X-MAS
Introduction
Moving on to day 4, we have a grid problem in front of us, we are given some numbers in the form of a grid, i.e. some rows and columns with some upper case letters. What we need to do is to find is the word XMAS
in any direction (up, left, down, right, diagonals), and in the second part we need to find the word MAS
forming an X.
So, let’s see how we can approach this and solve it in golang.
You can check out my solutions here on GitHub.
Constructing the grid
The most fundamental part of the problem lies in actually converting the text into a grid or a matrix form. We can split the lines, into individual lines and append each character as an element in a list, and that way we can have a list of list of strings which is a matrix or grid-like (2-dimensional) structure.
So, below is the input for the puzzle.
MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX
We need to convert it into something like this
[
[M M M S X X M A S M]
[M S A M X M S M S A]
[A M X S X M A A M M]
[M S A M A S M S M X]
[X M A S A M X A M M]
[X X A M M X X A M A]
[S M S M S A S X S S]
[S A X A M A S A A A]
[M A M M M X M M M M]
[M X M X A X M A S X]
]
So, this is a list of strings, we can say in golang it is a [][]string
. We can do that by creating a function like this:
func ConstructGrid(lines []string) [][]string {
grid := [][]string{}
for _, line := range lines {
row := []string{}
for _, char := range strings.Split(line, "") {
row = append(row, char)
}
grid = append(grid, row)
}
return grid
}
The above function takes in a list of strings and returns a list of list of strings that are individual letters in the grid.
We can read the file bytes and split the bytes on newline characters and then this will be used as the input for this function.
So, once the input is parsed into a grid, we can start thinking about the actual logic of finding the word XMAS
in it.
Part 1
So, in the first part, we need to find the word XMAS
in the matrix which could be appearing:
forwards (as
XMAS
)backward (as
SAMX
)upwards
S A M X
downwards
X M A S
Diagonal upwards (right or up left)
S A M X OR S A M X
Diagonals downwards (right or left)
X M A S OR X M A S
So, there are 8 directions where XMAS
could appear in the grid, there could n number of these XMAS
. We need to find the count of these in the grid.
To approach this, we can either find the first character in the word XMAS
and then search in all directions one by one and check if we find M
and if we have found M
in any of the direction, we keep moving ahead in that direction and check if there is A
and S
in that direction.
The approach looks like this:
Initialize the counter to 0
Iterate over each line
Iterate over each character in the line
Check if the character is equal to
X
If the character is
X
→Iterate over all the directions (up, down, right, left, up-left, up-right, down-left, down-right)
For that direction if we find the character to be
M
Keep moving ahead in the same direction to find
A
andS
similarly, if we found all the charactersXMAS
then, increment the counterElse choose another direction in the loop
This looks complex and large but is simple, focus one thing at a time and you can solve this easily.
So, for the implementation of this, we need to define a few things first:
var directions [][]int = [][]int{
[]int{0, -1}, // up
[]int{0, 1}, // down
[]int{1, 0}, // right
[]int{-1, 0}, // left
[]int{1, -1}, // up right
[]int{-1, -1}, // up left
[]int{1, 1}, // down right
[]int{-1, 1}, // down left
}
var wordList []string = []string{"X", "M", "A", "S"}
So, we have defined the list of integers in the directions which are the x and y coordinates that we need to add or subtract to get to the desired location. It is basically like a unit vector, it has a distance of 1 and a direction indicated by +
or -
indicating to move the left or right for x coordinates and up and down for y c-ordinates.
So, let me explain that more clearly, let’s say I am at (1,2)
in the grid which is of 4x4 dimension.
A B C D
E F G H
I J K L
M N O P
So, at 2,1 we have G
, so we check some directions for this
up → 0,-1
→ 2+0, 1-1 → 2,0, we have moved to C
right → 1,0
→ 2+1, 1+0 → 3,1 , we have moved to H
down, left → -1,1
→ 2-1, 1+1 → 1, 2, we have moved to J
So, you get the idea, that we are moving in some directions using these coordinates.
We can use these to get the next direction jump we want to make to find if that element has the next character in the word that we are searching.
We will write a function that does this first and abstract the function that checks if we have found the word in the grid.
func TraverseGrid(grid [][]string) int {
score := 0
for x, row := range grid {
for y, char := range row {
if char == wordList[0] {
for _, direction := range directions {
if FindXMAS(x, y, 1, direction, grid) {
score += 1
}
}
}
}
}
return score
}
The above function takes in a grid and returns an integer which will be the score i.e. the count of words XMAS
found in the grid/matrix.
First, we need to iterate through each of the rows in the grid, for each row, we iterate over the character, so we will have x and y coordinates as the index of the grid. We need to then check if the current character is X
or wordList[0]
, if that is the case, we iterate over all the directions and check if we can find XMAS
i.e. MAS
in that direction, if so we increment the counter. What is the FindXMAS
function, let’s abstract that away, and pass in the x
, y
, which are the coordinates of the current word, 1
which will be the word position of the XMAS
and in this case, we already have found X
we need to find MAS
in that direction. We pass the grid and the direction, so this function will return true or false if that direction has MAS
in it.
So to iterate:
We iterate over the grid and get
row
andx
as the list of strings and index of the current row.For each row i.e. list of strings, we iterate over the list of strings to get
char
andy
as the character (string) and the index of that character in the list of the string.If we find the current character to be equal to
X
which is the 0th index of thewordList
thenWe iterate over all the directions and call the function
FindXMAS
to check if the remaining wordMAS
in that directionIf we find all the words, we increment the counter.
So, we return the counter as we count the number of words
XMAS
in the grid/matrix.
Now, we can implement the FindXMAS
function, that takes in a x
, y
coordinates, the wordPosition
, the direction and the grid, and return if the word is found.
First, we take the current x coordinates and add the direction’s x component (0th index or first element)
add current y coordinates to the direction’s y component (1st index or second element)
if the word position i.e.. the word index or the word itself in the current function is equal to the wordList, it means that it has found the required word completely
We need to check by adding the direction to the x and y coordinates, we are not overshooting the width and height of the grid, so if we do, we return a false
The final if is for checking if the current character is equal to the word that we are looking for, it could be
M
,A
, orS
. If so, we return the recursively call theFindXMAS
function by passing the updated x and y coordinates and the next word in the wordList, we keep the direction the same and pass the entire grid.
func FindXMAS(x, y, wordPosition int, direction []int, grid [][]string) bool {
xNext := x + direction[0]
yNext := y + direction[1]
if wordPosition > len(wordList)-1 {
return true
}
if xNext < 0 || xNext >= len(grid) || yNext < 0 || yNext >= len(grid[x]) {
return false
}
if grid[xNext][yNext] == wordList[wordPosition] {
return FindXMAS(xNext, yNext, wordPosition+1, direction, grid)
}
return false
}
So, we have implemented the FindXMAS
function, this will just return if we have found the MAS
word by going in a particular direction by updating the coordinates and checking if the word at that position in the gird is the next word in MAS
list.
So, this is what the entire first part looks like:
func main() {
lines := ReadFileLines("sample.txt")
grid := ConstructGrid(lines)
score := TraverseGrid(grid)
fmt.Println(score)
}
We take in the lines as a list of strings and pass that to ConstructGrid
and get the grid, finally, we call TraverseGrid
, by passing the grid and getting the score as the count of the words XMAS
in the grid.
That’s it from the part 1.
Part 2
For part two, we need to find MAS
in the cross shape, like below:
M.S
.A.
M.S
So, to solve this, we can do a similar approach but much simpler, we just need to find A
as there will always be the word MAS
in the center, so we just check if we have A
and the top-left, top-right, or bottom-right, bottom-left has M
or S
.
We get the coordinates of the top-right, top-left positions, down-right, and down-left positions by adding and subtracting 1 from it. We make a basic check if we are not overshooting the boundary of the grid. If we overshoot the boundaries, we won’t find the MAS
But if we are within the grid, we now get the character at those 4 positions, we check if the top-left and bottom-right have M
and S
or S
or M
, similarly for top-right and bottom-left has M
and S
or S
or M
respectively. This is the diagonal search for M
and S
above and below the character A
.
So, if we have both the diagonal matched we return true.
func FindMAS(x, y int, grid [][]string, wordList []string) bool {
xL, yT := x-1, y+1 // Top-left neighbor
xR, yD := x+1, y-1 // Bottom-right neighbor
// Check if the indices are within bounds
if xL < 0 || xR >= len(grid) || yT < 0 || yD < 0 ||
yT >= len(grid[xL]) || yD >= len(grid[xR]) {
return false
}
topLeft := grid[xL][yT]
bottomRight := grid[xR][yD]
topRight := grid[xR][yT]
bottomLeft := grid[xL][yD]
word1, word3 := wordList[1], wordList[3]
isDiagonalMatch := (topLeft == word1 && bottomRight == word3) || (topLeft == word3 && bottomRight == word1)
isAntiDiagonalMatch := (topRight == word1 && bottomLeft == word3) || (topRight == word3 && bottomLeft == word1)
return isDiagonalMatch && isAntiDiagonalMatch
}
So, that is the simple implementation for finding the MAS
the diagonal.
Now, we need to change the TraverseGrid
a bit, as we just iterate over the grid, and check if we have A
in the character in the row, i.e. wordList[2]
. Now, if we have A
, we need to call the FindMAS
function with the current coordinates and the grid, if that function returns true, we increment the counter,.
func TraverseGrid2(grid [][]string) int {
score := 0
for x, row := range grid {
for y, char := range row {
if char == wordList[2] {
if FindMAS(x, y, grid) {
score += 1
}
}
}
}
return score
}
So, that is the final implementation of part 2, we get the count of MAS
in the cross direction.
You can check out my solutions here on GitHub.
Conclusion
So, that is it from day 4 of Advent of Code in Golang, let me know if you have any suggestions, and how you approached it. any better solutions?
Happy Coding :)