advent-of-code 13 min read

[Day 5] If You Give A Seed A Fertilizer// Advent of Code 2023 (Python)

Solving Advent of Code 2023 Day 5 in Python: mapping seeds on demand, then range intersections that beat a 34 minute brute force for seed ranges.

If You Give A Seed A Fertilizer (via DALL-E 3)

If You Give A Seed A Fertilizer (via DALL-E 3)

Advent of Code
You take the boat and find the gardener right where you were told he would be: managing a giant “garden” that looks…adventofcode.com


Part One

You take the boat and find the gardener right where you were told he would be: managing a giant “garden” that looks more to you like a farm.

“A water source? Island Island is the water source!” You point out that Snow Island isn’t receiving any water.

“Oh, we had to stop the water because we ran out of sand to filter it with! Can’t make snow with dirty water. Don’t worry, I’m sure we’ll get more sand soon; we only turned off the water a few days… weeks… oh no.” His face sinks into a look of horrified realization.

“I’ve been so busy making sure everyone here has food that I completely forgot to check why we stopped getting more sand! There’s a ferry leaving soon that is headed over in that direction — it’s much faster than your boat. Could you please go check it out?”

You barely have time to agree to this request when he brings up another. “While you wait for the ferry, maybe you can help us with our food production problem. The latest Island Island Almanac just arrived and we’re having trouble making sense of it.”

The almanac (your puzzle input) lists all of the seeds that need to be planted. It also lists what type of soil to use with each kind of seed, what type of fertilizer to use with each kind of soil, what type of water to use with each kind of fertilizer, and so on. Every type of seed, soil, fertilizer and so on is identified with a number, but numbers are reused by each category — that is, soil 123 and fertilizer 123 aren’t necessarily related to each other.

For example:

seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4

The almanac starts by listing which seeds need to be planted: seeds 79, 14, 55, and 13.

The rest of the almanac contains a list of maps which describe how to convert numbers from a source category into numbers in a destination category. That is, the section that starts with seed-to-soil map: describes how to convert a seed number (the source) to a soil number (the destination). This lets the gardener and his team know which soil to use with which seeds, which water to use with which fertilizer, and so on.

Rather than list every source number and its corresponding destination number one by one, the maps describe entire ranges of numbers that can be converted. Each line within a map contains three numbers: the destination range start, the source range start, and the range length.

Consider again the example seed-to-soil map:

50 98 2
52 50 48

The first line has a destination range start of 50, a source range start of 98, and a range length of 2. This line means that the source range starts at 98 and contains two values: 98 and 99. The destination range is the same length, but it starts at 50, so its two values are 50 and 51. With this information, you know that seed number 98 corresponds to soil number 50 and that seed number 99 corresponds to soil number 51.

The second line means that the source range starts at 50 and contains 48values: 50, 51, …, 96, 97. This corresponds to a destination range starting at 52 and also containing 48 values: 52, 53, …, 98, 99. So, seed number 53 corresponds to soil number 55.

Any source numbers that aren’t mapped correspond to the same destination number. So, seed number 10 corresponds to soil number 10.

So, the entire list of seed numbers and their corresponding soil numbers looks like this:

seed soil
0 0
1 1
… …
48 48
49 49
50 52
51 53
… …
96 98
97 99
98 50
99 51

With this map, you can look up the soil number required for each initial seed number:

Seed number 79 corresponds to soil number 81.

Seed number 14 corresponds to soil number 14.

Seed number 55 corresponds to soil number 57.

Seed number 13 corresponds to soil number 13.

The gardener and his team want to get started as soon as possible, so they’d like to know the closest location that needs a seed. Using these maps, find the lowest location number that corresponds to any of the initial seeds. To do this, you’ll need to convert each seed number through other categories until you can find its corresponding location number. In this example, the corresponding types are:

Seed 79, soil 81, fertilizer 81, water 81, light 74, temperature 78, humidity 78, location 82.

Seed 14, soil 14, fertilizer 53, water 49, light 42, temperature 42, humidity 43, location 43.

Seed 55, soil 57, fertilizer 57, water 53, light 46, temperature 82, humidity 82, location 86.

Seed 13, soil 13, fertilizer 52, water 41, light 34, temperature 34, humidity 35, location 35.

