From e4195ca5bed203c49cd55a3d788ac3eaaa4f5d4f Mon Sep 17 00:00:00 2001 From: Steven Lischer Date: Tue, 18 Dec 2018 05:24:29 -0500 Subject: [PATCH 1/3] itertools cycle ftw: a bit faster than fast_mode Zipping through a list of itertools.cycle() objects is much faster for managing state of scanners than recalculating the scanner position each time. On my laptop I consistently saw times of 1.2 seconds for this script versus 5 seconds for fast_mode.py --- faster_mode.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 faster_mode.py diff --git a/faster_mode.py b/faster_mode.py new file mode 100644 index 0000000..60c6c70 --- /dev/null +++ b/faster_mode.py @@ -0,0 +1,48 @@ +import cProfile +import itertools + + +def build_scanner(scanner_pos, scanner_height): + ''' + Create a scanner. A scanner's cycle frequency is offset by its position so + that an entire row of zipped scanners represents one start time run. False + blocks a packet and True let's it past. + + ''' + freq = (scanner_height - 1) * 2 + scanner = [True] * freq + cycle_offset_due_to_pos = 0 - scanner_pos%freq + if cycle_offset_due_to_pos < 0: + cycle_offset_due_to_pos += freq + scanner[cycle_offset_due_to_pos] = False + return scanner + + +def firewall_from_file(firewall_file): + ''' + From a firewall input file, iterate back a firewall's scanners. + + ''' + with open(firewall_file) as f: + for line in f: + scanner_pos, scanner_height = map(int, line.strip().split(': ')) + scanner = build_scanner(scanner_pos, scanner_height) + yield itertools.cycle(scanner) + + +def find_start(firewall): + ''' + Unpack scanners from a firewall and simulataneously step through them + to find the minimum start time to get the packet through (aka all + scanners are True.) + + ''' + for t_start, possible_solution in enumerate(zip(*firewall)): + if False in possible_solution: + continue + else: + return t_start + + +cProfile.run('start = find_start(firewall_from_file("firewall.input"))') +print(f'start at {start}') From a17a3dd510a32455691da2c4595d22cc74c4b688 Mon Sep 17 00:00:00 2001 From: Steven Lischer Date: Tue, 18 Dec 2018 05:47:47 -0500 Subject: [PATCH 2/3] filename error --- faster_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/faster_mode.py b/faster_mode.py index 60c6c70..e4741bb 100644 --- a/faster_mode.py +++ b/faster_mode.py @@ -44,5 +44,5 @@ def find_start(firewall): return t_start -cProfile.run('start = find_start(firewall_from_file("firewall.input"))') +cProfile.run('start = find_start(firewall_from_file("./day13/input.txt"))') print(f'start at {start}') From 4e9accf20a7a6627323c14bbcad17cb2e27d1d2b Mon Sep 17 00:00:00 2001 From: Steven Lischer Date: Tue, 18 Dec 2018 22:31:39 -0500 Subject: [PATCH 3/3] firewall optimizing and refactoring Major refactor of the code to help keep concepts clear as I added in firewall optimizing. The 'offset scanner' approach means scanners of the same size can be merged together to a single scanner and small scanners that are exactly half, a third, a quarter, etc. the size of another scanner can be expanded and merged as well. With the example firewall, 43 scanners in the firewall are reduced to 5 using these techniques. From the previous build, this doubles performance. From fast_mode.py, this is ~11x faster and easily runs under one second. --- faster_mode.py | 104 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/faster_mode.py b/faster_mode.py index e4741bb..6b86a04 100644 --- a/faster_mode.py +++ b/faster_mode.py @@ -1,34 +1,89 @@ import cProfile import itertools +class Scanner(): + def __init__(self, position, cycle_length, shift=True): + ''' + Create a scanner and shift its cycle by an offset determined by its + position and frequency. Shifting the cycle means we no longer care about + the position of the scanner in the firewall. -def build_scanner(scanner_pos, scanner_height): - ''' - Create a scanner. A scanner's cycle frequency is offset by its position so - that an entire row of zipped scanners represents one start time run. False - blocks a packet and True let's it past. - - ''' - freq = (scanner_height - 1) * 2 - scanner = [True] * freq - cycle_offset_due_to_pos = 0 - scanner_pos%freq - if cycle_offset_due_to_pos < 0: - cycle_offset_due_to_pos += freq - scanner[cycle_offset_due_to_pos] = False - return scanner + ''' + self._cycle = [1] + [0] * (cycle_length-1) + if shift is True: + self.shift_cycle(position) + def __len__(self,): + return len(self._cycle) -def firewall_from_file(firewall_file): - ''' - From a firewall input file, iterate back a firewall's scanners. + def __iter__(self): + for pos in self._cycle: + yield pos - ''' - with open(firewall_file) as f: - for line in f: - scanner_pos, scanner_height = map(int, line.strip().split(': ')) - scanner = build_scanner(scanner_pos, scanner_height) + def shift_cycle(self, position): + ''' + Shift the cycle relative to its position. Allows for scanners of + similar size or harmonic cycle times to be merged/flattened into a + single scanner. Removes the need to look ahead from the start time to + see if a packet will pass the scanner. + ''' + self._cycle = [0] * len(self) + offset = 0 - (position % len(self)) + if offset < 0: + offset += len(self) + self._cycle[offset] = 1 + + def merge(self, scanner): + ''' + Merge the passed scanner's cycle into this scanner. + ''' + self._cycle = tuple((max(v) for v in zip(scanner, self))) + + +class Firewall(): + def __init__(self, filepath): + ''' + From a firewall input file, create a firewall's scanners. + + ''' + self.scanners = {} + with open(filepath) as f: + for line in f: + scanner_pos, scanner_height = map(int, line.strip().split(': ')) + scanner_freq = 2 * (scanner_height - 1) + scanner = Scanner(scanner_pos, scanner_freq) + self.add_scanner(scanner) + + self.optimize() + + def __iter__(self): + for scanner in self.scanners.values(): yield itertools.cycle(scanner) + def add_scanner(self, scanner): + if len(scanner) in self.scanners: + self.scanners[len(scanner)].merge(scanner) + else: + self.scanners[len(scanner)] = scanner + + def optimize(self): + """ + Merge small scanners into larger ones if possible to reduce number of + scanners. + """ + cycle_lengths = sorted(self.scanners.keys()) + cycle_max = max(cycle_lengths) + for cycle_lenth in cycle_lengths: + for factor in itertools.count(start=2): + cycle_key = cycle_lenth * factor + if cycle_key > cycle_max or not cycle_key in self.scanners: + break + else: + expanded_scanner = list(self.scanners[cycle_lenth]) * factor + self.scanners[cycle_key].merge(expanded_scanner) + del self.scanners[cycle_lenth] + break + def find_start(firewall): ''' @@ -38,11 +93,12 @@ def find_start(firewall): ''' for t_start, possible_solution in enumerate(zip(*firewall)): - if False in possible_solution: + if 1 in possible_solution: continue else: return t_start -cProfile.run('start = find_start(firewall_from_file("./day13/input.txt"))') +firewall = Firewall(filepath="./day13/input.txt") +cProfile.run('start = find_start(firewall)') print(f'start at {start}')