|
| 1 | +# -------------------------------- Input data ---------------------------------------- # |
| 2 | +import os, heapq |
| 3 | + |
| 4 | +test_data = {} |
| 5 | + |
| 6 | +test = 1 |
| 7 | +test_data[test] = { |
| 8 | + "input": """pos=<0,0,0>, r=4 |
| 9 | +pos=<1,0,0>, r=1 |
| 10 | +pos=<4,0,0>, r=3 |
| 11 | +pos=<0,2,0>, r=1 |
| 12 | +pos=<0,5,0>, r=3 |
| 13 | +pos=<0,0,3>, r=1 |
| 14 | +pos=<1,1,1>, r=1 |
| 15 | +pos=<1,1,2>, r=1 |
| 16 | +pos=<1,3,1>, r=1""", |
| 17 | + "expected": ["7", "Unknown"], |
| 18 | +} |
| 19 | +test += 1 |
| 20 | +test_data[test] = { |
| 21 | + "input": """pos=<10,12,12>, r=2 |
| 22 | +pos=<12,14,12>, r=2 |
| 23 | +pos=<16,12,12>, r=4 |
| 24 | +pos=<14,14,14>, r=6 |
| 25 | +pos=<50,50,50>, r=200 |
| 26 | +pos=<10,10,10>, r=5""", |
| 27 | + "expected": ["Unknown", "Position 12, 12, 12 => 36"], |
| 28 | +} |
| 29 | +test += 1 |
| 30 | +test_data[test] = { |
| 31 | + "input": """pos=<20,0,0>, r=15 |
| 32 | +pos=<0,0,0>, r=6""", |
| 33 | + "expected": ["Unknown", "5"], |
| 34 | +} |
| 35 | + |
| 36 | +test = "real" |
| 37 | +input_file = os.path.join( |
| 38 | + os.path.dirname(__file__), |
| 39 | + "Inputs", |
| 40 | + os.path.basename(__file__).replace(".py", ".txt"), |
| 41 | +) |
| 42 | +test_data[test] = { |
| 43 | + "input": open(input_file, "r+").read().strip(), |
| 44 | + "expected": ["761", "89915526"], |
| 45 | +} |
| 46 | + |
| 47 | +# -------------------------------- Control program execution ------------------------- # |
| 48 | + |
| 49 | +case_to_test = "real" |
| 50 | +part_to_test = 2 |
| 51 | + |
| 52 | +# -------------------------------- Initialize some variables ------------------------- # |
| 53 | + |
| 54 | +puzzle_input = test_data[case_to_test]["input"] |
| 55 | +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] |
| 56 | +puzzle_actual_result = "Unknown" |
| 57 | + |
| 58 | + |
| 59 | +# -------------------------------- Various functions ----------------------------- # |
| 60 | + |
| 61 | + |
| 62 | +def manhattan_distance(source, target): |
| 63 | + dist = 0 |
| 64 | + for i in range(len(source)): |
| 65 | + dist += abs(target[i] - source[i]) |
| 66 | + return dist |
| 67 | + |
| 68 | + |
| 69 | +def in_range_cube(corners): |
| 70 | + nb = 0 |
| 71 | + for bot in bots: |
| 72 | + xb, yb, zb = bot |
| 73 | + radius = bots[bot] |
| 74 | + |
| 75 | + # bot is outside the cube extended by radius in a cubic manner |
| 76 | + # said differently: bot is outside cube of size initial_size+radius*2 |
| 77 | + if xb < corners[0][0] - radius or xb > corners[1][0] + radius: |
| 78 | + continue |
| 79 | + if yb < corners[0][1] - radius or yb > corners[1][1] + radius: |
| 80 | + continue |
| 81 | + if zb < corners[0][2] - radius or zb > corners[1][2] + radius: |
| 82 | + continue |
| 83 | + |
| 84 | + # bot is inside the cube |
| 85 | + if xb >= corners[0][0] and xb <= corners[1][0]: |
| 86 | + if yb >= corners[0][1] and yb <= corners[1][1]: |
| 87 | + if zb >= corners[0][2] and zb <= corners[1][2]: |
| 88 | + nb += 1 |
| 89 | + continue |
| 90 | + |
| 91 | + # bot is too far from the cube's center |
| 92 | + cube_size = ( |
| 93 | + corners[1][0] - corners[0][0] + 4 |
| 94 | + ) # 4 added for margin of error & rounding |
| 95 | + center = [(corners[0][i] + corners[1][i]) // 2 for i in (0, 1, 2)] |
| 96 | + # The center is at cube_size // 2 * 3 distance from each corner |
| 97 | + max_distance = cube_size // 2 * 3 + radius |
| 98 | + if manhattan_distance(center, bot) <= max_distance: |
| 99 | + nb += 1 |
| 100 | + |
| 101 | + return nb |
| 102 | + |
| 103 | + |
| 104 | +def all_corners(cube): |
| 105 | + coords = list(zip(*cube)) |
| 106 | + corners = [[x, y, z] for x in coords[0] for y in coords[1] for z in coords[2]] |
| 107 | + return corners |
| 108 | + |
| 109 | + |
| 110 | +def in_range_spot(spot): |
| 111 | + nb = 0 |
| 112 | + for bot in bots: |
| 113 | + if manhattan_distance(spot, bot) <= bots[bot]: |
| 114 | + nb += 1 |
| 115 | + |
| 116 | + return nb |
| 117 | + |
| 118 | + |
| 119 | +def add_each(a, b): |
| 120 | + cpy = a.copy() |
| 121 | + for i in range(len(cpy)): |
| 122 | + cpy[i] += b[i] |
| 123 | + return cpy |
| 124 | + |
| 125 | + |
| 126 | +# -------------------------------- Actual code execution ----------------------------- # |
| 127 | + |
| 128 | + |
| 129 | +bots = {} |
| 130 | +for string in puzzle_input.split("\n"): |
| 131 | + if string == "": |
| 132 | + continue |
| 133 | + pos, rad = string.split(", ") |
| 134 | + pos = tuple(map(int, pos[5:-1].split(","))) |
| 135 | + bots[pos] = int(rad[2:]) |
| 136 | + |
| 137 | +max_strength = max(bots.values()) |
| 138 | +max_strength_bots = [x for x in bots if bots[x] == max(bots.values())] |
| 139 | + |
| 140 | + |
| 141 | +if part_to_test == 1: |
| 142 | + in_range = {} |
| 143 | + for bot in max_strength_bots: |
| 144 | + in_range[bot] = 0 |
| 145 | + for target in bots: |
| 146 | + if manhattan_distance(bot, target) <= max_strength: |
| 147 | + in_range[bot] += 1 |
| 148 | + puzzle_actual_result = max(in_range.values()) |
| 149 | + |
| 150 | +else: |
| 151 | + x, y, z = zip(*bots) |
| 152 | + corners = [[min(x), min(y), min(z)], [max(x), max(y), max(z)]] |
| 153 | + cube_size = max(max(x) - min(x), max(y) - min(y), max(z) - min(z)) |
| 154 | + count_bots = in_range_cube(corners) |
| 155 | + |
| 156 | + cubes = [(-count_bots, cube_size, corners)] |
| 157 | + heapq.heapify(cubes) |
| 158 | + |
| 159 | + all_cubes = [(count_bots, cube_size, corners)] |
| 160 | + |
| 161 | + # First, octree algorithm: the best candidates are split in 8 and analyzed |
| 162 | + min_bots = 1 |
| 163 | + best_dot = [10 ** 9, 10 ** 9, 10 ** 9] |
| 164 | + while cubes: |
| 165 | + nb, cube_size, cube = heapq.heappop(cubes) |
| 166 | + |
| 167 | + if -nb < min_bots: |
| 168 | + # Not enough bots in range |
| 169 | + continue |
| 170 | + if -nb == min_bots: |
| 171 | + if manhattan_distance((0, 0, 0), cube[0]) > sum(map(abs, best_dot)): |
| 172 | + # Cube is too far away from source |
| 173 | + continue |
| 174 | + |
| 175 | + # print (-nb, len(cubes), min_bots, cube_size, cube, best_dot, sum(map(abs, best_dot))) |
| 176 | + |
| 177 | + # Analyze all corners in all cases, it helps reduce the volume in the end |
| 178 | + corners = all_corners(cube) |
| 179 | + for dot in corners: |
| 180 | + nb_spot = in_range_spot(dot) |
| 181 | + if nb_spot > min_bots: |
| 182 | + min_bots = nb_spot |
| 183 | + best_dot = dot |
| 184 | + print("Min bots updated to ", nb_spot, "for dot", dot) |
| 185 | + elif nb_spot == min_bots: |
| 186 | + if manhattan_distance((0, 0, 0), best_dot) > manhattan_distance( |
| 187 | + (0, 0, 0), dot |
| 188 | + ): |
| 189 | + best_dot = dot |
| 190 | + print("Best dot set to ", dot) |
| 191 | + |
| 192 | + if cube_size == 1: |
| 193 | + # We can't divide it any further |
| 194 | + continue |
| 195 | + |
| 196 | + cube_size = (cube_size // 2) if cube_size % 2 == 0 else (cube_size // 2 + 1) |
| 197 | + |
| 198 | + new_cubes = [ |
| 199 | + [ |
| 200 | + add_each(cube[0], [x, y, z]), |
| 201 | + add_each(cube[0], [x + cube_size, y + cube_size, z + cube_size]), |
| 202 | + ] |
| 203 | + for x in (0, cube_size) |
| 204 | + for y in (0, cube_size) |
| 205 | + for z in (0, cube_size) |
| 206 | + ] |
| 207 | + |
| 208 | + for new_cube in new_cubes: |
| 209 | + count_bots = in_range_cube(new_cube) |
| 210 | + if count_bots >= min_bots: |
| 211 | + heapq.heappush(cubes, (-count_bots, cube_size, new_cube)) |
| 212 | + all_cubes.append((count_bots, cube_size, new_cube)) |
| 213 | + |
| 214 | + print("max power", min_bots) |
| 215 | + puzzle_actual_result = manhattan_distance((0, 0, 0), best_dot) |
| 216 | + |
| 217 | + |
| 218 | +# -------------------------------- Outputs / results --------------------------------- # |
| 219 | + |
| 220 | +print("Expected result : " + str(puzzle_expected_result)) |
| 221 | +print("Actual result : " + str(puzzle_actual_result)) |
0 commit comments