So, the lowest location number in this example is 35.

What is the lowest location number that corresponds to any of the initial seed numbers?


Phew! That was a big read! But often bigger problems have the most trivial solutions.

💭 On a closer observation, we can clearly make out that this is the standard map of map of map... problem where you ned to traverse across multiple maps almost in a pipe | like fashion where:

location = map1[map2[map3[map4[map5[map6[seed]]]]]]

where map1...N are being built in realtime while parsing the puzzle input!

So, as always, let’s first write a test case as given in the question, so we can start to structure our code and validate that with base case:

def test_find_lowest_location_number():
    almanac = """seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
"""

    answer = find_lowest_location_number(almanac)
    assert answer == 35, f"Tst failed: Expected 35, got {answer}"
    print("All tests passed succesfully!")

Now, it’s time to whip out a quick terminal to do some string operations, and as with the case with last few challenges, it seems like our good’ol friend split() will have to once again do some heavy lifting!

💡 Might be good to start looking into regex re for these usecases as well, but since the puzzle input is predictable and well structured, we can get away by using methods like data.split("\n\n")[0].split(":")[0]. In real life aka production code, this will not fly, but then again, the amount of such hacky code I have seen in production, only if I had ₹1 for every time, I would be a rich person!

Once, we know how we can parse the input, let’s try to implement our find_lowest_location_number() function. I can envision based on experience that our map building logic can be common here as we need to generate a dict based on destination, source, range_length as shared in base test case above!

💡 We don’t really need to worry about any source numbers that aren’t mapped, as we can just assume source == destination which becomes quite trivial in python with dict.get(key, default_value) where default value will just be the key itself.

So, I was able to write a simple loop which will basically call each and every map chained to one another and will store the final location in seed_location_map and from there we could just return min(seed_location_map.values()) for the answers.

💡 Edge case: We need to ensure that we maintain the order of maps as we are parsing the input, as if we mess up the order, the pipe | approach as we discussed above wouldn’t work. I am using a collections.deque object to ensure we reach from seed to the seed-to-soil > soil-to-fertilizer > fertilizer-to-water > water-to-light > light-to-temperature > temperature-to-humidity > humidity-to-location location.

So, with a lot of hit and trial and basically after living in the debugger, we can see that we are able to build our queue_of_dicts properly, where it is maintaining the correct order of dicts as parsed! (Hint: keeping a track of dict_name in queue_of_dicts does help a lot here).

# --- Day 5: If You Give A Seed A Fertilizer ---

## --- Part One ---

from collections import deque

def dict_builder(raw_map):
    raw_map = raw_map.strip("\n")
    dict_name, mappings = raw_map.split(':')
    dict_name = dict_name.split()[0]
    mappings = mappings.strip("\n").split("\n")

    dict_data = {}

    for row in mappings:
        dest, src, range_length = [int(num) for num in row.split()]
        for i in range(range_length):
            dict_data[src+i] = dest+i
    
    return dict_name, dict_data

def find_lowest_location_number(almanac):
    almanac = almanac.split("\n\n")

    # extract out seeds? assuming it's always first in the puzzle input
    seeds = [int(seed) for seed in almanac[0].split(":")[1].strip().split(" ")] 

    queue_of_dicts = deque() 

    for raw_map in almanac[1:]:
        tmp_name, tmp_data = dict_builder(raw_map)
        queue_of_dicts.append((tmp_name, tmp_data))

    # print(queue_of_dicts)

    seed_location_map = {}
    for seed in seeds:
        final_val = seed
        for _, tmp_map in queue_of_dicts:
            final_val = tmp_map.get(final_val, final_val) # update if found, else keep it same as key
        
        seed_location_map[seed]=final_val

    print("Seed location map:", seed_location_map)
    return min(seed_location_map.values())

def part_1():
    with open("day_5/input.txt", "r") as f:
        almanac = f.read()
    
    answer = find_lowest_location_number(almanac)
    print("Lowest location number:", answer)

def test_find_lowest_location_number():
    almanac = """seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
"""

    answer = find_lowest_location_number(almanac)
    assert answer == 35, f"Tst failed: Expected 35, got {answer}"
    print("All tests passed succesfully!")

