diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8c1a94..007257b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,18 +10,18 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '17' + java-version: '19' check-latest: true - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: ~/.m2 - key: m2-${{ runner.os }}-${{ matrix.java-version }}-${{ hashFiles('**/pom.xml') }} + key: m2-${{ runner.os }}-19-${{ hashFiles('**/pom.xml') }} restore-keys: | - m2-${{ runner.os }}-${{ matrix.java-version }} + m2-${{ runner.os }}-19 m2-${{ runner.os }} m2 - run: mvn clean install diff --git a/.gitignore b/.gitignore index db33628..4d131ae 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ target/ src/test/resources/config.properties src/test/resources/input src/test/resources/2020 +.DS_Store diff --git a/.java-version b/.java-version index 98d9bcb..6ea9a3b 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -17 +19.0 diff --git a/README.md b/README.md index 8235f91..29e9c19 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -# Advent of Code 2021 (Java) +# Advent of Code 2022 (Java) This is the code I used to solve the -[Advent of Code](https://adventofcode.com/2021) puzzles. The main branch +[Advent of Code](https://adventofcode.com/2022) puzzles. The main branch contains the most recent year in which I participated. Other years have their own branch. ## Other Editions -* 2020 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/tree/2020) | [Rust](https://github.com/l0s/advent-of-code-rust)) +* 2020 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/tree/2020) | [Rust](https://github.com/l0s/advent-of-code-rust/releases/tag/y2020)) +* 2021 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/releases/tag/2021)) +* 2022 Advent of Code ([Rust](https://github.com/l0s/advent-of-code-rust)) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7099d59..fc13d64 100644 --- a/pom.xml +++ b/pom.xml @@ -6,21 +6,21 @@ com.macasaet advent-of-code - 0.2021.0-SNAPSHOT + 0.2022.0-SNAPSHOT - Advent of Code 2021 + Advent of Code 2022 UTF-8 - 17 - 17 + 19 + 19 org.junit.jupiter junit-jupiter - 5.8.2 + 5.9.0 diff --git a/src/test/java/com/macasaet/Day01.java b/src/test/java/com/macasaet/Day01.java index 0485635..d70e9ed 100644 --- a/src/test/java/com/macasaet/Day01.java +++ b/src/test/java/com/macasaet/Day01.java @@ -1,65 +1,76 @@ package com.macasaet; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; import java.util.ArrayList; -import java.util.LinkedList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; import java.util.List; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 1: Sonar Sweep --- + * --- Day 1: Calorie Counting --- */ public class Day01 { - /** - * Perform a sonar sweep of the nearby sea floor. - * - * @return measurements of the sea floor depth further and further away from the submarine - */ - protected List getInput() { + protected Iterator getInput() { return StreamSupport .stream(new LineSpliterator("day-01.txt"), false) - .mapToInt(Integer::parseInt) - .collect(ArrayList::new, List::add, List::addAll); + .iterator(); + } + + protected List getElves() { + var calories = new ArrayList(); + final var elves = new ArrayList(); + for (final var i = getInput(); i.hasNext(); ) { + final var line = i.next(); + if (line.isBlank()) { + elves.add(new Elf(Collections.unmodifiableList(calories))); + calories = new ArrayList<>(); + } else { + calories.add(new BigInteger(line.strip())); + } + } + if (!calories.isEmpty()) { + elves.add(new Elf(Collections.unmodifiableList(calories))); + } + return Collections.unmodifiableList(elves); } @Test public final void part1() { - final var list = getInput(); - int increases = countIncreases(list); - System.out.println("Part 1: " + increases); + final var elves = getElves(); + final var elf = elves.stream() + .max(Comparator.comparing(Elf::totalCaloriesCarried)) + .get(); + + System.out.println("Part 1: " + elf.totalCaloriesCarried()); } @Test public final void part2() { - final var list = getInput(); - final var windows = new LinkedList(); - for (int i = 2; i < list.size(); i++) { - windows.add(list.get(i) + list.get(i - 1) + list.get(i - 2)); - } - final int increases = countIncreases(windows); - System.out.println("Part 2: " + increases); + final var elves = getElves(); + final var list = elves.stream() + .sorted(Comparator.comparing(Elf::totalCaloriesCarried).reversed()) + .toList(); + + System.out.println("Part 2: " + (list.get(0).totalCaloriesCarried().add(list.get(1).totalCaloriesCarried()).add(list.get(2).totalCaloriesCarried()))); } /** - * Determine how quickly the depth increases. + * An elf who collects food for the reindeer. * - * @param list progressively further measurements of the sea floor depth - * @return the number of times a depth measurement increase from the previous measurement + * @param itemCalories The number of calories of each item carried by the elf */ - protected int countIncreases(final List list) { - int previous = list.get(0); - int increases = 0; - for (int i = 1; i < list.size(); i++) { - final var current = list.get(i); - if (current > previous) { - increases++; - } - previous = current; + public record Elf(List itemCalories) { + public BigInteger totalCaloriesCarried() { + return itemCalories().stream() + .reduce(BigInteger::add) + .get(); } - return increases; } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day02.java b/src/test/java/com/macasaet/Day02.java index f5a2e9f..424b241 100644 --- a/src/test/java/com/macasaet/Day02.java +++ b/src/test/java/com/macasaet/Day02.java @@ -1,110 +1,175 @@ package com.macasaet; -import java.util.Locale; +import org.junit.jupiter.api.Test; + import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 2: Dive! --- + * --- Day 2: Rock Paper Scissors --- + * https://adventofcode.com/2022/day/2 */ public class Day02 { - public enum Operation { - FORWARD { - public Position adjust(Position position, int magnitude) { - return new Position(position.horizontalPosition() + magnitude, position.depth()); + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-02.txt"), + false) + .map(line -> { + final var components = line.strip().split(" "); + return new Round(Shape.forChar(components[0].charAt(0)), + Shape.forChar(components[1].charAt(0)), + ResponseStrategy.forChar(components[1].charAt(0))); + }); + } + + @Test + public final void part1() { + final var result = getInput().mapToInt(Round::naiveScore).sum(); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var result = getInput().mapToInt(Round::score).sum(); + + System.out.println("Part 2: " + result); + } + + /** + * A shape that a contestant can play in a round + */ + public enum Shape { + Rock { + public int score() { + return 1; + } + + public Shape beatenBy() { + return Paper; } - public OrientedPosition adjust(OrientedPosition position, int magnitude) { - return new OrientedPosition(position.horizontalPosition() + magnitude, - position.depth() + (position.aim() * magnitude), - position.aim()); + public Shape beats() { + return Scissors; } }, - DOWN { - public Position adjust(Position position, int magnitude) { - return new Position(position.horizontalPosition(), position.depth() + magnitude); + Paper { + public int score() { + return 2; } - public OrientedPosition adjust(OrientedPosition position, int magnitude) { - return new OrientedPosition(position.horizontalPosition(), - position.depth(), - position.aim() + magnitude); + public Shape beatenBy() { + return Scissors; + } + + public Shape beats() { + return Rock; } }, - UP { - public Position adjust(Position position, int magnitude) { - return new Position(position.horizontalPosition(), position.depth() - magnitude); + Scissors { + public int score() { + return 3; } - public OrientedPosition adjust(OrientedPosition position, int magnitude) { - return new OrientedPosition(position.horizontalPosition(), - position.depth(), - position.aim() - magnitude); + @Override + public Shape beatenBy() { + return Rock; } - }; - public abstract Position adjust(Position position, int magnitude); - - public abstract OrientedPosition adjust(OrientedPosition position, int magnitude); - } + public Shape beats() { + return Paper; + } + }; - public record Command(Operation operation, int magnitude) { - public static Command parse(final String string) { - final String[] components = string.split(" "); - final var operation = Operation.valueOf(components[0].toUpperCase(Locale.US)); - final int magnitude = Integer.parseInt(components[1]); - return new Command(operation, magnitude); + public static Shape forChar(final int c) { + return switch (c) { + case 'X': + case 'A': + yield Shape.Rock; + case 'Y': + case 'B': + yield Shape.Paper; + case 'Z': + case 'C': + yield Shape.Scissors; + default: + throw new IllegalArgumentException("Invalid shape: " + c); + }; } - public Position adjust(final Position position) { - return operation().adjust(position, magnitude()); - } + /** + * @return the inherent value of this shape + */ + public abstract int score(); - public OrientedPosition adjust(final OrientedPosition position) { - return operation().adjust(position, magnitude()); - } - } + /** + * @return the shape that beats this one + */ + public abstract Shape beatenBy(); - public record Position(int horizontalPosition, int depth) { - public int result() { - return horizontalPosition() * depth(); - } + /** + * @return the shape this one beats + */ + public abstract Shape beats(); } - public record OrientedPosition(int horizontalPosition, int depth, int aim) { - public int result() { - return horizontalPosition() * depth(); + /** + * An approach to responding to the shape played by the opponent + */ + public enum ResponseStrategy { + Lose { + public Shape respond(Shape opponent) { + return opponent.beats(); + } + }, + Draw { + public Shape respond(Shape opponent) { + return opponent; + } + }, + Win { + public Shape respond(Shape opponent) { + return opponent.beatenBy(); + } + }; + + public static ResponseStrategy forChar(final char c) { + return switch (c) { + case 'X' -> Lose; + case 'Y' -> Draw; + case 'Z' -> Win; + default -> throw new IllegalArgumentException("Invalid strategy: " + c); + }; } - } - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-02.txt"), - false) - .map(Command::parse); + public abstract Shape respond(final Shape opponent); } - @Test - public final void part1() { - var position = new Position(0, 0); - for (final var i = getInput().iterator(); i.hasNext(); ) { - final var operation = i.next(); - position = operation.adjust(position); + /** + * A single round of the game + * + * @param opponent The shape played by the opponent + * @param player The shape chosen based on the original (incorrect) interpretation of the responseStrategy guide + * @param responseStrategy The responseStrategy for responding to the opponent according to the responseStrategy guide + */ + public record Round(Shape opponent, Shape player, ResponseStrategy responseStrategy) { + /** + * @return the score based on the simple (incorrect) interpretation of the strategy guide + */ + public int naiveScore() { + final var outcome = opponent() == player() ? 3 : opponent().beatenBy() == player() ? 6 : 0; + return outcome + player().score(); } - System.out.println("Part 1: " + position.result()); - } - @Test - public final void part2() { - var position = new OrientedPosition(0, 0, 0); - for (final var i = getInput().iterator(); i.hasNext(); ) { - final var operation = i.next(); - position = operation.adjust(position); + /** + * @return the score based on following the strategy guide + */ + public int score() { + final var response = responseStrategy().respond(opponent()); + final var outcome = opponent() == response ? 3 : opponent().beatenBy() == response ? 6 : 0; + return outcome + response.score(); } - System.out.println("Part 2: " + position.result()); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day03.java b/src/test/java/com/macasaet/Day03.java index 47f6ec5..af6e98a 100644 --- a/src/test/java/com/macasaet/Day03.java +++ b/src/test/java/com/macasaet/Day03.java @@ -1,120 +1,117 @@ package com.macasaet; +import org.junit.jupiter.api.Test; + import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.stream.Collectors; +import java.util.Set; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 3: Binary Diagnostic --- + * --- Day 3: Rucksack Reörganisation --- + * https://adventofcode.com/2022/day/3 */ public class Day03 { - /** - * @return "a list of binary numbers which, when decoded properly, can tell you many useful things about the - * conditions of the submarine" - */ - protected Stream getDiagnosticReport() { + protected static int priority(final char c) { + if (c >= 'a' && c <= 'z') { + return c - 'a' + 1; + } + return c - 'A' + 27; + } + + protected Stream getInput() { return StreamSupport .stream(new LineSpliterator("day-03.txt"), false) - .map(string -> { - final var chars = string.toCharArray(); - final var bits = new byte[chars.length]; - for (int i = chars.length; --i >= 0; bits[i] = chars[i] == '0' ? (byte) 0 : (byte) 1) ; - return bits; - }); + .map(Rucksack::parse); } - protected int toUnsignedInt(final byte[] bits) { - int result = 0; - for (int i = bits.length; --i >= 0; result += bits[i] * Math.pow(2, bits.length - i - 1)) ; - return result; + @Test + public final void part1() { + final var result = getInput().mapToInt(Rucksack::priority).sum(); + + System.out.println("Part 1: " + result); } @Test - public final void part1() { - final var list = getDiagnosticReport().collect(Collectors.toList()); - final int width = list.get(0).length; - final int[] zeroCounts = new int[width]; - for (int i = zeroCounts.length; --i >= 0; zeroCounts[i] = 0) ; - final int[] oneCounts = new int[width]; - for (int i = oneCounts.length; --i >= 0; oneCounts[i] = 0) ; - for (final var array : list) { - for (int j = 0; j < width; j++) { - if (array[j] == 0) { - zeroCounts[j] += 1; - } else { - oneCounts[j] += 1; - } + public final void part2() { + final var groups = new ArrayList>(); + var currentGroup = new ArrayList(3); + + for (final var i = getInput().iterator(); i.hasNext(); ) { + final var rucksack = i.next(); + if (currentGroup.size() == 3) { + groups.add(Collections.unmodifiableList(currentGroup)); + currentGroup = new ArrayList<>(3); } + currentGroup.add(rucksack); } - final byte[] gammaArray = new byte[width]; - final byte[] epsilonArray = new byte[width]; - for (int i = gammaArray.length; --i >= 0; ) { - gammaArray[i] = zeroCounts[i] > oneCounts[i] ? (byte) 0 : (byte) 1; - epsilonArray[i] = zeroCounts[i] > oneCounts[i] ? (byte) 1 : (byte) 0; + if (currentGroup.size() == 3) { + groups.add(Collections.unmodifiableList(currentGroup)); } + final var result = groups.stream().map(this::getBadge).mapToInt(Day03::priority).sum(); - final int gammaRate = toUnsignedInt(gammaArray); - final int epsilonRate = toUnsignedInt(epsilonArray); - System.out.println("Part 1: " + (gammaRate * epsilonRate)); + System.out.println("Part 2: " + result); } - @Test - public final void part2() { - final var list = getDiagnosticReport().collect(Collectors.toList()); - final int width = list.get(0).length; - List oxygenCandidates = new ArrayList<>(list); - for (int i = 0; i < width && oxygenCandidates.size() > 1; i++) { - int zeros = 0; - int ones = 0; - for (final var value : oxygenCandidates) { - if (value[i] == 0) { - zeros++; - } else { - ones++; - } - } - final int index = i; - if (ones >= zeros) { - oxygenCandidates = oxygenCandidates.stream().filter(value -> value[index] == 1).collect(Collectors.toList()); - } else { - oxygenCandidates = oxygenCandidates.stream().filter(value -> value[index] == 0).collect(Collectors.toList()); + protected char getBadge(final List group) { + final var first = group.get(0); + for (final var item : first.allItems()) { + if (group.get(1).allItems().contains(item) && group.get(2).allItems().contains(item)) { + return item; } } - if (oxygenCandidates.size() > 1) { - throw new IllegalStateException("Too many oxygen candidates"); + throw new IllegalStateException(); + } + + /** + * An Elf's container of supplies for a jungle journey. "Each rucksack has two large compartments. All items of a + * given type are meant to go into exactly one of the two compartments." + * + * @param firstCompartment All the items in one compartment + * @param secondCompartment All the items in one compartment + * @param allItems All the items + */ + public record Rucksack(Set firstCompartment, Set secondCompartment, Set allItems) { + + public static Rucksack parse(final String line) { + final var chars = line.toCharArray(); + if (chars.length % 2 != 0) { + throw new IllegalArgumentException(); + } + final var firstCompartment = new HashSet(chars.length / 2); + final var secondCompartment = new HashSet(chars.length / 2); + for (int i = 0; i < chars.length / 2; i++) { + firstCompartment.add(chars[i]); + } + for (int i = chars.length / 2; i < chars.length; i++) { + secondCompartment.add(chars[i]); + } + final var union = new HashSet(chars.length); + union.addAll(firstCompartment); + union.addAll(secondCompartment); + return new Rucksack(Collections.unmodifiableSet(firstCompartment), + Collections.unmodifiableSet(secondCompartment), + Collections.unmodifiableSet(union)); } - List co2Candidates = new ArrayList<>(list); - for (int i = 0; i < width && co2Candidates.size() > 1; i++) { - int zeros = 0; - int ones = 0; - for (final var value : co2Candidates) { - if (value[i] == 0) { - zeros++; - } else { - ones++; + + public int priority() { + final var intersection = new HashSet(); + for (final char c : firstCompartment) { + if (secondCompartment.contains(c)) { + intersection.add(c); } } - final int index = i; - if (zeros <= ones) { - co2Candidates = co2Candidates.stream().filter(value -> value[index] == 0).collect(Collectors.toList()); - } else { - co2Candidates = co2Candidates.stream().filter(value -> value[index] == 1).collect(Collectors.toList()); + if (intersection.size() != 1) { + throw new IllegalStateException("There should only be one common item between compartments"); } + return Day03.priority(intersection.iterator().next()); } - if (co2Candidates.size() > 1) { - throw new IllegalStateException("Too many CO2 candidates"); - } - final byte[] oxyArray = oxygenCandidates.get(0); - final byte[] co2Array = co2Candidates.get(0); - final int oxygenGeneratorRating = toUnsignedInt(oxyArray); - final int co2ScrubberRating = toUnsignedInt(co2Array); - System.out.println("Part 2: " + (oxygenGeneratorRating * co2ScrubberRating)); - } + + } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day04.java b/src/test/java/com/macasaet/Day04.java index d69541d..8b3d13d 100644 --- a/src/test/java/com/macasaet/Day04.java +++ b/src/test/java/com/macasaet/Day04.java @@ -1,181 +1,69 @@ package com.macasaet; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import org.junit.jupiter.api.Test; + import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 4: Giant Squid --- + * --- Day 4: Camp Cleanup --- + * https://adventofcode.com/2022/day/4 */ public class Day04 { /** - * The number of rows and columns on each square Bingo card + * One crew member responsible for cleaning up the camp. They are responsible for a contiguous range of sections. + * + * @param sectionMin the lowest section ID for which this Elf is responsible (inclusive) + * @param sectionMax the highest section ID for which this Elf is responsible (inclusive). */ - static final int EDGE_LENGTH = 5; - - public static class Board { - private final int[][] grid; - private final boolean[][] marked; - - protected Board(final int[][] grid, final boolean[][] marked) { - this.grid = grid; - this.marked = marked; - } + public record Elf(int sectionMin, int sectionMax) { - public Board(final int[][] grid) { - this(grid, new boolean[grid.length][]); - for (int i = grid.length; --i >= 0; this.marked[i] = new boolean[grid.length]) ; + public boolean fullyContains(final Elf other) { + return sectionMin() <= other.sectionMin() && sectionMax() >= other.sectionMax(); } - public boolean isWinner() { - // check rows - for (int i = marked.length; --i >= 0; ) { - final var row = marked[i]; - boolean complete = true; - for (int j = row.length; --j >= 0 && complete; complete = row[j]) ; - if (complete) { - return true; - } - } - // check columns - for (int j = marked.length; --j >= 0; ) { - boolean complete = true; - for (int i = marked.length; --i >= 0 && complete; complete = marked[i][j]) ; - if (complete) { - return true; - } - } - return false; - } - - public int score(final int lastDrawn) { - int sum = 0; - for (int i = grid.length; --i >= 0; ) { - final var row = grid[i]; - for (int j = row.length; --j >= 0; ) { - if (!marked[i][j]) { - sum += row[j]; - } - } - } - return sum * lastDrawn; - } - - public void mark(final int drawn) { - for (int i = grid.length; --i >= 0; ) { - final var row = grid[i]; - for (int j = row.length; --j >= 0; ) { - if (row[j] == drawn) { - marked[i][j] = true; - } - } - } + public static Elf parse(final String string) { + final var components = string.split("-"); + return new Elf(Integer.parseInt(components[0]), Integer.parseInt(components[1])); } } - public record Game(List boards, List numbers) { - - public int countBoards() { - return boards().size(); + /** + * Two elves (of a larger crew) assigned to clean up the camp. + */ + public record Pair(Elf left, Elf right) { + public boolean oneFullyContainsTheOther() { + return left.fullyContains(right) || right.fullyContains(left); } - - public void removeBoard(final int index) { - this.boards().remove(index); + public boolean hasOverlap() { + return (left.sectionMin() <= right.sectionMin() && left.sectionMax() >= right.sectionMin()) + || (right.sectionMin() <= left.sectionMin() && right.sectionMax() >= left.sectionMin()); + } + public static Pair parse(final String line) { + final var components = line.split(","); + return new Pair(Elf.parse(components[0]), Elf.parse(components[1])); } - } - - protected Stream getInput() { + protected Stream getInput() { return StreamSupport .stream(new LineSpliterator("day-04.txt"), - false); - } - - protected Game getGame() { - final var input = getInput().iterator(); - final var moves = input.next(); - - List boards = new ArrayList<>(); - int[][] grid = null; - int gridIndex = -1; - while (input.hasNext()) { - final var line = input.next(); - if (line.isBlank()) { - if (grid != null) { - boards.add(new Board(grid)); - } - grid = new int[EDGE_LENGTH][]; - for (int i = EDGE_LENGTH; --i >= 0; grid[i] = new int[EDGE_LENGTH]) ; - gridIndex = 0; - continue; - } - final var cells = line.split("\\s"); - if (cells.length > 0) { - final var values = Arrays.stream(cells) - .filter(candidate -> !candidate.isBlank()) - .mapToInt(Integer::parseInt) - .toArray(); - if (values.length > 0) { - grid[gridIndex++] = values; - } - } - } - if (grid != null) { - boards.add(new Board(grid)); - } - final var moveArray = Arrays.stream(moves.split(",")) - .mapToInt(Integer::parseInt) - .collect(ArrayList::new, List::add, List::addAll); - return new Game(boards, moveArray); + false) + .map(Pair::parse); } @Test public final void part1() { - final var game = getGame(); - for (final var number : game.numbers()) { - for (final var board : game.boards()) { - board.mark(number); - if (board.isWinner()) { - final int score = board.score(number); - System.out.println("Part 1: " + score); - return; - } - } - } - throw new IllegalStateException("No winners"); + final var result = getInput().filter(Pair::oneFullyContainsTheOther).count(); + + System.out.println("Part 1: " + result); } @Test public final void part2() { - final var game = getGame(); - for (final var number : game.numbers()) { - if (game.countBoards() == 1) { - final var lastWinner = game.boards().get(0); - lastWinner.mark(number); - if (!lastWinner.isWinner()) { - continue; - } - System.out.println("Part 2: " + lastWinner.score(number)); - return; - } - final List idsToRemove = new ArrayList<>(); - for (int i = game.boards().size(); --i >= 0; ) { - final var board = game.boards().get(i); - board.mark(number); - if (board.isWinner()) { - idsToRemove.add(i); - } - } - for (final var id : idsToRemove) { - game.removeBoard(id); - } - } - throw new IllegalStateException("Tie for last place"); + final var result = getInput().filter(Pair::hasOverlap).count(); + + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day05.java b/src/test/java/com/macasaet/Day05.java index bb23695..97c43c4 100644 --- a/src/test/java/com/macasaet/Day05.java +++ b/src/test/java/com/macasaet/Day05.java @@ -1,188 +1,139 @@ package com.macasaet; -import java.util.function.Predicate; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.Deque; +import java.util.LinkedList; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 5: Hydrothermal Venture --- + * --- Day 5: Supply Stacks --- + * https://adventofcode.com/2022/day/5 */ public class Day05 { - /** - * A point on the ocean floor - */ - public static record Point(int x, int y) { - public static Point parse(final String string) { - final var components = string.split(","); - return new Point(Integer.parseInt(components[0]), Integer.parseInt(components[1])); + public record CrateMover9000Instruction(int count, int from, int to) { + public static CrateMover9000Instruction parse(final String line) { + final var components = line.split(" "); + final var count = Integer.parseInt(components[1]); + final var from = Integer.parseInt(components[3]) - 1; + final var to = Integer.parseInt(components[5]) - 1; + return new CrateMover9000Instruction(count, from, to); } - - } - - /** - * A portion of the ocean floor on which there are hydrothermal vents, which produce large, opaque clouds - */ - public static record LineSegment(Point start, Point end) { - public static LineSegment parse(final String string) { - final var components = string.split(" -> "); - return new LineSegment(Point.parse(components[0]), Point.parse(components[1])); - } - - /** - * Highlight the location of this line segment on the diagram. Each cell that this segment covers will have its - * value incremented. The higher the number, the more vents cover the cell. - * - * @param diagram A map of the ocean floor which will be updated by this call. - */ - public void update(final int[][] diagram) { - /* - "Because of the limits of the hydrothermal vent mapping system, the lines in your list will only ever be - horizontal, vertical, or a diagonal line at exactly 45 degrees." - */ - final int horizontalStep = start().x() == end().x() - ? 0 - : start().x() < end().x() - ? 1 - : -1; - final int verticalStep = start().y() == end().y() - ? 0 - : start().y() < end().y() - ? 1 - : -1; - final Predicate xTester = start().x() == end().x() - ? x -> true - : start().x() < end().x() - ? x -> x <= end().x() - : x -> x >= end().x(); - final Predicate yTester = start().y() == end().y() - ? y -> true - : start().y() < end().y() - ? y -> y <= end().y() - : y -> y >= end().y(); - - for (int i = start().x(), j = start().y(); - xTester.test(i) && yTester.test(j); - i += horizontalStep, j += verticalStep) { - diagram[i][j]++; + public void execute(final Deque[] columns) { + final Deque from = columns[from()]; + final Deque to = columns[to()]; + for(int i = count(); --i >= 0; ) { + to.push(from.pop()); } } + } - public int lowestX() { - return Math.min(start().x(), end().x()); - } - - public int highestX() { - return Math.max(start().x(), end().x()); - } - - public int lowestY() { - return Math.min(start().y(), end().y()); - } - - public int highestY() { - return Math.max(start().y(), end().y()); + public record CrateMover9001Instruction(int count, int from, int to) { + public static CrateMover9001Instruction parse(final String line) { + final var components = line.split(" "); + final var count = Integer.parseInt(components[1]); + final var from = Integer.parseInt(components[3]) - 1; + final var to = Integer.parseInt(components[5]) - 1; + return new CrateMover9001Instruction(count, from, to); } - - public String toString() { - return start() + " -> " + end(); + public void execute(final Deque[] columns) { + final Deque from = columns[from()]; + final Deque to = columns[to()]; + final var buffer = new LinkedList(); + for(int i = count(); --i >= 0; ) { + buffer.push(from.pop()); + } + while(!buffer.isEmpty()) { + to.push(buffer.pop()); + } } } - - protected Stream getInput() { + protected Stream getInput() { return StreamSupport .stream(new LineSpliterator("day-05.txt"), - false) - .map(LineSegment::parse); - } - - protected record Extremes(int lowestX, int lowestY, int highestX, int highestY) { - public Extremes combine(final LineSegment segment) { - return new Extremes(Math.min(lowestX(), segment.lowestX()), - Math.min(lowestY(), segment.lowestY()), - Math.max(highestX(), segment.highestX()), - Math.max(highestY(), segment.highestY())); - } - - public Extremes combine(final Extremes other) { - return new Extremes(Math.min(lowestX(), other.lowestX()), - Math.min(lowestY(), other.lowestY()), - Math.max(highestX(), other.highestX()), - Math.max(highestY(), other.highestY())); - } - - public int[][] createBlankDiagram() { - final int[][] result = new int[highestX() + 1][]; - for (int i = result.length; --i >= 0; result[i] = new int[highestY() + 1]) ; - return result; - } + false); } @Test public final void part1() { - final var segments = getInput() - // "For now, only consider horizontal and vertical lines: lines where either x1 = x2 or y1 = y2." - .filter(segment -> segment.start().x() == segment.end().x() || segment.start().y() == segment.end().y()) - .collect(Collectors.toList()); - - final var extremes = segments - .stream() - .reduce(new Extremes(0, 0, 0, 0), - Extremes::combine, - Extremes::combine); - // there are no negative values - // Note, we could save a little bit of space and time by using a smaller map since none of the line segments - // need point 0,0. However, the savings are likely negligible. - final int[][] diagram = extremes.createBlankDiagram(); - - for (final var segment : segments) { - segment.update(diagram); - } - int sum = 0; - for (int i = diagram.length; --i >= 0; ) { - final var row = diagram[i]; - for (int j = row.length; --j >= 0; ) { - if (row[j] >= 2) { - sum++; + int mode = 0; + final Deque[] columns = new Deque[9]; + for(int i = columns.length; --i >= 0; columns[i] = new LinkedList<>()); + for(final var line : getInput().toList()) { + if(line.isBlank()) { + mode = 1; + } + if( mode == 0 ) { + final var chars = line.toCharArray(); + int index = -1; + for(int i = 0; i < chars.length; i++) { + if(chars[i] == '[') { + index = i / 4; + } else if(index >= 0) { + columns[index].addLast(chars[i]); + index = -1; + } + } + } else { + if(line.isBlank()) { + continue; } + final var instruction = CrateMover9000Instruction.parse(line); + instruction.execute(columns); } } - System.out.println("Part 1: " + sum); + final var builder = new StringBuilder(); + for(final var column : columns) { + if(!column.isEmpty()) { + builder.append(column.getFirst()); + } + } + final var result = builder.toString(); + + System.out.println("Part 1: " + result); } @Test public final void part2() { - /* - "Unfortunately, considering only horizontal and vertical lines doesn't give you the full picture; you need to - also consider diagonal lines." - */ - final var segments = getInput() - .collect(Collectors.toList()); - final var extremes = segments - .stream() - .reduce(new Extremes(0, 0, 0, 0), - Extremes::combine, - Extremes::combine); - // there are no negative values - // Note, we could save a little bit of space and time by using a smaller map since none of the line segments - // need point 0,0. However, the savings are likely negligible. - final int[][] diagram = extremes.createBlankDiagram(); - for (final var segment : segments) { - segment.update(diagram); - } - int sum = 0; - for (int i = diagram.length; --i >= 0; ) { - final var row = diagram[i]; - for (int j = row.length; --j >= 0; ) { - if (row[j] >= 2) { - sum++; + int mode = 0; + final Deque[] columns = new Deque[9]; + for(int i = columns.length; --i >= 0; columns[i] = new LinkedList<>()); + for(final var line : getInput().toList()) { + if(line.isBlank()) { + mode = 1; + } + if( mode == 0 ) { + final var chars = line.toCharArray(); + int index = -1; + for(int i = 0; i < chars.length; i++) { + if(chars[i] == '[') { + index = i / 4; + } else if(index >= 0) { +// System.err.println("column[ " + index + " ].addLast( " + chars[ i ] + " )" ); + columns[index].addLast(chars[i]); + index = -1; + } } + } else { + if(line.isBlank()) { + continue; + } + final var instruction = CrateMover9001Instruction.parse(line); + instruction.execute(columns); + } + } + final var builder = new StringBuilder(); + for(final var column : columns) { + if(!column.isEmpty()) { + builder.append(column.getFirst()); } } - System.out.println("Part 2: " + sum); + final var result = builder.toString(); + + System.out.println("Part 2: " + result); + } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day06.java b/src/test/java/com/macasaet/Day06.java index 10a59b3..769fb1b 100644 --- a/src/test/java/com/macasaet/Day06.java +++ b/src/test/java/com/macasaet/Day06.java @@ -1,120 +1,55 @@ package com.macasaet; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 6: Lanternfish --- + * --- Day 6: --- + * https://adventofcode.com/2022/day/6 */ public class Day06 { - /** - * A glowing fish that spawns very quickly. Their population grows exponentially. - */ - public static class Lanternfish { - - private int daysToSpawn; - - /** - * @param daysToSpawn the number of days until it creates a new {@link Lanternfish} - */ - public Lanternfish(final int daysToSpawn) { - setDaysToSpawn(daysToSpawn); - } - - /** - * Simulate the passage of one day - * - * @return either a new Lanternfish or nothing depending on whether the fish spawned - */ - public Optional tick() { - final var timer = getDaysToSpawn() - 1; - if (timer < 0) { - setDaysToSpawn(6); - return Optional.of(new Lanternfish(8)); - } else { - setDaysToSpawn(timer); - return Optional.empty(); - } - } - - /** - * @return the number of days until the fish spawns - */ - public int getDaysToSpawn() { - return this.daysToSpawn; - } - - /** - * Update this fish's days to spawn - * - * @param daysToSpawn the number of days until the fish spawns, must be non-negative - */ - protected void setDaysToSpawn(final int daysToSpawn) { - if (daysToSpawn < 0) { - throw new IllegalArgumentException("\"days to spawn\" must be non-negative"); - } - this.daysToSpawn = daysToSpawn; - } - } - protected Stream getInput() { return StreamSupport .stream(new LineSpliterator("day-06.txt"), false); } - public List parseInput() { - final var list = getInput().toList(); - final var line = list.get(0); - final var components = line.split(","); - return Arrays.stream(components) - .mapToInt(Integer::parseInt) - .mapToObj(Lanternfish::new) - .collect(Collectors.toList()); - } - @Test public final void part1() { - var population = parseInput(); - for (int _i = 80; --_i >= 0; ) { - final List list = new ArrayList<>(population); - for (final var fish : population) { - final var result = fish.tick(); - result.ifPresent(list::add); + final var input = getInput().findFirst().get(); + for(int i = 4; i < input.length(); i++) { + final var set = new HashSet(4); + for(int j = 0; j < 4; j++) { + set.add(input.charAt(i - 4 + j)); + } + if(set.size() >= 4) { + final var result = i; + System.out.println("Part 1: " + result); + return; } - population = list; } - System.out.println("Part 1: " + population.size()); + throw new IllegalStateException(); } @Test public final void part2() { - final var initial = parseInput(); - var map = new long[9]; - for (final var fish : initial) { - map[fish.getDaysToSpawn()]++; - } - for (int _i = 256; --_i >= 0; ) { - final var temp = new long[map.length]; - for (int daysToSpawn = map.length; --daysToSpawn >= 0; ) { - final var count = map[daysToSpawn]; - final var prototype = new Lanternfish(daysToSpawn); - final var result = prototype.tick(); - temp[prototype.getDaysToSpawn()] += count; - result.ifPresent(spawn -> temp[spawn.getDaysToSpawn()] = temp[spawn.getDaysToSpawn()] + count); + final var input = getInput().findFirst().get(); + for(int i = 14; i < input.length(); i++) { + final var set = new HashSet(14); + for(int j = 0; j < 14; j++) { + set.add(input.charAt(i - 14 + j)); + } + if(set.size() >= 14) { + final var result = i; + System.out.println("Part 2: " + result); + return; } - map = temp; } - final var result = Arrays.stream(map).reduce(0L, Long::sum); - System.out.println("Part 2: " + result); + throw new IllegalStateException(); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day07.java b/src/test/java/com/macasaet/Day07.java index 4393d46..5aee1bc 100644 --- a/src/test/java/com/macasaet/Day07.java +++ b/src/test/java/com/macasaet/Day07.java @@ -1,112 +1,240 @@ package com.macasaet; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 7: The Treachery of Whales --- + * --- Day 7: --- + * https://adventofcode.com/2022/day/7 */ public class Day07 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-07.txt"), - false); + static class Session { + private final Directory root = new Directory("/", new HashMap<>()); + private Directory workingDirectory = root; + private final Map parentMap = new HashMap<>(); + } + + static abstract class File { + abstract int size(); + } + + static class Directory extends File { + private final String name; + private final Map files; + + public Directory(String name, Map files) { + this.name = name; + this.files = files; + } + + int size() { + int result = 0; + for(final var file : files.values()) { + result += file.size(); + } + return result; + } + + public String toString() { + return "Directory{" + + "name='" + name + '\'' + + '}'; + } + + final Set findDirectoriesSmallerThan(final int maxSize) { + final var result = new HashSet(); + for(final var file : files.values()) { + try { + final var directory = (Directory)file; + result.addAll(directory.findDirectoriesSmallerThan(maxSize)); + } catch(final ClassCastException ignored) { + } + } + if(size() < maxSize) { // FIXME duplicated traversal + result.add(this); + } + return Collections.unmodifiableSet(result); + } + final Set findDirectoriesLargerThan(final int minSize) { + final var result = new HashSet(); + if(size() >= minSize) { + for (final var file : files.values()) { // FIXME duplicated traversal + try { + final var directory = (Directory) file; + result.addAll(directory.findDirectoriesLargerThan(minSize)); + } catch (final ClassCastException ignored) { + } + } + result.add(this); + } + return Collections.unmodifiableSet(result); + } + } + + static class Leaf extends File { + private final String name; + private final int size; + + public Leaf(String name, int size) { + this.name = name; + this.size = size; + } + + int size() { + return size; + } + + @Override + public String toString() { + return "Leaf{" + + "name='" + name + '\'' + + ", size=" + size + + '}'; + } + } + + static abstract class Line { + static Line parse(final String line) { + if(line.startsWith("$")) { + return Command.parse(line); + } + return Output.parse(line); + } + abstract void execute(Session session); + } + + static abstract class Command extends Line { + static Command parse(final String line) { + if(line.startsWith("$ cd")) { + return ChangeDirectory.parse(line); + } + return ListContents.parse(line); + } } - /** - * @return the horizontal position of each crab submarine in the swarm - */ - protected List getCrabPositions() { - final var list = getInput().collect(Collectors.toList()); - final var line = list.get(0); - return Arrays.stream(line.split(",")) - .mapToInt(Integer::parseInt) - .collect(ArrayList::new, List::add, List::addAll); + static class ListContents extends Command { + void execute(final Session session) { + } + static Command parse(final String ignored) { + return new ListContents(); + } } - /** - * Assuming a constant fuel consumption rate, calculate the fuel required for the swarm to reach alignmentPoint. - * - * @param positions the starting position of each crab submarine - * @param alignmentPoint a potential point for the crabs to gather in order to blast a hole in the ocean floor - * @return the fuel required to reach the alignment point - */ - protected int calculateConstantFuel(final Iterable positions, final int alignmentPoint) { - int sum = 0; - for (final var position : positions) { - sum += Math.abs(alignmentPoint - position); - } - return sum; + static class ChangeDirectory extends Command { + private final String argument; + + public ChangeDirectory(String argument) { + this.argument = argument; + } + + void execute(Session session) { + if("..".equals(argument)) { + final var parent = session.parentMap.get(session.workingDirectory); + if(parent == null) { + throw new IllegalArgumentException("Working directory has no parent: " + session.workingDirectory); + } + session.workingDirectory = parent; + } else if( "/".equals(argument)) { + session.workingDirectory = session.root; + } else { + final var target = (Directory) session.workingDirectory.files.get(argument); + if(target == null) { + throw new IllegalArgumentException("No directory named \"" + argument + "\" inside \"" + session.workingDirectory + "\""); + } + session.workingDirectory = target; + } + } + + static ChangeDirectory parse(final String line) { + return new ChangeDirectory(line.split(" ")[2]); + } } - /** - * Calculate the fuel required for the swarm to reach alignmentPoint - * - * @param positions the starting position for each crab submarine - * @param alignmentPoint a potential point for the crabs to gather in order to blast a hole in the ocean floor - * @return the fuel required to reach the alignment point - */ - protected int calculateFuel(final Iterable positions, final int alignmentPoint) { - int sum = 0; - for (final var position : positions) { - sum += calculateFuel(position, alignmentPoint); - } - return sum; + static abstract class Output extends Line { + static Output parse(final String line) { + if(line.startsWith("dir")) { + return DirectoryListing.parse(line); + } + return LeafListing.parse(line); + } } - /** - * Calculate the fuel required for a single crab submarine to travel from one horizontal position to the next. - * - * @param start the starting position (inclusive) - * @param end the ending position (inclusive) - * @return the amount of fuel consumed in the journey - */ - protected int calculateFuel(final int start, final int end) { - final int target = Math.abs(end - start); - int sum = 0; - for (int i = target; --i >= 0; ) { - sum += i + 1; - } - return sum; + static class DirectoryListing extends Output { + private final String name; + + public DirectoryListing(String name) { + this.name = name; + } + + void execute(Session session) { + final var directory = new Directory(name, new HashMap<>()); + session.parentMap.put(directory, session.workingDirectory); // TODO method on Session + session.workingDirectory.files.put(name, directory); + } + + static DirectoryListing parse(final String line) { + final var components = line.split(" "); + return new DirectoryListing(components[1]); + } + } + + static class LeafListing extends Output { + private final int size; + private final String name; + + public LeafListing(int size, String name) { + this.size = size; + this.name = name; + } + + void execute(Session session) { + session.workingDirectory.files.put(name, new Leaf(name, size)); + } + + static LeafListing parse(final String line) { + final var components = line.split(" "); + final var size = Integer.parseInt(components[0]); + final var name = components[1]; + return new LeafListing(size, name); + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-07.txt"), + false) + .map(Line::parse); } @Test public final void part1() { - int min = Integer.MAX_VALUE; - int max = Integer.MIN_VALUE; - final var positions = getCrabPositions(); - for (final var position : positions) { - min = Math.min(min, position); - max = Math.max(max, position); - } - final int result = IntStream.range(min, max) - .map(alignmentPoint -> calculateConstantFuel(positions, alignmentPoint)) - .min() - .getAsInt(); + final var session = new Session(); + getInput().forEach(line -> line.execute(session)); + final var result = session.root.findDirectoriesSmallerThan(100_000).stream().mapToInt(Directory::size).sum(); System.out.println("Part 1: " + result); } @Test public final void part2() { - int min = Integer.MAX_VALUE; - int max = Integer.MIN_VALUE; - final var positions = getCrabPositions(); - for (final var position : positions) { - min = Math.min(min, position); - max = Math.max(max, position); - } - final int result = IntStream.range(min, max) - .map(alignmentPoint -> calculateFuel(positions, alignmentPoint)) - .min() - .getAsInt(); + final var session = new Session(); + getInput().forEach(line -> line.execute(session)); + final var consumed = session.root.size(); + final var unused = 70_000_000 - consumed; + final var required = 30_000_000 - unused; + final var result = session.root.findDirectoriesLargerThan(required) + .stream() + .min(Comparator.comparing(Directory::size)) + .get() + .size(); System.out.println("Part 2: " + result); } diff --git a/src/test/java/com/macasaet/Day08.java b/src/test/java/com/macasaet/Day08.java index 7ac8b58..98c498e 100644 --- a/src/test/java/com/macasaet/Day08.java +++ b/src/test/java/com/macasaet/Day08.java @@ -1,224 +1,160 @@ package com.macasaet; -import java.util.*; +import org.junit.jupiter.api.Test; + import java.util.stream.Collectors; -import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 8: Seven Segment Search --- + * --- Day 8: Treetop Tree House --- + * https://adventofcode.com/2022/day/8 */ public class Day08 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-08.txt"), - false); - } - - public record Digit(List segments) { - public Digit decode(final Map map) { - return new Digit(segments().stream() - .map(map::get) - .collect(Collectors.toUnmodifiableList())); - } - - public int asInt() { - if (segments().size() == 6 && hasSegments('a', 'b', 'c', 'e', 'f', 'g')) { // FIXME use on/off mechanism - return 0; - } else if (segments().size() == 2 && hasSegments('c', 'f')) { - return 1; - } else if (segments().size() == 5 && hasSegments('a', 'c', 'd', 'e', 'g')) { - return 2; - } else if (segments().size() == 5 && hasSegments('a', 'c', 'd', 'f', 'g')) { - return 3; - } else if (segments().size() == 4 && hasSegments('b', 'c', 'd', 'f')) { - return 4; - } else if (segments().size() == 5 && hasSegments('a', 'b', 'd', 'f', 'g')) { - return 5; - } else if (segments().size() == 6 && hasSegments('a', 'b', 'd', 'e', 'f', 'g')) { - return 6; - } else if (segments().size() == 3 && hasSegments('a', 'c', 'f')) { - return 7; - } else if (segments().size() == 7 && hasSegments('a', 'b', 'c', 'd', 'e', 'f', 'g')) { - return 8; - } else if (segments().size() == 6 && hasSegments('a', 'b', 'c', 'd', 'f', 'g')) { - return 9; + record Forest(int[][] grid) { + public int countVisible() { + int result = 0; + for(int i = grid().length; --i >= 0; ) { + final var row = grid()[i]; + for(int j = row.length; --j >= 0; ) { + if(isVisible(i, j)) { + result++; + } + } } - throw new IllegalStateException("Invalid Digit: " + this); + return result; } - public boolean hasSegments(final char... segments) { - for (final var segment : segments) { - if (!hasSegment(segment)) { - return false; + public int scenicScore(final int x, final int y) { + final int treeHeight = grid()[x][y]; + int northScore = 0; + for(int i = x; --i >= 0; ) { + final var height = grid()[i][y]; + northScore += 1; + if(height >= treeHeight) { + break; } } - return true; - } - - public boolean hasSegment(final char segment) { - return segments().contains(segment); - } - - public static Digit parse(final String string) { - final var array = string.toCharArray(); - final var list = new ArrayList(array.length); - for (final var c : array) { - list.add(c); + int southScore = 0; + for(int i = x + 1; i < grid().length; i++) { + final var height = grid()[i][y]; + southScore += 1; + if(height >= treeHeight) { + break; + } } - return new Digit(Collections.unmodifiableList(list)); - } - } - - public static record Entry(List uniqueSignalPatterns, List outputValue) { - - public int decodeOutput() { - final var map = buildDecodingMap(); - final StringBuilder builder = new StringBuilder(); - for (final var outputDigit : outputValue()) { - final var decodedDigit = outputDigit.decode(map); - final int digit = decodedDigit.asInt(); - builder.append(digit); + int westScore = 0; + for(int j = y; --j >= 0; ) { + final var height = grid()[x][j]; + westScore += 1; + if(height >= treeHeight) { + break; + } } - final String stringInt = builder.toString(); - return Integer.parseInt(stringInt); - } - - protected SortedSet getDigitSegmentsWithCount(final int n) { - return uniqueSignalPatterns().stream() - .filter(digit -> digit.segments().size() == n) - .findFirst() - .get() - .segments() - .stream() - .collect(TreeSet::new, SortedSet::add, SortedSet::addAll); - } - - protected Set getDigitsWithCount(final int n) { // TODO return stream - return uniqueSignalPatterns() - .stream() - .filter(digit -> digit.segments().size() == n).collect(Collectors.toUnmodifiableSet()); - } - - public Map buildDecodingMap() { - final var encodingMap = buildEncodingMap(); - final var result = new HashMap(); - for(final var entry : encodingMap.entrySet()) { - result.put(entry.getValue(), entry.getKey()); + int eastScore = 0; + for(int j = y + 1; j < grid()[x].length; j++) { + final var height = grid()[x][j]; + eastScore += 1; + if(height >= treeHeight) { + break; + } } - return Collections.unmodifiableMap(result); + return northScore * eastScore * southScore * westScore; } - public Map buildEncodingMap() { - final var map = new HashMap(); - final var oneSegments = getDigitSegmentsWithCount(2); - final var sevenSegments = getDigitSegmentsWithCount(3); - final var fourSegments = getDigitSegmentsWithCount(4); - final var eightSegments = getDigitSegmentsWithCount(7); - final var aMapping = sevenSegments.stream().filter(c -> !oneSegments.contains(c)).findFirst().get(); - map.put('a', aMapping); - - final var zeroSixNine = getDigitsWithCount(6); - var zsnSegments = zeroSixNine.stream().flatMap(digit -> digit.segments().stream()).collect(Collectors.toList()); - zsnSegments.removeIf(sevenSegments::contains); - zsnSegments.removeIf(fourSegments::contains); - final var sssMap = new HashMap(); - for (final var c : zsnSegments) { - sssMap.compute(c, (_key, old) -> old == null ? 1 : old + 1); + boolean isVisible(final int x, final int y) { + if(x == 0 || x == grid().length || y == 0 || y == grid()[x].length) { + // trees on the edge + return true; } - if(sssMap.size() != 2) { - throw new IllegalStateException("More segments for 0, 6, 9 encountered: " + sssMap); + final int treeHeight = grid()[x][y]; + if (!isObstructedFromTheNorth(x, y, treeHeight)) { + return true; } - for (final var entry : sssMap.entrySet()) { - if (entry.getValue() == 3) { - map.put('g', entry.getKey()); - } else if (entry.getValue() == 2) { - map.put('e', entry.getKey()); - } else { - throw new IllegalStateException(); - } + if (!isObstructedFromTheSouth(x, y, treeHeight)) { + return true; } - - final var twoFiveThree = getDigitsWithCount(5); - var tftSegments = twoFiveThree.stream().flatMap(digit -> digit.segments.stream()).collect(Collectors.toList()); - tftSegments.removeIf(sevenSegments::contains); - tftSegments.removeIf(candidate -> candidate.equals(map.get('e'))); - tftSegments.removeIf(candidate -> candidate.equals(map.get('g'))); - final var tftCounts = new HashMap(); - for(final var c : tftSegments) { - tftCounts.compute(c, (_key, old) -> old == null ? 1 : old + 1); + if (!isObstructedFromTheWest(x, y, treeHeight)) { + return true; } - for(final var entry : tftCounts.entrySet()) { - if(entry.getValue() == 3) { - map.put('d', entry.getKey()); - } else if(entry.getValue() == 1) { - map.put('b', entry.getKey()); - } else { - throw new IllegalStateException(); - } + if (!isObstructedFromTheEast(x, y, treeHeight)) { + return true; } + return false; + } - zsnSegments = zeroSixNine.stream().flatMap(digit -> digit.segments().stream()).collect(Collectors.toList()); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('a'))); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('b'))); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('d'))); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('e'))); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('g'))); - final var zsnCounts = new HashMap(); - for(final var c : zsnSegments) { - zsnCounts.compute(c, (_key, old) -> old == null ? 1 : old + 1); - } - for(final var entry : zsnCounts.entrySet()) { - if(entry.getValue() == 2) { - map.put('c', entry.getKey()); - } else if( entry.getValue() == 3) { - map.put('f', entry.getKey()); - } else { - throw new IllegalStateException(); + private boolean isObstructedFromTheEast(int x, int y, int treeHeight) { + for(int j = grid()[x].length; --j > y; ) { + if(grid()[x][j] >= treeHeight) { + return true; } } + return false; + } - return map; + private boolean isObstructedFromTheWest(int x, int y, int treeHeight) { + for(int j = y; --j >= 0; ) { + if(grid()[x][j] >= treeHeight) { + return true; + } + } + return false; } - public static Entry parse(final String string) { - final var components = string.split(" \\| "); - final var uniqueSignalPatterns = components[0].split(" "); - final var outputValue = components[1].split(" "); + private boolean isObstructedFromTheSouth(int x, int y, int treeHeight) { + for(int i = grid().length; --i > x; ) { + if(grid()[i][y] >= treeHeight) { + return true; + } + } + return false; + } - return new Entry(Arrays.stream(uniqueSignalPatterns) - .map(Digit::parse) - .collect(Collectors.toUnmodifiableList()), - Arrays.stream(outputValue) - .map(Digit::parse) - .collect(Collectors.toUnmodifiableList())); + private boolean isObstructedFromTheNorth(int x, int y, int treeHeight) { + for(int i = x; --i >= 0; ) { + if(grid()[i][y] >= treeHeight) { + return true; + } + } + return false; } + } + protected Forest getInput() { + final var list = StreamSupport + .stream(new LineSpliterator("day-08.txt"), + false) + .map(line -> { + final var chars = line.toCharArray(); + final var row = new int[chars.length]; + for(int i = chars.length; --i >= 0; row[i] = chars[i] - '0'); + return row; + }) + .collect(Collectors.toList()); + final var grid = new int[list.size()][]; + for(int i = list.size(); --i >= 0; grid[i] = list.get(i)); + return new Forest(grid); } @Test public final void part1() { - final var result = getInput() - .map(Entry::parse) - .flatMap(entry -> entry.outputValue().stream()) - .filter(digit -> { - final var segments = digit.segments(); - final var numSegments = segments.size(); - return numSegments == 2 || numSegments == 4 || numSegments == 3 || numSegments == 7; - }) - .count(); + final var forest = getInput(); + final var result = forest.countVisible(); System.out.println("Part 1: " + result); } @Test public final void part2() { - final var result = getInput() - .map(Entry::parse) - .mapToInt(Entry::decodeOutput).sum(); - + final var forest = getInput(); + int result = Integer.MIN_VALUE; + for(int i = forest.grid().length; --i >= 0; ) { + for( int j = forest.grid.length; --j >= 0; ) { + final var score = forest.scenicScore(i, j); + if(score > result) { + result = score; + } + } + } System.out.println("Part 2: " + result); } diff --git a/src/test/java/com/macasaet/Day09.java b/src/test/java/com/macasaet/Day09.java index abd2f4c..b6bcb00 100644 --- a/src/test/java/com/macasaet/Day09.java +++ b/src/test/java/com/macasaet/Day09.java @@ -1,176 +1,168 @@ package com.macasaet; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; +import org.junit.jupiter.api.Test; + +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.TreeSet; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 9: Smoke Basin --- + * --- Day 9: Rope Bridge --- + * https://adventofcode.com/2022/day/9 */ public class Day09 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-09.txt"), - false); - } - - public HeightMap getHeightMap() { - final var list = getInput().map(line -> { - final var chars = line.toCharArray(); - final var ints = new int[chars.length]; - for (int i = chars.length; --i >= 0; ints[i] = chars[i] - '0') ; - return ints; - }).collect(Collectors.toList()); - final int[][] grid = new int[list.size()][]; - for (int i = list.size(); --i >= 0; grid[i] = list.get(i)) ; - return new HeightMap(grid); - } + record Coordinate(int x, int y) { - /** - * A height map of the floor of the nearby caves generated by the submarine - */ - public record HeightMap(int[][] grid) { // FIXME use bytes + public int distance(final Coordinate other) { + return (int)Math.sqrt(Math.pow((double)x() - (double)other.x(), 2.0) + Math.pow((double)y() - (double)other.y(), 2.0)); + } - public Stream points() { - return IntStream.range(0, grid().length) - .boxed() - .flatMap(i -> IntStream.range(0, grid()[i].length) - .mapToObj(j -> new Point(i, j))); + public Coordinate step(final int xDistance, final int yDistance) { + return new Coordinate(x() + xDistance, y + yDistance); } + public Coordinate stepTowards(final Coordinate leader) { + final var xDistance = Integer.compare(leader.x(), x()); + final var yDistance = Integer.compare(leader.y(), y()); + return step(xDistance, yDistance); + } + } - /** - * A location on the floor of a nearby cave - */ - public class Point { - final int x; - final int y; + public static class Rope { - public Point(final int x, final int y) { - this.x = x; - this.y = y; - } + Coordinate[] knotCoordinates; - public int x() { - return this.x; - } + final SortedMap> visited = new TreeMap<>(); - public int y() { - return this.y; - } + public Rope(final int knots) { + knotCoordinates = new Coordinate[knots]; + for(int i = knots; --i >= 0; knotCoordinates[i] = new Coordinate(0, 0)); + visited.computeIfAbsent(0, (key) -> new TreeSet<>()).add(0); + } - public int getBasinSize() { - return getBasinPoints().size(); + public int countVisited() { + int result = 0; + for( final var map : visited.values() ) { + result += map.size(); } + return result; + } - /** - * Identify all the higher points that are also part of the same basin, assuming this location is part of a - * basin. - * - * @return all the higher points, if any, that are part of the same basin. - */ - public Set getBasinPoints() { - if (getHeight() >= 9) { - return Collections.emptySet(); + public void process(final Instruction instruction) { + final int xStep = instruction.direction().xStep(); + final int yStep = instruction.direction().yStep(); + + for(int i = instruction.distance(); --i >= 0; ) { + knotCoordinates[0] = knotCoordinates[0].step(xStep, yStep); + for(int j = 1; j < knotCoordinates.length; j++) { + moveKnot(j); } - final var result = new HashSet(); - result.add(this); - final Function> basinPointRetriever = neighbour -> { - if (neighbour.getHeight() >= 9 || neighbour.getHeight() <= getHeight() || result.contains(neighbour)) { - return Stream.empty(); - } - return neighbour.getBasinPoints().stream(); - }; - above().stream().flatMap(basinPointRetriever).forEach(result::add); - below().stream().flatMap(basinPointRetriever).forEach(result::add); - left().stream().flatMap(basinPointRetriever).forEach(result::add); - right().stream().flatMap(basinPointRetriever).forEach(result::add); - return Collections.unmodifiableSet(result); } + } - /** - * @return true if and only if this location is lower than all of its adjacent locations (up to four, - * diagonals do not count) - */ - public boolean isLowPoint() { - final var compareTo = new ArrayList(4); - above().ifPresent(compareTo::add); - below().ifPresent(compareTo::add); - left().ifPresent(compareTo::add); - right().ifPresent(compareTo::add); - return compareTo.stream().allMatch(neighbour -> neighbour.getHeight() > getHeight()); + protected void moveKnot(int knotIndex) { + if(knotIndex <= 0) { + throw new IllegalArgumentException("Cannot move head"); } + final var leader = knotCoordinates[knotIndex - 1]; + var follower = knotCoordinates[knotIndex]; - /** - * @return an assessment of the risk from smoke flowing through the cave - */ - public int getRiskLevel() { - return getHeight() + 1; + if(leader.equals(follower)) { + return; + } else if (leader.distance(follower) <= 1) { + return; } - /** - * @return the height of this particular location, from 0-9 - */ - public int getHeight() { - return grid()[x()][y()]; - } + follower = follower.stepTowards(leader); + knotCoordinates[knotIndex] = follower; - public Optional above() { - return x() > 0 ? Optional.of(new Point(x() - 1, y())) : Optional.empty(); + if(knotIndex == knotCoordinates.length - 1) { + visited.computeIfAbsent(follower.x(), (key) -> new TreeSet<>()).add(follower.y()); } + } - public Optional below() { - return x() < grid().length - 1 ? Optional.of(new Point(x() + 1, y())) : Optional.empty(); - } + } - public Optional left() { - return y() > 0 ? Optional.of(new Point(x(), y() - 1)) : Optional.empty(); + enum Direction { + Up { + int xStep() { + return -1; } - - public Optional right() { - return y() < grid()[x()].length - 1 ? Optional.of(new Point(x(), y() + 1)) : Optional.empty(); + int yStep() { + return 0; } - - public int hashCode() { - return Objects.hash(x(), y()); + }, + Down { + int xStep() { + return 1; } - - public boolean equals(final Object o) { - try { - final Point other = (Point) o; - return this.x() == other.x() && this.y() == other.y(); - } catch (final ClassCastException cce) { - return false; - } + int yStep() { + return 0; + } + }, + Left { + int xStep() { + return 0; } + int yStep() { + return -1; + } + }, + Right { + int xStep() { + return 0; + } + int yStep() { + return 1; + } + }; + + abstract int xStep(); + abstract int yStep(); + + static Direction parse(final String string) { + return switch(string.trim()) { + case "U" -> Up; + case "D" -> Down; + case "L" -> Left; + case "R" -> Right; + default -> throw new IllegalArgumentException("Invalid direction: " + string); + }; } + } + record Instruction(Direction direction, int distance) { + static Instruction parse(final String string) { + final var components = string.split(" "); + final var direction = Direction.parse(components[0]); + final var distance = Integer.parseInt(components[1]); + return new Instruction(direction, distance); + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-09.txt"), + false) + .map(Instruction::parse); } @Test public final void part1() { - final var map = getHeightMap(); - final int sum = map.points() - .filter(HeightMap.Point::isLowPoint) - .mapToInt(HeightMap.Point::getRiskLevel) - .sum(); - System.out.println("Part 1: " + sum); + final var rope = new Rope(2); + getInput().forEach(rope::process); + final var result = rope.countVisited(); + System.out.println("Part 1: " + result); } @Test public final void part2() { - final var map = getHeightMap(); - final var basinSizes = map.points() - .filter(HeightMap.Point::isLowPoint) - .mapToInt(HeightMap.Point::getBasinSize) - .collect(() -> new TreeSet(Comparator.reverseOrder()), SortedSet::add, SortedSet::addAll); - final var iterator = basinSizes.iterator(); - final var result = iterator.next() * iterator.next() * iterator.next(); + final var rope = new Rope(10); + getInput().forEach(rope::process); + final var result = rope.countVisited(); System.out.println("Part 2: " + result); } diff --git a/src/test/java/com/macasaet/Day10.java b/src/test/java/com/macasaet/Day10.java index a7723f4..a289ed3 100644 --- a/src/test/java/com/macasaet/Day10.java +++ b/src/test/java/com/macasaet/Day10.java @@ -1,137 +1,153 @@ package com.macasaet; -import java.util.ArrayList; -import java.util.LinkedList; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 10: Syntax Scoring --- + * --- Day 10: Cathode-Ray Tube --- + * https://adventofcode.com/2022/day/10 */ public class Day10 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-10.txt"), - false); - } + public enum Instruction { + noop { + public int cycles() { + return 0; + } + }, + addx { + public int cycles() { + return 2; + } + }; + + public abstract int cycles(); - /** - * The type of open and closing delimiter for a chunk in the navigation subsystem - */ - public enum BracketType { - PARENTHESIS('(', ')', 3, 1), - SQUARE('[', ']', 57, 2), - CURLY('{', '}', 1197, 3), - ANGLED('<', '>', 25137, 4); - - private final char open; - private final char close; - private final int corruptionPoints; - private final int autocompletePoints; - - BracketType(char open, char close, int corruptionPoints, final int autocompletePoints) { - this.open = open; - this.close = close; - this.corruptionPoints = corruptionPoints; - this.autocompletePoints = autocompletePoints; + public static Instruction parse(final String string) { + return Instruction.valueOf(string); } + } - public static BracketType forOpen(final char c) { - return switch (c) { - case '(' -> PARENTHESIS; - case '[' -> SQUARE; - case '{' -> CURLY; - case '<' -> ANGLED; - default -> throw new IllegalStateException("Unexpected value: " + c); + public record Operation(Instruction instruction, Integer argument) { + public List execute(int cycle, int register) { + return switch (instruction()) { + case noop -> Collections.singletonList(new CycleSnapshot(cycle + 1, register)); + case addx -> + List.of(new CycleSnapshot(cycle + 1, register), + new CycleSnapshot(cycle + 2, register + argument)); }; } - public static BracketType forClose(final char c) { - return switch (c) { - case ')' -> PARENTHESIS; - case ']' -> SQUARE; - case '}' -> CURLY; - case '>' -> ANGLED; - default -> throw new IllegalStateException("Unexpected value: " + c); - }; + public static Operation parse(final String line) { + final var components = line.split(" "); + final var instruction = Instruction.parse(components[0]); + Integer argument = null; + if(instruction == Instruction.addx) { + argument = Integer.parseInt(components[1]); + } + return new Operation(instruction, argument); } } - /** - * @param line a line in the navigation subsystem - * @return a score of how corrupt the line is. A score of zero means it is not corrupt. The higher the value, the - * more corrupt the line is. - */ - public int calculateCorruptionScore(final char[] line) { - final var stack = new LinkedList(); - for (int i = 0; i < line.length; i++) { - final var c = line[i]; - if (c == '(' || c == '[' || c == '{' || c == '<') { - stack.push(BracketType.forOpen(c)); - } else if (c == ')' || c == ']' || c == '}' || c == '>') { - if (stack.peek().close == c) { - stack.pop(); - } else { - // corrupt - return BracketType.forClose(c).corruptionPoints; - } - } + public record CycleSnapshot(int cycle, int register) { + public int signalStrength() { + return cycle() * register(); } - // if stack is not empty, it's incomplete - return 0; } - /** - * @param line a non-corrupt line in the navigation subsystem. Behaviour is undefined for corrupt lines. - * @return the score for the suffix required to complete the line - */ - public long calculateCompletionScore(final char[] line) { - final var stack = new LinkedList(); - for (int i = 0; i < line.length; i++) { - final var c = line[i]; - if (c == '(' || c == '[' || c == '{' || c == '<') { - stack.push(BracketType.forOpen(c)); - } else if (c == ')' || c == ']' || c == '}' || c == '>') { - if (stack.peek().close == c) { - stack.pop(); - } else { - throw new IllegalArgumentException("Corrupt: " + new String(line)); - } + public static class State { + private int register = 1; + private int cycle = 1; + + public List getActivePixels() { + return Arrays.asList(register - 1, register, register + 1); + } + + public List execute(final Operation operation) { + final var result = operation.execute(cycle, register); + final var last = result.get(result.size() - 1); + cycle = last.cycle(); + register = last.register(); + return result; + } + } + + public static class Display { + final char[][] pixels = new char[6][]; + + { + for(int i = pixels.length; --i >= 0; pixels[i] = new char[40]); + } + + public void update(final CycleSnapshot snapshot) { + final var pixelIndex = snapshot.cycle() - 1; + final var spritePositions = + Arrays.asList(snapshot.register() - 1, snapshot.register(), snapshot.register() + 1); + final int row = pixelIndex / 40; + final int column = pixelIndex % 40; + if(row >= pixels.length) { + return; + } + if(spritePositions.contains(column)) { + pixels[row][column] = '#'; + } else { + pixels[row][column] = '.'; } } - long result = 0; - while (!stack.isEmpty()) { - final var unclosed = stack.pop(); - result = result * 5 + unclosed.autocompletePoints; + public String toString() { + final var buffer = new StringBuilder(); + buffer.append(pixels[0], 0, 40); + buffer.append('\n'); + buffer.append(pixels[1], 0, 40); + buffer.append('\n'); + buffer.append(pixels[2], 0, 40); + buffer.append('\n'); + buffer.append(pixels[3], 0, 40); + buffer.append('\n'); + buffer.append(pixels[4], 0, 40); + buffer.append('\n'); + buffer.append(pixels[5], 0, 40); + return buffer.toString(); } - return result; + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-10.txt"), + false) + .map(Operation::parse); } @Test public final void part1() { - final var result = getInput() - .map(String::toCharArray) - .filter(line -> line.length > 0) - .mapToInt(this::calculateCorruptionScore) - .sum(); + final var interestingCycles = Arrays.asList(20, 60, 100, 140, 180, 220); + final var state = new State(); + final var accumulator = new AtomicInteger(0); + getInput().forEach(instruction -> { + final var sideEffects = state.execute(instruction); + for(final var sideEffect : sideEffects) { + if(interestingCycles.contains(sideEffect.cycle)) { + accumulator.addAndGet(sideEffect.signalStrength()); + } + } + }); + final var result = accumulator.get(); System.out.println("Part 1: " + result); } @Test public final void part2() { - final var list = getInput() - .map(String::toCharArray) - .filter(line -> line.length > 0) - .filter(line -> calculateCorruptionScore(line) <= 0) // discard corrupted lines - .mapToLong(this::calculateCompletionScore) - .sorted() - .collect(ArrayList::new, List::add, List::addAll); - final var result = list.get(list.size() / 2); - System.out.println("Part 2: " + result); + final var state = new State(); + final var display = new Display(); + display.update(new CycleSnapshot(1, 1)); + getInput().forEach(instruction -> state.execute(instruction).forEach(display::update)); + System.out.println("Part 2:\n" + display); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day11.java b/src/test/java/com/macasaet/Day11.java index f566fd3..45db617 100644 --- a/src/test/java/com/macasaet/Day11.java +++ b/src/test/java/com/macasaet/Day11.java @@ -1,169 +1,189 @@ package com.macasaet; -import java.util.ArrayList; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; -import java.util.stream.Stream; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 11: Dumbo Octopus --- + * --- Day 11: Monkey in the Middle --- + * https://adventofcode.com/2022/day/11 */ public class Day11 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-11.txt"), - false); + public enum Operator implements BiFunction { + ADD { + public BigInteger apply(BigInteger x, BigInteger y) { + return x.add(y); + } + }, + MULTIPLY { + public BigInteger apply(BigInteger x, BigInteger y) { + return x.multiply(y); + } + }; + + public static Operator parse(final String string) { + return switch(string) { + case "*" -> MULTIPLY; + case "+" -> ADD; + default -> throw new IllegalArgumentException("Invalid operator: " + string); + }; + } } - /** - * @return a spatial grid of the cavern indicating the location of the octopuses - */ - protected Octopus[][] getOctopusGrid() { - final var list = getInput().map(line -> { - final var chars = line.toCharArray(); - final byte[] result = new byte[chars.length]; - for (int i = chars.length; --i >= 0; result[i] = (byte) (chars[i] - '0')) ; - return result; - }).collect(ArrayList::new, List::add, List::addAll); - final var result = new Octopus[list.size()][]; - for (int i = list.size(); --i >= 0; ) { - final var row = list.get(i); - result[i] = new Octopus[row.length]; - for (int j = row.length; --j >= 0; result[i][j] = new Octopus(i, j, row[j])) ; + public record Operation(Operator operator, + Function lValueSupplier, + Function rValueSupplier) implements Function { + + public BigInteger apply(final BigInteger oldValue) { + Objects.requireNonNull(oldValue); + final var lValue = lValueSupplier.apply(oldValue); + final var rValue = rValueSupplier.apply(oldValue); + return operator.apply(lValue, rValue); + } + + public static Operation parse(String line) { + line = line.strip(); + if(!line.trim().startsWith("Operation:")) { + throw new IllegalArgumentException("Not an operation: " + line); + } + final var components = line.split(" "); + final var lValueExpression = components[3]; + final Function lValueSupplier = "old".equalsIgnoreCase(lValueExpression) + ? old -> old + : ignored -> new BigInteger(lValueExpression); + final var operator = Operator.parse(components[4]); + final var rValueExpression = components[5]; + final Function rValueSupplier = "old".equalsIgnoreCase(rValueExpression) + ? old -> old + : ignored -> new BigInteger(rValueExpression); + return new Operation(operator, lValueSupplier, rValueSupplier); } - return result; } /** - * A rare bioluminescent dumbo octopus + * An observation of how a single monkey behaves + * + * @param id the monkey's unique identifier + * @param items your worry level for each belonging currently held by this monkey + * @param operation a function describing how your worry level changes when the monkey inspects the item + * @param divisor used by the monkey to evaluate your worry level and decide what to do with the item + * @param targetIfTrue the ID of the monkey who will receive the item should the test pass + * @param targetIfFalse the ID of the monkey who will receive the item should the test fail + * @param itemsInspected the total number of times this monkey has inspected an item */ - public static class Octopus { - private final int x, y; - private byte energyLevel; - - public Octopus(final int x, final int y, final byte energyLevel) { - this.x = x; - this.y = y; - this.energyLevel = energyLevel; + public record Monkey(int id, List items, Operation operation, BigInteger divisor, int targetIfTrue, int targetIfFalse, AtomicReference itemsInspected) { + public static Monkey parse(final String block) { + final var lines = block.split("\n"); + final var id = Integer.parseInt(lines[0].replaceAll("[^0-9]", "")); + final var startingItems = Arrays.stream(lines[1].strip() + .replaceAll("^Starting items: ", "") + .split(", ")) + .map(item -> new BigInteger(item)) + .collect(Collectors.toList()); // must be mutable + final var operation = Operation.parse(lines[2]); + final var divisor = new BigInteger(lines[3].replaceAll("[^0-9]", "")); + final var targetIfTrue = Integer.parseInt(lines[4].replaceAll("[^0-9]", "")); + final var targetIfFalse = Integer.parseInt(lines[5].replaceAll("[^0-9]", "")); + return new Monkey(id, startingItems, operation, divisor, targetIfTrue, targetIfFalse, new AtomicReference<>(BigInteger.ZERO)); } - /** - * Increase the octopus' energy level and, if appropriate, propagate side effects to its neighbours. - * - * @param population the full population of octopuses - */ - public void prepareStep(final Population population) { - if (this.energyLevel > 9) { - // "An octopus can only flash at most once per step." - return; - } - // "First, the energy level of each octopus increases by 1." - this.energyLevel++; - if (this.energyLevel > 9) { - // "Then, any octopus with an energy level greater than 9 flashes. This increases the energy level of - // all adjacent octopuses by 1, including octopuses that are diagonally adjacent." - final var grid = population.grid(); - final var hasRowAbove = x > 0; - final var hasRowBelow = x < grid.length - 1; - final var hasColumnToLeft = y > 0; - final var hasColumnToRight = y < grid[x].length - 1; - - if (hasRowAbove) { - grid[x - 1][y].prepareStep(population); - if (hasColumnToLeft) { - grid[x - 1][y - 1].prepareStep(population); - } - if (hasColumnToRight) { - grid[x - 1][y + 1].prepareStep(population); - } - } - if (hasColumnToLeft) { - grid[x][y - 1].prepareStep(population); - } - if (hasColumnToRight) { - grid[x][y + 1].prepareStep(population); - } - if (hasRowBelow) { - grid[x + 1][y].prepareStep(population); - if (hasColumnToLeft) { - grid[x + 1][y - 1].prepareStep(population); - } - if (hasColumnToRight) { - grid[x + 1][y + 1].prepareStep(population); - } - } - } + public BigInteger countItemsInspected() { + return itemsInspected.get(); } - /** - * Complete the step and finalise any side effects. - * - * @return true if and only if the octopus flashed during this step. - */ - public boolean finishStep() { - if (this.energyLevel > 9) { - // "Finally, any octopus that flashed during this step has its energy level set to 0, as it used all of - // its energy to flash." - this.energyLevel = 0; - return true; + public Throw inspectItem(BigInteger reliefFactor) { + // this assumes monkeys can throw items to themselves + if(items.isEmpty()) { + return null; } - return false; + var worryLevel = items().remove(0); + worryLevel = operation().apply(worryLevel); + worryLevel = worryLevel.divide(reliefFactor); + final var target = worryLevel.mod(divisor()).equals(BigInteger.ZERO) + ? targetIfTrue() + : targetIfFalse(); + itemsInspected().updateAndGet(old -> old.add(BigInteger.ONE)); + return new Throw(target, worryLevel); } - } - /** - * The full population of dumbo octopuses. The population members will be modified with each step. - */ - public record Population(Octopus[][] grid) { - public int step() { - for (int i = grid.length; --i >= 0; ) { - final var row = grid[i]; - for (int j = row.length; --j >= 0; ) { - row[j].prepareStep(this); - } - } - int flashes = 0; - for (int i = grid.length; --i >= 0; ) { - final var row = grid[i]; - for (int j = row.length; --j >= 0; ) { - if (row[j].finishStep()) flashes++; - } - } - return flashes; + public List inspectItems(Function worryUpdater) { + // this assumes monkeys cannot throw items to themselves + final var result = items().stream().map(worryLevel -> { + worryLevel = operation().apply(worryLevel); + worryLevel = worryUpdater.apply(worryLevel); + final var target = worryLevel.mod(divisor()).equals(BigInteger.ZERO) + ? targetIfTrue() + : targetIfFalse(); + return new Throw(target, worryLevel); + }).toList(); + itemsInspected().updateAndGet(old -> old.add(BigInteger.valueOf(result.size()))); + items().clear(); + return result; } } - @Test - public final void part1() { - final var energyLevels = getOctopusGrid(); - final var population = new Population(energyLevels); + public record Throw(int target, BigInteger itemWorryLevel) { + } - int flashes = 0; + protected List getInput() { + final var input = StreamSupport + .stream(new LineSpliterator("day-11.txt"), + false).collect(Collectors.joining("\n")); + return Arrays.stream(input.split("\n\n")) + .map(Monkey::parse) + .toList(); + } - for (int step = 0; step < 100; step++) { - flashes += population.step(); + @Test + public final void part1() { + final var monkeys = getInput(); + final Function worryUpdater = worryLevel -> worryLevel.divide(BigInteger.valueOf(3)); + for(int i = 20; --i >= 0; ) { + for(final var monkey : monkeys) { + for(final var toss : monkey.inspectItems(worryUpdater)) { + monkeys.get(toss.target()).items().add(toss.itemWorryLevel()); + } + } } - - System.out.println("Part 1: " + flashes); + final var result = monkeys.stream() + .map(Monkey::countItemsInspected) + .sorted(Comparator.reverseOrder()) + .limit(2) + .reduce(BigInteger::multiply) + .get(); + System.out.println("Part 1: " + result); } @Test public final void part2() { - final var energyLevels = getOctopusGrid(); - final var population = new Population(energyLevels); - int step = 0; - while(true) { - final int flashes = population.step(); - step++; - if(flashes == 100) { - System.out.println("Part 2: " + step); - break; + final var monkeys = getInput(); + final var productOfDivisors = monkeys.stream().map(Monkey::divisor).reduce(BigInteger::multiply).get(); + final Function worryUpdater = worryLevel -> worryLevel.mod(productOfDivisors); + for(int i = 10_000; --i >= 0; ) { + for(final var monkey : monkeys) { + for(final var toss : monkey.inspectItems(worryUpdater)) { + monkeys.get(toss.target()).items().add(toss.itemWorryLevel()); + } } } + final var result = monkeys.stream() + .map(Monkey::countItemsInspected) + .sorted(Comparator.reverseOrder()) + .limit(2) + .reduce(BigInteger::multiply) + .get(); + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day12.java b/src/test/java/com/macasaet/Day12.java index af7de96..3257259 100644 --- a/src/test/java/com/macasaet/Day12.java +++ b/src/test/java/com/macasaet/Day12.java @@ -1,182 +1,164 @@ package com.macasaet; -import java.util.*; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.stream.StreamSupport; + /** - * --- Day 12: Passage Pathing --- + * --- Day 12: Hill Climbing Algorithm --- + * https://adventofcode.com/2022/day/12 */ public class Day12 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-12.txt"), - false); - } - - /** - * @return a map of the connected caves - */ - protected Map getMap() { - final var map = new HashMap(); - getInput().forEach(line -> { - final var components = line.split("-"); - final var sourceLabel = components[0]; - final var targetLabel = components[1]; - final var source = map.computeIfAbsent(sourceLabel, Node::new); - final var target = map.computeIfAbsent(targetLabel, Node::new); - source.connected.add(target); - target.connected.add(source); - }); - return Collections.unmodifiableMap(map); + record Coordinate(int x, int y) { } - public Node getStartingPoint() { - return getMap().get("start"); - } - - /** - * A distinct path through the cave system - */ - public record Path(List nodes, Node specialCave, int specialCaveVisits) { + public record HeightMap(int[][] grid, Coordinate start, Coordinate end) { - public int hashCode() { - int result = 0; - for (final var node : nodes()) { - result = result * 31 + node.hashCode(); - } - return result; + public int lengthOfShortestPath() { + return this.lengthOfShortestPath(this.start); } - public boolean equals(final Object o) { - if (o == null) { - return false; - } - try { - final var other = (Path) o; - return nodes().equals(other.nodes()); - } catch (final ClassCastException cce) { - return false; + public int lengthOfShortestPath(final Coordinate startingPoint) { + final var cheapestCostToNode = new HashMap(); + cheapestCostToNode.put(startingPoint, 0); + final var estimatedCostToFinish = new HashMap(); + estimatedCostToFinish.put(startingPoint, estimateDistance(startingPoint, this.end)); + final var openSet = new PriorityQueue((x, y) -> { + final var xScore = estimatedCostToFinish.getOrDefault(x, Integer.MAX_VALUE); + final var yScore = estimatedCostToFinish.getOrDefault(y, Integer.MAX_VALUE); + return xScore.compareTo(yScore); + }); + openSet.add(startingPoint); + while (!openSet.isEmpty()) { + final var current = openSet.remove(); + if (current.equals(this.end)) { + return cheapestCostToNode.get(current); + } + for (final var neighbour : neighbours(current)) { + final var tentativeGScore = cheapestCostToNode.get(current) + 1; + if (tentativeGScore < cheapestCostToNode.getOrDefault(neighbour, Integer.MAX_VALUE)) { + cheapestCostToNode.put(neighbour, tentativeGScore); + estimatedCostToFinish.put(neighbour, tentativeGScore + estimateDistance(neighbour, this.end)); + if (!openSet.contains(neighbour)) { + openSet.add(neighbour); + } + } + } + } + return Integer.MAX_VALUE; } - } - } - - public static class Node { - private final boolean isStart; - private final boolean isEnd; - private final boolean isSmallCave; - private final String label; - - private final Set connected = new HashSet<>(); - public Node(final String label) { - this("start".equalsIgnoreCase(label), "end".equalsIgnoreCase(label), - label.toLowerCase(Locale.ROOT).equals(label), label); - } - - protected Node(boolean isStart, boolean isEnd, boolean isSmallCave, final String label) { - this.isStart = isStart; - this.isEnd = isEnd; - this.isSmallCave = isSmallCave; - this.label = label; - } - - public int hashCode() { - int result = 0; - result += result * 31 + label.hashCode(); - return result; - } - - public boolean equals(final Object o) { - if (o == null) { - return false; + public List getPotentialTrailHeads() { + final var list = new ArrayList(); + for(int i = this.grid().length; --i >= 0; ) { + final var row = this.grid()[i]; + for(int j = row.length; --j >= 0; ) { + if(row[j] == 0) { + list.add(new Coordinate(i, j)); + } + } + } + return Collections.unmodifiableList(list); } - try { - final Node other = (Node) o; - return label.equals(other.label); - } catch (final ClassCastException cce) { - return false; + + int height(final Coordinate coordinate) { + return this.grid[coordinate.x()][coordinate.y()]; } - } - } + List neighbours(final Coordinate coordinate) { + final var list = new ArrayList(4); + if (coordinate.x() > 0) { + final var up = new Coordinate(coordinate.x() - 1, coordinate.y()); + if (height(coordinate) + 1 >= height(up)) { + list.add(up); + } + } + if (coordinate.x() < this.grid.length - 1) { + final var down = new Coordinate(coordinate.x() + 1, coordinate.y()); + if (height(coordinate) + 1 >= height(down)) { + list.add(down); + } + } + if (coordinate.y() > 0) { + final var left = new Coordinate(coordinate.x(), coordinate.y() - 1); + if (height(coordinate) + 1 >= height(left)) { + list.add(left); + } + } + final var row = this.grid[coordinate.x()]; + if (coordinate.y() < row.length - 1) { + final var right = new Coordinate(coordinate.x(), coordinate.y() + 1); + if (height(coordinate) + 1 >= height(right)) { + list.add(right); + } + } + return Collections.unmodifiableList(list); + } - protected Set getPaths(final Node node, final Path pathSoFar) { - final var result = new HashSet(); + int estimateDistance(final Coordinate from, final Coordinate to) { + return (int) Math.sqrt(Math.pow(from.x() - to.x(), 2.0) + Math.pow(from.y() - to.y(), 2.0)); + } - if (node.isStart && pathSoFar.nodes.size() > 1) { - // "once you leave the start cave, you may not return to it" - return Collections.emptySet(); } - final var nodes = new ArrayList<>(pathSoFar.nodes()); - if (node.isEnd) { - // "once you reach the end cave, the path must end immediately" - nodes.add(node); - return Collections.singleton(new Path(Collections.unmodifiableList(nodes), pathSoFar.specialCave(), pathSoFar.specialCaveVisits())); - } - int specialCaveVisits = pathSoFar.specialCaveVisits(); - if (node.isSmallCave) { - if (node.equals(pathSoFar.specialCave())) { - // "a single small cave can be visited at most twice" - if (pathSoFar.specialCaveVisits() < 1) { - specialCaveVisits++; + protected HeightMap getInput() { + final var charGrid = StreamSupport.stream(new LineSpliterator("day-12.txt"), false).map(line -> { + final var list = new ArrayList(line.length()); + for (final var c : line.toCharArray()) { + list.add(c); + } + return list; + }).toList(); + Coordinate origin = null; + Coordinate destination = null; + int[][] grid = new int[charGrid.size()][]; + for(int i = charGrid.size(); --i >= 0; ) { + final var row = charGrid.get(i); + grid[i] = new int[row.size()]; + for(int j = row.size(); --j >= 0; ) { + final char c = row.get(j); + if(c == 'S') { + origin = new Coordinate(i, j); + grid[i][j] = 0; + } else if(c == 'E') { + destination = new Coordinate(i, j); + grid[i][j] = 'z' - 'a'; } else { - return Collections.emptySet(); - } - } else { - if (pathSoFar.nodes().contains(node)) { - // "the remaining small caves can be visited at most once" - return Collections.emptySet(); + grid[i][j] = c - 'a'; } } } - nodes.add(node); - for (final var neighbour : node.connected) { - if (neighbour.isSmallCave && pathSoFar.specialCave() == null) { - result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), null, 0))); - result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), neighbour, 0))); - } else { - result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), pathSoFar.specialCave(), specialCaveVisits))); - } - } - return Collections.unmodifiableSet(result); - } - - protected int countPaths(final Node node, final Set visitedSmallCaves) { - int result = 0; - if (node.isEnd) { - return 1; - } - if (visitedSmallCaves.contains(node)) { - // invalid path - return 0; - } - if (node.isSmallCave) { - visitedSmallCaves.add(node); - } - for (final var connected : node.connected) { - final var set = new HashSet<>(visitedSmallCaves); - result += countPaths(connected, set); - } - return result; + Objects.requireNonNull(origin); + Objects.requireNonNull(destination); + return new HeightMap(grid, origin, destination); } @Test public final void part1() { - final var start = getStartingPoint(); - final int result = countPaths(start, new HashSet<>()); + final var map = getInput(); + final var result = map.lengthOfShortestPath(); System.out.println("Part 1: " + result); } @Test public final void part2() { - final var start = getStartingPoint(); - final var paths = getPaths(start, new Path(Collections.emptyList(), null, 0)); - System.out.println("Part 2: " + paths.size()); + final var map = getInput(); + var result = Integer.MAX_VALUE; + for(final var candidate : map.getPotentialTrailHeads()) { + final var length = map.lengthOfShortestPath(candidate); + if(length < result) { + result = length; + } + } + + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day13.java b/src/test/java/com/macasaet/Day13.java index b60355f..457145f 100644 --- a/src/test/java/com/macasaet/Day13.java +++ b/src/test/java/com/macasaet/Day13.java @@ -1,187 +1,161 @@ package com.macasaet; -import java.util.*; +import org.junit.jupiter.api.Test; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 13: Transparent Origami --- + * --- Day 13: Distress Signal --- + * https://adventofcode.com/2022/day/13 */ public class Day13 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-13.txt"), - false); - } - - /** - * A point on the translucent sheet of paper. Note that x and y correspond to a particular - * {@link Axis}. - */ - public record Point(int x, int y) { - } + public record Pair(ListItem x, ListItem y) { + static Pair parse(final String lines) { + final var components = lines.split("\n"); + final var x = ListItem.parse(components[0]); + final var y = ListItem.parse(components[1]); + return new Pair(x, y); + } - /** - * An axis of the translucent sheet of paper - */ - public enum Axis { - /** - * The axis that increases to the right - */ - X, - - /** - * The axis that increases downward - */ - Y, - } + public boolean isInOrder() { + return x().compareTo(y()) < 0; + } - /** - * An equation for a line - */ - public record Line(Axis axis, int value) { - public String toString() { - return switch (axis()) { - case X -> "x=" + value; - case Y -> "y=" + value; - }; + public Stream stream() { + return Stream.of(x(), y()).sorted(); } } - public record Input(Collection points, List folds, int maxX, int maxY) { - public Sheet getSheet() { - final boolean[][] grid = new boolean[maxY + 1][]; - for (int i = grid.length; --i >= 0; ) { - grid[i] = new boolean[maxX + 1]; - } - for (final var point : points) { - /* The first value, x, increases to the right. The second value, y, increases downward. */ - grid[point.y()][point.x()] = true; + interface Item extends Comparable { + + int compareToList(ListItem other); + int compareToLiteral(Literal other); + + default int compareTo(Item other) { + if(other instanceof ListItem) { + return compareToList((ListItem)other); + } else { + if(!(other instanceof Literal)) { + throw new IllegalArgumentException("Unknown implementation"); + } + return compareToLiteral((Literal) other); } - return new Sheet(grid); } } - /** - * A sheet of translucent paper - */ - public record Sheet(boolean[][] grid) { - - public int countDots() { - int result = 0; - final var grid = grid(); - for (int i = grid.length; --i >= 0; ) { - for (int j = grid[i].length; --j >= 0; ) { - if (grid[i][j]) { - result++; + public record ListItem(List items) implements Item { + public static ListItem parse(final String string) { + final var stack = new ArrayDeque(); + StringBuilder numberBuffer = new StringBuilder(); + for(final char c : string.toCharArray()) { + if(c == '[') { + stack.push(new ListItem(new ArrayList<>())); + } else if(c == ']') { + if(!numberBuffer.isEmpty()) { + final var numberString = numberBuffer.toString(); + numberBuffer.delete(0, numberBuffer.length()); + final var number = Integer.parseInt(numberString); + stack.peek().items().add(new Literal(number)); + } + if(stack.size() > 1) { + final var completed = stack.pop(); + stack.peek().items().add(completed); } + } else if(c == ',') { + if(!numberBuffer.isEmpty()) { + final var numberString = numberBuffer.toString(); + numberBuffer.delete(0, numberBuffer.length()); + final var number = Integer.parseInt(numberString); + stack.peek().items().add(new Literal(number)); + } + } else { + numberBuffer.append(c); } } - return result; + return stack.pop(); } - public String toString() { - final var builder = new StringBuilder(); - for (final var row : grid) { - for (final var cell : row) { - builder.append(cell ? '#' : '.'); + public int compareToList(ListItem other) { + final Iterator x = this.items().iterator(); + final Iterator y = other.items().iterator(); + while(x.hasNext() && y.hasNext()) { + final var xItem = x.next(); + final var yItem = y.next(); + final var comparison = xItem.compareTo(yItem); + if(comparison != 0) { + return comparison; } - builder.append('\n'); } - return builder.toString(); + if(y.hasNext()) { + return -1; + } else if(x.hasNext()) { + return 1; + } + return 0; } - public Sheet fold(final Line line) { - // note, value is always positive - return switch (line.axis()) { - case X -> { - // fold along the x-axis (vertical line) - final var newGrid = new boolean[grid.length][]; - for (int i = newGrid.length; --i >= 0; ) { - final var newRow = new boolean[line.value() + 1]; - for (int j = newRow.length; --j >= 0; newRow[j] = grid[i][j]) ; - for(int j = grid[i].length - line.value(); --j > 0; ) { - if(grid[i][line.value() + j]) { - newRow[line.value() - j] = true; - } - } - newGrid[i] = newRow; - } - yield new Sheet(newGrid); - } - case Y -> { - // fold along the y-axis (horizontal line) - final var newGrid = new boolean[line.value()][]; - for (int i = newGrid.length; --i >= 0; ) { - final var newRow = new boolean[grid[i].length]; - for (int j = grid[i].length; --j >= 0; newRow[j] = grid[i][j]) ; - newGrid[i] = newRow; - } - for (int i = grid.length - line.value(); --i > 0; ) { - final var oldRow = grid[line.value() + i]; - for (int j = oldRow.length; - --j >= 0; - newGrid[line.value() - i][j] |= oldRow[j]) - ; - } - yield new Sheet(newGrid); - } - }; + public int compareToLiteral(Literal other) { + return compareToList(other.asList()); } + } - public Input parseInput() { - int section = 0; - final var points = new HashSet(); - final var folds = new ArrayList(); - int maxX = Integer.MIN_VALUE; - int maxY = Integer.MIN_VALUE; - for (final var line : getInput().collect(Collectors.toList())) { - if (line.isBlank()) { - section++; - continue; - } - if (section == 0) { // points - final var components = line.split(","); - final var x = Integer.parseInt(components[0]); - maxX = Math.max(x, maxX); - final var y = Integer.parseInt(components[1]); - maxY = Math.max(y, maxY); - final var point = new Point(x, y); - points.add(point); - } else { // commands - final var equation = line.replaceFirst("fold along ", ""); - final var components = equation.split("="); - final var axis = Axis.valueOf(components[0].toUpperCase(Locale.ROOT)); - final var value = Integer.parseInt(components[1]); - final var fold = new Line(axis, value); - folds.add(fold); - } + public record Literal(int item) implements Item { + public int compareToList(ListItem other) { + return asList().compareToList(other); } - return new Input(points, folds, maxX, maxY); + + public int compareToLiteral(Literal other) { + return Integer.compare(item(), other.item()); + } + + public ListItem asList() { + return new ListItem(Collections.singletonList(this)); + } + } + + protected List getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-13.txt"), false) + .collect(Collectors.joining("\n")); + final var blocks = lines.split("\n\n"); + return Arrays.stream(blocks).map(block -> Pair.parse(block)).toList(); } @Test public final void part1() { - final var input = parseInput(); - final var sheet = input.getSheet(); - final var firstFold = input.folds().get(0); - final var result = sheet.fold(firstFold); - System.out.println("Part 1: " + result.countDots()); + final var pairs = getInput(); + var result = 0; + for(int i = 0; i < pairs.size(); i++) { + final var pair = pairs.get(i); + if(pair.isInOrder()) { + result += i + 1; + } + } + System.out.println("Part 1: " + result); } @Test public final void part2() { - final var input = parseInput(); - var sheet = input.getSheet(); - for (final var fold : input.folds()) { - sheet = sheet.fold(fold); - } - System.out.println("Part 2:\n" + sheet); + final var pairs = getInput(); + final var packets = pairs.stream().flatMap(Pair::stream).sorted().toList(); + final int leftSearchResult = Collections.binarySearch(packets, + new ListItem(Collections.singletonList(new ListItem(Collections.singletonList(new Literal(2)))))); + final int leftInsertionPoint = -(leftSearchResult + 1) + 1; + final int rightSearchResult = Collections.binarySearch(packets, + new ListItem(Collections.singletonList(new ListItem(Collections.singletonList(new Literal(6)))))); + final int rightInsertionPoint = -(rightSearchResult + 1) + 2; + final int result = leftInsertionPoint * rightInsertionPoint; + + System.out.println("Part 2: " + result); } -} +} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day14.java b/src/test/java/com/macasaet/Day14.java index c44d50b..7f7d777 100644 --- a/src/test/java/com/macasaet/Day14.java +++ b/src/test/java/com/macasaet/Day14.java @@ -1,166 +1,212 @@ package com.macasaet; -import java.math.BigInteger; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + /** - * --- Day 14: Extended Polymerization --- + * --- Day 14: Regolith Reservoir --- + * https://adventofcode.com/2022/day/14 */ public class Day14 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-14.txt"), - false); + public enum Cell { + ROCK, + SAND } - public record Polymer(Map pairCounts, char firstElement, char lastElement) { - public static Polymer forTemplate(final String templateString) { - final var firstElement = templateString.charAt(0); - final var lastElement = templateString.charAt(templateString.length() - 1); - final var map = new HashMap(); - for (int i = 1; i < templateString.length(); i++) { - map.merge(new ElementPair(templateString.charAt(i - 1), templateString.charAt(i)), - BigInteger.ONE, - BigInteger::add); - } - return new Polymer(Collections.unmodifiableMap(map), firstElement, lastElement); + record Coordinate(int verticalDepth, int horizontalOffset) { + + public static Coordinate parse(final String string) { + final var components = string.split(","); + final var verticalDepth = Integer.parseInt(components[1]); + final var horizontalOffset = Integer.parseInt(components[0]); + return new Coordinate(verticalDepth, horizontalOffset); } + } - /** - * Apply the pair insertion process one time. - * - * @param rules pair insertion rules for generating a new polymer - * @return the new polymer that results - */ - public Polymer applyRules(final Map rules) { - final var map = new HashMap(); - for (final var entry : pairCounts().entrySet()) { - final var key = entry.getKey(); - final var count = entry.getValue(); - final var rule = rules.get(key); - final var left = new ElementPair(key.start(), rule.insert()); - final var right = new ElementPair(rule.insert(), key.end()); - - map.compute(left, (_key, oldCount) -> oldCount == null ? count : oldCount.add(count)); - map.compute(right, (_key, oldCount) -> oldCount == null ? count : oldCount.add(count)); + public record Cave(Map> grid, int maxDepth, int minHorizontalOffset, int maxHorizontalOffset) { + + public int pourSandIntoAbyss() { + int settledSand = 0; + while(true) { + var fallingSandCoordinate = new Coordinate(0, 500); + while(true) { + final var next = getNextCoordinate(fallingSandCoordinate, null); + if(next != null) { + fallingSandCoordinate = next; + if(fallingSandCoordinate.verticalDepth() >= maxDepth()) { + return settledSand; + } + } else { + final var row = grid.computeIfAbsent(fallingSandCoordinate.verticalDepth(), key -> new HashMap<>()); + row.put(fallingSandCoordinate.horizontalOffset(), Cell.SAND); + settledSand++; + break; + } + } } - return new Polymer(Collections.unmodifiableMap(map), firstElement(), lastElement()); } - /** - * Determine how many times each element appears in the polymer - * - * @return the number of times each element appears in the polymer - */ - public SortedMap> histogram() { - final var map = new HashMap(); - for (final var entry : pairCounts().entrySet()) { - final var pair = entry.getKey(); - final var count = entry.getValue(); - map.compute(pair.start(), - (_key, oldValue) -> oldValue == null ? count : oldValue.add(count)); - map.compute(pair.end(), - (_key, oldValue) -> oldValue == null ? count : oldValue.add(count)); - } - for (final var entry : map.entrySet()) { - final var element = entry.getKey(); - final var count = entry.getValue(); - if (element.equals(firstElement()) || element.equals(lastElement())) { - entry.setValue(count.divide(BigInteger.TWO).add(BigInteger.ONE)); - } else { - entry.setValue(count.divide(BigInteger.TWO)); + public int fillAperture() { + int settledSand = 0; + while(true) { + var fallingSandCoordinate = new Coordinate(0, 500); + while(true) { + final var next = getNextCoordinate(fallingSandCoordinate, floorDepth()); + if(next != null) { + fallingSandCoordinate = next; + } else { + final var secondRow = grid().computeIfAbsent(1, key -> new HashMap<>()); + if(secondRow.containsKey(499) && secondRow.containsKey(500) && secondRow.containsKey(501)) { + return settledSand + 1; + } + final var row = grid.computeIfAbsent(fallingSandCoordinate.verticalDepth(), key -> new HashMap<>()); + row.put(fallingSandCoordinate.horizontalOffset(), Cell.SAND); + settledSand++; + break; + } } } - final var result = new TreeMap>(); - for (final var entry : map.entrySet()) { - final var target = result.computeIfAbsent(entry.getValue(), _key -> new HashSet<>()); - target.add(entry.getKey()); - } - return Collections.unmodifiableSortedMap(result); } - } - /** - * A pair of elements that appear adjacent to each other. This may be used in the context of a pair insertion rule - * definition or a polymer. - * - * @see Polymer - * @see PairInsertionRule - */ - protected record ElementPair(char start, char end) { - } - - /** - * A single instruction to aid in finding the optimal polymer formula - */ - public record PairInsertionRule(char start, char end, char insert) { + int floorDepth() { + return maxDepth() + 2; + } - public static PairInsertionRule parse(final String string) { - final var components = string.split(" -> "); - final var match = components[0].toCharArray(); - return new PairInsertionRule(match[0], match[1], components[1].toCharArray()[0]); + Coordinate getNextCoordinate(final Coordinate start, Integer floorDepth) { + final var x = start.verticalDepth(); + final var y = start.horizontalOffset(); + if(floorDepth != null && x + 1 >= floorDepth) { + return null; + } + final var nextRow = grid().computeIfAbsent(x + 1, key -> new HashMap<>()); + if(!nextRow.containsKey(y)) { + return new Coordinate(x + 1, y); + } else if(!nextRow.containsKey(y - 1)) { + return new Coordinate(x + 1, y - 1); + } else if(!nextRow.containsKey(y + 1)) { + return new Coordinate(x + 1, y + 1); + } + return null; } - } + public static Cave parse(final Collection lines) { + int maxDepth = 0; + int maxHorizontalOffset = Integer.MIN_VALUE; + int minHorizontalOffset = Integer.MAX_VALUE; + + final var grid = new HashMap>(); + for(final var line : lines) { + final var rockPath = parseRockPaths(line); + var last = rockPath.get(0); + if(last.verticalDepth() > maxDepth) { + maxDepth = last.verticalDepth(); + } + if(last.horizontalOffset() < minHorizontalOffset) { + minHorizontalOffset = last.horizontalOffset(); + } + if(last.horizontalOffset() > maxHorizontalOffset) { + maxHorizontalOffset = last.horizontalOffset(); + } + for(int i = 1; i < rockPath.size(); i++) { + final var current = rockPath.get(i); + if(last.verticalDepth() == current.verticalDepth()) { + // horizontal line + int start; + int end; + if(last.horizontalOffset() < current.horizontalOffset()) { + start = last.horizontalOffset(); + end = current.horizontalOffset(); + } else { + start = current.horizontalOffset(); + end = last.horizontalOffset(); + } + final var row = grid.computeIfAbsent(last.verticalDepth(), key -> new HashMap<>()); + for(int y = start; y <= end; y++) { + row.put(y, Cell.ROCK); + } + } else { + if(last.horizontalOffset() != current.horizontalOffset()) { + throw new IllegalStateException("Line segments are not on the same vertical axis"); + } + // vertical line + int start; + int end; + if(last.verticalDepth() < current.verticalDepth()) { + start = last.verticalDepth(); + end = current.verticalDepth(); + } else { + start = current.verticalDepth(); + end = last.verticalDepth(); + } + for(int x = start; x <= end; x++) { + final var row = grid.computeIfAbsent(x, key -> new HashMap<>()); + row.put(last.horizontalOffset(), Cell.ROCK); + } + } + if(current.verticalDepth() > maxDepth) { + maxDepth = current.verticalDepth(); + } + if(current.horizontalOffset() < minHorizontalOffset) { + minHorizontalOffset = current.horizontalOffset(); + } + if(current.horizontalOffset() > maxHorizontalOffset) { + maxHorizontalOffset = current.horizontalOffset(); + } + last = current; + } + } + return new Cave(grid, maxDepth, minHorizontalOffset, maxHorizontalOffset); + } - protected record Input(Polymer polymerTemplate, List rules) { - } + static List parseRockPaths(final String line) { + return Arrays.stream(line.split(" -> ")).map(Coordinate::parse).toList(); + } - protected Input parseInput() { - final var list = getInput().collect(Collectors.toList()); - int mode = 0; - Polymer polymer = null; - final var rules = new ArrayList(); - for (final var line : list) { - if (line.isBlank()) { - mode++; - continue; - } - if (mode == 0) { - polymer = Polymer.forTemplate(line); - } else { - rules.add(PairInsertionRule.parse(line)); + @Override + public String toString() { + final var buffer = new StringBuilder(); + for(int i = 0; i <= floorDepth(); i++) { + buffer.append(i).append(' '); + final var row = grid.getOrDefault(i, Collections.emptyMap()); + for(int j = minHorizontalOffset(); j <= maxHorizontalOffset(); j++) { + final var cell = row.get(j); + final char marker = cell == null ? ' ' : Cell.ROCK.equals(cell) ? '#' : 'o'; + buffer.append(marker); + } + buffer.append('\n'); } + return buffer.toString(); } - return new Input(polymer, rules); + } + + protected Cave getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-14.txt"), false) + .toList(); + return Cave.parse(lines); } @Test public final void part1() { - final var input = parseInput(); - var polymer = input.polymerTemplate(); - final var rules = input.rules(); - final var ruleMap = new HashMap(); - for (final var rule : rules) { - ruleMap.put(new ElementPair(rule.start(), rule.end()), rule); - } - for (int _i = 0; _i < 10; _i++) { - polymer = polymer.applyRules(ruleMap); - } - final var histogram = polymer.histogram(); - System.out.println("Part 1: " + histogram.lastKey().subtract(histogram.firstKey())); + final var cave = getInput(); + final var result = cave.pourSandIntoAbyss(); + + System.out.println("Part 1: " + result); } @Test public final void part2() { - final var input = parseInput(); - var polymer = input.polymerTemplate(); - final var rules = input.rules(); - final var ruleMap = new HashMap(); - for (final var rule : rules) { - ruleMap.put(new ElementPair(rule.start(), rule.end()), rule); - } - for (int _i = 0; _i < 40; _i++) { - polymer = polymer.applyRules(ruleMap); - } - final var histogram = polymer.histogram(); - System.out.println("Part 2: " + histogram.lastKey().subtract(histogram.firstKey())); + final var cave = getInput(); + final var result = cave.fillAperture(); + + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day15.java b/src/test/java/com/macasaet/Day15.java index e93e89f..2fea62c 100644 --- a/src/test/java/com/macasaet/Day15.java +++ b/src/test/java/com/macasaet/Day15.java @@ -1,228 +1,205 @@ package com.macasaet; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.IntPredicate; +import java.util.stream.StreamSupport; + /** - * --- Day 15: Chiton --- + * --- Day 15: Beacon Exclusion Zone --- + * https://adventofcode.com/2022/day/15 */ public class Day15 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-15.txt"), - false); - } - - protected int[][] getGrid() { - final var list = getInput().collect(Collectors.toList()); - final int[][] grid = new int[list.size()][]; - for (int i = 0; i < grid.length; i++) { - final var chars = list.get(i).toCharArray(); - final var row = new int[chars.length]; - for (int j = chars.length; --j >= 0; row[j] = chars[j] - '0') ; - grid[i] = row; + record Coordinate(int x, int y) { + static Coordinate parse(final String string) { + final var components = string.split(", "); + return new Coordinate(Integer.parseInt(components[1].replaceAll("^y=", "")), + Integer.parseInt(components[0].replaceAll("^x=", ""))); } - return grid; - } - - public record Point(int x, int y) { - public int risk(int[][] risks) { - return risks[x][y]; + Map getRow(final Map> grid) { + return grid.computeIfAbsent(x(), ignored -> new HashMap<>()); } + int distanceTo(final Coordinate other) { + return Math.abs(x() - other.x()) + Math.abs(y() - other.y()); + } } - public record Cavern(int[][] grid) { - - public Set predecessors(final Point source) { - final var result = new HashSet(); - if (source.x() > 0) { - result.add(new Point(source.x() - 1, source.y())); - } - if (source.y() > 0) { - result.add(new Point(source.x(), source.y() - 1)); - } - return Collections.unmodifiableSet(result); + public record Sensor(Coordinate location, Coordinate beaconLocation) { + public static Sensor parse(final String line) { + final var location = Coordinate.parse(line.replaceAll("^Sensor at ", "").replaceAll(": closest beacon is at .*$", "")); + final var beaconLocation = Coordinate.parse(line.replaceAll("^.*: closest beacon is at ", "")); + return new Sensor(location, beaconLocation); } - public Set successors(final Point source) { - final var result = new HashSet(); - if (source.x() < grid().length - 1) { - result.add(new Point(source.x() + 1, source.y())); - } - if (source.y() < grid()[source.x()].length - 1) { - result.add(new Point(source.x(), source.y() + 1)); + void setSensor(final Map> grid, IntPredicate includeRow, IntPredicate includeColumn) { + if(includeRow.test(location().x()) && includeColumn.test(location().y())) { + location().getRow(grid).put(location().y(), Item.SENSOR); } - return Collections.unmodifiableSet(result); } - public Cavern explode() { - final int[][] largeGrid = new int[grid.length * 5][]; - for (int i = largeGrid.length; --i >= 0; ) { - largeGrid[i] = new int[grid.length * 5]; - } - for (int i = grid.length; --i >= 0; ) { - for (int j = grid.length; --j >= 0; ) { - largeGrid[i][j] = grid[i][j]; - } + void setBeacon(final Map> grid, IntPredicate includeRow, IntPredicate includeColumn) { + if(includeRow.test(beaconLocation().x()) && includeColumn.test(beaconLocation().y())) { + beaconLocation().getRow(grid).put(beaconLocation().y(), Item.BEACON); } - for (int tileRow = 0; tileRow < 5; tileRow++) { - for (int tileColumn = 0; tileColumn < 5; tileColumn++) { - if (tileRow > 0) { - for (int i = grid.length; --i >= 0; ) { - for (int j = grid.length; --j >= 0; ) { - // copy from row above - int value = largeGrid[(tileRow - 1) * grid.length + i][tileColumn * grid.length + j] + 1; - if (value == 10) { - value = 1; - } - largeGrid[tileRow * grid.length + i][tileColumn * grid.length + j] = value; - } - } - } else if (tileColumn > 0) { - for (int i = grid.length; --i >= 0; ) { - for (int j = grid.length; --j >= 0; ) { - // copy from column to the left - int value = largeGrid[tileRow * grid.length + i][(tileColumn - 1) * grid.length + j] + 1; - if (value == 10) { - value = 1; - } - largeGrid[tileRow * grid.length + i][tileColumn * grid.length + j] = value; - } - } - } - } - } - return new Cavern(largeGrid); } - public long[][] calculateCumulativeRisk() { - final var cumulative = new long[grid().length][]; - for (int i = cumulative.length; --i >= 0; cumulative[i] = new long[grid()[i].length]) ; - final var visited = new HashSet(); - final var queue = new LinkedList(); - final var destination = new Point(grid().length - 1, grid()[grid().length - 1].length - 1); - queue.add(destination); - visited.add(destination); - - while (!queue.isEmpty()) { - final var node = queue.remove(); - final var successors = successors(node); - if (successors.isEmpty()) { - // destination - cumulative[node.x][node.y] = node.risk(grid()); - } else { - var minSuccessorRisk = Long.MAX_VALUE; - for (final var successor : successors) { - if (!visited.contains(successor)) { - throw new IllegalStateException("Successor has not been visited"); - } - minSuccessorRisk = Math.min(minSuccessorRisk, cumulative[successor.x][successor.y]); - } - cumulative[node.x][node.y] = node.risk(grid()) + minSuccessorRisk; + void setCoverageArea(final Map> grid, IntPredicate includeRow, IntPredicate includeColumn) { + final var distance = distanceToBeacon(); + final var x = location().x(); + final var y = location().y(); + + for(int i = 0; i <= distance; i++ ) { + if(!includeRow.test(x + i) && !includeRow.test(x - i)) { + continue; } + final var lowerRow = includeRow.test(x + i) + ? grid.computeIfAbsent(x + i, ignored -> new HashMap<>()) + : new HashMap(); + final var upperRow = includeRow.test(x - i) + ? grid.computeIfAbsent(x - i, ignored -> new HashMap<>()) + : new HashMap(); + for(int j = 0; j <= distance - i; j++ ) { + if(includeColumn.test(y + j)) { + // SE + lowerRow.putIfAbsent(y + j, Item.COVERED); + // NE + upperRow.putIfAbsent(y + j, Item.COVERED); + } + if(includeColumn.test(y - j)) { + // SW + lowerRow.putIfAbsent(y - j, Item.COVERED); - for (final var predecessor : predecessors(node)) { - if (!visited.contains(predecessor)) { - queue.add(predecessor); - visited.add(predecessor); + // NW + upperRow.putIfAbsent(y - j, Item.COVERED); } } } - return cumulative; } - /** - * @return the risk level associated with the path through the cavern that avoids the most chitons - */ - public int lowestRiskThroughTheCavern() { - // the lowest known risk from origin to a given node - final var lowestRiskToNode = new HashMap(); - // the estimated risk from origin to destination if it goes through a given node - final var estimatedRiskThroughNode = new HashMap(); - final var openSet = new PriorityQueue(Comparator.comparing(estimatedRiskThroughNode::get)); - - for (int i = grid().length; --i >= 0; ) { - final var row = grid()[i]; - for (int j = row.length; --j >= 0; ) { - final var point = new Point(i, j); - if (i == 0 && j == 0) { - lowestRiskToNode.put(point, 0); - estimatedRiskThroughNode.put(point, manhattanDistance(point)); - openSet.add(point); - } else { - lowestRiskToNode.put(point, Integer.MAX_VALUE); - estimatedRiskThroughNode.put(point, Integer.MAX_VALUE); - } - } - } + int distanceToBeacon() { + return location().distanceTo(beaconLocation()); + } + } - while(!openSet.isEmpty()) { - final var current = openSet.poll(); - if(current.x() == grid().length - 1 && current.y() == grid()[grid().length - 1].length - 1) { - return lowestRiskToNode.get(current); - } - final var lowestRiskToCurrent = lowestRiskToNode.get(current); - for(final var neighbour : neighbours(current)) { - final var tentativeRisk = lowestRiskToCurrent + neighbour.risk(grid()); - if(tentativeRisk < lowestRiskToNode.get(neighbour)) { - lowestRiskToNode.put(neighbour, tentativeRisk); - estimatedRiskThroughNode.put(neighbour, tentativeRisk + manhattanDistance(neighbour)); - if(!openSet.contains(neighbour)) { - openSet.add(neighbour); - } - } + enum Item { + SENSOR, + BEACON, + COVERED + } + public record CaveMap(Map> grid, int minX, int maxX, int minY, int maxY) { + public int countCoveredCellsInRow(final int x) { + final var row = grid().getOrDefault(x, Collections.emptyMap()); + int result = 0; + for(int j = minY(); j <= maxY(); j++) { + final var cell = row.get(j); + if(cell != null && cell != Item.BEACON) { + result++; } } - throw new IllegalStateException("No path out of the cavern!"); + return result; } - /** - * @param point - * @return - */ - protected int manhattanDistance(Point point) { - return Math.abs(point.x() - (grid().length - 1)) - + Math.abs(point.y() - (grid()[grid().length - 1].length - 1)); + public static CaveMap fromSensors(final Iterable sensors, IntPredicate includeRow, IntPredicate includeColumn) { + int minX = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + final var grid = new HashMap>(); + for(final var sensor : sensors) { + minX = Math.min(minX, sensor.location().x() - sensor.distanceToBeacon()); + maxX = Math.max(maxX, sensor.location().x() + sensor.distanceToBeacon()); + minY = Math.min(minY, sensor.location().y() - sensor.distanceToBeacon()); + maxY = Math.max(maxY, sensor.location().y() + sensor.distanceToBeacon()); + + sensor.setCoverageArea(grid, includeRow, includeColumn); + sensor.setBeacon(grid, includeRow, includeColumn); + sensor.setSensor(grid, includeRow, includeColumn); + } + + return new CaveMap(grid, minX, maxX, minY, maxY); } - public Set neighbours(final Point point) { - final var result = new HashSet(); - if (point.x() > 0) { - result.add(new Point(point.x() - 1, point.y())); - } - if (point.x() < grid().length - 1) { - result.add(new Point(point.x() + 1, point.y())); - } - if (point.y() > 0) { - result.add(new Point(point.x(), point.y() - 1)); - } - if (point.y() < grid()[point.x()].length - 1) { - result.add(new Point(point.x(), point.y() + 1)); + public String toString() { + final var builder = new StringBuilder(); + for(int i = minX(); i <= maxX(); i++) { + builder.append(i).append('\t'); + final var row = grid().getOrDefault(i, Collections.emptyMap()); + for(int j = minY(); j <= maxY(); j++) { + final var item = row.get(j); + final var marker = item == null + ? '.' + : item == Item.BEACON + ? 'B' + : item == Item.SENSOR + ? 'S' + : '#'; + builder.append(marker); + } + builder.append('\n'); } - return Collections.unmodifiableSet(result); + return builder.toString(); } + } + protected CaveMap getInput(final IntPredicate includeRow, IntPredicate includeColumn) { + final var sensors = getSensors(); + return CaveMap.fromSensors(sensors, includeRow, includeColumn); + } + + protected static List getSensors() { + return StreamSupport.stream(new LineSpliterator("day-15.txt"), false) + .map(Sensor::parse) + .toList(); } @Test public final void part1() { - final var grid = getGrid(); - final var cavern = new Cavern(grid); - System.out.println("Part 1: " + cavern.lowestRiskThroughTheCavern()); + final int rowOfInterest = 2_000_000; + final var map = getInput(row -> row == rowOfInterest, _column -> true); + final var result = map.countCoveredCellsInRow(rowOfInterest); + + System.out.println("Part 1: " + result); } @Test public final void part2() { - final var grid = getGrid(); - final var cavern = new Cavern(grid).explode(); - System.out.println("Part 2: " + cavern.lowestRiskThroughTheCavern()); + final int max = 4_000_000; + final var sensors = getSensors(); + for(final var sensor : sensors) { + final var x = sensor.location().x(); + final var y = sensor.location().y(); + final var reach = sensor.distanceToBeacon(); + // Find all the points just outside this sensor's reach + for(int horizontalOffset = 0; horizontalOffset <= reach + 1; horizontalOffset++) { + final var verticalOffset = reach + 1 - horizontalOffset; + Assertions.assertEquals(horizontalOffset + verticalOffset, reach + 1); + for(final var candidate : Arrays.asList(new Coordinate(x + verticalOffset, y + horizontalOffset), // SE + new Coordinate(x + verticalOffset, y - horizontalOffset), // SW + new Coordinate(x - verticalOffset, y + horizontalOffset), // NE + new Coordinate(x - verticalOffset, y - horizontalOffset))) { // NW + if(candidate.x() < 0 || candidate.y() < 0 || candidate.x() > max || candidate.y() > max) { + continue; + } + Assertions.assertTrue(candidate.distanceTo(sensor.location()) > sensor.distanceToBeacon()); + // Check if the point is also outside the reach of every other sensor + if(sensors.stream().allMatch(other -> candidate.distanceTo(other.location()) > other.distanceToBeacon())) { + final long result = (long)candidate.y() * 4_000_000l + (long)candidate.x(); + System.out.println("Part 2: " + result); + return; + } + } + } + } + throw new IllegalStateException("No uncovered point found"); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day16.java b/src/test/java/com/macasaet/Day16.java deleted file mode 100644 index b3eb4b0..0000000 --- a/src/test/java/com/macasaet/Day16.java +++ /dev/null @@ -1,352 +0,0 @@ -package com.macasaet; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * --- Day 16: Packet Decoder --- - */ -public class Day16 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-16.txt"), - false); - } - - public interface Packet { - long version(); - - void accept(PacketVisitor packetVisitor); - - long evaluate(); - } - - public record Literal(long version, long value) implements Packet { - - public void accept(PacketVisitor packetVisitor) { - packetVisitor.visit(this); - } - - public long evaluate() { - return value(); - } - } - - public enum OperatorType { - SUM { - public long evaluate(List operands) { - return operands.stream().mapToLong(Packet::evaluate).sum(); - } - }, - PRODUCT { - public long evaluate(List operands) { - return operands.stream().mapToLong(Packet::evaluate).reduce(1, (x, y) -> x * y); - } - }, - MINIMUM { - public long evaluate(List operands) { - return operands.stream().mapToLong(Packet::evaluate).min().orElseThrow(); - } - }, - MAXIMUM { - public long evaluate(List operands) { - return operands.stream().mapToLong(Packet::evaluate).max().orElseThrow(); - } - }, - GREATER_THAN { - public long evaluate(List operands) { - if (operands.size() != 2) { - throw new IllegalArgumentException("Invalid operand list for \"greater than\" operator: " + operands); - } - final var x = operands.get(0).evaluate(); - final var y = operands.get(1).evaluate(); - return x > y ? 1 : 0; - } - }, - LESS_THAN { - public long evaluate(List operands) { - if (operands.size() != 2) { - throw new IllegalStateException("Invalid operand list for \"less than\" operator: " + operands); - } - final var x = operands.get(0).evaluate(); - final var y = operands.get(1).evaluate(); - return x < y ? 1 : 0; - } - }, - EQUAL_TO { - public long evaluate(List operands) { - if (operands.size() != 2) { - throw new IllegalStateException("Invalid operand list for \"equal to\" operator: " + operands); - } - final var x = operands.get(0).evaluate(); - final var y = operands.get(1).evaluate(); - return x == y ? 1 : 0; - } - }; - - public abstract long evaluate(List operands); - - public static OperatorType forId(final int typeId) { - return switch (typeId) { - case 0 -> SUM; - case 1 -> PRODUCT; - case 2 -> MINIMUM; - case 3 -> MAXIMUM; - case 5 -> GREATER_THAN; - case 6 -> LESS_THAN; - case 7 -> EQUAL_TO; - default -> throw new IllegalArgumentException("Invalid operator type ID: " + typeId); - }; - } - } - - public record Operator(long version, OperatorType operatorType, List operands) implements Packet { - - public void accept(PacketVisitor packetVisitor) { - packetVisitor.enter(this); - for (final var subPacket : operands()) { - subPacket.accept(packetVisitor); - } - packetVisitor.exit(this); - } - - public long evaluate() { - return operatorType().evaluate(operands()); - } - } - - public interface PacketVisitor { - void visit(Literal literal); - - void enter(Operator operator); - - void exit(Operator operator); - } - - public static class PacketBuilder { - - private long version; - private long typeId; - private OptionalLong literalValue = OptionalLong.empty(); - private final List subPackets = new ArrayList<>(); - - public Packet readHex(final String hexString) { - final var hexDigits = hexString.toCharArray(); - final var bits = hexToBits(hexDigits); - read(bits, 0); - return toPacket(); - } - - public int read(final List bits, int transmissionCursor) { - final var versionBits = bits.subList(transmissionCursor, transmissionCursor + 3); - transmissionCursor += 3; - this.version = toLong(versionBits); - - final var typeBits = bits.subList(transmissionCursor, transmissionCursor + 3); - transmissionCursor += 3; - this.typeId = toLong(typeBits); - - // TODO consider adding methods to parse each type specifically - if (this.typeId == 4) { - boolean finalGroup = false; - final var literalBits = new ArrayList(); - while (!finalGroup) { - final var groupBits = bits.subList(transmissionCursor, transmissionCursor + 5); - transmissionCursor += 5; - finalGroup = groupBits.get(0) == 0; - literalBits.addAll(groupBits.subList(1, 5)); - } - if (literalBits.size() > 63) { - throw new IllegalArgumentException("Literal is too large for an long: " + literalBits.size()); - } - literalValue = OptionalLong.of(toLong(literalBits)); - return transmissionCursor; - } else { - final var lengthTypeIdBits = bits.subList(transmissionCursor, transmissionCursor + 1); - transmissionCursor += 1; - final var lengthTypeId = toLong(lengthTypeIdBits); - if (lengthTypeId == 0) { - final var lengthOfSubPacketsBits = bits.subList(transmissionCursor, transmissionCursor + 15); - transmissionCursor += 15; - final var lengthOfSubPackets = toLong(lengthOfSubPacketsBits); - int bitsRead = 0; - while (bitsRead < lengthOfSubPackets) { - final var subPacketBuilder = new PacketBuilder(); - final var newCursor = subPacketBuilder.read(bits, transmissionCursor); - final var subPacketSize = newCursor - transmissionCursor; // size of sub-packet in bits - transmissionCursor = newCursor; - - subPackets.add(subPacketBuilder.toPacket()); - bitsRead += subPacketSize; - } - return transmissionCursor; - } else if (lengthTypeId == 1) { - final var numSubPacketsBits = bits.subList(transmissionCursor, transmissionCursor + 11); - transmissionCursor += 11; - final var numSubPackets = toLong(numSubPacketsBits); - for (int packetsRead = 0; packetsRead < numSubPackets; packetsRead++) { - final var subPacketBuilder = new PacketBuilder(); - transmissionCursor = subPacketBuilder.read(bits, transmissionCursor); - subPackets.add(subPacketBuilder.toPacket()); - } - return transmissionCursor; - } else { - throw new IllegalArgumentException("Invalid length type ID: " + lengthTypeId); - } - } - } - - public Packet toPacket() { - if (typeId == 4) { - return new Literal(version, literalValue.orElseThrow()); - } else { - return new Operator(version, OperatorType.forId((int) typeId), subPackets); - } - } - - protected long toLong(final List bits) { - long result = 0; - for (int i = 0; i < bits.size(); i++) { - final var bit = bits.get(i); - if (bit == 1) { - final long shiftDistance = bits.size() - i - 1; - result |= 1L << shiftDistance; - } else if (bit != 0) { - throw new IllegalArgumentException("Invalid bit representation of an integer: " + bits); - } - } - return result; - } - - protected List hexToBits(final char[] hexDigits) { - final var result = new ArrayList(hexDigits.length * 4); - for (final var digit : hexDigits) { - final var bits = switch (digit) { - case '0' -> Arrays.asList((byte) 0, (byte) 0, (byte) 0, (byte) 0); - case '1' -> Arrays.asList((byte) 0, (byte) 0, (byte) 0, (byte) 1); - case '2' -> Arrays.asList((byte) 0, (byte) 0, (byte) 1, (byte) 0); - case '3' -> Arrays.asList((byte) 0, (byte) 0, (byte) 1, (byte) 1); - case '4' -> Arrays.asList((byte) 0, (byte) 1, (byte) 0, (byte) 0); - case '5' -> Arrays.asList((byte) 0, (byte) 1, (byte) 0, (byte) 1); - case '6' -> Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 0); - case '7' -> Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 1); - case '8' -> Arrays.asList((byte) 1, (byte) 0, (byte) 0, (byte) 0); - case '9' -> Arrays.asList((byte) 1, (byte) 0, (byte) 0, (byte) 1); - case 'A', 'a' -> Arrays.asList((byte) 1, (byte) 0, (byte) 1, (byte) 0); - case 'B', 'b' -> Arrays.asList((byte) 1, (byte) 0, (byte) 1, (byte) 1); - case 'C', 'c' -> Arrays.asList((byte) 1, (byte) 1, (byte) 0, (byte) 0); - case 'D', 'd' -> Arrays.asList((byte) 1, (byte) 1, (byte) 0, (byte) 1); - case 'E', 'e' -> Arrays.asList((byte) 1, (byte) 1, (byte) 1, (byte) 0); - case 'F', 'f' -> Arrays.asList((byte) 1, (byte) 1, (byte) 1, (byte) 1); - default -> throw new IllegalStateException("Unexpected value: " + digit); - }; - result.addAll(bits); - } - return Collections.unmodifiableList(result); - } - } - - @Test - public final void testParseLiteral() { - // given - final var input = "D2FE28"; - final var builder = new PacketBuilder(); - - // when - final var result = builder.readHex(input); - - // then - assertTrue(result instanceof Literal); - final var literal = (Literal) result; - assertEquals(2021, literal.value); - } - - @Test - public final void testOperatorWithTwoSubPackets() { - // given - final var input = "38006F45291200"; - final var builder = new PacketBuilder(); - - // when - final var result = builder.readHex(input); - - // then - assertTrue(result instanceof Operator); - final var operator = (Operator) result; - assertEquals(1, operator.version()); - assertEquals(OperatorType.LESS_THAN, operator.operatorType()); - assertEquals(2, operator.operands().size()); - final var a = (Literal) operator.operands().get(0); - assertEquals(10, a.value()); - final var b = (Literal) operator.operands().get(1); - assertEquals(20, b.value()); - } - - @Test - public final void part1() { - final var line = getInput().collect(Collectors.toList()).get(0); - final var builder = new PacketBuilder(); - final var packet = builder.readHex(line); - class VersionSummer implements PacketVisitor { - - int sum = 0; - - public void visit(Literal literal) { - sum += literal.version(); - } - - public void enter(Operator operator) { - } - - public void exit(Operator operator) { - sum += operator.version(); - } - } - final var summer = new VersionSummer(); - packet.accept(summer); - - System.out.println("Part 1: " + summer.sum); - } - - @Test - public final void part2() { - final var line = getInput().collect(Collectors.toList()).get(0); - final var builder = new PacketBuilder(); - final var packet = builder.readHex(line); - System.out.println("Part 2: " + packet.evaluate()); - } - - @Nested - public class PacketBuilderTest { - @Test - public void testToInt() { - // given - final var builder = new PacketBuilder(); - // when - // then - assertEquals(2021, builder.toLong(Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 1))); - } - - @Test - public final void testMaths() { - assertEquals(3, new PacketBuilder().readHex("C200B40A82").evaluate()); - assertEquals(54, new PacketBuilder().readHex("04005AC33890").evaluate()); - assertEquals(7, new PacketBuilder().readHex("880086C3E88112").evaluate()); - assertEquals(9, new PacketBuilder().readHex("CE00C43D881120").evaluate()); - assertEquals(1, new PacketBuilder().readHex("D8005AC2A8F0").evaluate()); - assertEquals(0, new PacketBuilder().readHex("F600BC2D8F").evaluate()); - assertEquals(0, new PacketBuilder().readHex("9C005AC2F8F0").evaluate()); - assertEquals(1, new PacketBuilder().readHex("9C0141080250320F1802104A08").evaluate()); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day17.java b/src/test/java/com/macasaet/Day17.java deleted file mode 100644 index 66dc6f1..0000000 --- a/src/test/java/com/macasaet/Day17.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.macasaet; - -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 17: Trick Shot --- - */ -public class Day17 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-17.txt"), - false); - } - - /** - * The target area in a large ocean trench - */ - public record Target(int minX, int maxX, int minY, int maxY) { - } - - /** - * A probe at a given point in time - */ - public record Probe(int xVelocity, int yVelocity, int x, int y) { - - /** - * Launch a probe from the origin - * - * @param xVelocity the starting horizontal velocity - * @param yVelocity the starting vertical velocity - * @return the initial state of the probe at the origin - */ - public static Probe launch(final int xVelocity, final int yVelocity) { - return new Probe(xVelocity, yVelocity, 0, 0); - } - - public Optional step() { - if(x > 0 && x + xVelocity < 0) { - return Optional.empty(); - } - if(y < 0 && y + yVelocity > 0) { - return Optional.empty(); - } - final int newX = x + xVelocity; - final int newY = y + yVelocity; - final int newXVelocity = xVelocity > 0 - ? xVelocity - 1 - : xVelocity < 0 - ? xVelocity + 1 - : xVelocity; - return Optional.of(new Probe(newXVelocity, - yVelocity - 1, - newX, - newY)); - } - - public Optional peak(final Target target) { - var peak = Integer.MIN_VALUE; - var p = Optional.of(this); - while (p.isPresent()) { - final var probe = p.get(); - peak = Math.max(peak, probe.y()); - if (probe.x() < target.minX() && probe.y() < target.minY()) { - // short - return Optional.empty(); - } else if (probe.x() > target.maxX()) { - // long - return Optional.empty(); - } else if (probe.x() >= target.minX() && probe.x() <= target.maxX() - && probe.y() >= target.minY() && probe.y() <= target.maxY()) { - return Optional.of(peak); - } - p = probe.step(); - } - return Optional.empty(); - } - - } - - @Test - public final void part1() { - final var line = getInput().collect(Collectors.toList()).get(0); - final var bounds = line.replaceFirst("target area: ", "").split(", "); - final var xBounds = bounds[0].replaceFirst("x=", "").split("\\.\\."); - final var yBounds = bounds[1].replaceFirst("y=", "").split("\\.\\."); - final int minX = Integer.parseInt(xBounds[0]); - final int maxX = Integer.parseInt(xBounds[1]); - final int minY = Integer.parseInt(yBounds[0]); - final int maxY = Integer.parseInt(yBounds[1]); - final var target = new Target(minX, maxX, minY, maxY); - - final var max = IntStream.range(0, 50) - .parallel() - .mapToObj(x -> IntStream.range(-50, 50) - .parallel() - .mapToObj(y -> Probe.launch(x, y)) - ).flatMap(probes -> probes) - .flatMapToInt(probe -> probe.peak(target) - .stream() - .mapToInt(peak -> peak)) - .max(); - - - System.out.println("Part 1: " + max.getAsInt()); - } - - @Test - public final void part2() { - final var line = getInput().collect(Collectors.toList()).get(0); - final var bounds = line.replaceFirst("target area: ", "").split(", "); - final var xBounds = bounds[0].replaceFirst("x=", "").split("\\.\\."); - final var yBounds = bounds[1].replaceFirst("y=", "").split("\\.\\."); - final int minX = Integer.parseInt(xBounds[0]); - final int maxX = Integer.parseInt(xBounds[1]); - final int minY = Integer.parseInt(yBounds[0]); - final int maxY = Integer.parseInt(yBounds[1]); - final var target = new Target(minX, maxX, minY, maxY); - int count = 0; - for (int x = 1; x <= 400; x++) { - for (int y = -400; y <= 400; y++) { - final var probe = Probe.launch(x, y); - if (probe.peak(target).isPresent()) { - count++; - } - } - } - - System.out.println("Part 2: " + count); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day18.java b/src/test/java/com/macasaet/Day18.java index afd2133..3c29e60 100644 --- a/src/test/java/com/macasaet/Day18.java +++ b/src/test/java/com/macasaet/Day18.java @@ -1,432 +1,176 @@ package com.macasaet; -import static com.macasaet.Day18.SnailfishNumber.parse; -import static org.junit.jupiter.api.Assertions.assertEquals; +import org.junit.jupiter.api.Test; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - /** - * --- Day 18: Snailfish --- + * --- Day 18: Boiling Boulders --- + * https://adventofcode.com/2022/day/18 */ public class Day18 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-18.txt"), - false); - } + public static final int SCAN_DIMENSION = 32; - /** - * An element of a {@link SnailfishNumber} - */ - public interface Token { + protected static Droplet getInput() { + final var cubeCoordinates = StreamSupport.stream(new LineSpliterator("day-18.txt"), false) + .map(Cube::parse) + .toList(); + return new Droplet(cubeCoordinates); } - /** - * A symbol in a {@link SnailfishNumber} - */ - public enum Symbol implements Token { - START_PAIR, - END_PAIR, - SEPARATOR, + @Test + public final void part1() { + final var droplet = getInput(); + final var result = droplet.surfaceArea(CubeType.Air); + System.out.println("Part 1: " + result); } - /** - * An integer in a {@link SnailfishNumber} - */ - public record Number(int value) implements Token { + @Test + public final void part2() { + final var droplet = getInput(); + droplet.immerse(); + final var result = droplet.surfaceArea(CubeType.Water); + + System.out.println("Part 2: " + result); } - /** - * "Snailfish numbers aren't like regular numbers. Instead, every snailfish number is a pair - an ordered list of - * two elements. Each element of the pair can be either a regular number or another pair." - */ - public record SnailfishNumber(List expression) { + public enum CubeType { + Air, + Lava, + Water, + } - public SnailfishNumber(final String string) { - this(parse(string)); + record Cube(int x, int y, int z) { + public static Cube parse(final String line) { + final var components = line.split(","); + return new Cube(Integer.parseInt(components[0]), Integer.parseInt(components[1]), Integer.parseInt(components[2])); } - /** - * "The magnitude of a pair is 3 times the magnitude of its left element plus 2 times the magnitude of its right - * element. The magnitude of a regular number is just that number." - * - * @return the snailfish number distilled into a single value - */ - public int magnitude() { - var stack = new LinkedList(); - for (final var token : expression) { - if (token.equals(Symbol.START_PAIR)) { - } else if (token instanceof final Number number) { - stack.push(number.value()); - } else if (token.equals(Symbol.END_PAIR)) { - final var rightValue = stack.pop(); - final var leftValue = stack.pop(); - stack.push(leftValue * 3 + rightValue * 2); - } - } - if (stack.size() != 1) { - throw new IllegalStateException("Invalid stack: " + stack); - } - return stack.get(0); + public CubeType getType(final CubeType[][][] grid) { + return grid[x()][y()][z()]; } - /** - * Repeatedly explode or split this snailfish number until those operations can no longer be performed. - * - * @return a representation of this snailfish number that cannot be reduced any further - */ - public SnailfishNumber reduce() { - var newExpression = expression; - while (true) { - var explosionIndex = getExplosionIndex(newExpression); - var splitIndex = getSplitIndex(newExpression); - if (explosionIndex > 0) { - newExpression = explode(newExpression, explosionIndex); - } else if (splitIndex > 0) { - newExpression = split(newExpression, splitIndex); - } else { - break; - } - } - return new SnailfishNumber(newExpression); + public void setType(final CubeType[][][] grid, final CubeType type) { + grid[x()][y()][z()] = type; } + } - /** - * Add a snailfish number. Note, this operation is *not commutative*: `x.add(y)` is not the same as `y.add(x)`. - * Also note that the process of addition may yield a snailfish number that needs to be reduced. - * - * @param addend the number to add to this snailfish number - * @return the sum of the snailfish numbers (may need to be reduced - * @see SnailfishNumber#reduce() - */ - public SnailfishNumber add(final SnailfishNumber addend) { - final var tokens = new ArrayList(); - tokens.add(Symbol.START_PAIR); - tokens.addAll(expression()); - tokens.add(Symbol.SEPARATOR); - tokens.addAll(addend.expression()); - tokens.add(Symbol.END_PAIR); - return new SnailfishNumber(Collections.unmodifiableList(tokens)); - } + public static class Droplet { - static List parse(final String expression) { - final var result = new ArrayList(); - for (int i = 0; i < expression.length(); i++) { - final var c = expression.charAt(i); - if (c == '[') { - result.add(Symbol.START_PAIR); - } else if (c == ']') { - result.add(Symbol.END_PAIR); - } else if (c == ',') { - result.add(Symbol.SEPARATOR); - } else if (c >= '0' && c <= '9') { - int endExclusive = i + 1; - while (endExclusive < expression.length()) { - final var d = expression.charAt(endExclusive); - if (d < '0' || d > '9') { - break; - } - endExclusive++; - } - final int value = Integer.parseInt(expression.substring(i, endExclusive)); - result.add(new Number(value)); - i = endExclusive - 1; + private final Collection cubes; + private final CubeType[][][] grid; + + public Droplet(final Collection cubes) { + final CubeType[][][] grid = new CubeType[SCAN_DIMENSION][][]; + for (int i = SCAN_DIMENSION; --i >= 0; ) { + grid[i] = new CubeType[SCAN_DIMENSION][]; + for (int j = grid[i].length; --j >= 0; ) { + grid[i][j] = new CubeType[SCAN_DIMENSION]; + for (int k = grid[i][j].length; --k >= 0; grid[i][j][k] = CubeType.Air); } } - return Collections.unmodifiableList(result); - } - - /** - * Split a regular number. "To split a regular number, replace it with a pair; the left element of the pair - * should be the regular number divided by two and rounded down, while the right element of the pair should be - * the regular number divided by two and rounded up." - * - * @param expression a raw representation of a snailfish number - * @param index the index of a regular number to split. The caller is responsible for ensuring that this number - * can be split and that it is the most appropriate action to take. - * @return the reduced snailfish number in raw tokens - */ - List split(final List expression, final int index) { - final var result = new ArrayList(); - if (index > 0) { - result.addAll(expression.subList(0, index)); + for (final var cube : cubes) { + cube.setType(grid, CubeType.Lava); } - final var regularNumber = (Number) expression.get(index); - - final var left = Math.floorDiv(regularNumber.value(), 2); - final var right = (int) Math.ceil(regularNumber.value() / 2.0d); - - result.add(Symbol.START_PAIR); - result.add(new Number(left)); - result.add(Symbol.SEPARATOR); - result.add(new Number(right)); - result.add(Symbol.END_PAIR); - if (index + 1 < expression.size()) { - result.addAll(expression.subList(index + 1, expression.size())); - } - return Collections.unmodifiableList(result); + this.grid = grid; + this.cubes = cubes; } - /** - * Determine whether any of the regular numbers can be split and if so, the highest-priority number to split. - * - * @param expression a raw representation of a snailfish number - * @return the index of the best regular number to split or -1 if none can be split - */ - int getSplitIndex(final List expression) { - for (int i = 0; i < expression.size(); i++) { - final var token = expression.get(i); - if (token instanceof final Number number) { - if (number.value() >= 10) { - return i; - + public int surfaceArea(final CubeType element) { + int result = 0; + for (final var cube : getCubes()) { + result += exposedFaces(cube, element); + } + return result; + } + + public void immerse() { + final var grid = getGrid(); + final var queue = new ArrayDeque(); + final var encountered = new HashSet(); + final var origin = new Cube(0, 0, 0); + encountered.add(origin); + queue.add(origin); + + while (!queue.isEmpty()) { + final var cube = queue.remove(); + for (final var neighbour : neighbours(cube).stream().filter(neighbour -> neighbour.getType(grid) == CubeType.Air).toList()) { + if (!encountered.contains(neighbour)) { + encountered.add(neighbour); + queue.add(neighbour); } } + cube.setType(grid, CubeType.Water); } - return -1; } - /** - * Explode the pair starting at `index`. "To explode a pair, the pair's left value is added to the first regular - * number to the left of the exploding pair (if any), and the pair's right value is added to the first regular - * number to the right of the exploding pair (if any). Exploding pairs will always consist of two regular - * numbers. Then, the entire exploding pair is replaced with the regular number 0." - * - * @param expression a raw representation of a snailfish number - * @param index the index of the opening brace of the pair to explode. The caller must ensure that an explosion - * operation is valid at the index and that the index represents the most appropriate pair to - * explode. - * @return the reduced expression in raw format - */ - List explode(final List expression, final int index) { - final var result = new ArrayList<>(expression); - final int leftNumberIndex = index + 1; - final int rightNumberIndex = index + 3; - final int left = ((Number) expression.get(leftNumberIndex)).value(); - final int right = ((Number) expression.get(rightNumberIndex)).value(); - int leftIndex = -1; - int rightIndex = -1; - - for (int i = index; --i >= 0; ) { - final var c = expression.get(i); - if (c instanceof Number) { - leftIndex = i; - break; - } + protected Collection neighbours(final Cube cube) { + final var result = new HashSet(); + final var x = cube.x(); + final var y = cube.y(); + final var z = cube.z(); + if (x > 0) { + result.add(new Cube(x - 1, y, z)); } - for (int i = rightNumberIndex + 1; i < expression.size(); i++) { - final var c = expression.get(i); - if (c instanceof Number) { - rightIndex = i; - break; - } + if (x < SCAN_DIMENSION - 1) { + result.add(new Cube(x + 1, y, z)); } - if (leftIndex < 0 && rightIndex < 0) { - throw new IllegalArgumentException("Cannot be exploded: " + expression); + if (y > 0) { + result.add(new Cube(x, y - 1, z)); } - // "the pair's left value is added to the first regular number to the left of the exploding pair (if any)" - if (leftIndex > 0) { - final int leftOperand = ((Number) expression.get(leftIndex)).value(); - final int replacement = leftOperand + left; - result.set(leftIndex, new Number(replacement)); + if (y < SCAN_DIMENSION - 1) { + result.add(new Cube(x, y + 1, z)); } - // "the pair's right value is added to the first regular number to the right of the exploding pair (if any)" - if (rightIndex > 0) { - final int rightOperand = ((Number) expression.get(rightIndex)).value(); - final int replacement = rightOperand + right; - result.set(rightIndex, new Number(replacement)); + if (z > 0) { + result.add(new Cube(x, y, z - 1)); } - // "Exploding pairs will always consist of two regular numbers. Then, the entire exploding pair is replaced - // with the regular number 0." - result.set(index, new Number(0)); - result.remove(index + 1); - result.remove(index + 1); - result.remove(index + 1); - result.remove(index + 1); - return Collections.unmodifiableList(result); - } - - /** - * @param expression a raw representation of a snailfish number - * @return the index of the most appropriate pair to explode (opening brace) or -1 if no explosion is appropriate - */ - int getExplosionIndex(final List expression) { - int depth = -1; - int maxDepth = Integer.MIN_VALUE; - int result = -1; - for (int i = 0; i < expression.size(); i++) { - final var token = expression.get(i); - if (token == Symbol.START_PAIR) { - depth++; - } else if (token == Symbol.END_PAIR) { - depth--; - } - if (depth > maxDepth) { - maxDepth = depth; - result = i; - } + if (z < SCAN_DIMENSION - 1) { + result.add(new Cube(x, y, z + 1)); } - return result > 3 ? result : -1; + return Collections.unmodifiableSet(result); } - } + public int exposedFaces(final Cube cube, final CubeType element) { + final int x = cube.x(); + final int y = cube.y(); + final int z = cube.z(); + final var grid = getGrid(); - @Nested - public class SnailfishNumberTest { - - @Test - public final void testAdd() { - assertEquals(new SnailfishNumber("[[[[0,7],4],[[7,8],[6,0]]],[8,1]]"), - new SnailfishNumber("[[[[4,3],4],4],[7,[[8,4],9]]]") - .add(new SnailfishNumber("[1,1]")) - .reduce()); - // either this example is broken or my bug is not triggered in the real puzzle input T_T -// assertEquals(new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]"), -// new SnailfishNumber("[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]]") -// .add(new SnailfishNumber("[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]]")) -// .reduce()); - } - - @Test - public final void testAddList() { - // given - final var lines = """ - [[[0,[4,5]],[0,0]],[[[4,5],[2,6]],[9,5]]] - [7,[[[3,7],[4,3]],[[6,3],[8,8]]]] - [[2,[[0,8],[3,4]]],[[[6,7],1],[7,[1,6]]]] - [[[[2,4],7],[6,[0,5]]],[[[6,8],[2,8]],[[2,1],[4,5]]]] - [7,[5,[[3,8],[1,4]]]] - [[2,[2,2]],[8,[8,1]]] - [2,9] - [1,[[[9,3],9],[[9,0],[0,7]]]] - [[[5,[7,4]],7],1] - [[[[4,2],2],6],[8,7]]"""; - final var list = Arrays.stream(lines.split("\n")) - .map(SnailfishNumber::new) - .collect(Collectors.toList()); - - // when - var sum = list.get(0); - for (final var addend : list.subList(1, list.size())) { - sum = sum.add(addend).reduce(); + int result = 0; + if (grid[x + 1][y][z] == element) { + result++; } - - // then - assertEquals(new SnailfishNumber("[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]"), - sum); - } - - @Test - public final void testSplit() { - final var instance = new SnailfishNumber(Collections.emptyList()); - - assertEquals(parse("[5, 5]"), - instance.split(parse("10"), 0)); - assertEquals(parse("[5, 6]"), - instance.split(parse("11"), 0)); - assertEquals(parse("[6, 6]"), - instance.split(parse("12"), 0)); - } - - @Test - public final void testExplosionIndex() { - final var instance = new SnailfishNumber(Collections.emptyList()); - assertEquals(4, - instance.getExplosionIndex(parse("[[[[[9,8],1],2],3],4]"))); - assertEquals(12, - instance.getExplosionIndex(parse("[7,[6,[5,[4,[3,2]]]]]"))); - assertEquals(10, - instance.getExplosionIndex(parse("[[6,[5,[4,[3,2]]]],1]"))); - assertEquals(10, - instance.getExplosionIndex(parse("[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]"))); - assertEquals(24, - instance.getExplosionIndex(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"))); - } - - @Test - public final void testExplode() { - final var instance = new SnailfishNumber(Collections.emptyList()); - assertEquals(parse("[[[[0,9],2],3],4]"), - instance - .explode(parse("[[[[[9,8],1],2],3],4]"), 4)); - assertEquals(parse("[7,[6,[5,[7,0]]]]"), - instance - .explode(parse("[7,[6,[5,[4,[3,2]]]]]"), 12)); - assertEquals(parse("[[6,[5,[7,0]]],3]"), - instance - .explode(parse("[[6,[5,[4,[3,2]]]],1]"), 10)); - assertEquals(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"), - instance - .explode(parse("[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]"), 10)); - assertEquals(parse("[[3,[2,[8,0]]],[9,[5,[7,0]]]]"), - instance - .explode(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"), 24)); - } - - @Test - public final void testMagnitude() { - assertEquals(29, new SnailfishNumber("[9,1]").magnitude()); - assertEquals(21, new SnailfishNumber("[1,9]").magnitude()); - assertEquals(143, new SnailfishNumber("[[1,2],[[3,4],5]]").magnitude()); - assertEquals(1384, new SnailfishNumber("[[[[0,7],4],[[7,8],[6,0]]],[8,1]]").magnitude()); - assertEquals(445, new SnailfishNumber("[[[[1,1],[2,2]],[3,3]],[4,4]]").magnitude()); - assertEquals(791, new SnailfishNumber("[[[[3,0],[5,3]],[4,4]],[5,5]]").magnitude()); - assertEquals(1137, new SnailfishNumber("[[[[5,0],[7,4]],[5,5]],[6,6]]").magnitude()); - assertEquals(3488, new SnailfishNumber("[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]").magnitude()); - assertEquals(3993, new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]").magnitude()); - } - - @Test - public final void verifyStableState() { - // given - final var original = new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]"); - - // when - final var reduced = original.reduce(); - - // then - assertEquals(original, reduced); + if (x <= 0 || grid[x - 1][y][z] == element) { + result++; + } + if (grid[x][y + 1][z] == element) { + result++; + } + if (y == 0 || grid[x][y - 1][z] == element) { + result++; + } + if (grid[x][y][z + 1] == element) { + result++; + } + if (z == 0 || grid[x][y][z - 1] == element) { + result++; + } + return result; } - } - - @Test - public final void part1() { - final var list = - getInput().map(SnailfishNumber::new).collect(Collectors.toList()); - var sum = list.get(0); - for (final var addend : list.subList(1, list.size())) { - sum = sum.add(addend).reduce(); + public Collection getCubes() { + return cubes; } - System.out.println("Part 1: " + sum.magnitude()); - } - @Test - public final void part2() { - final var list = - getInput().map(SnailfishNumber::new).collect(Collectors.toList()); - int max = Integer.MIN_VALUE; - for (final var x : list) { - for (final var y : list) { - if (x.equals(y)) { - continue; - } - final var sum = x.add(y).reduce(); - final var magnitude = sum.magnitude(); - if (magnitude > max) { - max = magnitude; - } - } + public CubeType[][][] getGrid() { + return grid; } - System.out.println("Part 2: " + max); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day19.java b/src/test/java/com/macasaet/Day19.java deleted file mode 100644 index a11c202..0000000 --- a/src/test/java/com/macasaet/Day19.java +++ /dev/null @@ -1,395 +0,0 @@ -package com.macasaet; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * --- Day 19: Beacon Scanner --- - */ -public class Day19 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-19.txt"), - false); - } - - protected List getScanners() { - final var list = getInput().toList(); - final var result = new ArrayList(); - var observations = new HashSet(); - int id = -1; - for (final var line : list) { - if (line.startsWith("--- scanner ")) { - id = Integer.parseInt(line - .replaceFirst("^--- scanner ", "") - .replaceFirst(" ---$", "")); - } else if (!line.isBlank()) { - observations.add(Position.parse(line)); - } else { // line is blank - result.add(new Scanner(id, Collections.unmodifiableSet(observations))); - observations = new HashSet<>(); - id = -1; - } - } - result.add(new Scanner(id, Collections.unmodifiableSet(observations))); - return Collections.unmodifiableList(result); - } - - public enum Direction { - POSITIVE_X { - public Position face(Position position) { - return position; - } - }, - NEGATIVE_X { - public Position face(Position position) { - return new Position(-position.x(), position.y(), -position.z()); - } - }, - POSITIVE_Y { - public Position face(Position position) { - return new Position(position.y(), -position.x(), position.z()); - } - }, - NEGATIVE_Y { - public Position face(Position position) { - return new Position(-position.y(), position.x(), position.z()); - } - }, - POSITIVE_Z { - public Position face(Position position) { - return new Position(position.z(), position.y(), -position.x()); - } - }, - NEGATIVE_Z { - public Position face(Position position) { - return new Position(-position.z(), position.y(), position.x()); - } - }; - - public abstract Position face(final Position position); - - } - - public enum Rotation { - r0 { - public Position rotate(final Position position) { - return position; - } - }, - r90 { - public Position rotate(Position position) { - return new Position(position.x(), -position.z(), position.y()); - } - }, - r180 { - public Position rotate(Position position) { - return new Position(position.x(), -position.y(), -position.z()); - } - }, - r270 { - public Position rotate(Position position) { - return new Position(position.x(), position.z(), -position.y()); - } - }; - - public abstract Position rotate(final Position position); - } - - public record Transformation(Direction direction, Rotation rotation) { - /** - * Look at a position from a specific orientation - * - * @param position a position relative to one point of view - * @return the same position relative to a different point of view - */ - public Position reorient(final Position position) { - return rotation.rotate(direction.face(position)); - } - - } - - public record Position(int x, int y, int z) { - public static Position parse(final String line) { - final var components = line.split(","); - return new Position(Integer.parseInt(components[0]), - Integer.parseInt(components[1]), - Integer.parseInt(components[2])); - } - - public Position plus(Position amount) { - return new Position(x() + amount.x(), y() + amount.y(), z() + amount.z()); - } - - public Position minus(final Position other) { - return new Position(x() - other.x(), y() - other.y(), z() - other.z()); - } - } - - public interface OverlapResult { - } - - public record Overlap(Position distance, Transformation transformation, - Set overlappingBeacons) implements OverlapResult { - } - - public record None() implements OverlapResult { - } - - public record Scanner(int id, Set observations) { - - public OverlapResult getOverlappingBeacons(final Scanner other) { - for (final var direction : Direction.values()) { - for (final var rotation : Rotation.values()) { - final var transformation = new Transformation(direction, rotation); - final var distances = observations().stream() - .flatMap(a -> other.observations() - .stream() - .map(transformation::reorient) - .map(a::minus)) - .collect(Collectors.toList()); - for (final var offset : distances) { - final var intersection = other.observations() - .stream() - .map(transformation::reorient) - .map(observation -> observation.plus(offset)) - .filter(observations()::contains) - .collect(Collectors.toUnmodifiableSet()); - if (intersection.size() >= 12) { - return new Overlap(offset, transformation, intersection); - } - } - } - } - return new None(); - } - - } - - @Nested - public class ScannerTest { - @Test - public final void testOverlapWithOrigin() { - // given - final var scanner0Observations = """ - 404,-588,-901 - 528,-643,409 - -838,591,734 - 390,-675,-793 - -537,-823,-458 - -485,-357,347 - -345,-311,381 - -661,-816,-575 - -876,649,763 - -618,-824,-621 - 553,345,-567 - 474,580,667 - -447,-329,318 - -584,868,-557 - 544,-627,-890 - 564,392,-477 - 455,729,728 - -892,524,684 - -689,845,-530 - 423,-701,434 - 7,-33,-71 - 630,319,-379 - 443,580,662 - -789,900,-551 - 459,-707,401 - """; - final var scanner1Observations = """ - 686,422,578 - 605,423,415 - 515,917,-361 - -336,658,858 - 95,138,22 - -476,619,847 - -340,-569,-846 - 567,-361,727 - -460,603,-452 - 669,-402,600 - 729,430,532 - -500,-761,534 - -322,571,750 - -466,-666,-811 - -429,-592,574 - -355,545,-477 - 703,-491,-529 - -328,-685,520 - 413,935,-424 - -391,539,-444 - 586,-435,557 - -364,-763,-893 - 807,-499,-711 - 755,-354,-619 - 553,889,-390 - """; - final var scanner0 = new Scanner(0, - Arrays.stream(scanner0Observations.split("\n")) - .map(Position::parse) - .collect(Collectors.toUnmodifiableSet())); - final var scanner1 = new Scanner(0, - Arrays.stream(scanner1Observations.split("\n")) - .map(Position::parse) - .collect(Collectors.toUnmodifiableSet())); - - // when - final var result = scanner0.getOverlappingBeacons(scanner1); - - // then - assertTrue(result instanceof Overlap); - final var overlap = (Overlap) result; - assertEquals(12, overlap.overlappingBeacons().size()); - } - - @Test - public final void testOverlapWithNonOrigin() { - // given - final var scanner1Observations = """ - 686,422,578 - 605,423,415 - 515,917,-361 - -336,658,858 - 95,138,22 - -476,619,847 - -340,-569,-846 - 567,-361,727 - -460,603,-452 - 669,-402,600 - 729,430,532 - -500,-761,534 - -322,571,750 - -466,-666,-811 - -429,-592,574 - -355,545,-477 - 703,-491,-529 - -328,-685,520 - 413,935,-424 - -391,539,-444 - 586,-435,557 - -364,-763,-893 - 807,-499,-711 - 755,-354,-619 - 553,889,-390 - """; - final var scanner4Observations = """ - 727,592,562 - -293,-554,779 - 441,611,-461 - -714,465,-776 - -743,427,-804 - -660,-479,-426 - 832,-632,460 - 927,-485,-438 - 408,393,-506 - 466,436,-512 - 110,16,151 - -258,-428,682 - -393,719,612 - -211,-452,876 - 808,-476,-593 - -575,615,604 - -485,667,467 - -680,325,-822 - -627,-443,-432 - 872,-547,-609 - 833,512,582 - 807,604,487 - 839,-516,451 - 891,-625,532 - -652,-548,-490 - 30,-46,-14 - """; - final var scanner1 = new Scanner(0, - Arrays.stream(scanner1Observations.split("\n")) - .map(Position::parse) - .collect(Collectors.toUnmodifiableSet())); - final var scanner4 = new Scanner(0, - Arrays.stream(scanner4Observations.split("\n")) - .map(Position::parse) - .collect(Collectors.toUnmodifiableSet())); - - // when - final var result = scanner1.getOverlappingBeacons(scanner4); - - // then - assertTrue(result instanceof Overlap); - final var overlap = (Overlap) result; - assertEquals(12, overlap.overlappingBeacons().size()); - } - } - - @Test - public final void part1() { - final var scanners = getScanners(); - final var knownBeacons = new HashSet(); - final var origin = new Scanner(-1, knownBeacons); - final var remaining = new ArrayList<>(scanners); - while (!remaining.isEmpty()) { - final var other = remaining.remove(0); - if (knownBeacons.isEmpty()) { - knownBeacons.addAll(other.observations()); - continue; - } - final var result = origin.getOverlappingBeacons(other); - if (result instanceof final Overlap overlap) { - knownBeacons.addAll(other.observations() - .stream() - .map(overlap.transformation()::reorient) - .map(observation -> observation.plus(overlap.distance())) - .collect(Collectors.toList())); - } else { - remaining.add(other); - } - } - System.out.println("Part 1: " + knownBeacons.size()); - } - - @Test - public final void part2() { - final var scanners = getScanners(); - final var knownBeacons = new HashSet(); - final var origin = new Scanner(-1, knownBeacons); - final var remaining = new ArrayList<>(scanners); - final var distances = new HashSet(); - while (!remaining.isEmpty()) { - final var other = remaining.remove(0); - if (knownBeacons.isEmpty()) { - knownBeacons.addAll(other.observations()); - continue; - } - final var result = origin.getOverlappingBeacons(other); - if (result instanceof final Overlap overlap) { - knownBeacons.addAll(other.observations() - .stream() - .map(overlap.transformation()::reorient) - .map(observation -> observation.plus(overlap.distance())) - .collect(Collectors.toList())); - distances.add(overlap.distance()); - } else { - remaining.add(other); - } - } - int maxDistance = Integer.MIN_VALUE; - for (final var x : distances) { - for (final var y : distances) { - final int distance = Math.abs(x.x() - y.x()) - + Math.abs(x.y() - y.y()) - + Math.abs(x.z() - y.z()); - maxDistance = Math.max(maxDistance, distance); - } - } - System.out.println("Part 2: " + maxDistance); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day20.java b/src/test/java/com/macasaet/Day20.java index 85115e9..4125a47 100644 --- a/src/test/java/com/macasaet/Day20.java +++ b/src/test/java/com/macasaet/Day20.java @@ -1,233 +1,106 @@ package com.macasaet; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; -import java.util.*; +import java.math.BigInteger; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - /** - * --- Day 20: Trench Map --- + * --- Day 20: Grove Positioning System --- + * https://adventofcode.com/2022/day/20 */ public class Day20 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-20.txt"), - false); - } - - public record ImageEnhancementAlgorithm(boolean[] map) { - - public boolean isPixelLit(final int code) { - return map()[code]; - } - - public static ImageEnhancementAlgorithm parse(final String line) { - final var map = new boolean[line.length()]; - for (int i = line.length(); --i >= 0; map[i] = line.charAt(i) == '#') ; - return new ImageEnhancementAlgorithm(map); + public record Number(int originalIndex, int value, BigInteger decryptedValue) { + Number(int originalIndex, int value) { + this(originalIndex, value, BigInteger.valueOf(value).multiply(BigInteger.valueOf(811_589_153))); } } - protected record Coordinate(int x, int y) { - } - - public record Image(SortedMap> pixels, int minX, int maxX, int minY, - int maxY, boolean isEven) { - public Image enhance(final ImageEnhancementAlgorithm algorithm) { - final var enhancedPixelMap = new TreeMap>(); - int enhancedMinX = minX; - int enhancedMaxX = maxX; - int enhancedMinY = minY; - int enhancedMaxY = maxY; - for (int i = minX - 1; i <= maxX + 1; i++) { - final var targetRow = new TreeMap(); - for (int j = minY - 1; j <= maxY + 1; j++) { - final int replacementId = decode(i, j, !isEven && algorithm.isPixelLit(0)); - final var shouldLight = algorithm.isPixelLit(replacementId); - if (shouldLight) { - // save space by only storing an entry when lit - targetRow.put(j, true); - enhancedMinY = Math.min(enhancedMinY, j); - enhancedMaxY = Math.max(enhancedMaxY, j); - } - } - if (!targetRow.isEmpty()) { - // save space by only storing a row if at least one cell is lit - enhancedPixelMap.put(i, Collections.unmodifiableSortedMap(targetRow)); - enhancedMinX = Math.min(enhancedMinX, i); - enhancedMaxX = Math.max(enhancedMaxX, i); - } - } - return new Image(Collections.unmodifiableSortedMap(enhancedPixelMap), enhancedMinX, enhancedMaxX, enhancedMinY, enhancedMaxY, !isEven); + protected static List getInput() { + final List numbers = StreamSupport.stream(new LineSpliterator("day-20.txt"), false) + .mapToInt(Integer::parseInt) + .collect(ArrayList::new, (x, y) -> x.add(y), (x, y) -> x.addAll(y)); + final var result = new ArrayList(numbers.size()); + for(int i = 0; i < numbers.size(); i++) { + result.add(new Number(i, numbers.get(i))); } + return Collections.unmodifiableList(result); + } - int decode(final int x, final int y, boolean voidIsLit) { - final var list = getNeighbouringCoordinates(x, y).stream() - .map(coordinate -> isBitSet(coordinate, voidIsLit)) - .toList(); - int result = 0; - for (int i = list.size(); --i >= 0; ) { - if (list.get(i)) { - final int shiftDistance = list.size() - i - 1; - result |= 1 << shiftDistance; - } - } - if (result < 0 || result > 512) { - throw new IllegalStateException("Unable to decode pixel at " + x + ", " + y); + @Test + public final void part1() { + final var numbers = getInput(); + final var indexMap = new HashMap(numbers.size()); + Number zero = null; + for(final var number : numbers) { + indexMap.put(number.originalIndex, number.value()); + if(number.value() == 0) { + zero = number; } - return result; } - - boolean isBitSet(final Coordinate coordinate, boolean voidIsLit) { - final var row = pixels().get(coordinate.x()); - if((coordinate.x() < minX || coordinate.x() > maxX) && row == null) { - return voidIsLit; - } - else if(row == null) { - return false; + final var workingSet = new ArrayList<>(numbers); + + for(final var number : numbers) { + final var originalIndex = workingSet.indexOf(number); + workingSet.remove(originalIndex); + var newIndex = (originalIndex + number.value()) % (numbers.size() - 1); + if(newIndex < 0) { + newIndex += numbers.size() - 1; } - return row.getOrDefault(coordinate.y(), (coordinate.y() < minY || coordinate.y() > maxY) && voidIsLit); - } - - List getNeighbouringCoordinates(int x, int y) { - return Arrays.asList( - new Coordinate(x - 1, y - 1), - new Coordinate(x - 1, y), - new Coordinate(x - 1, y + 1), - new Coordinate(x, y - 1), - new Coordinate(x, y), - new Coordinate(x, y + 1), - new Coordinate(x + 1, y - 1), - new Coordinate(x + 1, y), - new Coordinate(x + 1, y + 1) - ); + workingSet.add(newIndex, number); } - public long countLitPixels() { - return pixels().values() - .stream() - .flatMap(row -> row.values().stream()) - .filter(isLit -> isLit) - .count(); - } + final var x = workingSet.get((workingSet.indexOf(zero) + 1000) % workingSet.size()).value(); + final var y = workingSet.get((workingSet.indexOf(zero) + 2000) % workingSet.size()).value(); + final var z = workingSet.get((workingSet.indexOf(zero) + 3000) % workingSet.size()).value(); - public int width() { - return maxX - minX + 1; - } + final var result = (long)x + (long)y + (long)z; - public int height() { - return maxY - minY + 1; - } + System.out.println("Part 1: " + result); + } - public String toString() { - final var builder = new StringBuilder(); - builder.append(width()).append('x').append(height()).append('\n'); - for (int i = minX; i <= maxX; i++) { - final var row = pixels.getOrDefault(i, Collections.emptySortedMap()); - for (int j = minY; j <= maxY; j++) { - final var value = row.getOrDefault(j, false); - builder.append(value ? '#' : '.'); - } - builder.append('\n'); + @Test + public final void part2() { + final var numbers = getInput(); + final var indexMap = new HashMap(numbers.size()); + Number zero = null; + for(final var number : numbers) { + indexMap.put(number.originalIndex, number.value()); + if(number.value() == 0) { + zero = number; } - return builder.toString(); } - - public static Image parse(final List lines) { - final var pixels = new TreeMap>(); - final int minX = 0; - final int minY = 0; - int maxX = 0; - int maxY = 0; - for (int i = lines.size(); --i >= 0; ) { - final var line = lines.get(i); - final var row = new TreeMap(); - for (int j = line.length(); --j >= 0; ) { - final var pixel = line.charAt(j); - row.put(j, pixel == '#'); - if (pixel == '#') { - row.put(j, true); - maxY = Math.max(maxY, j); - } - } - if (!row.isEmpty()) { - maxX = Math.max(maxX, i); - pixels.put(i, Collections.unmodifiableSortedMap(row)); + final var workingSet = new ArrayList<>(numbers); + + for(int i = 10; --i >= 0; ) { + for (final var number : numbers) { + final var originalIndex = workingSet.indexOf(number); + workingSet.remove(originalIndex); + var newIndex = number.decryptedValue().add(BigInteger.valueOf(originalIndex)).mod(BigInteger.valueOf(numbers.size() - 1)).intValue(); + if (newIndex < 0) { + newIndex += numbers.size() - 1; } + workingSet.add(newIndex, number); } - return new Image(Collections.unmodifiableSortedMap(pixels), minX, maxX, minY, maxY, true); - } - - } - - @Nested - public class ImageTest { - @Test - public final void testToInt() { - final var string = """ - #..#. - #.... - ##..# - ..#.. - ..### - """; - final var image = Image.parse(Arrays.asList(string.split("\n"))); - assertEquals(34, image.decode(2, 2, false)); } - @Test - public final void flipAllOn() { - final var template = "#........"; - final var imageString = """ - ... - ... - ... - """; - final var image = Image.parse(Arrays.asList(imageString.split("\n"))); - final var result = image.enhance(ImageEnhancementAlgorithm.parse(template)); - assertTrue(result.pixels().get(1).get(1)); - } + final var x = workingSet.get((workingSet.indexOf(zero) + 1000) % workingSet.size()).decryptedValue(); + final var y = workingSet.get((workingSet.indexOf(zero) + 2000) % workingSet.size()).decryptedValue(); + final var z = workingSet.get((workingSet.indexOf(zero) + 3000) % workingSet.size()).decryptedValue(); - @Test - public final void turnOffPixel() { - final var templateBuilder = new StringBuilder(); - for (int i = 511; --i >= 0; templateBuilder.append('#')) ; - templateBuilder.append('.'); - final var template = templateBuilder.toString(); - final var imageString = """ - ### - ### - ### - """; - final var image = Image.parse(Arrays.asList(imageString.split("\n"))); - final var result = image.enhance(ImageEnhancementAlgorithm.parse(template)); - final var middleRow = result.pixels().get(1); - assertFalse(middleRow.containsKey(1)); - } - } + final var result = x.add(y).add(z); - @Test - public final void part1() { - final var list = getInput().toList(); - final var algorithm = ImageEnhancementAlgorithm.parse(list.get(0)); - final var image = Image.parse(list.subList(2, list.size())) - .enhance(algorithm) - .enhance(algorithm); - System.out.println("Part 1: " + image.countLitPixels()); - } - - @Test - public final void part2() { - final var list = getInput().toList(); - final var algorithm = ImageEnhancementAlgorithm.parse(list.get(0)); - var image = Image.parse(list.subList(2, list.size())); - for(int _i = 50; --_i >= 0; image = image.enhance(algorithm)); - System.out.println("Part 2: " + image.countLitPixels()); + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day21.java b/src/test/java/com/macasaet/Day21.java index 1223048..b2fbfc8 100644 --- a/src/test/java/com/macasaet/Day21.java +++ b/src/test/java/com/macasaet/Day21.java @@ -1,159 +1,323 @@ package com.macasaet; -import java.math.BigInteger; +import org.junit.jupiter.api.Test; + +import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 21: Dirac Dice --- + * --- Day 21: Monkey Math --- + * https://adventofcode.com/2022/day/21 */ public class Day21 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-21.txt"), - false); + public record Monkey(String name, Job job) { + public long yell(final Map monkeys, final Map results) { + return job().yell(monkeys, results); + } + + public Simplification simplify(final Map monkeys, final Map results) { + return job().simplify(monkeys, results); + } + + public static Monkey parse(final String line) { + final var components = line.split(": "); + final var name = components[0].trim(); + final var job = Job.parse(components[1].trim()); + return new Monkey(name, job); + } + } + + public interface Simplification { + Simplification simplify(); } - public static class DeterministicDie { - int value; - int totalRolls = 0; + record Expression(Simplification x, Operation operation, Simplification y) implements Simplification { - protected DeterministicDie(final int startingValue) { - this.value = startingValue; + public Simplification simplify() { + final var simpleX = x().simplify(); + final var simpleY = y().simplify(); + if (!x().equals(simpleX) || !y().equals(simpleY)) { + return new Expression(simpleX, operation(), simpleY); + } else if (x() instanceof Value && y() instanceof Value) { + return new Value(operation().operate(((Value) x).value(), ((Value) y).value())); + } else if (operation() == Operation.IS_EQUAL) { + if (x() instanceof final Value constant && y() instanceof final Expression expression) { + // e.g. 5=2/x or 5=x/2 + final var inverse = expression.operation().inverse(); + if (expression.x() instanceof Value) { + // e.g. 5=2/x + if (expression.operation.isSymmetric()) { + // e.g. 2=5x -> 10=x + final var lValue = new Expression(constant, inverse, expression.x()).simplify(); + return new Expression(lValue, Operation.IS_EQUAL, expression.y()); + } else { + // e.g. 5=2/x -> 5x=2 + final var lValue = new Expression(constant, inverse, expression.y()); + return new Expression(lValue, Operation.IS_EQUAL, expression.x()); + } + } else if (expression.y() instanceof Value) { + // e.g. 5=x/2 -> 5*2=x -> 10=x + final var lValue = new Expression(constant, inverse, expression.y()).simplify(); + return new Expression(lValue, Operation.IS_EQUAL, expression.x()); + } + // cannot simplify further + return this; + } else if (x() instanceof final Expression expression && y() instanceof final Value constant) { + // e.g. 5/x=2 or x/5=2 + final var inverse = expression.operation().inverse(); + if (expression.x() instanceof Value) { + // e.g. 5/x=2 or x/5=2 + if (expression.operation().isSymmetric()) { + // e.g. 2x=5 -> x=5*2 -> x=10 + final var rValue = new Expression(constant, inverse, expression.x()).simplify(); + return new Expression(expression.y(), Operation.IS_EQUAL, rValue); + } else { + // e.g. 5/x=2 -> 5=2x + final var rValue = new Expression(constant, inverse, expression.y()); + return new Expression(expression.x(), Operation.IS_EQUAL, rValue); + } + } else if (expression.y() instanceof Value) { + // e.g. x/5=2 -> x=2*5 -> x=10 + final var rValue = new Expression(constant, inverse, expression.y()).simplify(); + return new Expression(expression.x(), Operation.IS_EQUAL, rValue); + } + // cannot simplify further + return this; + } + } + return this; } - public DeterministicDie() { - this(1); + public String toString() { + return "(" + x() + ") " + operation() + " (" + y() + ")"; } + } - public int roll() { - final int result = value; - value += 1; - if (value > 100) { - value -= 100; - } - totalRolls++; - return result; + record Value(long value) implements Simplification { + public String toString() { + return "" + value(); + } + + public Simplification simplify() { + return this; } } - public record Pawn(int position, int score) { - public Pawn fork() { - return new Pawn(position(), score()); + record Variable() implements Simplification { + public String toString() { + return "x"; + } + + public Simplification simplify() { + return this; } + } + + public interface Job { + long yell(final Map monkeys, final Map results); + + Simplification simplify(final Map monkeys, final Map results); - public Pawn move(final int distance) { - int newPosition = (position() + distance) % 10; - if (newPosition == 0) { - newPosition = 10; + static Job parse(final String string) { + final var components = string.trim().split(" "); + if (components.length == 1) { + return Yell.parse(string.trim()); } - final int newScore = score() + newPosition; - return new Pawn(newPosition, newScore); + return Math.parse(components); } + } + + public enum Operation { + Add { + public long operate(final long x, final long y) { + return x + y; + } + + public Operation inverse() { + return Subtract; + } + + public boolean isSymmetric() { + return true; + } + }, + Subtract { + public long operate(final long x, final long y) { + return x - y; + } - public Pawn takeTurn(final DeterministicDie die) { - final int distance = die.roll() + die.roll() + die.roll(); - return move(distance); + public Operation inverse() { + return Add; + } + + public boolean isSymmetric() { + return false; + } + }, + Multiply { + public long operate(final long x, final long y) { + return x * y; + } + + public Operation inverse() { + return Divide; + } + + public boolean isSymmetric() { + return true; + } + }, + Divide { + public long operate(final long x, final long y) { + return x / y; + } + + public Operation inverse() { + return Multiply; + } + + public boolean isSymmetric() { + return false; + } + }, + IS_EQUAL { + public long operate(final long x, final long y) { + // what a horrible hack, who would do this? + return x == y ? 1L : 0L; + } + + public Operation inverse() { + return IS_EQUAL; + } + + public boolean isSymmetric() { + return true; + } + }; + + public abstract long operate(long x, long y); + + public abstract Operation inverse(); + + public abstract boolean isSymmetric(); + + public String toString() { + return switch (this) { + case Add -> "+"; + case Subtract -> "-"; + case Multiply -> "*"; + case Divide -> "/"; + case IS_EQUAL -> "="; + }; + } + + public static Operation parse(final String operator) { + return switch (operator.trim()) { + case "+" -> Add; + case "-" -> Subtract; + case "*" -> Multiply; + case "/" -> Divide; + default -> throw new IllegalArgumentException("Invalid operator: " + operator); + }; } } - public record Game(Pawn player1, Pawn player2, boolean playerOnesTurn) { + public record Unknown() implements Job { + public long yell(Map monkeys, Map results) { + throw new UnsupportedOperationException(); // Oof + } + public Simplification simplify(Map monkeys, Map results) { + return new Variable(); + } } - public record ScoreCard(BigInteger playerOneWins, BigInteger playerTwoWins) { - public ScoreCard add(final ScoreCard other) { - return new ScoreCard(playerOneWins().add(other.playerOneWins()), playerTwoWins().add(other.playerTwoWins())); + record Yell(long number) implements Job { + public long yell(Map monkeys, Map results) { + return number; + } + + public Simplification simplify(Map monkeys, Map results) { + return new Value(number()); + } + + public static Yell parse(final String string) { + return new Yell(Integer.parseInt(string)); } } - public static class QuantumDie { - private final Map cache = new HashMap<>(); - - public ScoreCard play(final Game game) { - if (cache.containsKey(game)) { - return cache.get(game); - } - final var reverseScenario = new Game(game.player2(), game.player1(), !game.playerOnesTurn()); - if (cache.containsKey(reverseScenario)) { - final var reverseResult = cache.get(reverseScenario); - return new ScoreCard(reverseResult.playerTwoWins(), reverseResult.playerOneWins()); - } - - if (game.player1().score() >= 21) { - final var result = new ScoreCard(BigInteger.ONE, BigInteger.ZERO); - cache.put(game, result); - return result; - } else if (game.player2().score() >= 21) { - final var result = new ScoreCard(BigInteger.ZERO, BigInteger.ONE); - cache.put(game, result); - return result; - } - - var result = new ScoreCard(BigInteger.ZERO, BigInteger.ZERO); - for (int i = 1; i <= 3; i++) { - for (int j = 1; j <= 3; j++) { - for (int k = 1; k <= 3; k++) { - final int movementDistance = i + j + k; - final var forkResult = game.playerOnesTurn() - ? play(new Game(game.player1().move(movementDistance), game.player2(), false)) - : play(new Game(game.player1(), game.player2().fork().move(movementDistance), true)); - result = result.add(forkResult); - } - } + public record Math(String monkeyX, Operation operation, String monkeyY) implements Job { + + public long yell(Map monkeys, Map results) { + final var x = getVariable(monkeyX(), monkeys, results); + final var y = getVariable(monkeyY(), monkeys, results); + return operation().operate(x, y); + } + + public Simplification simplify(Map monkeys, Map results) { + final var x = monkeys.get(monkeyX()).simplify(monkeys, results); + final var y = monkeys.get(monkeyY()).simplify(monkeys, results); + if (x instanceof final Value xValue && y instanceof final Value yValue) { + return new Value(operation().operate(xValue.value(), yValue.value())); + } + return new Expression(x, operation(), y); + } + + long getVariable(final String monkeyName, final Map monkeys, final Map results) { + if (results.containsKey(monkeyName)) { + return results.get(monkeyName); } - cache.put(game, result); + final var result = monkeys.get(monkeyName).yell(monkeys, results); + results.put(monkeyName, result); return result; } + + public static Math parse(final String[] components) { + final var monkeyX = components[0].trim(); + final var operation = Operation.parse(components[1].trim()); + final var monkeyY = components[2].trim(); + return new Math(monkeyX, operation, monkeyY); + } + } + + protected static Map getInput() { + final Map result = new HashMap<>(); + StreamSupport.stream(new LineSpliterator("day-21.txt"), false) + .map(Monkey::parse) + .forEach(monkey -> result.put(monkey.name(), monkey)); + return Collections.unmodifiableMap(result); } @Test public final void part1() { - final var lines = getInput().toList(); - final int playerOnePosition = - Integer.parseInt(lines.get(0).replaceAll("Player . starting position: ", "")); - final int playerTwoPosition = - Integer.parseInt(lines.get(1).replaceAll("Player . starting position: ", "")); - var playerOne = new Pawn(playerOnePosition, 0); - var playerTwo = new Pawn(playerTwoPosition, 0); - final var die = new DeterministicDie(); - while (true) { - // player 1 - playerOne = playerOne.takeTurn(die); - if (playerOne.score() >= 1000) { - break; - } - - // player 2 - playerTwo = playerTwo.takeTurn(die); - if (playerTwo.score() >= 1000) { - break; - } - } - int losingScore = Math.min(playerOne.score(), playerTwo.score()); + final var monkeys = getInput(); + final var results = new HashMap(); + final var result = monkeys.get("root").yell(monkeys, results); - System.out.println("Part 1: " + (losingScore * die.totalRolls)); + System.out.println("Part 1: " + result); } @Test public final void part2() { - final var lines = getInput().toList(); - final int playerOnePosition = - Integer.parseInt(lines.get(0).replaceAll("Player . starting position: ", "")); - final int playerTwoPosition = - Integer.parseInt(lines.get(1).replaceAll("Player . starting position: ", "")); - final var playerOne = new Pawn(playerOnePosition, 0); - final var playerTwo = new Pawn(playerTwoPosition, 0); - final var die = new QuantumDie(); - final var game = new Game(playerOne, playerTwo, true); - final var result = die.play(game); - final var winningScore = result.playerOneWins().max(result.playerTwoWins()); - System.out.println("Part 2: " + winningScore); + final var monkeys = new HashMap<>(getInput()); + final var results = new HashMap(); + final var oldRoot = monkeys.get("root"); + final var oldJob = (Math) oldRoot.job(); + monkeys.put("root", new Monkey("root", new Math(oldJob.monkeyX(), Operation.IS_EQUAL, oldJob.monkeyY()))); + monkeys.put("humn", new Monkey("humn", new Unknown())); + + var simplification = monkeys.get("root").simplify(monkeys, results); + while (true) { + final var candidate = simplification.simplify(); + if (candidate.equals(simplification)) { + break; + } + simplification = candidate; + } + System.out.println("Part 2: " + simplification); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day22.java b/src/test/java/com/macasaet/Day22.java deleted file mode 100644 index 333be59..0000000 --- a/src/test/java/com/macasaet/Day22.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.macasaet; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.math.BigInteger; -import java.util.*; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * --- Day 22: Reactor Reboot --- - */ -public class Day22 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-22.txt"), - false); - } - - public record ReactorCore( - SortedMap>> cubes) { - - public long count(final long xMin, final long xMax, final long yMin, final long yMax, final long zMin, final long zMax) { - long sum = 0; - for (var i = xMin; i <= xMax; i++) { - final var xDimension = cubes().getOrDefault(i, Collections.emptySortedMap()); - for (var j = yMin; j <= yMax; j++) { - final var yDimension = xDimension.getOrDefault(j, Collections.emptySortedMap()); - for (var k = zMin; k <= zMax; k++) { - if (yDimension.getOrDefault(k, false)) { - sum++; - } - } - } - } - return sum; - } - - public void process(final Instruction instruction) { - final var block = instruction.block(); - final var on = instruction.on(); - for (var i = block.xMin(); i <= block.xMax(); i++) { - final var xDimension = cubes().computeIfAbsent(i, _key -> new TreeMap<>()); - for (var j = block.yMin(); j <= block.yMax(); j++) { - final var yDimension = xDimension.computeIfAbsent(j, _key -> new TreeMap<>()); - for (var k = block.zMin(); k <= block.zMax(); k++) { - yDimension.put(k, on); - } - } - } - } - - } - - public record Instruction(boolean on, Block block) { - public static Instruction parse(final String string) { - final var components = string.split(" "); - final boolean on = "on".equalsIgnoreCase(components[0]); - final var block = Block.parse(components[1]); - return new Instruction(on, block); - } - - } - - public record Block(long xMin, long xMax, long yMin, long yMax, long zMin, long zMax) { - - public BigInteger volume() { - return (BigInteger.valueOf(xMax).subtract(BigInteger.valueOf(xMin)).add(BigInteger.ONE)) - .multiply(BigInteger.valueOf(yMax).subtract(BigInteger.valueOf(yMin)).add(BigInteger.ONE)) - .multiply(BigInteger.valueOf(zMax).subtract(BigInteger.valueOf(zMin)).add(BigInteger.ONE)); - } - - public static Block parse(final String string) { - final var ranges = string.split(","); - final var xRange = ranges[0].split("\\.\\."); - final var xMin = Long.parseLong(xRange[0].replaceAll("x=", "")); - final var xMax = Long.parseLong((xRange[1])); - final var yRange = ranges[1].split("\\.\\."); - final var yMin = Long.parseLong((yRange[0].replaceAll("y=", ""))); - final var yMax = Long.parseLong((yRange[1])); - final var zRange = ranges[2].split("\\.\\."); - final var zMin = Long.parseLong((zRange[0].replaceAll("z=", ""))); - final var zMax = Long.parseLong((zRange[1])); - return new Block(xMin, xMax, yMin, yMax, zMin, zMax); - } - - public boolean overlaps(final Block other) { - return intersection(other).isPresent(); - } - - public Optional intersection(final Block other) { - if (xMin > other.xMax() || xMax < other.xMin() - || yMin > other.yMax() || yMax < other.yMin() - || zMin > other.zMax() || zMax < other.zMin()) { - return Optional.empty(); - } - final var result = new Block(Math.max(xMin, other.xMin()), Math.min(xMax, other.xMax()), - Math.max(yMin, other.yMin()), Math.min(yMax, other.yMax()), - Math.max(zMin, other.zMin()), Math.min(zMax, other.zMax())); - return Optional.of(result); - } - } - - @Nested - public class BlockTest { - @Test - public final void verifyEqualBlocksOverlap() { - // given - final var x = new Block(-2, 2, -2, 2, -2, 2); - final var y = new Block(-2, 2, -2, 2, -2, 2); - - // when - - // then - assertTrue(x.overlaps(y)); - assertTrue(y.overlaps(x)); - } - - @Test - public final void verifyNestedBlocksOverlap() { - final var inner = new Block(-2, 2, -2, 2, -2, 2); - final var outer = new Block(-4, 4, -4, 4, -4, 4); - - assertTrue(inner.overlaps(outer)); - assertTrue(outer.overlaps(inner)); - } - - @Test - public final void verifyIntersectingBlocksOverlap() { - final var x = new Block(10, 12, 10, 12, 10, 12); - final var y = new Block(11, 13, 11, 13, 11, 13); - - assertTrue(x.overlaps(y)); - assertTrue(y.overlaps(x)); - } - - @Test - public final void testIntersection() { - final var x = new Block(10, 12, 10, 12, 10, 12); - final var y = new Block(11, 13, 11, 13, 11, 13); - - assertTrue(x.intersection(y).isPresent()); - assertEquals(BigInteger.valueOf(8), x.intersection(y).orElseThrow().volume()); - assertTrue(y.intersection(x).isPresent()); - assertEquals(BigInteger.valueOf(8), y.intersection(x).orElseThrow().volume()); - assertEquals(x.intersection(y).orElseThrow(), y.intersection(x).orElseThrow()); - } - } - - @Test - public final void part1() { - final var core = new ReactorCore(new TreeMap<>()); - getInput().map(Instruction::parse).map(fullInstruction -> { - final var fullBlock = fullInstruction.block(); - final var truncatedBlock = new Block(Math.max(fullBlock.xMin(), -50), Math.min(fullBlock.xMax(), 50), - Math.max(fullBlock.yMin(), -50), Math.min(fullBlock.yMax(), 50), - Math.max(fullBlock.zMin(), -50), Math.min(fullBlock.zMax(), 50)); - return new Instruction(fullInstruction.on(), truncatedBlock); - }).forEach(core::process); - System.out.println("Part 1: " + core.count(-50, 50, -50, 50, -50, 50)); - } - - @Test - public final void part2() { - final var originalList = getInput().map(Instruction::parse).toList(); - final var appliedInstructions = new ArrayList(); - for (final var instruction : originalList) { - final var modifiedInstructions = new ArrayList(); - if (instruction.on()) { - // only add initial instructions that turn ON cubes - modifiedInstructions.add(instruction); - } - // override any previous instructions - for (final var previousInstruction : appliedInstructions) { - // add compensating instructions to handle the overlaps - instruction.block() - .intersection(previousInstruction.block()) - .map(intersection -> new Instruction(!previousInstruction.on(), intersection)) - .ifPresent(modifiedInstructions::add); - } - - appliedInstructions.addAll(modifiedInstructions); - } - - final var sum = appliedInstructions.stream() - .map(instruction -> instruction.block() - .volume() - .multiply(instruction.on() - ? BigInteger.ONE - : BigInteger.valueOf(-1))) - .reduce(BigInteger.ZERO, - BigInteger::add); - - System.out.println("Part 2: " + sum); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day23.java b/src/test/java/com/macasaet/Day23.java index b023161..b701a77 100644 --- a/src/test/java/com/macasaet/Day23.java +++ b/src/test/java/com/macasaet/Day23.java @@ -1,897 +1,644 @@ package com.macasaet; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.function.Function; -import java.util.function.Supplier; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - /** - * --- Day 23: Amphipod --- + * --- Day 23: Unstable Diffusion --- + * https://adventofcode.com/2022/day/23 */ public class Day23 { - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-23.txt"), - false); - } - - protected Tile[][] parseGrid(List lines) { - final var result = new Tile[lines.size()][]; - for (int i = lines.size(); --i >= 0; ) { - final var line = lines.get(i); - result[i] = new Tile[line.length()]; - for (int j = line.length(); --j >= 0; ) { - final AmphipodType targetType = AmphipodType.forDestinationColumn(j); - final var c = line.charAt(j); - result[i][j] = switch (c) { - case '.' -> new Tile(new Point(i, j), null, null); - case 'A' -> new Tile(new Point(i, j), targetType, AmphipodType.AMBER); - case 'B' -> new Tile(new Point(i, j), targetType, AmphipodType.BRONZE); - case 'C' -> new Tile(new Point(i, j), targetType, AmphipodType.COPPER); - case 'D' -> new Tile(new Point(i, j), targetType, AmphipodType.DESERT); - default -> null; - }; - } - } - - return result; - } - - public enum AmphipodType { - AMBER(1, 3), - BRONZE(10, 5), - COPPER(100, 7), - DESERT(1000, 9); - - private final int energyPerStep; - private final int destinationColumn; - - AmphipodType(final int energyPerStep, final int destinationColumn) { - this.energyPerStep = energyPerStep; - this.destinationColumn = destinationColumn; + record Coordinate(int x, int y) { + public boolean hasElf(final Map> grid) { + return grid.getOrDefault(x(), Collections.emptyMap()).getOrDefault(y(), false); } - public static AmphipodType forDestinationColumn(final int destinationColumn) { - for (final var candidate : values()) { - if (candidate.destinationColumn == destinationColumn) { - return candidate; + public Set neighbours() { + final var neighbours = new HashSet(8); + for (int i = x() - 1; i <= x() + 1; i++) { + for (int j = y() - 1; j <= y() + 1; j++) { + if (i == x() && j == y()) { + continue; + } + neighbours.add(new Coordinate(i, j)); } } - return null; + return Collections.unmodifiableSet(neighbours); } } - public record Move(Point from, Point to) { - } - - public record BranchResult(Node node, int cost) { - } - - public record Node(Tile[][] tiles, Map, BranchResult> branchCache) { - - private static final Map estimatedDistanceCache = new ConcurrentHashMap<>(); - private static final Map> branchesCache = new ConcurrentHashMap<>(); - private static final Map solutionCache = new ConcurrentHashMap<>(); + enum Direction { + North { + public Set relativeCoordinates(Coordinate reference) { + return Set.of(new Coordinate(reference.x() - 1, reference.y() - 1), + adjacent(reference), + new Coordinate(reference.x() - 1, reference.y() + 1)); + } - public static Node createInitialNode(final Tile[][] tiles) { - return new Node(tiles, new ConcurrentHashMap<>()); - } + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x() - 1, reference.y()); + } + }, + East { + public Set relativeCoordinates(Coordinate reference) { + return Set.of(new Coordinate(reference.x() - 1, reference.y() + 1), + adjacent(reference), + new Coordinate(reference.x() + 1, reference.y() + 1)); + } - public BranchResult branch(final List moves) { - if (moves.size() == 0) { - System.err.println("How is this empty?"); - return new BranchResult(this, 0); + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x(), reference.y() + 1); } - if (branchCache.containsKey(moves)) { - return branchCache.get(moves); + }, + South { + public Set relativeCoordinates(Coordinate reference) { + return Set.of(new Coordinate(reference.x() + 1, reference.y() - 1), + adjacent(reference), + new Coordinate(reference.x() + 1, reference.y() + 1)); } - final var copy = new Tile[tiles.length][]; - for (int i = tiles.length; --i >= 0; ) { - final var row = new Tile[tiles[i].length]; - System.arraycopy(tiles[i], 0, row, 0, tiles[i].length); - copy[i] = row; + + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x() + 1, reference.y()); } - final var source = moves.get(0).from(); - final var destination = moves.get(moves.size() - 1).to(); - final var sourceTile = source.getTile(tiles); - final var destinationTile = destination.getTile(tiles); - final var amphipod = sourceTile.amphipodType(); - if (amphipod == null) { - System.err.println("source amphipod is missing :-("); + }, + West { + public Set relativeCoordinates(Coordinate reference) { + return Set.of(new Coordinate(reference.x() - 1, reference.y() - 1), + adjacent(reference), + new Coordinate(reference.x() + 1, reference.y() - 1)); } - source.setTile(copy, sourceTile.updateType(null)); - destination.setTile(copy, destinationTile.updateType(amphipod)); - final var cost = moves.size() * amphipod.energyPerStep; - final var result = new BranchResult(new Node(copy, new ConcurrentHashMap<>()), cost); - branchCache.put(moves, result); - return result; - } - boolean isSideRoom(final Point point) { - final var x = point.x(); - final var y = point.y(); - return x > 1 && (y == 3 || y == 5 || y == 7 || y == 9); - } + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x(), reference.y() - 1); + } + }; - boolean isCorridor(final Point point) { - return point.x() == 1; - } + public abstract Set relativeCoordinates(Coordinate reference); - public int hashCode() { - // equality based on layout of the burrow regardless of how the amphipods got to that state - // FNV hash - long result = 2166136261L; - final Function rowHasher = row -> { - long rowHash = 2166136261L; - for (final var tile : row) { - rowHash = (16777619L * rowHash) ^ (tile == null ? 0L : (long) Objects.hashCode(tile.amphipodType())); - } - return rowHash; - }; - for (final var row : tiles()) { - result = (16777619L * result) ^ rowHasher.apply(row); - } + public abstract Coordinate adjacent(Coordinate reference); + } - return Long.hashCode(result); - // Bob Jenkins' One-at-a-Time hash -// int result = 0; -// final Function rowHasher = row -> { -// int rowHash = 0; -// for(final var tile : row) { -// final var tileHash = tile != null ? Objects.hashCode(tile.amphipodType()) : 0; -// rowHash += tileHash; -// rowHash += rowHash << 10; -// rowHash ^= rowHash >> 6; -// } -// rowHash += rowHash << 3; -// rowHash ^= rowHash >> 11; -// rowHash += rowHash << 15; -// return rowHash; -// }; -// for(final var row : tiles()) { -// result += rowHasher.apply(row); -// result += (result << 10); -// result ^= (result >> 6); -// } -// result += result << 3; -// result ^= result >> 11; -// result += result << 15; -// return result; + public static class Crater { + + private final Map> grid; + private int minX; + private int maxX; + private int minY; + private int maxY; + private List movementPriority = + new ArrayList<>(Arrays.asList(Direction.North, Direction.South, Direction.West, Direction.East)); + + public Crater(final Map> grid, int minX, int maxX, int minY, int maxY) { + this.grid = grid; + setMinX(minX); + setMinY(minY); + setMaxX(maxX); + setMaxY(maxY); } - public boolean equals(final Object o) { - if (o == null) { - return false; - } - if (this == o) { - return true; - } - try { - final var other = (Node) o; - // equality based on layout of the burrow regardless of how the amphipods got to that state - if (tiles().length != other.tiles().length) { - return false; - } - for (int i = tiles().length; --i >= 0; ) { - final var xRow = tiles()[i]; - final var yRow = other.tiles()[i]; - if (xRow.length != yRow.length) { - return false; - } - for (int j = xRow.length; --j >= 0; ) { - if (xRow[j] != yRow[j]) { - if (xRow[j] == null - || yRow[j] == null - || !Objects.equals(xRow[j].amphipodType(), yRow[j].amphipodType())) { - return false; + public int round() { + final var destinations = new HashMap>(); + final var moves = new HashMap(); + // first half of round: planning phase + for (int i = getMinX(); i <= getMaxX(); i++) { + final var row = getGrid().get(i); + for (int j = getMinY(); j <= getMaxY(); j++) { + final var from = new Coordinate(i, j); + if (row.getOrDefault(j, false)) { + // determine destination + final var hasNeighbour = from.neighbours() + .stream() + .anyMatch(c -> getGrid().getOrDefault(c.x(), Collections.emptyMap()) + .getOrDefault(c.y(), false)); + if (!hasNeighbour) { + // "If no other Elves are in one of those eight positions, the Elf does not do anything + // during this round." + continue; + } + for (final var direction : getMovementPriority()) { + final var clear = direction.relativeCoordinates(from) + .stream() + .noneMatch(neighbour -> neighbour.hasElf(getGrid())); + if (clear) { + final var to = direction.adjacent(from); + destinations.computeIfAbsent(to, _row -> new HashSet<>()).add(from); + moves.put(from, to); + break; } } } } - return true; - } catch (final ClassCastException cce) { - return false; } - } - public String toString() { - final var builder = new StringBuilder(); - for (final var row : tiles()) { - for (final var cell : row) { - if (cell == null) { - builder.append('#'); - } else if (cell.amphipodType() == null) { - builder.append('.'); - } else { - builder.append(switch (cell.amphipodType()) { - case AMBER -> 'A'; - case BRONZE -> 'B'; - case COPPER -> 'C'; - case DESERT -> 'D'; - }); - } + // second half of round: movement phase + for (final var move : moves.entrySet()) { + final var from = move.getKey(); + final var to = move.getValue(); + // "each Elf moves to their proposed destination tile if they were the only Elf to propose moving to + // that position. If two or more Elves propose moving to the same position, none of those Elves move." + if (destinations.get(to).size() == 1) { + getGrid().computeIfAbsent(to.x(), _row -> new HashMap<>()).put(to.y(), true); + getGrid().get(from.x()).put(from.y(), false); + setMinX(Math.min(getMinX(), to.x())); + setMaxX(Math.max(getMaxX(), to.x())); + setMinY(Math.min(getMinY(), to.y())); + setMaxY(Math.max(getMaxY(), to.y())); } - builder.append('\n'); } - return builder.toString(); - } - - public boolean isSolution() { - if (solutionCache.containsKey(this)) { - return solutionCache.get(this); + // prune edges + // minX + final var highestRow = getGrid().get(getMinX()); + if (highestRow.values().stream().noneMatch(hasElf -> hasElf)) { + getGrid().remove(getMinX()); + setMinX(getMinX() + 1); + } + // maxX + final var lowestRow = getGrid().get(getMaxX()); + if (lowestRow.values().stream().noneMatch(hasElf -> hasElf)) { + getGrid().remove(getMaxX()); + setMaxX(getMaxX() - 1); + } + // minY + if (getGrid().values().stream().map(row -> row.get(getMinY())).noneMatch(hasElf -> hasElf != null && hasElf)) { + for (final var row : getGrid().values()) { + row.remove(getMinY()); + } + setMinY(getMinY() + 1); } - final var result = Arrays.stream(tiles()) - .flatMap(Arrays::stream) - .filter(Objects::nonNull) - .allMatch(Tile::hasTargetType); - solutionCache.put(this, result); - return result; + // maxY + if (getGrid().values().stream().map(row -> row.get(getMaxY())).noneMatch(hasElf -> hasElf != null && hasElf)) { + for (final var row : getGrid().values()) { + row.remove(getMaxY()); + } + setMaxY(getMaxY() + 1); + } + + // "Finally, at the end of the round, the first direction the Elves considered is moved to the end of the + // list of directions." + final var previousFirst = getMovementPriority().remove(0); + getMovementPriority().add(previousFirst); + return moves.size(); } - public Stream getBranches() { - if (branchesCache.containsKey(this)) { - return branchesCache.get(this).stream(); + public int countEmptyGroundTiles() { + int result = 0; + for (int i = getMinX(); i <= getMaxX(); i++) { + final var row = getGrid().getOrDefault(i, Collections.emptyMap()); + for (int j = getMinY(); j <= getMaxY(); j++) { + if (!row.getOrDefault(j, false)) { + result++; + } + } } - final var branchResults = new Vector(); - return Arrays.stream(tiles()) - .parallel() - .flatMap(row -> Arrays.stream(row) - .parallel() - .filter(Objects::nonNull) - .filter(tile -> !tile.isVacant()) - .flatMap(this::getMoves)) - .map(this::branch) - .peek(branchResults::add) - .onClose(() -> branchesCache.put(this, Collections.unmodifiableList(branchResults))); + return result; } -// @Deprecated -// public Set> getMoves() { -// final var result = new HashSet>(); -// for (final var row : tiles()) { -// for (final var tile : row) { -// if (tile != null && !tile.isVacant()) { -// result.addAll(getMoves(tile).collect(Collectors.toSet())); -// } -// } -// } -// return Collections.unmodifiableSet(result); -// } - - /** - * Find all the actions (series of moves) that can be taken for a single amphipod. - * - * @param occupiedTile a tile with an amphipod - * @return All the moves that can be applied starting with occupiedTile that end in a valid temporary - * destination for the amphipod - */ - Stream> getMoves(final Tile occupiedTile) { - if (isSideRoom(occupiedTile.location()) && occupiedTile.hasTargetType()) { - var roomComplete = true; - for (var cursor = tiles[occupiedTile.location().x() + 1][occupiedTile.location().y()]; - cursor != null; - cursor = tiles[cursor.location().x() + 1][cursor.location().y()]) { - if (!cursor.hasTargetType()) { - // one of the amphipods in this room is destined elsewhere - // so the amphipod from the original tile will need to move out of the way - roomComplete = false; - break; + public static Crater fromString(final String block) { + final var lines = block.split("\n"); + final Map> grid = new HashMap<>(lines.length); + int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE; + for (int i = lines.length; --i >= 0; ) { + final var line = lines[i]; + final var chars = line.toCharArray(); + final Map row = new HashMap<>(chars.length); + boolean rowHasElf = false; + for (int j = chars.length; --j >= 0; ) { + if (chars[j] == '#') { + minY = Math.min(minY, j); + maxY = Math.max(maxY, j); + row.put(j, true); + rowHasElf |= true; + } else { + row.put(j, false); } } - if (roomComplete) { - // all the amphipods in this room have this as their intended destination - return Stream.empty(); + grid.put(i, row); + if (rowHasElf) { + minX = Math.min(minX, i); + maxX = Math.max(maxX, i); } } + return new Crater(grid, minX, maxX, minY, maxY); + } - var paths = iterateThroughPaths(occupiedTile.amphipodType(), - occupiedTile, - Collections.singletonList(occupiedTile.location())); - if (isCorridor(occupiedTile.location())) { - /* - * "Once an amphipod stops moving in the hallway, it will stay in that spot until it can move into a - * room. (That is, once any amphipod starts moving, any other amphipods currently in the hallway are - * locked in place and will not move again until they can move fully into a room.)" - */ - paths = paths - // only select paths that end in a room - .filter(path -> isSideRoom(path.get(path.size() - 1) - .getTile(tiles()) - .location())); - } else { - // reduce the search space by only considering rooms from rooms into hallways - // prune any path that starts from a room and ends in a room -// paths = paths -// .filter(path -> isCorridor(path.get(path.size() - 1) -// .getTile(tiles()) -// .location())); - } - // convert tiles to moves - return paths - .filter(path -> path.size() > 1) // filter out paths in which the amphipod does not move - .map(points -> { - final var moves = new ArrayList(points.size() - 1); - for (int i = 1; i < points.size(); i++) { - moves.add(new Move(points.get(i - 1), points.get(i))); - } - return Collections.unmodifiableList(moves); - }); + public String toString() { + final var builder = new StringBuilder(); + builder.append("X: [").append(getMinX()).append(", ").append(getMaxX()).append("]\n"); + builder.append("Y: [").append(getMinY()).append(", ").append(getMaxY()).append("]\n"); + builder.append("Movement priority: ").append(getMovementPriority()).append("\n"); + appendGrid(builder); + return builder.toString(); } - Stream> iterateThroughPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - // TODO store `pathSoFar` as a stack so checking for node becomes O(1) instead of O(n) - final int x = current.location.x(); - final int y = current.location.y(); - final var up = tiles[x - 1][y]; - final var down = tiles[x + 1][y]; - final var left = tiles[x][y - 1]; - final var right = tiles[x][y + 1]; - final var suppliers = new ArrayList>>>(4); - if (up != null && up.isVacant() && !pathSoFar.contains(up.location())) { - suppliers.add(() -> streamUpPaths(amphipodType, current, pathSoFar)); - } - if (down != null - && down.isVacant() - && !pathSoFar.contains(down.location()) - // don't enter side room unless it is the ultimate destination - && down.targetType == amphipodType) { - suppliers.add(() -> streamDownPaths(amphipodType, current, pathSoFar)); - } - if (left != null && left.isVacant() && !pathSoFar.contains(left.location())) { - suppliers.add(() -> streamLeftPaths(amphipodType, current, pathSoFar)); - } - if (right != null && right.isVacant() && !pathSoFar.contains(right.location())) { - suppliers.add(() -> streamRightPaths(amphipodType, current, pathSoFar)); - } - if (suppliers.isEmpty()) { - // dead end, emit the path so far - suppliers.add(() -> Stream.of(Collections.unmodifiableList(pathSoFar))); + protected void appendGrid(final StringBuilder builder) { + for (int i = getMinX(); i <= getMaxX(); i++) { + final var row = getGrid().getOrDefault(i, Collections.emptyMap()); + for (int j = getMinY(); j <= getMaxY(); j++) { + final var c = row.getOrDefault(j, false) + ? '#' + : '.'; + builder.append(c); + } + builder.append('\n'); } + } - return suppliers.stream() - .flatMap(Supplier::get); + protected Map> getGrid() { + return grid; } - Stream> streamUpPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - final int x = current.location.x(); - final int y = current.location.y(); - final var up = tiles[x - 1][y]; - // amphipod is in a side room - if (isSideRoom(up.location()) && current.targetType == amphipodType) { - // amphipod is in the back of the room in which it belongs, stop here - return Stream.of(pathSoFar); - } - final var incrementalPath = new ArrayList<>(pathSoFar); - incrementalPath.add(up.location()); - // whether "up" is the front of the room or the corridor outside the room, we have to keep moving - return iterateThroughPaths(amphipodType, up, incrementalPath); + protected int getMinX() { + return minX; } - Stream> streamDownPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - final int x = current.location.x(); - final int y = current.location.y(); - final var down = tiles[x + 1][y]; - final var incrementalPath = new ArrayList<>(pathSoFar); - incrementalPath.add(down.location()); - if ((isCorridor(current.location()) && canEnterRoom(amphipodType, down)) || isSideRoom(current.location())) { - // go as for back into the room as possible, don't just stop at the entrance - return iterateThroughPaths(amphipodType, down, incrementalPath); - } - return Stream.empty(); + protected void setMinX(int minX) { + this.minX = minX; } - Stream> streamLeftPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - final int x = current.location.x(); - final int y = current.location.y(); - final var left = tiles[x][y - 1]; - Stream> result = Stream.empty(); - final var incrementalPath = new ArrayList<>(pathSoFar); - incrementalPath.add(left.location()); - if (tiles[left.location().x() + 1][left.location().y()] == null || !isSideRoom(tiles[left.location().x() + 1][left.location().y()].location())) { - // this is not in front of a side room, - // we can stop here while other amphipods move - result = Stream.concat(result, Stream.of(Collections.unmodifiableList(incrementalPath))); - } - result = Stream.concat(result, iterateThroughPaths(amphipodType, left, incrementalPath)); - return result; + protected int getMaxX() { + return maxX; } - Stream> streamRightPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - final int x = current.location.x(); - final int y = current.location.y(); - final var right = tiles[x][y + 1]; - Stream> result = Stream.empty(); - final var incrementalPath = new ArrayList<>(pathSoFar); - incrementalPath.add(right.location()); - if (tiles[right.location().x() + 1][right.location().y()] == null || !isSideRoom(tiles[right.location().x() + 1][right.location().y()].location())) { - // this is not in front of a side room, - // we can stop here while other amphipods move - result = Stream.concat(result, Stream.of(Collections.unmodifiableList(incrementalPath))); - } - result = Stream.concat(result, iterateThroughPaths(amphipodType, right, incrementalPath)); - return result; + protected void setMaxX(int maxX) { + this.maxX = maxX; } -// /** -// * Find all the paths an amphipod can take -// * -// * @param amphipodType the type of amphipod that is moving -// * @param current a tile through which the amphipod will take -// * @param pathSoFar the full path of the amphipod so far, *must* include _current_ -// * @return all the paths (start to finish) the amphipod can take -// */ -// @Deprecated -// List> getPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { -// final int x = current.location.x(); -// final int y = current.location.y(); -// final var up = tiles[x - 1][y]; -// final var down = tiles[x + 1][y]; -// final var left = tiles[x][y - 1]; -// final var right = tiles[x][y + 1]; -// -// final var result = new ArrayList>(); -// if (up != null && up.isVacant() && !pathSoFar.contains(up.location())) { -// // amphipod is in a side room -// if (isSideRoom(up.location()) && current.targetType == amphipodType) { -// // amphipod is in the back of the room in which it belongs, stop here -// return Collections.singletonList(pathSoFar); -// } -// final var incrementalPath = new ArrayList<>(pathSoFar); -// incrementalPath.add(up.location()); -// // whether "up" is the front of the room or the corridor outside the room, we have to keep moving -// result.addAll(getPaths(amphipodType, up, incrementalPath)); -// } -// if (down != null -// && down.isVacant() -// && !pathSoFar.contains(down.location()) -// // don't enter side room unless it is the ultimate destination -// && down.targetType == amphipodType) { -// // either entering a room or moving to the back of the room -// final var incrementalPath = new ArrayList<>(pathSoFar); -// incrementalPath.add(down.location()); -// if ((isCorridor(current.location()) && canEnterRoom(amphipodType, down)) || isSideRoom(current.location())) { -// // go as for back into the room as possible, don't just stop at the entrance -// result.addAll(getPaths(amphipodType, down, incrementalPath)); -// } -// } -// if (left != null && left.isVacant() && !pathSoFar.contains(left.location())) { -// final var incrementalPath = new ArrayList<>(pathSoFar); -// incrementalPath.add(left.location()); -// if (tiles[left.location().x() + 1][left.location().y()] == null || !isSideRoom(tiles[left.location().x() + 1][left.location().y()].location())) { -// // this is not in front of a side room, -// // we can stop here while other amphipods move -// result.add(Collections.unmodifiableList(incrementalPath)); -// } -// result.addAll(getPaths(amphipodType, left, incrementalPath)); -// } -// if (right != null && right.isVacant() && !pathSoFar.contains(right.location())) { -// final var incrementalPath = new ArrayList<>(pathSoFar); -// incrementalPath.add(right.location()); -// if (tiles[right.location().x() + 1][right.location().y()] == null || !isSideRoom(tiles[right.location().x() + 1][right.location().y()].location())) { -// // this is not in front of a side room, -// // we can stop here while other amphipods move -// result.add(Collections.unmodifiableList(incrementalPath)); -// } -// result.addAll(getPaths(amphipodType, right, incrementalPath)); -// } -// if (result.isEmpty() && pathSoFar.size() > 1) { -// // dead end, emit the path so far -// result.add(pathSoFar); -// } -// return Collections.unmodifiableList(result); -// } - - boolean canEnterRoom(final AmphipodType amphipodType, final Tile frontOfRoom) { - if (!isSideRoom(frontOfRoom.location())) { - throw new IllegalArgumentException("Not a side room: " + frontOfRoom); - } - if (frontOfRoom.targetType() != amphipodType) { - // this is not the destination room - return false; - } - // ensure all occupants have this as their destination - boolean hasOccupants = false; - for (var roomTile = tiles()[frontOfRoom.location().x() + 1][frontOfRoom.location().y()]; - roomTile != null; - roomTile = tiles()[roomTile.location().x() + 1][roomTile.location().y()]) { - if (roomTile.amphipodType() == null) { - if (hasOccupants) { - System.err.println("***There is a gap in the room***"); - return false; // there is a gap that shouldn't be here - } - continue; - } else { - hasOccupants = true; - } - if (!roomTile.hasTargetType()) { - return false; - } - } - return true; + protected int getMinY() { + return minY; } - /** - * Estimate the energy required for all the amphipods to get to their side rooms. This strictly underestimates - * the amount of energy required. It assumes: each amphipod has an unobstructed path to their room, they only - * need to get to the front of the room, not all the way to the back, they do not need to take any detours to - * let other amphipods pass (e.g. they don't need to leave the room and then come back in). - * - * @return an underestimate of the energy required for the amphipods of the burrow to self-organise - */ - public int estimatedDistanceToSolution() { - if (estimatedDistanceCache.containsKey(this)) { - return estimatedDistanceCache.get(this); - } - int result = 0; - for (int i = tiles.length; --i >= 0; ) { - final var row = tiles[i]; - for (int j = row.length; --j >= 0; ) { - final var tile = row[j]; - if (tile != null && tile.amphipodType != null) { - final int horizontalDistance = - Math.abs(tile.amphipodType.destinationColumn - tile.location().y()); - int verticalDistance = 0; - if (horizontalDistance != 0) { - // get to the corridor - verticalDistance = tile.location().x() - 1; - // enter the side room - verticalDistance += 1; - } else if (isCorridor(tile.location())) { - // it's implied that horizontal distance is 0 - // we're right outside the target room - // enter the side room - verticalDistance = 1; - } - final int distance = verticalDistance + horizontalDistance; - result += distance * tile.amphipodType().energyPerStep; - } - } - } - estimatedDistanceCache.put(this, result); - return result; + protected void setMinY(int minY) { + this.minY = minY; } - } + protected int getMaxY() { + return maxY; + } - protected record Point(int x, int y) { + protected void setMaxY(int maxY) { + this.maxY = maxY; + } - public Tile getTile(final Tile[][] tiles) { - return tiles[x()][y()]; + protected List getMovementPriority() { + return movementPriority; } - public void setTile(final Tile[][] tiles, final Tile tile) { - if (tile.location().x() != x() || tile.location().y() != y()) { - throw new IllegalArgumentException("Tile and location do not match"); - } - tiles[x()][y()] = tile; + protected void setMovementPriority(List movementPriority) { + this.movementPriority = movementPriority; } } - public record Tile(Point location, AmphipodType targetType, AmphipodType amphipodType) { + protected static Crater getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-23.txt"), false) + .collect(Collectors.joining("\n")); + return Crater.fromString(lines); + } - public Tile updateType(final AmphipodType newType) { - return new Tile(location, targetType, newType); - } + @Test + public final void part1() { + final var crater = getInput(); + for (int i = 10; --i >= 0; crater.round()) ; + final var result = crater.countEmptyGroundTiles(); - public boolean isVacant() { - return amphipodType == null; - } + System.out.println("Part 1: " + result); + } - public boolean hasTargetType() { - return Objects.equals(amphipodType(), targetType()); - } + @Test + public final void testRound1() { + // given + final var block = """ + ## + #. + .. + ## + """; + final var crater = Crater.fromString(block); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ## + .. + #. + .# + #. + """, result); + } + @Test + public final void testRound2() { + // given + final var block = """ + ## + .. + #. + .# + #. + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.South, Direction.West, Direction.East, Direction.North))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + .##. + #... + ...# + .... + .#.. + """, result); } -// public int lwst(final Node start) { -// final var lowestCostToNode = new ConcurrentHashMap(); -// final var estimatedCostThroughNode = new ConcurrentHashMap(); -// final var openSet = new PriorityBlockingQueue(100000, Comparator.comparing(estimatedCostThroughNode::get)); -// -// // add the starting node, getting there is free -// lowestCostToNode.put(start, 0); -// estimatedCostThroughNode.put(start, start.estimatedDistanceToSolution()); -// openSet.add(start); -// -// final var executor = ForkJoinPool.commonPool(); -// final var stateModifiers = new LinkedBlockingDeque>(); -// final var complete = new AtomicBoolean(false); -// executor.execute(() -> { -// while (!complete.get()) { -// Thread.yield(); -// try { -// final var current = openSet.take(); -// if (current.isSolution()) { -// stateModifiers.addFirst(() -> lowestCostToNode.get(current)); -// } -// final var lowestCostToCurrent = lowestCostToNode.get(current); -// executor.execute(() -> current.getBranches().map(branchResult -> { -// final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); -// final var branchNode = branchResult.node(); -// final Supplier updater = () -> { -// if (tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE)) { -// // either we've never visited this node before, -// // or the last time we did, we took a more expensive route -// lowestCostToNode.put(branchNode, tentativeBranchCost); -// -// // update the cost through this branch -// // need to remove and re-add to get correct ordering in the open set -// estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); -// -// openSet.remove(branchNode); // O(n) -// openSet.add(branchNode); // O(log(n)) -// } -// return (Integer)null; -// }; -// return updater; -// }).forEach(stateModifiers::addLast)); -// } catch (InterruptedException e) { -// e.printStackTrace(); -// complete.set(true); -// Thread.currentThread().interrupt(); -// throw new RuntimeException(e.getMessage(), e); -// } -// } -// }); -// // process updates sequentially -// while (!complete.get()) { -// Thread.yield(); -// try { -// final var updater = stateModifiers.takeFirst(); -// final var cost = updater.get(); -// if (cost != null) { -// complete.set(true); -// return cost; -// } -// } catch (InterruptedException e) { -// e.printStackTrace(); -// complete.set(true); -// Thread.currentThread().interrupt(); -// throw new RuntimeException(e.getMessage(), e); -// } -// } -// throw new IllegalStateException("An error occurred"); -// } -// -// protected String id(Node node) { -// final var outputStream = new ByteArrayOutputStream(); -// outputStream.write(node.hashCode()); -// return Base64.getEncoder().encodeToString(outputStream.toByteArray()); -// } - - public int lowest(final Node start) { - final var lowestCostToNode = new ConcurrentHashMap(); - final var estimatedCostThroughNode = new ConcurrentHashMap(); - final var openSet = new PriorityBlockingQueue(100000, Comparator.comparing(estimatedCostThroughNode::get)); - - // add the starting node, getting there is free - lowestCostToNode.put(start, 0); - estimatedCostThroughNode.put(start, start.estimatedDistanceToSolution()); - openSet.add(start); - - while (!openSet.isEmpty()) { - if(Node.solutionCache.size() % 10000 == 0) { - System.err.println(openSet.size() + " branches left to check in the open set"); - System.err.println("Appraised the energy cost to " + lowestCostToNode.size() + " nodes."); - System.err.println("Estimated the energy cost through " + estimatedCostThroughNode.size() + " nodes."); - System.err.println("Lowest estimated cost so far: " + estimatedCostThroughNode.get(openSet.peek())); - } - final var current = openSet.poll(); // O(log(n)) - if (current.isSolution()) { - System.err.println("Found solution:\n" + current); - return lowestCostToNode.get(current); - } - final var lowestCostToCurrent = lowestCostToNode.get(current); - current.getBranches() - .parallel() - .filter(branchResult -> { - final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); - final var branchNode = branchResult.node(); - return tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE); - }) - .forEach(branchResult -> { - final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); - final var branchNode = branchResult.node(); - // either we've never visited this node before, - // or the last time we did, we took a more expensive route - lowestCostToNode.put(branchNode, tentativeBranchCost); - - // update the cost through this branch - // need to remove and re-add to get correct ordering in the open set -// openSet.remove(branchNode); // O(n) - openSet.removeIf(node -> node.equals(branchNode)); - estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); - openSet.add(branchNode); // O(log(n)) - }); -// current.getBranches().forEach(branchResult -> { -// final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); -// final var branchNode = branchResult.node(); -// if (tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE)) { -// // either we've never visited this node before, -// // or the last time we did, we took a more expensive route -// lowestCostToNode.put(branchNode, tentativeBranchCost); -// -// // update the cost through this branch -// // need to remove and re-add to get correct ordering in the open set -// openSet.remove(branchNode); // O(n) -// estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); -// openSet.add(branchNode); // O(log(n)) -// } -// }); - } - throw new IllegalStateException("Amphipods are gridlocked :-("); + @Test + public final void testRound3() { + // given + final var block = """ + .##. + #... + ...# + .... + .#.. + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.West, Direction.East, Direction.North, Direction.South))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ..#.. + ....# + #.... + ....# + ..... + ..#.. + """, result); } - @Nested - public class NodeTest { - - @Test - public final void verifyEquality() { - // given - final var string = """ - ############# - #.....D.D.A.# - ###.#B#C#.### - #A#B#C#.# - ######### - """; - final var x = Node.createInitialNode(parseGrid(string.lines().toList())); - final var y = Node.createInitialNode(parseGrid(string.lines().toList())); - - // when - - // then - assertEquals(x.hashCode(), y.hashCode()); - assertEquals(x, y); - } + @Test + public final void testLargerRound1() { + // given + final var block = """ + .......#...... + .....###.#.... + ...#...#.#.... + ....#...##.... + ...#.###...... + ...##.#.##.... + ....#..#...... + """; + final var crater = Crater.fromString(block); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + .....#... + ...#...#. + .#..#.#.. + .....#..# + ..#.#.##. + #..#.#... + #.#.#.##. + ......... + ..#..#... + """, result); + } - @Test - public final void verifyBranchEquality() { - // given - final var string = """ - ############# - #.....D.D.A.# - ###.#B#C#.### - #A#B#C#.# - ######### - """; - final var original = Node.createInitialNode(parseGrid(string.lines().toList())); - - // when - final var x = original.branch(Collections.singletonList(new Move(new Point(2, 5), new Point(1, 2)))); - final var y = original.branch(Collections.singletonList(new Move(new Point(2, 5), new Point(1, 2)))); - - // then - assertEquals(x.hashCode(), y.hashCode()); - assertEquals(x, y); - } + @Test + public final void testLargerRound2() { + // given + final var block = """ + .....#... + ...#...#. + .#..#.#.. + .....#..# + ..#.#.##. + #..#.#... + #.#.#.##. + ......... + ..#..#... + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.South, Direction.West, Direction.East, Direction.North))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#.... + ...#.....#. + ..#..#.#... + ......#...# + ..#..#.#... + #...#.#.#.. + ........... + .#.#.#.##.. + ...#..#.... + """, result); + } - @Test - public final void verifyEstimatedDistanceIsZero() { - // given - final var string = """ - ############# - #...........# - ###A#B#C#D### - #A#B#C#D# - ######### - """; - final var initial = Node.createInitialNode(parseGrid(string.lines().toList())); - - // when - final var result = initial.estimatedDistanceToSolution(); - - // then - assertEquals(0, result); - } + @Test + public final void testLargerRound3() { + // given + final var block = """ + ......#.... + ...#.....#. + ..#..#.#... + ......#...# + ..#..#.#... + #...#.#.#.. + ........... + .#.#.#.##.. + ...#..#.... + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.West, Direction.East, Direction.North, Direction.South))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#.... + ....#....#. + .#..#...#.. + ......#...# + ..#..#.#... + #..#.....#. + ......##... + .##.#....#. + ..#........ + ......#.... + """, result); + } - @Test - public final void verifyEstimationOrdering() { - // given - final var states = new String[]{ - """ - ############# - #...........# - ###B#C#B#D### - #A#D#C#A# - ######### - """, - """ - ############# - #...B.......# - ###B#C#.#D### - #A#D#C#A# - ######### - """, - """ - ############# - #...B.......# - ###B#.#C#D### - #A#D#C#A# - ######### - """, - """ - ############# - #.....D.....# - ###B#.#C#D### - #A#B#C#A# - ######### - """, - """ - ############# - #.....D.....# - ###.#B#C#D### - #A#B#C#A# - ######### - """, - """ - ############# - #.....D.D.A.# - ###.#B#C#.### - #A#B#C#.# - ######### - """, - """ - ############# - #.........A.# - ###.#B#C#D### - #A#B#C#D# - ######### - """, - """ - ############# - #...........# - ###A#B#C#D### - #A#B#C#D# - ######### - """ - }; - final var nodes = Arrays.stream(states) - .map(String::lines) - .map(Stream::toList) - .map(Day23.this::parseGrid) - .map(Node::createInitialNode) - .toList(); - - // when - - // then - for (int i = 1; i < nodes.size(); i++) { - final var previous = nodes.get(i - 1); - final var current = nodes.get(i); - assertTrue(previous.estimatedDistanceToSolution() >= current.estimatedDistanceToSolution(), - "Previous state has a lower estimated distance. Previous:\n" + previous + "\n(cost: " + previous.estimatedDistanceToSolution() + ")\nCurrent:\n" + current + "\n(cost: " + current.estimatedDistanceToSolution() + ")"); - } - } + @Test + public final void testLargerRound4() { + // given + final var block = """ + ......#.... + ....#....#. + .#..#...#.. + ......#...# + ..#..#.#... + #..#.....#. + ......##... + .##.#....#. + ..#........ + ......#.... + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.East, Direction.North, Direction.South, Direction.West))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#.... + .....#....# + .#...##.... + ..#.....#.# + ........#.. + #...###..#. + .#......#.. + ...##....#. + ...#....... + ......#.... + """, result); } @Test - public final void part1() { - final var initial = Node.createInitialNode(parseGrid(getInput().toList())); + public final void testLargerRound5() { + // given + final var block = """ + ......#.... + .....#....# + .#...##.... + ..#.....#.# + ........#.. + #...###..#. + .#......#.. + ...##....#. + ...#....... + ......#.... + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.North, Direction.South, Direction.West, Direction.East))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#.... + ........... + .#..#.....# + ........#.. + .....##...# + #.#.####... + ..........# + ...##..#... + .#......... + .........#. + ...#..#.... + """, result); + } - System.out.println("Part 1: " + lowest(initial)); + @Test + public final void testLargerRounds() { + // given + final var block = """ + .......#...... + .....###.#.... + ...#...#.#.... + ....#...##.... + ...#.###...... + ...##.#.##.... + ....#..#...... + """; + final var crater = Crater.fromString(block); + + // when + for (int i = 10; --i >= 0; crater.round()) ; + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#..... + ..........#. + .#.#..#..... + .....#...... + ..#.....#..# + #......##... + ....##...... + .#........#. + ...#.#..#... + ............ + ...#..#..#.. + """, result); + Assertions.assertEquals(110, crater.countEmptyGroundTiles()); } @Test public final void part2() { - final var lines = getInput().collect(Collectors.toList()); - lines.add(3, " #D#B#A#C# "); - lines.add(3, " #D#C#B#A# "); - final var initial = Node.createInitialNode(parseGrid(lines)); - System.err.println("Initial state:\n" + initial); - - System.out.println("Part 2: " + lowest(initial)); + final var crater = getInput(); + int rounds = 0; + while (true) { + final var moves = crater.round(); + rounds++; + if (moves == 0) { + break; + } + } + System.out.println("Part 2: " + rounds); } } \ No newline at end of file diff --git a/src/test/resources/sample/day-01.txt b/src/test/resources/sample/day-01.txt index 167e291..444e241 100644 --- a/src/test/resources/sample/day-01.txt +++ b/src/test/resources/sample/day-01.txt @@ -1,10 +1,14 @@ -199 -200 -208 -210 -200 -207 -240 -269 -260 -263 +1000 +2000 +3000 + +4000 + +5000 +6000 + +7000 +8000 +9000 + +10000 \ No newline at end of file diff --git a/src/test/resources/sample/day-02.txt b/src/test/resources/sample/day-02.txt index b7172ac..25097e8 100644 --- a/src/test/resources/sample/day-02.txt +++ b/src/test/resources/sample/day-02.txt @@ -1,6 +1,3 @@ -forward 5 -down 5 -forward 8 -up 3 -down 8 -forward 2 +A Y +B X +C Z \ No newline at end of file diff --git a/src/test/resources/sample/day-03.txt b/src/test/resources/sample/day-03.txt index a6366a8..9919ffa 100644 --- a/src/test/resources/sample/day-03.txt +++ b/src/test/resources/sample/day-03.txt @@ -1,12 +1,6 @@ -00100 -11110 -10110 -10111 -10101 -01111 -00111 -11100 -10000 -11001 -00010 -01010 +vJrwpWtwJgWrhcsFMMfFFhFp +jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL +PmmdzqPrVvPwwTWBwg +wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn +ttgJtRGJQctTZtZT +CrZsJsPPZsGzwwsLwLmpwMDw \ No newline at end of file diff --git a/src/test/resources/sample/day-04.txt b/src/test/resources/sample/day-04.txt index 669a51d..99a66c5 100644 --- a/src/test/resources/sample/day-04.txt +++ b/src/test/resources/sample/day-04.txt @@ -1,19 +1,6 @@ -7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1 - -22 13 17 11 0 - 8 2 23 4 24 -21 9 14 16 7 - 6 10 3 18 5 - 1 12 20 15 19 - - 3 15 0 2 22 - 9 18 13 17 5 -19 8 7 25 23 -20 11 10 24 4 -14 21 16 12 6 - -14 21 17 24 4 -10 16 15 9 19 -18 8 23 26 20 -22 11 13 6 5 - 2 0 12 3 7 +2-4,6-8 +2-3,4-5 +5-7,7-9 +2-8,3-7 +6-6,4-6 +2-6,4-8 \ No newline at end of file diff --git a/src/test/resources/sample/day-05.txt b/src/test/resources/sample/day-05.txt index b258f68..e98aba4 100644 --- a/src/test/resources/sample/day-05.txt +++ b/src/test/resources/sample/day-05.txt @@ -1,10 +1,9 @@ -0,9 -> 5,9 -8,0 -> 0,8 -9,4 -> 3,4 -2,2 -> 2,1 -7,0 -> 7,4 -6,4 -> 2,0 -0,9 -> 2,9 -3,4 -> 1,4 -0,0 -> 8,8 -5,5 -> 8,2 + [D] +[N] [C] +[Z] [M] [P] + 1 2 3 + +move 1 from 2 to 1 +move 3 from 1 to 3 +move 2 from 2 to 1 +move 1 from 1 to 2 \ No newline at end of file diff --git a/src/test/resources/sample/day-06.txt b/src/test/resources/sample/day-06.txt index 55129f1..5a2b0a7 100644 --- a/src/test/resources/sample/day-06.txt +++ b/src/test/resources/sample/day-06.txt @@ -1 +1 @@ -3,4,3,1,2 +mjqjpqmgbljsphdztnvjfqwrcgsmlb \ No newline at end of file diff --git a/src/test/resources/sample/day-07.txt b/src/test/resources/sample/day-07.txt index 18bd32a..bcbb513 100644 --- a/src/test/resources/sample/day-07.txt +++ b/src/test/resources/sample/day-07.txt @@ -1 +1,23 @@ -16,1,2,0,4,2,7,1,2,14 +$ cd / +$ ls +dir a +14848514 b.txt +8504156 c.dat +dir d +$ cd a +$ ls +dir e +29116 f +2557 g +62596 h.lst +$ cd e +$ ls +584 i +$ cd .. +$ cd .. +$ cd d +$ ls +4060174 j +8033020 d.log +5626152 d.ext +7214296 k \ No newline at end of file diff --git a/src/test/resources/sample/day-08.txt b/src/test/resources/sample/day-08.txt index c9f629b..6557024 100644 --- a/src/test/resources/sample/day-08.txt +++ b/src/test/resources/sample/day-08.txt @@ -1,10 +1,5 @@ -be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe -edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc -fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg -fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb -aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea -fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb -dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe -bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef -egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb -gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce +30373 +25512 +65332 +33549 +35390 \ No newline at end of file diff --git a/src/test/resources/sample/day-09.txt b/src/test/resources/sample/day-09.txt index 6dee4a4..cbea2b3 100644 --- a/src/test/resources/sample/day-09.txt +++ b/src/test/resources/sample/day-09.txt @@ -1,5 +1,8 @@ -2199943210 -3987894921 -9856789892 -8767896789 -9899965678 +R 4 +U 4 +L 3 +D 1 +R 4 +D 1 +L 5 +R 2 \ No newline at end of file diff --git a/src/test/resources/sample/day-10.txt b/src/test/resources/sample/day-10.txt index b1518d9..94cd0a8 100644 --- a/src/test/resources/sample/day-10.txt +++ b/src/test/resources/sample/day-10.txt @@ -1,10 +1,146 @@ -[({(<(())[]>[[{[]{<()<>> -[(()[<>])]({[<{<<[]>>( -{([(<{}[<>[]}>{[]{[(<()> -(((({<>}<{<{<>}{[]{[]{} -[[<[([]))<([[{}[[()]]] -[{[{({}]{}}([{[{{{}}([] -{<[[]]>}<{[{[{[]{()[[[] -[<(<(<(<{}))><([]([]() -<{([([[(<>()){}]>(<<{{ -<{([{{}}[<[[[<>{}]]]>[]] +addx 15 +addx -11 +addx 6 +addx -3 +addx 5 +addx -1 +addx -8 +addx 13 +addx 4 +noop +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx -35 +addx 1 +addx 24 +addx -19 +addx 1 +addx 16 +addx -11 +noop +noop +addx 21 +addx -15 +noop +noop +addx -3 +addx 9 +addx 1 +addx -3 +addx 8 +addx 1 +addx 5 +noop +noop +noop +noop +noop +addx -36 +noop +addx 1 +addx 7 +noop +noop +noop +addx 2 +addx 6 +noop +noop +noop +noop +noop +addx 1 +noop +noop +addx 7 +addx 1 +noop +addx -13 +addx 13 +addx 7 +noop +addx 1 +addx -33 +noop +noop +noop +addx 2 +noop +noop +noop +addx 8 +noop +addx -1 +addx 2 +addx 1 +noop +addx 17 +addx -9 +addx 1 +addx 1 +addx -3 +addx 11 +noop +noop +addx 1 +noop +addx 1 +noop +noop +addx -13 +addx -19 +addx 1 +addx 3 +addx 26 +addx -30 +addx 12 +addx -1 +addx 3 +addx 1 +noop +noop +noop +addx -9 +addx 18 +addx 1 +addx 2 +noop +noop +addx 9 +noop +noop +noop +addx -1 +addx 2 +addx -37 +addx 1 +addx 3 +noop +addx 15 +addx -21 +addx 22 +addx -6 +addx 1 +noop +addx 2 +addx 1 +noop +addx -10 +noop +noop +addx 20 +addx 1 +addx 2 +addx 2 +addx -6 +addx -11 +noop +noop +noop \ No newline at end of file diff --git a/src/test/resources/sample/day-11.txt b/src/test/resources/sample/day-11.txt index 03743f6..c04eddb 100644 --- a/src/test/resources/sample/day-11.txt +++ b/src/test/resources/sample/day-11.txt @@ -1,10 +1,27 @@ -5483143223 -2745854711 -5264556173 -6141336146 -6357385478 -4167524645 -2176841721 -6882881134 -4846848554 -5283751526 +Monkey 0: + Starting items: 79, 98 + Operation: new = old * 19 + Test: divisible by 23 + If true: throw to monkey 2 + If false: throw to monkey 3 + +Monkey 1: + Starting items: 54, 65, 75, 74 + Operation: new = old + 6 + Test: divisible by 19 + If true: throw to monkey 2 + If false: throw to monkey 0 + +Monkey 2: + Starting items: 79, 60, 97 + Operation: new = old * old + Test: divisible by 13 + If true: throw to monkey 1 + If false: throw to monkey 3 + +Monkey 3: + Starting items: 74 + Operation: new = old + 3 + Test: divisible by 17 + If true: throw to monkey 0 + If false: throw to monkey 1 \ No newline at end of file diff --git a/src/test/resources/sample/day-12.txt b/src/test/resources/sample/day-12.txt index 6fd8c41..86e9cac 100644 --- a/src/test/resources/sample/day-12.txt +++ b/src/test/resources/sample/day-12.txt @@ -1,7 +1,5 @@ -start-A -start-b -A-c -A-b -b-d -A-end -b-end +Sabqponm +abcryxxl +accszExk +acctuvwj +abdefghi diff --git a/src/test/resources/sample/day-13.txt b/src/test/resources/sample/day-13.txt index 282114c..27c8912 100644 --- a/src/test/resources/sample/day-13.txt +++ b/src/test/resources/sample/day-13.txt @@ -1,21 +1,23 @@ -6,10 -0,14 -9,10 -0,3 -10,4 -4,11 -6,0 -6,12 -4,1 -0,13 -10,12 -3,4 -3,0 -8,4 -1,10 -2,14 -8,10 -9,0 +[1,1,3,1,1] +[1,1,5,1,1] -fold along y=7 -fold along x=5 +[[1],[2,3,4]] +[[1],4] + +[9] +[[8,7,6]] + +[[4,4],4,4] +[[4,4],4,4,4] + +[7,7,7,7] +[7,7,7] + +[] +[3] + +[[[]]] +[[]] + +[1,[2,[3,[4,[5,6,7]]]],8,9] +[1,[2,[3,[4,[5,6,0]]]],8,9] \ No newline at end of file diff --git a/src/test/resources/sample/day-14.txt b/src/test/resources/sample/day-14.txt index b5594dd..1926028 100644 --- a/src/test/resources/sample/day-14.txt +++ b/src/test/resources/sample/day-14.txt @@ -1,18 +1,2 @@ -NNCB - -CH -> B -HH -> N -CB -> H -NH -> C -HB -> C -HC -> B -HN -> C -NN -> C -BH -> H -NC -> B -NB -> B -BN -> B -BB -> N -BC -> B -CC -> N -CN -> C +498,4 -> 498,6 -> 496,6 +503,4 -> 502,4 -> 502,9 -> 494,9 \ No newline at end of file diff --git a/src/test/resources/sample/day-15.txt b/src/test/resources/sample/day-15.txt index ab80887..652e631 100644 --- a/src/test/resources/sample/day-15.txt +++ b/src/test/resources/sample/day-15.txt @@ -1,10 +1,14 @@ -1163751742 -1381373672 -2136511328 -3694931569 -7463417111 -1319128137 -1359912421 -3125421639 -1293138521 -2311944581 +Sensor at x=2, y=18: closest beacon is at x=-2, y=15 +Sensor at x=9, y=16: closest beacon is at x=10, y=16 +Sensor at x=13, y=2: closest beacon is at x=15, y=3 +Sensor at x=12, y=14: closest beacon is at x=10, y=16 +Sensor at x=10, y=20: closest beacon is at x=10, y=16 +Sensor at x=14, y=17: closest beacon is at x=10, y=16 +Sensor at x=8, y=7: closest beacon is at x=2, y=10 +Sensor at x=2, y=0: closest beacon is at x=2, y=10 +Sensor at x=0, y=11: closest beacon is at x=2, y=10 +Sensor at x=20, y=14: closest beacon is at x=25, y=17 +Sensor at x=17, y=20: closest beacon is at x=21, y=22 +Sensor at x=16, y=7: closest beacon is at x=15, y=3 +Sensor at x=14, y=3: closest beacon is at x=15, y=3 +Sensor at x=20, y=1: closest beacon is at x=15, y=3 \ No newline at end of file diff --git a/src/test/resources/sample/day-16.txt b/src/test/resources/sample/day-16.txt deleted file mode 100644 index 3f0eda1..0000000 --- a/src/test/resources/sample/day-16.txt +++ /dev/null @@ -1 +0,0 @@ -D2FE28 diff --git a/src/test/resources/sample/day-17.txt b/src/test/resources/sample/day-17.txt deleted file mode 100644 index a07e02d..0000000 --- a/src/test/resources/sample/day-17.txt +++ /dev/null @@ -1 +0,0 @@ -target area: x=20..30, y=-10..-5 diff --git a/src/test/resources/sample/day-18.txt b/src/test/resources/sample/day-18.txt index 1368dc4..d18bf98 100644 --- a/src/test/resources/sample/day-18.txt +++ b/src/test/resources/sample/day-18.txt @@ -1,10 +1,13 @@ -[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]] -[[[5,[2,8]],4],[5,[[9,9],0]]] -[6,[[[6,2],[5,6]],[[7,6],[4,7]]]] -[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]] -[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]] -[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]] -[[[[5,4],[7,7]],8],[[8,3],8]] -[[9,3],[[9,9],[6,[4,9]]]] -[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]] -[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]] +2,2,2 +1,2,2 +3,2,2 +2,1,2 +2,3,2 +2,2,1 +2,2,3 +2,2,4 +2,2,6 +1,2,5 +3,2,5 +2,1,5 +2,3,5 \ No newline at end of file diff --git a/src/test/resources/sample/day-19.txt b/src/test/resources/sample/day-19.txt deleted file mode 100644 index 4e496e9..0000000 --- a/src/test/resources/sample/day-19.txt +++ /dev/null @@ -1,136 +0,0 @@ ---- scanner 0 --- -404,-588,-901 -528,-643,409 --838,591,734 -390,-675,-793 --537,-823,-458 --485,-357,347 --345,-311,381 --661,-816,-575 --876,649,763 --618,-824,-621 -553,345,-567 -474,580,667 --447,-329,318 --584,868,-557 -544,-627,-890 -564,392,-477 -455,729,728 --892,524,684 --689,845,-530 -423,-701,434 -7,-33,-71 -630,319,-379 -443,580,662 --789,900,-551 -459,-707,401 - ---- scanner 1 --- -686,422,578 -605,423,415 -515,917,-361 --336,658,858 -95,138,22 --476,619,847 --340,-569,-846 -567,-361,727 --460,603,-452 -669,-402,600 -729,430,532 --500,-761,534 --322,571,750 --466,-666,-811 --429,-592,574 --355,545,-477 -703,-491,-529 --328,-685,520 -413,935,-424 --391,539,-444 -586,-435,557 --364,-763,-893 -807,-499,-711 -755,-354,-619 -553,889,-390 - ---- scanner 2 --- -649,640,665 -682,-795,504 --784,533,-524 --644,584,-595 --588,-843,648 --30,6,44 --674,560,763 -500,723,-460 -609,671,-379 --555,-800,653 --675,-892,-343 -697,-426,-610 -578,704,681 -493,664,-388 --671,-858,530 --667,343,800 -571,-461,-707 --138,-166,112 --889,563,-600 -646,-828,498 -640,759,510 --630,509,768 --681,-892,-333 -673,-379,-804 --742,-814,-386 -577,-820,562 - ---- scanner 3 --- --589,542,597 -605,-692,669 --500,565,-823 --660,373,557 --458,-679,-417 --488,449,543 --626,468,-788 -338,-750,-386 -528,-832,-391 -562,-778,733 --938,-730,414 -543,643,-506 --524,371,-870 -407,773,750 --104,29,83 -378,-903,-323 --778,-728,485 -426,699,580 --438,-605,-362 --469,-447,-387 -509,732,623 -647,635,-688 --868,-804,481 -614,-800,639 -595,780,-596 - ---- scanner 4 --- -727,592,562 --293,-554,779 -441,611,-461 --714,465,-776 --743,427,-804 --660,-479,-426 -832,-632,460 -927,-485,-438 -408,393,-506 -466,436,-512 -110,16,151 --258,-428,682 --393,719,612 --211,-452,876 -808,-476,-593 --575,615,604 --485,667,467 --680,325,-822 --627,-443,-432 -872,-547,-609 -833,512,582 -807,604,487 -839,-516,451 -891,-625,532 --652,-548,-490 -30,-46,-14 diff --git a/src/test/resources/sample/day-20.txt b/src/test/resources/sample/day-20.txt index 8fa4bd4..5cbf3d9 100644 --- a/src/test/resources/sample/day-20.txt +++ b/src/test/resources/sample/day-20.txt @@ -1,7 +1,7 @@ -..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..# - -#..#. -#.... -##..# -..#.. -..### +1 +2 +-3 +3 +-2 +0 +4 \ No newline at end of file diff --git a/src/test/resources/sample/day-21.txt b/src/test/resources/sample/day-21.txt index 3f69194..7993b87 100644 --- a/src/test/resources/sample/day-21.txt +++ b/src/test/resources/sample/day-21.txt @@ -1,2 +1,15 @@ -Player 1 starting position: 4 -Player 2 starting position: 8 +root: pppw + sjmn +dbpl: 5 +cczh: sllz + lgvd +zczc: 2 +ptdq: humn - dvpt +dvpt: 3 +lfqf: 4 +humn: 5 +ljgn: 2 +sjmn: drzm * dbpl +sllz: 4 +pppw: cczh / lfqf +lgvd: ljgn * ptdq +drzm: hmdt - zczc +hmdt: 32 \ No newline at end of file diff --git a/src/test/resources/sample/day-22.txt b/src/test/resources/sample/day-22.txt deleted file mode 100644 index 2790bed..0000000 --- a/src/test/resources/sample/day-22.txt +++ /dev/null @@ -1,60 +0,0 @@ -on x=-5..47,y=-31..22,z=-19..33 -on x=-44..5,y=-27..21,z=-14..35 -on x=-49..-1,y=-11..42,z=-10..38 -on x=-20..34,y=-40..6,z=-44..1 -off x=26..39,y=40..50,z=-2..11 -on x=-41..5,y=-41..6,z=-36..8 -off x=-43..-33,y=-45..-28,z=7..25 -on x=-33..15,y=-32..19,z=-34..11 -off x=35..47,y=-46..-34,z=-11..5 -on x=-14..36,y=-6..44,z=-16..29 -on x=-57795..-6158,y=29564..72030,z=20435..90618 -on x=36731..105352,y=-21140..28532,z=16094..90401 -on x=30999..107136,y=-53464..15513,z=8553..71215 -on x=13528..83982,y=-99403..-27377,z=-24141..23996 -on x=-72682..-12347,y=18159..111354,z=7391..80950 -on x=-1060..80757,y=-65301..-20884,z=-103788..-16709 -on x=-83015..-9461,y=-72160..-8347,z=-81239..-26856 -on x=-52752..22273,y=-49450..9096,z=54442..119054 -on x=-29982..40483,y=-108474..-28371,z=-24328..38471 -on x=-4958..62750,y=40422..118853,z=-7672..65583 -on x=55694..108686,y=-43367..46958,z=-26781..48729 -on x=-98497..-18186,y=-63569..3412,z=1232..88485 -on x=-726..56291,y=-62629..13224,z=18033..85226 -on x=-110886..-34664,y=-81338..-8658,z=8914..63723 -on x=-55829..24974,y=-16897..54165,z=-121762..-28058 -on x=-65152..-11147,y=22489..91432,z=-58782..1780 -on x=-120100..-32970,y=-46592..27473,z=-11695..61039 -on x=-18631..37533,y=-124565..-50804,z=-35667..28308 -on x=-57817..18248,y=49321..117703,z=5745..55881 -on x=14781..98692,y=-1341..70827,z=15753..70151 -on x=-34419..55919,y=-19626..40991,z=39015..114138 -on x=-60785..11593,y=-56135..2999,z=-95368..-26915 -on x=-32178..58085,y=17647..101866,z=-91405..-8878 -on x=-53655..12091,y=50097..105568,z=-75335..-4862 -on x=-111166..-40997,y=-71714..2688,z=5609..50954 -on x=-16602..70118,y=-98693..-44401,z=5197..76897 -on x=16383..101554,y=4615..83635,z=-44907..18747 -off x=-95822..-15171,y=-19987..48940,z=10804..104439 -on x=-89813..-14614,y=16069..88491,z=-3297..45228 -on x=41075..99376,y=-20427..49978,z=-52012..13762 -on x=-21330..50085,y=-17944..62733,z=-112280..-30197 -on x=-16478..35915,y=36008..118594,z=-7885..47086 -off x=-98156..-27851,y=-49952..43171,z=-99005..-8456 -off x=2032..69770,y=-71013..4824,z=7471..94418 -on x=43670..120875,y=-42068..12382,z=-24787..38892 -off x=37514..111226,y=-45862..25743,z=-16714..54663 -off x=25699..97951,y=-30668..59918,z=-15349..69697 -off x=-44271..17935,y=-9516..60759,z=49131..112598 -on x=-61695..-5813,y=40978..94975,z=8655..80240 -off x=-101086..-9439,y=-7088..67543,z=33935..83858 -off x=18020..114017,y=-48931..32606,z=21474..89843 -off x=-77139..10506,y=-89994..-18797,z=-80..59318 -off x=8476..79288,y=-75520..11602,z=-96624..-24783 -on x=-47488..-1262,y=24338..100707,z=16292..72967 -off x=-84341..13987,y=2429..92914,z=-90671..-1318 -off x=-37810..49457,y=-71013..-7894,z=-105357..-13188 -off x=-27365..46395,y=31009..98017,z=15428..76570 -off x=-70369..-16548,y=22648..78696,z=-1892..86821 -on x=-53470..21291,y=-120233..-33476,z=-44150..38147 -off x=-93533..-4276,y=-16170..68771,z=-104985..-24507 \ No newline at end of file diff --git a/src/test/resources/sample/day-23.txt b/src/test/resources/sample/day-23.txt index 6a7120d..7ac3ba9 100644 --- a/src/test/resources/sample/day-23.txt +++ b/src/test/resources/sample/day-23.txt @@ -1,5 +1,7 @@ -############# -#...........# -###B#C#B#D### - #A#D#C#A# - ######### +....#.. +..###.# +#...#.# +.#...## +#.###.. +##.#.## +.#..#.. \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy