[Day 11] Cosmic Expansion // Advent of Code 2023 (Python)

Jatin K Malik
18 min readDec 18, 2023

--

Cosmic Expansion (via DALL-E 3)

Part One

You continue following signs for “Hot Springs” and eventually come across an observatory. The Elf within turns out to be a researcher studying cosmic expansion using the giant telescope here.

He doesn’t know anything about the missing machine parts; he’s only visiting for this research project. However, he confirms that the hot springs are the next-closest area likely to have people; he’ll even take you straight there once he’s done with today’s observation analysis.

Maybe you can help him with the analysis to speed things up?

The researcher has collected a bunch of data and compiled the data into a single giant image (your puzzle input). The image includes empty space (.) and galaxies (#). For example:

...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#.....

The researcher is trying to figure out the sum of the lengths of the shortest path between every pair of galaxies. However, there’s a catch: the universe expanded in the time it took the light from those galaxies to reach the observatory.

Due to something involving gravitational effects, only some space expands. In fact, the result is that any rows or columns that contain no galaxiesshould all actually be twice as big.

In the above example, three columns and two rows contain no galaxies:

v v v
...#......
.......#..
#.........
>..........<
......#...
.#........
.........#
>..........<
.......#..
#...#.....
^ ^ ^

These rows and columns need to be twice as big; the result of cosmic expansion therefore looks like this:

....#........
.........#...
#............
.............
.............
........#....
.#...........
............#
.............
.............
.........#...
#....#.......

Equipped with this expanded universe, the shortest path between every pair of galaxies can be found. It can help to assign every galaxy a unique number:

....1........
.........2...
3............
.............
.............
........4....
.5...........
............6
.............
.............
.........7...
8....9.......

In these 9 galaxies, there are 36 pairs. Only count each pair once; order within the pair doesn’t matter. For each pair, find any shortest path between the two galaxies using only steps that move up, down, left, or right exactly one . or # at a time. (The shortest path between two galaxies is allowed to pass through another galaxy.)

For example, here is one of the shortest paths between galaxies 5 and 9:

....1........
.........2...
3............
.............
.............
........4....
.5...........
.##.........6
..##.........
...##........
....##...7...
8....9.......

This path has length 9 because it takes a minimum of nine steps to get from galaxy 5 to galaxy 9 (the eight locations marked # plus the step onto galaxy 9 itself). Here are some other example shortest path lengths:

Between galaxy 1 and galaxy 7: 15

Between galaxy 3 and galaxy 6: 17

Between galaxy 8 and galaxy 9: 5

In this example, after expanding the universe, the sum of the shortest path between all 36 pairs of galaxies is 374.

Expand the universe, then find the length of the shortest path between every pair of galaxies. What is the sum of these lengths?

Finally! A space exploration problem. I am a sucker for those!

So, let’s look at the problem, we have an image of space from the observatory and it looks something like this:

...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#.....

And we need to compute the sum of the lengths of the shortest path between every pair of galaxies!

❗️The catch being, that by the time light reaches to us, the space expands due to ever expanding universe, but only some part, resulting in any rows or columns that contain no galaxies should all actually be twice as big.

Let’s visualise this?

This seems like a good first step

Once we have the final image! We can just number the galaxies like:

And then we can just find the shortest distance from each pair of galaxies!

Seems pretty straight forward right? Let’s code!

As usual, I will begin with writing our base case as the ultimate validator!

And now let’s start expanding! It’s clear we first need to expand space by finding empty rows and columns and doubling it. So, let’s first just do that.

Here’s a simple function expand_space that just does that:

💡 Caught a bug, as if we modify the space_map while iterating through it, the empty_rows elements start to point to an incorrect index. To fix that, we can just introduce something like a mod_count which can help us keep a track of original index in modified space_map.

Fixed!

Now we can find all the galaxies locations in the expanded_space_map with a simple function:

💭 The last step is to now take 1 galaxy and find its shortest distance from all other galaxies and then repeat this process for all galaxies!

Well, before even touching the code, I can already see a lot of steps to optimise this one! For example: If we have found the shortest distance from Galaxy_1 to Galaxy_2 , it will be same other way around, so we can save extra compute by just refering to a cache.

....1........
.........2...
3............
.............
.............
........4....
.5...........
............6
.............
.............
.........7...
8....9.......

So, in case of this example, where we have 9 galaxies can could have 81 combinations, since we can’t repeat a pair, the formula becomes:

Number of combinations= n! / r!(nr)!​

Solving for our case:

9C2​ = 9! / 2!(9−2)! ​= 36

36 combinations!

Let’s write some code? I will be using good’ol BFS (breadth first search) to find the shortest path for each pair of galaxies. We may come back and optimise this to a more greedy approach for faster computation!

Classic BFS!
Ofcourse, I forgot `list` is unhashable 🤦‍♂️

We can always hash a tupple though! ;)

Finally after a lot of iterations:

36!

Let’s check our test case now?

Here’s our MVP code till now:

# Day 11: Cosmic Expansion

from enum import Enum


EMPTY_SPACE = "."
GALAXY = "#"
DISTANCE_MAP = {}

class Direction(Enum):
RIGHT = (0, 1)
LEFT = (0, -1)
DOWN = (1, 0)
UP = (-1, 0)


def expand_space_map(space_map):
empty_rows, empty_cols = [], []

for i, row in enumerate(space_map):
if GALAXY not in row:
empty_rows.append(i)

for i, col in enumerate(space_map[0]):
if GALAXY not in [row[i] for row in space_map]:
empty_cols.append(i)

mod_count = 0
for i in empty_rows:
space_map.insert(i + mod_count, EMPTY_SPACE * len(space_map[i]))
mod_count += 1

mod_count = 0
for i in empty_cols:
for j, row in enumerate(space_map):
space_map[j] = row[: i + mod_count] + EMPTY_SPACE + row[i + mod_count :]
mod_count += 1

return space_map


def find_shortest_distance(galaxy1, galaxy2, space_map):
print(f"Finding shortest distance between {galaxy1} and {galaxy2}")
# check if the distance between the two galaxies is already calculated
distance = DISTANCE_MAP.get(galaxy1, {}).get(galaxy2) or DISTANCE_MAP.get(
galaxy2, {}
).get(galaxy1)
if distance:
return distance

# using BFS to find the shortest path between two galaxies
queue = [galaxy1] # initialize the queue with the starting galaxy
visited = set() # set to keep track of visited galaxies
distance = 0 # initialize the distance to 0
while queue:
for _ in range(len(queue)):
current = queue.pop(0) # get the next galaxy from the queue
if current == galaxy2: # if we reach the destination galaxy
if galaxy1 not in DISTANCE_MAP:
DISTANCE_MAP[galaxy1] = {}
DISTANCE_MAP[galaxy1][galaxy2] = distance # store the distance between the galaxies
return distance
visited.add(current) # mark the current galaxy as visited

for direction in Direction: # explore all possible directions
i, j = current[0] + direction.value[0], current[1] + direction.value[1]
if (
0 <= i < len(space_map)
and 0 <= j < len(space_map[0])
and (i, j) not in visited
):
queue.append((i, j)) # add the neighboring galaxy to the queue
distance += 1 # increment the distance after exploring all galaxies at the current level

raise Exception("No path found between the two galaxies")


def find_all_galaxies(space_map):
galaxies = []
for i, row in enumerate(space_map):
for j, col in enumerate(row):
if col == GALAXY:
galaxies.append((i, j))
return galaxies


def analyze_space(space_map):
expanded_space_map = expand_space_map(space_map)
print(f"Expanded space map is {len(expanded_space_map)} x {len(expanded_space_map[0])}")
galaxies_locations = find_all_galaxies(expanded_space_map)
print(f"Number of galaxies: {len(galaxies_locations)}")

for galaxy1 in galaxies_locations:
for galaxy2 in galaxies_locations:
if galaxy1 != galaxy2:
find_shortest_distance(galaxy1, galaxy2, expanded_space_map)

sum = 0
for k,v in DISTANCE_MAP.items():
for k1,v1 in v.items():
sum += v1

return sum

def part_one():
with open("day_11/input.txt") as f:
space_map = f.read().splitlines()
sum = analyze_space(space_map)
print(f"❗️ Sum of the lengths of the shortest path between every pair of galaxies: {sum}")

def test_analyze_space():
space_map = """...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#....."""

sum = analyze_space(space_map.splitlines())
assert sum == 374, f"Expected 374, got {sum}"
print("✅ Passed test_analyze_observation()")


if __name__ == "__main__":
test_analyze_space()
# part_one()
[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_11/cosmic_expansion.py"
✅ Passed test_analyze_observation()

[Done] exited with code=0 in 7.131 seconds

Well, it did took 7.131 seconds to compute for a simple testcase, let’s see if it can do it for puzzle input?

And compute!!

2 mins in, doesn’t seem likely!

Let’s add some logging to understand the scale of the problem?

Yeah….this is not gonna compute!

We have 426 Galaxies in a map of size 151 x 147 which gives us 426C2 combinations, equaling 90525 possible pairs! We need to make it more efficient!

While, I can take the easy path of just doings the Bi-directional BFS where I can look up from both Galaxy 1 and Galaxy 2, I feels it’s better to take a peak into A* or Dijkstra’s Algorithm for our shortest path finding to be more efficient!

I am bit rusty don’t remember A* algo exactly but I do remember that A* is a type of greedy approch which does a smart search through a maze, where at each step it makes an informed guess about the best next step based on how far it has already traveled and how far it still has to go, according to the map. It’s a blend of exploring and using what you know to make good guesses, which makes it efficient and effective for finding paths.

Let’s use Github Copilot to quickly generate a A* method for searching and then refactor it to suit our galaxy and space_map problem:

Looks good!

Now, let’s run for the base test case with our logging enabled?

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_11/cosmic_expansion.py"
Expanded space map is 12 x 13
Number of galaxies: 9
Finding shortest distance between (0, 4) and (1, 9)
Finding shortest distance between (0, 4) and (2, 0)
Finding shortest distance between (0, 4) and (5, 8)
Finding shortest distance between (0, 4) and (6, 1)
Finding shortest distance between (0, 4) and (7, 12)
Finding shortest distance between (0, 4) and (10, 9)
Finding shortest distance between (0, 4) and (11, 0)
Finding shortest distance between (0, 4) and (11, 5)
Finding shortest distance between (1, 9) and (0, 4)
Finding shortest distance between (1, 9) and (2, 0)
Finding shortest distance between (1, 9) and (5, 8)
Finding shortest distance between (1, 9) and (6, 1)
Finding shortest distance between (1, 9) and (7, 12)
Finding shortest distance between (1, 9) and (10, 9)
Finding shortest distance between (1, 9) and (11, 0)
Finding shortest distance between (1, 9) and (11, 5)
Finding shortest distance between (2, 0) and (0, 4)
Finding shortest distance between (2, 0) and (1, 9)
Finding shortest distance between (2, 0) and (5, 8)
Finding shortest distance between (2, 0) and (6, 1)
Finding shortest distance between (2, 0) and (7, 12)
Finding shortest distance between (2, 0) and (10, 9)
Finding shortest distance between (2, 0) and (11, 0)
Finding shortest distance between (2, 0) and (11, 5)
Finding shortest distance between (5, 8) and (0, 4)
Finding shortest distance between (5, 8) and (1, 9)
Finding shortest distance between (5, 8) and (2, 0)
Finding shortest distance between (5, 8) and (6, 1)
Finding shortest distance between (5, 8) and (7, 12)
Finding shortest distance between (5, 8) and (10, 9)
Finding shortest distance between (5, 8) and (11, 0)
Finding shortest distance between (5, 8) and (11, 5)
Finding shortest distance between (6, 1) and (0, 4)
Finding shortest distance between (6, 1) and (1, 9)
Finding shortest distance between (6, 1) and (2, 0)
Finding shortest distance between (6, 1) and (5, 8)
Finding shortest distance between (6, 1) and (7, 12)
Finding shortest distance between (6, 1) and (10, 9)
Finding shortest distance between (6, 1) and (11, 0)
Finding shortest distance between (6, 1) and (11, 5)
Finding shortest distance between (7, 12) and (0, 4)
Finding shortest distance between (7, 12) and (1, 9)
Finding shortest distance between (7, 12) and (2, 0)
Finding shortest distance between (7, 12) and (5, 8)
Finding shortest distance between (7, 12) and (6, 1)
Finding shortest distance between (7, 12) and (10, 9)
Finding shortest distance between (7, 12) and (11, 0)
Finding shortest distance between (7, 12) and (11, 5)
Finding shortest distance between (10, 9) and (0, 4)
Finding shortest distance between (10, 9) and (1, 9)
Finding shortest distance between (10, 9) and (2, 0)
Finding shortest distance between (10, 9) and (5, 8)
Finding shortest distance between (10, 9) and (6, 1)
Finding shortest distance between (10, 9) and (7, 12)
Finding shortest distance between (10, 9) and (11, 0)
Finding shortest distance between (10, 9) and (11, 5)
Finding shortest distance between (11, 0) and (0, 4)
Finding shortest distance between (11, 0) and (1, 9)
Finding shortest distance between (11, 0) and (2, 0)
Finding shortest distance between (11, 0) and (5, 8)
Finding shortest distance between (11, 0) and (6, 1)
Finding shortest distance between (11, 0) and (7, 12)
Finding shortest distance between (11, 0) and (10, 9)
Finding shortest distance between (11, 0) and (11, 5)
Finding shortest distance between (11, 5) and (0, 4)
Finding shortest distance between (11, 5) and (1, 9)
Finding shortest distance between (11, 5) and (2, 0)
Finding shortest distance between (11, 5) and (5, 8)
Finding shortest distance between (11, 5) and (6, 1)
Finding shortest distance between (11, 5) and (7, 12)
Finding shortest distance between (11, 5) and (10, 9)
Finding shortest distance between (11, 5) and (11, 0)
✅ Passed test_analyze_observation()

[Done] exited with code=0 in 0.121 seconds

Woah! 0.121 seconds for the base test case. Comparing this to our previous BFS approach that took 7.131 seconds to churn out the answers, A* approach is almost ~59x faster! 🚀

Let’s run for our puzzle input and go make a coffee! ☕️

.

.

.

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_11/cosmic_expansion.py"

❗️ Sum of the lengths of the shortest path between every pair of galaxies: XXXXX

[Done] exited with code=0 in 982.651 seconds

Well, this took only 982.651 seconds ~ 16.377 minutes to run! And this happened because I just ran it and went to make coffee and when I came back, I was it was somewhere around 148th row. Since, I knew we have a total 151 rows only, I just let it run!

Finding shortest distance between (148, 104) and (119, 66)
Finding shortest distance between (148, 104) and (119, 105)
Finding shortest distance between (148, 104) and (119, 116)

So, let’s input our answer into the box and…

Yay! 🙌

Here’s our unpythonic code till now, I will optimise it in Part 2:

# Day 11: Cosmic Expansion

from enum import Enum
import heapq


EMPTY_SPACE = "."
GALAXY = "#"
DISTANCE_MAP = {}


class Direction(Enum):
RIGHT = (0, 1)
LEFT = (0, -1)
DOWN = (1, 0)
UP = (-1, 0)


def calculate_distance(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1]) # Manhattan distance


def get_adjacent_galaxies(space_map, galaxy):
adjacent_galaxies = []
for direction in Direction:
i, j = galaxy[0] + direction.value[0], galaxy[1] + direction.value[1]
if 0 <= i < len(space_map) and 0 <= j < len(space_map[0]):
adjacent_galaxies.append((i, j))
return adjacent_galaxies


def find_shortest_path(space_map, start_galaxy, end_galaxy):
galaxies_to_visit = []
heapq.heappush(galaxies_to_visit, (0, start_galaxy))
previous_galaxy = {}
cost_to_reach = {start_galaxy: 0}
estimated_total_cost = {start_galaxy: calculate_distance(start_galaxy, end_galaxy)}

while galaxies_to_visit:
current_galaxy = heapq.heappop(galaxies_to_visit)[1]

if current_galaxy == end_galaxy:
return cost_to_reach[
current_galaxy
] # Return the cost to reach the end galaxy

for adjacent_galaxy in get_adjacent_galaxies(space_map, current_galaxy):
tentative_cost = cost_to_reach[current_galaxy] + 1 # assuming uniform cost
if (
adjacent_galaxy not in cost_to_reach
or tentative_cost < cost_to_reach[adjacent_galaxy]
):
previous_galaxy[adjacent_galaxy] = current_galaxy
cost_to_reach[adjacent_galaxy] = tentative_cost
estimated_total_cost[
adjacent_galaxy
] = tentative_cost + calculate_distance(adjacent_galaxy, end_galaxy)
heapq.heappush(
galaxies_to_visit,
(estimated_total_cost[adjacent_galaxy], adjacent_galaxy),
)

raise Exception("No path found between the two galaxies")


def expand_space_map(space_map):
empty_rows, empty_cols = [], []

for i, row in enumerate(space_map):
if GALAXY not in row:
empty_rows.append(i)

for i, col in enumerate(space_map[0]):
if GALAXY not in [row[i] for row in space_map]:
empty_cols.append(i)

mod_count = 0
for i in empty_rows:
space_map.insert(i + mod_count, EMPTY_SPACE * len(space_map[i]))
mod_count += 1

mod_count = 0
for i in empty_cols:
for j, row in enumerate(space_map):
space_map[j] = row[: i + mod_count] + EMPTY_SPACE + row[i + mod_count :]
mod_count += 1

return space_map


def find_shortest_distance(galaxy1, galaxy2, space_map):
print(f"Finding shortest distance between {galaxy1} and {galaxy2}")

distance = DISTANCE_MAP.get(galaxy1, {}).get(galaxy2) or DISTANCE_MAP.get(
galaxy2, {}
).get(galaxy1)
if distance:
return distance

distance = find_shortest_path(space_map, galaxy1, galaxy2) # Use A* for pathfinding

if galaxy1 not in DISTANCE_MAP:
DISTANCE_MAP[galaxy1] = {}
DISTANCE_MAP[galaxy1][galaxy2] = distance
return distance


def find_all_galaxies(space_map):
galaxies = []
for i, row in enumerate(space_map):
for j, col in enumerate(row):
if col == GALAXY:
galaxies.append((i, j))
return galaxies


def analyze_space(space_map):
expanded_space_map = expand_space_map(space_map)
print(
f"Expanded space map is {len(expanded_space_map)} x {len(expanded_space_map[0])}"
)
galaxies_locations = find_all_galaxies(expanded_space_map)
print(f"Number of galaxies: {len(galaxies_locations)}")

for galaxy1 in galaxies_locations:
for galaxy2 in galaxies_locations:
if galaxy1 != galaxy2:
find_shortest_distance(galaxy1, galaxy2, expanded_space_map)

sum = 0
for k, v in DISTANCE_MAP.items():
for k1, v1 in v.items():
sum += v1

return sum


def part_one():
with open("day_11/input.txt") as f:
space_map = f.read().splitlines()
sum = analyze_space(space_map)
print(
f"❗️ Sum of the lengths of the shortest path between every pair of galaxies: {sum}"
)


def test_analyze_space():
space_map = """...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#....."""

sum = analyze_space(space_map.splitlines())
assert sum == 374, f"Expected 374, got {sum}"
print("✅ Passed test_analyze_observation()")


if __name__ == "__main__":
# test_analyze_space()
part_one()

Part Two

The galaxies are much older (and thus much farther apart) than the researcher initially estimated.

Now, instead of the expansion you did before, make each empty row or column one million times larger. That is, each empty row should be replaced with 1000000 empty rows, and each empty column should be replaced with 1000000empty columns.

(In the example above, if each empty row or column were merely 10 times larger, the sum of the shortest paths between every pair of galaxies would be 1030. If each empty row or column were merely 100 times larger, the sum of the shortest paths between every pair of galaxies would be 8410. However, your universe will need to expand far beyond these values.)

Starting with the same initial image, expand the universe according to these new rules, then find the length of the shortest path between every pair of galaxies. What is the sum of these lengths?

Well, of course, the galaxies are much older!

So, looks like we need to update how we expand our space map buy a factor of one million times, that is, 1000000x, for each empty row and column!

Yep! Our current code will not scale at all. Probably our solar system sun will collapse into itself before our single threaded Python will be able to compute anything!

So…let’s rethink this!

.

.

Well shit! Why do I even need any path finding algo here? Isn’t this just simple matrix manipulation? I think I ended up doing over-engineering here! This is classic trap of being Principal Engineer, making problems look more complex than they actually are!

Revisting Part 1

This problem required solving just Manhattan distance between 2 nodes. Here’s an example to get distance between (Galaxy 1, Galaxy 4) and (Galaxy 5, Galaxy 8):

The formula to get this is absurdly simple as well:

Manhattan Distance = |x1 — y1| + |x2 — y2|

Which makes my code as simple as:

Solving for part 1, testing with base test case and puzzle input:

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_11/cosmic_expansion.py"
Expanded space map is 12 x 13
Number of galaxies: 9
Number of possible pairs of galaxies: 36
✅ Passed test_analyze_observation()
Expanded space map is 151 x 147
Number of galaxies: 426
Number of possible pairs of galaxies: 90525
❗️ Sum of the lengths of the shortest path between every pair of galaxies: xxxxxx

[Done] exited with code=0 in 0.199 seconds

Yep, correct answer! 🥲

Back to Part 2

Now, all that is changing is, the expansion distance between pairs of galaxies.

💭 If we can somehow just find the number of rows + cols to be modified compared to initial galaxy, we can just do the computation on original space_map and then just increase the distance by a factor of 1000000

💡 We can further simplify our code by not having to do the expansion at all as per part 1

We can introduce a new param — expansion_factor which we can pass as 2 for the part one, and 1000000 for part two to keep our code modular.

Let’s update our base test case:

And let’s run this:


[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_11/cosmic_expansion.py"
✅ Passed test_analyze_observation() for expansion_factor=2
✅ Passed test_analyze_observation() for expansion_factor=10
✅ Passed test_analyze_observation() for expansion_factor=100

Phew! We can now just load up our puzzle input and call for our 10⁶ expansion_factor :

Let’s run this up!


[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_11/cosmic_expansion.py"
✅ Passed test_analyze_observation() for expansion_factor=2
✅ Passed test_analyze_observation() for expansion_factor=10
✅ Passed test_analyze_observation() for expansion_factor=100
❗️ Sum of the lengths of the shortest path between every pair of galaxies for expansion_factor=2: xxxx
‼️ Sum of the lengths of the shortest path between every pair of galaxies for expansion_factor=1000000: xxxxxxxxxx

[Done] exited with code=0 in 0.843 seconds

We get the answer in 0.843 seconds….for BOTH the parts + 3 test cases! 😹

This is why I absolutely love coding!

Let’s put this answer in the input box and….profit!

🌌🌌🌌

Here’s our final pythonic yet highly performant code:

# Day 11: Cosmic Expansion

class CosmicGrid:
def __init__(self, space_grid):
# Initialize the cosmic grid with the provided space grid
self.space_grid = space_grid
self.grid_height = len(space_grid)
self.grid_width = len(space_grid[0])
# Find rows and columns that are entirely empty
self.empty_rows = self._find_empty_rows()
self.empty_columns = self._find_empty_columns()
# Locate all galaxies within the grid
self.galaxy_positions = self._find_galaxy_positions()

def _find_empty_rows(self):
# Identify rows that contain only empty space
empty_rows = set()
for row in range(self.grid_height):
if all(cell == '.' for cell in self.space_grid[row]):
empty_rows.add(row)
return empty_rows

def _find_empty_columns(self):
# Identify columns that contain only empty space
empty_columns = set()
for col in range(self.grid_width):
if all(self.space_grid[row][col] == '.' for row in range(self.grid_height)):
empty_columns.add(col)
return empty_columns

def _find_galaxy_positions(self):
# Record the positions of all galaxies in the grid
galaxy_positions = []
for row in range(self.grid_height):
for col in range(self.grid_width):
if self.space_grid[row][col] == '#':
galaxy_positions.append((row, col))
return galaxy_positions

def find_shortest_distance(self, galaxy1, galaxy2, expansion_factor):
# Calculate the shortest distance between two galaxies, factoring in cosmic expansion
row_distance = 0
for r in range(min(galaxy1[0], galaxy2[0]), max(galaxy1[0], galaxy2[0]) + 1):
if r in self.empty_rows:
row_distance += 1

col_distance = 0
for c in range(min(galaxy1[1], galaxy2[1]), max(galaxy1[1], galaxy2[1]) + 1):
if c in self.empty_columns:
col_distance += 1

# Expanded distance is calculated by multiplying empty distances by the expansion factor
# Direct distance is the straight line distance, not through empty space
expanded_row_distance = row_distance * expansion_factor
expanded_col_distance = col_distance * expansion_factor
direct_distance = abs(galaxy1[0] - galaxy2[0]) + abs(galaxy1[1] - galaxy2[1])

return expanded_row_distance + expanded_col_distance + direct_distance - row_distance - col_distance

def sum_of_shortest_paths(self, expansion_factor):
# Sum the shortest paths between all pairs of galaxies
total_distance = 0
for i, galaxy1 in enumerate(self.galaxy_positions):
for galaxy2 in self.galaxy_positions[i + 1:]:
total_distance += self.find_shortest_distance(galaxy1, galaxy2, expansion_factor)
return total_distance



def part_one():
with open("day_11/input.txt") as f:
space_map = f.read().splitlines()

cosmic_grid = CosmicGrid(space_map)
sum = cosmic_grid.sum_of_shortest_paths(expansion_factor=2)
print(
f"❗️ Sum of the lengths of the shortest path between every pair of galaxies for expansion_factor=2: {sum}"
)


def part_two():
with open("day_11/input.txt") as f:
space_map = f.read().splitlines()

cosmic_grid = CosmicGrid(space_map)
sum = cosmic_grid.sum_of_shortest_paths(expansion_factor=1000000)
print(
f"‼️ Sum of the lengths of the shortest path between every pair of galaxies for expansion_factor=1000000: {sum}"
)


def test_analyze_space():
space_map = """...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#....."""
cosmic_grid = CosmicGrid(space_map.splitlines())
# part one
sum = cosmic_grid.sum_of_shortest_paths(expansion_factor=2)
assert sum == 374, f"Expected 374, got {sum}"
print("✅ Passed test_analyze_observation() for expansion_factor=2")

# part two
sum = cosmic_grid.sum_of_shortest_paths(expansion_factor=10)
assert sum == 1030, f"Expected 1030, got {sum}"
print("✅ Passed test_analyze_observation() for expansion_factor=10")

sum = cosmic_grid.sum_of_shortest_paths(expansion_factor=100)
assert sum == 8410, f"Expected 8410, got {sum}"
print("✅ Passed test_analyze_observation() for expansion_factor=100")


if __name__ == "__main__":
test_analyze_space()
part_one()
part_two()

☘️ Moral of the story? As a Principal Engineer, I’m wired to navigate complex problems, often defaulting to intricate solutions like BFS, DFS, Dijkstra, and A*.

Here, I overcomplicated a straightforward task that simply needed the Manhattan distance formula. This experience was a humbling reminder that sometimes, simplicity is key. It’s easy to overlook the obvious when your mind is tuned to complexity. It’s crucial to step back and reassess, as the simplest path often leads to the most elegant solution.

This was a lesson in the beauty of simplicity in problem-solving.

p.s: I am deliberating not adding answers here so you can actually spend some time trying to understand how we solved the problem!

Feel free to checkout code on my Github repo and try to set it up and run on your machine 👇

--

--

Jatin K Malik

I write words that control computers to tell other computers to build fake computers that run on different computers.