From e717792d97860a83e32c1ab16ef32381edcdf50c Mon Sep 17 00:00:00 2001 From: Dan Carroll Date: Sun, 8 Dec 2024 11:54:08 -0500 Subject: [PATCH 1/5] Day 8 Part 1 solution --- bin/day8.dart | 10 +++++++ lib/day8/part_1.dart | 44 ++++++++++++++++++++++++++++++ lib/day8/shared.dart | 50 ++++++++++++++++++++++++++++++++++ lib/shared/resources.dart | 3 +- resources/sample_data/day8.txt | 12 ++++++++ test/day8_test.dart | 21 ++++++++++++++ 6 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 bin/day8.dart create mode 100644 lib/day8/part_1.dart create mode 100644 lib/day8/shared.dart create mode 100644 resources/sample_data/day8.txt create mode 100644 test/day8_test.dart diff --git a/bin/day8.dart b/bin/day8.dart new file mode 100644 index 0000000..ec14e53 --- /dev/null +++ b/bin/day8.dart @@ -0,0 +1,10 @@ +import 'package:aoc_2024/lib.dart'; +import 'package:aoc_2024/day8/part_1.dart' as part1; + +Future main(List arguments) async { + await runDay( + day: 7, + part1: part1.calculate, + part2: (_) => Future.value(0), + ); +} diff --git a/lib/day8/part_1.dart b/lib/day8/part_1.dart new file mode 100644 index 0000000..f27e506 --- /dev/null +++ b/lib/day8/part_1.dart @@ -0,0 +1,44 @@ +import 'package:aoc_2024/lib.dart'; + +import 'shared.dart'; + +//typedef LocationPair = ({AntennaLocation a, AntennaLocation b}); + +Future calculate(Resources resources) async { + final frequencyMap = await loadData(resources); + + // Store a map of antinodes. Since antinodes could be at the + // same location as another antinode, each location has a + // list of frequencies. + Map> antinodes = {}; + + for (final frequency in frequencyMap.antennaLocations.entries) { + for (final pair in pairwise(frequency.value)) { + final antinodeLocations = anitnodeLocations(pair.$1, pair.$2) + .where((l) => frequencyMap.inBounds(l)); + + for (final location in antinodeLocations) { + antinodes.putIfAbsent(location, () => {}).add(frequency.key); + } + } + } + + return antinodes.keys.length; +} + +List<(T, T)> pairwise(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; +} + +Iterable anitnodeLocations(Location a, Location b) { + final diff = a - b; + final locations = [a + diff, a - diff, b + diff, b - diff]; + + return locations.where((e) => e != a && e != b); +} diff --git a/lib/day8/shared.dart b/lib/day8/shared.dart new file mode 100644 index 0000000..1b1f22a --- /dev/null +++ b/lib/day8/shared.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +import 'package:aoc_2024/lib.dart'; + +typedef Location = Point; + +final class FrequencyMap { + // Map of frequencies to locations. + final Map> antennaLocations; + final int height; + final int width; + + FrequencyMap( + {required this.antennaLocations, + required this.height, + required this.width}); + + bool inBounds(final Location location) { + return location.x >= 0 && + location.x < height && + location.y >= 0 && + location.y < width; + } +} + +/// 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/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/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..74062c2 --- /dev/null +++ b/test/day8_test.dart @@ -0,0 +1,21 @@ +import 'package:aoc_2024/lib.dart'; +import 'package:aoc_2024/day8/part_1.dart' as part1; +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); + }); + }); + + group('real data', tags: 'real-data', () { + final resources = Resources.real; + + test('part1', () async { + expect(await part1.calculate(resources), 394); + }); + }); +} From 0b096aa355e9ea8eeb1775dea94111fd4c4420f7 Mon Sep 17 00:00:00 2001 From: Dan Carroll Date: Sun, 8 Dec 2024 12:51:09 -0500 Subject: [PATCH 2/5] Day 8 Part 2 solution (includes significant refactoring to part 1) --- bin/day8.dart | 3 ++- lib/day8/part_1.dart | 38 +------------------------------------ lib/day8/part_2.dart | 8 ++++++++ lib/day8/shared.dart | 44 +++++++++++++++++++++++++++++++++++++++++++ lib/lib.dart | 1 + lib/shared/utils.dart | 9 +++++++++ test/day8_test.dart | 9 +++++++++ 7 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 lib/day8/part_2.dart create mode 100644 lib/shared/utils.dart diff --git a/bin/day8.dart b/bin/day8.dart index ec14e53..e723259 100644 --- a/bin/day8.dart +++ b/bin/day8.dart @@ -1,10 +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: (_) => Future.value(0), + part2: part2.calculate, ); } diff --git a/lib/day8/part_1.dart b/lib/day8/part_1.dart index f27e506..1b3a1d1 100644 --- a/lib/day8/part_1.dart +++ b/lib/day8/part_1.dart @@ -2,43 +2,7 @@ import 'package:aoc_2024/lib.dart'; import 'shared.dart'; -//typedef LocationPair = ({AntennaLocation a, AntennaLocation b}); - Future calculate(Resources resources) async { final frequencyMap = await loadData(resources); - - // Store a map of antinodes. Since antinodes could be at the - // same location as another antinode, each location has a - // list of frequencies. - Map> antinodes = {}; - - for (final frequency in frequencyMap.antennaLocations.entries) { - for (final pair in pairwise(frequency.value)) { - final antinodeLocations = anitnodeLocations(pair.$1, pair.$2) - .where((l) => frequencyMap.inBounds(l)); - - for (final location in antinodeLocations) { - antinodes.putIfAbsent(location, () => {}).add(frequency.key); - } - } - } - - return antinodes.keys.length; -} - -List<(T, T)> pairwise(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; -} - -Iterable anitnodeLocations(Location a, Location b) { - final diff = a - b; - final locations = [a + diff, a - diff, b + diff, b - diff]; - - return locations.where((e) => e != a && e != b); + 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..e87574c --- /dev/null +++ b/lib/day8/part_2.dart @@ -0,0 +1,8 @@ +import 'package:aoc_2024/lib.dart'; + +import 'shared.dart'; + +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 index 1b1f22a..ef18adb 100644 --- a/lib/day8/shared.dart +++ b/lib/day8/shared.dart @@ -21,6 +21,50 @@ final class FrequencyMap { location.y >= 0 && location.y < width; } + + 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; + } + + 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 ((includeHarmonics || !addedOneAntinode) && + (includeHarmonics || (next != a && next != b)) && + inBounds(next) && + !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 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/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/test/day8_test.dart b/test/day8_test.dart index 74062c2..0178f9f 100644 --- a/test/day8_test.dart +++ b/test/day8_test.dart @@ -1,5 +1,6 @@ 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() { @@ -9,6 +10,10 @@ void main() { test('part1', () async { expect(await part1.calculate(resources), 14); }); + + test('part2', () async { + expect(await part2.calculate(resources), 34); + }); }); group('real data', tags: 'real-data', () { @@ -17,5 +22,9 @@ void main() { test('part1', () async { expect(await part1.calculate(resources), 394); }); + + test('part2', () async { + expect(await part2.calculate(resources), 1277); + }); }); } From b23a81eb003f219abde3e4acb49513b2f86bad9c Mon Sep 17 00:00:00 2001 From: Dan Carroll Date: Sun, 8 Dec 2024 13:04:09 -0500 Subject: [PATCH 3/5] Add comments to explain the Day 8 solutions --- lib/day8/part_1.dart | 9 +++++++++ lib/day8/part_2.dart | 10 ++++++++++ lib/day8/shared.dart | 43 ++++++++++++++++++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/lib/day8/part_1.dart b/lib/day8/part_1.dart index 1b3a1d1..99f6941 100644 --- a/lib/day8/part_1.dart +++ b/lib/day8/part_1.dart @@ -2,6 +2,15 @@ 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 index e87574c..198a906 100644 --- a/lib/day8/part_2.dart +++ b/lib/day8/part_2.dart @@ -2,6 +2,16 @@ 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 index ef18adb..9d9d544 100644 --- a/lib/day8/shared.dart +++ b/lib/day8/shared.dart @@ -4,10 +4,16 @@ import 'package:aoc_2024/lib.dart'; typedef Location = Point; +/// Represents frequencies being broadcast from antennas within a map. final class FrequencyMap { - // Map of frequencies to locations. + /// 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( @@ -15,6 +21,8 @@ final class FrequencyMap { 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 && @@ -22,6 +30,17 @@ final class FrequencyMap { 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 = {}; @@ -35,6 +54,8 @@ final class FrequencyMap { 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, @@ -53,10 +74,22 @@ final class FrequencyMap { for (final direction in directionFunctions) { var addedOneAntinode = false; var next = direction.hopFunc(direction.starting); - while ((includeHarmonics || !addedOneAntinode) && - (includeHarmonics || (next != a && next != b)) && - inBounds(next) && - !locations.contains(next)) { + 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); From 182c3d7a4c522d8cbd07a77172abac287de842b6 Mon Sep 17 00:00:00 2001 From: Dan Carroll Date: Sun, 8 Dec 2024 13:08:04 -0500 Subject: [PATCH 4/5] Add missing descriptions to the Day 7 solutions, for posterity --- lib/day7/part_1.dart | 10 ++++++++++ lib/day7/part_2.dart | 2 ++ 2 files changed, 12 insertions(+) diff --git a/lib/day7/part_1.dart b/lib/day7/part_1.dart index 68a3d63..6b0a92d 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 26c9d24..ef29dbe 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); From e718744eef947f4d9dcfb61065b87c49fe158a1f Mon Sep 17 00:00:00 2001 From: Dan Carroll Date: Sun, 8 Dec 2024 22:20:00 -0500 Subject: [PATCH 5/5] Run Day 8 in aoc.dart --- bin/aoc.dart | 2 ++ 1 file changed, 2 insertions(+) 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('');