if __name__ == "__main__":
    test_find_lowest_location_number()
❯ /Users/jmalik/.pyenv/versions/3.11.0rc2/bin/python /Users/jmalik/github/advent-of-code-2023/day_5/if_you_give_a_seed_a_fertilizer.py
Seed location map: {79: 82, 14: 43, 55: 86, 13: 35}
All tests passed succesfully!

Let’s load up the puzzle input…

And let’s run it against our function here!

Well….after 1 minute we were still waiting for it to finish…

Look at that CPU% 📈

Look at that CPU% 📈

And after 2 minutes of processing on my 10 core M1 Macbook, I was sure this is not really close to being optimal to solve it and… ‼️

And we hit the motherload!

And we hit the motherload!

So, let’s quit the program using Ctrl+C to break! Let’s see why?

If we observe the puzzle input v/s base test case, it will evident what is the bottleneck:

A chunk of puzzle input

A chunk of puzzle input

The numbers are just too damn high ⏫, and our approach of building full blown hashmaps (despite optimising for missing numbers in map by just returning the key, see 💡 above) using dict_builder() seems to waste a lot of CPU cycles especially when we see that the number of seeds are just 20!

seeds: 950527520 85181200 546703948 123777711 63627802 279111951 1141059215 246466925 1655973293 98210926 3948361820 92804510 2424412143 247735408 4140139679 82572647 2009732824 325159757 3575518161 370114248

Surely, there should be a way for us to avoid all of the preprocessing and just do it on demand?

💡 Instead of creating a complete mapping for each step, we can write a function that, given a number and a map range, computes the corresponding number dynamically. This way, we avoid holding large maps in memory!

In this updated code, find_mapped_number function dynamically calculates the mapped number for a given input number and a single mapping range.

It works by iterating through a list of mapping rules and, for each rule, checks if the given number falls within the specified source range. If it does, the function calculates the mapped number by adding the difference between the given number and the source range start to the destination range start.

This function is used within find_lowest_location_number to process each seed number through the series of mappings without storing the entire range of mappings in memory. This should resolve the memory issue and allow the program to handle large input data efficiently 🤞

Our base test case is working as intended!

❯ /Users/jmalik/.pyenv/versions/3.11.0rc2/bin/python /Users/jmalik/github/advent-of-code-2023/day_5/if_you_give_a_seed_a_fertilizer.py
All tests passed succesfully!

Let’s try to run this again for input.txt and let’s see how quick it is, this time?

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_5/if_you_give_a_seed_a_fertilizer.py"
The lowest location number is: xxxxxx

[Done] exited with code=0 in 0.05 seconds

Got the answer in 0.05 seconds! Isn’t that crazy how we were struggling with OutOfMemoryError just a while back, and with a small change, we are able to solve the same problem in just 0.05 seconds!

💭 Kids, this is why DSA is very important! You can save so much $$$ in production code wrt your compute and data cost if you write your code efficiently!

Let’s put our answer into the box and…

🎉🎉

🎉🎉

Here’s our pythonic code till now! Encapsulating the logic within a class allows better organization and readability!

# --- Day 5: If You Give A Seed A Fertilizer ---

class AlmanacProcessor:
    def __init__(self, almanac):
        self.almanac = almanac
        self.seeds, self.mappings = self._parse_input(almanac)

    def find_lowest_location_number(self):
        """Finds the lowest location number based on the almanac's rules."""
        seed_locations = {}
        for seed in self.seeds:
            current_number = seed
            for map_data in self.mappings:
                current_number = self._find_mapped_number(current_number, map_data)
            seed_locations[seed] = current_number  # location of seed in last map
        return min(seed_locations.values())

    def _parse_input(self, input_data):
        """Parses the input data into seeds and mappings."""
        sections = input_data.strip().split('\n\n')
        seeds = list(map(int, sections[0].split(': ')[1].split()))
        mappings = [section.split(':\n')[1].split('\n') for section in sections[1:]]
        return seeds, mappings

    def _find_mapped_number(self, number, map_data):
        """Finds the corresponding mapped number based on the mapping rules."""
        for line in map_data:
            if line.strip():
                dest_start, src_start, length = map(int, line.split())
                if src_start <= number < src_start + length:
                    return dest_start + (number - src_start)
        return number

