diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffeb9d9..007257b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +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@v3 + with: + path: ~/.m2 + key: m2-${{ runner.os }}-19-${{ hashFiles('**/pom.xml') }} + restore-keys: | + m2-${{ runner.os }}-19 + m2-${{ runner.os }} + m2 - run: mvn clean install diff --git a/.gitignore b/.gitignore index 5c56b6a..4d131ae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ .settings advent-of-code.iml 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 6947951..29e9c19 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ -# Advent of Code 2020 (Java) +# Advent of Code 2022 (Java) + +This is the code I used to solve the +[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 ([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 22284a9..fc13d64 100644 --- a/pom.xml +++ b/pom.xml @@ -6,14 +6,37 @@ com.macasaet advent-of-code - 0.2020.0-SNAPSHOT + 0.2022.0-SNAPSHOT - Advent of Code 2020 + Advent of Code 2022 UTF-8 - 17 - 17 + 19 + 19 + + + org.junit.jupiter + junit-jupiter + 5.9.0 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + Day*.java + + + + + + diff --git a/src/test/java/com/macasaet/Day01.java b/src/test/java/com/macasaet/Day01.java index 678b200..d70e9ed 100644 --- a/src/test/java/com/macasaet/Day01.java +++ b/src/test/java/com/macasaet/Day01.java @@ -1,58 +1,76 @@ package com.macasaet; -import java.io.IOException; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; import java.util.List; -import java.util.Spliterator; import java.util.stream.StreamSupport; - +/** + * --- Day 1: Calorie Counting --- + */ public class Day01 { - public static void main(String[] args) throws IOException { - try( var spliterator = new LineSpliterator(Day01.class.getResourceAsStream("/day-1-input.txt")) ) { -// part1(spliterator); - part2(spliterator); - } + protected Iterator getInput() { + return StreamSupport + .stream(new LineSpliterator("day-01.txt"), + false) + .iterator(); } - protected static void part1(final Spliterator spliterator) { - final var items = StreamSupport.stream(spliterator, false) - .mapToInt(Integer::parseInt) - .filter(candidate -> candidate <= 2020) // filter out the obvious - .sorted() // sort to ensure the complement is to the left - .collect(ArrayList::new, List::add, List::addAll); - for( int i = items.size(); --i >= 0; ) { - final int x = items.get(i); - for( int j = i; --j >= 0; ) { // avoid retrying combinations - final int y = items.get(j); - if( x + y == 2020 ) { - System.out.println( "" + ( x * y ) ); - return; - } + 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); } - protected static void part2(final Spliterator spliterator) { - final var items = StreamSupport.stream(spliterator, false) - .mapToInt(Integer::parseInt) - .filter(candidate -> candidate <= 2020) // filter out the obvious - .sorted() // sort to ensure the complements are to the left - .collect(ArrayList::new, List::add, List::addAll); - for( int i = items.size(); --i >= 0; ) { - final int x = items.get(i); - for( int j = i; --j >= 0; ) { - final int y = items.get(j); - for( int k = j; --k >= 0; ) { - final int z = items.get(k); - if( x + y + z == 2020 ) { - System.out.println( "" + ( x * y * z ) ); - return; - } - } + @Test + public final void part1() { + 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 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()))); + } + + /** + * An elf who collects food for the reindeer. + * + * @param itemCalories The number of calories of each item carried by the elf + */ + public record Elf(List itemCalories) { + public BigInteger totalCaloriesCarried() { + return itemCalories().stream() + .reduce(BigInteger::add) + .get(); } } -} + +} \ 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 7bfb3d0..424b241 100644 --- a/src/test/java/com/macasaet/Day02.java +++ b/src/test/java/com/macasaet/Day02.java @@ -1,91 +1,175 @@ package com.macasaet; -import java.io.IOException; -import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 2: Rock Paper Scissors --- + * https://adventofcode.com/2022/day/2 + */ public class Day02 { - public static void main(String[] args) throws IOException { - try( var spliterator = new LineSpliterator( Day02.class.getResourceAsStream("/day-2-input.txt" ) ) ) { - final long count = StreamSupport.stream(spliterator, false) -// .map(SledEntry::build) // part 1 - .map(TobogganEntry::build) // part 2 - .filter(Entry::isValid).count(); - System.out.println("" + count); - } - + 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))); + }); } - public static abstract class Entry { - - // TODO consider a more robust pattern: - // https://stackoverflow.com/a/4731164/914887 - protected static final Pattern separator = Pattern.compile("\\s"); - protected static final Pattern rangeSeparator = Pattern.compile("-"); - protected final char c; // TODO support code points - protected final String password; + @Test + public final void part1() { + final var result = getInput().mapToInt(Round::naiveScore).sum(); - protected Entry(final char c, final String password) { - this.c = c; - this.password = password; - } + System.out.println("Part 1: " + result); + } - public abstract boolean isValid(); + @Test + public final void part2() { + final var result = getInput().mapToInt(Round::score).sum(); + System.out.println("Part 2: " + result); } - public static class SledEntry extends Entry { - private final long minIterations; - private final long maxIterations; - - public SledEntry( final char c, final String password, final long minIterations, final long maxIterations ) { - super(c, password); - this.minIterations = minIterations; - this.maxIterations = maxIterations; + /** + * 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 Shape beats() { + return Scissors; + } + }, + Paper { + public int score() { + return 2; + } + + public Shape beatenBy() { + return Scissors; + } + + public Shape beats() { + return Rock; + } + }, + Scissors { + public int score() { + return 3; + } + + @Override + public Shape beatenBy() { + return Rock; + } + + public Shape beats() { + return Paper; + } + }; + + 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 static Entry build(final String line) { - final var components = separator.split(line, 3); - final var range = components[0]; - final var rangeComponents = rangeSeparator.split(range, 2); - return new SledEntry(components[1].charAt(0), - components[2], - Long.parseLong(rangeComponents[ 0 ]), - Long.parseLong(rangeComponents[ 1 ] )); - } + /** + * @return the inherent value of this shape + */ + public abstract int score(); - public boolean isValid() { - final var count = password.chars() - .filter(candidate -> (char) candidate == this.c) - .count(); - return count >= minIterations && count <= maxIterations; - } - } + /** + * @return the shape that beats this one + */ + public abstract Shape beatenBy(); - public static class TobogganEntry extends Entry { - private final int firstPosition; - private final int secondPosition; + /** + * @return the shape this one beats + */ + public abstract Shape beats(); + } - public TobogganEntry( final char c, final String password, final int firstPosition, final int secondPosition ) { - super( c, password ); - this.firstPosition = firstPosition; - this.secondPosition = secondPosition; + /** + * 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); + }; } - public static Entry build( final String line ) { - final var components = separator.split( line, 3 ); - final var positionComponents = rangeSeparator.split( components[ 0 ], 2 ); - return new TobogganEntry( components[ 1 ].charAt( 0 ), - components[ 2 ], - Integer.parseInt( positionComponents[ 0 ] ), - Integer.parseInt( positionComponents[ 1 ] ) ); + public abstract Shape respond(final Shape opponent); + } + + /** + * 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(); } - public boolean isValid() { - final var x = password.charAt(firstPosition - 1); - final var y = password.charAt(secondPosition - 1); - return x == c ^ y == c; + /** + * @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(); } } + } \ 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 344fc9f..af6e98a 100644 --- a/src/test/java/com/macasaet/Day03.java +++ b/src/test/java/com/macasaet/Day03.java @@ -1,57 +1,117 @@ package com.macasaet; -import java.io.IOException; -import java.math.BigInteger; -import java.util.Arrays; -import java.util.stream.Collectors; +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.Set; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 3: Rucksack Reörganisation --- + * https://adventofcode.com/2022/day/3 + */ public class Day03 { - public static void main(String[] args) throws IOException { - try( var spliterator = new LineSpliterator( Day03.class.getResourceAsStream("/day-3-input.txt" ) ) ) { - final var rowList = StreamSupport.stream(spliterator, false) - .map(String::toCharArray) - .collect(Collectors.toUnmodifiableList()); - final var matrix = new char[rowList.size()][]; - for (int i = rowList.size(); --i >= 0; matrix[i] = rowList.get(i)); - - final var slopes = new int[][] { - new int[] { 1, 1 }, - new int[] { 3, 1 }, // uncomment all but this for "part 1" - new int[] { 5, 1 }, - new int[] { 7, 1 }, - new int[] { 1, 2 }, - }; - final var totalTrees = Arrays.stream(slopes) - .mapToLong(pair -> { - final int slopeRight = pair[0]; - final int slopeDown = pair[1]; - - long numTrees = 0; - int rowIndex = 0; - int columnIndex = 0; - - do { - final var row = matrix[rowIndex]; - final var cell = row[columnIndex]; - if (cell == '#') { - numTrees += 1l; - } - rowIndex += slopeDown; - columnIndex = columnIndex + slopeRight; - columnIndex = columnIndex % row.length; // "These aren't the only trees, though; due to - // something you read about once involving - // arboreal genetics and biome stability, the - // same pattern repeats to the right many times" - } while( rowIndex < matrix.length ); - return numTrees; - }).mapToObj(BigInteger::valueOf) // I wasn't sure how large these (multiplied) values could get, in retrospect, `long` would have been fine - .reduce(BigInteger::multiply) - .get(); - System.out.println("" + totalTrees.toString()); + 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(Rucksack::parse); + } + + @Test + public final void part1() { + final var result = getInput().mapToInt(Rucksack::priority).sum(); + + System.out.println("Part 1: " + result); + } + + @Test + 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); + } + if (currentGroup.size() == 3) { + groups.add(Collections.unmodifiableList(currentGroup)); } + final var result = groups.stream().map(this::getBadge).mapToInt(Day03::priority).sum(); + System.out.println("Part 2: " + result); } + 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; + } + } + 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)); + } + + public int priority() { + final var intersection = new HashSet(); + for (final char c : firstCompartment) { + if (secondCompartment.contains(c)) { + intersection.add(c); + } + } + if (intersection.size() != 1) { + throw new IllegalStateException("There should only be one common item between compartments"); + } + return Day03.priority(intersection.iterator().next()); + } + + + } } \ 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 138675b..8b3d13d 100644 --- a/src/test/java/com/macasaet/Day04.java +++ b/src/test/java/com/macasaet/Day04.java @@ -1,83 +1,69 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Set; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 4: Camp Cleanup --- + * https://adventofcode.com/2022/day/4 + */ public class Day04 { - public static void main(String[] args) throws IOException { - try (var spliterator = new LineSpliterator( Day04.class.getResourceAsStream( "/day-4-input.txt" ) ) ) { - final var rawLines = StreamSupport.stream(spliterator, false) - .collect(Collectors.toUnmodifiableList()); - String current = ""; - // collect all the text blocks into entries (separated by empty lines) - final var entries = new ArrayList(); - for (final var line : rawLines) { - if (line.isBlank()) { - if (!current.isBlank()) { - entries.add(current); - current = ""; - } - } else { - current += " " + line; - } - } - if (!current.isBlank()) { - entries.add(current); - } - int numValid = 0; - for (final var entry : entries) { - final var pairs = entry.split("\\s"); - final var map = new HashMap(); - for (final var pair : pairs) { - if (pair.isBlank()) { - continue; - } - final var components = pair.split(":", 2); - map.put(components[0].trim(), components[1].trim()); - } - final var birthYearString = map.get("byr"); - final var issueYearString = map.get("iyr"); - final var expirationYearString = map.get("eyr"); - final var heightString = map.get("hgt"); - final var hairColour = map.get("hcl"); - final var eyeColour = map.get("ecl"); - final var validEyeColours = Set.of("amb", "blu", "brn", "gry", "grn", "hzl", "oth"); - final var passportId = map.get("pid"); - if (birthYearString == null) continue; - final int birthYear = Integer.parseInt(birthYearString); - if (birthYear < 1920 || birthYear > 2002) continue; - if (issueYearString == null) continue; - final int issueYear = Integer.parseInt(issueYearString); - if (issueYear < 2010 || issueYear > 2020) continue; - if (expirationYearString == null) continue; - final int expirationYear = Integer.parseInt(expirationYearString); - if (expirationYear < 2020 || expirationYear > 2030) continue; - if (heightString == null) continue; - if (heightString.endsWith("cm")) { - final int centimetres = Integer.parseInt(heightString.replace("cm", "")); - if (centimetres < 150 || centimetres > 193) continue; - } else if (heightString.endsWith("in")) { - final int inches = Integer.parseInt(heightString.replace("in", "")); - if (inches < 59 || inches > 76) continue; - } else { - continue; - } - if (hairColour == null) continue; - if (!hairColour.matches("#[0-9a-f]{6}")) continue; - if (eyeColour == null) continue; - if (!validEyeColours.contains(eyeColour)) continue; - if (passportId == null) continue; - if (!passportId.matches("[0-9]{9}")) continue; - numValid++; - } - System.out.println(numValid); + /** + * 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). + */ + public record Elf(int sectionMin, int sectionMax) { + + public boolean fullyContains(final Elf other) { + return sectionMin() <= other.sectionMin() && sectionMax() >= other.sectionMax(); + } + + public static Elf parse(final String string) { + final var components = string.split("-"); + return new Elf(Integer.parseInt(components[0]), Integer.parseInt(components[1])); + } + } + + /** + * 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 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() { + return StreamSupport + .stream(new LineSpliterator("day-04.txt"), + false) + .map(Pair::parse); + } + + @Test + public final void part1() { + final var result = getInput().filter(Pair::oneFullyContainsTheOther).count(); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + 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 dcb1d6a..97c43c4 100644 --- a/src/test/java/com/macasaet/Day05.java +++ b/src/test/java/com/macasaet/Day05.java @@ -1,55 +1,138 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import org.junit.jupiter.api.Test; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 5: Supply Stacks --- + * https://adventofcode.com/2022/day/5 + */ public class Day05 { - public static void main(String[] args) throws IOException { - try (var spliterator = new LineSpliterator( Day05.class.getResourceAsStream( "/day-5-input.txt" ) ) ) { - final var seatIds = StreamSupport.stream(spliterator, false) - .mapToInt(id -> { - int maxRowExclusive = 128; - int row = 0; - for( int i = 0; i < 7; i++ ) { - final char partition = id.charAt(i); - if( partition == 'B' ) { - row = ( ( maxRowExclusive - row ) / 2 ) + row; - } else if( partition == 'F' ) { - maxRowExclusive = maxRowExclusive - ( ( maxRowExclusive - row ) / 2 ); - } else { - throw new IllegalArgumentException("Invalid row partition: " + partition); - } + 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); + } + 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 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 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() { + return StreamSupport + .stream(new LineSpliterator("day-05.txt"), + false); + } + + @Test + public final void part1() { + 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; } - int column = 0; - int maxColumnExclusive = 8; - for( int i = 7; i < 10; i++ ) { - final char half = id.charAt(i); - if( half == 'R' ) { - column = ( ( maxColumnExclusive - column ) / 2 ) + column; - } else if( half == 'L' ) { - maxColumnExclusive = maxColumnExclusive - ( ( maxColumnExclusive - column ) / 2 ); - } else { - throw new IllegalArgumentException("Invalid column partition: " + half); - } + } + } else { + if(line.isBlank()) { + continue; + } + final var instruction = CrateMover9000Instruction.parse(line); + instruction.execute(columns); + } + } + 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() { + 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; } - final int seatId = row * 8 + column; - return seatId; - }) - .sorted() - .collect(ArrayList::new, List::add, List::addAll); - System.out.println("part 1: " + seatIds.get(seatIds.size() - 1)); - for( int i = seatIds.size(); --i >= 1; ) { - final int x = seatIds.get( i - 1 ); - final int y = seatIds.get( i ); - if( y - x > 1 ) { - System.out.println("part 2: " + ( x + 1 ) ); - return; } + } 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()); + } + } + final var result = builder.toString(); + + System.out.println("Part 2: " + result); } diff --git a/src/test/java/com/macasaet/Day06.java b/src/test/java/com/macasaet/Day06.java index 91a46e6..769fb1b 100644 --- a/src/test/java/com/macasaet/Day06.java +++ b/src/test/java/com/macasaet/Day06.java @@ -1,48 +1,55 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; +import org.junit.jupiter.api.Test; + import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 6: --- + * https://adventofcode.com/2022/day/6 + */ public class Day06 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day06.class.getResourceAsStream("/day-6-input.txt"))) { - final var rawLines = StreamSupport.stream(spliterator, false) - .collect(Collectors.toUnmodifiableList()); - var currentGroup = new ArrayList(); - // collect all the text blocks into entries (separated by empty lines) - final var groups = new ArrayList>(); - for (final var line : rawLines) { - if (line.isBlank()) { - if (currentGroup != null && !currentGroup.isEmpty()) { - groups.add(currentGroup); - currentGroup = new ArrayList<>(); - } - } else { - currentGroup.add(line); - } + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-06.txt"), + false); + } + + @Test + public final void part1() { + 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; + } + } + throw new IllegalStateException(); + } + + @Test + public final void part2() { + 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 (currentGroup != null && !currentGroup.isEmpty()) { - groups.add(currentGroup); + if(set.size() >= 14) { + final var result = i; + System.out.println("Part 2: " + result); + return; } - final int sum = groups.stream().mapToInt(group -> { - final var uniqueCharacters = group.stream() - .flatMapToInt(String::chars) - .collect(HashSet::new, Set::add, Set::addAll); - return (int) uniqueCharacters.stream() - .filter(c -> (int) group.stream() - .map(String::chars) - .filter(chars -> chars.anyMatch(answer -> answer == c)) - .count() == group.size()) - .count(); - }).sum(); - System.out.println(sum); } + 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 00c3dc8..5aee1bc 100644 --- a/src/test/java/com/macasaet/Day07.java +++ b/src/test/java/com/macasaet/Day07.java @@ -1,75 +1,241 @@ package com.macasaet; -import java.io.IOException; -import java.util.Arrays; +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; +/** + * --- Day 7: --- + * https://adventofcode.com/2022/day/7 + */ public class Day07 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day07.class.getResourceAsStream("/day-7-input.txt"))) { - final var bag = "shiny gold"; - final var ruleMap = StreamSupport.stream(spliterator, false) - .map(Rule::fromSentence) - .collect(HashMap::new, - (map, rule) -> map.put(rule.containerColour, rule), - Map::putAll); - final var count = ruleMap.values() - .stream() - .filter(rule -> rule.canContain(bag, ruleMap)) - .count(); - System.out.println("part 1: " + count); - System.out.println("part 2: " + (ruleMap.get(bag).countContained(ruleMap) - 1)); + 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); + } } - protected static class Rule { - final String containerColour; - final Map containedCounts; + 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); + } + } - public Rule(final String containerColour, final Map containedCounts) { - // TODO validation - this.containerColour = containerColour; - this.containedCounts = containedCounts; + static class ListContents extends Command { + void execute(final Session session) { + } + static Command parse(final String ignored) { + return new ListContents(); } + } - public int countContained(final Map ruleMap) { - return 1 + containedCounts.entrySet().stream().mapToInt(entry -> { - final var containedColour = entry.getKey(); - final int multiplier = entry.getValue(); + static class ChangeDirectory extends Command { + private final String argument; - final var subRule = ruleMap.get(containedColour); - final int base = subRule.countContained(ruleMap); - return base * multiplier; - }).sum(); + public ChangeDirectory(String argument) { + this.argument = argument; } - public boolean canContain(final String colour, final Map ruleMap) { - if (containedCounts.containsKey(colour)) { - return true; + 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; } - return containedCounts.keySet() - .stream() - .map(ruleMap::get) - .anyMatch(rule -> rule != null && rule.canContain(colour, ruleMap)); } - public static Rule fromSentence(final String sentence) { - final var components = sentence.split(" bags contain ", 2); - if ("no other bags.".equalsIgnoreCase(components[1])) { - return new Rule(components[0], Collections.emptyMap()); + static ChangeDirectory parse(final String line) { + return new ChangeDirectory(line.split(" ")[2]); + } + } + + static abstract class Output extends Line { + static Output parse(final String line) { + if(line.startsWith("dir")) { + return DirectoryListing.parse(line); } - final var containedPhrases = components[1].split(", "); - final var containedCounts = Arrays.stream(containedPhrases) - .map(phrase -> phrase.replaceFirst(" bag.*$", "")) - .map(phrase -> phrase.split(" ", 2)) - .collect(HashMap::new, - (map, phraseComponents) -> map.put(phraseComponents[1], Integer.parseInt(phraseComponents[0])), - Map::putAll); - return new Rule(components[0], Collections.unmodifiableMap(containedCounts)); + return LeafListing.parse(line); } } + + 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() { + 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() { + 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); + } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day08.java b/src/test/java/com/macasaet/Day08.java index 290c2c2..98c498e 100644 --- a/src/test/java/com/macasaet/Day08.java +++ b/src/test/java/com/macasaet/Day08.java @@ -1,96 +1,161 @@ package com.macasaet; -import java.io.IOException; -import java.util.HashSet; +import org.junit.jupiter.api.Test; + import java.util.stream.Collectors; import java.util.stream.StreamSupport; +/** + * --- Day 8: Treetop Tree House --- + * https://adventofcode.com/2022/day/8 + */ public class Day08 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day08.class.getResourceAsStream("/day-8-input.txt"))) { - final var instructions = - StreamSupport.stream(spliterator, false) - .map(Instruction::fromLine) - .collect(Collectors.toUnmodifiableList()); - - instructions: - // "exactly one instruction is corrupted" - try them all - for (int i = instructions.size(); --i >= 0; ) { - final var toReplace = instructions.get(i); - - if (toReplace.operation == Operation.acc) { - // "No acc instructions were harmed in the corruption of this boot code." - continue; - } - final var opposite = toReplace.operation == Operation.nop ? Operation.jmp : Operation.nop; - final var replacement = new Instruction(opposite, toReplace.argument); - - int total = 0; - int index = 0; - - final var visited = new HashSet(); - // "The program is supposed to terminate by attempting to execute an instruction immediately after the last instruction in the file." - while (index < instructions.size()) { - final var instruction = index == i ? replacement : instructions.get(index); - if (visited.contains(index)) { - // replacing the instruction causes an infinite loop - // try replacing a different one - // NB: Simply re-visiting this instruction is an infinite loop because the argument to each instruction is constant and never dependent on the value of "total" - continue instructions; + 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++; } - visited.add(index); - total = instruction.updateTotal(total); - index = instruction.updateIndex(index); } - System.out.println("part 2: " + total); - return; // "exactly one instruction is corrupted" } + return result; } - } - public enum Operation { - acc { - public int updateTotal(final int previousTotal, final int argument) { - return previousTotal + argument; + 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; + } } - }, - nop, - jmp { - public int updateIndex(final int previousIndex, final int argument) { - return previousIndex + argument; + int southScore = 0; + for(int i = x + 1; i < grid().length; i++) { + final var height = grid()[i][y]; + southScore += 1; + if(height >= treeHeight) { + break; + } } - }; - - public int updateIndex(final int previousIndex, final int argument) { - return previousIndex + 1; + int westScore = 0; + for(int j = y; --j >= 0; ) { + final var height = grid()[x][j]; + westScore += 1; + if(height >= treeHeight) { + break; + } + } + 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 northScore * eastScore * southScore * westScore; } - public int updateTotal(final int previousTotal, final int argument) { - return previousTotal; + 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; + } + final int treeHeight = grid()[x][y]; + if (!isObstructedFromTheNorth(x, y, treeHeight)) { + return true; + } + if (!isObstructedFromTheSouth(x, y, treeHeight)) { + return true; + } + if (!isObstructedFromTheWest(x, y, treeHeight)) { + return true; + } + if (!isObstructedFromTheEast(x, y, treeHeight)) { + return true; + } + return false; } - } - public static class Instruction { - private final Operation operation; - private final int argument; + 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; + } - public Instruction(final Operation operation, final int argument) { - this.operation = operation; - this.argument = argument; + 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 Instruction fromLine(final String line) { - final var components = line.split(" ", 2); - return new Instruction(Operation.valueOf(components[0]), Integer.parseInt(components[1])); + 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; } - public int updateIndex(final int previousIndex) { - return operation.updateIndex(previousIndex, argument); + private boolean isObstructedFromTheNorth(int x, int y, int treeHeight) { + for(int i = x; --i >= 0; ) { + if(grid()[i][y] >= treeHeight) { + return true; + } + } + return false; } + } - public int updateTotal(final int previousTotal) { - return operation.updateTotal(previousTotal, argument); + 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 forest = getInput(); + final var result = forest.countVisible(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + 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); } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day09.java b/src/test/java/com/macasaet/Day09.java index ac86ab7..b6bcb00 100644 --- a/src/test/java/com/macasaet/Day09.java +++ b/src/test/java/com/macasaet/Day09.java @@ -1,57 +1,169 @@ package com.macasaet; -import java.io.IOException; -import java.util.stream.Collectors; +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; +/** + * --- Day 9: Rope Bridge --- + * https://adventofcode.com/2022/day/9 + */ public class Day09 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day09.class.getResourceAsStream("/day-9-input.txt"))) { - final int preambleSize = 25; - final var list = StreamSupport.stream(spliterator, false) - .map(Long::parseLong) - .collect(Collectors.toUnmodifiableList()); - long invalid = Long.MIN_VALUE; - outer: for (int i = preambleSize; i < list.size(); i++) { - final var current = list.get(i); - final var valid = list - .subList(i - preambleSize, i) - .stream() - .filter(l -> l <= current) // no negative numbers in the input, so filter out anything larger than our target - .collect(Collectors.toUnmodifiableList()); - - for (int j = valid.size(); --j >= 1; ) { - final var x = valid.get(j); - for (int k = j; --k >= 0; ) { - final var y = valid.get(k); - if (x + y == current) { - continue outer; - } - } - } - invalid = current; - System.out.println("Part 1: " + invalid); - break; - } - outer: for (int i = list.size(); --i >= 1;) { - var total = list.get(i); - var min = total; - var max = total; - for (int j = i; --j >= 0;) { - final var current = list.get(j); - if( current < min ) min = current; - if( current > max ) max = current; - total += current; - if (total == invalid) { - final var result = min + max; - System.out.println("Part 2: " + result); - return; - } else if (total > invalid) { // I can only do this because there are no negative numbers - continue outer; - } + record Coordinate(int x, int y) { + + 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 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); + } + } + + public static class Rope { + + Coordinate[] knotCoordinates; + + final SortedMap> visited = new TreeMap<>(); + + 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 countVisited() { + int result = 0; + for( final var map : visited.values() ) { + result += map.size(); + } + return result; + } + + 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); } } } + + protected void moveKnot(int knotIndex) { + if(knotIndex <= 0) { + throw new IllegalArgumentException("Cannot move head"); + } + final var leader = knotCoordinates[knotIndex - 1]; + var follower = knotCoordinates[knotIndex]; + + if(leader.equals(follower)) { + return; + } else if (leader.distance(follower) <= 1) { + return; + } + + follower = follower.stepTowards(leader); + knotCoordinates[knotIndex] = follower; + + if(knotIndex == knotCoordinates.length - 1) { + visited.computeIfAbsent(follower.x(), (key) -> new TreeSet<>()).add(follower.y()); + } + } + } + + enum Direction { + Up { + int xStep() { + return -1; + } + int yStep() { + return 0; + } + }, + Down { + int xStep() { + return 1; + } + 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 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 rope = new Rope(10); + getInput().forEach(rope::process); + final var result = rope.countVisited(); + System.out.println("Part 2: " + result); + } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day10.java b/src/test/java/com/macasaet/Day10.java index 66a5189..a289ed3 100644 --- a/src/test/java/com/macasaet/Day10.java +++ b/src/test/java/com/macasaet/Day10.java @@ -1,80 +1,153 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 10: Cathode-Ray Tube --- + * https://adventofcode.com/2022/day/10 + */ public class Day10 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day10.class.getResourceAsStream("/day-10-input.txt"))) { - final var adapterJoltages = StreamSupport.stream(spliterator, false) - .map(Integer::parseInt) - .sorted() // ensure the next adapter in the chain is always to the right - .distinct() - .collect(Collectors.toUnmodifiableList()); - final var targetJoltage = adapterJoltages.get(adapterJoltages.size() - 1) + 3; - - int current = 0; - var distribution = new int[]{0, 0, 1}; // the last chain link has a difference of 3 - while (current < targetJoltage - 3) { - final var _current = current; - final var next = adapterJoltages - .stream() - .filter(candidate -> candidate - _current >= 1 && candidate - _current <= 3) - .findFirst() - .get(); - final var difference = next - current; - distribution[difference - 1]++; - current = next; + public enum Instruction { + noop { + public int cycles() { + return 0; + } + }, + addx { + public int cycles() { + return 2; } - System.out.println("Part 1: " + (distribution[0] * distribution[2])); - System.out.println("Part 2: " + count(adapterJoltages, 0, targetJoltage)); + }; + + public abstract int cycles(); + + public static Instruction parse(final String string) { + return Instruction.valueOf(string); } } - private static final Map cache = new HashMap<>(); + 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)); + }; + } - protected static int hash(final Collection adapters, final int from, final int to) { - int retval = 0; - retval = 31 * retval + from; - retval = 31 * retval + to; - retval = 31 * retval + adapters.hashCode(); - return retval; + 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); + } } - protected static long count(final List adapters, final int from, final int to) { - final var hash = hash(adapters, from, to); - if (cache.containsKey(hash)) { - return cache.get(hash); + public record CycleSnapshot(int cycle, int register) { + public int signalStrength() { + return cycle() * register(); + } + } + + public static class State { + private int register = 1; + private int cycle = 1; + + public List getActivePixels() { + return Arrays.asList(register - 1, register, register + 1); } - if (adapters.isEmpty()) { - return from + 3 == to ? 1 : 0; - } else if (adapters.size() == 1) { - final int single = adapters.get(0); - return single == to - 3 ? 1 : 0; + 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][]; - long retval = 0; - for (int i = 3; --i >= 0; ) { - if (i >= adapters.size()) continue; - final int first = adapters.get(i); - final int difference = first - from; - if (difference >= 1 && difference <= 3) { - final var remaining = - i < adapters.size() - ? adapters.subList(i + 1, adapters.size()) - : new ArrayList(); - retval += count(remaining, first, to); + { + 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] = '.'; } } - cache.put(hash, retval); - return retval; + 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(); + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-10.txt"), + false) + .map(Operation::parse); } + + @Test + public final void part1() { + 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 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 a74e3e9..45db617 100644 --- a/src/test/java/com/macasaet/Day11.java +++ b/src/test/java/com/macasaet/Day11.java @@ -1,277 +1,189 @@ package com.macasaet; -import java.io.IOException; +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.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; +/** + * --- Day 11: Monkey in the Middle --- + * https://adventofcode.com/2022/day/11 + */ public class Day11 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day11.class.getResourceAsStream("/day-11-input.txt"))) { - final var list = StreamSupport.stream(spliterator, false).map(String::toCharArray).collect(Collectors.toList()); - final var original = list.toArray(new char[list.size()][]); - var current = copy(original); - while (true) { - final var transformed = transform(current); - if (areEqual(current, transformed)) { - break; - } - current = transformed; + public enum Operator implements BiFunction { + ADD { + public BigInteger apply(BigInteger x, BigInteger y) { + return x.add(y); } - System.out.println( "Part 1: " + countOccupiedSeats(current)); - current = copy(original); - while( true ) { - final var transformed = transform2(current); - if (areEqual(current, transformed)) { - break; - } - current = transformed; + }, + MULTIPLY { + public BigInteger apply(BigInteger x, BigInteger y) { + return x.multiply(y); } - System.out.println( "Part 2: " + countOccupiedSeats(current)); - } - } + }; - protected static char[][] copy(final char[][] original) { - final char[][] retval = new char[ original.length ][]; - for( int i = original.length; --i >= 0; ) { - final var row = original[ i ]; - final var copiedRow = new char[ row.length ]; - System.arraycopy(row, 0, copiedRow, 0, row.length); - retval[ i ] = copiedRow; + public static Operator parse(final String string) { + return switch(string) { + case "*" -> MULTIPLY; + case "+" -> ADD; + default -> throw new IllegalArgumentException("Invalid operator: " + string); + }; } - return retval; } - protected static char[][] transform2(final char[][] source) { - final char[][] retval = new char[source.length][]; - for (int i = retval.length; --i >= 0; retval[i] = new char[source[0].length]) ; - rows: for (int i = source.length; --i >= 0; ) { - final var row = source[i]; - columns: for (int j = row.length; --j >= 0; ) { - final var original = source[ i ][ j ]; - retval[ i ][ j ] = original; - if( original == '.' ) { - continue; - } else if( original == 'L' ) { - // North - for( int x = i; --x >= 0; ) { - final var visibleSeat = source[ x ][ j ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // North-West - for( int x = i, y = j; --x >= 0 && -- y >= 0; ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // West - for( int y = j; --y >= 0; ) { - final var visibleSeat = source[ i ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // South-West - for( int x = i + 1, y = j - 1; x < source.length && y >= 0; x++, y-- ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // South - for( int x = i + 1; x < source.length; x++ ) { - final var visibleSeat = source[ x ][ j ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // South-East - for( int x = i + 1, y = j + 1; x < source.length && y < row.length; x++, y++ ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // East - for( int y = j + 1; y < row.length; y++ ) { - final var visibleSeat = source[ i ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // North-East - for( int x = i - 1, y = j + 1; x >= 0 && y < row.length; x--, y++ ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - retval[ i ][ j ] = '#'; - } else if( original == '#' ) { - int visibleNeighbours = 0; - // North - for( int x = i; --x >= 0 && visibleNeighbours < 5; ) { - final var visibleSeat = source[x][j]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // North-West - for( int x = i, y = j; --x >= 0 && -- y >= 0 && visibleNeighbours < 5; ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // West - for( int y = j; --y >= 0 && visibleNeighbours < 5; ) { - final var visibleSeat = source[i][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // South-West - for( int x = i + 1, y = j - 1; x < source.length && y >= 0 && visibleNeighbours < 5; x++, y-- ) { - final var visibleSeat = source[x][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // South - for( int x = i + 1; x < source.length && visibleNeighbours < 5; x++ ) { - final var visibleSeat = source[x][j]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // South-East - for( int x = i + 1, y = j + 1; x < source.length && y < row.length && visibleNeighbours < 5; x++, y++ ) { - final var visibleSeat = source[x][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // East - for( int y = j + 1; y < row.length && visibleNeighbours < 5; y++ ) { - final var visibleSeat = source[i][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // North-East - for( int x = i - 1, y = j + 1; x >= 0 && y < row.length && visibleNeighbours < 5; x--, y++ ) { - final var visibleSeat = source[x][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - if( visibleNeighbours >= 5 ) { - retval[ i ][ j ] = 'L'; - } - } else { - throw new IllegalArgumentException("Unsupported char: " + original); - } + 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); } - return retval; - } - protected static int countOccupiedSeats(final char[][] matrix) { - int retval = 0; - for( int i = matrix.length; --i >= 0; ) { - final var row = matrix[ i ]; - for( int j = row.length; --j >= 0; ) { - if( matrix[ i ][ j ] == '#' ) retval++; + 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 retval; } - protected static void print(final char[][] matrix) { - for (int i = 0; i < matrix.length; i++) { - final var row = matrix[i]; - System.err.println(new String(row)); + /** + * 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 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)); } - } - protected static char[][] transform(final char[][] source) { - final char[][] retval = new char[source.length][]; - for (int i = retval.length; --i >= 0; retval[i] = new char[source[0].length]) ; - for (int i = source.length; --i >= 0; ) { - final var row = source[i]; - for (int j = row.length; --j >= 0; ) { - process(i, j, source, retval); - } + public BigInteger countItemsInspected() { + return itemsInspected.get(); } - return retval; - } - protected static boolean areEqual(final char[][] x, final char[][] y) { - for (int i = x.length; --i >= 0; ) { - final var row = x[i]; - for (int j = row.length; --j >= 0; ) { - if (x[i][j] != y[i][j]) { - return false; - } + public Throw inspectItem(BigInteger reliefFactor) { + // this assumes monkeys can throw items to themselves + if(items.isEmpty()) { + return null; } + 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); } - return true; - } - protected static void process(final int x, final int y, final char[][] source, final char[][] target) { - final var original = source[x][y]; - if (original == '.') { - target[x][y] = '.'; - return; - } else if (original == 'L') { - target[x][y] = !isOccupied(x - 1, y - 1, source) && !isOccupied(x - 1, y, source) - && !isOccupied(x - 1, y + 1, source) && !isOccupied(x, y - 1, source) && !isOccupied(x, y + 1, source) - && !isOccupied(x + 1, y - 1, source) && !isOccupied(x + 1, y, source) && !isOccupied(x + 1, y + 1, source) ? '#' : original; - } else if (original == '#') { - int occupiedNeighbors = 0; - occupiedNeighbors += isOccupied(x - 1, y - 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x - 1, y, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x - 1, y + 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x, y - 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x, y + 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x + 1, y - 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x + 1, y, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x + 1, y + 1, source) ? 1 : 0; - target[x][y] = occupiedNeighbors >= 4 ? 'L' : original; - } else { - throw new IllegalArgumentException("Unsupported char: " + original); + 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; } + + } + + public record Throw(int target, BigInteger itemWorryLevel) { + } + + 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(); } - protected static boolean isOccupied(final int x, final int y, final char[][] matrix) { - if (x < 0 || y < 0 || x >= matrix.length) { - return false; + @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()); + } + } } - final char[] row = matrix[x]; - if (y >= row.length) { - return false; + 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 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()); + } + } } - return row[y] == '#'; + 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 c494842..3257259 100644 --- a/src/test/java/com/macasaet/Day12.java +++ b/src/test/java/com/macasaet/Day12.java @@ -1,200 +1,164 @@ package com.macasaet; -import java.io.IOException; -import java.util.stream.Collectors; +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: Hill Climbing Algorithm --- + * https://adventofcode.com/2022/day/12 + */ public class Day12 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day12.class.getResourceAsStream("/day-12-input.txt"))) { - // 0, 360 % 360 = North - // 90 = East - // 180 = South - // 270 = West - int direction = 90; - int x = 0; // positive means East, negative means West - int y = 0; // positive means North, negative means South - - - final var actions = StreamSupport.stream(spliterator, false) - .map(Action::fromLine) - .collect(Collectors.toUnmodifiableList()); - for (final var action : actions) { - switch (action.actionCode) { - case 'N': - y += action.value; - break; - case 'S': - y -= action.value; - break; - case 'E': - x += action.value; - break; - case 'W': - x -= action.value; - break; - case 'L': - final int relativeDirection = direction - action.value; - direction = relativeDirection < 0 ? relativeDirection + 360 : relativeDirection; - break; - case 'R': - direction = (direction + action.value) % 360; - break; - case 'F': - switch (direction) { - case 0: - y += action.value; - break; - case 90: - x += action.value; - break; - case 180: - y -= action.value; - break; - case 270: - x -= action.value; - break; - default: - throw new IllegalStateException("Unhandled direction: " + direction); + record Coordinate(int x, int y) { + } + + public record HeightMap(int[][] grid, Coordinate start, Coordinate end) { + + public int lengthOfShortestPath() { + return this.lengthOfShortestPath(this.start); + } + + 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); + } } - break; - default: - throw new IllegalArgumentException("Invalid action: " + action.actionCode); + } } + return Integer.MAX_VALUE; } - System.out.println("Part 1: " + (Math.abs(x) + Math.abs(y))); - - // waypoint coordinates - int wx = 10; // positive means North, negative means South - int wy = 1; // positive means East, negative means West - - // ship coordinates - int sx = 0; - int sy = 0; - - for (final var action : actions) { - int xDiff = wx - sx; - int yDiff = wy - sy; - switch (action.actionCode) { - case 'N': - wy += action.value; - break; - case 'S': - wy -= action.value; - break; - case 'E': - wx += action.value; - break; - case 'W': - wx -= action.value; - break; - case 'L': - for (int i = action.value / 90; --i >= 0; ) { - wx = sx - yDiff; - wy = sy + xDiff; - xDiff = wx - sx; - yDiff = wy - sy; - } - break; - case 'R': - for (int i = action.value / 90; --i >= 0; ) { - wx = sx + yDiff; - wy = sy - xDiff; - xDiff = wx - sx; - yDiff = wy - sy; + + 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)); } - break; - case 'F': - sx += xDiff * action.value; - sy += yDiff * action.value; - wx = sx + xDiff; - wy = sy + yDiff; - break; - default: - throw new IllegalArgumentException("Unsupported argument: " + action.actionCode); + } } + return Collections.unmodifiableList(list); + } + + 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); } - System.out.println("Part 2: " + (Math.abs(sx) + Math.abs(sy))); - } - } - protected static class Action { - final char actionCode; - final int value; + 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)); + } - public Action(final char actionCode, final int value) { - this.actionCode = actionCode; - this.value = value; } - public static Action fromLine(final String line) { - final var direction = line.charAt(0); - final var valueString = line.substring(1); - final int value = Integer.parseInt(valueString); - return new Action(direction, value); + 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 { + grid[i][j] = c - 'a'; + } + } } + Objects.requireNonNull(origin); + Objects.requireNonNull(destination); + return new HeightMap(grid, origin, destination); + } -// public SimpleNavState navigate(final SimpleNavState current) { -// switch (actionCode) { -// case 'N': -// return new SimpleNavState(current.x, current.y + value, actionCode); -// case 'S': -// return new SimpleNavState(current.x, current.y - value, actionCode); -// case 'E': -// return new SimpleNavState(current.x + value, current.y, actionCode); -// case 'W': -// return new SimpleNavState(current.x - value, current.y, actionCode); -// case 'L': -// final int relativeDirection = current.direction - value; -// final int direction = relativeDirection < 0 ? relativeDirection + 360 : relativeDirection; -// return new SimpleNavState(current.x, current.y, direction); -// case 'R': -// return new SimpleNavState(current.x, current.y, (current.direction + value) % 360); -// case 'F': -// switch (current.direction) { -// case 0: -// return new SimpleNavState(current.x, current.y + value, current.direction); -// case 90: -// return new SimpleNavState(current.x + value, current.y, current.direction); -// case 180: -// return new SimpleNavState(current.x, current.y - value, current.direction); -// case 270: -// return new SimpleNavState(current.x - value, current.y, current.direction); -// default: -// throw new IllegalStateException("Unhandled direction: " + current.direction); -// } -// default: -// throw new IllegalArgumentException("Invalid action: " + current.direction); -// } -// } + @Test + public final void part1() { + final var map = getInput(); + final var result = map.lengthOfShortestPath(); + System.out.println("Part 1: " + result); + } + @Test + public final void part2() { + 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); } -// -// protected static class SimpleNavState { -// private final int x; -// private final int y; -// private final int direction; -// -// public SimpleNavState(int x, int y, int direction) { -// this.x = x; -// this.y = y; -// this.direction = direction; -// } -// } -// -// protected static class AdvancedNavState { -// private final int sx; -// private final int sy; -// private final int wx; -// -// public AdvancedNavState(int sx, int sy, int wx, int wy) { -// this.sx = sx; -// this.sy = sy; -// this.wx = wx; -// this.wy = wy; -// } -// -// private final int wy; -// } + } \ 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 cfb7520..457145f 100644 --- a/src/test/java/com/macasaet/Day13.java +++ b/src/test/java/com/macasaet/Day13.java @@ -1,93 +1,161 @@ package com.macasaet; -import java.io.IOException; +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; +/** + * --- Day 13: Distress Signal --- + * https://adventofcode.com/2022/day/13 + */ public class Day13 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day13.class.getResourceAsStream("/day-13-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - final int earliestDeparture = Integer.parseInt(lines.get(0)); - final var idsString = lines.get(1); - final var idStrings = idsString.split(","); - final var candidate = Arrays.stream(idStrings).filter(busId -> !"x".equalsIgnoreCase(busId)).map(Integer::parseInt).map(busId -> { - if (earliestDeparture % busId == 0) { - return new Candidate(busId, 0); + 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); + } + + public boolean isInOrder() { + return x().compareTo(y()) < 0; + } + + public Stream stream() { + return Stream.of(x(), y()).sorted(); + } + } + + 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"); } - final int d = earliestDeparture / busId; - return new Candidate(busId, ((d + 1) * busId) - earliestDeparture); - }).sorted().findFirst().get(); - final int result = candidate.busId * candidate.timeToWait; - System.out.println("Part 1: " + result); - - final List list = new ArrayList(idStrings.length); - for (int i = 0; i < idStrings.length; i++) { - final var idString = idStrings[i]; - if ("x".equalsIgnoreCase(idString)) continue; - final var busId = Long.parseLong(idString); - list.add(new Bus(busId, i)); + return compareToLiteral((Literal) other); } - long sum = 0; - long productOfModSpaces = 1; - for (int i = list.size(); --i >= 0; ) { - final var bus = list.get(i); - productOfModSpaces *= bus.busId; - long remainder = bus.busId - bus.index; - remainder = remainder % bus.busId; - long productOfOtherModSpaces = 1; - for (int j = list.size(); --j >= 0; ) { - if (j == i) continue; - final var otherBus = list.get(j); - productOfOtherModSpaces *= otherBus.busId; + } + } + + 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); } - final long inverse = findInverse(productOfOtherModSpaces, bus.busId); - sum += remainder * productOfOtherModSpaces * inverse; - } - while (true) { - final long smallerSolution = sum - productOfModSpaces; - if (smallerSolution < 0) break; - sum = smallerSolution; } - System.out.println("Part 2: " + sum); + return stack.pop(); } - } - protected static long findInverse(final long partialProduct, final long modSpace) { - for (long multiplier = 1; ; multiplier++) { - if ((partialProduct * multiplier) % modSpace == 1) { - return multiplier; + 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; + } } + if(y.hasNext()) { + return -1; + } else if(x.hasNext()) { + return 1; + } + return 0; } + + public int compareToLiteral(Literal other) { + return compareToList(other.asList()); + } + } - protected static class Bus { - final long busId; - final long index; + public record Literal(int item) implements Item { + public int compareToList(ListItem other) { + return asList().compareToList(other); + } - public Bus(final long busId, final long index) { - this.busId = busId; - this.index = index; + public int compareToLiteral(Literal other) { + return Integer.compare(item(), other.item()); } + public ListItem asList() { + return new ListItem(Collections.singletonList(this)); + } } - protected static class Candidate implements Comparable { - final int busId; - final int timeToWait; + 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(); + } - public Candidate(final int busId, final int timeToWait) { - this.busId = busId; - this.timeToWait = timeToWait; + @Test + public final void part1() { + 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); + } - public int compareTo(Candidate other) { - return Integer.compare(timeToWait, other.timeToWait); - } + @Test + public final void part2() { + 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 a86ae7f..7f7d777 100644 --- a/src/test/java/com/macasaet/Day14.java +++ b/src/test/java/com/macasaet/Day14.java @@ -1,127 +1,212 @@ package com.macasaet; -import java.io.IOException; -import java.math.BigInteger; -import java.util.ArrayList; +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.stream.Collectors; +import java.util.Map; import java.util.stream.StreamSupport; +/** + * --- Day 14: Regolith Reservoir --- + * https://adventofcode.com/2022/day/14 + */ public class Day14 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day14.class.getResourceAsStream("/day-14-input.txt"))) { - final var memory = new HashMap(); - char[] mask = null; - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - for (final var line : lines) { - final var components = line.split(" = "); - final var stringValue = components[1].strip(); - if ("mask".equalsIgnoreCase(components[0].strip())) { - mask = stringValue.strip().toCharArray(); - } else { - final var address = new BigInteger(components[0].replaceAll("[^0-9]", "")); - var binaryString = Integer.toBinaryString(Integer.parseInt(stringValue)); - final int padSize = 36 - binaryString.length(); - final var pad = new char[padSize]; - for (int i = padSize; --i >= 0; pad[i] = '0') ; - binaryString = new String(pad) + binaryString; - - final var valueBinary = new char[36]; - for (int j = 36; --j >= 0; ) { - if (mask[j] == 'X') { - valueBinary[j] = binaryString.charAt(j); - } else { - valueBinary[j] = mask[j]; + public enum Cell { + ROCK, + SAND + } + + 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); + } + } + + 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; } - - final var result = toInt(valueBinary); - memory.put(address, result); } } - var sum = memory.values().stream().reduce(BigInteger::add).get(); - System.out.println("Part 1: " + sum); - - memory.clear(); - mask = null; - for (final var line : lines) { - final var components = line.split(" = "); - final var stringValue = components[1].strip(); - if ("mask".equalsIgnoreCase(components[0].strip())) { - mask = stringValue.toCharArray(); - } else { - final var addressDecimal = Integer.parseInt(components[0].strip().replaceAll("[^0-9]", "")); - var addressBinaryString = Integer.toBinaryString(addressDecimal); - final int padSize = 36 - addressBinaryString.length(); - final var addressSpec = new char[36]; - for (int i = 0; i < padSize; addressSpec[i++] = '0') ; - for (int i = 0; i < addressBinaryString.length(); i++) { - addressSpec[i + padSize] = addressBinaryString.charAt(i); + } + + 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; } - for (int i = 36; --i >= 0; ) { - if (mask[i] == '1') { - addressSpec[i] = '1'; - } else if (mask[i] == 'X') { - addressSpec[i] = 'X'; + } + } + } + + int floorDepth() { + return maxDepth() + 2; + } + + 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); } } - final var value = toInt(Integer.parseInt(components[1].strip())); - for (final var address : explode(addressSpec)) { - memory.put(address, value); + if(current.verticalDepth() > maxDepth) { + maxDepth = current.verticalDepth(); } + if(current.horizontalOffset() < minHorizontalOffset) { + minHorizontalOffset = current.horizontalOffset(); + } + if(current.horizontalOffset() > maxHorizontalOffset) { + maxHorizontalOffset = current.horizontalOffset(); + } + last = current; } } - sum = memory.values().stream().reduce(BigInteger::add).get(); - System.out.println("Part 2: " + sum); + return new Cave(grid, maxDepth, minHorizontalOffset, maxHorizontalOffset); } - } - - protected static BigInteger toInt(final int decimal) { - final var string = Integer.toBinaryString(decimal); - return toInt(string.toCharArray()); - } - protected static BigInteger toInt(final char[] chars) { - var retval = BigInteger.ZERO; - for (int i = chars.length; --i >= 0; ) { - final int power = chars.length - i - 1; - final var multiplier = chars[i] == '0' ? BigInteger.ZERO : BigInteger.ONE; - retval = retval.add(BigInteger.TWO.pow(power).multiply(multiplier)); + static List parseRockPaths(final String line) { + return Arrays.stream(line.split(" -> ")).map(Coordinate::parse).toList(); } - return retval; - } - protected static List explode(final char[] chars) { - final var floatingIndices = new ArrayList(); - for (int i = 36; --i >= 0; ) { - if (chars[i] == 'X') { - floatingIndices.add(i); + @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 explode(chars, floatingIndices); } - protected static List explode(final char[] chars, final List floatingIndices) { - if (floatingIndices.isEmpty()) { - return Collections.singletonList(toInt(chars)); - } - final var index = floatingIndices.get(0); + protected Cave getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-14.txt"), false) + .toList(); + return Cave.parse(lines); + } - var list = new ArrayList(); + @Test + public final void part1() { + final var cave = getInput(); + final var result = cave.pourSandIntoAbyss(); - final var copy = Arrays.copyOf(chars, 36); - final var sub = floatingIndices.subList(1, floatingIndices.size()); + System.out.println("Part 1: " + result); + } - copy[index] = '0'; - list.addAll(explode(copy, sub)); - copy[index] = '1'; - list.addAll(explode(copy, sub)); + @Test + public final void part2() { + final var cave = getInput(); + final var result = cave.fillAperture(); - return Collections.unmodifiableList(list); + 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 3de8788..2fea62c 100644 --- a/src/test/java/com/macasaet/Day15.java +++ b/src/test/java/com/macasaet/Day15.java @@ -1,49 +1,205 @@ package com.macasaet; -import java.io.IOException; +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.LinkedList; import java.util.List; +import java.util.Map; +import java.util.function.IntPredicate; +import java.util.stream.StreamSupport; +/** + * --- Day 15: Beacon Exclusion Zone --- + * https://adventofcode.com/2022/day/15 + */ public class Day15 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day15.class.getResourceAsStream("/day-15-test.txt"))) { -// final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); -// final var line = lines.get(0); -// final var numbers = Arrays.stream(line.split(",")).map(String::strip).map(Integer::parseInt).collect(Collectors.toUnmodifiableList()); -// final var numbers = new int[]{ 0, 3, 6 }; - final var numbers = new int[]{ 13,16,0,12,15,1 }; - - int last = -1; - final var oralHistory = new HashMap>(); - for( int i = 0; i < numbers.length; i++ ) { - final int number = numbers[ i ]; - oralHistory.computeIfAbsent(number, k -> new LinkedList<>()).add( i ); - last = number; - System.err.println( "Speak: " + number ); + 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=", ""))); + } + + 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 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); + } + + 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); + } + } + + 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); + } + } + + 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); + + // NW + upperRow.putIfAbsent(y - j, Item.COVERED); + } + } + } + } + + int distanceToBeacon() { + return location().distanceTo(beaconLocation()); + } + } + + 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++; + } + } + return result; + } + + 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); } - for( int i = numbers.length; i < 30_000_000; i++ ) { - final var history = oralHistory.computeIfAbsent(last, k -> new LinkedList<>()); - if( history.isEmpty() ) { - throw new IllegalStateException("No history for: " + last ); - } else if( history.size() == 1 ) { // spoken only once before - final int numberToSpeak = 0; - System.err.println( "Turn " + ( i + 1 ) + ": Speak: " + numberToSpeak ); - oralHistory.getOrDefault( numberToSpeak, new LinkedList<>() ).add( i ); - last = numberToSpeak; - } else { // spoken 2+ times - final int lastMention = history.get(history.size() - 1); - final int penultimateMention = history.get(history.size() - 2); - final int numberToSpeak = lastMention - penultimateMention; - System.err.println( "Turn " + ( i + 1 ) + ": Speak: " + numberToSpeak ); - oralHistory.computeIfAbsent(numberToSpeak, k -> new LinkedList<>()).add( i ); - last = numberToSpeak; + return new CaveMap(grid, minX, maxX, minY, maxY); + } + + 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 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 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 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; + } } } - System.out.println( "Part 1: " + last ); } + 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 01bd853..0000000 --- a/src/test/java/com/macasaet/Day16.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.StreamSupport; - -public class Day16 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day16.class.getResourceAsStream("/day-16-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - - final var fields = new HashSet(); - int section = 0; - Ticket myTicket = null; - final var nearbyTickets = new ArrayList(); - for (final var line : lines) { - if (line.isBlank()) { - section++; - continue; - } else if ("your ticket:".equalsIgnoreCase(line.strip()) || "nearby tickets:".equalsIgnoreCase(line.strip())) { - continue; - } - switch (section) { - case 0 -> { - final var sections = line.split(": "); - final var label = sections[0].strip().replaceAll(":$", ""); - final var rangesString = sections[1].strip(); - final var rangeStringArray = rangesString.split(" or "); - fields.add(new Field(label, - Arrays.stream(rangeStringArray) - .map(Range::fromString) - .sorted() - .collect(Collectors.toUnmodifiableList()))); - } - case 1 -> myTicket = new Ticket(Arrays.stream(line.split(",")) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList())); - case 2 -> nearbyTickets.add(new Ticket(Arrays.stream(line.split(",")) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList()))); - } - } - - final int sum = nearbyTickets - .stream() - .flatMapToInt(ticket -> ticket - .getInvalidNumbers(fields) - .stream() - .mapToInt(Integer::intValue)) - .sum(); - System.out.println("Part 1: " + sum); - - final var validTickets = nearbyTickets - .stream() - .filter(candidate -> candidate.isValid(fields)) - .collect(Collectors.toUnmodifiableSet()); - final var unmappedIndices = IntStream - .range(0, myTicket.numbers.size()) - .collect(HashSet::new, Set::add, Set::addAll); - final var fieldTable = new HashMap(); - final var unmappedFields = new HashSet<>(fields); - while (!unmappedFields.isEmpty()) { - final var indicesToRemove = new HashSet(); - for (final int index : unmappedIndices) { - final var candidates = new HashSet<>(unmappedFields); - final var toRemove = new HashSet(); - for (final var ticket : validTickets) { - final var number = ticket.numbers.get(index); - for (final var candidate : candidates) { - if (!candidate.contains(number)) { - toRemove.add(candidate); - } - } - candidates.removeAll(toRemove); - } - if (candidates.isEmpty()) { - throw new IllegalStateException("no candidates for index: " + index); - } else if (candidates.size() == 1) { - // map candidate to index - final var field = candidates.iterator().next(); - fieldTable.put(field.label, index); - unmappedFields.remove(field); - indicesToRemove.add(index); - } - } - unmappedIndices.removeAll(indicesToRemove); - } - final var numbers = myTicket.numbers; - final long product = fieldTable - .keySet() - .stream() - .filter(candidate -> candidate.startsWith("departure")) - .map(fieldTable::get) - .map(numbers::get) - .mapToLong(Long::valueOf) - .reduce((x, y) -> x * y).getAsLong(); - System.out.println("Part 2: " + product); - } - } - - protected static class Ticket { - private final List numbers; - - public Ticket(final List numbers) { - this.numbers = numbers; - } - - public boolean isValid(final Collection fields) { - return getInvalidNumbers(fields).isEmpty(); - } - - public List getInvalidNumbers(final Collection fields) { - final List list = new ArrayList<>(); - outer: - for (final var number : numbers) { - for (final var field : fields) { - if (field.contains(number)) { - continue outer; - } - } - list.add(number); - } - return Collections.unmodifiableList(list); - } - } - - protected static class Field { - private final String label; - private final List ranges; - - public Field(final String label, final List ranges) { - this.label = label; - this.ranges = ranges; - } - - public boolean contains(final int number) { - for (final var range : ranges) { - if (range.contains(number)) { - return true; - } - } - return false; - } - - public int hashCode() { - return label.hashCode(); - } - - public boolean equals(final Object object) { - if( this == object ) { - return true; - } else if( object == null ) { - return false; - } - try { - final Field other = ( Field )object; - return label.equals(other.label); - } catch( final ClassCastException cce ) { - return false; - } - } - } - - protected static class Range implements Comparable { - - private final int minInclusive; - private final int maxInclusive; - - public Range(final int minInclusive, final int maxInclusive) { - this.minInclusive = minInclusive; - this.maxInclusive = maxInclusive; - } - - public static Range fromString(final String string) { - final var ends = string.split("-", 2); - return new Range(Integer.parseInt(ends[0].strip()), Integer.parseInt(ends[1].strip())); - } - - public int compareTo(final Range other) { - return Integer.compare(minInclusive, other.minInclusive); - } - - public boolean contains(final int candidate) { - return candidate >= minInclusive && candidate <= maxInclusive; - } - - } -} \ 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 cf0456c..0000000 --- a/src/test/java/com/macasaet/Day17.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class Day17 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day17.class.getResourceAsStream("/day-17-input.txt"))) { - final var grid = new Grid(); - - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - for( int x = 0; x < lines.size(); x++ ) { - final var line = lines.get(x); - for( int y = 0; y < line.length(); y++ ) { - final char state = line.charAt(y); - final int z = 0; - final int w = 0; - grid.setInitial(x, y, z, w, state); - } - } - - for( int i = 0; i < 6; i++ ) { - grid.cycle(); - } - System.out.println( "Part 2: " + grid.countActive() ); - } - } - - public static class Grid { - - private final SortedMap>>> map = new TreeMap<>(); - private int minX = 0, maxX = 0, minY = 0, maxY = 0, minZ = 0, maxZ = 0, minW = 0, maxW = 0; - - public void setInitial(final int x, final int y, final int z, final int w, final char state) { - final var dimX = map.computeIfAbsent(x, key -> new TreeMap<>()); - final var dimY = dimX.computeIfAbsent(y, key -> new TreeMap<>()); - final var dimZ = dimY.computeIfAbsent(z, key -> new TreeMap<>()); - dimZ.put(w, state); - minX = Math.min(minX, x); - maxX = Math.max(maxX, x); - minY = Math.min(minY, y); - maxY = Math.max(maxY, y); - minZ = Math.min(minZ, z); - maxZ = Math.max(maxZ, z); - minW = Math.min(minW, w); - maxW = Math.max(maxW, w); - } - - public Runnable defer(final int x, final int y, final int z, final int w) { - int activeNeighbours = 0; - for( final var neighbour : neighbours( x, y, z, w ) ) { - if( neighbour.getState() == '#' ) { - activeNeighbours++; - } - } - final var cell = new Cell(x, y, z, w); - if( cell.getState() == '#' ) { // active - return activeNeighbours == 2 || activeNeighbours == 3 ? () -> {} : () -> cell.setState('.'); - } else { // inactive - return activeNeighbours == 3 ? () -> cell.setState('#') : () -> {}; - } - } - - public void cycle() { - final var updateTasks = new LinkedList(); - for( int x = minX - 1; x <= maxX + 1; x++ ) { - for( int y = minY - 1; y <= maxY + 1; y++ ) { - for( int z = minZ - 1; z <= maxZ + 1; z++ ) { - for( int w = minW - 1; w <= maxW + 1; w++ ) { - updateTasks.add(defer(x, y, z, w)); - } - } - } - } - updateTasks.forEach(Runnable::run); - } - - public int countActive() { - return map.values() - .stream() - .flatMapToInt(yDim -> yDim.values() - .stream() - .flatMapToInt(zDim -> zDim.values() - .stream() - .flatMapToInt(wDim -> wDim.values() - .stream() - .mapToInt(state -> state == '#' ? 1 : 0 )))) - .sum(); - } - - protected Collection neighbours(final int x, final int y, final int z, int w) { - final var list = new ArrayList(80); - for( int i = x - 1; i <= x + 1; i++ ) { - for( int j = y - 1; j <= y + 1; j++ ) { - for( int k = z - 1; k <= z + 1; k++ ) { - for( int l = w - 1; l <= w + 1; l++ ) { - if (i == x && j == y && k == z && l == w) continue; - list.add(new Cell(i, j, k, l)); - } - } - } - } - if( list.size() != 80 ) { - throw new IllegalStateException("There should be 80 neighbours :-("); - } - return Collections.unmodifiableList(list); - } - - protected class Cell { - final int x, y, z, w; - - public Cell(final int x, final int y, final int z, int w) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - public char getState() { - final var dimensionX = map.getOrDefault(x, Collections.emptySortedMap()); - final var dimensionY = dimensionX.getOrDefault(y, Collections.emptySortedMap()); - final var dimensionZ = dimensionY.getOrDefault(z, Collections.emptySortedMap()); - return dimensionZ.getOrDefault(w, '.'); - } - - public void setState(final char state) { - final var dimensionX = map.computeIfAbsent(x, key -> new TreeMap<>()); - final var dimensionY = dimensionX.computeIfAbsent(y, key -> new TreeMap<>()); - final var dimensionZ = dimensionY.computeIfAbsent(z, key -> new TreeMap<>()); - dimensionZ.put(w, state); - minX = Math.min(minX, x); - maxX = Math.max(maxX, x); - minY = Math.min(minY, y); - maxY = Math.max(maxY, y); - minZ = Math.min(minZ, z); - maxZ = Math.max(maxZ, z); - minW = Math.min(minW, w); - maxW = Math.max(maxW, w); - } - } - } -} \ 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 61b25aa..3c29e60 100644 --- a/src/test/java/com/macasaet/Day18.java +++ b/src/test/java/com/macasaet/Day18.java @@ -1,91 +1,175 @@ package com.macasaet; -import java.io.IOException; -import java.math.BigInteger; -import java.util.LinkedList; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.stream.StreamSupport; +/** + * --- Day 18: Boiling Boulders --- + * https://adventofcode.com/2022/day/18 + */ public class Day18 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day18.class.getResourceAsStream("/day-18-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - BigInteger sum = BigInteger.ZERO; - for (final var line : lines) { - final var outputQueue = new LinkedList(); - final var operatorStack = new LinkedList(); - - String numberString = ""; - for (final char c : line.toCharArray()) { - if (Character.isDigit(c)) { - numberString += c; - } else { - if (!"".equals(numberString)) { - outputQueue.add(numberString); - numberString = ""; - } - switch (c) { - case '+', '*' -> { - Character topOperator = operatorStack.peek(); - while (topOperator != null && topOperator > c /*&& topOperator != '('*/) { - outputQueue.add("" + operatorStack.pop()); - topOperator = operatorStack.peek(); - } - operatorStack.push(c); - } - case '(' -> operatorStack.push(c); - case ')' -> { - Character topOperator = operatorStack.peek(); - while (topOperator != null && topOperator != '(') { - outputQueue.add("" + operatorStack.pop()); - topOperator = operatorStack.peek(); - } - if (topOperator == null /*|| topOperator != '('*/) { - throw new IllegalStateException("mis-matched parentheses :-("); - } - operatorStack.pop(); - } - case ' ' -> { - } - default -> throw new IllegalStateException("Unexpected value: " + c); - } - } + public static final int SCAN_DIMENSION = 32; + protected static Droplet getInput() { + final var cubeCoordinates = StreamSupport.stream(new LineSpliterator("day-18.txt"), false) + .map(Cube::parse) + .toList(); + return new Droplet(cubeCoordinates); + } + + @Test + public final void part1() { + final var droplet = getInput(); + final var result = droplet.surfaceArea(CubeType.Air); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var droplet = getInput(); + droplet.immerse(); + final var result = droplet.surfaceArea(CubeType.Water); + + System.out.println("Part 2: " + result); + } + + public enum CubeType { + Air, + Lava, + Water, + } + + 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])); + } + + public CubeType getType(final CubeType[][][] grid) { + return grid[x()][y()][z()]; + } + + public void setType(final CubeType[][][] grid, final CubeType type) { + grid[x()][y()][z()] = type; + } + } + + public static class Droplet { + + 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); } - if (!numberString.isBlank()) { - outputQueue.add(numberString); - } - while (!operatorStack.isEmpty()) { - outputQueue.add("" + operatorStack.pop()); - } - final var stack = new LinkedList(); - for (final var item : outputQueue) { - try { - stack.push(new BigInteger(item)); - } catch (final NumberFormatException nfe) { - switch (item) { - case "+" -> { - final var rValue = stack.pop(); - final var lValue = stack.pop(); - final var result = lValue.add(rValue); - stack.push(result); - } - case "*" -> { - final var rValue = stack.pop(); - final var lValue = stack.pop(); - final var result = lValue.multiply(rValue); - stack.push(result); - } - } + } + for (final var cube : cubes) { + cube.setType(grid, CubeType.Lava); + } + this.grid = grid; + this.cubes = cubes; + } + + 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); } } - if (stack.size() != 1) { - throw new IllegalStateException("Invalid stack: " + stack); - } - sum = sum.add(stack.get(0)); + cube.setType(grid, CubeType.Water); + } + } + + 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)); + } + if (x < SCAN_DIMENSION - 1) { + result.add(new Cube(x + 1, y, z)); + } + if (y > 0) { + result.add(new Cube(x, y - 1, z)); + } + if (y < SCAN_DIMENSION - 1) { + result.add(new Cube(x, y + 1, z)); + } + if (z > 0) { + result.add(new Cube(x, y, z - 1)); + } + if (z < SCAN_DIMENSION - 1) { + result.add(new Cube(x, y, z + 1)); } - System.out.println("Part 2: " + sum); + 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(); + + int result = 0; + if (grid[x + 1][y][z] == element) { + result++; + } + 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; + } + + public Collection getCubes() { + return cubes; + } + + public CubeType[][][] getGrid() { + return grid; } } diff --git a/src/test/java/com/macasaet/Day19.java b/src/test/java/com/macasaet/Day19.java deleted file mode 100644 index 007972a..0000000 --- a/src/test/java/com/macasaet/Day19.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Spliterators; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -public class Day19 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day19.class.getResourceAsStream("/day-19-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - - final var ruleMap = new HashMap(); - int sum = 0; - int mode = 0; - for (var line : lines) { - // comment out the remapping for Part 1 - if ("8: 42".equals(line.strip())) { - line = "8: 42 | 42 8"; - } else if ("11: 42 31".equals(line.strip())) { - line = "11: 42 31 | 42 11 31"; - } - if (line.isBlank()) { - mode++; - continue; - } - if (mode == 0) { - final var components = line.split(": ", 2); - final var ruleId = Integer.parseInt(components[0].strip()); - final var value = components[1].strip(); - if (value.matches("\"[a-z]\"")) { - final var c = value.charAt(1); - final var rule = new CharacterRule(ruleId, c); - ruleMap.put(ruleId, rule); - } else if (value.contains("|")) { - final var ruleSets = value.split(" \\| "); - final var set = Arrays.stream(ruleSets) - .map(ruleSet -> Arrays.stream(ruleSet.split(" ")) - .map(String::strip) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList())) - .collect(Collectors.toUnmodifiableSet()); - final var rule = new DisjunctionRule(ruleId, set); - ruleMap.put(ruleId, rule); - } else { - final var subRules = Arrays.stream(value.split(" ")) - .map(String::strip) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList()); - final var rule = new SequenceRule(ruleId, subRules); - ruleMap.put(ruleId, rule); - } - } else { - final var rule = ruleMap.get(0); - if (rule.matches(line.strip(), ruleMap)) { - sum++; - } - } - } - System.out.println("Result: " + sum); - } - } - - public abstract static class Rule { - protected final int id; - - protected Rule(final int id) { - this.id = id; - } - - public boolean matches(final String string, final Map map) { - return getMatchingSuffixes(Stream.of(string), map).distinct().anyMatch(String::isBlank); - } - - protected abstract Stream getMatchingSuffixes(Stream strings, Map map); - } - - public static class CharacterRule extends Rule { - - private final char c; - - public CharacterRule(final int id, final char c) { - super(id); - this.c = c; - } - - protected Stream getMatchingSuffixes(Stream strings, Map map) { - return strings.flatMap(string -> - string.startsWith("" + c) - ? Stream.of(string.substring(1)) - : Stream.empty()); - } - } - - public static class SequenceRule extends Rule { - private final List ruleIds; - - public SequenceRule(final int id, final List ruleIds) { - super(id); - this.ruleIds = ruleIds; - } - - protected Stream getMatchingSuffixes(Stream strings, Map map) { - return strings.flatMap(string -> { - var result = Stream.of(string); - for (final var ruleId : ruleIds) { // match all in sequence - final var rule = map.get(ruleId); - final var iterator = rule.getMatchingSuffixes(result, map).iterator(); - if (!iterator.hasNext()) { - result = Stream.empty(); - break; - } - result = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); - } - return result; - }).distinct(); - } - } - - public static class DisjunctionRule extends Rule { - - private final Collection> options; - - public DisjunctionRule(final int id, final Collection> options) { - super(id); - this.options = options; - } - - protected Stream getMatchingSuffixes(final Stream strings, final Map map) { - return strings.flatMap(string -> options.stream().flatMap(option -> { - var result = Stream.of(string); - for (final var ruleId : option) { // match all in sequence - final var rule = map.get(ruleId); - final var iterator = rule.getMatchingSuffixes(result, map).iterator(); - if (!iterator.hasNext()) { - result = Stream.empty(); - break; - } - result = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); - } - return result; - })).distinct(); - } - } -} \ 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 de4daeb..4125a47 100644 --- a/src/test/java/com/macasaet/Day20.java +++ b/src/test/java/com/macasaet/Day20.java @@ -1,406 +1,106 @@ package com.macasaet; -import java.io.IOException; +import org.junit.jupiter.api.Test; + 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.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.IntConsumer; -import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 20: Grove Positioning System --- + * https://adventofcode.com/2022/day/20 + */ public class Day20 { - private static final String seaMonsterString = - " # \n" + - "# ## ## ###\n" + - " # # # # # # "; - private static final char[][] seaMonster; - - static { - final var seaMonsterLines = seaMonsterString.split("\n"); - seaMonster = new char[seaMonsterLines.length][]; - for (int i = seaMonsterLines.length; --i >= 0; seaMonster[i] = seaMonsterLines[i].toCharArray()) ; - } - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day20.class.getResourceAsStream("/day-20-input.txt"))) { - final var tiles = new LinkedList(); - final var lines = - StreamSupport.stream(spliterator, false) - .map(String::strip) - .collect(Collectors.toUnmodifiableList()); - int tileId = -1; - var rows = new ArrayList(); - for (final var line : lines) { - if (line.startsWith("Tile ")) { - tileId = Integer.parseInt(line.replaceAll("[^0-9]", "")); - rows = new ArrayList<>(); - } else if (line.isBlank()) { - final var tile = new Tile(tileId, rows.toArray(new char[rows.size()][])); - tiles.add(tile); - tileId = -1; - rows = new ArrayList<>(); - } else { - rows.add(line.toCharArray()); - } - } - if (tileId > 0) { - final var tile = new Tile(tileId, rows.toArray(new char[rows.size()][])); - tiles.add(tile); - } - final int size = (int) Math.sqrt(tiles.size()); // the image is square - - final var possibleArrangements = getValidArrangements(Collections.emptyList(), tiles, size); - if (possibleArrangements.isEmpty()) { - throw new IllegalStateException("No possible arrangements"); - } else { - // there are multiple possible arrangements, but they are just rotated and/or flipped versions of each other - // TODO is there a matrix transform I can put in hashCode/equals that will treat these as equivalent? - System.err.println(possibleArrangements.size() + " possible arrangements: " + possibleArrangements); - for (final var arrangement : possibleArrangements) { - final var topLeft = arrangement.get(0); - final var topRight = arrangement.get(size - 1); - final var bottomLeft = arrangement.get(arrangement.size() - size); - final var bottomRight = arrangement.get(arrangement.size() - 1); - final var result = - Stream.of(topLeft, topRight, bottomLeft, bottomRight) - .map(corner -> corner.id) - .map(Long::valueOf) - .map(BigInteger::valueOf) - .reduce(BigInteger::multiply) - .get(); - System.out.println("Part 1: " + result); - - final var orderedCroppedTiles = - arrangement.stream().map(Tile::removeBorders).collect(Collectors.toUnmodifiableList()); - final var combined = combine(orderedCroppedTiles); - - for (final var permutation : combined.getPermutations()) { - final var counter = new AtomicInteger(0); - highlightSeaMonsters(permutation, counter::set); - final var numSeaMonsters = counter.get(); - if (numSeaMonsters > 0) { - System.err.println(permutation + " has " + numSeaMonsters + " sea monsters"); - int sum = 0; - for (int i = permutation.edgeLength; --i >= 0; ) { - for (int j = permutation.edgeLength; --j >= 0; ) { - if (permutation.grid[i][j] == '#') { - sum++; - } - } - } - System.out.println("Part 2: " + sum); - } - } - } - } + 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))); } } - public static void highlightSeaMonsters(final Tile tile, final IntConsumer counter) { - final int windowHeight = seaMonster.length; - final int windowWidth = seaMonster[0].length; - final int verticalWindows = tile.edgeLength - windowHeight; - final int horizontalWindows = tile.edgeLength - windowWidth; - - int sum = 0; - for (int i = 0; i < verticalWindows; i++) { - for (int j = 0; j < horizontalWindows; j++) { - if (contains(tile.grid, i, j)) { - sum++; - highlight(tile.grid, i, j); - } - } + 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))); } - counter.accept(sum); + return Collections.unmodifiableList(result); } - protected static boolean contains(final char[][] image, final int verticalOffset, final int horizontalOffset) { - for (int i = verticalOffset; i < verticalOffset + seaMonster.length; i++) { // loop the height of the pattern - final var patternRow = seaMonster[i - verticalOffset]; - final var imageRow = image[i]; - for (int j = horizontalOffset; j < horizontalOffset + patternRow.length; j++) { // loop the width of the pattern - final var p = patternRow[j - horizontalOffset]; - // spaces can be anything - if (p == '#' && imageRow[j] != '#') { - // only the # need to match - return false; - } + @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 true; - } + final var workingSet = new ArrayList<>(numbers); - protected static void highlight(final char[][] image, final int verticalOffset, final int horizontalOffset) { - for (int i = verticalOffset; i < verticalOffset + seaMonster.length; i++) { - final var patternRow = seaMonster[i - verticalOffset]; - final var imageRow = image[i]; - for (int j = horizontalOffset; j < horizontalOffset + patternRow.length; j++) { - final var p = patternRow[j - horizontalOffset]; - if (p == ' ') continue; - if (p == '#') imageRow[j] = 'O'; + 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; } + workingSet.add(newIndex, number); } - } - protected static Set> getValidArrangements(final List partialArrangement, - final List remainingTiles, - final int edgeLength) { - if (remainingTiles.isEmpty()) { - return Collections.singleton(partialArrangement); - } else if (partialArrangement.isEmpty()) { - // find the candidates for the top-left tile - final Set> set = new HashSet<>(); - for (int i = remainingTiles.size(); --i >= 0; ) { - final var candidate = remainingTiles.get(i); // candidate for first tile - // consider all possible orientations - for (final var orientation : candidate.getPermutations()) { -// System.err.println("Trying " + orientation + " in the top left with."); - final var partial = Collections.singletonList(orientation); - final var remaining = new ArrayList(remainingTiles.size() - 1); - remaining.addAll(remainingTiles.subList(0, i)); - remaining.addAll(remainingTiles.subList(i + 1, remainingTiles.size())); + 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(); - final var validArrangements = - getValidArrangements(partial, Collections.unmodifiableList(remaining), edgeLength); - if (!validArrangements.isEmpty()) { - System.err.println("Found arrangement with " + orientation + " in the top left."); - } - set.addAll(validArrangements); - } - } - return Collections.unmodifiableSet(set); - } + final var result = (long)x + (long)y + (long)z; - final Set> set = new HashSet<>(); - for (int i = remainingTiles.size(); --i >= 0; ) { - final var candidate = remainingTiles.get(i); - final var oriented = fits(partialArrangement, candidate, edgeLength); - if (oriented != null) { -// System.err.println(oriented + " fits at index " + partialArrangement.size()); - final var permutation = new ArrayList<>(partialArrangement); - permutation.add(oriented); // this is a new valid partial arrangement (is it the only one?) - - final var remaining = new ArrayList(remainingTiles.size() - 1); - remaining.addAll(remainingTiles.subList(0, i)); - remaining.addAll(remainingTiles.subList(i + 1, remainingTiles.size())); - - final var validArrangements = - getValidArrangements(Collections.unmodifiableList(permutation), - Collections.unmodifiableList(remaining), edgeLength); - set.addAll(validArrangements); - } - } - return Collections.unmodifiableSet(set); - } - - protected static Tile getTileAbove(final List partialArrangement, final int index, final int edgeLength) { - if (index < edgeLength) { - return null; - } - return partialArrangement.get(index - edgeLength); - } - - protected static Tile getTileToLeft(final List partialArrangement, final int index, final int edgeLength) { - if (index % edgeLength == 0) { - return null; - } - return partialArrangement.get(index - 1); - } - - protected static Tile fits(final List partialArrangement, final Tile candidate, final int edgeLength) { - final int index = partialArrangement.size(); - final var tileAbove = getTileAbove(partialArrangement, index, edgeLength); - final var tileToLeft = getTileToLeft(partialArrangement, index, edgeLength); - for (final var orientation : candidate.getPermutations()) { - final var topFits = tileAbove == null || tileAbove.getBottomBorder().equals(orientation.getTopBorder()); - final var leftFits = tileToLeft == null || tileToLeft.getRightBorder().equals(orientation.getLeftBorder()); - if (topFits && leftFits) { - return orientation; - } - } - return null; - } - - protected static Tile combine(final List arrangement) { - final int tilesOnEdge = (int) Math.sqrt(arrangement.size()); - final var combinedLength = tilesOnEdge * arrangement.get(0).edgeLength; - char[][] combinedGrid = new char[combinedLength][]; - int maxId = Integer.MIN_VALUE; - int id = 0; - for (int index = arrangement.size(); --index >= 0; ) { - final var tile = arrangement.get(index); - maxId = Math.max(maxId, tile.id); - id = id * 31 + tile.id; - final int tileRow = index / tilesOnEdge; - final int tileColumn = index % tilesOnEdge; - final int rowOffset = tile.edgeLength * tileRow; - final int columnOffset = tile.edgeLength * tileColumn; - for (int row = tile.edgeLength; --row >= 0; ) { - if (combinedGrid[row + rowOffset] == null) { - combinedGrid[row + rowOffset] = new char[combinedLength]; - } - for (int column = tile.edgeLength; --column >= 0; ) { - combinedGrid[row + rowOffset][column + columnOffset] = tile.grid[row][column]; - } - } - } - return new Tile(id % maxId, combinedGrid); + System.out.println("Part 1: " + result); } - protected static class Tile { - private final int id; - private final char[][] grid; - private final int edgeLength; - private final String label; - - public Tile(final int id, final char[][] grid) { - this(id, grid, "original"); - } - - protected Tile(final int id, final char[][] grid, final String label) { - this.id = id; - this.grid = grid; - this.edgeLength = grid.length; - this.label = label; - } - - public String getTopBorder() { - return new String(grid[0]); - } - - public String getBottomBorder() { - return new String(grid[edgeLength - 1]); - } - - public String getLeftBorder() { - final char[] array = new char[edgeLength]; - for (int i = edgeLength; --i >= 0; array[i] = grid[i][0]) ; - return new String(array); - } - - public String getRightBorder() { - final char[] array = new char[edgeLength]; - for (int i = edgeLength; --i >= 0; array[i] = grid[i][edgeLength - 1]) ; - return new String(array); - } - - public Collection getPermutations() { - return List.of(this, flipHorizontal(), flipVertical(), rotate90(), rotate180(), rotate270(), - rotate90().flipHorizontal(), rotate90().flipVertical(), - rotate180().flipHorizontal(), rotate180().flipVertical(), - rotate270().flipHorizontal(), rotate270().flipVertical()); - } - - public Tile flipVertical() { - final var flipped = new char[edgeLength][]; - for (int row = edgeLength; --row >= 0; ) { - final var flippedRow = new char[edgeLength]; - for (int oldColumn = edgeLength; --oldColumn >= 0; ) { - final int newColumn = edgeLength - oldColumn - 1; - flippedRow[newColumn] = grid[row][oldColumn]; + @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; + } + } + 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; } - flipped[row] = flippedRow; + workingSet.add(newIndex, number); } - return new Tile(id, flipped, label + ", flipped around vertical axis"); } - public Tile flipHorizontal() { - final var flipped = new char[edgeLength][]; - for (int i = edgeLength; --i >= 0; ) { - final int newRowId = edgeLength - i - 1; - final char[] row = new char[edgeLength]; - System.arraycopy(grid[i], 0, row, 0, edgeLength); - flipped[newRowId] = row; - } - return new Tile(id, flipped, label + ", flipped around horizontal axis"); - } + 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(); - public Tile transpose() { - final var transposed = new char[edgeLength][]; // should this be its own permutation? - for (int i = edgeLength; --i >= 0; ) { - transposed[i] = new char[edgeLength]; - for (int j = edgeLength; --j >= 0; ) { - transposed[i][j] = grid[j][i]; - } - } - return new Tile(id, transposed, label + ", transposed"); - } + final var result = x.add(y).add(z); - public Tile rotate90() { - final var transposed = transpose().grid; - final var reversedRows = new char[edgeLength][]; - for (int i = edgeLength; --i >= 0; ) { - final var newRow = new char[edgeLength]; - for (int j = edgeLength; --j >= 0; ) { - final int newColumn = edgeLength - j - 1; - newRow[newColumn] = transposed[i][j]; - } - reversedRows[i] = newRow; - } - return new Tile(id, reversedRows, label + ", rotated 90 degrees"); - } - - public Tile rotate180() { - return new Tile(id, rotate90().rotate90().grid, label + ", rotated 180 degrees"); - } - - public Tile rotate270() { - return new Tile(id, rotate180().rotate90().grid, label + ", rotated 270 degrees"); - } - - public Tile removeBorders() { - final int length = edgeLength - 2; - final var cropped = new char[length][]; - for (int i = edgeLength - 1; --i >= 1; ) { - final var row = new char[length]; - System.arraycopy(grid[i], 1, row, 0, length); - cropped[i - 1] = row; - } - return new Tile(id, cropped, label + " cropped"); - } - - public String toString() { - return "Tile{ " + id + ", " + label + ", top=" + getTopBorder() + " }"; - } - - public int hashCode() { - int retval = 0; - retval = 31 * retval + id; - for (int i = edgeLength; --i >= 0; ) { - for (int j = edgeLength; --j >= 0; ) { - retval = 31 * retval + grid[i][j]; - } - } - return retval; - } - - public boolean equals(final Object o) { - if (o == null) { - return false; - } else if (this == o) { - return true; - } - try { - final Tile other = (Tile) o; - boolean retval = id == other.id; - for (int i = edgeLength; --i >= 0 && retval; ) { - for (int j = edgeLength; --j >= 0 && retval; ) { - retval &= grid[i][j] == other.grid[i][j]; - } - } - return retval; - } catch (final ClassCastException cce) { - return false; - } - } + 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 db1a5c6..b2fbfc8 100644 --- a/src/test/java/com/macasaet/Day21.java +++ b/src/test/java/com/macasaet/Day21.java @@ -1,120 +1,323 @@ package com.macasaet; -import java.io.IOException; -import java.util.Comparator; +import org.junit.jupiter.api.Test; + +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.Map.Entry; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.Map; import java.util.stream.StreamSupport; +/** + * --- Day 21: Monkey Math --- + * https://adventofcode.com/2022/day/21 + */ public class Day21 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day21.class.getResourceAsStream("/day-21-input.txt"))) { - - final var foods = StreamSupport.stream(spliterator, false) - .map(String::strip) - .map(Food::fromLine) - .collect(Collectors.toUnmodifiableSet()); - final var allergenToFood = new HashMap>(); - final var ingredientToFood = new HashMap>(); - for (final var food : foods) { - for (final var allergen : food.allergens) { - allergenToFood.computeIfAbsent(allergen, key -> new HashSet<>()).add(food); - } - for (final var ingredient : food.ingredients) { - ingredientToFood.computeIfAbsent(ingredient, key -> new HashSet<>()).add(food); - } - } + public record Monkey(String name, Job job) { + public long yell(final Map monkeys, final Map results) { + return job().yell(monkeys, results); + } - final var ingredientsThatContainNoAllergens = new HashSet<>(ingredientToFood.keySet()); - final var allergenToIngredient = new HashMap>(); - allergenToFood.entrySet().stream().forEach(entry -> { - final var allergen = entry.getKey(); - final var appearances = entry.getValue(); - Set commonIngredients = null; - for (final var food : appearances) { - if (commonIngredients == null) { - commonIngredients = new HashSet<>(food.ingredients); - } else { - commonIngredients.retainAll(food.ingredients); - } - } - System.err.println(allergen + " may be found in: " + commonIngredients); - allergenToIngredient.put(allergen, commonIngredients); - ingredientsThatContainNoAllergens.removeAll(commonIngredients); - }); - - int sum = 0; - for (final var food : foods) { - for (final var ingredient : ingredientsThatContainNoAllergens) { - if (food.ingredients.contains(ingredient)) { - sum++; - } - } - } - System.out.println("Part 1: " + sum); - - final var ingredientToAllergen = new HashMap(); - - final var dangerousIngredients = new HashSet<>(ingredientToFood.keySet()); - dangerousIngredients.removeAll(ingredientsThatContainNoAllergens); - while (!dangerousIngredients.isEmpty()) { - for (final var i = dangerousIngredients.iterator(); i.hasNext(); ) { - final var ingredient = i.next(); - boolean mappedIngredient = false; - for (final var j = allergenToIngredient.entrySet().iterator(); j.hasNext(); ) { - final var entry = j.next(); - final var allergen = entry.getKey(); - final var ingredients = entry.getValue(); - if (ingredients.size() == 1 && ingredients.contains(ingredient)) { - // this is the only ingredient known to contain this allergen - ingredientToAllergen.put(ingredient, allergen); - System.err.println("Mapping " + ingredient + " to " + allergen); - j.remove(); - mappedIngredient |= true; - break; + 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(); + } + + record Expression(Simplification x, Operation operation, Simplification y) implements Simplification { + + 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()); } - if (mappedIngredient) { - for (final var entry : allergenToIngredient.entrySet()) { - final var ingredients = entry.getValue(); - ingredients.remove(ingredient); + // 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); } - i.remove(); + } 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; + } - final var result = - ingredientToAllergen - .entrySet() - .stream() - .sorted(Comparator.comparing(Entry::getValue)) - .map(Entry::getKey) - .collect(Collectors.joining(",")); - System.out.println("Part 2: " + result); + public String toString() { + return "(" + x() + ") " + operation() + " (" + y() + ")"; } } - public static class Food { - private final Set ingredients; - private final Set allergens; + record Value(long value) implements Simplification { + public String toString() { + return "" + value(); + } - public Food(final Set ingredients, final Set allergens) { - this.ingredients = ingredients; - this.allergens = allergens; + public Simplification simplify() { + return this; } + } - public static Food fromLine(final String line) { - final var components = line.split("\\(contains "); - final var ingredientsArray = components[0].strip().split(" "); - final var allergensString = components[1].strip().replaceAll("\\)", ""); - final var allergensArray = allergensString.split(", "); - return new Food(Set.of(ingredientsArray), Set.of(allergensArray)); + 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); + + static Job parse(final String string) { + final var components = string.trim().split(" "); + if (components.length == 1) { + return Yell.parse(string.trim()); + } + 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 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 Unknown() implements Job { + public long yell(Map monkeys, Map results) { + throw new UnsupportedOperationException(); // Oof + } + + public Simplification simplify(Map monkeys, Map results) { + return new Variable(); + } + } + + 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 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); + } + 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 monkeys = getInput(); + final var results = new HashMap(); + final var result = monkeys.get("root").yell(monkeys, results); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + 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 c6e1598..0000000 --- a/src/test/java/com/macasaet/Day22.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class Day22 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day22.class.getResourceAsStream("/day-22-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false) - .collect(Collectors.toUnmodifiableList()); - - final var player1cards = new LinkedList(); - final var player2cards = new LinkedList(); - var target = player1cards; - for (final var line : lines) { - if (line.isBlank()) { - target = player2cards; - } - if (line.matches("^[0-9]+$")) { - target.add(Integer.parseInt(line)); - } - } - var player1 = new Player(1, new LinkedList<>(player1cards)); - var player2 = new Player(2, new LinkedList<>(player2cards)); - - // Part 1 - while (player1.hasCards() && player2.hasCards()) { - final int p1 = player1.drawTop(); - final int p2 = player2.drawTop(); - if (p1 > p2) { - player1.addToBottom(p1); - player1.addToBottom(p2); - } else { - player2.addToBottom(p2); - player2.addToBottom(p1); - } - } - var winner = player1.hasCards() ? player1 : player2; - System.out.println("Part 1: " + winner.score()); - - // Part 2 - player1 = new Player(1, new LinkedList<>(player1cards)); - player2 = new Player(2, new LinkedList<>(player2cards)); - winner = playGame(player1, player2); - System.out.println("Part 2: " + winner.score()); - } - } - - protected static Player playGame(final Player player1, final Player player2) { - final var rounds = new HashSet<>(); - while (player1.hasCards() && player2.hasCards()) { - final var round = new Round(player1, player2); - if (rounds.contains(round)) { - return player1; - } - rounds.add(round); - final int p1 = player1.drawTop(); - final int p2 = player2.drawTop(); - if (player1.cardCountIsAtLeast(p1) && player2.cardCountIsAtLeast(p2)) { - final var winner = playGame(player1.clone(p1), player2.clone(p2)); - if (winner.id == player1.id) { - player1.addToBottom(p1); - player1.addToBottom(p2); - } else { - player2.addToBottom(p2); - player2.addToBottom(p1); - } - } else { - if (p1 > p2) { - player1.addToBottom(p1); - player1.addToBottom(p2); - } else { - player2.addToBottom(p2); - player2.addToBottom(p1); - } - } - } - return player1.hasCards() ? player1 : player2; - } - - protected static class Player { - final int id; - final List deck; - - public Player(final int id, final List deck) { - this.id = id; - this.deck = deck; - } - - public boolean hasCards() { - return !deck.isEmpty(); - } - - public boolean cardCountIsAtLeast(final int count) { - return deck.size() >= count; - } - - public int drawTop() { - return deck.remove(0); - } - - public void addToBottom(final int card) { - deck.add(card); - } - - public Player clone(final int cardCount) { - return new Player(id, new LinkedList<>(deck.subList(0, cardCount))); - } - - public int score() { - int retval = 0; - int multiplier = deck.size(); - for (final var card : deck) { - retval += card * (multiplier--); - } - return retval; - } - } - - protected static class Round { - private final List x; - private final List y; - - public Round(final List x, final List y) { - this.x = List.copyOf(x); - this.y = List.copyOf(y); - } - - public Round(final Player x, final Player y) { - this(x.deck, y.deck); - } - - public int hashCode() { - return Objects.hash(x, y); - } - - public boolean equals(final Object o) { - if (this == o) { - return true; - } else if (o == null) { - return false; - } - try { - final Round other = (Round) o; - return Objects.equals(x, other.x) && Objects.equals(y, other.y); - } catch (final ClassCastException cce) { - return false; - } - } - } -} \ 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 aa1f327..b701a77 100644 --- a/src/test/java/com/macasaet/Day23.java +++ b/src/test/java/com/macasaet/Day23.java @@ -1,104 +1,644 @@ package com.macasaet; -import java.io.IOException; -import java.math.BigInteger; +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.StreamSupport; +/** + * --- Day 23: Unstable Diffusion --- + * https://adventofcode.com/2022/day/23 + */ public class Day23 { - public static void main(final String[] args) throws IOException { - final var labels = new ArrayList(); - try (var inputStream = Day23.class.getResourceAsStream("/day-23-input.txt")) { - for (final var c : new String(inputStream.readAllBytes()).strip().toCharArray()) { - labels.add(Integer.parseInt("" + c)); - } - } - - final var map = new Cup[1_000_0000 + 1]; - Cup first = null; - Cup current = null; - int min = Integer.MAX_VALUE; - int max = Integer.MIN_VALUE; - for (final int label : labels) { - final var cup = new Cup(label); - map[label] = cup; - if (first == null) { - first = cup; - } else { - current.next = cup; - } - current = cup; - - min = Math.min(min, label); - max = Math.max(max, label); - } - - for (int i = max + 1; i <= 1_000_000; i++) { // Part 2 - final var cup = new Cup(i); - map[i] = cup; - current.next = cup; - current = cup; - } - max = 1_000_000; - current.next = first; - current = current.next; - - for (int moveId = 1; moveId <= 10_000_000; moveId++) { // Part 1 looped only 100 times - final var a = current.take(); - final var b = current.take(); - final var c = current.take(); - - int destinationValue = current.label - 1; - while (destinationValue == a.label || destinationValue == b.label || destinationValue == c.label || destinationValue < min || destinationValue > max) { - destinationValue--; - if (destinationValue < min) { - destinationValue = max; + record Coordinate(int x, int y) { + public boolean hasElf(final Map> grid) { + return grid.getOrDefault(x(), Collections.emptyMap()).getOrDefault(y(), false); + } + + 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 Collections.unmodifiableSet(neighbours); + } + } + + 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 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 Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x(), reference.y() + 1); + } + }, + 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)); + } + + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x() + 1, reference.y()); + } + }, + 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)); + } + + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x(), reference.y() - 1); + } + }; - final var destination = map[destinationValue]; - c.linkBefore(destination.next); - b.linkBefore(c); - a.linkBefore(b); - destination.linkBefore(a); - - current = current.next; - } - final var reference = map[1]; -// String result = ""; -// var cursor = reference.next; -// for (int i = labels.size() - 1; --i >= 0; ) { -// result += cursor.label; -// cursor = cursor.next; -// } -// System.out.println("Part 1: " + result); - final BigInteger x = new BigInteger("" + map[reference.next.label].label); - final BigInteger y = new BigInteger("" + map[reference.next.next.label].label); - System.out.println("Part 2: " + x.multiply(y).toString()); + public abstract Set relativeCoordinates(Coordinate reference); + + public abstract Coordinate adjacent(Coordinate reference); } - protected static class Cup { - private final int label; - private Cup next; + 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 Cup(final int label) { - this.label = label; + 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 Cup take() { - final Cup retval = next; - next = retval.next; - retval.next = null; - return retval; + 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; + } + } + } + } + } + + // 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())); + } + } + // 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); + } + // 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 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++; + } + } + } + return result; } - public void linkBefore(final Cup cup) { - next = cup; + 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); + } + } + grid.put(i, row); + if (rowHasElf) { + minX = Math.min(minX, i); + maxX = Math.max(maxX, i); + } + } + return new Crater(grid, minX, maxX, minY, maxY); } public String toString() { - return "Cup{ label=" + label + ", next=" + (next != null ? next.label : null) + " }"; + 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(); + } + + 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'); + } + } + + protected Map> getGrid() { + return grid; + } + + protected int getMinX() { + return minX; + } + + protected void setMinX(int minX) { + this.minX = minX; + } + + protected int getMaxX() { + return maxX; + } + + protected void setMaxX(int maxX) { + this.maxX = maxX; + } + + protected int getMinY() { + return minY; + } + + protected void setMinY(int minY) { + this.minY = minY; + } + + protected int getMaxY() { + return maxY; + } + + protected void setMaxY(int maxY) { + this.maxY = maxY; + } + + protected List getMovementPriority() { + return movementPriority; + } + + protected void setMovementPriority(List movementPriority) { + this.movementPriority = movementPriority; + } + } + + protected static Crater getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-23.txt"), false) + .collect(Collectors.joining("\n")); + return Crater.fromString(lines); + } + + @Test + public final void part1() { + final var crater = getInput(); + for (int i = 10; --i >= 0; crater.round()) ; + final var result = crater.countEmptyGroundTiles(); + + System.out.println("Part 1: " + result); + } + + @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); + } + + @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); + } + + @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 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 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 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 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); + } + + @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 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/java/com/macasaet/Day24.java b/src/test/java/com/macasaet/Day24.java deleted file mode 100644 index 3d3282e..0000000 --- a/src/test/java/com/macasaet/Day24.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map.Entry; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -public class Day24 { - - public static void main(final String[] args) throws IOException { - final Address origin = new Address(0, 0, 0); - final var lobbyFloor = new HashMap(); - try (var spliterator = new LineSpliterator(Day22.class.getResourceAsStream("/day-24-input.txt"))) { - StreamSupport.stream(spliterator, false) - .map(String::toCharArray) - .map(array -> { - int i = 0; - var directions = new ArrayList(array.length); - while (i < array.length) { - if (i < array.length - 1) { // check if there are at least 2 chars available - final var twoLetterDirection = "" + array[i] + array[i + 1]; - // check if two chars is a valid direction - if ("se".equalsIgnoreCase(twoLetterDirection) - || "sw".equalsIgnoreCase(twoLetterDirection) - || "nw".equalsIgnoreCase(twoLetterDirection) - || "ne".equalsIgnoreCase(twoLetterDirection)) { - directions.add(Direction.forAbbreviation(twoLetterDirection)); - i = i + 2; - continue; - } - } - final var oneLetterDirection = "" + array[i]; - if ("e".equalsIgnoreCase(oneLetterDirection) || "w".equalsIgnoreCase(oneLetterDirection)) { - directions.add(Direction.forAbbreviation(oneLetterDirection)); - i = i + 1; - continue; - } - throw new IllegalArgumentException("Invalid direction: " + oneLetterDirection); - } - return Collections.unmodifiableList(directions); - }).map(directions -> { - Address cursor = origin; - for (final var direction : directions) { - cursor = direction.travel(cursor); - } - return cursor; - }).forEach(address -> lobbyFloor.merge(address, 1, (old, def) -> old + 1)); - } - final int blackTiles = lobbyFloor.values().stream().mapToInt(count -> count % 2).sum(); - System.out.println("Part 1: " + blackTiles); - - for (int i = 1; i <= 100; i++) { - final var tasks = lobbyFloor.entrySet().stream().flatMap(entry -> { - // get the mapped tile - // as well as unmapped (white) adjacent tiles - final var from = entry.getKey(); - return Stream.concat(Stream.of(entry), Arrays.stream(Direction.values()).flatMap(direction -> { - final var neighbour = direction.travel(from); - if (!lobbyFloor.containsKey(neighbour)) { - // neighbour has never been visited, create a virtual entry for them - return Stream.of(new Entry() { - public Address getKey() { - return neighbour; - } - - public Integer getValue() { - return 0; - } - - public Integer setValue(Integer value) { - throw new UnsupportedOperationException(); - } - }); - } - return Stream.empty(); - })); - }).map(entry -> { // NB: this might not be a real tile - final var address = entry.getKey(); - final int adjacentBlackTiles = Arrays.stream(Direction.values()) - .map(direction -> direction.travel(address)) - .mapToInt(neighbour -> { - final Integer neighbouringCount = lobbyFloor.get(neighbour); - return neighbouringCount != null && neighbouringCount % 2 == 1 ? 1 : 0; - }).sum(); - if (entry.getValue() % 2 == 1 && (adjacentBlackTiles == 0 || adjacentBlackTiles > 2)) { - // Any black tile with zero or more than 2 black tiles immediately adjacent to it is flipped to white. - return (Runnable) () -> lobbyFloor.put(address, 0); - } else if (entry.getValue() % 2 == 0 && adjacentBlackTiles == 2) { - // Any white tile with exactly 2 black tiles immediately adjacent to it is flipped to black. - return (Runnable) () -> lobbyFloor.put(address, 1); - } - return (Runnable) () -> { - }; - }).collect(Collectors.toUnmodifiableList()); - for (final var task : tasks) { - task.run(); - } - } - final int count = lobbyFloor.values().stream().mapToInt(value -> value % 2).sum(); - System.out.println("Part 2: " + count); - } - - public enum Direction { - EAST("e", 1, -1, 0), - SOUTH_EAST("se", 0, -1, 1), - SOUTH_WEST("sw", -1, 0, 1), - WEST("w", -1, 1, 0), - NORTH_WEST("nw", 0, 1, -1), - NORTH_EAST("ne", 1, 0, -1); - - private final String abbreviation; - private final int xOffset; - private final int yOffset; - private final int zOffset; - - Direction(final String abbreviation, final int xOffset, final int yOffset, final int zOffset) { - this.abbreviation = abbreviation; - this.xOffset = xOffset; - this.yOffset = yOffset; - this.zOffset = zOffset; - } - - public static Direction forAbbreviation(final String abbreviation) { - for (final var candidate : values()) { - if (candidate.abbreviation.equalsIgnoreCase(abbreviation)) { - return candidate; - } - } - throw new IllegalArgumentException("Invalid direction: " + abbreviation); - } - - public Address travel(final Address from) { - return new Address(from.x + xOffset, from.y + yOffset, from.z + zOffset); - } - - } - - public static class Address { - - private final int x; - private final int y; - private final int z; - - public Address(final int x, final int y, final int z) { - this.x = x; - this.y = y; - this.z = z; - } - - public int hashCode() { - int retval = 0; - retval = 31 * retval + x; - retval = 31 * retval + y; - retval = 31 * retval + z; - return retval; - } - - public boolean equals(final Object o) { - if (this == o) { - return true; - } else if (o == null) { - return false; - } - try { - final Address other = (Address) o; - return x == other.x && y == other.y && z == other.z; - } catch (final ClassCastException cce) { - return false; - } - } - - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day25.java b/src/test/java/com/macasaet/Day25.java deleted file mode 100644 index 66be1df..0000000 --- a/src/test/java/com/macasaet/Day25.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class Day25 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day22.class.getResourceAsStream("/day-25-input.txt"))) { - final var publicKeys = StreamSupport.stream(spliterator, false) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList()); - final int cardPublicKey = publicKeys.get(0); - final int doorPublicKey = publicKeys.get(1); - final long initialSubjectNumber = 7; - - int cardLoopSize = 0; - for (long value = 1; value != cardPublicKey; cardLoopSize++) { - value = transformOnce(value, initialSubjectNumber); - } - System.out.println("cardLoopSize: " + cardLoopSize); - - int doorLoopSize = 0; - for (long value = 1; value != doorPublicKey; doorLoopSize++) { - value = transformOnce(value, initialSubjectNumber); - } - System.out.println("doorLoopSize: " + doorLoopSize); - - final long e1 = transformCompletely(doorPublicKey, cardLoopSize); - final long e2 = transformCompletely(cardPublicKey, doorLoopSize); - System.out.println("e1: " + e1); - System.out.println("e2: " + e2); - } - } - - protected static long transformCompletely(final long subjectNumber, final int loopSize) { - long value = 1; - for (int i = loopSize; --i >= 0; value = transformOnce(value, subjectNumber)) ; - return value; - } - - protected static long transformOnce(final long value, final long subjectNumber) { - return (value * subjectNumber) % 20201227; - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/LineSpliterator.java b/src/test/java/com/macasaet/LineSpliterator.java index 5333f65..6e3c360 100644 --- a/src/test/java/com/macasaet/LineSpliterator.java +++ b/src/test/java/com/macasaet/LineSpliterator.java @@ -1,18 +1,26 @@ package com.macasaet; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; +import java.io.*; import java.util.Objects; +import java.util.Properties; import java.util.Spliterator; import java.util.function.Consumer; public class LineSpliterator implements Spliterator, AutoCloseable { + private static final String prefix; private final BufferedReader reader; + static { + final var properties = new Properties(); + try { + final var config = LineSpliterator.class.getResourceAsStream("/config.properties"); + if (config != null) properties.load(config); + } catch (final IOException ignored) { + } + prefix = properties.getProperty("prefix", "/sample"); + } + public LineSpliterator(final BufferedReader reader) { Objects.requireNonNull(reader); this.reader = reader; @@ -26,6 +34,10 @@ public LineSpliterator(final InputStream stream) { this(new InputStreamReader(stream)); } + public LineSpliterator(final String fileName) { + this(LineSpliterator.class.getResourceAsStream(prefix + "/" + fileName)); + } + public boolean tryAdvance(final Consumer action) { try { final var line = reader.readLine(); @@ -35,7 +47,6 @@ public boolean tryAdvance(final Consumer action) { } reader.close(); } catch (final IOException ioe) { - ioe.printStackTrace(); } return false; } diff --git a/src/test/resources/sample/day-01.txt b/src/test/resources/sample/day-01.txt new file mode 100644 index 0000000..444e241 --- /dev/null +++ b/src/test/resources/sample/day-01.txt @@ -0,0 +1,14 @@ +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 new file mode 100644 index 0000000..25097e8 --- /dev/null +++ b/src/test/resources/sample/day-02.txt @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..9919ffa --- /dev/null +++ b/src/test/resources/sample/day-03.txt @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..99a66c5 --- /dev/null +++ b/src/test/resources/sample/day-04.txt @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..e98aba4 --- /dev/null +++ b/src/test/resources/sample/day-05.txt @@ -0,0 +1,9 @@ + [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 new file mode 100644 index 0000000..5a2b0a7 --- /dev/null +++ b/src/test/resources/sample/day-06.txt @@ -0,0 +1 @@ +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 new file mode 100644 index 0000000..bcbb513 --- /dev/null +++ b/src/test/resources/sample/day-07.txt @@ -0,0 +1,23 @@ +$ 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 new file mode 100644 index 0000000..6557024 --- /dev/null +++ b/src/test/resources/sample/day-08.txt @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000..cbea2b3 --- /dev/null +++ b/src/test/resources/sample/day-09.txt @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..94cd0a8 --- /dev/null +++ b/src/test/resources/sample/day-10.txt @@ -0,0 +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 new file mode 100644 index 0000000..c04eddb --- /dev/null +++ b/src/test/resources/sample/day-11.txt @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..86e9cac --- /dev/null +++ b/src/test/resources/sample/day-12.txt @@ -0,0 +1,5 @@ +Sabqponm +abcryxxl +accszExk +acctuvwj +abdefghi diff --git a/src/test/resources/sample/day-13.txt b/src/test/resources/sample/day-13.txt new file mode 100644 index 0000000..27c8912 --- /dev/null +++ b/src/test/resources/sample/day-13.txt @@ -0,0 +1,23 @@ +[1,1,3,1,1] +[1,1,5,1,1] + +[[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 new file mode 100644 index 0000000..1926028 --- /dev/null +++ b/src/test/resources/sample/day-14.txt @@ -0,0 +1,2 @@ +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 new file mode 100644 index 0000000..652e631 --- /dev/null +++ b/src/test/resources/sample/day-15.txt @@ -0,0 +1,14 @@ +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-18.txt b/src/test/resources/sample/day-18.txt new file mode 100644 index 0000000..d18bf98 --- /dev/null +++ b/src/test/resources/sample/day-18.txt @@ -0,0 +1,13 @@ +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-20.txt b/src/test/resources/sample/day-20.txt new file mode 100644 index 0000000..5cbf3d9 --- /dev/null +++ b/src/test/resources/sample/day-20.txt @@ -0,0 +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 new file mode 100644 index 0000000..7993b87 --- /dev/null +++ b/src/test/resources/sample/day-21.txt @@ -0,0 +1,15 @@ +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-23.txt b/src/test/resources/sample/day-23.txt new file mode 100644 index 0000000..7ac3ba9 --- /dev/null +++ b/src/test/resources/sample/day-23.txt @@ -0,0 +1,7 @@ +....#.. +..###.# +#...#.# +.#...## +#.###.. +##.#.## +.#..#.. \ 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