day07_part2.py (2395B)
1 #!/usr/bin/env python 2 """Advent of Code 2021, day 7 (part 2): optimize 'crab submarine' alignment to 3 minimize fuel""" 4 5 # Well the actual code I wrote for part 1 isn't very useful here, but the 6 # general approach works so that's cool I guess. It's probably faster to 7 # evaluate the fuel use at every position than it is to deploy an optimizer to 8 # solve the problem. HOWEVER, I'm using AoC to learn, so today I'm learning how 9 # to use `scipy.optimize.minimize()`. 10 11 from scipy.optimize import minimize 12 from utils import get_puzzle_input, convert_int_line_to_series 13 14 def find_minimum_fuel_use(positions): 15 """Given the positions of the 'crab submarines', find the minimum fuel use 16 to line them up""" 17 # Unlike part 1, we need to actually work to find the solution. What's 18 # interesting is that an optimization procedure found that, for the example 19 # input, a position of 4.7 had better fuel use (167.55) than the example 20 # solution of 5 (168). The problem doesn't explicitly state that only 21 # integer solutions are allowed, but we'll round the result. Knowing this, 22 # we can loose up the tolerances quite a bit, which gives us a nice 23 # speed boost. 24 best_position_solution = minimize( 25 lambda x: calculate_fuel_use_to(positions, x[0]), 26 0, method='nelder-mead', options={'xatol': 0.1, 'fatol': 0.01} 27 ) 28 if not best_position_solution.success: 29 raise RuntimeError('optimizer failed to find solution') 30 return calculate_fuel_use_to(positions, 31 float(best_position_solution.x[0].round())) 32 33 def calculate_fuel_use_to(start_positions, end_position): 34 """Given a series that represents the (1-D) positions of a collection of 'crab 35 submarines', find the fuel used to move to the given end position""" 36 # \sum_{i=1}^{n}i=\frac{n(n+1)}{2} 37 return ( 38 start_positions 39 .sub(end_position) 40 .abs() 41 .apply(lambda n: n*(n+1)/2) 42 .sum() 43 ) 44 45 def solve_puzzle(input_string): 46 """Return the numeric solution to the puzzle""" 47 return find_minimum_fuel_use(convert_int_line_to_series(input_string)) 48 49 def main(): 50 """Run when the file is called as a script""" 51 assert solve_puzzle("16,1,2,0,4,2,7,1,2,14\n") == 168.0 52 print("Actual fuel spent to align crabs:", 53 f"{solve_puzzle(get_puzzle_input(7)):.0f}") 54 55 if __name__ == "__main__": 56 main()