def part_1():
    with open("day_5/input.txt", "r") as f:
        almanac = f.read()

    processor = AlmanacProcessor(almanac)
    lowest_location = processor.find_lowest_location_number()
    print("The lowest location number is:", lowest_location)

def test_find_lowest_location_number():
    almanac = """seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
"""

    processor = AlmanacProcessor(almanac)
    lowest_location = processor.find_lowest_location_number()
    assert lowest_location == 35, f"Tst failed: Expected 35, got {lowest_location}"
    print("All tests passed succesfully!")

if __name__ == "__main__":
    # Uncomment the line below to run the test
    # test_find_lowest_location_number()

    part_1()

Part Two

Everyone will starve if you only plant such a small number of seeds. Re-reading the almanac, it looks like the seeds: line actually describes ranges of seed numbers.

The values on the initial seeds: line come in pairs. Within each pair, the first value is the start of the range and the second value is the length of the range. So, in the first line of the example above:

seeds: 79 14 55 13

This line describes two ranges of seed numbers to be planted in the garden. The first range starts with seed number 79 and contains 14 values: 79, 80, …, 91, 92. The second range starts with seed number 55 and contains 13values: 55, 56, …, 66, 67.

Now, rather than considering four seed numbers, you need to consider a total of 27 seed numbers.

In the above example, the lowest location number can be obtained from seed number 82, which corresponds to soil 84, fertilizer 84, water 84, light 77, temperature 45, humidity 46, and location 46. So, the lowest location number is 46.

Consider all of the initial seed numbers listed in the ranges on the first line of the almanac. What is the lowest location number that corresponds to any of the initial seed numbers?


So, reading the Part Two of the problem…

💭 It’s evident that we have more number of seeds that we initially thought and it will require us to implement as similar range logic for seeds as we did for the mappings.

Let’s play a bit in ipython3 do build the logic to expand seeds:

And now let’s update our code to generate out seeds list correctly:

And it seems like base test case is working as we get: 46 as the answer.

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_5/if_you_give_a_seed_a_fertilizer.py"
All tests passed succesfully!

Here’s our code:

# --- Day 5: If You Give A Seed A Fertilizer ---

class AlmanacProcessor:
    def __init__(self, almanac):
        self.almanac = almanac
        self.seeds, self.mappings = self._parse_input(almanac)

    def find_lowest_location_number(self):
        """Finds the lowest location number based on the almanac's rules."""
        seed_locations = {}
        for seed in self.seeds:
            current_number = seed
            for map_data in self.mappings:
                current_number = self._find_mapped_number(current_number, map_data)
            seed_locations[seed] = current_number  # location of seed in last map
        return min(seed_locations.values())

    def _parse_input(self, input_data):
        """Parses the input data into seeds and mappings."""
        sections = input_data.strip().split('\n\n')

        # part One assumes seeds to be given as a list of integers
        # seeds = list(map(int, sections[0].split(': ')[1].split()))

        # part Two explains seeds to be given as a list of ranges as a pair
        seed_ranges = sections[0].split(': ')[1].split()
        seeds = self._generate_seed_list(seed_ranges)

        mappings = [section.split(':\n')[1].split('\n') for section in sections[1:]]
        return seeds, mappings

    def _generate_seed_list(self, seed_ranges):
        """Generates a list of seeds based on the ranges provided in pairs"""
        seeds = []
        for i in range(0, len(seed_ranges), 2):
            start = int(seed_ranges[i])
            length = int(seed_ranges[i + 1])
            seeds.extend(range(start, start + length))
        return seeds

    def _find_mapped_number(self, number, map_data):
        """Finds the corresponding mapped number based on the mapping rules."""
        for line in map_data:
            if line.strip():
                dest_start, src_start, length = map(int, line.split())
                if src_start <= number < src_start + length:
                    return dest_start + (number - src_start)
        return number

def main():
    with open("day_5/input.txt", "r") as f:
        almanac = f.read()

    processor = AlmanacProcessor(almanac)
    lowest_location = processor.find_lowest_location_number()
    print("The lowest location number is:", lowest_location)

