diff --git a/bin/aoc.dart b/bin/aoc.dart index 3c36f95..e5e710c 100644 --- a/bin/aoc.dart +++ b/bin/aoc.dart @@ -5,6 +5,7 @@ import 'day4.dart' as day4; import 'day5.dart' as day5; import 'day6.dart' as day6; import 'day7.dart' as day7; +import 'day8.dart' as day8; void main(List arguments) async { print(''); @@ -16,6 +17,7 @@ void main(List arguments) async { day5.main, day6.main, day7.main, + day8.main, ]) { await day(arguments); print(''); diff --git a/bin/day8.dart b/bin/day8.dart new file mode 100644 index 0000000..e723259 --- /dev/null +++ b/bin/day8.dart @@ -0,0 +1,11 @@ +import 'package:aoc_2024/lib.dart'; +import 'package:aoc_2024/day8/part_1.dart' as part1; +import 'package:aoc_2024/day8/part_2.dart' as part2; + +Future main(List arguments) async { + await runDay( + day: 7, + part1: part1.calculate, + part2: part2.calculate, + ); +} diff --git a/lib/day7/part_1.dart b/lib/day7/part_1.dart index 2e079a9..d5f0965 100644 --- a/lib/day7/part_1.dart +++ b/lib/day7/part_1.dart @@ -2,6 +2,16 @@ import 'package:aoc_2024/lib.dart'; import 'shared.dart'; +/// Given a list of equations without operators, determine which +/// equations could be valid, and return the sum of all equations +/// which could be valid. +/// +/// Equations are given of the form: +/// 3267: 81 40 27 +/// +/// The first value is the equation result, and the remaining value +/// are the equation operands. The only valid operators are add (`+`) +/// and multiply (`*`). Future calculate(Resources resources) async { final equations = await loadData(resources); diff --git a/lib/day7/part_2.dart b/lib/day7/part_2.dart index 8be3aa0..060674b 100644 --- a/lib/day7/part_2.dart +++ b/lib/day7/part_2.dart @@ -2,6 +2,8 @@ import 'package:aoc_2024/lib.dart'; import 'shared.dart'; +/// Following from part 1, but introduce a new operator: +/// concatenate (`||`). Future calculate(Resources resources) async { final equations = await loadData(resources); diff --git a/lib/day8/part_1.dart b/lib/day8/part_1.dart new file mode 100644 index 0000000..99f6941 --- /dev/null +++ b/lib/day8/part_1.dart @@ -0,0 +1,17 @@ +import 'package:aoc_2024/lib.dart'; + +import 'shared.dart'; + +/// Find the number of antinodes from a map of antenna locations. +/// +/// An antinode occurs at any point that is perfectly in line with two +/// antennas of the same frequency - but only when one of the antennas +/// is twice as far away as the other. +/// +/// This function should return the number of locations on the map that +/// are antinodes (some locations may be antinodes for multiple +/// frequencies, so only count those once). +Future calculate(Resources resources) async { + final frequencyMap = await loadData(resources); + return frequencyMap.antinodes(includeHarmonics: false).length; +} diff --git a/lib/day8/part_2.dart b/lib/day8/part_2.dart new file mode 100644 index 0000000..198a906 --- /dev/null +++ b/lib/day8/part_2.dart @@ -0,0 +1,18 @@ +import 'package:aoc_2024/lib.dart'; + +import 'shared.dart'; + +/// Continuing from part 1, include all harmonics of the frequency +/// when determining the number of antinode locations. +/// +/// This mean including any grid position that is exactly in line with +/// at least two antennas of the same frequency, regardless of distance. +/// It also means that the antenna locations themselves can be considered +/// as antinode locations. +/// +/// Return value is the same as part 1: number of unique locations that +/// are antinodes. +Future calculate(Resources resources) async { + final frequencyMap = await loadData(resources); + return frequencyMap.antinodes(includeHarmonics: true).length; +} diff --git a/lib/day8/shared.dart b/lib/day8/shared.dart new file mode 100644 index 0000000..9d9d544 --- /dev/null +++ b/lib/day8/shared.dart @@ -0,0 +1,127 @@ +import 'dart:math'; + +import 'package:aoc_2024/lib.dart'; + +typedef Location = Point; + +/// Represents frequencies being broadcast from antennas within a map. +final class FrequencyMap { + /// Map of frequenct to locations with an antenna broadcasting that + /// frequency. + final Map> antennaLocations; + + /// Height bound of the map. + final int height; + + /// Width bound of the map. + final int width; + + FrequencyMap( + {required this.antennaLocations, + required this.height, + required this.width}); + + /// Returns true if the given location fits within the bounds of + /// the map. + bool inBounds(final Location location) { + return location.x >= 0 && + location.x < height && + location.y >= 0 && + location.y < width; + } + + /// Generates a list of all antinodes for this map. Antinodes are points + /// in which the broadcast from two antennas of the same frequency are + /// amplified. + /// + /// If [includeHarmonics] is false, this will generate two antinodes per + /// pair of antennas on the same frequency. The antinodes will be on either + /// side of each antenna, where one antinode is twice as far from one antenna. + /// + /// If [includeHarmonics] is true, this will generate all antinodes along the + /// straightline path between two antennas. Each antinode is spaced out + /// according to the distance between the two antennas. + Set antinodes({bool includeHarmonics = false}) { + Set antinodes = {}; + + for (final antennaLocations in antennaLocations.values) { + for (final pair in pairs(antennaLocations)) { + antinodes.addAll(_generateAntinodesUntilOutOfBounds( + a: pair.$1, b: pair.$2, includeHarmonics: includeHarmonics)); + } + } + + return antinodes; + } + + /// Generates the list of antinodes for a single pair of antenna locations. + /// See [antinodes] for a description of the generation. + Set _generateAntinodesUntilOutOfBounds( + {required Location a, + required Location b, + required bool includeHarmonics}) { + final hop = a - b; + Set locations = {}; + + final List<({Location starting, Location Function(Location) hopFunc})> + directionFunctions = [ + (starting: a, hopFunc: (l) => l + hop), + (starting: a, hopFunc: (l) => l - hop), + (starting: b, hopFunc: (l) => l + hop), + (starting: b, hopFunc: (l) => l - hop), + ]; + + for (final direction in directionFunctions) { + var addedOneAntinode = false; + var next = direction.hopFunc(direction.starting); + while ( + // If not including all harmonics, only process this loop until + // an antinode has been added. + (includeHarmonics || !addedOneAntinode) && + // The two antenna locations themselves are only eligible to + // be considered antinodes when including all harmonics. + (includeHarmonics || (next != a && next != b)) && + // Stop processing when encountering an out-of-bounds + // location. + inBounds(next) && + // If a location has already been seen, we can stop processing. + // This is because [directionFunctions] will attempt to process + // each direction from both antenna locations. This allows us to + // avoid figuring out which direction to travel from a given + // antenna, but avoid processing the same locations twice. + !locations.contains(next)) { + locations.add(next); + addedOneAntinode = true; + next = direction.hopFunc(next); + } + } + + return locations; + } +} + +/// Loads data from file, which is a map of frequency to +/// locations (points on a map). +Future loadData(Resources resources) async { + final file = resources.file(Day.day8); + final lines = await file.readAsLines(); + + Map> frequencies = {}; + + for (int r = 0; r < lines.length; r++) { + final line = lines[r]; + for (int c = 0; c < line.length; c++) { + if (line[c] == '.') { + continue; + } + + final frequency = frequencies.putIfAbsent(line[c], () => []); + frequency.add(Location(r, c)); + } + } + + return FrequencyMap( + antennaLocations: frequencies, + height: lines.length, + width: lines[0].length); +} diff --git a/lib/lib.dart b/lib/lib.dart index 6a9cde2..8fcfee0 100644 --- a/lib/lib.dart +++ b/lib/lib.dart @@ -1,2 +1,3 @@ export 'shared/resources.dart'; export 'shared/runner.dart'; +export 'shared/utils.dart'; diff --git a/lib/shared/resources.dart b/lib/shared/resources.dart index 7077596..ec32391 100644 --- a/lib/shared/resources.dart +++ b/lib/shared/resources.dart @@ -22,7 +22,8 @@ enum Day { day4, day5, day6, - day7; + day7, + day8; } /// Manager for loading a resource file, based on type and day. diff --git a/lib/shared/utils.dart b/lib/shared/utils.dart new file mode 100644 index 0000000..1777c53 --- /dev/null +++ b/lib/shared/utils.dart @@ -0,0 +1,9 @@ +List<(T, T)> pairs(List items) { + List<(T, T)> records = []; + for (int i = 0; i < items.length - 1; i++) { + for (int j = i + 1; j < items.length; j++) { + records.add((items[i], items[j])); + } + } + return records; +} diff --git a/resources/sample_data/day8.txt b/resources/sample_data/day8.txt new file mode 100644 index 0000000..de0f909 --- /dev/null +++ b/resources/sample_data/day8.txt @@ -0,0 +1,12 @@ +............ +........0... +.....0...... +.......0.... +....0....... +......A..... +............ +............ +........A... +.........A.. +............ +............ \ No newline at end of file diff --git a/test/day8_test.dart b/test/day8_test.dart new file mode 100644 index 0000000..0178f9f --- /dev/null +++ b/test/day8_test.dart @@ -0,0 +1,30 @@ +import 'package:aoc_2024/lib.dart'; +import 'package:aoc_2024/day8/part_1.dart' as part1; +import 'package:aoc_2024/day8/part_2.dart' as part2; +import 'package:test/test.dart'; + +void main() { + group('sample data', tags: 'sample-data', () { + final resources = Resources.sample; + + test('part1', () async { + expect(await part1.calculate(resources), 14); + }); + + test('part2', () async { + expect(await part2.calculate(resources), 34); + }); + }); + + group('real data', tags: 'real-data', () { + final resources = Resources.real; + + test('part1', () async { + expect(await part1.calculate(resources), 394); + }); + + test('part2', () async { + expect(await part2.calculate(resources), 1277); + }); + }); +}