Advent of Code - Day 12
I would climb the mountain high, so high, to be able to touch the sky.
Today’s puzzle in short: 新高山登れ
The input data
We get a file with a bunch of alphabetical characters:
- The input has 41 lines and 71 columns
- Every character represents the height of the surface:
a
is the lowestz
is the highestb
is 1 level higher thana
c
is 1 level higher thanb
- …
z
is 1 level higher thany
S
represents the start pointE
represents the end point
Game rules
One step at a time to a neighboring field only when:
- the neighbor field is maximum one level higher
- … or equal to the current one
- or lower (with any level) then the current one
Our task is get from S
to E
in the shortest possible way.
The data model
Since part one and two are almost the same (again), I start with the common part. First we need to define a class to represent a field on the height map and also store the neighbor fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Position
{
public ?Position $top = null;
public ?Position $right = null;
public ?Position $bottom = null;
public ?Position $left = null;
public bool $isVisited = false;
public ?int $distance = null;
public function __construct(public int $row, public int $col, public int $height, public bool $isEnd)
{}
/**
* @return array<int, Position>
*/
public function getNeighbors(): array
{
$neighbors = [];
if ($this->top !== null) {
$neighbors[] = $this->top;
}
if ($this->right !== null) {
$neighbors[] = $this->right;
}
if ($this->bottom !== null) {
$neighbors[] = $this->bottom;
}
if ($this->left !== null) {
$neighbors[] = $this->left;
}
return $neighbors;
}
}
So we can instantiate this class with some basic information:
- the vertical and horizontal position on the map,
- the height of the field,
- a flag whether it’s the target field or not.
For the neighboring fields we created four nullable properties. Obviously the fields on the sides at least one neighbor is NULL
.
We also add a function to collect all non-NULL neighbors.
Now it’s time to create the height map from the input data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/** @var array<int, array<int, Position>> $heightMap */
$heightMap = [];
/** @var array<int,Position> $startPositions */
$startPositions = [];
// Read file and fill the height map with positions
if ($fn = fopen(__DIR__ . '/input.txt', 'r')) {
$row = 0;
while (($line = fgets($fn, 1024)) !== false) {
$line = trim($line);
$letters = str_split($line);
$cols = count($letters);
$heightMap[$row] = [];
foreach ($letters as $col => $character) {
$height = match($character)
{
'S' => 1,
'E' => ord('z') - ord('a') + 1,
default => ord($character) - ord('a') + 1
};
$position = new Position(
row: $row,
col: $col,
height: $height,
isEnd: $character === 'E'
);
if ($character === 'S') {
$startPositions[] = $position;
}
$heightMap[$row][$col] = $position;
}
$row++;
}
fclose($fn);
}
Although for part one there’s only one start position, I use an array to store it, because in part two there are multiple start positions, so my goal is to achieve both tasks with the less work.
The trick here is the same I used before: convert the characters into numbers. This way, I will get mutually exclusive correspondence:
- for
a
we will get1
, - for
b
we will get2
, - …
- for
z
we will get26
.
There are the two special cases:
- for
S
we set the height to1
and add the current position to the$startPositions
, - for
E
we set the height to26
and use this information on thePosition
’s constructor.
The only reason is to store these Position
instances in a multidimensional array is to keep the relative positions between
the fields. Therefore, we have all to Position
s, so we can link the neighbors by reference.
But we do it smart: we link the neighbor field only when it fulfills the Game rules:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
foreach ($heightMap as $row => $cols) {
foreach ($cols as $col => $position) {
if (isset($heightMap[$row - 1][$col])) {
$top = $heightMap[$row - 1][$col];
if ($position->height + 1 >= $top->height) {
$position->top = $top;
}
}
if (isset($cols[$col + 1])) {
$right = $cols[$col + 1];
if ($position->height + 1 >= $right->height) {
$position->right = $right;
}
}
if (isset($heightMap[$row + 1][$col])) {
$bottom = $heightMap[$row + 1][$col];
if ($position->height + 1 >= $bottom->height) {
$position->bottom = $bottom;
}
}
if (isset($cols[$col - 1])) {
$left = $cols[$col - 1];
if ($position->height + 1 >= $left->height) {
$position->left = $left;
}
}
}
}
Now, we have the Position
s properly initializes, it’s time to find the path. For this I create a new class, called Path
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class Path
{
/** @var array<int array<int, int>> */
public array $distanceMap = [];
/** @var array<int, Position> */
public array $queue = [];
public ?Position $endPosition = null;
public function __construct(public Position $startPosition, public int $rows, public int $cols)
{
$tmp = array_fill(0, $this->cols, null);
$this->distanceMap = array_fill(0, $this->rows, $tmp);
$this->startPosition->distance = 0;
$this->queue[] = $startPosition;
}
public function findPath(): void
{
do {
$current = array_shift($this->queue);
if ($current->isEnd) {
$this->endPosition = $current;
break;
}
if ($current->isVisited) {
continue;
}
$current->isVisited = true;
if ($this->distanceMap[$current->row][$current->col] === null) {
$this->distanceMap[$current->row][$current->col] = $current->distance;
}
$neighbors = $current->getNeighbors();
foreach ($neighbors as $position) {
if ($position->distance === null) {
$position->distance = $current->distance + 1;
}
}
$this->queue = array_merge($this->queue, $neighbors);
} while (!empty($this->queue));
}
public function printDistanceMap(): void
{
for ($row = 0; $row < $this->rows; $row++) {
for ($col = 0; $col < $this->cols; $col++) {
echo str_pad($this->distanceMap[$row][$col], 5, ' ', STR_PAD_BOTH).' |';
}
echo PHP_EOL;
echo str_repeat('------+', $this->cols).PHP_EOL;
}
}
}
To start, we need to initialize this class with the $startPosition
and number of rows and columns of the height map.
Inside this class we will create a new “map”, a multidimensional array to track the distance, we already climbed.
We will also need a queue know how many Position
s more we need to climb. The path find process ends in two cases:
- we reach the End,
- we run out of
Position
s in the queue.
Upon instantiation, we add the $startPosition
directly into the queue. Then we call the findPath()
public method, that
will start the search:
- It takes the first element out from the queue.
- If the current
Position
is the End, we save thePosition
in to theendPosition
property and stop the process. - If the current
Position
is marked as already visited, we skip to the nextPosition
in the queue. We can do that, because when we visited it earlier, that means we already added all its neighbors to the queue. - Mark the current
Position
as visited. - If the current
Position
is not yet recorded in thedistanceMap
, we save thePosition
’s distance into it. - We get the current
Position
’s neighbors. And check one-by-one:- If the given neighbor has no distance set, we set the current
Position
’s distance, plus one.
- If the given neighbor has no distance set, we set the current
- Merge the neighbors into the queue. Most likely there will be a ton of redundancy in the queue, but the
isVisited
check will fasten up the process.
I also added a printDistanceMap
method to visualize, which Position
s were checked during the process.
Part one
Now we have everything to complete the task. Here we have one fix starting point, so our script will look like the following:
1
2
3
4
5
6
7
8
9
10
11
$rows = count($heightMap);
$cols = count($heightMap[0]);
$path = new Path(
startPosition: $startPositions[0],
rows: $rows,
cols: $cols
);
$path->findPath();
echo $path->endPosition->distance.PHP_EOL;
Part two
Here we have to take all the level 1 fields as start point, and find the shortest of them all. For this, we need to modify our part one code only a bit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ...
if ($fn = fopen(__DIR__ . '/input.txt', 'r')) {
// ...
while (($line = fgets($fn, 1024)) !== false) {
// ...
foreach ($letters as $col => $character) {
// ...
if ($height === 1) {
$startPositions[] = $position;
}
// ...
}
// ...
}
// ...
}
// ...
$minimalPath = PHP_INT_MAX;
foreach ($startPositions as $start) {
$path = new Path(
startPosition: $start,
rows: $rows,
cols: $cols
);
$path->findPath();
if ($path->endPosition) {
$minimalPath = min($minimalPath, $path->endPosition->distance);
}
for ($row = 0; $row < $rows; $row++) {
for ($col = 0; $col < $cols; $col++) {
$heightMap[$row][$col]->isVisited = false;
$heightMap[$row][$col]->distance = null;
}
}
}
echo $minimalPath.PHP_EOL;
One small thing to notice here: we should not forget to “reset” every Position
in every loop.