def test_find_lowest_location_number():
    almanac = """seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
"""

    processor = AlmanacProcessor(almanac)
    lowest_location = processor.find_lowest_location_number()
    # assert lowest_location == 35, f"Tst failed: Expected 35, got {lowest_location}" # As per part 1 logic for seeds
    assert lowest_location == 46, f"Tst failed: Expected 35, got {lowest_location}" # As per part 2 logic for seeds
    print("All tests passed succesfully!")

if __name__ == "__main__":
    # Uncomment the line below to run the test
    test_find_lowest_location_number()

    main()

Let’s run for input.txt I don’t have a good feeling about this as the number of seeds is now too damn high!

Yep, process running from 1:58 mins 😅

Yep, process running from 1:58 mins 😅

Lost my patience after waiting for 4:22 minutes!

Lost my patience after waiting for 4:22 minutes!

Let’s exit it and take another look at the bottleneck now. Clearly, it’s the insanely high number of seeds in the puzzle input:

seeds: 950527520 85181200 546703948 123777711 63627802 279111951 1141059215 246466925 1655973293 98210926 3948361820 92804510 2424412143 247735408 4140139679 82572647 2009732824 325159757 3575518161 370114248

which basically translates to the huge numbers of seeds we need as per:

seed_start, till_range
950527520, 85181200 
546703948, 123777711
63627802, 279111951
1141059215, 246466925
1655973293, 98210926
3948361820, 92804510
2424412143, 247735408
4140139679, 82572647
2009732824, 325159757
3575518161, 370114248

💡 One of the bottlenecks I can see is where I am trying to store seed_locations dict for all seeds and then later running a min(seed_locations.values()) to get the lowest location. Maybe we can just keep the minimum value and compare on each iteration within the loop?!

This should save some time?

This should save some time?

Let’s run it again? 🤞

Well, not much help!

Well, not much help!

I think we need to deep dive further into reducing our reliance on memory and computation and try to reduce the number of calculations somehow! 🤔

💡 We need to optimise our mapping to preprocess into a dict and avoid generating all seeds and storing them in memory, instead we can just do everything on the fly? We can take a seed, go through the maps to find the location and just check as per lowest_location = min(lowest_location, current_number) to continously update the lowerst location.

That should help a lot in fixing the performance of this code!

NOPE!

💡I also wondered if there is any overlap in seed numbers leading to duplicate calculation and hence ended up using excel to just create seed number ranges available to us:

   6,36,27,802 to   34,27,39,753
  54,67,03,948 to   67,04,81,659
  95,05,27,520 to 1,03,57,08,720
1,14,10,59,215 to 1,38,75,26,140
1,65,59,73,293 to 1,75,41,84,219
2,33,48,92,581 to 2,33,48,92,581
2,42,44,12,143 to 2,67,21,47,551
3,57,55,18,161 to 3,94,56,32,409
3,94,83,61,820 to 4,04,11,66,330
4,14,01,39,679 to 4,22,27,12,326

Turns out there is no overlap. So, keeping a track of processed_seeds will also not help! 🤦‍♂️

I think now the only solution I can think off is to parallelize the seed -> location lookup and just find the lowest seed in each range and then compare at the end?

❯ /Users/jmalik/.pyenv/versions/3.11.0rc2/bin/python /Users/jmalik/github/advent-of-code-2023/day_5/if_you_give_a_seed_a_fertilizer.py
Starting a thread for seed range: 950527520 1035708720
Starting a thread for seed range: 546703948 670481659
Starting a thread for seed range: 63627802 342739753
Starting a thread for seed range: 1141059215 1387526140
Starting a thread for seed range: 1655973293 1754184219
Starting a thread for seed range: 3948361820 4041166330
Starting a thread for seed range: 2424412143 2672147551
Starting a thread for seed range: 4140139679 4222712326
Starting a thread for seed range: 2009732824 2334892581
Starting a thread for seed range: 3575518161 3945632409

It did spawn 10 proccesss, 1 for each range but..

It did spawn 10 proccesss, 1 for each range but..

I think we need MOAR processes! Need to divide and conquer further!

Let’s do some subprocesses in batches of 1000?

Wait! I am making it too complex!! While I did get the correct answer from brute force in 34 minutes while my laptop ran 🔥, there has to be a simpler way!

