Dazbo's Advent of Code solutions, written in Python
We’re dealing with a firewall that has a blacklist of IP addresses. The IPs are represented as 32-bit integers, ranging from 0 to 4294967295. The blacklist is a series of ranges, inclusive of the start and end values.
Here’s an example of the input data:
5-8
0-2
4-7
Our goal is to find the allowed IP addresses.
What is the lowest-valued IP that is not blocked?
My initial approach was to create a class that manages the denied IP ranges. The idea is to merge any overlapping or adjacent ranges to simplify the blacklist. Once all the blacklist ranges are processed and merged, I can determine the allowed ranges by looking at the gaps between the denied ranges.
Here’s the Allowed_IPs class from my first solution (ip_addresses.py):
class Allowed_IPs():
    def __init__(self, min_ip, max_ip) -> None:
        self._min_ip = min_ip
        self._max_ip = max_ip
        self._deny_ranges = []
        self._allowed_ranges = []
    
    # ... (properties omitted for brevity)
    
    def add_deny(self, new_min, new_max):
        # ... (code omitted for brevity)
The add_deny method is where the magic happens. It takes a new blacklist range and merges it with the existing _deny_ranges. Let’s break down the logic:
deny_ranges. For each deny_range, it checks for overlaps with the new_range.
    new_range can extend an existing deny_range to the left or right, it updates the boundaries of the existing range.new_range is completely contained within an existing deny_range, no action is needed.new_range is added as a new entry in _deny_ranges.Sort: After a potential addition, the _deny_ranges list is sorted by the lower bound of each range. This is crucial for the next step.
After adding all the ranges, _compute_allow_ranges calculates the gaps between the sorted and consolidated _deny_ranges.
This approach worked for Part 1, but I ran into issues with Part 2, and it’s not the most efficient way to handle this problem.
I found a more elegant solution online that uses Python’s bisect module for more efficient range management. This solution maintains a list of allowed ranges and uses deny to chip away at them.
bisect?The bisect module in Python provides support for maintaining a list in sorted order without having to sort the list after each insertion. It uses a binary search algorithm, which is much more efficient (O(log n)) for searching and finding insertion points than the linear scan (O(n)) I was doing in my original solution.
Here’s the IntRange class from the second solution:
import bisect
class IntRange(object):
    def __init__(self, min_ip, max_ip):
        self._ranges = [(min_ip, max_ip)]
    def __len__(self):
        return sum(hi - lo + 1 for (lo, hi) in self._ranges)
    def deny(self, deny_min, deny_max):
        i = 0
        while i < len(self._ranges):
            lo, hi = self._ranges[i]
            # Range is completely denied
            if deny_min <= lo and hi <= deny_max:
                del self._ranges[i]
                continue
            # Denial is completely within the range, split it
            elif lo < deny_min and deny_max < hi:
                del self._ranges[i]
                self._ranges.insert(i, (lo, deny_min - 1))
                self._ranges.insert(i + 1, (deny_max + 1, hi))
            # Partial overlap, adjust the range
            elif lo <= deny_min <= hi:
                self._ranges[i] = (lo, deny_min - 1)
            elif lo <= deny_max <= hi:
                self._ranges[i] = (deny_max + 1, hi)
            i += 1
The deny method in this class is much cleaner. It handles all the cases of overlap and splitting of ranges in a more straightforward way:
deny range, it’s simply deleted.deny range falls in the middle of an allowed range, the allowed range is split into two new, smaller allowed ranges.deny range overlaps with one end of an allowed range, the allowed range is simply truncated.This approach is more robust and less prone to the edge case errors I encountered with my initial solution. The full implementation of this class also uses bisect.insort to add new allowed ranges, which keeps the _ranges list sorted and makes the deny logic simpler.
How many IPs are allowed by the blacklist?
With the IntRange class, this becomes a simple matter of getting the length of the IntRange object, which is the sum of the lengths of all the allowed ranges.
Using my original Allowed_IPs class, I could get the answer by summing the sizes of the allowed ranges.
Here’s the output from the more efficient ip_addresses2.py solution:
First IP: 17348584
Number of allowed IPs: 104
Execution time: 0.0150 seconds
My original solution was a bit slower, but produced the same results. This was a good lesson in choosing the right data structures and algorithms for the job. The bisect module is a powerful tool for working with sorted lists.