Let’s go back to drawing board!

💡 The essence of part 2 solution lies in handling of the mappings as ranges instead of individual values which are well over in ~4Bn+.

During the conversion process, we can iterate over each mapping, determining the intersection between the current value ranges (starting with seeds) and the conversion ranges specified in the (subsequent) maps.

If a mapping applies to a particular range, we can transform it accordingly; otherwise, the range remains unchanged.

💡 This approach efficiently reduces the computational load by avoiding the need to process each individual value, instead working with batches of values encapsulated within ranges.

After processing all the mappings, we can get a final set of location ranges, post which we can find the lowest possible location number by selecting the minimum lower bound from these final location ranges.

Here’s the final code:

# --- Day 5: If You Give A Seed A Fertilizer ---

# --- Part Two ---

class AlmanacProcessor:
    def __init__(self, almanac):
        self.seeds = []
        self.maps = []
        self._parse_input(almanac)

    def _parse_input(self, almanac):
        """Parses the input sections into seeds and maps."""
        self.sections = [block.splitlines() for block in almanac.strip().split("\n\n")]
        self.seeds = [int(i) for i in self.sections[0][0].split()[1:]]
        self.maps = [[list(map(int, line.split())) for line in block[1:]] for block in self.sections[1:]]

    def process_maps(self):
        """Processes each map to convert the ranges according to the almanac."""
        # this will store the lower and upper bounds of each range
        values = [(self.seeds[i], self.seeds[i] + self.seeds[i + 1]) for i in range(0, len(self.seeds), 2)] 

        for map in self.maps:
            next_map = []
            for dest_start, src_start, length in map:
                next_range = []
                for val_low, val_high in values:
                    sorted_bounds = sorted([val_low, val_high, src_start, src_start + length])
                    for low, high in zip(sorted_bounds, sorted_bounds[1:]):
                        if val_low <= low < high <= val_high:
                            if src_start <= low < high <= src_start + length:
                                next_map.append((low - src_start + dest_start, high - src_start + dest_start))
                            else:
                                next_range.append((low, high))
                values = next_range
            values += next_map

        return values

    def find_minimum_location(self, values):
        """Finds the minimum location value."""
        return min(low for low, high in values)

def test_find_lowest_location_number():
    almanac = """seeds: 79 14 55 13

seed-to-soil map:
50 98 2
52 50 48

soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15

fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4

water-to-light map:
88 18 7
18 25 70

light-to-temperature map:
45 77 23
81 45 19
68 64 13

temperature-to-humidity map:
0 69 1
1 0 69

humidity-to-location map:
60 56 37
56 93 4
"""

    processor = AlmanacProcessor(almanac)
    values = processor.process_maps()
    lowest_location = processor.find_minimum_location(values)
    assert lowest_location == 46, f"Tst failed: Expected 35, got {lowest_location}" # As per part 2 logic for seeds
    print("All tests passed succesfully!")

def main():
    with open("day_5/input.txt", "r") as f:
        almanac = f.read()

    processor = AlmanacProcessor(almanac)
    values = processor.process_maps()
    min_location = processor.find_minimum_location(values)

    print(f"Minimum location number: {min_location}")

if __name__ == "__main__":
    test_find_lowest_location_number()
    main()

Let’s run this one! 🤞

[Running] python3 "/Users/jmalik/github/advent-of-code-2023/day_5/if_you_give_a_seed_a_fertilizer_take2.py"
All tests passed succesfully!
Minimum location number: xxxxxx

[Done] exited with code=0 in 0.06 seconds

And we get an answer in just 0.06 seconds a much much faster result than 34 minutes of brutal brute force! 🧐

Let’s input that in our answer box and…

🥁

🥁


This one was just pure madness, I absolutely loved it! 🙌

On to the next one!


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 👇

GitHub - jatinkrmalik/advent-of-code-2023: Advent of Code is an Advent calendar of small…
Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can…github.com

Leave a comment

Keep reading

advent-of-code 2 min

🎄Solve Advent of Code 2024 With Me!

Announcing a spoiler-free YouTube video series for Advent of Code 2024 that focuses on how to analyze and approach each puzzle, not just the answers.