From 361742a7bed3863824cb9fe10fc396f88fc5b4bd Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Mon, 29 Nov 2021 22:38:08 -0800 Subject: [PATCH 01/44] Prepare for Advent of Code 2021 --- README.md | 4 +- pom.xml | 27 +- src/test/java/com/macasaet/Day01.java | 57 +--- src/test/java/com/macasaet/Day02.java | 91 ------ src/test/java/com/macasaet/Day03.java | 57 ---- src/test/java/com/macasaet/Day04.java | 83 ------ src/test/java/com/macasaet/Day05.java | 56 ---- src/test/java/com/macasaet/Day06.java | 48 --- src/test/java/com/macasaet/Day07.java | 75 ----- src/test/java/com/macasaet/Day08.java | 96 ------ src/test/java/com/macasaet/Day09.java | 57 ---- src/test/java/com/macasaet/Day10.java | 80 ----- src/test/java/com/macasaet/Day11.java | 277 ------------------ src/test/java/com/macasaet/Day12.java | 200 ------------- src/test/java/com/macasaet/Day13.java | 93 ------ src/test/java/com/macasaet/Day14.java | 127 -------- src/test/java/com/macasaet/Day15.java | 49 ---- src/test/java/com/macasaet/Day16.java | 198 ------------- src/test/java/com/macasaet/Day17.java | 150 ---------- src/test/java/com/macasaet/Day18.java | 92 ------ src/test/java/com/macasaet/Day19.java | 152 ---------- src/test/java/com/macasaet/Day20.java | 406 -------------------------- src/test/java/com/macasaet/Day21.java | 120 -------- src/test/java/com/macasaet/Day22.java | 157 ---------- src/test/java/com/macasaet/Day23.java | 104 ------- src/test/java/com/macasaet/Day24.java | 179 ------------ src/test/java/com/macasaet/Day25.java | 47 --- 27 files changed, 39 insertions(+), 3043 deletions(-) delete mode 100644 src/test/java/com/macasaet/Day02.java delete mode 100644 src/test/java/com/macasaet/Day03.java delete mode 100644 src/test/java/com/macasaet/Day04.java delete mode 100644 src/test/java/com/macasaet/Day05.java delete mode 100644 src/test/java/com/macasaet/Day06.java delete mode 100644 src/test/java/com/macasaet/Day07.java delete mode 100644 src/test/java/com/macasaet/Day08.java delete mode 100644 src/test/java/com/macasaet/Day09.java delete mode 100644 src/test/java/com/macasaet/Day10.java delete mode 100644 src/test/java/com/macasaet/Day11.java delete mode 100644 src/test/java/com/macasaet/Day12.java delete mode 100644 src/test/java/com/macasaet/Day13.java delete mode 100644 src/test/java/com/macasaet/Day14.java delete mode 100644 src/test/java/com/macasaet/Day15.java delete mode 100644 src/test/java/com/macasaet/Day16.java delete mode 100644 src/test/java/com/macasaet/Day17.java delete mode 100644 src/test/java/com/macasaet/Day18.java delete mode 100644 src/test/java/com/macasaet/Day19.java delete mode 100644 src/test/java/com/macasaet/Day20.java delete mode 100644 src/test/java/com/macasaet/Day21.java delete mode 100644 src/test/java/com/macasaet/Day22.java delete mode 100644 src/test/java/com/macasaet/Day23.java delete mode 100644 src/test/java/com/macasaet/Day24.java delete mode 100644 src/test/java/com/macasaet/Day25.java diff --git a/README.md b/README.md index 6947951..61666d8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# Advent of Code 2020 (Java) +# Advent of Code 2021 (Java) ## 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)) diff --git a/pom.xml b/pom.xml index 22284a9..7099d59 100644 --- a/pom.xml +++ b/pom.xml @@ -6,9 +6,9 @@ com.macasaet advent-of-code - 0.2020.0-SNAPSHOT + 0.2021.0-SNAPSHOT - Advent of Code 2020 + Advent of Code 2021 UTF-8 @@ -16,4 +16,27 @@ 17 + + + org.junit.jupiter + junit-jupiter + 5.8.2 + + + + + + + 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..f995e18 100644 --- a/src/test/java/com/macasaet/Day01.java +++ b/src/test/java/com/macasaet/Day01.java @@ -1,58 +1,25 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Spliterator; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +import org.junit.jupiter.api.Test; + 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 Stream getInput() { + return StreamSupport.stream(new LineSpliterator(getClass().getResourceAsStream("/day-1-input.txt")), + false); } - 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; - } - } - } + @Test + public final void part1() { + } - 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 part2() { + } } diff --git a/src/test/java/com/macasaet/Day02.java b/src/test/java/com/macasaet/Day02.java deleted file mode 100644 index 7bfb3d0..0000000 --- a/src/test/java/com/macasaet/Day02.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.regex.Pattern; -import java.util.stream.StreamSupport; - -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); - } - - } - - 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; - - protected Entry(final char c, final String password) { - this.c = c; - this.password = password; - } - - public abstract boolean isValid(); - - } - - 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; - } - - 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 ] )); - } - - public boolean isValid() { - final var count = password.chars() - .filter(candidate -> (char) candidate == this.c) - .count(); - return count >= minIterations && count <= maxIterations; - } - } - - public static class TobogganEntry extends Entry { - private final int firstPosition; - private final int secondPosition; - - public TobogganEntry( final char c, final String password, final int firstPosition, final int secondPosition ) { - super( c, password ); - this.firstPosition = firstPosition; - this.secondPosition = secondPosition; - } - - 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 boolean isValid() { - final var x = password.charAt(firstPosition - 1); - final var y = password.charAt(secondPosition - 1); - return x == c ^ y == c; - } - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day03.java b/src/test/java/com/macasaet/Day03.java deleted file mode 100644 index 344fc9f..0000000 --- a/src/test/java/com/macasaet/Day03.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.Arrays; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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()); - } - - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day04.java b/src/test/java/com/macasaet/Day04.java deleted file mode 100644 index 138675b..0000000 --- a/src/test/java/com/macasaet/Day04.java +++ /dev/null @@ -1,83 +0,0 @@ -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 java.util.stream.StreamSupport; - -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); - } - - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day05.java b/src/test/java/com/macasaet/Day05.java deleted file mode 100644 index dcb1d6a..0000000 --- a/src/test/java/com/macasaet/Day05.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.StreamSupport; - -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); - } - } - 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); - } - } - 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; - } - } - } - - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day06.java b/src/test/java/com/macasaet/Day06.java deleted file mode 100644 index 91a46e6..0000000 --- a/src/test/java/com/macasaet/Day06.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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); - } - } - if (currentGroup != null && !currentGroup.isEmpty()) { - groups.add(currentGroup); - } - 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); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day07.java b/src/test/java/com/macasaet/Day07.java deleted file mode 100644 index 00c3dc8..0000000 --- a/src/test/java/com/macasaet/Day07.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.StreamSupport; - -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)); - } - } - - protected static class Rule { - final String containerColour; - final Map containedCounts; - - public Rule(final String containerColour, final Map containedCounts) { - // TODO validation - this.containerColour = containerColour; - this.containedCounts = containedCounts; - } - - public int countContained(final Map ruleMap) { - return 1 + containedCounts.entrySet().stream().mapToInt(entry -> { - final var containedColour = entry.getKey(); - final int multiplier = entry.getValue(); - - final var subRule = ruleMap.get(containedColour); - final int base = subRule.countContained(ruleMap); - return base * multiplier; - }).sum(); - } - - public boolean canContain(final String colour, final Map ruleMap) { - if (containedCounts.containsKey(colour)) { - return true; - } - 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()); - } - 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)); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day08.java b/src/test/java/com/macasaet/Day08.java deleted file mode 100644 index 290c2c2..0000000 --- a/src/test/java/com/macasaet/Day08.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.HashSet; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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; - } - visited.add(index); - total = instruction.updateTotal(total); - index = instruction.updateIndex(index); - } - System.out.println("part 2: " + total); - return; // "exactly one instruction is corrupted" - } - } - } - - public enum Operation { - acc { - public int updateTotal(final int previousTotal, final int argument) { - return previousTotal + argument; - } - }, - nop, - jmp { - public int updateIndex(final int previousIndex, final int argument) { - return previousIndex + argument; - } - }; - - public int updateIndex(final int previousIndex, final int argument) { - return previousIndex + 1; - } - - public int updateTotal(final int previousTotal, final int argument) { - return previousTotal; - } - } - - public static class Instruction { - private final Operation operation; - private final int argument; - - public Instruction(final Operation operation, final int argument) { - this.operation = operation; - this.argument = argument; - } - - public static Instruction fromLine(final String line) { - final var components = line.split(" ", 2); - return new Instruction(Operation.valueOf(components[0]), Integer.parseInt(components[1])); - } - - public int updateIndex(final int previousIndex) { - return operation.updateIndex(previousIndex, argument); - } - - public int updateTotal(final int previousTotal) { - return operation.updateTotal(previousTotal, argument); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day09.java b/src/test/java/com/macasaet/Day09.java deleted file mode 100644 index ac86ab7..0000000 --- a/src/test/java/com/macasaet/Day09.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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; - } - } - } - } - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day10.java b/src/test/java/com/macasaet/Day10.java deleted file mode 100644 index 66a5189..0000000 --- a/src/test/java/com/macasaet/Day10.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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; - } - System.out.println("Part 1: " + (distribution[0] * distribution[2])); - System.out.println("Part 2: " + count(adapterJoltages, 0, targetJoltage)); - } - } - - private static final Map cache = new HashMap<>(); - - 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; - } - - 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); - } - - 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; - } - - 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); - } - } - cache.put(hash, retval); - return retval; - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day11.java b/src/test/java/com/macasaet/Day11.java deleted file mode 100644 index a74e3e9..0000000 --- a/src/test/java/com/macasaet/Day11.java +++ /dev/null @@ -1,277 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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; - } - System.out.println( "Part 1: " + countOccupiedSeats(current)); - current = copy(original); - while( true ) { - final var transformed = transform2(current); - if (areEqual(current, transformed)) { - break; - } - current = transformed; - } - 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; - } - 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); - } - - } - } - 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++; - } - } - 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)); - } - } - - 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); - } - } - 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; - } - } - } - 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); - } - } - - protected static boolean isOccupied(final int x, final int y, final char[][] matrix) { - if (x < 0 || y < 0 || x >= matrix.length) { - return false; - } - final char[] row = matrix[x]; - if (y >= row.length) { - return false; - } - return row[y] == '#'; - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day12.java b/src/test/java/com/macasaet/Day12.java deleted file mode 100644 index c494842..0000000 --- a/src/test/java/com/macasaet/Day12.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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); - } - break; - default: - throw new IllegalArgumentException("Invalid action: " + action.actionCode); - } - } - 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; - } - 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); - } - } - System.out.println("Part 2: " + (Math.abs(sx) + Math.abs(sy))); - } - } - - protected static class Action { - final char actionCode; - final int value; - - 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); - } - -// 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); -// } -// } - - } -// -// 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 deleted file mode 100644 index cfb7520..0000000 --- a/src/test/java/com/macasaet/Day13.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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); - } - 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)); - } - 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; - } - 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); - } - } - - protected static long findInverse(final long partialProduct, final long modSpace) { - for (long multiplier = 1; ; multiplier++) { - if ((partialProduct * multiplier) % modSpace == 1) { - return multiplier; - } - } - } - - protected static class Bus { - final long busId; - final long index; - - public Bus(final long busId, final long index) { - this.busId = busId; - this.index = index; - } - - } - - protected static class Candidate implements Comparable { - final int busId; - final int timeToWait; - - public Candidate(final int busId, final int timeToWait) { - this.busId = busId; - this.timeToWait = timeToWait; - } - - public int compareTo(Candidate other) { - return Integer.compare(timeToWait, other.timeToWait); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day14.java b/src/test/java/com/macasaet/Day14.java deleted file mode 100644 index a86ae7f..0000000 --- a/src/test/java/com/macasaet/Day14.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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]; - } - } - - 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); - } - for (int i = 36; --i >= 0; ) { - if (mask[i] == '1') { - addressSpec[i] = '1'; - } else if (mask[i] == 'X') { - addressSpec[i] = 'X'; - } - } - final var value = toInt(Integer.parseInt(components[1].strip())); - for (final var address : explode(addressSpec)) { - memory.put(address, value); - } - } - } - sum = memory.values().stream().reduce(BigInteger::add).get(); - System.out.println("Part 2: " + sum); - } - } - - 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)); - } - 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); - } - } - 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); - - var list = new ArrayList(); - - final var copy = Arrays.copyOf(chars, 36); - final var sub = floatingIndices.subList(1, floatingIndices.size()); - - copy[index] = '0'; - list.addAll(explode(copy, sub)); - copy[index] = '1'; - list.addAll(explode(copy, sub)); - - return Collections.unmodifiableList(list); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day15.java b/src/test/java/com/macasaet/Day15.java deleted file mode 100644 index 3de8788..0000000 --- a/src/test/java/com/macasaet/Day15.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; - -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 ); - } - - 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; - } - } - System.out.println( "Part 1: " + last ); - } - } - -} \ 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 deleted file mode 100644 index 61b25aa..0000000 --- a/src/test/java/com/macasaet/Day18.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.LinkedList; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -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); - } - } - - } - 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); - } - } - } - } - if (stack.size() != 1) { - throw new IllegalStateException("Invalid stack: " + stack); - } - sum = sum.add(stack.get(0)); - } - System.out.println("Part 2: " + sum); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day19.java b/src/test/java/com/macasaet/Day19.java deleted file mode 100644 index 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 deleted file mode 100644 index de4daeb..0000000 --- a/src/test/java/com/macasaet/Day20.java +++ /dev/null @@ -1,406 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -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; - -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 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); - } - } - } - counter.accept(sum); - } - - 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; - } - } - } - return true; - } - - 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'; - } - } - } - - 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 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 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); - } - - 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]; - } - flipped[row] = flippedRow; - } - 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"); - } - - 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"); - } - - 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; - } - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day21.java b/src/test/java/com/macasaet/Day21.java deleted file mode 100644 index db1a5c6..0000000 --- a/src/test/java/com/macasaet/Day21.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.Comparator; -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.stream.StreamSupport; - -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); - } - } - - 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; - } - } - if (mappedIngredient) { - for (final var entry : allergenToIngredient.entrySet()) { - final var ingredients = entry.getValue(); - ingredients.remove(ingredient); - } - i.remove(); - } - } - } - - final var result = - ingredientToAllergen - .entrySet() - .stream() - .sorted(Comparator.comparing(Entry::getValue)) - .map(Entry::getKey) - .collect(Collectors.joining(",")); - System.out.println("Part 2: " + result); - } - } - - public static class Food { - private final Set ingredients; - private final Set allergens; - - public Food(final Set ingredients, final Set allergens) { - this.ingredients = ingredients; - this.allergens = allergens; - } - - 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)); - } - - } -} \ 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 deleted file mode 100644 index aa1f327..0000000 --- a/src/test/java/com/macasaet/Day23.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.ArrayList; - -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; - } - } - - 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()); - } - - protected static class Cup { - private final int label; - private Cup next; - - public Cup(final int label) { - this.label = label; - } - - public Cup take() { - final Cup retval = next; - next = retval.next; - retval.next = null; - return retval; - } - - public void linkBefore(final Cup cup) { - next = cup; - } - - public String toString() { - return "Cup{ label=" + label + ", next=" + (next != null ? next.label : null) + " }"; - } - } - -} \ 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 From db781d62e5ebf856ba63c1b8df7e3fee05b064bb Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Tue, 30 Nov 2021 21:37:21 -0800 Subject: [PATCH 02/44] Day 1 --- .gitignore | 3 ++ src/test/java/com/macasaet/Day01.java | 54 ++++++++++++++++--- .../java/com/macasaet/LineSpliterator.java | 22 ++++++-- src/test/resources/sample/day-01.txt | 10 ++++ 4 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 src/test/resources/sample/day-01.txt diff --git a/.gitignore b/.gitignore index 5c56b6a..db33628 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ .settings advent-of-code.iml target/ +src/test/resources/config.properties +src/test/resources/input +src/test/resources/2020 diff --git a/src/test/java/com/macasaet/Day01.java b/src/test/java/com/macasaet/Day01.java index f995e18..0485635 100644 --- a/src/test/java/com/macasaet/Day01.java +++ b/src/test/java/com/macasaet/Day01.java @@ -1,25 +1,65 @@ package com.macasaet; -import java.util.stream.Stream; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; import java.util.stream.StreamSupport; import org.junit.jupiter.api.Test; - +/** + * --- Day 1: Sonar Sweep --- + */ public class Day01 { - protected Stream getInput() { - return StreamSupport.stream(new LineSpliterator(getClass().getResourceAsStream("/day-1-input.txt")), - false); + /** + * Perform a sonar sweep of the nearby sea floor. + * + * @return measurements of the sea floor depth further and further away from the submarine + */ + protected List getInput() { + return StreamSupport + .stream(new LineSpliterator("day-01.txt"), + false) + .mapToInt(Integer::parseInt) + .collect(ArrayList::new, List::add, List::addAll); } @Test public final void part1() { - + final var list = getInput(); + int increases = countIncreases(list); + System.out.println("Part 1: " + increases); } @Test public final void part2() { + final var list = getInput(); + final var windows = new LinkedList(); + for (int i = 2; i < list.size(); i++) { + windows.add(list.get(i) + list.get(i - 1) + list.get(i - 2)); + } + final int increases = countIncreases(windows); + System.out.println("Part 2: " + increases); + } + /** + * Determine how quickly the depth increases. + * + * @param list progressively further measurements of the sea floor depth + * @return the number of times a depth measurement increase from the previous measurement + */ + protected int countIncreases(final List list) { + int previous = list.get(0); + int increases = 0; + for (int i = 1; i < list.size(); i++) { + final var current = list.get(i); + if (current > previous) { + increases++; + } + previous = current; + } + return increases; } -} + +} \ 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..58580ad 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(); diff --git a/src/test/resources/sample/day-01.txt b/src/test/resources/sample/day-01.txt new file mode 100644 index 0000000..167e291 --- /dev/null +++ b/src/test/resources/sample/day-01.txt @@ -0,0 +1,10 @@ +199 +200 +208 +210 +200 +207 +240 +269 +260 +263 From b916f11c6601cc4fcba4f30f861061c0f716fbb1 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Wed, 1 Dec 2021 21:55:30 -0800 Subject: [PATCH 03/44] Day 2 --- .github/workflows/ci.yml | 8 ++ README.md | 7 +- src/test/java/com/macasaet/Day02.java | 110 ++++++++++++++++++ .../java/com/macasaet/LineSpliterator.java | 1 - src/test/resources/sample/day-02.txt | 6 + 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/macasaet/Day02.java create mode 100644 src/test/resources/sample/day-02.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffeb9d9..a8c1a94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,4 +16,12 @@ jobs: distribution: 'temurin' java-version: '17' check-latest: true + - uses: actions/cache@v2 + with: + path: ~/.m2 + key: m2-${{ runner.os }}-${{ matrix.java-version }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + m2-${{ runner.os }}-${{ matrix.java-version }} + m2-${{ runner.os }} + m2 - run: mvn clean install diff --git a/README.md b/README.md index 61666d8..8235f91 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # Advent of Code 2021 (Java) +This is the code I used to solve the +[Advent of Code](https://adventofcode.com/2021) puzzles. The main branch +contains the most recent year in which I participated. Other years have +their own branch. + ## Other Editions -* 2020 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/tree/2020)|[Rust](https://github.com/l0s/advent-of-code-rust)) +* 2020 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/tree/2020) | [Rust](https://github.com/l0s/advent-of-code-rust)) diff --git a/src/test/java/com/macasaet/Day02.java b/src/test/java/com/macasaet/Day02.java new file mode 100644 index 0000000..f5a2e9f --- /dev/null +++ b/src/test/java/com/macasaet/Day02.java @@ -0,0 +1,110 @@ +package com.macasaet; + +import java.util.Locale; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 2: Dive! --- + */ +public class Day02 { + + public enum Operation { + FORWARD { + public Position adjust(Position position, int magnitude) { + return new Position(position.horizontalPosition() + magnitude, position.depth()); + } + + public OrientedPosition adjust(OrientedPosition position, int magnitude) { + return new OrientedPosition(position.horizontalPosition() + magnitude, + position.depth() + (position.aim() * magnitude), + position.aim()); + } + }, + DOWN { + public Position adjust(Position position, int magnitude) { + return new Position(position.horizontalPosition(), position.depth() + magnitude); + } + + public OrientedPosition adjust(OrientedPosition position, int magnitude) { + return new OrientedPosition(position.horizontalPosition(), + position.depth(), + position.aim() + magnitude); + } + }, + UP { + public Position adjust(Position position, int magnitude) { + return new Position(position.horizontalPosition(), position.depth() - magnitude); + } + + public OrientedPosition adjust(OrientedPosition position, int magnitude) { + return new OrientedPosition(position.horizontalPosition(), + position.depth(), + position.aim() - magnitude); + } + }; + + public abstract Position adjust(Position position, int magnitude); + + public abstract OrientedPosition adjust(OrientedPosition position, int magnitude); + } + + public record Command(Operation operation, int magnitude) { + public static Command parse(final String string) { + final String[] components = string.split(" "); + final var operation = Operation.valueOf(components[0].toUpperCase(Locale.US)); + final int magnitude = Integer.parseInt(components[1]); + return new Command(operation, magnitude); + } + + public Position adjust(final Position position) { + return operation().adjust(position, magnitude()); + } + + public OrientedPosition adjust(final OrientedPosition position) { + return operation().adjust(position, magnitude()); + } + } + + public record Position(int horizontalPosition, int depth) { + public int result() { + return horizontalPosition() * depth(); + } + } + + public record OrientedPosition(int horizontalPosition, int depth, int aim) { + public int result() { + return horizontalPosition() * depth(); + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-02.txt"), + false) + .map(Command::parse); + } + + @Test + public final void part1() { + var position = new Position(0, 0); + for (final var i = getInput().iterator(); i.hasNext(); ) { + final var operation = i.next(); + position = operation.adjust(position); + } + System.out.println("Part 1: " + position.result()); + } + + @Test + public final void part2() { + var position = new OrientedPosition(0, 0, 0); + for (final var i = getInput().iterator(); i.hasNext(); ) { + final var operation = i.next(); + position = operation.adjust(position); + } + System.out.println("Part 2: " + position.result()); + } + +} \ 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 58580ad..6e3c360 100644 --- a/src/test/java/com/macasaet/LineSpliterator.java +++ b/src/test/java/com/macasaet/LineSpliterator.java @@ -47,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-02.txt b/src/test/resources/sample/day-02.txt new file mode 100644 index 0000000..b7172ac --- /dev/null +++ b/src/test/resources/sample/day-02.txt @@ -0,0 +1,6 @@ +forward 5 +down 5 +forward 8 +up 3 +down 8 +forward 2 From 0d5179a3d8335c24d08da18ecf84e935e7f28ff7 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Thu, 2 Dec 2021 22:10:22 -0800 Subject: [PATCH 04/44] Day 3 --- src/test/java/com/macasaet/Day03.java | 120 ++++++++++++++++++++++++++ src/test/resources/sample/day-03.txt | 12 +++ 2 files changed, 132 insertions(+) create mode 100644 src/test/java/com/macasaet/Day03.java create mode 100644 src/test/resources/sample/day-03.txt diff --git a/src/test/java/com/macasaet/Day03.java b/src/test/java/com/macasaet/Day03.java new file mode 100644 index 0000000..47f6ec5 --- /dev/null +++ b/src/test/java/com/macasaet/Day03.java @@ -0,0 +1,120 @@ +package com.macasaet; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 3: Binary Diagnostic --- + */ +public class Day03 { + + /** + * @return "a list of binary numbers which, when decoded properly, can tell you many useful things about the + * conditions of the submarine" + */ + protected Stream getDiagnosticReport() { + return StreamSupport + .stream(new LineSpliterator("day-03.txt"), + false) + .map(string -> { + final var chars = string.toCharArray(); + final var bits = new byte[chars.length]; + for (int i = chars.length; --i >= 0; bits[i] = chars[i] == '0' ? (byte) 0 : (byte) 1) ; + return bits; + }); + } + + protected int toUnsignedInt(final byte[] bits) { + int result = 0; + for (int i = bits.length; --i >= 0; result += bits[i] * Math.pow(2, bits.length - i - 1)) ; + return result; + } + + @Test + public final void part1() { + final var list = getDiagnosticReport().collect(Collectors.toList()); + final int width = list.get(0).length; + final int[] zeroCounts = new int[width]; + for (int i = zeroCounts.length; --i >= 0; zeroCounts[i] = 0) ; + final int[] oneCounts = new int[width]; + for (int i = oneCounts.length; --i >= 0; oneCounts[i] = 0) ; + for (final var array : list) { + for (int j = 0; j < width; j++) { + if (array[j] == 0) { + zeroCounts[j] += 1; + } else { + oneCounts[j] += 1; + } + } + } + final byte[] gammaArray = new byte[width]; + final byte[] epsilonArray = new byte[width]; + for (int i = gammaArray.length; --i >= 0; ) { + gammaArray[i] = zeroCounts[i] > oneCounts[i] ? (byte) 0 : (byte) 1; + epsilonArray[i] = zeroCounts[i] > oneCounts[i] ? (byte) 1 : (byte) 0; + } + + final int gammaRate = toUnsignedInt(gammaArray); + final int epsilonRate = toUnsignedInt(epsilonArray); + System.out.println("Part 1: " + (gammaRate * epsilonRate)); + } + + @Test + public final void part2() { + final var list = getDiagnosticReport().collect(Collectors.toList()); + final int width = list.get(0).length; + List oxygenCandidates = new ArrayList<>(list); + for (int i = 0; i < width && oxygenCandidates.size() > 1; i++) { + int zeros = 0; + int ones = 0; + for (final var value : oxygenCandidates) { + if (value[i] == 0) { + zeros++; + } else { + ones++; + } + } + final int index = i; + if (ones >= zeros) { + oxygenCandidates = oxygenCandidates.stream().filter(value -> value[index] == 1).collect(Collectors.toList()); + } else { + oxygenCandidates = oxygenCandidates.stream().filter(value -> value[index] == 0).collect(Collectors.toList()); + } + } + if (oxygenCandidates.size() > 1) { + throw new IllegalStateException("Too many oxygen candidates"); + } + List co2Candidates = new ArrayList<>(list); + for (int i = 0; i < width && co2Candidates.size() > 1; i++) { + int zeros = 0; + int ones = 0; + for (final var value : co2Candidates) { + if (value[i] == 0) { + zeros++; + } else { + ones++; + } + } + final int index = i; + if (zeros <= ones) { + co2Candidates = co2Candidates.stream().filter(value -> value[index] == 0).collect(Collectors.toList()); + } else { + co2Candidates = co2Candidates.stream().filter(value -> value[index] == 1).collect(Collectors.toList()); + } + } + if (co2Candidates.size() > 1) { + throw new IllegalStateException("Too many CO2 candidates"); + } + final byte[] oxyArray = oxygenCandidates.get(0); + final byte[] co2Array = co2Candidates.get(0); + final int oxygenGeneratorRating = toUnsignedInt(oxyArray); + final int co2ScrubberRating = toUnsignedInt(co2Array); + System.out.println("Part 2: " + (oxygenGeneratorRating * co2ScrubberRating)); + } + +} \ No newline at end of file diff --git a/src/test/resources/sample/day-03.txt b/src/test/resources/sample/day-03.txt new file mode 100644 index 0000000..a6366a8 --- /dev/null +++ b/src/test/resources/sample/day-03.txt @@ -0,0 +1,12 @@ +00100 +11110 +10110 +10111 +10101 +01111 +00111 +11100 +10000 +11001 +00010 +01010 From 1a9177a5ed36c1812f0cbbaa4a1da7217297a398 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Fri, 3 Dec 2021 22:31:03 -0800 Subject: [PATCH 05/44] Day 4 --- src/test/java/com/macasaet/Day04.java | 181 ++++++++++++++++++++++++++ src/test/resources/sample/day-04.txt | 19 +++ 2 files changed, 200 insertions(+) create mode 100644 src/test/java/com/macasaet/Day04.java create mode 100644 src/test/resources/sample/day-04.txt diff --git a/src/test/java/com/macasaet/Day04.java b/src/test/java/com/macasaet/Day04.java new file mode 100644 index 0000000..d69541d --- /dev/null +++ b/src/test/java/com/macasaet/Day04.java @@ -0,0 +1,181 @@ +package com.macasaet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 4: Giant Squid --- + */ +public class Day04 { + + /** + * The number of rows and columns on each square Bingo card + */ + static final int EDGE_LENGTH = 5; + + public static class Board { + private final int[][] grid; + private final boolean[][] marked; + + protected Board(final int[][] grid, final boolean[][] marked) { + this.grid = grid; + this.marked = marked; + } + + public Board(final int[][] grid) { + this(grid, new boolean[grid.length][]); + for (int i = grid.length; --i >= 0; this.marked[i] = new boolean[grid.length]) ; + } + + public boolean isWinner() { + // check rows + for (int i = marked.length; --i >= 0; ) { + final var row = marked[i]; + boolean complete = true; + for (int j = row.length; --j >= 0 && complete; complete = row[j]) ; + if (complete) { + return true; + } + } + // check columns + for (int j = marked.length; --j >= 0; ) { + boolean complete = true; + for (int i = marked.length; --i >= 0 && complete; complete = marked[i][j]) ; + if (complete) { + return true; + } + } + return false; + } + + public int score(final int lastDrawn) { + int sum = 0; + for (int i = grid.length; --i >= 0; ) { + final var row = grid[i]; + for (int j = row.length; --j >= 0; ) { + if (!marked[i][j]) { + sum += row[j]; + } + } + } + return sum * lastDrawn; + } + + public void mark(final int drawn) { + for (int i = grid.length; --i >= 0; ) { + final var row = grid[i]; + for (int j = row.length; --j >= 0; ) { + if (row[j] == drawn) { + marked[i][j] = true; + } + } + } + } + } + + public record Game(List boards, List numbers) { + + public int countBoards() { + return boards().size(); + } + + public void removeBoard(final int index) { + this.boards().remove(index); + } + + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-04.txt"), + false); + } + + protected Game getGame() { + final var input = getInput().iterator(); + final var moves = input.next(); + + List boards = new ArrayList<>(); + int[][] grid = null; + int gridIndex = -1; + while (input.hasNext()) { + final var line = input.next(); + if (line.isBlank()) { + if (grid != null) { + boards.add(new Board(grid)); + } + grid = new int[EDGE_LENGTH][]; + for (int i = EDGE_LENGTH; --i >= 0; grid[i] = new int[EDGE_LENGTH]) ; + gridIndex = 0; + continue; + } + final var cells = line.split("\\s"); + if (cells.length > 0) { + final var values = Arrays.stream(cells) + .filter(candidate -> !candidate.isBlank()) + .mapToInt(Integer::parseInt) + .toArray(); + if (values.length > 0) { + grid[gridIndex++] = values; + } + } + } + if (grid != null) { + boards.add(new Board(grid)); + } + final var moveArray = Arrays.stream(moves.split(",")) + .mapToInt(Integer::parseInt) + .collect(ArrayList::new, List::add, List::addAll); + return new Game(boards, moveArray); + } + + @Test + public final void part1() { + final var game = getGame(); + for (final var number : game.numbers()) { + for (final var board : game.boards()) { + board.mark(number); + if (board.isWinner()) { + final int score = board.score(number); + System.out.println("Part 1: " + score); + return; + } + } + } + throw new IllegalStateException("No winners"); + } + + @Test + public final void part2() { + final var game = getGame(); + for (final var number : game.numbers()) { + if (game.countBoards() == 1) { + final var lastWinner = game.boards().get(0); + lastWinner.mark(number); + if (!lastWinner.isWinner()) { + continue; + } + System.out.println("Part 2: " + lastWinner.score(number)); + return; + } + final List idsToRemove = new ArrayList<>(); + for (int i = game.boards().size(); --i >= 0; ) { + final var board = game.boards().get(i); + board.mark(number); + if (board.isWinner()) { + idsToRemove.add(i); + } + } + for (final var id : idsToRemove) { + game.removeBoard(id); + } + } + throw new IllegalStateException("Tie for last place"); + } + +} \ 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..669a51d --- /dev/null +++ b/src/test/resources/sample/day-04.txt @@ -0,0 +1,19 @@ +7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1 + +22 13 17 11 0 + 8 2 23 4 24 +21 9 14 16 7 + 6 10 3 18 5 + 1 12 20 15 19 + + 3 15 0 2 22 + 9 18 13 17 5 +19 8 7 25 23 +20 11 10 24 4 +14 21 16 12 6 + +14 21 17 24 4 +10 16 15 9 19 +18 8 23 26 20 +22 11 13 6 5 + 2 0 12 3 7 From 7c8ccb9ce926e5fe21908df2af21cb89b7089d28 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sat, 4 Dec 2021 22:34:31 -0800 Subject: [PATCH 06/44] Day 5 --- src/test/java/com/macasaet/Day05.java | 188 ++++++++++++++++++++++++++ src/test/resources/sample/day-05.txt | 10 ++ 2 files changed, 198 insertions(+) create mode 100644 src/test/java/com/macasaet/Day05.java create mode 100644 src/test/resources/sample/day-05.txt diff --git a/src/test/java/com/macasaet/Day05.java b/src/test/java/com/macasaet/Day05.java new file mode 100644 index 0000000..bb23695 --- /dev/null +++ b/src/test/java/com/macasaet/Day05.java @@ -0,0 +1,188 @@ +package com.macasaet; + +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 5: Hydrothermal Venture --- + */ +public class Day05 { + + /** + * A point on the ocean floor + */ + public static record Point(int x, int y) { + public static Point parse(final String string) { + final var components = string.split(","); + return new Point(Integer.parseInt(components[0]), Integer.parseInt(components[1])); + } + + } + + /** + * A portion of the ocean floor on which there are hydrothermal vents, which produce large, opaque clouds + */ + public static record LineSegment(Point start, Point end) { + public static LineSegment parse(final String string) { + final var components = string.split(" -> "); + return new LineSegment(Point.parse(components[0]), Point.parse(components[1])); + } + + /** + * Highlight the location of this line segment on the diagram. Each cell that this segment covers will have its + * value incremented. The higher the number, the more vents cover the cell. + * + * @param diagram A map of the ocean floor which will be updated by this call. + */ + public void update(final int[][] diagram) { + /* + "Because of the limits of the hydrothermal vent mapping system, the lines in your list will only ever be + horizontal, vertical, or a diagonal line at exactly 45 degrees." + */ + final int horizontalStep = start().x() == end().x() + ? 0 + : start().x() < end().x() + ? 1 + : -1; + final int verticalStep = start().y() == end().y() + ? 0 + : start().y() < end().y() + ? 1 + : -1; + final Predicate xTester = start().x() == end().x() + ? x -> true + : start().x() < end().x() + ? x -> x <= end().x() + : x -> x >= end().x(); + final Predicate yTester = start().y() == end().y() + ? y -> true + : start().y() < end().y() + ? y -> y <= end().y() + : y -> y >= end().y(); + + for (int i = start().x(), j = start().y(); + xTester.test(i) && yTester.test(j); + i += horizontalStep, j += verticalStep) { + diagram[i][j]++; + } + } + + public int lowestX() { + return Math.min(start().x(), end().x()); + } + + public int highestX() { + return Math.max(start().x(), end().x()); + } + + public int lowestY() { + return Math.min(start().y(), end().y()); + } + + public int highestY() { + return Math.max(start().y(), end().y()); + } + + public String toString() { + return start() + " -> " + end(); + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-05.txt"), + false) + .map(LineSegment::parse); + } + + protected record Extremes(int lowestX, int lowestY, int highestX, int highestY) { + public Extremes combine(final LineSegment segment) { + return new Extremes(Math.min(lowestX(), segment.lowestX()), + Math.min(lowestY(), segment.lowestY()), + Math.max(highestX(), segment.highestX()), + Math.max(highestY(), segment.highestY())); + } + + public Extremes combine(final Extremes other) { + return new Extremes(Math.min(lowestX(), other.lowestX()), + Math.min(lowestY(), other.lowestY()), + Math.max(highestX(), other.highestX()), + Math.max(highestY(), other.highestY())); + } + + public int[][] createBlankDiagram() { + final int[][] result = new int[highestX() + 1][]; + for (int i = result.length; --i >= 0; result[i] = new int[highestY() + 1]) ; + return result; + } + } + + @Test + public final void part1() { + final var segments = getInput() + // "For now, only consider horizontal and vertical lines: lines where either x1 = x2 or y1 = y2." + .filter(segment -> segment.start().x() == segment.end().x() || segment.start().y() == segment.end().y()) + .collect(Collectors.toList()); + + final var extremes = segments + .stream() + .reduce(new Extremes(0, 0, 0, 0), + Extremes::combine, + Extremes::combine); + // there are no negative values + // Note, we could save a little bit of space and time by using a smaller map since none of the line segments + // need point 0,0. However, the savings are likely negligible. + final int[][] diagram = extremes.createBlankDiagram(); + + for (final var segment : segments) { + segment.update(diagram); + } + int sum = 0; + for (int i = diagram.length; --i >= 0; ) { + final var row = diagram[i]; + for (int j = row.length; --j >= 0; ) { + if (row[j] >= 2) { + sum++; + } + } + } + System.out.println("Part 1: " + sum); + } + + @Test + public final void part2() { + /* + "Unfortunately, considering only horizontal and vertical lines doesn't give you the full picture; you need to + also consider diagonal lines." + */ + final var segments = getInput() + .collect(Collectors.toList()); + final var extremes = segments + .stream() + .reduce(new Extremes(0, 0, 0, 0), + Extremes::combine, + Extremes::combine); + // there are no negative values + // Note, we could save a little bit of space and time by using a smaller map since none of the line segments + // need point 0,0. However, the savings are likely negligible. + final int[][] diagram = extremes.createBlankDiagram(); + for (final var segment : segments) { + segment.update(diagram); + } + int sum = 0; + for (int i = diagram.length; --i >= 0; ) { + final var row = diagram[i]; + for (int j = row.length; --j >= 0; ) { + if (row[j] >= 2) { + sum++; + } + } + } + System.out.println("Part 2: " + sum); + } + +} \ 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..b258f68 --- /dev/null +++ b/src/test/resources/sample/day-05.txt @@ -0,0 +1,10 @@ +0,9 -> 5,9 +8,0 -> 0,8 +9,4 -> 3,4 +2,2 -> 2,1 +7,0 -> 7,4 +6,4 -> 2,0 +0,9 -> 2,9 +3,4 -> 1,4 +0,0 -> 8,8 +5,5 -> 8,2 From fb909b599acbd99ee4aaafff711410fb285a85bf Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sun, 5 Dec 2021 21:50:25 -0800 Subject: [PATCH 07/44] Day 6 --- src/test/java/com/macasaet/Day06.java | 120 ++++++++++++++++++++++++++ src/test/resources/sample/day-06.txt | 1 + 2 files changed, 121 insertions(+) create mode 100644 src/test/java/com/macasaet/Day06.java create mode 100644 src/test/resources/sample/day-06.txt diff --git a/src/test/java/com/macasaet/Day06.java b/src/test/java/com/macasaet/Day06.java new file mode 100644 index 0000000..10a59b3 --- /dev/null +++ b/src/test/java/com/macasaet/Day06.java @@ -0,0 +1,120 @@ +package com.macasaet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 6: Lanternfish --- + */ +public class Day06 { + + /** + * A glowing fish that spawns very quickly. Their population grows exponentially. + */ + public static class Lanternfish { + + private int daysToSpawn; + + /** + * @param daysToSpawn the number of days until it creates a new {@link Lanternfish} + */ + public Lanternfish(final int daysToSpawn) { + setDaysToSpawn(daysToSpawn); + } + + /** + * Simulate the passage of one day + * + * @return either a new Lanternfish or nothing depending on whether the fish spawned + */ + public Optional tick() { + final var timer = getDaysToSpawn() - 1; + if (timer < 0) { + setDaysToSpawn(6); + return Optional.of(new Lanternfish(8)); + } else { + setDaysToSpawn(timer); + return Optional.empty(); + } + } + + /** + * @return the number of days until the fish spawns + */ + public int getDaysToSpawn() { + return this.daysToSpawn; + } + + /** + * Update this fish's days to spawn + * + * @param daysToSpawn the number of days until the fish spawns, must be non-negative + */ + protected void setDaysToSpawn(final int daysToSpawn) { + if (daysToSpawn < 0) { + throw new IllegalArgumentException("\"days to spawn\" must be non-negative"); + } + this.daysToSpawn = daysToSpawn; + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-06.txt"), + false); + } + + public List parseInput() { + final var list = getInput().toList(); + final var line = list.get(0); + final var components = line.split(","); + return Arrays.stream(components) + .mapToInt(Integer::parseInt) + .mapToObj(Lanternfish::new) + .collect(Collectors.toList()); + } + + @Test + public final void part1() { + var population = parseInput(); + for (int _i = 80; --_i >= 0; ) { + final List list = new ArrayList<>(population); + for (final var fish : population) { + final var result = fish.tick(); + result.ifPresent(list::add); + } + population = list; + } + System.out.println("Part 1: " + population.size()); + } + + @Test + public final void part2() { + final var initial = parseInput(); + var map = new long[9]; + for (final var fish : initial) { + map[fish.getDaysToSpawn()]++; + } + for (int _i = 256; --_i >= 0; ) { + final var temp = new long[map.length]; + for (int daysToSpawn = map.length; --daysToSpawn >= 0; ) { + final var count = map[daysToSpawn]; + final var prototype = new Lanternfish(daysToSpawn); + final var result = prototype.tick(); + temp[prototype.getDaysToSpawn()] += count; + result.ifPresent(spawn -> temp[spawn.getDaysToSpawn()] = temp[spawn.getDaysToSpawn()] + count); + } + map = temp; + } + final var result = Arrays.stream(map).reduce(0L, Long::sum); + System.out.println("Part 2: " + result); + } + +} \ 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..55129f1 --- /dev/null +++ b/src/test/resources/sample/day-06.txt @@ -0,0 +1 @@ +3,4,3,1,2 From 6ec140fc5b9f558770c64de4573af89c42e59dbb Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Mon, 6 Dec 2021 21:24:10 -0800 Subject: [PATCH 08/44] Day 7 --- src/test/java/com/macasaet/Day07.java | 113 ++++++++++++++++++++++++++ src/test/resources/sample/day-07.txt | 1 + 2 files changed, 114 insertions(+) create mode 100644 src/test/java/com/macasaet/Day07.java create mode 100644 src/test/resources/sample/day-07.txt diff --git a/src/test/java/com/macasaet/Day07.java b/src/test/java/com/macasaet/Day07.java new file mode 100644 index 0000000..4393d46 --- /dev/null +++ b/src/test/java/com/macasaet/Day07.java @@ -0,0 +1,113 @@ +package com.macasaet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 7: The Treachery of Whales --- + */ +public class Day07 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-07.txt"), + false); + } + + /** + * @return the horizontal position of each crab submarine in the swarm + */ + protected List getCrabPositions() { + final var list = getInput().collect(Collectors.toList()); + final var line = list.get(0); + return Arrays.stream(line.split(",")) + .mapToInt(Integer::parseInt) + .collect(ArrayList::new, List::add, List::addAll); + } + + /** + * Assuming a constant fuel consumption rate, calculate the fuel required for the swarm to reach alignmentPoint. + * + * @param positions the starting position of each crab submarine + * @param alignmentPoint a potential point for the crabs to gather in order to blast a hole in the ocean floor + * @return the fuel required to reach the alignment point + */ + protected int calculateConstantFuel(final Iterable positions, final int alignmentPoint) { + int sum = 0; + for (final var position : positions) { + sum += Math.abs(alignmentPoint - position); + } + return sum; + } + + /** + * Calculate the fuel required for the swarm to reach alignmentPoint + * + * @param positions the starting position for each crab submarine + * @param alignmentPoint a potential point for the crabs to gather in order to blast a hole in the ocean floor + * @return the fuel required to reach the alignment point + */ + protected int calculateFuel(final Iterable positions, final int alignmentPoint) { + int sum = 0; + for (final var position : positions) { + sum += calculateFuel(position, alignmentPoint); + } + return sum; + } + + /** + * Calculate the fuel required for a single crab submarine to travel from one horizontal position to the next. + * + * @param start the starting position (inclusive) + * @param end the ending position (inclusive) + * @return the amount of fuel consumed in the journey + */ + protected int calculateFuel(final int start, final int end) { + final int target = Math.abs(end - start); + int sum = 0; + for (int i = target; --i >= 0; ) { + sum += i + 1; + } + return sum; + } + + @Test + public final void part1() { + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + final var positions = getCrabPositions(); + for (final var position : positions) { + min = Math.min(min, position); + max = Math.max(max, position); + } + final int result = IntStream.range(min, max) + .map(alignmentPoint -> calculateConstantFuel(positions, alignmentPoint)) + .min() + .getAsInt(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + int min = Integer.MAX_VALUE; + int max = Integer.MIN_VALUE; + final var positions = getCrabPositions(); + for (final var position : positions) { + min = Math.min(min, position); + max = Math.max(max, position); + } + final int result = IntStream.range(min, max) + .map(alignmentPoint -> calculateFuel(positions, alignmentPoint)) + .min() + .getAsInt(); + System.out.println("Part 2: " + result); + } + +} \ 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..18bd32a --- /dev/null +++ b/src/test/resources/sample/day-07.txt @@ -0,0 +1 @@ +16,1,2,0,4,2,7,1,2,14 From 721dc5355b6477dc664932b424b6d5e9497ac9ce Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Wed, 8 Dec 2021 00:26:07 -0800 Subject: [PATCH 09/44] Day 8 --- src/test/java/com/macasaet/Day08.java | 225 ++++++++++++++++++++++++++ src/test/resources/sample/day-08.txt | 10 ++ 2 files changed, 235 insertions(+) create mode 100644 src/test/java/com/macasaet/Day08.java create mode 100644 src/test/resources/sample/day-08.txt diff --git a/src/test/java/com/macasaet/Day08.java b/src/test/java/com/macasaet/Day08.java new file mode 100644 index 0000000..7ac8b58 --- /dev/null +++ b/src/test/java/com/macasaet/Day08.java @@ -0,0 +1,225 @@ +package com.macasaet; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 8: Seven Segment Search --- + */ +public class Day08 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-08.txt"), + false); + } + + public record Digit(List segments) { + public Digit decode(final Map map) { + return new Digit(segments().stream() + .map(map::get) + .collect(Collectors.toUnmodifiableList())); + } + + public int asInt() { + if (segments().size() == 6 && hasSegments('a', 'b', 'c', 'e', 'f', 'g')) { // FIXME use on/off mechanism + return 0; + } else if (segments().size() == 2 && hasSegments('c', 'f')) { + return 1; + } else if (segments().size() == 5 && hasSegments('a', 'c', 'd', 'e', 'g')) { + return 2; + } else if (segments().size() == 5 && hasSegments('a', 'c', 'd', 'f', 'g')) { + return 3; + } else if (segments().size() == 4 && hasSegments('b', 'c', 'd', 'f')) { + return 4; + } else if (segments().size() == 5 && hasSegments('a', 'b', 'd', 'f', 'g')) { + return 5; + } else if (segments().size() == 6 && hasSegments('a', 'b', 'd', 'e', 'f', 'g')) { + return 6; + } else if (segments().size() == 3 && hasSegments('a', 'c', 'f')) { + return 7; + } else if (segments().size() == 7 && hasSegments('a', 'b', 'c', 'd', 'e', 'f', 'g')) { + return 8; + } else if (segments().size() == 6 && hasSegments('a', 'b', 'c', 'd', 'f', 'g')) { + return 9; + } + throw new IllegalStateException("Invalid Digit: " + this); + } + + public boolean hasSegments(final char... segments) { + for (final var segment : segments) { + if (!hasSegment(segment)) { + return false; + } + } + return true; + } + + public boolean hasSegment(final char segment) { + return segments().contains(segment); + } + + public static Digit parse(final String string) { + final var array = string.toCharArray(); + final var list = new ArrayList(array.length); + for (final var c : array) { + list.add(c); + } + return new Digit(Collections.unmodifiableList(list)); + } + } + + public static record Entry(List uniqueSignalPatterns, List outputValue) { + + public int decodeOutput() { + final var map = buildDecodingMap(); + final StringBuilder builder = new StringBuilder(); + for (final var outputDigit : outputValue()) { + final var decodedDigit = outputDigit.decode(map); + final int digit = decodedDigit.asInt(); + builder.append(digit); + } + final String stringInt = builder.toString(); + return Integer.parseInt(stringInt); + } + + protected SortedSet getDigitSegmentsWithCount(final int n) { + return uniqueSignalPatterns().stream() + .filter(digit -> digit.segments().size() == n) + .findFirst() + .get() + .segments() + .stream() + .collect(TreeSet::new, SortedSet::add, SortedSet::addAll); + } + + protected Set getDigitsWithCount(final int n) { // TODO return stream + return uniqueSignalPatterns() + .stream() + .filter(digit -> digit.segments().size() == n).collect(Collectors.toUnmodifiableSet()); + } + + public Map buildDecodingMap() { + final var encodingMap = buildEncodingMap(); + final var result = new HashMap(); + for(final var entry : encodingMap.entrySet()) { + result.put(entry.getValue(), entry.getKey()); + } + return Collections.unmodifiableMap(result); + } + + public Map buildEncodingMap() { + final var map = new HashMap(); + final var oneSegments = getDigitSegmentsWithCount(2); + final var sevenSegments = getDigitSegmentsWithCount(3); + final var fourSegments = getDigitSegmentsWithCount(4); + final var eightSegments = getDigitSegmentsWithCount(7); + final var aMapping = sevenSegments.stream().filter(c -> !oneSegments.contains(c)).findFirst().get(); + map.put('a', aMapping); + + final var zeroSixNine = getDigitsWithCount(6); + var zsnSegments = zeroSixNine.stream().flatMap(digit -> digit.segments().stream()).collect(Collectors.toList()); + zsnSegments.removeIf(sevenSegments::contains); + zsnSegments.removeIf(fourSegments::contains); + final var sssMap = new HashMap(); + for (final var c : zsnSegments) { + sssMap.compute(c, (_key, old) -> old == null ? 1 : old + 1); + } + if(sssMap.size() != 2) { + throw new IllegalStateException("More segments for 0, 6, 9 encountered: " + sssMap); + } + for (final var entry : sssMap.entrySet()) { + if (entry.getValue() == 3) { + map.put('g', entry.getKey()); + } else if (entry.getValue() == 2) { + map.put('e', entry.getKey()); + } else { + throw new IllegalStateException(); + } + } + + final var twoFiveThree = getDigitsWithCount(5); + var tftSegments = twoFiveThree.stream().flatMap(digit -> digit.segments.stream()).collect(Collectors.toList()); + tftSegments.removeIf(sevenSegments::contains); + tftSegments.removeIf(candidate -> candidate.equals(map.get('e'))); + tftSegments.removeIf(candidate -> candidate.equals(map.get('g'))); + final var tftCounts = new HashMap(); + for(final var c : tftSegments) { + tftCounts.compute(c, (_key, old) -> old == null ? 1 : old + 1); + } + for(final var entry : tftCounts.entrySet()) { + if(entry.getValue() == 3) { + map.put('d', entry.getKey()); + } else if(entry.getValue() == 1) { + map.put('b', entry.getKey()); + } else { + throw new IllegalStateException(); + } + } + + zsnSegments = zeroSixNine.stream().flatMap(digit -> digit.segments().stream()).collect(Collectors.toList()); + zsnSegments.removeIf(candidate -> candidate.equals(map.get('a'))); + zsnSegments.removeIf(candidate -> candidate.equals(map.get('b'))); + zsnSegments.removeIf(candidate -> candidate.equals(map.get('d'))); + zsnSegments.removeIf(candidate -> candidate.equals(map.get('e'))); + zsnSegments.removeIf(candidate -> candidate.equals(map.get('g'))); + final var zsnCounts = new HashMap(); + for(final var c : zsnSegments) { + zsnCounts.compute(c, (_key, old) -> old == null ? 1 : old + 1); + } + for(final var entry : zsnCounts.entrySet()) { + if(entry.getValue() == 2) { + map.put('c', entry.getKey()); + } else if( entry.getValue() == 3) { + map.put('f', entry.getKey()); + } else { + throw new IllegalStateException(); + } + } + + return map; + } + + public static Entry parse(final String string) { + final var components = string.split(" \\| "); + final var uniqueSignalPatterns = components[0].split(" "); + final var outputValue = components[1].split(" "); + + return new Entry(Arrays.stream(uniqueSignalPatterns) + .map(Digit::parse) + .collect(Collectors.toUnmodifiableList()), + Arrays.stream(outputValue) + .map(Digit::parse) + .collect(Collectors.toUnmodifiableList())); + } + + } + + @Test + public final void part1() { + final var result = getInput() + .map(Entry::parse) + .flatMap(entry -> entry.outputValue().stream()) + .filter(digit -> { + final var segments = digit.segments(); + final var numSegments = segments.size(); + return numSegments == 2 || numSegments == 4 || numSegments == 3 || numSegments == 7; + }) + .count(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var result = getInput() + .map(Entry::parse) + .mapToInt(Entry::decodeOutput).sum(); + + System.out.println("Part 2: " + result); + } + +} \ 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..c9f629b --- /dev/null +++ b/src/test/resources/sample/day-08.txt @@ -0,0 +1,10 @@ +be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe +edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc +fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg +fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb +aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea +fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb +dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe +bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef +egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb +gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce From 84d347b68d79b12bd0bc6ba9fe40258a7834f277 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Wed, 8 Dec 2021 22:42:50 -0800 Subject: [PATCH 10/44] Day 9 --- src/test/java/com/macasaet/Day09.java | 177 ++++++++++++++++++++++++++ src/test/resources/sample/day-09.txt | 5 + 2 files changed, 182 insertions(+) create mode 100644 src/test/java/com/macasaet/Day09.java create mode 100644 src/test/resources/sample/day-09.txt diff --git a/src/test/java/com/macasaet/Day09.java b/src/test/java/com/macasaet/Day09.java new file mode 100644 index 0000000..abd2f4c --- /dev/null +++ b/src/test/java/com/macasaet/Day09.java @@ -0,0 +1,177 @@ +package com.macasaet; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 9: Smoke Basin --- + */ +public class Day09 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-09.txt"), + false); + } + + public HeightMap getHeightMap() { + final var list = getInput().map(line -> { + final var chars = line.toCharArray(); + final var ints = new int[chars.length]; + for (int i = chars.length; --i >= 0; ints[i] = chars[i] - '0') ; + return ints; + }).collect(Collectors.toList()); + final int[][] grid = new int[list.size()][]; + for (int i = list.size(); --i >= 0; grid[i] = list.get(i)) ; + return new HeightMap(grid); + } + + /** + * A height map of the floor of the nearby caves generated by the submarine + */ + public record HeightMap(int[][] grid) { // FIXME use bytes + + public Stream points() { + return IntStream.range(0, grid().length) + .boxed() + .flatMap(i -> IntStream.range(0, grid()[i].length) + .mapToObj(j -> new Point(i, j))); + } + + /** + * A location on the floor of a nearby cave + */ + public class Point { + final int x; + final int y; + + public Point(final int x, final int y) { + this.x = x; + this.y = y; + } + + public int x() { + return this.x; + } + + public int y() { + return this.y; + } + + public int getBasinSize() { + return getBasinPoints().size(); + } + + /** + * Identify all the higher points that are also part of the same basin, assuming this location is part of a + * basin. + * + * @return all the higher points, if any, that are part of the same basin. + */ + public Set getBasinPoints() { + if (getHeight() >= 9) { + return Collections.emptySet(); + } + final var result = new HashSet(); + result.add(this); + final Function> basinPointRetriever = neighbour -> { + if (neighbour.getHeight() >= 9 || neighbour.getHeight() <= getHeight() || result.contains(neighbour)) { + return Stream.empty(); + } + return neighbour.getBasinPoints().stream(); + }; + above().stream().flatMap(basinPointRetriever).forEach(result::add); + below().stream().flatMap(basinPointRetriever).forEach(result::add); + left().stream().flatMap(basinPointRetriever).forEach(result::add); + right().stream().flatMap(basinPointRetriever).forEach(result::add); + return Collections.unmodifiableSet(result); + } + + /** + * @return true if and only if this location is lower than all of its adjacent locations (up to four, + * diagonals do not count) + */ + public boolean isLowPoint() { + final var compareTo = new ArrayList(4); + above().ifPresent(compareTo::add); + below().ifPresent(compareTo::add); + left().ifPresent(compareTo::add); + right().ifPresent(compareTo::add); + return compareTo.stream().allMatch(neighbour -> neighbour.getHeight() > getHeight()); + } + + /** + * @return an assessment of the risk from smoke flowing through the cave + */ + public int getRiskLevel() { + return getHeight() + 1; + } + + /** + * @return the height of this particular location, from 0-9 + */ + public int getHeight() { + return grid()[x()][y()]; + } + + public Optional above() { + return x() > 0 ? Optional.of(new Point(x() - 1, y())) : Optional.empty(); + } + + public Optional below() { + return x() < grid().length - 1 ? Optional.of(new Point(x() + 1, y())) : Optional.empty(); + } + + public Optional left() { + return y() > 0 ? Optional.of(new Point(x(), y() - 1)) : Optional.empty(); + } + + public Optional right() { + return y() < grid()[x()].length - 1 ? Optional.of(new Point(x(), y() + 1)) : Optional.empty(); + } + + public int hashCode() { + return Objects.hash(x(), y()); + } + + public boolean equals(final Object o) { + try { + final Point other = (Point) o; + return this.x() == other.x() && this.y() == other.y(); + } catch (final ClassCastException cce) { + return false; + } + } + } + + } + + @Test + public final void part1() { + final var map = getHeightMap(); + final int sum = map.points() + .filter(HeightMap.Point::isLowPoint) + .mapToInt(HeightMap.Point::getRiskLevel) + .sum(); + System.out.println("Part 1: " + sum); + } + + @Test + public final void part2() { + final var map = getHeightMap(); + final var basinSizes = map.points() + .filter(HeightMap.Point::isLowPoint) + .mapToInt(HeightMap.Point::getBasinSize) + .collect(() -> new TreeSet(Comparator.reverseOrder()), SortedSet::add, SortedSet::addAll); + final var iterator = basinSizes.iterator(); + final var result = iterator.next() * iterator.next() * iterator.next(); + System.out.println("Part 2: " + result); + } + +} \ 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..6dee4a4 --- /dev/null +++ b/src/test/resources/sample/day-09.txt @@ -0,0 +1,5 @@ +2199943210 +3987894921 +9856789892 +8767896789 +9899965678 From 785ff7b0b439c64ba5d9edd13789547353abc868 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Thu, 9 Dec 2021 22:06:20 -0800 Subject: [PATCH 11/44] Day 10 --- src/test/java/com/macasaet/Day10.java | 137 ++++++++++++++++++++++++++ src/test/resources/sample/day-10.txt | 10 ++ 2 files changed, 147 insertions(+) create mode 100644 src/test/java/com/macasaet/Day10.java create mode 100644 src/test/resources/sample/day-10.txt diff --git a/src/test/java/com/macasaet/Day10.java b/src/test/java/com/macasaet/Day10.java new file mode 100644 index 0000000..a7723f4 --- /dev/null +++ b/src/test/java/com/macasaet/Day10.java @@ -0,0 +1,137 @@ +package com.macasaet; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 10: Syntax Scoring --- + */ +public class Day10 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-10.txt"), + false); + } + + /** + * The type of open and closing delimiter for a chunk in the navigation subsystem + */ + public enum BracketType { + PARENTHESIS('(', ')', 3, 1), + SQUARE('[', ']', 57, 2), + CURLY('{', '}', 1197, 3), + ANGLED('<', '>', 25137, 4); + + private final char open; + private final char close; + private final int corruptionPoints; + private final int autocompletePoints; + + BracketType(char open, char close, int corruptionPoints, final int autocompletePoints) { + this.open = open; + this.close = close; + this.corruptionPoints = corruptionPoints; + this.autocompletePoints = autocompletePoints; + } + + public static BracketType forOpen(final char c) { + return switch (c) { + case '(' -> PARENTHESIS; + case '[' -> SQUARE; + case '{' -> CURLY; + case '<' -> ANGLED; + default -> throw new IllegalStateException("Unexpected value: " + c); + }; + } + + public static BracketType forClose(final char c) { + return switch (c) { + case ')' -> PARENTHESIS; + case ']' -> SQUARE; + case '}' -> CURLY; + case '>' -> ANGLED; + default -> throw new IllegalStateException("Unexpected value: " + c); + }; + } + } + + /** + * @param line a line in the navigation subsystem + * @return a score of how corrupt the line is. A score of zero means it is not corrupt. The higher the value, the + * more corrupt the line is. + */ + public int calculateCorruptionScore(final char[] line) { + final var stack = new LinkedList(); + for (int i = 0; i < line.length; i++) { + final var c = line[i]; + if (c == '(' || c == '[' || c == '{' || c == '<') { + stack.push(BracketType.forOpen(c)); + } else if (c == ')' || c == ']' || c == '}' || c == '>') { + if (stack.peek().close == c) { + stack.pop(); + } else { + // corrupt + return BracketType.forClose(c).corruptionPoints; + } + } + } + // if stack is not empty, it's incomplete + return 0; + } + + /** + * @param line a non-corrupt line in the navigation subsystem. Behaviour is undefined for corrupt lines. + * @return the score for the suffix required to complete the line + */ + public long calculateCompletionScore(final char[] line) { + final var stack = new LinkedList(); + for (int i = 0; i < line.length; i++) { + final var c = line[i]; + if (c == '(' || c == '[' || c == '{' || c == '<') { + stack.push(BracketType.forOpen(c)); + } else if (c == ')' || c == ']' || c == '}' || c == '>') { + if (stack.peek().close == c) { + stack.pop(); + } else { + throw new IllegalArgumentException("Corrupt: " + new String(line)); + } + } + } + long result = 0; + while (!stack.isEmpty()) { + final var unclosed = stack.pop(); + result = result * 5 + unclosed.autocompletePoints; + } + return result; + } + + @Test + public final void part1() { + final var result = getInput() + .map(String::toCharArray) + .filter(line -> line.length > 0) + .mapToInt(this::calculateCorruptionScore) + .sum(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var list = getInput() + .map(String::toCharArray) + .filter(line -> line.length > 0) + .filter(line -> calculateCorruptionScore(line) <= 0) // discard corrupted lines + .mapToLong(this::calculateCompletionScore) + .sorted() + .collect(ArrayList::new, List::add, List::addAll); + final var result = list.get(list.size() / 2); + System.out.println("Part 2: " + result); + } + +} \ 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..b1518d9 --- /dev/null +++ b/src/test/resources/sample/day-10.txt @@ -0,0 +1,10 @@ +[({(<(())[]>[[{[]{<()<>> +[(()[<>])]({[<{<<[]>>( +{([(<{}[<>[]}>{[]{[(<()> +(((({<>}<{<{<>}{[]{[]{} +[[<[([]))<([[{}[[()]]] +[{[{({}]{}}([{[{{{}}([] +{<[[]]>}<{[{[{[]{()[[[] +[<(<(<(<{}))><([]([]() +<{([([[(<>()){}]>(<<{{ +<{([{{}}[<[[[<>{}]]]>[]] From e413e5bf5bbb3c6a70b658e4ed309711ddc7706b Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Fri, 10 Dec 2021 22:18:11 -0800 Subject: [PATCH 12/44] Day 11 --- src/test/java/com/macasaet/Day11.java | 169 ++++++++++++++++++++++++++ src/test/resources/sample/day-11.txt | 10 ++ 2 files changed, 179 insertions(+) create mode 100644 src/test/java/com/macasaet/Day11.java create mode 100644 src/test/resources/sample/day-11.txt diff --git a/src/test/java/com/macasaet/Day11.java b/src/test/java/com/macasaet/Day11.java new file mode 100644 index 0000000..f566fd3 --- /dev/null +++ b/src/test/java/com/macasaet/Day11.java @@ -0,0 +1,169 @@ +package com.macasaet; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 11: Dumbo Octopus --- + */ +public class Day11 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-11.txt"), + false); + } + + /** + * @return a spatial grid of the cavern indicating the location of the octopuses + */ + protected Octopus[][] getOctopusGrid() { + final var list = getInput().map(line -> { + final var chars = line.toCharArray(); + final byte[] result = new byte[chars.length]; + for (int i = chars.length; --i >= 0; result[i] = (byte) (chars[i] - '0')) ; + return result; + }).collect(ArrayList::new, List::add, List::addAll); + final var result = new Octopus[list.size()][]; + for (int i = list.size(); --i >= 0; ) { + final var row = list.get(i); + result[i] = new Octopus[row.length]; + for (int j = row.length; --j >= 0; result[i][j] = new Octopus(i, j, row[j])) ; + } + return result; + } + + /** + * A rare bioluminescent dumbo octopus + */ + public static class Octopus { + private final int x, y; + private byte energyLevel; + + public Octopus(final int x, final int y, final byte energyLevel) { + this.x = x; + this.y = y; + this.energyLevel = energyLevel; + } + + /** + * Increase the octopus' energy level and, if appropriate, propagate side effects to its neighbours. + * + * @param population the full population of octopuses + */ + public void prepareStep(final Population population) { + if (this.energyLevel > 9) { + // "An octopus can only flash at most once per step." + return; + } + // "First, the energy level of each octopus increases by 1." + this.energyLevel++; + if (this.energyLevel > 9) { + // "Then, any octopus with an energy level greater than 9 flashes. This increases the energy level of + // all adjacent octopuses by 1, including octopuses that are diagonally adjacent." + final var grid = population.grid(); + final var hasRowAbove = x > 0; + final var hasRowBelow = x < grid.length - 1; + final var hasColumnToLeft = y > 0; + final var hasColumnToRight = y < grid[x].length - 1; + + if (hasRowAbove) { + grid[x - 1][y].prepareStep(population); + if (hasColumnToLeft) { + grid[x - 1][y - 1].prepareStep(population); + } + if (hasColumnToRight) { + grid[x - 1][y + 1].prepareStep(population); + } + } + if (hasColumnToLeft) { + grid[x][y - 1].prepareStep(population); + } + if (hasColumnToRight) { + grid[x][y + 1].prepareStep(population); + } + if (hasRowBelow) { + grid[x + 1][y].prepareStep(population); + if (hasColumnToLeft) { + grid[x + 1][y - 1].prepareStep(population); + } + if (hasColumnToRight) { + grid[x + 1][y + 1].prepareStep(population); + } + } + } + } + + /** + * Complete the step and finalise any side effects. + * + * @return true if and only if the octopus flashed during this step. + */ + public boolean finishStep() { + if (this.energyLevel > 9) { + // "Finally, any octopus that flashed during this step has its energy level set to 0, as it used all of + // its energy to flash." + this.energyLevel = 0; + return true; + } + return false; + } + } + + /** + * The full population of dumbo octopuses. The population members will be modified with each step. + */ + public record Population(Octopus[][] grid) { + public int step() { + for (int i = grid.length; --i >= 0; ) { + final var row = grid[i]; + for (int j = row.length; --j >= 0; ) { + row[j].prepareStep(this); + } + } + int flashes = 0; + for (int i = grid.length; --i >= 0; ) { + final var row = grid[i]; + for (int j = row.length; --j >= 0; ) { + if (row[j].finishStep()) flashes++; + } + } + return flashes; + } + + } + + @Test + public final void part1() { + final var energyLevels = getOctopusGrid(); + final var population = new Population(energyLevels); + + int flashes = 0; + + for (int step = 0; step < 100; step++) { + flashes += population.step(); + } + + System.out.println("Part 1: " + flashes); + } + + @Test + public final void part2() { + final var energyLevels = getOctopusGrid(); + final var population = new Population(energyLevels); + int step = 0; + while(true) { + final int flashes = population.step(); + step++; + if(flashes == 100) { + System.out.println("Part 2: " + step); + break; + } + } + } + +} \ 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..03743f6 --- /dev/null +++ b/src/test/resources/sample/day-11.txt @@ -0,0 +1,10 @@ +5483143223 +2745854711 +5264556173 +6141336146 +6357385478 +4167524645 +2176841721 +6882881134 +4846848554 +5283751526 From 1f7a19f07aa1f4f8f2bc54c001e8f5cb0c1d9f3e Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sat, 11 Dec 2021 22:32:46 -0800 Subject: [PATCH 13/44] Day 12 --- src/test/java/com/macasaet/Day12.java | 182 ++++++++++++++++++++++++++ src/test/resources/sample/day-12.txt | 7 + 2 files changed, 189 insertions(+) create mode 100644 src/test/java/com/macasaet/Day12.java create mode 100644 src/test/resources/sample/day-12.txt diff --git a/src/test/java/com/macasaet/Day12.java b/src/test/java/com/macasaet/Day12.java new file mode 100644 index 0000000..af7de96 --- /dev/null +++ b/src/test/java/com/macasaet/Day12.java @@ -0,0 +1,182 @@ +package com.macasaet; + +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 12: Passage Pathing --- + */ +public class Day12 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-12.txt"), + false); + } + + /** + * @return a map of the connected caves + */ + protected Map getMap() { + final var map = new HashMap(); + getInput().forEach(line -> { + final var components = line.split("-"); + final var sourceLabel = components[0]; + final var targetLabel = components[1]; + final var source = map.computeIfAbsent(sourceLabel, Node::new); + final var target = map.computeIfAbsent(targetLabel, Node::new); + source.connected.add(target); + target.connected.add(source); + }); + return Collections.unmodifiableMap(map); + } + + public Node getStartingPoint() { + return getMap().get("start"); + } + + /** + * A distinct path through the cave system + */ + public record Path(List nodes, Node specialCave, int specialCaveVisits) { + + public int hashCode() { + int result = 0; + for (final var node : nodes()) { + result = result * 31 + node.hashCode(); + } + return result; + } + + public boolean equals(final Object o) { + if (o == null) { + return false; + } + try { + final var other = (Path) o; + return nodes().equals(other.nodes()); + } catch (final ClassCastException cce) { + return false; + } + } + } + + public static class Node { + private final boolean isStart; + private final boolean isEnd; + private final boolean isSmallCave; + private final String label; + + private final Set connected = new HashSet<>(); + + public Node(final String label) { + this("start".equalsIgnoreCase(label), "end".equalsIgnoreCase(label), + label.toLowerCase(Locale.ROOT).equals(label), label); + } + + protected Node(boolean isStart, boolean isEnd, boolean isSmallCave, final String label) { + this.isStart = isStart; + this.isEnd = isEnd; + this.isSmallCave = isSmallCave; + this.label = label; + } + + public int hashCode() { + int result = 0; + result += result * 31 + label.hashCode(); + return result; + } + + public boolean equals(final Object o) { + if (o == null) { + return false; + } + try { + final Node other = (Node) o; + return label.equals(other.label); + } catch (final ClassCastException cce) { + return false; + } + } + + } + + protected Set getPaths(final Node node, final Path pathSoFar) { + final var result = new HashSet(); + + if (node.isStart && pathSoFar.nodes.size() > 1) { + // "once you leave the start cave, you may not return to it" + return Collections.emptySet(); + } + + final var nodes = new ArrayList<>(pathSoFar.nodes()); + if (node.isEnd) { + // "once you reach the end cave, the path must end immediately" + nodes.add(node); + return Collections.singleton(new Path(Collections.unmodifiableList(nodes), pathSoFar.specialCave(), pathSoFar.specialCaveVisits())); + } + int specialCaveVisits = pathSoFar.specialCaveVisits(); + if (node.isSmallCave) { + if (node.equals(pathSoFar.specialCave())) { + // "a single small cave can be visited at most twice" + if (pathSoFar.specialCaveVisits() < 1) { + specialCaveVisits++; + } else { + return Collections.emptySet(); + } + } else { + if (pathSoFar.nodes().contains(node)) { + // "the remaining small caves can be visited at most once" + return Collections.emptySet(); + } + } + } + nodes.add(node); + for (final var neighbour : node.connected) { + if (neighbour.isSmallCave && pathSoFar.specialCave() == null) { + result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), null, 0))); + result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), neighbour, 0))); + } else { + result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), pathSoFar.specialCave(), specialCaveVisits))); + } + } + return Collections.unmodifiableSet(result); + } + + protected int countPaths(final Node node, final Set visitedSmallCaves) { + int result = 0; + if (node.isEnd) { + return 1; + } + if (visitedSmallCaves.contains(node)) { + // invalid path + return 0; + } + if (node.isSmallCave) { + visitedSmallCaves.add(node); + } + for (final var connected : node.connected) { + final var set = new HashSet<>(visitedSmallCaves); + result += countPaths(connected, set); + } + return result; + } + + @Test + public final void part1() { + final var start = getStartingPoint(); + final int result = countPaths(start, new HashSet<>()); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var start = getStartingPoint(); + final var paths = getPaths(start, new Path(Collections.emptyList(), null, 0)); + System.out.println("Part 2: " + paths.size()); + } + +} \ 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..6fd8c41 --- /dev/null +++ b/src/test/resources/sample/day-12.txt @@ -0,0 +1,7 @@ +start-A +start-b +A-c +A-b +b-d +A-end +b-end From 5a73ccb5bae347dc86928c3976617f7d3d7c2dd8 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sun, 12 Dec 2021 22:22:23 -0800 Subject: [PATCH 14/44] Day 13 --- src/test/java/com/macasaet/Day13.java | 187 ++++++++++++++++++++++++++ src/test/resources/sample/day-13.txt | 21 +++ 2 files changed, 208 insertions(+) create mode 100644 src/test/java/com/macasaet/Day13.java create mode 100644 src/test/resources/sample/day-13.txt diff --git a/src/test/java/com/macasaet/Day13.java b/src/test/java/com/macasaet/Day13.java new file mode 100644 index 0000000..b60355f --- /dev/null +++ b/src/test/java/com/macasaet/Day13.java @@ -0,0 +1,187 @@ +package com.macasaet; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 13: Transparent Origami --- + */ +public class Day13 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-13.txt"), + false); + } + + /** + * A point on the translucent sheet of paper. Note that x and y correspond to a particular + * {@link Axis}. + */ + public record Point(int x, int y) { + } + + /** + * An axis of the translucent sheet of paper + */ + public enum Axis { + /** + * The axis that increases to the right + */ + X, + + /** + * The axis that increases downward + */ + Y, + } + + /** + * An equation for a line + */ + public record Line(Axis axis, int value) { + public String toString() { + return switch (axis()) { + case X -> "x=" + value; + case Y -> "y=" + value; + }; + } + } + + public record Input(Collection points, List folds, int maxX, int maxY) { + public Sheet getSheet() { + final boolean[][] grid = new boolean[maxY + 1][]; + for (int i = grid.length; --i >= 0; ) { + grid[i] = new boolean[maxX + 1]; + } + for (final var point : points) { + /* The first value, x, increases to the right. The second value, y, increases downward. */ + grid[point.y()][point.x()] = true; + } + return new Sheet(grid); + } + } + + /** + * A sheet of translucent paper + */ + public record Sheet(boolean[][] grid) { + + public int countDots() { + int result = 0; + final var grid = grid(); + for (int i = grid.length; --i >= 0; ) { + for (int j = grid[i].length; --j >= 0; ) { + if (grid[i][j]) { + result++; + } + } + } + return result; + } + + public String toString() { + final var builder = new StringBuilder(); + for (final var row : grid) { + for (final var cell : row) { + builder.append(cell ? '#' : '.'); + } + builder.append('\n'); + } + return builder.toString(); + } + + public Sheet fold(final Line line) { + // note, value is always positive + return switch (line.axis()) { + case X -> { + // fold along the x-axis (vertical line) + final var newGrid = new boolean[grid.length][]; + for (int i = newGrid.length; --i >= 0; ) { + final var newRow = new boolean[line.value() + 1]; + for (int j = newRow.length; --j >= 0; newRow[j] = grid[i][j]) ; + for(int j = grid[i].length - line.value(); --j > 0; ) { + if(grid[i][line.value() + j]) { + newRow[line.value() - j] = true; + } + } + newGrid[i] = newRow; + } + yield new Sheet(newGrid); + } + case Y -> { + // fold along the y-axis (horizontal line) + final var newGrid = new boolean[line.value()][]; + for (int i = newGrid.length; --i >= 0; ) { + final var newRow = new boolean[grid[i].length]; + for (int j = grid[i].length; --j >= 0; newRow[j] = grid[i][j]) ; + newGrid[i] = newRow; + } + for (int i = grid.length - line.value(); --i > 0; ) { + final var oldRow = grid[line.value() + i]; + for (int j = oldRow.length; + --j >= 0; + newGrid[line.value() - i][j] |= oldRow[j]) + ; + } + yield new Sheet(newGrid); + } + }; + } + } + + public Input parseInput() { + int section = 0; + final var points = new HashSet(); + final var folds = new ArrayList(); + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + for (final var line : getInput().collect(Collectors.toList())) { + if (line.isBlank()) { + section++; + continue; + } + if (section == 0) { // points + final var components = line.split(","); + final var x = Integer.parseInt(components[0]); + maxX = Math.max(x, maxX); + final var y = Integer.parseInt(components[1]); + maxY = Math.max(y, maxY); + final var point = new Point(x, y); + points.add(point); + } else { // commands + final var equation = line.replaceFirst("fold along ", ""); + final var components = equation.split("="); + final var axis = Axis.valueOf(components[0].toUpperCase(Locale.ROOT)); + final var value = Integer.parseInt(components[1]); + final var fold = new Line(axis, value); + folds.add(fold); + } + } + return new Input(points, folds, maxX, maxY); + } + + @Test + public final void part1() { + final var input = parseInput(); + final var sheet = input.getSheet(); + final var firstFold = input.folds().get(0); + final var result = sheet.fold(firstFold); + System.out.println("Part 1: " + result.countDots()); + } + + @Test + public final void part2() { + final var input = parseInput(); + var sheet = input.getSheet(); + for (final var fold : input.folds()) { + sheet = sheet.fold(fold); + } + System.out.println("Part 2:\n" + sheet); + } + +} diff --git a/src/test/resources/sample/day-13.txt b/src/test/resources/sample/day-13.txt new file mode 100644 index 0000000..282114c --- /dev/null +++ b/src/test/resources/sample/day-13.txt @@ -0,0 +1,21 @@ +6,10 +0,14 +9,10 +0,3 +10,4 +4,11 +6,0 +6,12 +4,1 +0,13 +10,12 +3,4 +3,0 +8,4 +1,10 +2,14 +8,10 +9,0 + +fold along y=7 +fold along x=5 From 02eba5b57f22cb13156399182882ea4b45fc76e0 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Tue, 14 Dec 2021 18:32:42 -0800 Subject: [PATCH 15/44] Day 14 --- src/test/java/com/macasaet/Day14.java | 166 ++++++++++++++++++++++++++ src/test/resources/sample/day-14.txt | 18 +++ 2 files changed, 184 insertions(+) create mode 100644 src/test/java/com/macasaet/Day14.java create mode 100644 src/test/resources/sample/day-14.txt diff --git a/src/test/java/com/macasaet/Day14.java b/src/test/java/com/macasaet/Day14.java new file mode 100644 index 0000000..c44d50b --- /dev/null +++ b/src/test/java/com/macasaet/Day14.java @@ -0,0 +1,166 @@ +package com.macasaet; + +import java.math.BigInteger; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 14: Extended Polymerization --- + */ +public class Day14 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-14.txt"), + false); + } + + public record Polymer(Map pairCounts, char firstElement, char lastElement) { + public static Polymer forTemplate(final String templateString) { + final var firstElement = templateString.charAt(0); + final var lastElement = templateString.charAt(templateString.length() - 1); + final var map = new HashMap(); + for (int i = 1; i < templateString.length(); i++) { + map.merge(new ElementPair(templateString.charAt(i - 1), templateString.charAt(i)), + BigInteger.ONE, + BigInteger::add); + } + return new Polymer(Collections.unmodifiableMap(map), firstElement, lastElement); + } + + /** + * Apply the pair insertion process one time. + * + * @param rules pair insertion rules for generating a new polymer + * @return the new polymer that results + */ + public Polymer applyRules(final Map rules) { + final var map = new HashMap(); + for (final var entry : pairCounts().entrySet()) { + final var key = entry.getKey(); + final var count = entry.getValue(); + final var rule = rules.get(key); + final var left = new ElementPair(key.start(), rule.insert()); + final var right = new ElementPair(rule.insert(), key.end()); + + map.compute(left, (_key, oldCount) -> oldCount == null ? count : oldCount.add(count)); + map.compute(right, (_key, oldCount) -> oldCount == null ? count : oldCount.add(count)); + } + return new Polymer(Collections.unmodifiableMap(map), firstElement(), lastElement()); + } + + /** + * Determine how many times each element appears in the polymer + * + * @return the number of times each element appears in the polymer + */ + public SortedMap> histogram() { + final var map = new HashMap(); + for (final var entry : pairCounts().entrySet()) { + final var pair = entry.getKey(); + final var count = entry.getValue(); + map.compute(pair.start(), + (_key, oldValue) -> oldValue == null ? count : oldValue.add(count)); + map.compute(pair.end(), + (_key, oldValue) -> oldValue == null ? count : oldValue.add(count)); + } + for (final var entry : map.entrySet()) { + final var element = entry.getKey(); + final var count = entry.getValue(); + if (element.equals(firstElement()) || element.equals(lastElement())) { + entry.setValue(count.divide(BigInteger.TWO).add(BigInteger.ONE)); + } else { + entry.setValue(count.divide(BigInteger.TWO)); + } + } + final var result = new TreeMap>(); + for (final var entry : map.entrySet()) { + final var target = result.computeIfAbsent(entry.getValue(), _key -> new HashSet<>()); + target.add(entry.getKey()); + } + return Collections.unmodifiableSortedMap(result); + } + } + + /** + * A pair of elements that appear adjacent to each other. This may be used in the context of a pair insertion rule + * definition or a polymer. + * + * @see Polymer + * @see PairInsertionRule + */ + protected record ElementPair(char start, char end) { + } + + /** + * A single instruction to aid in finding the optimal polymer formula + */ + public record PairInsertionRule(char start, char end, char insert) { + + public static PairInsertionRule parse(final String string) { + final var components = string.split(" -> "); + final var match = components[0].toCharArray(); + return new PairInsertionRule(match[0], match[1], components[1].toCharArray()[0]); + } + + } + + protected record Input(Polymer polymerTemplate, List rules) { + } + + protected Input parseInput() { + final var list = getInput().collect(Collectors.toList()); + int mode = 0; + Polymer polymer = null; + final var rules = new ArrayList(); + for (final var line : list) { + if (line.isBlank()) { + mode++; + continue; + } + if (mode == 0) { + polymer = Polymer.forTemplate(line); + } else { + rules.add(PairInsertionRule.parse(line)); + } + } + return new Input(polymer, rules); + } + + @Test + public final void part1() { + final var input = parseInput(); + var polymer = input.polymerTemplate(); + final var rules = input.rules(); + final var ruleMap = new HashMap(); + for (final var rule : rules) { + ruleMap.put(new ElementPair(rule.start(), rule.end()), rule); + } + for (int _i = 0; _i < 10; _i++) { + polymer = polymer.applyRules(ruleMap); + } + final var histogram = polymer.histogram(); + System.out.println("Part 1: " + histogram.lastKey().subtract(histogram.firstKey())); + } + + @Test + public final void part2() { + final var input = parseInput(); + var polymer = input.polymerTemplate(); + final var rules = input.rules(); + final var ruleMap = new HashMap(); + for (final var rule : rules) { + ruleMap.put(new ElementPair(rule.start(), rule.end()), rule); + } + for (int _i = 0; _i < 40; _i++) { + polymer = polymer.applyRules(ruleMap); + } + final var histogram = polymer.histogram(); + System.out.println("Part 2: " + histogram.lastKey().subtract(histogram.firstKey())); + } + +} \ 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..b5594dd --- /dev/null +++ b/src/test/resources/sample/day-14.txt @@ -0,0 +1,18 @@ +NNCB + +CH -> B +HH -> N +CB -> H +NH -> C +HB -> C +HC -> B +HN -> C +NN -> C +BH -> H +NC -> B +NB -> B +BN -> B +BB -> N +BC -> B +CC -> N +CN -> C From ca0db89972899cdad5e6c8154558eff37d9444f2 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Fri, 17 Dec 2021 00:22:31 -0800 Subject: [PATCH 16/44] Days 15 and 17 --- src/test/java/com/macasaet/Day15.java | 228 ++++++++++++++++++++++++++ src/test/java/com/macasaet/Day17.java | 138 ++++++++++++++++ src/test/resources/sample/day-15.txt | 10 ++ src/test/resources/sample/day-17.txt | 1 + 4 files changed, 377 insertions(+) create mode 100644 src/test/java/com/macasaet/Day15.java create mode 100644 src/test/java/com/macasaet/Day17.java create mode 100644 src/test/resources/sample/day-15.txt create mode 100644 src/test/resources/sample/day-17.txt diff --git a/src/test/java/com/macasaet/Day15.java b/src/test/java/com/macasaet/Day15.java new file mode 100644 index 0000000..e93e89f --- /dev/null +++ b/src/test/java/com/macasaet/Day15.java @@ -0,0 +1,228 @@ +package com.macasaet; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 15: Chiton --- + */ +public class Day15 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-15.txt"), + false); + } + + protected int[][] getGrid() { + final var list = getInput().collect(Collectors.toList()); + final int[][] grid = new int[list.size()][]; + for (int i = 0; i < grid.length; i++) { + final var chars = list.get(i).toCharArray(); + final var row = new int[chars.length]; + for (int j = chars.length; --j >= 0; row[j] = chars[j] - '0') ; + grid[i] = row; + } + return grid; + } + + public record Point(int x, int y) { + + public int risk(int[][] risks) { + return risks[x][y]; + } + + } + + public record Cavern(int[][] grid) { + + public Set predecessors(final Point source) { + final var result = new HashSet(); + if (source.x() > 0) { + result.add(new Point(source.x() - 1, source.y())); + } + if (source.y() > 0) { + result.add(new Point(source.x(), source.y() - 1)); + } + return Collections.unmodifiableSet(result); + } + + public Set successors(final Point source) { + final var result = new HashSet(); + if (source.x() < grid().length - 1) { + result.add(new Point(source.x() + 1, source.y())); + } + if (source.y() < grid()[source.x()].length - 1) { + result.add(new Point(source.x(), source.y() + 1)); + } + return Collections.unmodifiableSet(result); + } + + public Cavern explode() { + final int[][] largeGrid = new int[grid.length * 5][]; + for (int i = largeGrid.length; --i >= 0; ) { + largeGrid[i] = new int[grid.length * 5]; + } + for (int i = grid.length; --i >= 0; ) { + for (int j = grid.length; --j >= 0; ) { + largeGrid[i][j] = grid[i][j]; + } + } + for (int tileRow = 0; tileRow < 5; tileRow++) { + for (int tileColumn = 0; tileColumn < 5; tileColumn++) { + if (tileRow > 0) { + for (int i = grid.length; --i >= 0; ) { + for (int j = grid.length; --j >= 0; ) { + // copy from row above + int value = largeGrid[(tileRow - 1) * grid.length + i][tileColumn * grid.length + j] + 1; + if (value == 10) { + value = 1; + } + largeGrid[tileRow * grid.length + i][tileColumn * grid.length + j] = value; + } + } + } else if (tileColumn > 0) { + for (int i = grid.length; --i >= 0; ) { + for (int j = grid.length; --j >= 0; ) { + // copy from column to the left + int value = largeGrid[tileRow * grid.length + i][(tileColumn - 1) * grid.length + j] + 1; + if (value == 10) { + value = 1; + } + largeGrid[tileRow * grid.length + i][tileColumn * grid.length + j] = value; + } + } + } + } + } + return new Cavern(largeGrid); + } + + public long[][] calculateCumulativeRisk() { + final var cumulative = new long[grid().length][]; + for (int i = cumulative.length; --i >= 0; cumulative[i] = new long[grid()[i].length]) ; + final var visited = new HashSet(); + final var queue = new LinkedList(); + final var destination = new Point(grid().length - 1, grid()[grid().length - 1].length - 1); + queue.add(destination); + visited.add(destination); + + while (!queue.isEmpty()) { + final var node = queue.remove(); + final var successors = successors(node); + if (successors.isEmpty()) { + // destination + cumulative[node.x][node.y] = node.risk(grid()); + } else { + var minSuccessorRisk = Long.MAX_VALUE; + for (final var successor : successors) { + if (!visited.contains(successor)) { + throw new IllegalStateException("Successor has not been visited"); + } + minSuccessorRisk = Math.min(minSuccessorRisk, cumulative[successor.x][successor.y]); + } + cumulative[node.x][node.y] = node.risk(grid()) + minSuccessorRisk; + } + + for (final var predecessor : predecessors(node)) { + if (!visited.contains(predecessor)) { + queue.add(predecessor); + visited.add(predecessor); + } + } + } + return cumulative; + } + + /** + * @return the risk level associated with the path through the cavern that avoids the most chitons + */ + public int lowestRiskThroughTheCavern() { + // the lowest known risk from origin to a given node + final var lowestRiskToNode = new HashMap(); + // the estimated risk from origin to destination if it goes through a given node + final var estimatedRiskThroughNode = new HashMap(); + final var openSet = new PriorityQueue(Comparator.comparing(estimatedRiskThroughNode::get)); + + for (int i = grid().length; --i >= 0; ) { + final var row = grid()[i]; + for (int j = row.length; --j >= 0; ) { + final var point = new Point(i, j); + if (i == 0 && j == 0) { + lowestRiskToNode.put(point, 0); + estimatedRiskThroughNode.put(point, manhattanDistance(point)); + openSet.add(point); + } else { + lowestRiskToNode.put(point, Integer.MAX_VALUE); + estimatedRiskThroughNode.put(point, Integer.MAX_VALUE); + } + } + } + + while(!openSet.isEmpty()) { + final var current = openSet.poll(); + if(current.x() == grid().length - 1 && current.y() == grid()[grid().length - 1].length - 1) { + return lowestRiskToNode.get(current); + } + final var lowestRiskToCurrent = lowestRiskToNode.get(current); + for(final var neighbour : neighbours(current)) { + final var tentativeRisk = lowestRiskToCurrent + neighbour.risk(grid()); + if(tentativeRisk < lowestRiskToNode.get(neighbour)) { + lowestRiskToNode.put(neighbour, tentativeRisk); + estimatedRiskThroughNode.put(neighbour, tentativeRisk + manhattanDistance(neighbour)); + if(!openSet.contains(neighbour)) { + openSet.add(neighbour); + } + } + } + } + throw new IllegalStateException("No path out of the cavern!"); + } + + /** + * @param point + * @return + */ + protected int manhattanDistance(Point point) { + return Math.abs(point.x() - (grid().length - 1)) + + Math.abs(point.y() - (grid()[grid().length - 1].length - 1)); + } + + public Set neighbours(final Point point) { + final var result = new HashSet(); + if (point.x() > 0) { + result.add(new Point(point.x() - 1, point.y())); + } + if (point.x() < grid().length - 1) { + result.add(new Point(point.x() + 1, point.y())); + } + if (point.y() > 0) { + result.add(new Point(point.x(), point.y() - 1)); + } + if (point.y() < grid()[point.x()].length - 1) { + result.add(new Point(point.x(), point.y() + 1)); + } + return Collections.unmodifiableSet(result); + } + + } + + @Test + public final void part1() { + final var grid = getGrid(); + final var cavern = new Cavern(grid); + System.out.println("Part 1: " + cavern.lowestRiskThroughTheCavern()); + } + + @Test + public final void part2() { + final var grid = getGrid(); + final var cavern = new Cavern(grid).explode(); + System.out.println("Part 2: " + cavern.lowestRiskThroughTheCavern()); + } + +} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day17.java b/src/test/java/com/macasaet/Day17.java new file mode 100644 index 0000000..66dc6f1 --- /dev/null +++ b/src/test/java/com/macasaet/Day17.java @@ -0,0 +1,138 @@ +package com.macasaet; + +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 17: Trick Shot --- + */ +public class Day17 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-17.txt"), + false); + } + + /** + * The target area in a large ocean trench + */ + public record Target(int minX, int maxX, int minY, int maxY) { + } + + /** + * A probe at a given point in time + */ + public record Probe(int xVelocity, int yVelocity, int x, int y) { + + /** + * Launch a probe from the origin + * + * @param xVelocity the starting horizontal velocity + * @param yVelocity the starting vertical velocity + * @return the initial state of the probe at the origin + */ + public static Probe launch(final int xVelocity, final int yVelocity) { + return new Probe(xVelocity, yVelocity, 0, 0); + } + + public Optional step() { + if(x > 0 && x + xVelocity < 0) { + return Optional.empty(); + } + if(y < 0 && y + yVelocity > 0) { + return Optional.empty(); + } + final int newX = x + xVelocity; + final int newY = y + yVelocity; + final int newXVelocity = xVelocity > 0 + ? xVelocity - 1 + : xVelocity < 0 + ? xVelocity + 1 + : xVelocity; + return Optional.of(new Probe(newXVelocity, + yVelocity - 1, + newX, + newY)); + } + + public Optional peak(final Target target) { + var peak = Integer.MIN_VALUE; + var p = Optional.of(this); + while (p.isPresent()) { + final var probe = p.get(); + peak = Math.max(peak, probe.y()); + if (probe.x() < target.minX() && probe.y() < target.minY()) { + // short + return Optional.empty(); + } else if (probe.x() > target.maxX()) { + // long + return Optional.empty(); + } else if (probe.x() >= target.minX() && probe.x() <= target.maxX() + && probe.y() >= target.minY() && probe.y() <= target.maxY()) { + return Optional.of(peak); + } + p = probe.step(); + } + return Optional.empty(); + } + + } + + @Test + public final void part1() { + final var line = getInput().collect(Collectors.toList()).get(0); + final var bounds = line.replaceFirst("target area: ", "").split(", "); + final var xBounds = bounds[0].replaceFirst("x=", "").split("\\.\\."); + final var yBounds = bounds[1].replaceFirst("y=", "").split("\\.\\."); + final int minX = Integer.parseInt(xBounds[0]); + final int maxX = Integer.parseInt(xBounds[1]); + final int minY = Integer.parseInt(yBounds[0]); + final int maxY = Integer.parseInt(yBounds[1]); + final var target = new Target(minX, maxX, minY, maxY); + + final var max = IntStream.range(0, 50) + .parallel() + .mapToObj(x -> IntStream.range(-50, 50) + .parallel() + .mapToObj(y -> Probe.launch(x, y)) + ).flatMap(probes -> probes) + .flatMapToInt(probe -> probe.peak(target) + .stream() + .mapToInt(peak -> peak)) + .max(); + + + System.out.println("Part 1: " + max.getAsInt()); + } + + @Test + public final void part2() { + final var line = getInput().collect(Collectors.toList()).get(0); + final var bounds = line.replaceFirst("target area: ", "").split(", "); + final var xBounds = bounds[0].replaceFirst("x=", "").split("\\.\\."); + final var yBounds = bounds[1].replaceFirst("y=", "").split("\\.\\."); + final int minX = Integer.parseInt(xBounds[0]); + final int maxX = Integer.parseInt(xBounds[1]); + final int minY = Integer.parseInt(yBounds[0]); + final int maxY = Integer.parseInt(yBounds[1]); + final var target = new Target(minX, maxX, minY, maxY); + int count = 0; + for (int x = 1; x <= 400; x++) { + for (int y = -400; y <= 400; y++) { + final var probe = Probe.launch(x, y); + if (probe.peak(target).isPresent()) { + count++; + } + } + } + + System.out.println("Part 2: " + count); + } + +} \ No newline at end of file diff --git a/src/test/resources/sample/day-15.txt b/src/test/resources/sample/day-15.txt new file mode 100644 index 0000000..ab80887 --- /dev/null +++ b/src/test/resources/sample/day-15.txt @@ -0,0 +1,10 @@ +1163751742 +1381373672 +2136511328 +3694931569 +7463417111 +1319128137 +1359912421 +3125421639 +1293138521 +2311944581 diff --git a/src/test/resources/sample/day-17.txt b/src/test/resources/sample/day-17.txt new file mode 100644 index 0000000..a07e02d --- /dev/null +++ b/src/test/resources/sample/day-17.txt @@ -0,0 +1 @@ +target area: x=20..30, y=-10..-5 From 8f222bb0afa884a9c0cf7c13172a727a4bf3a4a2 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sat, 18 Dec 2021 19:27:55 -0800 Subject: [PATCH 17/44] Day 18 Note, this solution does not correctly solve the Part Two example. However, it produces the correct solution for the puzzle input assigned to me. I commented-out the test case for the example that breaks. --- src/test/java/com/macasaet/Day18.java | 432 ++++++++++++++++++++++++++ src/test/resources/sample/day-18.txt | 10 + 2 files changed, 442 insertions(+) create mode 100644 src/test/java/com/macasaet/Day18.java create mode 100644 src/test/resources/sample/day-18.txt diff --git a/src/test/java/com/macasaet/Day18.java b/src/test/java/com/macasaet/Day18.java new file mode 100644 index 0000000..afd2133 --- /dev/null +++ b/src/test/java/com/macasaet/Day18.java @@ -0,0 +1,432 @@ +package com.macasaet; + +import static com.macasaet.Day18.SnailfishNumber.parse; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * --- Day 18: Snailfish --- + */ +public class Day18 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-18.txt"), + false); + } + + /** + * An element of a {@link SnailfishNumber} + */ + public interface Token { + } + + /** + * A symbol in a {@link SnailfishNumber} + */ + public enum Symbol implements Token { + START_PAIR, + END_PAIR, + SEPARATOR, + + } + + /** + * An integer in a {@link SnailfishNumber} + */ + public record Number(int value) implements Token { + } + + /** + * "Snailfish numbers aren't like regular numbers. Instead, every snailfish number is a pair - an ordered list of + * two elements. Each element of the pair can be either a regular number or another pair." + */ + public record SnailfishNumber(List expression) { + + public SnailfishNumber(final String string) { + this(parse(string)); + } + + /** + * "The magnitude of a pair is 3 times the magnitude of its left element plus 2 times the magnitude of its right + * element. The magnitude of a regular number is just that number." + * + * @return the snailfish number distilled into a single value + */ + public int magnitude() { + var stack = new LinkedList(); + for (final var token : expression) { + if (token.equals(Symbol.START_PAIR)) { + } else if (token instanceof final Number number) { + stack.push(number.value()); + } else if (token.equals(Symbol.END_PAIR)) { + final var rightValue = stack.pop(); + final var leftValue = stack.pop(); + stack.push(leftValue * 3 + rightValue * 2); + } + } + if (stack.size() != 1) { + throw new IllegalStateException("Invalid stack: " + stack); + } + return stack.get(0); + } + + /** + * Repeatedly explode or split this snailfish number until those operations can no longer be performed. + * + * @return a representation of this snailfish number that cannot be reduced any further + */ + public SnailfishNumber reduce() { + var newExpression = expression; + while (true) { + var explosionIndex = getExplosionIndex(newExpression); + var splitIndex = getSplitIndex(newExpression); + if (explosionIndex > 0) { + newExpression = explode(newExpression, explosionIndex); + } else if (splitIndex > 0) { + newExpression = split(newExpression, splitIndex); + } else { + break; + } + } + return new SnailfishNumber(newExpression); + } + + /** + * Add a snailfish number. Note, this operation is *not commutative*: `x.add(y)` is not the same as `y.add(x)`. + * Also note that the process of addition may yield a snailfish number that needs to be reduced. + * + * @param addend the number to add to this snailfish number + * @return the sum of the snailfish numbers (may need to be reduced + * @see SnailfishNumber#reduce() + */ + public SnailfishNumber add(final SnailfishNumber addend) { + final var tokens = new ArrayList(); + tokens.add(Symbol.START_PAIR); + tokens.addAll(expression()); + tokens.add(Symbol.SEPARATOR); + tokens.addAll(addend.expression()); + tokens.add(Symbol.END_PAIR); + return new SnailfishNumber(Collections.unmodifiableList(tokens)); + } + + static List parse(final String expression) { + final var result = new ArrayList(); + for (int i = 0; i < expression.length(); i++) { + final var c = expression.charAt(i); + if (c == '[') { + result.add(Symbol.START_PAIR); + } else if (c == ']') { + result.add(Symbol.END_PAIR); + } else if (c == ',') { + result.add(Symbol.SEPARATOR); + } else if (c >= '0' && c <= '9') { + int endExclusive = i + 1; + while (endExclusive < expression.length()) { + final var d = expression.charAt(endExclusive); + if (d < '0' || d > '9') { + break; + } + endExclusive++; + } + final int value = Integer.parseInt(expression.substring(i, endExclusive)); + result.add(new Number(value)); + i = endExclusive - 1; + } + } + return Collections.unmodifiableList(result); + } + + /** + * Split a regular number. "To split a regular number, replace it with a pair; the left element of the pair + * should be the regular number divided by two and rounded down, while the right element of the pair should be + * the regular number divided by two and rounded up." + * + * @param expression a raw representation of a snailfish number + * @param index the index of a regular number to split. The caller is responsible for ensuring that this number + * can be split and that it is the most appropriate action to take. + * @return the reduced snailfish number in raw tokens + */ + List split(final List expression, final int index) { + final var result = new ArrayList(); + if (index > 0) { + result.addAll(expression.subList(0, index)); + } + final var regularNumber = (Number) expression.get(index); + + final var left = Math.floorDiv(regularNumber.value(), 2); + final var right = (int) Math.ceil(regularNumber.value() / 2.0d); + + result.add(Symbol.START_PAIR); + result.add(new Number(left)); + result.add(Symbol.SEPARATOR); + result.add(new Number(right)); + result.add(Symbol.END_PAIR); + if (index + 1 < expression.size()) { + result.addAll(expression.subList(index + 1, expression.size())); + } + return Collections.unmodifiableList(result); + } + + /** + * Determine whether any of the regular numbers can be split and if so, the highest-priority number to split. + * + * @param expression a raw representation of a snailfish number + * @return the index of the best regular number to split or -1 if none can be split + */ + int getSplitIndex(final List expression) { + for (int i = 0; i < expression.size(); i++) { + final var token = expression.get(i); + if (token instanceof final Number number) { + if (number.value() >= 10) { + return i; + + } + } + } + return -1; + } + + /** + * Explode the pair starting at `index`. "To explode a pair, the pair's left value is added to the first regular + * number to the left of the exploding pair (if any), and the pair's right value is added to the first regular + * number to the right of the exploding pair (if any). Exploding pairs will always consist of two regular + * numbers. Then, the entire exploding pair is replaced with the regular number 0." + * + * @param expression a raw representation of a snailfish number + * @param index the index of the opening brace of the pair to explode. The caller must ensure that an explosion + * operation is valid at the index and that the index represents the most appropriate pair to + * explode. + * @return the reduced expression in raw format + */ + List explode(final List expression, final int index) { + final var result = new ArrayList<>(expression); + final int leftNumberIndex = index + 1; + final int rightNumberIndex = index + 3; + final int left = ((Number) expression.get(leftNumberIndex)).value(); + final int right = ((Number) expression.get(rightNumberIndex)).value(); + int leftIndex = -1; + int rightIndex = -1; + + for (int i = index; --i >= 0; ) { + final var c = expression.get(i); + if (c instanceof Number) { + leftIndex = i; + break; + } + } + for (int i = rightNumberIndex + 1; i < expression.size(); i++) { + final var c = expression.get(i); + if (c instanceof Number) { + rightIndex = i; + break; + } + } + if (leftIndex < 0 && rightIndex < 0) { + throw new IllegalArgumentException("Cannot be exploded: " + expression); + } + // "the pair's left value is added to the first regular number to the left of the exploding pair (if any)" + if (leftIndex > 0) { + final int leftOperand = ((Number) expression.get(leftIndex)).value(); + final int replacement = leftOperand + left; + result.set(leftIndex, new Number(replacement)); + } + // "the pair's right value is added to the first regular number to the right of the exploding pair (if any)" + if (rightIndex > 0) { + final int rightOperand = ((Number) expression.get(rightIndex)).value(); + final int replacement = rightOperand + right; + result.set(rightIndex, new Number(replacement)); + } + // "Exploding pairs will always consist of two regular numbers. Then, the entire exploding pair is replaced + // with the regular number 0." + result.set(index, new Number(0)); + result.remove(index + 1); + result.remove(index + 1); + result.remove(index + 1); + result.remove(index + 1); + return Collections.unmodifiableList(result); + } + + /** + * @param expression a raw representation of a snailfish number + * @return the index of the most appropriate pair to explode (opening brace) or -1 if no explosion is appropriate + */ + int getExplosionIndex(final List expression) { + int depth = -1; + int maxDepth = Integer.MIN_VALUE; + int result = -1; + for (int i = 0; i < expression.size(); i++) { + final var token = expression.get(i); + if (token == Symbol.START_PAIR) { + depth++; + } else if (token == Symbol.END_PAIR) { + depth--; + } + if (depth > maxDepth) { + maxDepth = depth; + result = i; + } + } + return result > 3 ? result : -1; + } + + } + + @Nested + public class SnailfishNumberTest { + + @Test + public final void testAdd() { + assertEquals(new SnailfishNumber("[[[[0,7],4],[[7,8],[6,0]]],[8,1]]"), + new SnailfishNumber("[[[[4,3],4],4],[7,[[8,4],9]]]") + .add(new SnailfishNumber("[1,1]")) + .reduce()); + // either this example is broken or my bug is not triggered in the real puzzle input T_T +// assertEquals(new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]"), +// new SnailfishNumber("[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]]") +// .add(new SnailfishNumber("[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]]")) +// .reduce()); + } + + @Test + public final void testAddList() { + // given + final var lines = """ + [[[0,[4,5]],[0,0]],[[[4,5],[2,6]],[9,5]]] + [7,[[[3,7],[4,3]],[[6,3],[8,8]]]] + [[2,[[0,8],[3,4]]],[[[6,7],1],[7,[1,6]]]] + [[[[2,4],7],[6,[0,5]]],[[[6,8],[2,8]],[[2,1],[4,5]]]] + [7,[5,[[3,8],[1,4]]]] + [[2,[2,2]],[8,[8,1]]] + [2,9] + [1,[[[9,3],9],[[9,0],[0,7]]]] + [[[5,[7,4]],7],1] + [[[[4,2],2],6],[8,7]]"""; + final var list = Arrays.stream(lines.split("\n")) + .map(SnailfishNumber::new) + .collect(Collectors.toList()); + + // when + var sum = list.get(0); + for (final var addend : list.subList(1, list.size())) { + sum = sum.add(addend).reduce(); + } + + // then + assertEquals(new SnailfishNumber("[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]"), + sum); + } + + @Test + public final void testSplit() { + final var instance = new SnailfishNumber(Collections.emptyList()); + + assertEquals(parse("[5, 5]"), + instance.split(parse("10"), 0)); + assertEquals(parse("[5, 6]"), + instance.split(parse("11"), 0)); + assertEquals(parse("[6, 6]"), + instance.split(parse("12"), 0)); + } + + @Test + public final void testExplosionIndex() { + final var instance = new SnailfishNumber(Collections.emptyList()); + assertEquals(4, + instance.getExplosionIndex(parse("[[[[[9,8],1],2],3],4]"))); + assertEquals(12, + instance.getExplosionIndex(parse("[7,[6,[5,[4,[3,2]]]]]"))); + assertEquals(10, + instance.getExplosionIndex(parse("[[6,[5,[4,[3,2]]]],1]"))); + assertEquals(10, + instance.getExplosionIndex(parse("[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]"))); + assertEquals(24, + instance.getExplosionIndex(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"))); + } + + @Test + public final void testExplode() { + final var instance = new SnailfishNumber(Collections.emptyList()); + assertEquals(parse("[[[[0,9],2],3],4]"), + instance + .explode(parse("[[[[[9,8],1],2],3],4]"), 4)); + assertEquals(parse("[7,[6,[5,[7,0]]]]"), + instance + .explode(parse("[7,[6,[5,[4,[3,2]]]]]"), 12)); + assertEquals(parse("[[6,[5,[7,0]]],3]"), + instance + .explode(parse("[[6,[5,[4,[3,2]]]],1]"), 10)); + assertEquals(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"), + instance + .explode(parse("[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]"), 10)); + assertEquals(parse("[[3,[2,[8,0]]],[9,[5,[7,0]]]]"), + instance + .explode(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"), 24)); + } + + @Test + public final void testMagnitude() { + assertEquals(29, new SnailfishNumber("[9,1]").magnitude()); + assertEquals(21, new SnailfishNumber("[1,9]").magnitude()); + assertEquals(143, new SnailfishNumber("[[1,2],[[3,4],5]]").magnitude()); + assertEquals(1384, new SnailfishNumber("[[[[0,7],4],[[7,8],[6,0]]],[8,1]]").magnitude()); + assertEquals(445, new SnailfishNumber("[[[[1,1],[2,2]],[3,3]],[4,4]]").magnitude()); + assertEquals(791, new SnailfishNumber("[[[[3,0],[5,3]],[4,4]],[5,5]]").magnitude()); + assertEquals(1137, new SnailfishNumber("[[[[5,0],[7,4]],[5,5]],[6,6]]").magnitude()); + assertEquals(3488, new SnailfishNumber("[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]").magnitude()); + assertEquals(3993, new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]").magnitude()); + } + + @Test + public final void verifyStableState() { + // given + final var original = new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]"); + + // when + final var reduced = original.reduce(); + + // then + assertEquals(original, reduced); + } + } + + + @Test + public final void part1() { + final var list = + getInput().map(SnailfishNumber::new).collect(Collectors.toList()); + var sum = list.get(0); + for (final var addend : list.subList(1, list.size())) { + sum = sum.add(addend).reduce(); + } + System.out.println("Part 1: " + sum.magnitude()); + } + + @Test + public final void part2() { + final var list = + getInput().map(SnailfishNumber::new).collect(Collectors.toList()); + int max = Integer.MIN_VALUE; + for (final var x : list) { + for (final var y : list) { + if (x.equals(y)) { + continue; + } + final var sum = x.add(y).reduce(); + final var magnitude = sum.magnitude(); + if (magnitude > max) { + max = magnitude; + } + } + } + System.out.println("Part 2: " + max); + } + +} \ 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..1368dc4 --- /dev/null +++ b/src/test/resources/sample/day-18.txt @@ -0,0 +1,10 @@ +[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]] +[[[5,[2,8]],4],[5,[[9,9],0]]] +[6,[[[6,2],[5,6]],[[7,6],[4,7]]]] +[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]] +[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]] +[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]] +[[[[5,4],[7,7]],8],[[8,3],8]] +[[9,3],[[9,9],[6,[4,9]]]] +[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]] +[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]] From 4409fc74d4ad370a32ed399f28f74dd3325988eb Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sun, 19 Dec 2021 15:03:49 -0800 Subject: [PATCH 18/44] Day 19 --- src/test/java/com/macasaet/Day19.java | 395 ++++++++++++++++++++++++++ src/test/resources/sample/day-19.txt | 136 +++++++++ 2 files changed, 531 insertions(+) create mode 100644 src/test/java/com/macasaet/Day19.java create mode 100644 src/test/resources/sample/day-19.txt diff --git a/src/test/java/com/macasaet/Day19.java b/src/test/java/com/macasaet/Day19.java new file mode 100644 index 0000000..a11c202 --- /dev/null +++ b/src/test/java/com/macasaet/Day19.java @@ -0,0 +1,395 @@ +package com.macasaet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * --- Day 19: Beacon Scanner --- + */ +public class Day19 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-19.txt"), + false); + } + + protected List getScanners() { + final var list = getInput().toList(); + final var result = new ArrayList(); + var observations = new HashSet(); + int id = -1; + for (final var line : list) { + if (line.startsWith("--- scanner ")) { + id = Integer.parseInt(line + .replaceFirst("^--- scanner ", "") + .replaceFirst(" ---$", "")); + } else if (!line.isBlank()) { + observations.add(Position.parse(line)); + } else { // line is blank + result.add(new Scanner(id, Collections.unmodifiableSet(observations))); + observations = new HashSet<>(); + id = -1; + } + } + result.add(new Scanner(id, Collections.unmodifiableSet(observations))); + return Collections.unmodifiableList(result); + } + + public enum Direction { + POSITIVE_X { + public Position face(Position position) { + return position; + } + }, + NEGATIVE_X { + public Position face(Position position) { + return new Position(-position.x(), position.y(), -position.z()); + } + }, + POSITIVE_Y { + public Position face(Position position) { + return new Position(position.y(), -position.x(), position.z()); + } + }, + NEGATIVE_Y { + public Position face(Position position) { + return new Position(-position.y(), position.x(), position.z()); + } + }, + POSITIVE_Z { + public Position face(Position position) { + return new Position(position.z(), position.y(), -position.x()); + } + }, + NEGATIVE_Z { + public Position face(Position position) { + return new Position(-position.z(), position.y(), position.x()); + } + }; + + public abstract Position face(final Position position); + + } + + public enum Rotation { + r0 { + public Position rotate(final Position position) { + return position; + } + }, + r90 { + public Position rotate(Position position) { + return new Position(position.x(), -position.z(), position.y()); + } + }, + r180 { + public Position rotate(Position position) { + return new Position(position.x(), -position.y(), -position.z()); + } + }, + r270 { + public Position rotate(Position position) { + return new Position(position.x(), position.z(), -position.y()); + } + }; + + public abstract Position rotate(final Position position); + } + + public record Transformation(Direction direction, Rotation rotation) { + /** + * Look at a position from a specific orientation + * + * @param position a position relative to one point of view + * @return the same position relative to a different point of view + */ + public Position reorient(final Position position) { + return rotation.rotate(direction.face(position)); + } + + } + + public record Position(int x, int y, int z) { + public static Position parse(final String line) { + final var components = line.split(","); + return new Position(Integer.parseInt(components[0]), + Integer.parseInt(components[1]), + Integer.parseInt(components[2])); + } + + public Position plus(Position amount) { + return new Position(x() + amount.x(), y() + amount.y(), z() + amount.z()); + } + + public Position minus(final Position other) { + return new Position(x() - other.x(), y() - other.y(), z() - other.z()); + } + } + + public interface OverlapResult { + } + + public record Overlap(Position distance, Transformation transformation, + Set overlappingBeacons) implements OverlapResult { + } + + public record None() implements OverlapResult { + } + + public record Scanner(int id, Set observations) { + + public OverlapResult getOverlappingBeacons(final Scanner other) { + for (final var direction : Direction.values()) { + for (final var rotation : Rotation.values()) { + final var transformation = new Transformation(direction, rotation); + final var distances = observations().stream() + .flatMap(a -> other.observations() + .stream() + .map(transformation::reorient) + .map(a::minus)) + .collect(Collectors.toList()); + for (final var offset : distances) { + final var intersection = other.observations() + .stream() + .map(transformation::reorient) + .map(observation -> observation.plus(offset)) + .filter(observations()::contains) + .collect(Collectors.toUnmodifiableSet()); + if (intersection.size() >= 12) { + return new Overlap(offset, transformation, intersection); + } + } + } + } + return new None(); + } + + } + + @Nested + public class ScannerTest { + @Test + public final void testOverlapWithOrigin() { + // given + final var scanner0Observations = """ + 404,-588,-901 + 528,-643,409 + -838,591,734 + 390,-675,-793 + -537,-823,-458 + -485,-357,347 + -345,-311,381 + -661,-816,-575 + -876,649,763 + -618,-824,-621 + 553,345,-567 + 474,580,667 + -447,-329,318 + -584,868,-557 + 544,-627,-890 + 564,392,-477 + 455,729,728 + -892,524,684 + -689,845,-530 + 423,-701,434 + 7,-33,-71 + 630,319,-379 + 443,580,662 + -789,900,-551 + 459,-707,401 + """; + final var scanner1Observations = """ + 686,422,578 + 605,423,415 + 515,917,-361 + -336,658,858 + 95,138,22 + -476,619,847 + -340,-569,-846 + 567,-361,727 + -460,603,-452 + 669,-402,600 + 729,430,532 + -500,-761,534 + -322,571,750 + -466,-666,-811 + -429,-592,574 + -355,545,-477 + 703,-491,-529 + -328,-685,520 + 413,935,-424 + -391,539,-444 + 586,-435,557 + -364,-763,-893 + 807,-499,-711 + 755,-354,-619 + 553,889,-390 + """; + final var scanner0 = new Scanner(0, + Arrays.stream(scanner0Observations.split("\n")) + .map(Position::parse) + .collect(Collectors.toUnmodifiableSet())); + final var scanner1 = new Scanner(0, + Arrays.stream(scanner1Observations.split("\n")) + .map(Position::parse) + .collect(Collectors.toUnmodifiableSet())); + + // when + final var result = scanner0.getOverlappingBeacons(scanner1); + + // then + assertTrue(result instanceof Overlap); + final var overlap = (Overlap) result; + assertEquals(12, overlap.overlappingBeacons().size()); + } + + @Test + public final void testOverlapWithNonOrigin() { + // given + final var scanner1Observations = """ + 686,422,578 + 605,423,415 + 515,917,-361 + -336,658,858 + 95,138,22 + -476,619,847 + -340,-569,-846 + 567,-361,727 + -460,603,-452 + 669,-402,600 + 729,430,532 + -500,-761,534 + -322,571,750 + -466,-666,-811 + -429,-592,574 + -355,545,-477 + 703,-491,-529 + -328,-685,520 + 413,935,-424 + -391,539,-444 + 586,-435,557 + -364,-763,-893 + 807,-499,-711 + 755,-354,-619 + 553,889,-390 + """; + final var scanner4Observations = """ + 727,592,562 + -293,-554,779 + 441,611,-461 + -714,465,-776 + -743,427,-804 + -660,-479,-426 + 832,-632,460 + 927,-485,-438 + 408,393,-506 + 466,436,-512 + 110,16,151 + -258,-428,682 + -393,719,612 + -211,-452,876 + 808,-476,-593 + -575,615,604 + -485,667,467 + -680,325,-822 + -627,-443,-432 + 872,-547,-609 + 833,512,582 + 807,604,487 + 839,-516,451 + 891,-625,532 + -652,-548,-490 + 30,-46,-14 + """; + final var scanner1 = new Scanner(0, + Arrays.stream(scanner1Observations.split("\n")) + .map(Position::parse) + .collect(Collectors.toUnmodifiableSet())); + final var scanner4 = new Scanner(0, + Arrays.stream(scanner4Observations.split("\n")) + .map(Position::parse) + .collect(Collectors.toUnmodifiableSet())); + + // when + final var result = scanner1.getOverlappingBeacons(scanner4); + + // then + assertTrue(result instanceof Overlap); + final var overlap = (Overlap) result; + assertEquals(12, overlap.overlappingBeacons().size()); + } + } + + @Test + public final void part1() { + final var scanners = getScanners(); + final var knownBeacons = new HashSet(); + final var origin = new Scanner(-1, knownBeacons); + final var remaining = new ArrayList<>(scanners); + while (!remaining.isEmpty()) { + final var other = remaining.remove(0); + if (knownBeacons.isEmpty()) { + knownBeacons.addAll(other.observations()); + continue; + } + final var result = origin.getOverlappingBeacons(other); + if (result instanceof final Overlap overlap) { + knownBeacons.addAll(other.observations() + .stream() + .map(overlap.transformation()::reorient) + .map(observation -> observation.plus(overlap.distance())) + .collect(Collectors.toList())); + } else { + remaining.add(other); + } + } + System.out.println("Part 1: " + knownBeacons.size()); + } + + @Test + public final void part2() { + final var scanners = getScanners(); + final var knownBeacons = new HashSet(); + final var origin = new Scanner(-1, knownBeacons); + final var remaining = new ArrayList<>(scanners); + final var distances = new HashSet(); + while (!remaining.isEmpty()) { + final var other = remaining.remove(0); + if (knownBeacons.isEmpty()) { + knownBeacons.addAll(other.observations()); + continue; + } + final var result = origin.getOverlappingBeacons(other); + if (result instanceof final Overlap overlap) { + knownBeacons.addAll(other.observations() + .stream() + .map(overlap.transformation()::reorient) + .map(observation -> observation.plus(overlap.distance())) + .collect(Collectors.toList())); + distances.add(overlap.distance()); + } else { + remaining.add(other); + } + } + int maxDistance = Integer.MIN_VALUE; + for (final var x : distances) { + for (final var y : distances) { + final int distance = Math.abs(x.x() - y.x()) + + Math.abs(x.y() - y.y()) + + Math.abs(x.z() - y.z()); + maxDistance = Math.max(maxDistance, distance); + } + } + System.out.println("Part 2: " + maxDistance); + } + +} \ No newline at end of file diff --git a/src/test/resources/sample/day-19.txt b/src/test/resources/sample/day-19.txt new file mode 100644 index 0000000..4e496e9 --- /dev/null +++ b/src/test/resources/sample/day-19.txt @@ -0,0 +1,136 @@ +--- scanner 0 --- +404,-588,-901 +528,-643,409 +-838,591,734 +390,-675,-793 +-537,-823,-458 +-485,-357,347 +-345,-311,381 +-661,-816,-575 +-876,649,763 +-618,-824,-621 +553,345,-567 +474,580,667 +-447,-329,318 +-584,868,-557 +544,-627,-890 +564,392,-477 +455,729,728 +-892,524,684 +-689,845,-530 +423,-701,434 +7,-33,-71 +630,319,-379 +443,580,662 +-789,900,-551 +459,-707,401 + +--- scanner 1 --- +686,422,578 +605,423,415 +515,917,-361 +-336,658,858 +95,138,22 +-476,619,847 +-340,-569,-846 +567,-361,727 +-460,603,-452 +669,-402,600 +729,430,532 +-500,-761,534 +-322,571,750 +-466,-666,-811 +-429,-592,574 +-355,545,-477 +703,-491,-529 +-328,-685,520 +413,935,-424 +-391,539,-444 +586,-435,557 +-364,-763,-893 +807,-499,-711 +755,-354,-619 +553,889,-390 + +--- scanner 2 --- +649,640,665 +682,-795,504 +-784,533,-524 +-644,584,-595 +-588,-843,648 +-30,6,44 +-674,560,763 +500,723,-460 +609,671,-379 +-555,-800,653 +-675,-892,-343 +697,-426,-610 +578,704,681 +493,664,-388 +-671,-858,530 +-667,343,800 +571,-461,-707 +-138,-166,112 +-889,563,-600 +646,-828,498 +640,759,510 +-630,509,768 +-681,-892,-333 +673,-379,-804 +-742,-814,-386 +577,-820,562 + +--- scanner 3 --- +-589,542,597 +605,-692,669 +-500,565,-823 +-660,373,557 +-458,-679,-417 +-488,449,543 +-626,468,-788 +338,-750,-386 +528,-832,-391 +562,-778,733 +-938,-730,414 +543,643,-506 +-524,371,-870 +407,773,750 +-104,29,83 +378,-903,-323 +-778,-728,485 +426,699,580 +-438,-605,-362 +-469,-447,-387 +509,732,623 +647,635,-688 +-868,-804,481 +614,-800,639 +595,780,-596 + +--- scanner 4 --- +727,592,562 +-293,-554,779 +441,611,-461 +-714,465,-776 +-743,427,-804 +-660,-479,-426 +832,-632,460 +927,-485,-438 +408,393,-506 +466,436,-512 +110,16,151 +-258,-428,682 +-393,719,612 +-211,-452,876 +808,-476,-593 +-575,615,604 +-485,667,467 +-680,325,-822 +-627,-443,-432 +872,-547,-609 +833,512,582 +807,604,487 +839,-516,451 +891,-625,532 +-652,-548,-490 +30,-46,-14 From a943bfddbf44f74414f95c3550c742654524c9f3 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sun, 19 Dec 2021 18:37:51 -0800 Subject: [PATCH 19/44] Day 16 --- src/test/java/com/macasaet/Day16.java | 352 ++++++++++++++++++++++++++ src/test/resources/sample/day-16.txt | 1 + 2 files changed, 353 insertions(+) create mode 100644 src/test/java/com/macasaet/Day16.java create mode 100644 src/test/resources/sample/day-16.txt diff --git a/src/test/java/com/macasaet/Day16.java b/src/test/java/com/macasaet/Day16.java new file mode 100644 index 0000000..b3eb4b0 --- /dev/null +++ b/src/test/java/com/macasaet/Day16.java @@ -0,0 +1,352 @@ +package com.macasaet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * --- Day 16: Packet Decoder --- + */ +public class Day16 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-16.txt"), + false); + } + + public interface Packet { + long version(); + + void accept(PacketVisitor packetVisitor); + + long evaluate(); + } + + public record Literal(long version, long value) implements Packet { + + public void accept(PacketVisitor packetVisitor) { + packetVisitor.visit(this); + } + + public long evaluate() { + return value(); + } + } + + public enum OperatorType { + SUM { + public long evaluate(List operands) { + return operands.stream().mapToLong(Packet::evaluate).sum(); + } + }, + PRODUCT { + public long evaluate(List operands) { + return operands.stream().mapToLong(Packet::evaluate).reduce(1, (x, y) -> x * y); + } + }, + MINIMUM { + public long evaluate(List operands) { + return operands.stream().mapToLong(Packet::evaluate).min().orElseThrow(); + } + }, + MAXIMUM { + public long evaluate(List operands) { + return operands.stream().mapToLong(Packet::evaluate).max().orElseThrow(); + } + }, + GREATER_THAN { + public long evaluate(List operands) { + if (operands.size() != 2) { + throw new IllegalArgumentException("Invalid operand list for \"greater than\" operator: " + operands); + } + final var x = operands.get(0).evaluate(); + final var y = operands.get(1).evaluate(); + return x > y ? 1 : 0; + } + }, + LESS_THAN { + public long evaluate(List operands) { + if (operands.size() != 2) { + throw new IllegalStateException("Invalid operand list for \"less than\" operator: " + operands); + } + final var x = operands.get(0).evaluate(); + final var y = operands.get(1).evaluate(); + return x < y ? 1 : 0; + } + }, + EQUAL_TO { + public long evaluate(List operands) { + if (operands.size() != 2) { + throw new IllegalStateException("Invalid operand list for \"equal to\" operator: " + operands); + } + final var x = operands.get(0).evaluate(); + final var y = operands.get(1).evaluate(); + return x == y ? 1 : 0; + } + }; + + public abstract long evaluate(List operands); + + public static OperatorType forId(final int typeId) { + return switch (typeId) { + case 0 -> SUM; + case 1 -> PRODUCT; + case 2 -> MINIMUM; + case 3 -> MAXIMUM; + case 5 -> GREATER_THAN; + case 6 -> LESS_THAN; + case 7 -> EQUAL_TO; + default -> throw new IllegalArgumentException("Invalid operator type ID: " + typeId); + }; + } + } + + public record Operator(long version, OperatorType operatorType, List operands) implements Packet { + + public void accept(PacketVisitor packetVisitor) { + packetVisitor.enter(this); + for (final var subPacket : operands()) { + subPacket.accept(packetVisitor); + } + packetVisitor.exit(this); + } + + public long evaluate() { + return operatorType().evaluate(operands()); + } + } + + public interface PacketVisitor { + void visit(Literal literal); + + void enter(Operator operator); + + void exit(Operator operator); + } + + public static class PacketBuilder { + + private long version; + private long typeId; + private OptionalLong literalValue = OptionalLong.empty(); + private final List subPackets = new ArrayList<>(); + + public Packet readHex(final String hexString) { + final var hexDigits = hexString.toCharArray(); + final var bits = hexToBits(hexDigits); + read(bits, 0); + return toPacket(); + } + + public int read(final List bits, int transmissionCursor) { + final var versionBits = bits.subList(transmissionCursor, transmissionCursor + 3); + transmissionCursor += 3; + this.version = toLong(versionBits); + + final var typeBits = bits.subList(transmissionCursor, transmissionCursor + 3); + transmissionCursor += 3; + this.typeId = toLong(typeBits); + + // TODO consider adding methods to parse each type specifically + if (this.typeId == 4) { + boolean finalGroup = false; + final var literalBits = new ArrayList(); + while (!finalGroup) { + final var groupBits = bits.subList(transmissionCursor, transmissionCursor + 5); + transmissionCursor += 5; + finalGroup = groupBits.get(0) == 0; + literalBits.addAll(groupBits.subList(1, 5)); + } + if (literalBits.size() > 63) { + throw new IllegalArgumentException("Literal is too large for an long: " + literalBits.size()); + } + literalValue = OptionalLong.of(toLong(literalBits)); + return transmissionCursor; + } else { + final var lengthTypeIdBits = bits.subList(transmissionCursor, transmissionCursor + 1); + transmissionCursor += 1; + final var lengthTypeId = toLong(lengthTypeIdBits); + if (lengthTypeId == 0) { + final var lengthOfSubPacketsBits = bits.subList(transmissionCursor, transmissionCursor + 15); + transmissionCursor += 15; + final var lengthOfSubPackets = toLong(lengthOfSubPacketsBits); + int bitsRead = 0; + while (bitsRead < lengthOfSubPackets) { + final var subPacketBuilder = new PacketBuilder(); + final var newCursor = subPacketBuilder.read(bits, transmissionCursor); + final var subPacketSize = newCursor - transmissionCursor; // size of sub-packet in bits + transmissionCursor = newCursor; + + subPackets.add(subPacketBuilder.toPacket()); + bitsRead += subPacketSize; + } + return transmissionCursor; + } else if (lengthTypeId == 1) { + final var numSubPacketsBits = bits.subList(transmissionCursor, transmissionCursor + 11); + transmissionCursor += 11; + final var numSubPackets = toLong(numSubPacketsBits); + for (int packetsRead = 0; packetsRead < numSubPackets; packetsRead++) { + final var subPacketBuilder = new PacketBuilder(); + transmissionCursor = subPacketBuilder.read(bits, transmissionCursor); + subPackets.add(subPacketBuilder.toPacket()); + } + return transmissionCursor; + } else { + throw new IllegalArgumentException("Invalid length type ID: " + lengthTypeId); + } + } + } + + public Packet toPacket() { + if (typeId == 4) { + return new Literal(version, literalValue.orElseThrow()); + } else { + return new Operator(version, OperatorType.forId((int) typeId), subPackets); + } + } + + protected long toLong(final List bits) { + long result = 0; + for (int i = 0; i < bits.size(); i++) { + final var bit = bits.get(i); + if (bit == 1) { + final long shiftDistance = bits.size() - i - 1; + result |= 1L << shiftDistance; + } else if (bit != 0) { + throw new IllegalArgumentException("Invalid bit representation of an integer: " + bits); + } + } + return result; + } + + protected List hexToBits(final char[] hexDigits) { + final var result = new ArrayList(hexDigits.length * 4); + for (final var digit : hexDigits) { + final var bits = switch (digit) { + case '0' -> Arrays.asList((byte) 0, (byte) 0, (byte) 0, (byte) 0); + case '1' -> Arrays.asList((byte) 0, (byte) 0, (byte) 0, (byte) 1); + case '2' -> Arrays.asList((byte) 0, (byte) 0, (byte) 1, (byte) 0); + case '3' -> Arrays.asList((byte) 0, (byte) 0, (byte) 1, (byte) 1); + case '4' -> Arrays.asList((byte) 0, (byte) 1, (byte) 0, (byte) 0); + case '5' -> Arrays.asList((byte) 0, (byte) 1, (byte) 0, (byte) 1); + case '6' -> Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 0); + case '7' -> Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 1); + case '8' -> Arrays.asList((byte) 1, (byte) 0, (byte) 0, (byte) 0); + case '9' -> Arrays.asList((byte) 1, (byte) 0, (byte) 0, (byte) 1); + case 'A', 'a' -> Arrays.asList((byte) 1, (byte) 0, (byte) 1, (byte) 0); + case 'B', 'b' -> Arrays.asList((byte) 1, (byte) 0, (byte) 1, (byte) 1); + case 'C', 'c' -> Arrays.asList((byte) 1, (byte) 1, (byte) 0, (byte) 0); + case 'D', 'd' -> Arrays.asList((byte) 1, (byte) 1, (byte) 0, (byte) 1); + case 'E', 'e' -> Arrays.asList((byte) 1, (byte) 1, (byte) 1, (byte) 0); + case 'F', 'f' -> Arrays.asList((byte) 1, (byte) 1, (byte) 1, (byte) 1); + default -> throw new IllegalStateException("Unexpected value: " + digit); + }; + result.addAll(bits); + } + return Collections.unmodifiableList(result); + } + } + + @Test + public final void testParseLiteral() { + // given + final var input = "D2FE28"; + final var builder = new PacketBuilder(); + + // when + final var result = builder.readHex(input); + + // then + assertTrue(result instanceof Literal); + final var literal = (Literal) result; + assertEquals(2021, literal.value); + } + + @Test + public final void testOperatorWithTwoSubPackets() { + // given + final var input = "38006F45291200"; + final var builder = new PacketBuilder(); + + // when + final var result = builder.readHex(input); + + // then + assertTrue(result instanceof Operator); + final var operator = (Operator) result; + assertEquals(1, operator.version()); + assertEquals(OperatorType.LESS_THAN, operator.operatorType()); + assertEquals(2, operator.operands().size()); + final var a = (Literal) operator.operands().get(0); + assertEquals(10, a.value()); + final var b = (Literal) operator.operands().get(1); + assertEquals(20, b.value()); + } + + @Test + public final void part1() { + final var line = getInput().collect(Collectors.toList()).get(0); + final var builder = new PacketBuilder(); + final var packet = builder.readHex(line); + class VersionSummer implements PacketVisitor { + + int sum = 0; + + public void visit(Literal literal) { + sum += literal.version(); + } + + public void enter(Operator operator) { + } + + public void exit(Operator operator) { + sum += operator.version(); + } + } + final var summer = new VersionSummer(); + packet.accept(summer); + + System.out.println("Part 1: " + summer.sum); + } + + @Test + public final void part2() { + final var line = getInput().collect(Collectors.toList()).get(0); + final var builder = new PacketBuilder(); + final var packet = builder.readHex(line); + System.out.println("Part 2: " + packet.evaluate()); + } + + @Nested + public class PacketBuilderTest { + @Test + public void testToInt() { + // given + final var builder = new PacketBuilder(); + // when + // then + assertEquals(2021, builder.toLong(Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 1))); + } + + @Test + public final void testMaths() { + assertEquals(3, new PacketBuilder().readHex("C200B40A82").evaluate()); + assertEquals(54, new PacketBuilder().readHex("04005AC33890").evaluate()); + assertEquals(7, new PacketBuilder().readHex("880086C3E88112").evaluate()); + assertEquals(9, new PacketBuilder().readHex("CE00C43D881120").evaluate()); + assertEquals(1, new PacketBuilder().readHex("D8005AC2A8F0").evaluate()); + assertEquals(0, new PacketBuilder().readHex("F600BC2D8F").evaluate()); + assertEquals(0, new PacketBuilder().readHex("9C005AC2F8F0").evaluate()); + assertEquals(1, new PacketBuilder().readHex("9C0141080250320F1802104A08").evaluate()); + } + } + +} \ No newline at end of file diff --git a/src/test/resources/sample/day-16.txt b/src/test/resources/sample/day-16.txt new file mode 100644 index 0000000..3f0eda1 --- /dev/null +++ b/src/test/resources/sample/day-16.txt @@ -0,0 +1 @@ +D2FE28 From 1eee1992211fcd651ebd9ff3194535fc482a053f Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Mon, 20 Dec 2021 11:10:21 -0800 Subject: [PATCH 20/44] Day 20 --- src/test/java/com/macasaet/Day20.java | 233 ++++++++++++++++++++++++++ src/test/resources/sample/day-20.txt | 7 + 2 files changed, 240 insertions(+) create mode 100644 src/test/java/com/macasaet/Day20.java create mode 100644 src/test/resources/sample/day-20.txt diff --git a/src/test/java/com/macasaet/Day20.java b/src/test/java/com/macasaet/Day20.java new file mode 100644 index 0000000..85115e9 --- /dev/null +++ b/src/test/java/com/macasaet/Day20.java @@ -0,0 +1,233 @@ +package com.macasaet; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * --- Day 20: Trench Map --- + */ +public class Day20 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-20.txt"), + false); + } + + public record ImageEnhancementAlgorithm(boolean[] map) { + + public boolean isPixelLit(final int code) { + return map()[code]; + } + + public static ImageEnhancementAlgorithm parse(final String line) { + final var map = new boolean[line.length()]; + for (int i = line.length(); --i >= 0; map[i] = line.charAt(i) == '#') ; + return new ImageEnhancementAlgorithm(map); + } + } + + protected record Coordinate(int x, int y) { + } + + public record Image(SortedMap> pixels, int minX, int maxX, int minY, + int maxY, boolean isEven) { + public Image enhance(final ImageEnhancementAlgorithm algorithm) { + final var enhancedPixelMap = new TreeMap>(); + int enhancedMinX = minX; + int enhancedMaxX = maxX; + int enhancedMinY = minY; + int enhancedMaxY = maxY; + for (int i = minX - 1; i <= maxX + 1; i++) { + final var targetRow = new TreeMap(); + for (int j = minY - 1; j <= maxY + 1; j++) { + final int replacementId = decode(i, j, !isEven && algorithm.isPixelLit(0)); + final var shouldLight = algorithm.isPixelLit(replacementId); + if (shouldLight) { + // save space by only storing an entry when lit + targetRow.put(j, true); + enhancedMinY = Math.min(enhancedMinY, j); + enhancedMaxY = Math.max(enhancedMaxY, j); + } + } + if (!targetRow.isEmpty()) { + // save space by only storing a row if at least one cell is lit + enhancedPixelMap.put(i, Collections.unmodifiableSortedMap(targetRow)); + enhancedMinX = Math.min(enhancedMinX, i); + enhancedMaxX = Math.max(enhancedMaxX, i); + } + } + return new Image(Collections.unmodifiableSortedMap(enhancedPixelMap), enhancedMinX, enhancedMaxX, enhancedMinY, enhancedMaxY, !isEven); + } + + int decode(final int x, final int y, boolean voidIsLit) { + final var list = getNeighbouringCoordinates(x, y).stream() + .map(coordinate -> isBitSet(coordinate, voidIsLit)) + .toList(); + int result = 0; + for (int i = list.size(); --i >= 0; ) { + if (list.get(i)) { + final int shiftDistance = list.size() - i - 1; + result |= 1 << shiftDistance; + } + } + if (result < 0 || result > 512) { + throw new IllegalStateException("Unable to decode pixel at " + x + ", " + y); + } + return result; + } + + boolean isBitSet(final Coordinate coordinate, boolean voidIsLit) { + final var row = pixels().get(coordinate.x()); + if((coordinate.x() < minX || coordinate.x() > maxX) && row == null) { + return voidIsLit; + } + else if(row == null) { + return false; + } + return row.getOrDefault(coordinate.y(), (coordinate.y() < minY || coordinate.y() > maxY) && voidIsLit); + } + + List getNeighbouringCoordinates(int x, int y) { + return Arrays.asList( + new Coordinate(x - 1, y - 1), + new Coordinate(x - 1, y), + new Coordinate(x - 1, y + 1), + new Coordinate(x, y - 1), + new Coordinate(x, y), + new Coordinate(x, y + 1), + new Coordinate(x + 1, y - 1), + new Coordinate(x + 1, y), + new Coordinate(x + 1, y + 1) + ); + } + + public long countLitPixels() { + return pixels().values() + .stream() + .flatMap(row -> row.values().stream()) + .filter(isLit -> isLit) + .count(); + } + + public int width() { + return maxX - minX + 1; + } + + public int height() { + return maxY - minY + 1; + } + + public String toString() { + final var builder = new StringBuilder(); + builder.append(width()).append('x').append(height()).append('\n'); + for (int i = minX; i <= maxX; i++) { + final var row = pixels.getOrDefault(i, Collections.emptySortedMap()); + for (int j = minY; j <= maxY; j++) { + final var value = row.getOrDefault(j, false); + builder.append(value ? '#' : '.'); + } + builder.append('\n'); + } + return builder.toString(); + } + + public static Image parse(final List lines) { + final var pixels = new TreeMap>(); + final int minX = 0; + final int minY = 0; + int maxX = 0; + int maxY = 0; + for (int i = lines.size(); --i >= 0; ) { + final var line = lines.get(i); + final var row = new TreeMap(); + for (int j = line.length(); --j >= 0; ) { + final var pixel = line.charAt(j); + row.put(j, pixel == '#'); + if (pixel == '#') { + row.put(j, true); + maxY = Math.max(maxY, j); + } + } + if (!row.isEmpty()) { + maxX = Math.max(maxX, i); + pixels.put(i, Collections.unmodifiableSortedMap(row)); + } + } + return new Image(Collections.unmodifiableSortedMap(pixels), minX, maxX, minY, maxY, true); + } + + } + + @Nested + public class ImageTest { + @Test + public final void testToInt() { + final var string = """ + #..#. + #.... + ##..# + ..#.. + ..### + """; + final var image = Image.parse(Arrays.asList(string.split("\n"))); + assertEquals(34, image.decode(2, 2, false)); + } + + @Test + public final void flipAllOn() { + final var template = "#........"; + final var imageString = """ + ... + ... + ... + """; + final var image = Image.parse(Arrays.asList(imageString.split("\n"))); + final var result = image.enhance(ImageEnhancementAlgorithm.parse(template)); + assertTrue(result.pixels().get(1).get(1)); + } + + @Test + public final void turnOffPixel() { + final var templateBuilder = new StringBuilder(); + for (int i = 511; --i >= 0; templateBuilder.append('#')) ; + templateBuilder.append('.'); + final var template = templateBuilder.toString(); + final var imageString = """ + ### + ### + ### + """; + final var image = Image.parse(Arrays.asList(imageString.split("\n"))); + final var result = image.enhance(ImageEnhancementAlgorithm.parse(template)); + final var middleRow = result.pixels().get(1); + assertFalse(middleRow.containsKey(1)); + } + } + + @Test + public final void part1() { + final var list = getInput().toList(); + final var algorithm = ImageEnhancementAlgorithm.parse(list.get(0)); + final var image = Image.parse(list.subList(2, list.size())) + .enhance(algorithm) + .enhance(algorithm); + System.out.println("Part 1: " + image.countLitPixels()); + } + + @Test + public final void part2() { + final var list = getInput().toList(); + final var algorithm = ImageEnhancementAlgorithm.parse(list.get(0)); + var image = Image.parse(list.subList(2, list.size())); + for(int _i = 50; --_i >= 0; image = image.enhance(algorithm)); + System.out.println("Part 2: " + image.countLitPixels()); + } + +} \ 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..8fa4bd4 --- /dev/null +++ b/src/test/resources/sample/day-20.txt @@ -0,0 +1,7 @@ +..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..# + +#..#. +#.... +##..# +..#.. +..### From 7afb2cbc116c856cdb4a7f68f7ef026ecb3fb741 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Mon, 20 Dec 2021 23:15:46 -0800 Subject: [PATCH 21/44] Day 21 --- src/test/java/com/macasaet/Day21.java | 159 ++++++++++++++++++++++++++ src/test/resources/sample/day-21.txt | 2 + 2 files changed, 161 insertions(+) create mode 100644 src/test/java/com/macasaet/Day21.java create mode 100644 src/test/resources/sample/day-21.txt diff --git a/src/test/java/com/macasaet/Day21.java b/src/test/java/com/macasaet/Day21.java new file mode 100644 index 0000000..1223048 --- /dev/null +++ b/src/test/java/com/macasaet/Day21.java @@ -0,0 +1,159 @@ +package com.macasaet; + +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Test; + +/** + * --- Day 21: Dirac Dice --- + */ +public class Day21 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-21.txt"), + false); + } + + public static class DeterministicDie { + int value; + int totalRolls = 0; + + protected DeterministicDie(final int startingValue) { + this.value = startingValue; + } + + public DeterministicDie() { + this(1); + } + + public int roll() { + final int result = value; + value += 1; + if (value > 100) { + value -= 100; + } + totalRolls++; + return result; + } + } + + public record Pawn(int position, int score) { + public Pawn fork() { + return new Pawn(position(), score()); + } + + public Pawn move(final int distance) { + int newPosition = (position() + distance) % 10; + if (newPosition == 0) { + newPosition = 10; + } + final int newScore = score() + newPosition; + return new Pawn(newPosition, newScore); + } + + public Pawn takeTurn(final DeterministicDie die) { + final int distance = die.roll() + die.roll() + die.roll(); + return move(distance); + } + } + + public record Game(Pawn player1, Pawn player2, boolean playerOnesTurn) { + + } + + public record ScoreCard(BigInteger playerOneWins, BigInteger playerTwoWins) { + public ScoreCard add(final ScoreCard other) { + return new ScoreCard(playerOneWins().add(other.playerOneWins()), playerTwoWins().add(other.playerTwoWins())); + } + } + + public static class QuantumDie { + private final Map cache = new HashMap<>(); + + public ScoreCard play(final Game game) { + if (cache.containsKey(game)) { + return cache.get(game); + } + final var reverseScenario = new Game(game.player2(), game.player1(), !game.playerOnesTurn()); + if (cache.containsKey(reverseScenario)) { + final var reverseResult = cache.get(reverseScenario); + return new ScoreCard(reverseResult.playerTwoWins(), reverseResult.playerOneWins()); + } + + if (game.player1().score() >= 21) { + final var result = new ScoreCard(BigInteger.ONE, BigInteger.ZERO); + cache.put(game, result); + return result; + } else if (game.player2().score() >= 21) { + final var result = new ScoreCard(BigInteger.ZERO, BigInteger.ONE); + cache.put(game, result); + return result; + } + + var result = new ScoreCard(BigInteger.ZERO, BigInteger.ZERO); + for (int i = 1; i <= 3; i++) { + for (int j = 1; j <= 3; j++) { + for (int k = 1; k <= 3; k++) { + final int movementDistance = i + j + k; + final var forkResult = game.playerOnesTurn() + ? play(new Game(game.player1().move(movementDistance), game.player2(), false)) + : play(new Game(game.player1(), game.player2().fork().move(movementDistance), true)); + result = result.add(forkResult); + } + } + } + cache.put(game, result); + return result; + } + } + + @Test + public final void part1() { + final var lines = getInput().toList(); + final int playerOnePosition = + Integer.parseInt(lines.get(0).replaceAll("Player . starting position: ", "")); + final int playerTwoPosition = + Integer.parseInt(lines.get(1).replaceAll("Player . starting position: ", "")); + var playerOne = new Pawn(playerOnePosition, 0); + var playerTwo = new Pawn(playerTwoPosition, 0); + final var die = new DeterministicDie(); + while (true) { + // player 1 + playerOne = playerOne.takeTurn(die); + if (playerOne.score() >= 1000) { + break; + } + + // player 2 + playerTwo = playerTwo.takeTurn(die); + if (playerTwo.score() >= 1000) { + break; + } + } + int losingScore = Math.min(playerOne.score(), playerTwo.score()); + + System.out.println("Part 1: " + (losingScore * die.totalRolls)); + } + + @Test + public final void part2() { + final var lines = getInput().toList(); + final int playerOnePosition = + Integer.parseInt(lines.get(0).replaceAll("Player . starting position: ", "")); + final int playerTwoPosition = + Integer.parseInt(lines.get(1).replaceAll("Player . starting position: ", "")); + final var playerOne = new Pawn(playerOnePosition, 0); + final var playerTwo = new Pawn(playerTwoPosition, 0); + final var die = new QuantumDie(); + final var game = new Game(playerOne, playerTwo, true); + final var result = die.play(game); + final var winningScore = result.playerOneWins().max(result.playerTwoWins()); + System.out.println("Part 2: " + winningScore); + } + +} \ 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..3f69194 --- /dev/null +++ b/src/test/resources/sample/day-21.txt @@ -0,0 +1,2 @@ +Player 1 starting position: 4 +Player 2 starting position: 8 From 6cdd4c00954754f7e88367cffbd6cd7461748bfd Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Wed, 22 Dec 2021 19:00:31 -0800 Subject: [PATCH 22/44] Day 22 --- src/test/java/com/macasaet/Day22.java | 202 ++++++++++++++++++++++++++ src/test/resources/sample/day-22.txt | 60 ++++++++ 2 files changed, 262 insertions(+) create mode 100644 src/test/java/com/macasaet/Day22.java create mode 100644 src/test/resources/sample/day-22.txt diff --git a/src/test/java/com/macasaet/Day22.java b/src/test/java/com/macasaet/Day22.java new file mode 100644 index 0000000..333be59 --- /dev/null +++ b/src/test/java/com/macasaet/Day22.java @@ -0,0 +1,202 @@ +package com.macasaet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * --- Day 22: Reactor Reboot --- + */ +public class Day22 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-22.txt"), + false); + } + + public record ReactorCore( + SortedMap>> cubes) { + + public long count(final long xMin, final long xMax, final long yMin, final long yMax, final long zMin, final long zMax) { + long sum = 0; + for (var i = xMin; i <= xMax; i++) { + final var xDimension = cubes().getOrDefault(i, Collections.emptySortedMap()); + for (var j = yMin; j <= yMax; j++) { + final var yDimension = xDimension.getOrDefault(j, Collections.emptySortedMap()); + for (var k = zMin; k <= zMax; k++) { + if (yDimension.getOrDefault(k, false)) { + sum++; + } + } + } + } + return sum; + } + + public void process(final Instruction instruction) { + final var block = instruction.block(); + final var on = instruction.on(); + for (var i = block.xMin(); i <= block.xMax(); i++) { + final var xDimension = cubes().computeIfAbsent(i, _key -> new TreeMap<>()); + for (var j = block.yMin(); j <= block.yMax(); j++) { + final var yDimension = xDimension.computeIfAbsent(j, _key -> new TreeMap<>()); + for (var k = block.zMin(); k <= block.zMax(); k++) { + yDimension.put(k, on); + } + } + } + } + + } + + public record Instruction(boolean on, Block block) { + public static Instruction parse(final String string) { + final var components = string.split(" "); + final boolean on = "on".equalsIgnoreCase(components[0]); + final var block = Block.parse(components[1]); + return new Instruction(on, block); + } + + } + + public record Block(long xMin, long xMax, long yMin, long yMax, long zMin, long zMax) { + + public BigInteger volume() { + return (BigInteger.valueOf(xMax).subtract(BigInteger.valueOf(xMin)).add(BigInteger.ONE)) + .multiply(BigInteger.valueOf(yMax).subtract(BigInteger.valueOf(yMin)).add(BigInteger.ONE)) + .multiply(BigInteger.valueOf(zMax).subtract(BigInteger.valueOf(zMin)).add(BigInteger.ONE)); + } + + public static Block parse(final String string) { + final var ranges = string.split(","); + final var xRange = ranges[0].split("\\.\\."); + final var xMin = Long.parseLong(xRange[0].replaceAll("x=", "")); + final var xMax = Long.parseLong((xRange[1])); + final var yRange = ranges[1].split("\\.\\."); + final var yMin = Long.parseLong((yRange[0].replaceAll("y=", ""))); + final var yMax = Long.parseLong((yRange[1])); + final var zRange = ranges[2].split("\\.\\."); + final var zMin = Long.parseLong((zRange[0].replaceAll("z=", ""))); + final var zMax = Long.parseLong((zRange[1])); + return new Block(xMin, xMax, yMin, yMax, zMin, zMax); + } + + public boolean overlaps(final Block other) { + return intersection(other).isPresent(); + } + + public Optional intersection(final Block other) { + if (xMin > other.xMax() || xMax < other.xMin() + || yMin > other.yMax() || yMax < other.yMin() + || zMin > other.zMax() || zMax < other.zMin()) { + return Optional.empty(); + } + final var result = new Block(Math.max(xMin, other.xMin()), Math.min(xMax, other.xMax()), + Math.max(yMin, other.yMin()), Math.min(yMax, other.yMax()), + Math.max(zMin, other.zMin()), Math.min(zMax, other.zMax())); + return Optional.of(result); + } + } + + @Nested + public class BlockTest { + @Test + public final void verifyEqualBlocksOverlap() { + // given + final var x = new Block(-2, 2, -2, 2, -2, 2); + final var y = new Block(-2, 2, -2, 2, -2, 2); + + // when + + // then + assertTrue(x.overlaps(y)); + assertTrue(y.overlaps(x)); + } + + @Test + public final void verifyNestedBlocksOverlap() { + final var inner = new Block(-2, 2, -2, 2, -2, 2); + final var outer = new Block(-4, 4, -4, 4, -4, 4); + + assertTrue(inner.overlaps(outer)); + assertTrue(outer.overlaps(inner)); + } + + @Test + public final void verifyIntersectingBlocksOverlap() { + final var x = new Block(10, 12, 10, 12, 10, 12); + final var y = new Block(11, 13, 11, 13, 11, 13); + + assertTrue(x.overlaps(y)); + assertTrue(y.overlaps(x)); + } + + @Test + public final void testIntersection() { + final var x = new Block(10, 12, 10, 12, 10, 12); + final var y = new Block(11, 13, 11, 13, 11, 13); + + assertTrue(x.intersection(y).isPresent()); + assertEquals(BigInteger.valueOf(8), x.intersection(y).orElseThrow().volume()); + assertTrue(y.intersection(x).isPresent()); + assertEquals(BigInteger.valueOf(8), y.intersection(x).orElseThrow().volume()); + assertEquals(x.intersection(y).orElseThrow(), y.intersection(x).orElseThrow()); + } + } + + @Test + public final void part1() { + final var core = new ReactorCore(new TreeMap<>()); + getInput().map(Instruction::parse).map(fullInstruction -> { + final var fullBlock = fullInstruction.block(); + final var truncatedBlock = new Block(Math.max(fullBlock.xMin(), -50), Math.min(fullBlock.xMax(), 50), + Math.max(fullBlock.yMin(), -50), Math.min(fullBlock.yMax(), 50), + Math.max(fullBlock.zMin(), -50), Math.min(fullBlock.zMax(), 50)); + return new Instruction(fullInstruction.on(), truncatedBlock); + }).forEach(core::process); + System.out.println("Part 1: " + core.count(-50, 50, -50, 50, -50, 50)); + } + + @Test + public final void part2() { + final var originalList = getInput().map(Instruction::parse).toList(); + final var appliedInstructions = new ArrayList(); + for (final var instruction : originalList) { + final var modifiedInstructions = new ArrayList(); + if (instruction.on()) { + // only add initial instructions that turn ON cubes + modifiedInstructions.add(instruction); + } + // override any previous instructions + for (final var previousInstruction : appliedInstructions) { + // add compensating instructions to handle the overlaps + instruction.block() + .intersection(previousInstruction.block()) + .map(intersection -> new Instruction(!previousInstruction.on(), intersection)) + .ifPresent(modifiedInstructions::add); + } + + appliedInstructions.addAll(modifiedInstructions); + } + + final var sum = appliedInstructions.stream() + .map(instruction -> instruction.block() + .volume() + .multiply(instruction.on() + ? BigInteger.ONE + : BigInteger.valueOf(-1))) + .reduce(BigInteger.ZERO, + BigInteger::add); + + System.out.println("Part 2: " + sum); + } + +} \ No newline at end of file diff --git a/src/test/resources/sample/day-22.txt b/src/test/resources/sample/day-22.txt new file mode 100644 index 0000000..2790bed --- /dev/null +++ b/src/test/resources/sample/day-22.txt @@ -0,0 +1,60 @@ +on x=-5..47,y=-31..22,z=-19..33 +on x=-44..5,y=-27..21,z=-14..35 +on x=-49..-1,y=-11..42,z=-10..38 +on x=-20..34,y=-40..6,z=-44..1 +off x=26..39,y=40..50,z=-2..11 +on x=-41..5,y=-41..6,z=-36..8 +off x=-43..-33,y=-45..-28,z=7..25 +on x=-33..15,y=-32..19,z=-34..11 +off x=35..47,y=-46..-34,z=-11..5 +on x=-14..36,y=-6..44,z=-16..29 +on x=-57795..-6158,y=29564..72030,z=20435..90618 +on x=36731..105352,y=-21140..28532,z=16094..90401 +on x=30999..107136,y=-53464..15513,z=8553..71215 +on x=13528..83982,y=-99403..-27377,z=-24141..23996 +on x=-72682..-12347,y=18159..111354,z=7391..80950 +on x=-1060..80757,y=-65301..-20884,z=-103788..-16709 +on x=-83015..-9461,y=-72160..-8347,z=-81239..-26856 +on x=-52752..22273,y=-49450..9096,z=54442..119054 +on x=-29982..40483,y=-108474..-28371,z=-24328..38471 +on x=-4958..62750,y=40422..118853,z=-7672..65583 +on x=55694..108686,y=-43367..46958,z=-26781..48729 +on x=-98497..-18186,y=-63569..3412,z=1232..88485 +on x=-726..56291,y=-62629..13224,z=18033..85226 +on x=-110886..-34664,y=-81338..-8658,z=8914..63723 +on x=-55829..24974,y=-16897..54165,z=-121762..-28058 +on x=-65152..-11147,y=22489..91432,z=-58782..1780 +on x=-120100..-32970,y=-46592..27473,z=-11695..61039 +on x=-18631..37533,y=-124565..-50804,z=-35667..28308 +on x=-57817..18248,y=49321..117703,z=5745..55881 +on x=14781..98692,y=-1341..70827,z=15753..70151 +on x=-34419..55919,y=-19626..40991,z=39015..114138 +on x=-60785..11593,y=-56135..2999,z=-95368..-26915 +on x=-32178..58085,y=17647..101866,z=-91405..-8878 +on x=-53655..12091,y=50097..105568,z=-75335..-4862 +on x=-111166..-40997,y=-71714..2688,z=5609..50954 +on x=-16602..70118,y=-98693..-44401,z=5197..76897 +on x=16383..101554,y=4615..83635,z=-44907..18747 +off x=-95822..-15171,y=-19987..48940,z=10804..104439 +on x=-89813..-14614,y=16069..88491,z=-3297..45228 +on x=41075..99376,y=-20427..49978,z=-52012..13762 +on x=-21330..50085,y=-17944..62733,z=-112280..-30197 +on x=-16478..35915,y=36008..118594,z=-7885..47086 +off x=-98156..-27851,y=-49952..43171,z=-99005..-8456 +off x=2032..69770,y=-71013..4824,z=7471..94418 +on x=43670..120875,y=-42068..12382,z=-24787..38892 +off x=37514..111226,y=-45862..25743,z=-16714..54663 +off x=25699..97951,y=-30668..59918,z=-15349..69697 +off x=-44271..17935,y=-9516..60759,z=49131..112598 +on x=-61695..-5813,y=40978..94975,z=8655..80240 +off x=-101086..-9439,y=-7088..67543,z=33935..83858 +off x=18020..114017,y=-48931..32606,z=21474..89843 +off x=-77139..10506,y=-89994..-18797,z=-80..59318 +off x=8476..79288,y=-75520..11602,z=-96624..-24783 +on x=-47488..-1262,y=24338..100707,z=16292..72967 +off x=-84341..13987,y=2429..92914,z=-90671..-1318 +off x=-37810..49457,y=-71013..-7894,z=-105357..-13188 +off x=-27365..46395,y=31009..98017,z=15428..76570 +off x=-70369..-16548,y=22648..78696,z=-1892..86821 +on x=-53470..21291,y=-120233..-33476,z=-44150..38147 +off x=-93533..-4276,y=-16170..68771,z=-104985..-24507 \ No newline at end of file From 3078fd46831112f9db07d99692a94a179336a9f7 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sun, 2 Jan 2022 10:29:49 -0800 Subject: [PATCH 23/44] WIP - Day 23 This uses A* to find the least energy-intensive way to rearrange the burrow. Unfortunately, it takes prohibitively long to complete part 2. --- src/test/java/com/macasaet/Day23.java | 897 ++++++++++++++++++++++++++ src/test/resources/sample/day-23.txt | 5 + 2 files changed, 902 insertions(+) create mode 100644 src/test/java/com/macasaet/Day23.java create mode 100644 src/test/resources/sample/day-23.txt diff --git a/src/test/java/com/macasaet/Day23.java b/src/test/java/com/macasaet/Day23.java new file mode 100644 index 0000000..b023161 --- /dev/null +++ b/src/test/java/com/macasaet/Day23.java @@ -0,0 +1,897 @@ +package com.macasaet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * --- Day 23: Amphipod --- + */ +public class Day23 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-23.txt"), + false); + } + + protected Tile[][] parseGrid(List lines) { + final var result = new Tile[lines.size()][]; + for (int i = lines.size(); --i >= 0; ) { + final var line = lines.get(i); + result[i] = new Tile[line.length()]; + for (int j = line.length(); --j >= 0; ) { + final AmphipodType targetType = AmphipodType.forDestinationColumn(j); + final var c = line.charAt(j); + result[i][j] = switch (c) { + case '.' -> new Tile(new Point(i, j), null, null); + case 'A' -> new Tile(new Point(i, j), targetType, AmphipodType.AMBER); + case 'B' -> new Tile(new Point(i, j), targetType, AmphipodType.BRONZE); + case 'C' -> new Tile(new Point(i, j), targetType, AmphipodType.COPPER); + case 'D' -> new Tile(new Point(i, j), targetType, AmphipodType.DESERT); + default -> null; + }; + } + } + + return result; + } + + public enum AmphipodType { + AMBER(1, 3), + BRONZE(10, 5), + COPPER(100, 7), + DESERT(1000, 9); + + private final int energyPerStep; + private final int destinationColumn; + + AmphipodType(final int energyPerStep, final int destinationColumn) { + this.energyPerStep = energyPerStep; + this.destinationColumn = destinationColumn; + } + + public static AmphipodType forDestinationColumn(final int destinationColumn) { + for (final var candidate : values()) { + if (candidate.destinationColumn == destinationColumn) { + return candidate; + } + } + return null; + } + } + + public record Move(Point from, Point to) { + } + + public record BranchResult(Node node, int cost) { + } + + public record Node(Tile[][] tiles, Map, BranchResult> branchCache) { + + private static final Map estimatedDistanceCache = new ConcurrentHashMap<>(); + private static final Map> branchesCache = new ConcurrentHashMap<>(); + private static final Map solutionCache = new ConcurrentHashMap<>(); + + public static Node createInitialNode(final Tile[][] tiles) { + return new Node(tiles, new ConcurrentHashMap<>()); + } + + public BranchResult branch(final List moves) { + if (moves.size() == 0) { + System.err.println("How is this empty?"); + return new BranchResult(this, 0); + } + if (branchCache.containsKey(moves)) { + return branchCache.get(moves); + } + final var copy = new Tile[tiles.length][]; + for (int i = tiles.length; --i >= 0; ) { + final var row = new Tile[tiles[i].length]; + System.arraycopy(tiles[i], 0, row, 0, tiles[i].length); + copy[i] = row; + } + final var source = moves.get(0).from(); + final var destination = moves.get(moves.size() - 1).to(); + final var sourceTile = source.getTile(tiles); + final var destinationTile = destination.getTile(tiles); + final var amphipod = sourceTile.amphipodType(); + if (amphipod == null) { + System.err.println("source amphipod is missing :-("); + } + source.setTile(copy, sourceTile.updateType(null)); + destination.setTile(copy, destinationTile.updateType(amphipod)); + final var cost = moves.size() * amphipod.energyPerStep; + final var result = new BranchResult(new Node(copy, new ConcurrentHashMap<>()), cost); + branchCache.put(moves, result); + return result; + } + + boolean isSideRoom(final Point point) { + final var x = point.x(); + final var y = point.y(); + return x > 1 && (y == 3 || y == 5 || y == 7 || y == 9); + } + + boolean isCorridor(final Point point) { + return point.x() == 1; + } + + public int hashCode() { + // equality based on layout of the burrow regardless of how the amphipods got to that state + // FNV hash + long result = 2166136261L; + final Function rowHasher = row -> { + long rowHash = 2166136261L; + for (final var tile : row) { + rowHash = (16777619L * rowHash) ^ (tile == null ? 0L : (long) Objects.hashCode(tile.amphipodType())); + } + return rowHash; + }; + for (final var row : tiles()) { + result = (16777619L * result) ^ rowHasher.apply(row); + } + + return Long.hashCode(result); + // Bob Jenkins' One-at-a-Time hash +// int result = 0; +// final Function rowHasher = row -> { +// int rowHash = 0; +// for(final var tile : row) { +// final var tileHash = tile != null ? Objects.hashCode(tile.amphipodType()) : 0; +// rowHash += tileHash; +// rowHash += rowHash << 10; +// rowHash ^= rowHash >> 6; +// } +// rowHash += rowHash << 3; +// rowHash ^= rowHash >> 11; +// rowHash += rowHash << 15; +// return rowHash; +// }; +// for(final var row : tiles()) { +// result += rowHasher.apply(row); +// result += (result << 10); +// result ^= (result >> 6); +// } +// result += result << 3; +// result ^= result >> 11; +// result += result << 15; +// return result; + } + + public boolean equals(final Object o) { + if (o == null) { + return false; + } + if (this == o) { + return true; + } + try { + final var other = (Node) o; + // equality based on layout of the burrow regardless of how the amphipods got to that state + if (tiles().length != other.tiles().length) { + return false; + } + for (int i = tiles().length; --i >= 0; ) { + final var xRow = tiles()[i]; + final var yRow = other.tiles()[i]; + if (xRow.length != yRow.length) { + return false; + } + for (int j = xRow.length; --j >= 0; ) { + if (xRow[j] != yRow[j]) { + if (xRow[j] == null + || yRow[j] == null + || !Objects.equals(xRow[j].amphipodType(), yRow[j].amphipodType())) { + return false; + } + } + } + } + return true; + } catch (final ClassCastException cce) { + return false; + } + } + + public String toString() { + final var builder = new StringBuilder(); + for (final var row : tiles()) { + for (final var cell : row) { + if (cell == null) { + builder.append('#'); + } else if (cell.amphipodType() == null) { + builder.append('.'); + } else { + builder.append(switch (cell.amphipodType()) { + case AMBER -> 'A'; + case BRONZE -> 'B'; + case COPPER -> 'C'; + case DESERT -> 'D'; + }); + } + } + builder.append('\n'); + } + return builder.toString(); + } + + public boolean isSolution() { + if (solutionCache.containsKey(this)) { + return solutionCache.get(this); + } + final var result = Arrays.stream(tiles()) + .flatMap(Arrays::stream) + .filter(Objects::nonNull) + .allMatch(Tile::hasTargetType); + solutionCache.put(this, result); + return result; + } + + public Stream getBranches() { + if (branchesCache.containsKey(this)) { + return branchesCache.get(this).stream(); + } + final var branchResults = new Vector(); + return Arrays.stream(tiles()) + .parallel() + .flatMap(row -> Arrays.stream(row) + .parallel() + .filter(Objects::nonNull) + .filter(tile -> !tile.isVacant()) + .flatMap(this::getMoves)) + .map(this::branch) + .peek(branchResults::add) + .onClose(() -> branchesCache.put(this, Collections.unmodifiableList(branchResults))); + } + +// @Deprecated +// public Set> getMoves() { +// final var result = new HashSet>(); +// for (final var row : tiles()) { +// for (final var tile : row) { +// if (tile != null && !tile.isVacant()) { +// result.addAll(getMoves(tile).collect(Collectors.toSet())); +// } +// } +// } +// return Collections.unmodifiableSet(result); +// } + + /** + * Find all the actions (series of moves) that can be taken for a single amphipod. + * + * @param occupiedTile a tile with an amphipod + * @return All the moves that can be applied starting with occupiedTile that end in a valid temporary + * destination for the amphipod + */ + Stream> getMoves(final Tile occupiedTile) { + if (isSideRoom(occupiedTile.location()) && occupiedTile.hasTargetType()) { + var roomComplete = true; + for (var cursor = tiles[occupiedTile.location().x() + 1][occupiedTile.location().y()]; + cursor != null; + cursor = tiles[cursor.location().x() + 1][cursor.location().y()]) { + if (!cursor.hasTargetType()) { + // one of the amphipods in this room is destined elsewhere + // so the amphipod from the original tile will need to move out of the way + roomComplete = false; + break; + } + } + if (roomComplete) { + // all the amphipods in this room have this as their intended destination + return Stream.empty(); + } + } + + var paths = iterateThroughPaths(occupiedTile.amphipodType(), + occupiedTile, + Collections.singletonList(occupiedTile.location())); + if (isCorridor(occupiedTile.location())) { + /* + * "Once an amphipod stops moving in the hallway, it will stay in that spot until it can move into a + * room. (That is, once any amphipod starts moving, any other amphipods currently in the hallway are + * locked in place and will not move again until they can move fully into a room.)" + */ + paths = paths + // only select paths that end in a room + .filter(path -> isSideRoom(path.get(path.size() - 1) + .getTile(tiles()) + .location())); + } else { + // reduce the search space by only considering rooms from rooms into hallways + // prune any path that starts from a room and ends in a room +// paths = paths +// .filter(path -> isCorridor(path.get(path.size() - 1) +// .getTile(tiles()) +// .location())); + } + // convert tiles to moves + return paths + .filter(path -> path.size() > 1) // filter out paths in which the amphipod does not move + .map(points -> { + final var moves = new ArrayList(points.size() - 1); + for (int i = 1; i < points.size(); i++) { + moves.add(new Move(points.get(i - 1), points.get(i))); + } + return Collections.unmodifiableList(moves); + }); + } + + Stream> iterateThroughPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { + // TODO store `pathSoFar` as a stack so checking for node becomes O(1) instead of O(n) + final int x = current.location.x(); + final int y = current.location.y(); + final var up = tiles[x - 1][y]; + final var down = tiles[x + 1][y]; + final var left = tiles[x][y - 1]; + final var right = tiles[x][y + 1]; + final var suppliers = new ArrayList>>>(4); + if (up != null && up.isVacant() && !pathSoFar.contains(up.location())) { + suppliers.add(() -> streamUpPaths(amphipodType, current, pathSoFar)); + } + if (down != null + && down.isVacant() + && !pathSoFar.contains(down.location()) + // don't enter side room unless it is the ultimate destination + && down.targetType == amphipodType) { + suppliers.add(() -> streamDownPaths(amphipodType, current, pathSoFar)); + } + if (left != null && left.isVacant() && !pathSoFar.contains(left.location())) { + suppliers.add(() -> streamLeftPaths(amphipodType, current, pathSoFar)); + } + if (right != null && right.isVacant() && !pathSoFar.contains(right.location())) { + suppliers.add(() -> streamRightPaths(amphipodType, current, pathSoFar)); + } + if (suppliers.isEmpty()) { + // dead end, emit the path so far + suppliers.add(() -> Stream.of(Collections.unmodifiableList(pathSoFar))); + } + + return suppliers.stream() + .flatMap(Supplier::get); + } + + Stream> streamUpPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { + final int x = current.location.x(); + final int y = current.location.y(); + final var up = tiles[x - 1][y]; + // amphipod is in a side room + if (isSideRoom(up.location()) && current.targetType == amphipodType) { + // amphipod is in the back of the room in which it belongs, stop here + return Stream.of(pathSoFar); + } + final var incrementalPath = new ArrayList<>(pathSoFar); + incrementalPath.add(up.location()); + // whether "up" is the front of the room or the corridor outside the room, we have to keep moving + return iterateThroughPaths(amphipodType, up, incrementalPath); + } + + Stream> streamDownPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { + final int x = current.location.x(); + final int y = current.location.y(); + final var down = tiles[x + 1][y]; + final var incrementalPath = new ArrayList<>(pathSoFar); + incrementalPath.add(down.location()); + if ((isCorridor(current.location()) && canEnterRoom(amphipodType, down)) || isSideRoom(current.location())) { + // go as for back into the room as possible, don't just stop at the entrance + return iterateThroughPaths(amphipodType, down, incrementalPath); + } + return Stream.empty(); + } + + Stream> streamLeftPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { + final int x = current.location.x(); + final int y = current.location.y(); + final var left = tiles[x][y - 1]; + Stream> result = Stream.empty(); + final var incrementalPath = new ArrayList<>(pathSoFar); + incrementalPath.add(left.location()); + if (tiles[left.location().x() + 1][left.location().y()] == null || !isSideRoom(tiles[left.location().x() + 1][left.location().y()].location())) { + // this is not in front of a side room, + // we can stop here while other amphipods move + result = Stream.concat(result, Stream.of(Collections.unmodifiableList(incrementalPath))); + } + result = Stream.concat(result, iterateThroughPaths(amphipodType, left, incrementalPath)); + return result; + } + + Stream> streamRightPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { + final int x = current.location.x(); + final int y = current.location.y(); + final var right = tiles[x][y + 1]; + Stream> result = Stream.empty(); + final var incrementalPath = new ArrayList<>(pathSoFar); + incrementalPath.add(right.location()); + if (tiles[right.location().x() + 1][right.location().y()] == null || !isSideRoom(tiles[right.location().x() + 1][right.location().y()].location())) { + // this is not in front of a side room, + // we can stop here while other amphipods move + result = Stream.concat(result, Stream.of(Collections.unmodifiableList(incrementalPath))); + } + result = Stream.concat(result, iterateThroughPaths(amphipodType, right, incrementalPath)); + return result; + } + +// /** +// * Find all the paths an amphipod can take +// * +// * @param amphipodType the type of amphipod that is moving +// * @param current a tile through which the amphipod will take +// * @param pathSoFar the full path of the amphipod so far, *must* include _current_ +// * @return all the paths (start to finish) the amphipod can take +// */ +// @Deprecated +// List> getPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { +// final int x = current.location.x(); +// final int y = current.location.y(); +// final var up = tiles[x - 1][y]; +// final var down = tiles[x + 1][y]; +// final var left = tiles[x][y - 1]; +// final var right = tiles[x][y + 1]; +// +// final var result = new ArrayList>(); +// if (up != null && up.isVacant() && !pathSoFar.contains(up.location())) { +// // amphipod is in a side room +// if (isSideRoom(up.location()) && current.targetType == amphipodType) { +// // amphipod is in the back of the room in which it belongs, stop here +// return Collections.singletonList(pathSoFar); +// } +// final var incrementalPath = new ArrayList<>(pathSoFar); +// incrementalPath.add(up.location()); +// // whether "up" is the front of the room or the corridor outside the room, we have to keep moving +// result.addAll(getPaths(amphipodType, up, incrementalPath)); +// } +// if (down != null +// && down.isVacant() +// && !pathSoFar.contains(down.location()) +// // don't enter side room unless it is the ultimate destination +// && down.targetType == amphipodType) { +// // either entering a room or moving to the back of the room +// final var incrementalPath = new ArrayList<>(pathSoFar); +// incrementalPath.add(down.location()); +// if ((isCorridor(current.location()) && canEnterRoom(amphipodType, down)) || isSideRoom(current.location())) { +// // go as for back into the room as possible, don't just stop at the entrance +// result.addAll(getPaths(amphipodType, down, incrementalPath)); +// } +// } +// if (left != null && left.isVacant() && !pathSoFar.contains(left.location())) { +// final var incrementalPath = new ArrayList<>(pathSoFar); +// incrementalPath.add(left.location()); +// if (tiles[left.location().x() + 1][left.location().y()] == null || !isSideRoom(tiles[left.location().x() + 1][left.location().y()].location())) { +// // this is not in front of a side room, +// // we can stop here while other amphipods move +// result.add(Collections.unmodifiableList(incrementalPath)); +// } +// result.addAll(getPaths(amphipodType, left, incrementalPath)); +// } +// if (right != null && right.isVacant() && !pathSoFar.contains(right.location())) { +// final var incrementalPath = new ArrayList<>(pathSoFar); +// incrementalPath.add(right.location()); +// if (tiles[right.location().x() + 1][right.location().y()] == null || !isSideRoom(tiles[right.location().x() + 1][right.location().y()].location())) { +// // this is not in front of a side room, +// // we can stop here while other amphipods move +// result.add(Collections.unmodifiableList(incrementalPath)); +// } +// result.addAll(getPaths(amphipodType, right, incrementalPath)); +// } +// if (result.isEmpty() && pathSoFar.size() > 1) { +// // dead end, emit the path so far +// result.add(pathSoFar); +// } +// return Collections.unmodifiableList(result); +// } + + boolean canEnterRoom(final AmphipodType amphipodType, final Tile frontOfRoom) { + if (!isSideRoom(frontOfRoom.location())) { + throw new IllegalArgumentException("Not a side room: " + frontOfRoom); + } + if (frontOfRoom.targetType() != amphipodType) { + // this is not the destination room + return false; + } + // ensure all occupants have this as their destination + boolean hasOccupants = false; + for (var roomTile = tiles()[frontOfRoom.location().x() + 1][frontOfRoom.location().y()]; + roomTile != null; + roomTile = tiles()[roomTile.location().x() + 1][roomTile.location().y()]) { + if (roomTile.amphipodType() == null) { + if (hasOccupants) { + System.err.println("***There is a gap in the room***"); + return false; // there is a gap that shouldn't be here + } + continue; + } else { + hasOccupants = true; + } + if (!roomTile.hasTargetType()) { + return false; + } + } + return true; + } + + /** + * Estimate the energy required for all the amphipods to get to their side rooms. This strictly underestimates + * the amount of energy required. It assumes: each amphipod has an unobstructed path to their room, they only + * need to get to the front of the room, not all the way to the back, they do not need to take any detours to + * let other amphipods pass (e.g. they don't need to leave the room and then come back in). + * + * @return an underestimate of the energy required for the amphipods of the burrow to self-organise + */ + public int estimatedDistanceToSolution() { + if (estimatedDistanceCache.containsKey(this)) { + return estimatedDistanceCache.get(this); + } + int result = 0; + for (int i = tiles.length; --i >= 0; ) { + final var row = tiles[i]; + for (int j = row.length; --j >= 0; ) { + final var tile = row[j]; + if (tile != null && tile.amphipodType != null) { + final int horizontalDistance = + Math.abs(tile.amphipodType.destinationColumn - tile.location().y()); + int verticalDistance = 0; + if (horizontalDistance != 0) { + // get to the corridor + verticalDistance = tile.location().x() - 1; + // enter the side room + verticalDistance += 1; + } else if (isCorridor(tile.location())) { + // it's implied that horizontal distance is 0 + // we're right outside the target room + // enter the side room + verticalDistance = 1; + } + final int distance = verticalDistance + horizontalDistance; + result += distance * tile.amphipodType().energyPerStep; + } + } + } + estimatedDistanceCache.put(this, result); + return result; + } + + } + + protected record Point(int x, int y) { + + public Tile getTile(final Tile[][] tiles) { + return tiles[x()][y()]; + } + + public void setTile(final Tile[][] tiles, final Tile tile) { + if (tile.location().x() != x() || tile.location().y() != y()) { + throw new IllegalArgumentException("Tile and location do not match"); + } + tiles[x()][y()] = tile; + } + } + + public record Tile(Point location, AmphipodType targetType, AmphipodType amphipodType) { + + public Tile updateType(final AmphipodType newType) { + return new Tile(location, targetType, newType); + } + + public boolean isVacant() { + return amphipodType == null; + } + + public boolean hasTargetType() { + return Objects.equals(amphipodType(), targetType()); + } + + } + +// public int lwst(final Node start) { +// final var lowestCostToNode = new ConcurrentHashMap(); +// final var estimatedCostThroughNode = new ConcurrentHashMap(); +// final var openSet = new PriorityBlockingQueue(100000, Comparator.comparing(estimatedCostThroughNode::get)); +// +// // add the starting node, getting there is free +// lowestCostToNode.put(start, 0); +// estimatedCostThroughNode.put(start, start.estimatedDistanceToSolution()); +// openSet.add(start); +// +// final var executor = ForkJoinPool.commonPool(); +// final var stateModifiers = new LinkedBlockingDeque>(); +// final var complete = new AtomicBoolean(false); +// executor.execute(() -> { +// while (!complete.get()) { +// Thread.yield(); +// try { +// final var current = openSet.take(); +// if (current.isSolution()) { +// stateModifiers.addFirst(() -> lowestCostToNode.get(current)); +// } +// final var lowestCostToCurrent = lowestCostToNode.get(current); +// executor.execute(() -> current.getBranches().map(branchResult -> { +// final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); +// final var branchNode = branchResult.node(); +// final Supplier updater = () -> { +// if (tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE)) { +// // either we've never visited this node before, +// // or the last time we did, we took a more expensive route +// lowestCostToNode.put(branchNode, tentativeBranchCost); +// +// // update the cost through this branch +// // need to remove and re-add to get correct ordering in the open set +// estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); +// +// openSet.remove(branchNode); // O(n) +// openSet.add(branchNode); // O(log(n)) +// } +// return (Integer)null; +// }; +// return updater; +// }).forEach(stateModifiers::addLast)); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// complete.set(true); +// Thread.currentThread().interrupt(); +// throw new RuntimeException(e.getMessage(), e); +// } +// } +// }); +// // process updates sequentially +// while (!complete.get()) { +// Thread.yield(); +// try { +// final var updater = stateModifiers.takeFirst(); +// final var cost = updater.get(); +// if (cost != null) { +// complete.set(true); +// return cost; +// } +// } catch (InterruptedException e) { +// e.printStackTrace(); +// complete.set(true); +// Thread.currentThread().interrupt(); +// throw new RuntimeException(e.getMessage(), e); +// } +// } +// throw new IllegalStateException("An error occurred"); +// } +// +// protected String id(Node node) { +// final var outputStream = new ByteArrayOutputStream(); +// outputStream.write(node.hashCode()); +// return Base64.getEncoder().encodeToString(outputStream.toByteArray()); +// } + + public int lowest(final Node start) { + final var lowestCostToNode = new ConcurrentHashMap(); + final var estimatedCostThroughNode = new ConcurrentHashMap(); + final var openSet = new PriorityBlockingQueue(100000, Comparator.comparing(estimatedCostThroughNode::get)); + + // add the starting node, getting there is free + lowestCostToNode.put(start, 0); + estimatedCostThroughNode.put(start, start.estimatedDistanceToSolution()); + openSet.add(start); + + while (!openSet.isEmpty()) { + if(Node.solutionCache.size() % 10000 == 0) { + System.err.println(openSet.size() + " branches left to check in the open set"); + System.err.println("Appraised the energy cost to " + lowestCostToNode.size() + " nodes."); + System.err.println("Estimated the energy cost through " + estimatedCostThroughNode.size() + " nodes."); + System.err.println("Lowest estimated cost so far: " + estimatedCostThroughNode.get(openSet.peek())); + } + final var current = openSet.poll(); // O(log(n)) + if (current.isSolution()) { + System.err.println("Found solution:\n" + current); + return lowestCostToNode.get(current); + } + final var lowestCostToCurrent = lowestCostToNode.get(current); + current.getBranches() + .parallel() + .filter(branchResult -> { + final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); + final var branchNode = branchResult.node(); + return tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE); + }) + .forEach(branchResult -> { + final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); + final var branchNode = branchResult.node(); + // either we've never visited this node before, + // or the last time we did, we took a more expensive route + lowestCostToNode.put(branchNode, tentativeBranchCost); + + // update the cost through this branch + // need to remove and re-add to get correct ordering in the open set +// openSet.remove(branchNode); // O(n) + openSet.removeIf(node -> node.equals(branchNode)); + estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); + openSet.add(branchNode); // O(log(n)) + }); +// current.getBranches().forEach(branchResult -> { +// final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); +// final var branchNode = branchResult.node(); +// if (tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE)) { +// // either we've never visited this node before, +// // or the last time we did, we took a more expensive route +// lowestCostToNode.put(branchNode, tentativeBranchCost); +// +// // update the cost through this branch +// // need to remove and re-add to get correct ordering in the open set +// openSet.remove(branchNode); // O(n) +// estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); +// openSet.add(branchNode); // O(log(n)) +// } +// }); + } + throw new IllegalStateException("Amphipods are gridlocked :-("); + } + + @Nested + public class NodeTest { + + @Test + public final void verifyEquality() { + // given + final var string = """ + ############# + #.....D.D.A.# + ###.#B#C#.### + #A#B#C#.# + ######### + """; + final var x = Node.createInitialNode(parseGrid(string.lines().toList())); + final var y = Node.createInitialNode(parseGrid(string.lines().toList())); + + // when + + // then + assertEquals(x.hashCode(), y.hashCode()); + assertEquals(x, y); + } + + @Test + public final void verifyBranchEquality() { + // given + final var string = """ + ############# + #.....D.D.A.# + ###.#B#C#.### + #A#B#C#.# + ######### + """; + final var original = Node.createInitialNode(parseGrid(string.lines().toList())); + + // when + final var x = original.branch(Collections.singletonList(new Move(new Point(2, 5), new Point(1, 2)))); + final var y = original.branch(Collections.singletonList(new Move(new Point(2, 5), new Point(1, 2)))); + + // then + assertEquals(x.hashCode(), y.hashCode()); + assertEquals(x, y); + } + + @Test + public final void verifyEstimatedDistanceIsZero() { + // given + final var string = """ + ############# + #...........# + ###A#B#C#D### + #A#B#C#D# + ######### + """; + final var initial = Node.createInitialNode(parseGrid(string.lines().toList())); + + // when + final var result = initial.estimatedDistanceToSolution(); + + // then + assertEquals(0, result); + } + + @Test + public final void verifyEstimationOrdering() { + // given + final var states = new String[]{ + """ + ############# + #...........# + ###B#C#B#D### + #A#D#C#A# + ######### + """, + """ + ############# + #...B.......# + ###B#C#.#D### + #A#D#C#A# + ######### + """, + """ + ############# + #...B.......# + ###B#.#C#D### + #A#D#C#A# + ######### + """, + """ + ############# + #.....D.....# + ###B#.#C#D### + #A#B#C#A# + ######### + """, + """ + ############# + #.....D.....# + ###.#B#C#D### + #A#B#C#A# + ######### + """, + """ + ############# + #.....D.D.A.# + ###.#B#C#.### + #A#B#C#.# + ######### + """, + """ + ############# + #.........A.# + ###.#B#C#D### + #A#B#C#D# + ######### + """, + """ + ############# + #...........# + ###A#B#C#D### + #A#B#C#D# + ######### + """ + }; + final var nodes = Arrays.stream(states) + .map(String::lines) + .map(Stream::toList) + .map(Day23.this::parseGrid) + .map(Node::createInitialNode) + .toList(); + + // when + + // then + for (int i = 1; i < nodes.size(); i++) { + final var previous = nodes.get(i - 1); + final var current = nodes.get(i); + assertTrue(previous.estimatedDistanceToSolution() >= current.estimatedDistanceToSolution(), + "Previous state has a lower estimated distance. Previous:\n" + previous + "\n(cost: " + previous.estimatedDistanceToSolution() + ")\nCurrent:\n" + current + "\n(cost: " + current.estimatedDistanceToSolution() + ")"); + } + } + } + + @Test + public final void part1() { + final var initial = Node.createInitialNode(parseGrid(getInput().toList())); + + System.out.println("Part 1: " + lowest(initial)); + } + + @Test + public final void part2() { + final var lines = getInput().collect(Collectors.toList()); + lines.add(3, " #D#B#A#C# "); + lines.add(3, " #D#C#B#A# "); + final var initial = Node.createInitialNode(parseGrid(lines)); + System.err.println("Initial state:\n" + initial); + + System.out.println("Part 2: " + lowest(initial)); + } + +} \ 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..6a7120d --- /dev/null +++ b/src/test/resources/sample/day-23.txt @@ -0,0 +1,5 @@ +############# +#...........# +###B#C#B#D### + #A#D#C#A# + ######### From 64a26f94f89adfe85fd56ec3f56293c1991f76e5 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Tue, 29 Nov 2022 21:42:40 -0800 Subject: [PATCH 24/44] WIP Days 23-25 --- .gitignore | 1 + src/test/java/com/macasaet/Day23.java | 11 +- src/test/java/com/macasaet/Day24.java | 284 ++++++++++++++++++++++++++ src/test/java/com/macasaet/Day25.java | 159 ++++++++++++++ src/test/resources/sample/day-24.txt | 2 + src/test/resources/sample/day-25.txt | 9 + 6 files changed, 462 insertions(+), 4 deletions(-) create mode 100755 src/test/java/com/macasaet/Day24.java create mode 100755 src/test/java/com/macasaet/Day25.java create mode 100755 src/test/resources/sample/day-24.txt create mode 100755 src/test/resources/sample/day-25.txt diff --git a/.gitignore b/.gitignore index db33628..4d131ae 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ target/ src/test/resources/config.properties src/test/resources/input src/test/resources/2020 +.DS_Store diff --git a/src/test/java/com/macasaet/Day23.java b/src/test/java/com/macasaet/Day23.java index b023161..c545b82 100644 --- a/src/test/java/com/macasaet/Day23.java +++ b/src/test/java/com/macasaet/Day23.java @@ -1,7 +1,8 @@ package com.macasaet; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -12,8 +13,8 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * --- Day 23: Amphipod --- @@ -796,6 +797,7 @@ public final void verifyEstimatedDistanceIsZero() { assertEquals(0, result); } + @Disabled @Test public final void verifyEstimationOrdering() { // given @@ -883,6 +885,7 @@ public final void part1() { System.out.println("Part 1: " + lowest(initial)); } + @Disabled @Test public final void part2() { final var lines = getInput().collect(Collectors.toList()); diff --git a/src/test/java/com/macasaet/Day24.java b/src/test/java/com/macasaet/Day24.java new file mode 100755 index 0000000..51eea06 --- /dev/null +++ b/src/test/java/com/macasaet/Day24.java @@ -0,0 +1,284 @@ +package com.macasaet; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.PrimitiveIterator; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * --- Day 24: Arithmetic Logic Unit --- + */ +public class Day24 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-24.txt"), + false); + } + + public static class ArithmeticLogicUnit { + public BigInteger getW() { + return w; + } + + public void setW(BigInteger w) { + this.w = w; + } + + public BigInteger getX() { + return x; + } + + public void setX(BigInteger x) { + this.x = x; + } + + public BigInteger getY() { + return y; + } + + public void setY(BigInteger y) { + this.y = y; + } + + public BigInteger getZ() { + return z; + } + + public void setZ(BigInteger z) { + this.z = z; + } + + private BigInteger w = BigInteger.ZERO, x = BigInteger.ZERO, y = BigInteger.ZERO, z = BigInteger.ZERO; + + public List getInstructions() { + return instructions; + } + + public void setInstructions(List instructions) { + this.instructions = instructions; + } + + private List instructions = new ArrayList<>(); + + public boolean isValid(final String modelNumber) { + final var iterator = modelNumber.chars().map(Character::getNumericValue).iterator(); + for (final var instruction : getInstructions()) { + instruction.evaluate(iterator); + } + return BigInteger.ZERO.equals(getZ()); + } + + public static ArithmeticLogicUnit parse(final Stream lines) { + final var result = new ArithmeticLogicUnit(); + final List instructions = lines.map(line -> { + final var components = line.split(" "); + + final Consumer resultSetter = switch (components[1]) { + case "w" -> result::setW; + case "x" -> result::setX; + case "y" -> result::setY; + case "z" -> result::setZ; + default -> throw new IllegalArgumentException("Invalid instruction, invalid l-value: " + line); + }; + final Supplier xSupplier = switch (components[1]) { + case "w" -> result::getW; + case "x" -> result::getX; + case "y" -> result::getY; + case "z" -> result::getZ; + default -> throw new IllegalArgumentException("Invalid instruction, invalid l-value: " + line); + }; + final Supplier ySupplier = components.length > 2 ? switch (components[2]) { + case "w" -> result::getW; + case "x" -> result::getX; + case "y" -> result::getY; + case "z" -> result::getZ; + default -> () -> new BigInteger(components[2]); + } : () -> { + throw new IllegalStateException(); + }; + return switch (components[0]) { + case "inp" -> new Input(resultSetter); + case "add" -> new Add(resultSetter, xSupplier, ySupplier); + case "mul" -> new Multiply(resultSetter, xSupplier, ySupplier); + case "div" -> new Divide(resultSetter, xSupplier, ySupplier); + case "mod" -> new Modulo(resultSetter, xSupplier, ySupplier); + case "eql" -> new Equals(resultSetter, xSupplier, ySupplier); + default -> throw new IllegalArgumentException("Invalid instruction: " + line); + }; + }).toList(); + result.setInstructions(instructions); + return result; + } + + public void reset() { + setW(BigInteger.ZERO); + setX(BigInteger.ZERO); + setY(BigInteger.ZERO); + setZ(BigInteger.ZERO); + } + } + + public interface Instruction { + void evaluate(PrimitiveIterator.OfInt input); + } + + public record Input(Consumer setter) implements Instruction { + + public void evaluate(PrimitiveIterator.OfInt input) { + setter.accept(BigInteger.valueOf(input.nextInt())); + } + } + + public record Add(Consumer resultSetter, Supplier xSupplier, + Supplier ySupplier) implements Instruction { + public void evaluate(PrimitiveIterator.OfInt _input) { + resultSetter.accept(xSupplier.get().add(ySupplier.get())); + } + } + + public record Multiply(Consumer resultSetter, Supplier xSupplier, + Supplier ySupplier) implements Instruction { + public void evaluate(PrimitiveIterator.OfInt _input) { + resultSetter.accept(xSupplier.get().multiply(ySupplier.get())); + } + } + + public record Divide(Consumer resultSetter, Supplier xSupplier, + Supplier ySupplier) implements Instruction { + public void evaluate(PrimitiveIterator.OfInt _input) { + resultSetter.accept(xSupplier.get().divide(ySupplier.get())); + } + } + + public record Modulo(Consumer resultSetter, Supplier xSupplier, + Supplier ySupplier) implements Instruction { + public void evaluate(PrimitiveIterator.OfInt _input) { + resultSetter.accept(xSupplier.get().mod(ySupplier.get())); + } + } + + public record Equals(Consumer resultSetter, Supplier xSupplier, + Supplier ySupplier) implements Instruction { + public void evaluate(PrimitiveIterator.OfInt _input) { + resultSetter.accept(xSupplier.get().equals(ySupplier.get()) ? BigInteger.ONE : BigInteger.ZERO); + } + } + + @Nested + public class ArithmeticLogicUnitTest { + @Disabled + @Test + public void testNegation() { + // given + final var input = """ + inp x + mul x -1 + """; + final var alu = ArithmeticLogicUnit.parse(input.lines()); + + // when + alu.isValid("7"); + + // then + assertEquals(-7, alu.getX()); + } + + @Disabled + @Test + public final void testThreeTimes() { + // given + final var input = """ + inp z + inp x + mul z 3 + eql z x + """; + final var alu = ArithmeticLogicUnit.parse(input.lines()); + + // when + alu.isValid("39"); + + // then + assertEquals(1, alu.getZ()); + } + + @Disabled + @Test + public final void testBinaryConversion() { + // given + final var input = """ + inp w + add z w + mod z 2 + div w 2 + add y w + mod y 2 + div w 2 + add x w + mod x 2 + div w 2 + mod w 2 + """; + final var alu = ArithmeticLogicUnit.parse(input.lines()); + + // when + alu.isValid("9"); + + // then + assertEquals(1, alu.getW()); + assertEquals(0, alu.getX()); + assertEquals(0, alu.getY()); + assertEquals(1, alu.getZ()); + } + + @Test + public final void testIsValid() { + // given + final var alu = ArithmeticLogicUnit.parse(getInput()); + + // when + final var result = alu.isValid("13579246899999"); + + // then + System.err.println("z=" + alu.getZ()); + } + } + + @Disabled + @Test + public final void part1() { + final var monad = getInput().toList(); + final var counter = new AtomicInteger(0); + final var result = Stream.iterate(new BigInteger("99999999999999"), previous -> previous.subtract(BigInteger.ONE)) + .parallel() + .filter(candidate -> { + final int count = counter.updateAndGet(previous -> previous + 1); + if(count % 10000 == 0) { + System.err.println("Testing: " + candidate); + } + final var alu = ArithmeticLogicUnit.parse(monad.stream()); + return alu.isValid(candidate.toString()); + }).findFirst(); + System.out.println("Part 1: " + result.orElseThrow()); + } + + @Disabled + @Test + public final void part2() { + + System.out.println("Part 2: " + null); + } + +} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day25.java b/src/test/java/com/macasaet/Day25.java new file mode 100755 index 0000000..d9e5335 --- /dev/null +++ b/src/test/java/com/macasaet/Day25.java @@ -0,0 +1,159 @@ +package com.macasaet; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * + */ +public class Day25 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-25.txt"), + false); + } + + public record Herd() { + + } + + public record SeaCucumber() { + + } + + public record OceanFloor(char[][] grid) { + public static OceanFloor parse(final Stream lines) { + final var list = lines.toList(); + final var grid = new char[list.size()][]; + for (int i = list.size(); --i >= 0; ) { + final var line = list.get(i); + grid[i] = new char[line.length()]; + for (int j = line.length(); --j >= 0; grid[i][j] = line.charAt(j)) ; + } + return new OceanFloor(grid); + } + + public OceanFloor step() { + return stepEast().stepSouth(); + } + + boolean isOccupied(final int x, final int y) { + return grid[x][y] != '.'; + } + + char[][] createBlankGrid() { + final char[][] result = new char[grid().length][]; + for (int i = grid().length; --i >= 0; ) { + final var originalRow = grid()[i]; + final var newRow = new char[originalRow.length]; + for (int j = originalRow.length; --j >= 0; newRow[j] = '.') ; + result[i] = newRow; + } + return result; + } + + OceanFloor stepEast() { + final char[][] copy = createBlankGrid(); + for (int i = grid().length; --i >= 0; ) { + final var originalRow = grid()[i]; + for (int j = originalRow.length; --j >= 0; ) { + final var nextIndex = (j + 1) % originalRow.length; + if (originalRow[j] == '>') { + if (!isOccupied(i, nextIndex)) { + copy[i][nextIndex] = '>'; + } else { + copy[i][j] = '>'; + } + } else if (originalRow[j] != '.') { + copy[i][j] = originalRow[j]; + } + } + } + return new OceanFloor(copy); + } + + OceanFloor stepSouth() { + final char[][] copy = createBlankGrid(); + for (int i = grid().length; --i >= 0; ) { + final var originalRow = grid()[i]; + final var nextIndex = (i + 1) % grid().length; + for (int j = originalRow.length; --j >= 0; ) { + if (originalRow[j] == 'v') { + if (!isOccupied(nextIndex, j)) { + copy[nextIndex][j] = 'v'; + } else { + copy[i][j] = 'v'; + } + } else if (originalRow[j] != '.') { + copy[i][j] = originalRow[j]; + } + } + } + return new OceanFloor(copy); + } + + public String toString() { + final var builder = new StringBuilder(); + for (final var row : grid()) { + builder.append(row).append('\n'); + } + return builder.toString(); + } + + public int hashCode() { + int result = 1; + for (final var row : grid()) { + result += 31 * result + Arrays.hashCode(row); + } + return result; + } + + public boolean equals(final Object o) { + if (o == null) { + return false; + } else if (this == o) { + return true; + } + try { + final OceanFloor other = (OceanFloor) o; + if (grid().length != other.grid().length) { + return false; + } + for (int i = grid.length; --i >= 0; ) { + final var mine = grid()[i]; + final var theirs = other.grid()[i]; + if (!Arrays.equals(mine, theirs)) { + return false; + } + } + return true; + } catch (final ClassCastException _cce) { + return false; + } + } + } + + @Test + public final void part1() { + var oceanFloor = OceanFloor.parse(getInput()); + for (int i = 1; ; i++) { + final var next = oceanFloor.step(); + if (next.equals(oceanFloor)) { + System.out.println("Part 1: " + i); + break; + } + oceanFloor = next; + } + } + + @Test + public final void part2() { + + System.out.println("Part 2: " + null); + } + +} \ No newline at end of file diff --git a/src/test/resources/sample/day-24.txt b/src/test/resources/sample/day-24.txt new file mode 100755 index 0000000..a41747d --- /dev/null +++ b/src/test/resources/sample/day-24.txt @@ -0,0 +1,2 @@ +inp x +mul x -1 diff --git a/src/test/resources/sample/day-25.txt b/src/test/resources/sample/day-25.txt new file mode 100755 index 0000000..0f3c0cd --- /dev/null +++ b/src/test/resources/sample/day-25.txt @@ -0,0 +1,9 @@ +v...>>.vv> +.vv>>.vv.. +>>.>v>...v +>>v>>.>.v. +v>v.vv.v.. +>.>>..v... +.vv..>.>v. +v.v..>>v.v +....v..v.> From c6cad88a788d5a74acc164e27369db4eae5fdcb8 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Tue, 29 Nov 2022 21:54:08 -0800 Subject: [PATCH 25/44] Prepare for 2022 --- .github/workflows/ci.yml | 10 +- .java-version | 2 +- README.md | 8 +- pom.xml | 10 +- src/test/java/com/macasaet/Day01.java | 46 +- src/test/java/com/macasaet/Day02.java | 110 ---- src/test/java/com/macasaet/Day03.java | 120 ---- src/test/java/com/macasaet/Day04.java | 181 ------ src/test/java/com/macasaet/Day05.java | 188 ------ src/test/java/com/macasaet/Day06.java | 120 ---- src/test/java/com/macasaet/Day07.java | 113 ---- src/test/java/com/macasaet/Day08.java | 225 ------- src/test/java/com/macasaet/Day09.java | 177 ----- src/test/java/com/macasaet/Day10.java | 137 ---- src/test/java/com/macasaet/Day11.java | 169 ----- src/test/java/com/macasaet/Day12.java | 182 ------ src/test/java/com/macasaet/Day13.java | 187 ------ src/test/java/com/macasaet/Day14.java | 166 ----- src/test/java/com/macasaet/Day15.java | 228 ------- src/test/java/com/macasaet/Day16.java | 352 ---------- src/test/java/com/macasaet/Day17.java | 138 ---- src/test/java/com/macasaet/Day18.java | 432 ------------- src/test/java/com/macasaet/Day19.java | 395 ----------- src/test/java/com/macasaet/Day20.java | 233 ------- src/test/java/com/macasaet/Day21.java | 159 ----- src/test/java/com/macasaet/Day22.java | 202 ------ src/test/java/com/macasaet/Day23.java | 900 -------------------------- src/test/java/com/macasaet/Day24.java | 284 -------- src/test/java/com/macasaet/Day25.java | 159 ----- src/test/resources/sample/day-01.txt | 10 - src/test/resources/sample/day-02.txt | 6 - src/test/resources/sample/day-03.txt | 12 - src/test/resources/sample/day-04.txt | 19 - src/test/resources/sample/day-05.txt | 10 - src/test/resources/sample/day-06.txt | 1 - src/test/resources/sample/day-07.txt | 1 - src/test/resources/sample/day-08.txt | 10 - src/test/resources/sample/day-09.txt | 5 - src/test/resources/sample/day-10.txt | 10 - src/test/resources/sample/day-11.txt | 10 - src/test/resources/sample/day-12.txt | 7 - src/test/resources/sample/day-13.txt | 21 - src/test/resources/sample/day-14.txt | 18 - src/test/resources/sample/day-15.txt | 10 - src/test/resources/sample/day-16.txt | 1 - src/test/resources/sample/day-17.txt | 1 - src/test/resources/sample/day-18.txt | 10 - src/test/resources/sample/day-19.txt | 136 ---- src/test/resources/sample/day-20.txt | 7 - src/test/resources/sample/day-21.txt | 2 - src/test/resources/sample/day-22.txt | 60 -- src/test/resources/sample/day-23.txt | 5 - src/test/resources/sample/day-24.txt | 2 - src/test/resources/sample/day-25.txt | 9 - 54 files changed, 28 insertions(+), 5988 deletions(-) delete mode 100644 src/test/java/com/macasaet/Day02.java delete mode 100644 src/test/java/com/macasaet/Day03.java delete mode 100644 src/test/java/com/macasaet/Day04.java delete mode 100644 src/test/java/com/macasaet/Day05.java delete mode 100644 src/test/java/com/macasaet/Day06.java delete mode 100644 src/test/java/com/macasaet/Day07.java delete mode 100644 src/test/java/com/macasaet/Day08.java delete mode 100644 src/test/java/com/macasaet/Day09.java delete mode 100644 src/test/java/com/macasaet/Day10.java delete mode 100644 src/test/java/com/macasaet/Day11.java delete mode 100644 src/test/java/com/macasaet/Day12.java delete mode 100644 src/test/java/com/macasaet/Day13.java delete mode 100644 src/test/java/com/macasaet/Day14.java delete mode 100644 src/test/java/com/macasaet/Day15.java delete mode 100644 src/test/java/com/macasaet/Day16.java delete mode 100644 src/test/java/com/macasaet/Day17.java delete mode 100644 src/test/java/com/macasaet/Day18.java delete mode 100644 src/test/java/com/macasaet/Day19.java delete mode 100644 src/test/java/com/macasaet/Day20.java delete mode 100644 src/test/java/com/macasaet/Day21.java delete mode 100644 src/test/java/com/macasaet/Day22.java delete mode 100644 src/test/java/com/macasaet/Day23.java delete mode 100755 src/test/java/com/macasaet/Day24.java delete mode 100755 src/test/java/com/macasaet/Day25.java delete mode 100644 src/test/resources/sample/day-01.txt delete mode 100644 src/test/resources/sample/day-02.txt delete mode 100644 src/test/resources/sample/day-03.txt delete mode 100644 src/test/resources/sample/day-04.txt delete mode 100644 src/test/resources/sample/day-05.txt delete mode 100644 src/test/resources/sample/day-06.txt delete mode 100644 src/test/resources/sample/day-07.txt delete mode 100644 src/test/resources/sample/day-08.txt delete mode 100644 src/test/resources/sample/day-09.txt delete mode 100644 src/test/resources/sample/day-10.txt delete mode 100644 src/test/resources/sample/day-11.txt delete mode 100644 src/test/resources/sample/day-12.txt delete mode 100644 src/test/resources/sample/day-13.txt delete mode 100644 src/test/resources/sample/day-14.txt delete mode 100644 src/test/resources/sample/day-15.txt delete mode 100644 src/test/resources/sample/day-16.txt delete mode 100644 src/test/resources/sample/day-17.txt delete mode 100644 src/test/resources/sample/day-18.txt delete mode 100644 src/test/resources/sample/day-19.txt delete mode 100644 src/test/resources/sample/day-20.txt delete mode 100644 src/test/resources/sample/day-21.txt delete mode 100644 src/test/resources/sample/day-22.txt delete mode 100644 src/test/resources/sample/day-23.txt delete mode 100755 src/test/resources/sample/day-24.txt delete mode 100755 src/test/resources/sample/day-25.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8c1a94..1e789d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,18 +10,18 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '17' + java-version: '19' check-latest: true - uses: actions/cache@v2 with: path: ~/.m2 - key: m2-${{ runner.os }}-${{ matrix.java-version }}-${{ hashFiles('**/pom.xml') }} + key: m2-${{ runner.os }}-19-${{ hashFiles('**/pom.xml') }} restore-keys: | - m2-${{ runner.os }}-${{ matrix.java-version }} + m2-${{ runner.os }}-19 m2-${{ runner.os }} m2 - run: mvn clean install diff --git a/.java-version b/.java-version index 98d9bcb..6ea9a3b 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -17 +19.0 diff --git a/README.md b/README.md index 8235f91..29e9c19 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -# Advent of Code 2021 (Java) +# Advent of Code 2022 (Java) This is the code I used to solve the -[Advent of Code](https://adventofcode.com/2021) puzzles. The main branch +[Advent of Code](https://adventofcode.com/2022) puzzles. The main branch contains the most recent year in which I participated. Other years have their own branch. ## Other Editions -* 2020 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/tree/2020) | [Rust](https://github.com/l0s/advent-of-code-rust)) +* 2020 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/tree/2020) | [Rust](https://github.com/l0s/advent-of-code-rust/releases/tag/y2020)) +* 2021 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/releases/tag/2021)) +* 2022 Advent of Code ([Rust](https://github.com/l0s/advent-of-code-rust)) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 7099d59..fc13d64 100644 --- a/pom.xml +++ b/pom.xml @@ -6,21 +6,21 @@ com.macasaet advent-of-code - 0.2021.0-SNAPSHOT + 0.2022.0-SNAPSHOT - Advent of Code 2021 + Advent of Code 2022 UTF-8 - 17 - 17 + 19 + 19 org.junit.jupiter junit-jupiter - 5.8.2 + 5.9.0 diff --git a/src/test/java/com/macasaet/Day01.java b/src/test/java/com/macasaet/Day01.java index 0485635..7af31fc 100644 --- a/src/test/java/com/macasaet/Day01.java +++ b/src/test/java/com/macasaet/Day01.java @@ -1,65 +1,43 @@ package com.macasaet; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import java.util.stream.StreamSupport; -import org.junit.jupiter.api.Test; - /** - * --- Day 1: Sonar Sweep --- + * --- Day 1: --- */ public class Day01 { /** - * Perform a sonar sweep of the nearby sea floor. * - * @return measurements of the sea floor depth further and further away from the submarine + * + * @return */ - protected List getInput() { + protected List getInput() { return StreamSupport .stream(new LineSpliterator("day-01.txt"), false) - .mapToInt(Integer::parseInt) .collect(ArrayList::new, List::add, List::addAll); } + @Disabled @Test public final void part1() { final var list = getInput(); - int increases = countIncreases(list); - System.out.println("Part 1: " + increases); + + System.out.println("Part 1: " + null); } + @Disabled @Test public final void part2() { final var list = getInput(); - final var windows = new LinkedList(); - for (int i = 2; i < list.size(); i++) { - windows.add(list.get(i) + list.get(i - 1) + list.get(i - 2)); - } - final int increases = countIncreases(windows); - System.out.println("Part 2: " + increases); - } - /** - * Determine how quickly the depth increases. - * - * @param list progressively further measurements of the sea floor depth - * @return the number of times a depth measurement increase from the previous measurement - */ - protected int countIncreases(final List list) { - int previous = list.get(0); - int increases = 0; - for (int i = 1; i < list.size(); i++) { - final var current = list.get(i); - if (current > previous) { - increases++; - } - previous = current; - } - return increases; + System.out.println("Part 2: " + null); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day02.java b/src/test/java/com/macasaet/Day02.java deleted file mode 100644 index f5a2e9f..0000000 --- a/src/test/java/com/macasaet/Day02.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.macasaet; - -import java.util.Locale; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 2: Dive! --- - */ -public class Day02 { - - public enum Operation { - FORWARD { - public Position adjust(Position position, int magnitude) { - return new Position(position.horizontalPosition() + magnitude, position.depth()); - } - - public OrientedPosition adjust(OrientedPosition position, int magnitude) { - return new OrientedPosition(position.horizontalPosition() + magnitude, - position.depth() + (position.aim() * magnitude), - position.aim()); - } - }, - DOWN { - public Position adjust(Position position, int magnitude) { - return new Position(position.horizontalPosition(), position.depth() + magnitude); - } - - public OrientedPosition adjust(OrientedPosition position, int magnitude) { - return new OrientedPosition(position.horizontalPosition(), - position.depth(), - position.aim() + magnitude); - } - }, - UP { - public Position adjust(Position position, int magnitude) { - return new Position(position.horizontalPosition(), position.depth() - magnitude); - } - - public OrientedPosition adjust(OrientedPosition position, int magnitude) { - return new OrientedPosition(position.horizontalPosition(), - position.depth(), - position.aim() - magnitude); - } - }; - - public abstract Position adjust(Position position, int magnitude); - - public abstract OrientedPosition adjust(OrientedPosition position, int magnitude); - } - - public record Command(Operation operation, int magnitude) { - public static Command parse(final String string) { - final String[] components = string.split(" "); - final var operation = Operation.valueOf(components[0].toUpperCase(Locale.US)); - final int magnitude = Integer.parseInt(components[1]); - return new Command(operation, magnitude); - } - - public Position adjust(final Position position) { - return operation().adjust(position, magnitude()); - } - - public OrientedPosition adjust(final OrientedPosition position) { - return operation().adjust(position, magnitude()); - } - } - - public record Position(int horizontalPosition, int depth) { - public int result() { - return horizontalPosition() * depth(); - } - } - - public record OrientedPosition(int horizontalPosition, int depth, int aim) { - public int result() { - return horizontalPosition() * depth(); - } - } - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-02.txt"), - false) - .map(Command::parse); - } - - @Test - public final void part1() { - var position = new Position(0, 0); - for (final var i = getInput().iterator(); i.hasNext(); ) { - final var operation = i.next(); - position = operation.adjust(position); - } - System.out.println("Part 1: " + position.result()); - } - - @Test - public final void part2() { - var position = new OrientedPosition(0, 0, 0); - for (final var i = getInput().iterator(); i.hasNext(); ) { - final var operation = i.next(); - position = operation.adjust(position); - } - System.out.println("Part 2: " + position.result()); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day03.java b/src/test/java/com/macasaet/Day03.java deleted file mode 100644 index 47f6ec5..0000000 --- a/src/test/java/com/macasaet/Day03.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.macasaet; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 3: Binary Diagnostic --- - */ -public class Day03 { - - /** - * @return "a list of binary numbers which, when decoded properly, can tell you many useful things about the - * conditions of the submarine" - */ - protected Stream getDiagnosticReport() { - return StreamSupport - .stream(new LineSpliterator("day-03.txt"), - false) - .map(string -> { - final var chars = string.toCharArray(); - final var bits = new byte[chars.length]; - for (int i = chars.length; --i >= 0; bits[i] = chars[i] == '0' ? (byte) 0 : (byte) 1) ; - return bits; - }); - } - - protected int toUnsignedInt(final byte[] bits) { - int result = 0; - for (int i = bits.length; --i >= 0; result += bits[i] * Math.pow(2, bits.length - i - 1)) ; - return result; - } - - @Test - public final void part1() { - final var list = getDiagnosticReport().collect(Collectors.toList()); - final int width = list.get(0).length; - final int[] zeroCounts = new int[width]; - for (int i = zeroCounts.length; --i >= 0; zeroCounts[i] = 0) ; - final int[] oneCounts = new int[width]; - for (int i = oneCounts.length; --i >= 0; oneCounts[i] = 0) ; - for (final var array : list) { - for (int j = 0; j < width; j++) { - if (array[j] == 0) { - zeroCounts[j] += 1; - } else { - oneCounts[j] += 1; - } - } - } - final byte[] gammaArray = new byte[width]; - final byte[] epsilonArray = new byte[width]; - for (int i = gammaArray.length; --i >= 0; ) { - gammaArray[i] = zeroCounts[i] > oneCounts[i] ? (byte) 0 : (byte) 1; - epsilonArray[i] = zeroCounts[i] > oneCounts[i] ? (byte) 1 : (byte) 0; - } - - final int gammaRate = toUnsignedInt(gammaArray); - final int epsilonRate = toUnsignedInt(epsilonArray); - System.out.println("Part 1: " + (gammaRate * epsilonRate)); - } - - @Test - public final void part2() { - final var list = getDiagnosticReport().collect(Collectors.toList()); - final int width = list.get(0).length; - List oxygenCandidates = new ArrayList<>(list); - for (int i = 0; i < width && oxygenCandidates.size() > 1; i++) { - int zeros = 0; - int ones = 0; - for (final var value : oxygenCandidates) { - if (value[i] == 0) { - zeros++; - } else { - ones++; - } - } - final int index = i; - if (ones >= zeros) { - oxygenCandidates = oxygenCandidates.stream().filter(value -> value[index] == 1).collect(Collectors.toList()); - } else { - oxygenCandidates = oxygenCandidates.stream().filter(value -> value[index] == 0).collect(Collectors.toList()); - } - } - if (oxygenCandidates.size() > 1) { - throw new IllegalStateException("Too many oxygen candidates"); - } - List co2Candidates = new ArrayList<>(list); - for (int i = 0; i < width && co2Candidates.size() > 1; i++) { - int zeros = 0; - int ones = 0; - for (final var value : co2Candidates) { - if (value[i] == 0) { - zeros++; - } else { - ones++; - } - } - final int index = i; - if (zeros <= ones) { - co2Candidates = co2Candidates.stream().filter(value -> value[index] == 0).collect(Collectors.toList()); - } else { - co2Candidates = co2Candidates.stream().filter(value -> value[index] == 1).collect(Collectors.toList()); - } - } - if (co2Candidates.size() > 1) { - throw new IllegalStateException("Too many CO2 candidates"); - } - final byte[] oxyArray = oxygenCandidates.get(0); - final byte[] co2Array = co2Candidates.get(0); - final int oxygenGeneratorRating = toUnsignedInt(oxyArray); - final int co2ScrubberRating = toUnsignedInt(co2Array); - System.out.println("Part 2: " + (oxygenGeneratorRating * co2ScrubberRating)); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day04.java b/src/test/java/com/macasaet/Day04.java deleted file mode 100644 index d69541d..0000000 --- a/src/test/java/com/macasaet/Day04.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.macasaet; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 4: Giant Squid --- - */ -public class Day04 { - - /** - * The number of rows and columns on each square Bingo card - */ - static final int EDGE_LENGTH = 5; - - public static class Board { - private final int[][] grid; - private final boolean[][] marked; - - protected Board(final int[][] grid, final boolean[][] marked) { - this.grid = grid; - this.marked = marked; - } - - public Board(final int[][] grid) { - this(grid, new boolean[grid.length][]); - for (int i = grid.length; --i >= 0; this.marked[i] = new boolean[grid.length]) ; - } - - public boolean isWinner() { - // check rows - for (int i = marked.length; --i >= 0; ) { - final var row = marked[i]; - boolean complete = true; - for (int j = row.length; --j >= 0 && complete; complete = row[j]) ; - if (complete) { - return true; - } - } - // check columns - for (int j = marked.length; --j >= 0; ) { - boolean complete = true; - for (int i = marked.length; --i >= 0 && complete; complete = marked[i][j]) ; - if (complete) { - return true; - } - } - return false; - } - - public int score(final int lastDrawn) { - int sum = 0; - for (int i = grid.length; --i >= 0; ) { - final var row = grid[i]; - for (int j = row.length; --j >= 0; ) { - if (!marked[i][j]) { - sum += row[j]; - } - } - } - return sum * lastDrawn; - } - - public void mark(final int drawn) { - for (int i = grid.length; --i >= 0; ) { - final var row = grid[i]; - for (int j = row.length; --j >= 0; ) { - if (row[j] == drawn) { - marked[i][j] = true; - } - } - } - } - } - - public record Game(List boards, List numbers) { - - public int countBoards() { - return boards().size(); - } - - public void removeBoard(final int index) { - this.boards().remove(index); - } - - } - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-04.txt"), - false); - } - - protected Game getGame() { - final var input = getInput().iterator(); - final var moves = input.next(); - - List boards = new ArrayList<>(); - int[][] grid = null; - int gridIndex = -1; - while (input.hasNext()) { - final var line = input.next(); - if (line.isBlank()) { - if (grid != null) { - boards.add(new Board(grid)); - } - grid = new int[EDGE_LENGTH][]; - for (int i = EDGE_LENGTH; --i >= 0; grid[i] = new int[EDGE_LENGTH]) ; - gridIndex = 0; - continue; - } - final var cells = line.split("\\s"); - if (cells.length > 0) { - final var values = Arrays.stream(cells) - .filter(candidate -> !candidate.isBlank()) - .mapToInt(Integer::parseInt) - .toArray(); - if (values.length > 0) { - grid[gridIndex++] = values; - } - } - } - if (grid != null) { - boards.add(new Board(grid)); - } - final var moveArray = Arrays.stream(moves.split(",")) - .mapToInt(Integer::parseInt) - .collect(ArrayList::new, List::add, List::addAll); - return new Game(boards, moveArray); - } - - @Test - public final void part1() { - final var game = getGame(); - for (final var number : game.numbers()) { - for (final var board : game.boards()) { - board.mark(number); - if (board.isWinner()) { - final int score = board.score(number); - System.out.println("Part 1: " + score); - return; - } - } - } - throw new IllegalStateException("No winners"); - } - - @Test - public final void part2() { - final var game = getGame(); - for (final var number : game.numbers()) { - if (game.countBoards() == 1) { - final var lastWinner = game.boards().get(0); - lastWinner.mark(number); - if (!lastWinner.isWinner()) { - continue; - } - System.out.println("Part 2: " + lastWinner.score(number)); - return; - } - final List idsToRemove = new ArrayList<>(); - for (int i = game.boards().size(); --i >= 0; ) { - final var board = game.boards().get(i); - board.mark(number); - if (board.isWinner()) { - idsToRemove.add(i); - } - } - for (final var id : idsToRemove) { - game.removeBoard(id); - } - } - throw new IllegalStateException("Tie for last place"); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day05.java b/src/test/java/com/macasaet/Day05.java deleted file mode 100644 index bb23695..0000000 --- a/src/test/java/com/macasaet/Day05.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.macasaet; - -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 5: Hydrothermal Venture --- - */ -public class Day05 { - - /** - * A point on the ocean floor - */ - public static record Point(int x, int y) { - public static Point parse(final String string) { - final var components = string.split(","); - return new Point(Integer.parseInt(components[0]), Integer.parseInt(components[1])); - } - - } - - /** - * A portion of the ocean floor on which there are hydrothermal vents, which produce large, opaque clouds - */ - public static record LineSegment(Point start, Point end) { - public static LineSegment parse(final String string) { - final var components = string.split(" -> "); - return new LineSegment(Point.parse(components[0]), Point.parse(components[1])); - } - - /** - * Highlight the location of this line segment on the diagram. Each cell that this segment covers will have its - * value incremented. The higher the number, the more vents cover the cell. - * - * @param diagram A map of the ocean floor which will be updated by this call. - */ - public void update(final int[][] diagram) { - /* - "Because of the limits of the hydrothermal vent mapping system, the lines in your list will only ever be - horizontal, vertical, or a diagonal line at exactly 45 degrees." - */ - final int horizontalStep = start().x() == end().x() - ? 0 - : start().x() < end().x() - ? 1 - : -1; - final int verticalStep = start().y() == end().y() - ? 0 - : start().y() < end().y() - ? 1 - : -1; - final Predicate xTester = start().x() == end().x() - ? x -> true - : start().x() < end().x() - ? x -> x <= end().x() - : x -> x >= end().x(); - final Predicate yTester = start().y() == end().y() - ? y -> true - : start().y() < end().y() - ? y -> y <= end().y() - : y -> y >= end().y(); - - for (int i = start().x(), j = start().y(); - xTester.test(i) && yTester.test(j); - i += horizontalStep, j += verticalStep) { - diagram[i][j]++; - } - } - - public int lowestX() { - return Math.min(start().x(), end().x()); - } - - public int highestX() { - return Math.max(start().x(), end().x()); - } - - public int lowestY() { - return Math.min(start().y(), end().y()); - } - - public int highestY() { - return Math.max(start().y(), end().y()); - } - - public String toString() { - return start() + " -> " + end(); - } - } - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-05.txt"), - false) - .map(LineSegment::parse); - } - - protected record Extremes(int lowestX, int lowestY, int highestX, int highestY) { - public Extremes combine(final LineSegment segment) { - return new Extremes(Math.min(lowestX(), segment.lowestX()), - Math.min(lowestY(), segment.lowestY()), - Math.max(highestX(), segment.highestX()), - Math.max(highestY(), segment.highestY())); - } - - public Extremes combine(final Extremes other) { - return new Extremes(Math.min(lowestX(), other.lowestX()), - Math.min(lowestY(), other.lowestY()), - Math.max(highestX(), other.highestX()), - Math.max(highestY(), other.highestY())); - } - - public int[][] createBlankDiagram() { - final int[][] result = new int[highestX() + 1][]; - for (int i = result.length; --i >= 0; result[i] = new int[highestY() + 1]) ; - return result; - } - } - - @Test - public final void part1() { - final var segments = getInput() - // "For now, only consider horizontal and vertical lines: lines where either x1 = x2 or y1 = y2." - .filter(segment -> segment.start().x() == segment.end().x() || segment.start().y() == segment.end().y()) - .collect(Collectors.toList()); - - final var extremes = segments - .stream() - .reduce(new Extremes(0, 0, 0, 0), - Extremes::combine, - Extremes::combine); - // there are no negative values - // Note, we could save a little bit of space and time by using a smaller map since none of the line segments - // need point 0,0. However, the savings are likely negligible. - final int[][] diagram = extremes.createBlankDiagram(); - - for (final var segment : segments) { - segment.update(diagram); - } - int sum = 0; - for (int i = diagram.length; --i >= 0; ) { - final var row = diagram[i]; - for (int j = row.length; --j >= 0; ) { - if (row[j] >= 2) { - sum++; - } - } - } - System.out.println("Part 1: " + sum); - } - - @Test - public final void part2() { - /* - "Unfortunately, considering only horizontal and vertical lines doesn't give you the full picture; you need to - also consider diagonal lines." - */ - final var segments = getInput() - .collect(Collectors.toList()); - final var extremes = segments - .stream() - .reduce(new Extremes(0, 0, 0, 0), - Extremes::combine, - Extremes::combine); - // there are no negative values - // Note, we could save a little bit of space and time by using a smaller map since none of the line segments - // need point 0,0. However, the savings are likely negligible. - final int[][] diagram = extremes.createBlankDiagram(); - for (final var segment : segments) { - segment.update(diagram); - } - int sum = 0; - for (int i = diagram.length; --i >= 0; ) { - final var row = diagram[i]; - for (int j = row.length; --j >= 0; ) { - if (row[j] >= 2) { - sum++; - } - } - } - System.out.println("Part 2: " + sum); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day06.java b/src/test/java/com/macasaet/Day06.java deleted file mode 100644 index 10a59b3..0000000 --- a/src/test/java/com/macasaet/Day06.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.macasaet; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 6: Lanternfish --- - */ -public class Day06 { - - /** - * A glowing fish that spawns very quickly. Their population grows exponentially. - */ - public static class Lanternfish { - - private int daysToSpawn; - - /** - * @param daysToSpawn the number of days until it creates a new {@link Lanternfish} - */ - public Lanternfish(final int daysToSpawn) { - setDaysToSpawn(daysToSpawn); - } - - /** - * Simulate the passage of one day - * - * @return either a new Lanternfish or nothing depending on whether the fish spawned - */ - public Optional tick() { - final var timer = getDaysToSpawn() - 1; - if (timer < 0) { - setDaysToSpawn(6); - return Optional.of(new Lanternfish(8)); - } else { - setDaysToSpawn(timer); - return Optional.empty(); - } - } - - /** - * @return the number of days until the fish spawns - */ - public int getDaysToSpawn() { - return this.daysToSpawn; - } - - /** - * Update this fish's days to spawn - * - * @param daysToSpawn the number of days until the fish spawns, must be non-negative - */ - protected void setDaysToSpawn(final int daysToSpawn) { - if (daysToSpawn < 0) { - throw new IllegalArgumentException("\"days to spawn\" must be non-negative"); - } - this.daysToSpawn = daysToSpawn; - } - } - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-06.txt"), - false); - } - - public List parseInput() { - final var list = getInput().toList(); - final var line = list.get(0); - final var components = line.split(","); - return Arrays.stream(components) - .mapToInt(Integer::parseInt) - .mapToObj(Lanternfish::new) - .collect(Collectors.toList()); - } - - @Test - public final void part1() { - var population = parseInput(); - for (int _i = 80; --_i >= 0; ) { - final List list = new ArrayList<>(population); - for (final var fish : population) { - final var result = fish.tick(); - result.ifPresent(list::add); - } - population = list; - } - System.out.println("Part 1: " + population.size()); - } - - @Test - public final void part2() { - final var initial = parseInput(); - var map = new long[9]; - for (final var fish : initial) { - map[fish.getDaysToSpawn()]++; - } - for (int _i = 256; --_i >= 0; ) { - final var temp = new long[map.length]; - for (int daysToSpawn = map.length; --daysToSpawn >= 0; ) { - final var count = map[daysToSpawn]; - final var prototype = new Lanternfish(daysToSpawn); - final var result = prototype.tick(); - temp[prototype.getDaysToSpawn()] += count; - result.ifPresent(spawn -> temp[spawn.getDaysToSpawn()] = temp[spawn.getDaysToSpawn()] + count); - } - map = temp; - } - final var result = Arrays.stream(map).reduce(0L, Long::sum); - System.out.println("Part 2: " + result); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day07.java b/src/test/java/com/macasaet/Day07.java deleted file mode 100644 index 4393d46..0000000 --- a/src/test/java/com/macasaet/Day07.java +++ /dev/null @@ -1,113 +0,0 @@ -package com.macasaet; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 7: The Treachery of Whales --- - */ -public class Day07 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-07.txt"), - false); - } - - /** - * @return the horizontal position of each crab submarine in the swarm - */ - protected List getCrabPositions() { - final var list = getInput().collect(Collectors.toList()); - final var line = list.get(0); - return Arrays.stream(line.split(",")) - .mapToInt(Integer::parseInt) - .collect(ArrayList::new, List::add, List::addAll); - } - - /** - * Assuming a constant fuel consumption rate, calculate the fuel required for the swarm to reach alignmentPoint. - * - * @param positions the starting position of each crab submarine - * @param alignmentPoint a potential point for the crabs to gather in order to blast a hole in the ocean floor - * @return the fuel required to reach the alignment point - */ - protected int calculateConstantFuel(final Iterable positions, final int alignmentPoint) { - int sum = 0; - for (final var position : positions) { - sum += Math.abs(alignmentPoint - position); - } - return sum; - } - - /** - * Calculate the fuel required for the swarm to reach alignmentPoint - * - * @param positions the starting position for each crab submarine - * @param alignmentPoint a potential point for the crabs to gather in order to blast a hole in the ocean floor - * @return the fuel required to reach the alignment point - */ - protected int calculateFuel(final Iterable positions, final int alignmentPoint) { - int sum = 0; - for (final var position : positions) { - sum += calculateFuel(position, alignmentPoint); - } - return sum; - } - - /** - * Calculate the fuel required for a single crab submarine to travel from one horizontal position to the next. - * - * @param start the starting position (inclusive) - * @param end the ending position (inclusive) - * @return the amount of fuel consumed in the journey - */ - protected int calculateFuel(final int start, final int end) { - final int target = Math.abs(end - start); - int sum = 0; - for (int i = target; --i >= 0; ) { - sum += i + 1; - } - return sum; - } - - @Test - public final void part1() { - int min = Integer.MAX_VALUE; - int max = Integer.MIN_VALUE; - final var positions = getCrabPositions(); - for (final var position : positions) { - min = Math.min(min, position); - max = Math.max(max, position); - } - final int result = IntStream.range(min, max) - .map(alignmentPoint -> calculateConstantFuel(positions, alignmentPoint)) - .min() - .getAsInt(); - System.out.println("Part 1: " + result); - } - - @Test - public final void part2() { - int min = Integer.MAX_VALUE; - int max = Integer.MIN_VALUE; - final var positions = getCrabPositions(); - for (final var position : positions) { - min = Math.min(min, position); - max = Math.max(max, position); - } - final int result = IntStream.range(min, max) - .map(alignmentPoint -> calculateFuel(positions, alignmentPoint)) - .min() - .getAsInt(); - 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 deleted file mode 100644 index 7ac8b58..0000000 --- a/src/test/java/com/macasaet/Day08.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.macasaet; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 8: Seven Segment Search --- - */ -public class Day08 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-08.txt"), - false); - } - - public record Digit(List segments) { - public Digit decode(final Map map) { - return new Digit(segments().stream() - .map(map::get) - .collect(Collectors.toUnmodifiableList())); - } - - public int asInt() { - if (segments().size() == 6 && hasSegments('a', 'b', 'c', 'e', 'f', 'g')) { // FIXME use on/off mechanism - return 0; - } else if (segments().size() == 2 && hasSegments('c', 'f')) { - return 1; - } else if (segments().size() == 5 && hasSegments('a', 'c', 'd', 'e', 'g')) { - return 2; - } else if (segments().size() == 5 && hasSegments('a', 'c', 'd', 'f', 'g')) { - return 3; - } else if (segments().size() == 4 && hasSegments('b', 'c', 'd', 'f')) { - return 4; - } else if (segments().size() == 5 && hasSegments('a', 'b', 'd', 'f', 'g')) { - return 5; - } else if (segments().size() == 6 && hasSegments('a', 'b', 'd', 'e', 'f', 'g')) { - return 6; - } else if (segments().size() == 3 && hasSegments('a', 'c', 'f')) { - return 7; - } else if (segments().size() == 7 && hasSegments('a', 'b', 'c', 'd', 'e', 'f', 'g')) { - return 8; - } else if (segments().size() == 6 && hasSegments('a', 'b', 'c', 'd', 'f', 'g')) { - return 9; - } - throw new IllegalStateException("Invalid Digit: " + this); - } - - public boolean hasSegments(final char... segments) { - for (final var segment : segments) { - if (!hasSegment(segment)) { - return false; - } - } - return true; - } - - public boolean hasSegment(final char segment) { - return segments().contains(segment); - } - - public static Digit parse(final String string) { - final var array = string.toCharArray(); - final var list = new ArrayList(array.length); - for (final var c : array) { - list.add(c); - } - return new Digit(Collections.unmodifiableList(list)); - } - } - - public static record Entry(List uniqueSignalPatterns, List outputValue) { - - public int decodeOutput() { - final var map = buildDecodingMap(); - final StringBuilder builder = new StringBuilder(); - for (final var outputDigit : outputValue()) { - final var decodedDigit = outputDigit.decode(map); - final int digit = decodedDigit.asInt(); - builder.append(digit); - } - final String stringInt = builder.toString(); - return Integer.parseInt(stringInt); - } - - protected SortedSet getDigitSegmentsWithCount(final int n) { - return uniqueSignalPatterns().stream() - .filter(digit -> digit.segments().size() == n) - .findFirst() - .get() - .segments() - .stream() - .collect(TreeSet::new, SortedSet::add, SortedSet::addAll); - } - - protected Set getDigitsWithCount(final int n) { // TODO return stream - return uniqueSignalPatterns() - .stream() - .filter(digit -> digit.segments().size() == n).collect(Collectors.toUnmodifiableSet()); - } - - public Map buildDecodingMap() { - final var encodingMap = buildEncodingMap(); - final var result = new HashMap(); - for(final var entry : encodingMap.entrySet()) { - result.put(entry.getValue(), entry.getKey()); - } - return Collections.unmodifiableMap(result); - } - - public Map buildEncodingMap() { - final var map = new HashMap(); - final var oneSegments = getDigitSegmentsWithCount(2); - final var sevenSegments = getDigitSegmentsWithCount(3); - final var fourSegments = getDigitSegmentsWithCount(4); - final var eightSegments = getDigitSegmentsWithCount(7); - final var aMapping = sevenSegments.stream().filter(c -> !oneSegments.contains(c)).findFirst().get(); - map.put('a', aMapping); - - final var zeroSixNine = getDigitsWithCount(6); - var zsnSegments = zeroSixNine.stream().flatMap(digit -> digit.segments().stream()).collect(Collectors.toList()); - zsnSegments.removeIf(sevenSegments::contains); - zsnSegments.removeIf(fourSegments::contains); - final var sssMap = new HashMap(); - for (final var c : zsnSegments) { - sssMap.compute(c, (_key, old) -> old == null ? 1 : old + 1); - } - if(sssMap.size() != 2) { - throw new IllegalStateException("More segments for 0, 6, 9 encountered: " + sssMap); - } - for (final var entry : sssMap.entrySet()) { - if (entry.getValue() == 3) { - map.put('g', entry.getKey()); - } else if (entry.getValue() == 2) { - map.put('e', entry.getKey()); - } else { - throw new IllegalStateException(); - } - } - - final var twoFiveThree = getDigitsWithCount(5); - var tftSegments = twoFiveThree.stream().flatMap(digit -> digit.segments.stream()).collect(Collectors.toList()); - tftSegments.removeIf(sevenSegments::contains); - tftSegments.removeIf(candidate -> candidate.equals(map.get('e'))); - tftSegments.removeIf(candidate -> candidate.equals(map.get('g'))); - final var tftCounts = new HashMap(); - for(final var c : tftSegments) { - tftCounts.compute(c, (_key, old) -> old == null ? 1 : old + 1); - } - for(final var entry : tftCounts.entrySet()) { - if(entry.getValue() == 3) { - map.put('d', entry.getKey()); - } else if(entry.getValue() == 1) { - map.put('b', entry.getKey()); - } else { - throw new IllegalStateException(); - } - } - - zsnSegments = zeroSixNine.stream().flatMap(digit -> digit.segments().stream()).collect(Collectors.toList()); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('a'))); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('b'))); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('d'))); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('e'))); - zsnSegments.removeIf(candidate -> candidate.equals(map.get('g'))); - final var zsnCounts = new HashMap(); - for(final var c : zsnSegments) { - zsnCounts.compute(c, (_key, old) -> old == null ? 1 : old + 1); - } - for(final var entry : zsnCounts.entrySet()) { - if(entry.getValue() == 2) { - map.put('c', entry.getKey()); - } else if( entry.getValue() == 3) { - map.put('f', entry.getKey()); - } else { - throw new IllegalStateException(); - } - } - - return map; - } - - public static Entry parse(final String string) { - final var components = string.split(" \\| "); - final var uniqueSignalPatterns = components[0].split(" "); - final var outputValue = components[1].split(" "); - - return new Entry(Arrays.stream(uniqueSignalPatterns) - .map(Digit::parse) - .collect(Collectors.toUnmodifiableList()), - Arrays.stream(outputValue) - .map(Digit::parse) - .collect(Collectors.toUnmodifiableList())); - } - - } - - @Test - public final void part1() { - final var result = getInput() - .map(Entry::parse) - .flatMap(entry -> entry.outputValue().stream()) - .filter(digit -> { - final var segments = digit.segments(); - final var numSegments = segments.size(); - return numSegments == 2 || numSegments == 4 || numSegments == 3 || numSegments == 7; - }) - .count(); - System.out.println("Part 1: " + result); - } - - @Test - public final void part2() { - final var result = getInput() - .map(Entry::parse) - .mapToInt(Entry::decodeOutput).sum(); - - 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 deleted file mode 100644 index abd2f4c..0000000 --- a/src/test/java/com/macasaet/Day09.java +++ /dev/null @@ -1,177 +0,0 @@ -package com.macasaet; - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 9: Smoke Basin --- - */ -public class Day09 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-09.txt"), - false); - } - - public HeightMap getHeightMap() { - final var list = getInput().map(line -> { - final var chars = line.toCharArray(); - final var ints = new int[chars.length]; - for (int i = chars.length; --i >= 0; ints[i] = chars[i] - '0') ; - return ints; - }).collect(Collectors.toList()); - final int[][] grid = new int[list.size()][]; - for (int i = list.size(); --i >= 0; grid[i] = list.get(i)) ; - return new HeightMap(grid); - } - - /** - * A height map of the floor of the nearby caves generated by the submarine - */ - public record HeightMap(int[][] grid) { // FIXME use bytes - - public Stream points() { - return IntStream.range(0, grid().length) - .boxed() - .flatMap(i -> IntStream.range(0, grid()[i].length) - .mapToObj(j -> new Point(i, j))); - } - - /** - * A location on the floor of a nearby cave - */ - public class Point { - final int x; - final int y; - - public Point(final int x, final int y) { - this.x = x; - this.y = y; - } - - public int x() { - return this.x; - } - - public int y() { - return this.y; - } - - public int getBasinSize() { - return getBasinPoints().size(); - } - - /** - * Identify all the higher points that are also part of the same basin, assuming this location is part of a - * basin. - * - * @return all the higher points, if any, that are part of the same basin. - */ - public Set getBasinPoints() { - if (getHeight() >= 9) { - return Collections.emptySet(); - } - final var result = new HashSet(); - result.add(this); - final Function> basinPointRetriever = neighbour -> { - if (neighbour.getHeight() >= 9 || neighbour.getHeight() <= getHeight() || result.contains(neighbour)) { - return Stream.empty(); - } - return neighbour.getBasinPoints().stream(); - }; - above().stream().flatMap(basinPointRetriever).forEach(result::add); - below().stream().flatMap(basinPointRetriever).forEach(result::add); - left().stream().flatMap(basinPointRetriever).forEach(result::add); - right().stream().flatMap(basinPointRetriever).forEach(result::add); - return Collections.unmodifiableSet(result); - } - - /** - * @return true if and only if this location is lower than all of its adjacent locations (up to four, - * diagonals do not count) - */ - public boolean isLowPoint() { - final var compareTo = new ArrayList(4); - above().ifPresent(compareTo::add); - below().ifPresent(compareTo::add); - left().ifPresent(compareTo::add); - right().ifPresent(compareTo::add); - return compareTo.stream().allMatch(neighbour -> neighbour.getHeight() > getHeight()); - } - - /** - * @return an assessment of the risk from smoke flowing through the cave - */ - public int getRiskLevel() { - return getHeight() + 1; - } - - /** - * @return the height of this particular location, from 0-9 - */ - public int getHeight() { - return grid()[x()][y()]; - } - - public Optional above() { - return x() > 0 ? Optional.of(new Point(x() - 1, y())) : Optional.empty(); - } - - public Optional below() { - return x() < grid().length - 1 ? Optional.of(new Point(x() + 1, y())) : Optional.empty(); - } - - public Optional left() { - return y() > 0 ? Optional.of(new Point(x(), y() - 1)) : Optional.empty(); - } - - public Optional right() { - return y() < grid()[x()].length - 1 ? Optional.of(new Point(x(), y() + 1)) : Optional.empty(); - } - - public int hashCode() { - return Objects.hash(x(), y()); - } - - public boolean equals(final Object o) { - try { - final Point other = (Point) o; - return this.x() == other.x() && this.y() == other.y(); - } catch (final ClassCastException cce) { - return false; - } - } - } - - } - - @Test - public final void part1() { - final var map = getHeightMap(); - final int sum = map.points() - .filter(HeightMap.Point::isLowPoint) - .mapToInt(HeightMap.Point::getRiskLevel) - .sum(); - System.out.println("Part 1: " + sum); - } - - @Test - public final void part2() { - final var map = getHeightMap(); - final var basinSizes = map.points() - .filter(HeightMap.Point::isLowPoint) - .mapToInt(HeightMap.Point::getBasinSize) - .collect(() -> new TreeSet(Comparator.reverseOrder()), SortedSet::add, SortedSet::addAll); - final var iterator = basinSizes.iterator(); - final var result = iterator.next() * iterator.next() * iterator.next(); - 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 deleted file mode 100644 index a7723f4..0000000 --- a/src/test/java/com/macasaet/Day10.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.macasaet; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 10: Syntax Scoring --- - */ -public class Day10 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-10.txt"), - false); - } - - /** - * The type of open and closing delimiter for a chunk in the navigation subsystem - */ - public enum BracketType { - PARENTHESIS('(', ')', 3, 1), - SQUARE('[', ']', 57, 2), - CURLY('{', '}', 1197, 3), - ANGLED('<', '>', 25137, 4); - - private final char open; - private final char close; - private final int corruptionPoints; - private final int autocompletePoints; - - BracketType(char open, char close, int corruptionPoints, final int autocompletePoints) { - this.open = open; - this.close = close; - this.corruptionPoints = corruptionPoints; - this.autocompletePoints = autocompletePoints; - } - - public static BracketType forOpen(final char c) { - return switch (c) { - case '(' -> PARENTHESIS; - case '[' -> SQUARE; - case '{' -> CURLY; - case '<' -> ANGLED; - default -> throw new IllegalStateException("Unexpected value: " + c); - }; - } - - public static BracketType forClose(final char c) { - return switch (c) { - case ')' -> PARENTHESIS; - case ']' -> SQUARE; - case '}' -> CURLY; - case '>' -> ANGLED; - default -> throw new IllegalStateException("Unexpected value: " + c); - }; - } - } - - /** - * @param line a line in the navigation subsystem - * @return a score of how corrupt the line is. A score of zero means it is not corrupt. The higher the value, the - * more corrupt the line is. - */ - public int calculateCorruptionScore(final char[] line) { - final var stack = new LinkedList(); - for (int i = 0; i < line.length; i++) { - final var c = line[i]; - if (c == '(' || c == '[' || c == '{' || c == '<') { - stack.push(BracketType.forOpen(c)); - } else if (c == ')' || c == ']' || c == '}' || c == '>') { - if (stack.peek().close == c) { - stack.pop(); - } else { - // corrupt - return BracketType.forClose(c).corruptionPoints; - } - } - } - // if stack is not empty, it's incomplete - return 0; - } - - /** - * @param line a non-corrupt line in the navigation subsystem. Behaviour is undefined for corrupt lines. - * @return the score for the suffix required to complete the line - */ - public long calculateCompletionScore(final char[] line) { - final var stack = new LinkedList(); - for (int i = 0; i < line.length; i++) { - final var c = line[i]; - if (c == '(' || c == '[' || c == '{' || c == '<') { - stack.push(BracketType.forOpen(c)); - } else if (c == ')' || c == ']' || c == '}' || c == '>') { - if (stack.peek().close == c) { - stack.pop(); - } else { - throw new IllegalArgumentException("Corrupt: " + new String(line)); - } - } - } - long result = 0; - while (!stack.isEmpty()) { - final var unclosed = stack.pop(); - result = result * 5 + unclosed.autocompletePoints; - } - return result; - } - - @Test - public final void part1() { - final var result = getInput() - .map(String::toCharArray) - .filter(line -> line.length > 0) - .mapToInt(this::calculateCorruptionScore) - .sum(); - System.out.println("Part 1: " + result); - } - - @Test - public final void part2() { - final var list = getInput() - .map(String::toCharArray) - .filter(line -> line.length > 0) - .filter(line -> calculateCorruptionScore(line) <= 0) // discard corrupted lines - .mapToLong(this::calculateCompletionScore) - .sorted() - .collect(ArrayList::new, List::add, List::addAll); - final var result = list.get(list.size() / 2); - System.out.println("Part 2: " + result); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day11.java b/src/test/java/com/macasaet/Day11.java deleted file mode 100644 index f566fd3..0000000 --- a/src/test/java/com/macasaet/Day11.java +++ /dev/null @@ -1,169 +0,0 @@ -package com.macasaet; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 11: Dumbo Octopus --- - */ -public class Day11 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-11.txt"), - false); - } - - /** - * @return a spatial grid of the cavern indicating the location of the octopuses - */ - protected Octopus[][] getOctopusGrid() { - final var list = getInput().map(line -> { - final var chars = line.toCharArray(); - final byte[] result = new byte[chars.length]; - for (int i = chars.length; --i >= 0; result[i] = (byte) (chars[i] - '0')) ; - return result; - }).collect(ArrayList::new, List::add, List::addAll); - final var result = new Octopus[list.size()][]; - for (int i = list.size(); --i >= 0; ) { - final var row = list.get(i); - result[i] = new Octopus[row.length]; - for (int j = row.length; --j >= 0; result[i][j] = new Octopus(i, j, row[j])) ; - } - return result; - } - - /** - * A rare bioluminescent dumbo octopus - */ - public static class Octopus { - private final int x, y; - private byte energyLevel; - - public Octopus(final int x, final int y, final byte energyLevel) { - this.x = x; - this.y = y; - this.energyLevel = energyLevel; - } - - /** - * Increase the octopus' energy level and, if appropriate, propagate side effects to its neighbours. - * - * @param population the full population of octopuses - */ - public void prepareStep(final Population population) { - if (this.energyLevel > 9) { - // "An octopus can only flash at most once per step." - return; - } - // "First, the energy level of each octopus increases by 1." - this.energyLevel++; - if (this.energyLevel > 9) { - // "Then, any octopus with an energy level greater than 9 flashes. This increases the energy level of - // all adjacent octopuses by 1, including octopuses that are diagonally adjacent." - final var grid = population.grid(); - final var hasRowAbove = x > 0; - final var hasRowBelow = x < grid.length - 1; - final var hasColumnToLeft = y > 0; - final var hasColumnToRight = y < grid[x].length - 1; - - if (hasRowAbove) { - grid[x - 1][y].prepareStep(population); - if (hasColumnToLeft) { - grid[x - 1][y - 1].prepareStep(population); - } - if (hasColumnToRight) { - grid[x - 1][y + 1].prepareStep(population); - } - } - if (hasColumnToLeft) { - grid[x][y - 1].prepareStep(population); - } - if (hasColumnToRight) { - grid[x][y + 1].prepareStep(population); - } - if (hasRowBelow) { - grid[x + 1][y].prepareStep(population); - if (hasColumnToLeft) { - grid[x + 1][y - 1].prepareStep(population); - } - if (hasColumnToRight) { - grid[x + 1][y + 1].prepareStep(population); - } - } - } - } - - /** - * Complete the step and finalise any side effects. - * - * @return true if and only if the octopus flashed during this step. - */ - public boolean finishStep() { - if (this.energyLevel > 9) { - // "Finally, any octopus that flashed during this step has its energy level set to 0, as it used all of - // its energy to flash." - this.energyLevel = 0; - return true; - } - return false; - } - } - - /** - * The full population of dumbo octopuses. The population members will be modified with each step. - */ - public record Population(Octopus[][] grid) { - public int step() { - for (int i = grid.length; --i >= 0; ) { - final var row = grid[i]; - for (int j = row.length; --j >= 0; ) { - row[j].prepareStep(this); - } - } - int flashes = 0; - for (int i = grid.length; --i >= 0; ) { - final var row = grid[i]; - for (int j = row.length; --j >= 0; ) { - if (row[j].finishStep()) flashes++; - } - } - return flashes; - } - - } - - @Test - public final void part1() { - final var energyLevels = getOctopusGrid(); - final var population = new Population(energyLevels); - - int flashes = 0; - - for (int step = 0; step < 100; step++) { - flashes += population.step(); - } - - System.out.println("Part 1: " + flashes); - } - - @Test - public final void part2() { - final var energyLevels = getOctopusGrid(); - final var population = new Population(energyLevels); - int step = 0; - while(true) { - final int flashes = population.step(); - step++; - if(flashes == 100) { - System.out.println("Part 2: " + step); - break; - } - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day12.java b/src/test/java/com/macasaet/Day12.java deleted file mode 100644 index af7de96..0000000 --- a/src/test/java/com/macasaet/Day12.java +++ /dev/null @@ -1,182 +0,0 @@ -package com.macasaet; - -import java.util.*; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 12: Passage Pathing --- - */ -public class Day12 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-12.txt"), - false); - } - - /** - * @return a map of the connected caves - */ - protected Map getMap() { - final var map = new HashMap(); - getInput().forEach(line -> { - final var components = line.split("-"); - final var sourceLabel = components[0]; - final var targetLabel = components[1]; - final var source = map.computeIfAbsent(sourceLabel, Node::new); - final var target = map.computeIfAbsent(targetLabel, Node::new); - source.connected.add(target); - target.connected.add(source); - }); - return Collections.unmodifiableMap(map); - } - - public Node getStartingPoint() { - return getMap().get("start"); - } - - /** - * A distinct path through the cave system - */ - public record Path(List nodes, Node specialCave, int specialCaveVisits) { - - public int hashCode() { - int result = 0; - for (final var node : nodes()) { - result = result * 31 + node.hashCode(); - } - return result; - } - - public boolean equals(final Object o) { - if (o == null) { - return false; - } - try { - final var other = (Path) o; - return nodes().equals(other.nodes()); - } catch (final ClassCastException cce) { - return false; - } - } - } - - public static class Node { - private final boolean isStart; - private final boolean isEnd; - private final boolean isSmallCave; - private final String label; - - private final Set connected = new HashSet<>(); - - public Node(final String label) { - this("start".equalsIgnoreCase(label), "end".equalsIgnoreCase(label), - label.toLowerCase(Locale.ROOT).equals(label), label); - } - - protected Node(boolean isStart, boolean isEnd, boolean isSmallCave, final String label) { - this.isStart = isStart; - this.isEnd = isEnd; - this.isSmallCave = isSmallCave; - this.label = label; - } - - public int hashCode() { - int result = 0; - result += result * 31 + label.hashCode(); - return result; - } - - public boolean equals(final Object o) { - if (o == null) { - return false; - } - try { - final Node other = (Node) o; - return label.equals(other.label); - } catch (final ClassCastException cce) { - return false; - } - } - - } - - protected Set getPaths(final Node node, final Path pathSoFar) { - final var result = new HashSet(); - - if (node.isStart && pathSoFar.nodes.size() > 1) { - // "once you leave the start cave, you may not return to it" - return Collections.emptySet(); - } - - final var nodes = new ArrayList<>(pathSoFar.nodes()); - if (node.isEnd) { - // "once you reach the end cave, the path must end immediately" - nodes.add(node); - return Collections.singleton(new Path(Collections.unmodifiableList(nodes), pathSoFar.specialCave(), pathSoFar.specialCaveVisits())); - } - int specialCaveVisits = pathSoFar.specialCaveVisits(); - if (node.isSmallCave) { - if (node.equals(pathSoFar.specialCave())) { - // "a single small cave can be visited at most twice" - if (pathSoFar.specialCaveVisits() < 1) { - specialCaveVisits++; - } else { - return Collections.emptySet(); - } - } else { - if (pathSoFar.nodes().contains(node)) { - // "the remaining small caves can be visited at most once" - return Collections.emptySet(); - } - } - } - nodes.add(node); - for (final var neighbour : node.connected) { - if (neighbour.isSmallCave && pathSoFar.specialCave() == null) { - result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), null, 0))); - result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), neighbour, 0))); - } else { - result.addAll(getPaths(neighbour, new Path(Collections.unmodifiableList(nodes), pathSoFar.specialCave(), specialCaveVisits))); - } - } - return Collections.unmodifiableSet(result); - } - - protected int countPaths(final Node node, final Set visitedSmallCaves) { - int result = 0; - if (node.isEnd) { - return 1; - } - if (visitedSmallCaves.contains(node)) { - // invalid path - return 0; - } - if (node.isSmallCave) { - visitedSmallCaves.add(node); - } - for (final var connected : node.connected) { - final var set = new HashSet<>(visitedSmallCaves); - result += countPaths(connected, set); - } - return result; - } - - @Test - public final void part1() { - final var start = getStartingPoint(); - final int result = countPaths(start, new HashSet<>()); - System.out.println("Part 1: " + result); - } - - @Test - public final void part2() { - final var start = getStartingPoint(); - final var paths = getPaths(start, new Path(Collections.emptyList(), null, 0)); - System.out.println("Part 2: " + paths.size()); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day13.java b/src/test/java/com/macasaet/Day13.java deleted file mode 100644 index b60355f..0000000 --- a/src/test/java/com/macasaet/Day13.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.macasaet; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 13: Transparent Origami --- - */ -public class Day13 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-13.txt"), - false); - } - - /** - * A point on the translucent sheet of paper. Note that x and y correspond to a particular - * {@link Axis}. - */ - public record Point(int x, int y) { - } - - /** - * An axis of the translucent sheet of paper - */ - public enum Axis { - /** - * The axis that increases to the right - */ - X, - - /** - * The axis that increases downward - */ - Y, - } - - /** - * An equation for a line - */ - public record Line(Axis axis, int value) { - public String toString() { - return switch (axis()) { - case X -> "x=" + value; - case Y -> "y=" + value; - }; - } - } - - public record Input(Collection points, List folds, int maxX, int maxY) { - public Sheet getSheet() { - final boolean[][] grid = new boolean[maxY + 1][]; - for (int i = grid.length; --i >= 0; ) { - grid[i] = new boolean[maxX + 1]; - } - for (final var point : points) { - /* The first value, x, increases to the right. The second value, y, increases downward. */ - grid[point.y()][point.x()] = true; - } - return new Sheet(grid); - } - } - - /** - * A sheet of translucent paper - */ - public record Sheet(boolean[][] grid) { - - public int countDots() { - int result = 0; - final var grid = grid(); - for (int i = grid.length; --i >= 0; ) { - for (int j = grid[i].length; --j >= 0; ) { - if (grid[i][j]) { - result++; - } - } - } - return result; - } - - public String toString() { - final var builder = new StringBuilder(); - for (final var row : grid) { - for (final var cell : row) { - builder.append(cell ? '#' : '.'); - } - builder.append('\n'); - } - return builder.toString(); - } - - public Sheet fold(final Line line) { - // note, value is always positive - return switch (line.axis()) { - case X -> { - // fold along the x-axis (vertical line) - final var newGrid = new boolean[grid.length][]; - for (int i = newGrid.length; --i >= 0; ) { - final var newRow = new boolean[line.value() + 1]; - for (int j = newRow.length; --j >= 0; newRow[j] = grid[i][j]) ; - for(int j = grid[i].length - line.value(); --j > 0; ) { - if(grid[i][line.value() + j]) { - newRow[line.value() - j] = true; - } - } - newGrid[i] = newRow; - } - yield new Sheet(newGrid); - } - case Y -> { - // fold along the y-axis (horizontal line) - final var newGrid = new boolean[line.value()][]; - for (int i = newGrid.length; --i >= 0; ) { - final var newRow = new boolean[grid[i].length]; - for (int j = grid[i].length; --j >= 0; newRow[j] = grid[i][j]) ; - newGrid[i] = newRow; - } - for (int i = grid.length - line.value(); --i > 0; ) { - final var oldRow = grid[line.value() + i]; - for (int j = oldRow.length; - --j >= 0; - newGrid[line.value() - i][j] |= oldRow[j]) - ; - } - yield new Sheet(newGrid); - } - }; - } - } - - public Input parseInput() { - int section = 0; - final var points = new HashSet(); - final var folds = new ArrayList(); - int maxX = Integer.MIN_VALUE; - int maxY = Integer.MIN_VALUE; - for (final var line : getInput().collect(Collectors.toList())) { - if (line.isBlank()) { - section++; - continue; - } - if (section == 0) { // points - final var components = line.split(","); - final var x = Integer.parseInt(components[0]); - maxX = Math.max(x, maxX); - final var y = Integer.parseInt(components[1]); - maxY = Math.max(y, maxY); - final var point = new Point(x, y); - points.add(point); - } else { // commands - final var equation = line.replaceFirst("fold along ", ""); - final var components = equation.split("="); - final var axis = Axis.valueOf(components[0].toUpperCase(Locale.ROOT)); - final var value = Integer.parseInt(components[1]); - final var fold = new Line(axis, value); - folds.add(fold); - } - } - return new Input(points, folds, maxX, maxY); - } - - @Test - public final void part1() { - final var input = parseInput(); - final var sheet = input.getSheet(); - final var firstFold = input.folds().get(0); - final var result = sheet.fold(firstFold); - System.out.println("Part 1: " + result.countDots()); - } - - @Test - public final void part2() { - final var input = parseInput(); - var sheet = input.getSheet(); - for (final var fold : input.folds()) { - sheet = sheet.fold(fold); - } - System.out.println("Part 2:\n" + sheet); - } - -} diff --git a/src/test/java/com/macasaet/Day14.java b/src/test/java/com/macasaet/Day14.java deleted file mode 100644 index c44d50b..0000000 --- a/src/test/java/com/macasaet/Day14.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.macasaet; - -import java.math.BigInteger; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 14: Extended Polymerization --- - */ -public class Day14 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-14.txt"), - false); - } - - public record Polymer(Map pairCounts, char firstElement, char lastElement) { - public static Polymer forTemplate(final String templateString) { - final var firstElement = templateString.charAt(0); - final var lastElement = templateString.charAt(templateString.length() - 1); - final var map = new HashMap(); - for (int i = 1; i < templateString.length(); i++) { - map.merge(new ElementPair(templateString.charAt(i - 1), templateString.charAt(i)), - BigInteger.ONE, - BigInteger::add); - } - return new Polymer(Collections.unmodifiableMap(map), firstElement, lastElement); - } - - /** - * Apply the pair insertion process one time. - * - * @param rules pair insertion rules for generating a new polymer - * @return the new polymer that results - */ - public Polymer applyRules(final Map rules) { - final var map = new HashMap(); - for (final var entry : pairCounts().entrySet()) { - final var key = entry.getKey(); - final var count = entry.getValue(); - final var rule = rules.get(key); - final var left = new ElementPair(key.start(), rule.insert()); - final var right = new ElementPair(rule.insert(), key.end()); - - map.compute(left, (_key, oldCount) -> oldCount == null ? count : oldCount.add(count)); - map.compute(right, (_key, oldCount) -> oldCount == null ? count : oldCount.add(count)); - } - return new Polymer(Collections.unmodifiableMap(map), firstElement(), lastElement()); - } - - /** - * Determine how many times each element appears in the polymer - * - * @return the number of times each element appears in the polymer - */ - public SortedMap> histogram() { - final var map = new HashMap(); - for (final var entry : pairCounts().entrySet()) { - final var pair = entry.getKey(); - final var count = entry.getValue(); - map.compute(pair.start(), - (_key, oldValue) -> oldValue == null ? count : oldValue.add(count)); - map.compute(pair.end(), - (_key, oldValue) -> oldValue == null ? count : oldValue.add(count)); - } - for (final var entry : map.entrySet()) { - final var element = entry.getKey(); - final var count = entry.getValue(); - if (element.equals(firstElement()) || element.equals(lastElement())) { - entry.setValue(count.divide(BigInteger.TWO).add(BigInteger.ONE)); - } else { - entry.setValue(count.divide(BigInteger.TWO)); - } - } - final var result = new TreeMap>(); - for (final var entry : map.entrySet()) { - final var target = result.computeIfAbsent(entry.getValue(), _key -> new HashSet<>()); - target.add(entry.getKey()); - } - return Collections.unmodifiableSortedMap(result); - } - } - - /** - * A pair of elements that appear adjacent to each other. This may be used in the context of a pair insertion rule - * definition or a polymer. - * - * @see Polymer - * @see PairInsertionRule - */ - protected record ElementPair(char start, char end) { - } - - /** - * A single instruction to aid in finding the optimal polymer formula - */ - public record PairInsertionRule(char start, char end, char insert) { - - public static PairInsertionRule parse(final String string) { - final var components = string.split(" -> "); - final var match = components[0].toCharArray(); - return new PairInsertionRule(match[0], match[1], components[1].toCharArray()[0]); - } - - } - - protected record Input(Polymer polymerTemplate, List rules) { - } - - protected Input parseInput() { - final var list = getInput().collect(Collectors.toList()); - int mode = 0; - Polymer polymer = null; - final var rules = new ArrayList(); - for (final var line : list) { - if (line.isBlank()) { - mode++; - continue; - } - if (mode == 0) { - polymer = Polymer.forTemplate(line); - } else { - rules.add(PairInsertionRule.parse(line)); - } - } - return new Input(polymer, rules); - } - - @Test - public final void part1() { - final var input = parseInput(); - var polymer = input.polymerTemplate(); - final var rules = input.rules(); - final var ruleMap = new HashMap(); - for (final var rule : rules) { - ruleMap.put(new ElementPair(rule.start(), rule.end()), rule); - } - for (int _i = 0; _i < 10; _i++) { - polymer = polymer.applyRules(ruleMap); - } - final var histogram = polymer.histogram(); - System.out.println("Part 1: " + histogram.lastKey().subtract(histogram.firstKey())); - } - - @Test - public final void part2() { - final var input = parseInput(); - var polymer = input.polymerTemplate(); - final var rules = input.rules(); - final var ruleMap = new HashMap(); - for (final var rule : rules) { - ruleMap.put(new ElementPair(rule.start(), rule.end()), rule); - } - for (int _i = 0; _i < 40; _i++) { - polymer = polymer.applyRules(ruleMap); - } - final var histogram = polymer.histogram(); - System.out.println("Part 2: " + histogram.lastKey().subtract(histogram.firstKey())); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day15.java b/src/test/java/com/macasaet/Day15.java deleted file mode 100644 index e93e89f..0000000 --- a/src/test/java/com/macasaet/Day15.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.macasaet; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 15: Chiton --- - */ -public class Day15 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-15.txt"), - false); - } - - protected int[][] getGrid() { - final var list = getInput().collect(Collectors.toList()); - final int[][] grid = new int[list.size()][]; - for (int i = 0; i < grid.length; i++) { - final var chars = list.get(i).toCharArray(); - final var row = new int[chars.length]; - for (int j = chars.length; --j >= 0; row[j] = chars[j] - '0') ; - grid[i] = row; - } - return grid; - } - - public record Point(int x, int y) { - - public int risk(int[][] risks) { - return risks[x][y]; - } - - } - - public record Cavern(int[][] grid) { - - public Set predecessors(final Point source) { - final var result = new HashSet(); - if (source.x() > 0) { - result.add(new Point(source.x() - 1, source.y())); - } - if (source.y() > 0) { - result.add(new Point(source.x(), source.y() - 1)); - } - return Collections.unmodifiableSet(result); - } - - public Set successors(final Point source) { - final var result = new HashSet(); - if (source.x() < grid().length - 1) { - result.add(new Point(source.x() + 1, source.y())); - } - if (source.y() < grid()[source.x()].length - 1) { - result.add(new Point(source.x(), source.y() + 1)); - } - return Collections.unmodifiableSet(result); - } - - public Cavern explode() { - final int[][] largeGrid = new int[grid.length * 5][]; - for (int i = largeGrid.length; --i >= 0; ) { - largeGrid[i] = new int[grid.length * 5]; - } - for (int i = grid.length; --i >= 0; ) { - for (int j = grid.length; --j >= 0; ) { - largeGrid[i][j] = grid[i][j]; - } - } - for (int tileRow = 0; tileRow < 5; tileRow++) { - for (int tileColumn = 0; tileColumn < 5; tileColumn++) { - if (tileRow > 0) { - for (int i = grid.length; --i >= 0; ) { - for (int j = grid.length; --j >= 0; ) { - // copy from row above - int value = largeGrid[(tileRow - 1) * grid.length + i][tileColumn * grid.length + j] + 1; - if (value == 10) { - value = 1; - } - largeGrid[tileRow * grid.length + i][tileColumn * grid.length + j] = value; - } - } - } else if (tileColumn > 0) { - for (int i = grid.length; --i >= 0; ) { - for (int j = grid.length; --j >= 0; ) { - // copy from column to the left - int value = largeGrid[tileRow * grid.length + i][(tileColumn - 1) * grid.length + j] + 1; - if (value == 10) { - value = 1; - } - largeGrid[tileRow * grid.length + i][tileColumn * grid.length + j] = value; - } - } - } - } - } - return new Cavern(largeGrid); - } - - public long[][] calculateCumulativeRisk() { - final var cumulative = new long[grid().length][]; - for (int i = cumulative.length; --i >= 0; cumulative[i] = new long[grid()[i].length]) ; - final var visited = new HashSet(); - final var queue = new LinkedList(); - final var destination = new Point(grid().length - 1, grid()[grid().length - 1].length - 1); - queue.add(destination); - visited.add(destination); - - while (!queue.isEmpty()) { - final var node = queue.remove(); - final var successors = successors(node); - if (successors.isEmpty()) { - // destination - cumulative[node.x][node.y] = node.risk(grid()); - } else { - var minSuccessorRisk = Long.MAX_VALUE; - for (final var successor : successors) { - if (!visited.contains(successor)) { - throw new IllegalStateException("Successor has not been visited"); - } - minSuccessorRisk = Math.min(minSuccessorRisk, cumulative[successor.x][successor.y]); - } - cumulative[node.x][node.y] = node.risk(grid()) + minSuccessorRisk; - } - - for (final var predecessor : predecessors(node)) { - if (!visited.contains(predecessor)) { - queue.add(predecessor); - visited.add(predecessor); - } - } - } - return cumulative; - } - - /** - * @return the risk level associated with the path through the cavern that avoids the most chitons - */ - public int lowestRiskThroughTheCavern() { - // the lowest known risk from origin to a given node - final var lowestRiskToNode = new HashMap(); - // the estimated risk from origin to destination if it goes through a given node - final var estimatedRiskThroughNode = new HashMap(); - final var openSet = new PriorityQueue(Comparator.comparing(estimatedRiskThroughNode::get)); - - for (int i = grid().length; --i >= 0; ) { - final var row = grid()[i]; - for (int j = row.length; --j >= 0; ) { - final var point = new Point(i, j); - if (i == 0 && j == 0) { - lowestRiskToNode.put(point, 0); - estimatedRiskThroughNode.put(point, manhattanDistance(point)); - openSet.add(point); - } else { - lowestRiskToNode.put(point, Integer.MAX_VALUE); - estimatedRiskThroughNode.put(point, Integer.MAX_VALUE); - } - } - } - - while(!openSet.isEmpty()) { - final var current = openSet.poll(); - if(current.x() == grid().length - 1 && current.y() == grid()[grid().length - 1].length - 1) { - return lowestRiskToNode.get(current); - } - final var lowestRiskToCurrent = lowestRiskToNode.get(current); - for(final var neighbour : neighbours(current)) { - final var tentativeRisk = lowestRiskToCurrent + neighbour.risk(grid()); - if(tentativeRisk < lowestRiskToNode.get(neighbour)) { - lowestRiskToNode.put(neighbour, tentativeRisk); - estimatedRiskThroughNode.put(neighbour, tentativeRisk + manhattanDistance(neighbour)); - if(!openSet.contains(neighbour)) { - openSet.add(neighbour); - } - } - } - } - throw new IllegalStateException("No path out of the cavern!"); - } - - /** - * @param point - * @return - */ - protected int manhattanDistance(Point point) { - return Math.abs(point.x() - (grid().length - 1)) - + Math.abs(point.y() - (grid()[grid().length - 1].length - 1)); - } - - public Set neighbours(final Point point) { - final var result = new HashSet(); - if (point.x() > 0) { - result.add(new Point(point.x() - 1, point.y())); - } - if (point.x() < grid().length - 1) { - result.add(new Point(point.x() + 1, point.y())); - } - if (point.y() > 0) { - result.add(new Point(point.x(), point.y() - 1)); - } - if (point.y() < grid()[point.x()].length - 1) { - result.add(new Point(point.x(), point.y() + 1)); - } - return Collections.unmodifiableSet(result); - } - - } - - @Test - public final void part1() { - final var grid = getGrid(); - final var cavern = new Cavern(grid); - System.out.println("Part 1: " + cavern.lowestRiskThroughTheCavern()); - } - - @Test - public final void part2() { - final var grid = getGrid(); - final var cavern = new Cavern(grid).explode(); - System.out.println("Part 2: " + cavern.lowestRiskThroughTheCavern()); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day16.java b/src/test/java/com/macasaet/Day16.java deleted file mode 100644 index b3eb4b0..0000000 --- a/src/test/java/com/macasaet/Day16.java +++ /dev/null @@ -1,352 +0,0 @@ -package com.macasaet; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * --- Day 16: Packet Decoder --- - */ -public class Day16 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-16.txt"), - false); - } - - public interface Packet { - long version(); - - void accept(PacketVisitor packetVisitor); - - long evaluate(); - } - - public record Literal(long version, long value) implements Packet { - - public void accept(PacketVisitor packetVisitor) { - packetVisitor.visit(this); - } - - public long evaluate() { - return value(); - } - } - - public enum OperatorType { - SUM { - public long evaluate(List operands) { - return operands.stream().mapToLong(Packet::evaluate).sum(); - } - }, - PRODUCT { - public long evaluate(List operands) { - return operands.stream().mapToLong(Packet::evaluate).reduce(1, (x, y) -> x * y); - } - }, - MINIMUM { - public long evaluate(List operands) { - return operands.stream().mapToLong(Packet::evaluate).min().orElseThrow(); - } - }, - MAXIMUM { - public long evaluate(List operands) { - return operands.stream().mapToLong(Packet::evaluate).max().orElseThrow(); - } - }, - GREATER_THAN { - public long evaluate(List operands) { - if (operands.size() != 2) { - throw new IllegalArgumentException("Invalid operand list for \"greater than\" operator: " + operands); - } - final var x = operands.get(0).evaluate(); - final var y = operands.get(1).evaluate(); - return x > y ? 1 : 0; - } - }, - LESS_THAN { - public long evaluate(List operands) { - if (operands.size() != 2) { - throw new IllegalStateException("Invalid operand list for \"less than\" operator: " + operands); - } - final var x = operands.get(0).evaluate(); - final var y = operands.get(1).evaluate(); - return x < y ? 1 : 0; - } - }, - EQUAL_TO { - public long evaluate(List operands) { - if (operands.size() != 2) { - throw new IllegalStateException("Invalid operand list for \"equal to\" operator: " + operands); - } - final var x = operands.get(0).evaluate(); - final var y = operands.get(1).evaluate(); - return x == y ? 1 : 0; - } - }; - - public abstract long evaluate(List operands); - - public static OperatorType forId(final int typeId) { - return switch (typeId) { - case 0 -> SUM; - case 1 -> PRODUCT; - case 2 -> MINIMUM; - case 3 -> MAXIMUM; - case 5 -> GREATER_THAN; - case 6 -> LESS_THAN; - case 7 -> EQUAL_TO; - default -> throw new IllegalArgumentException("Invalid operator type ID: " + typeId); - }; - } - } - - public record Operator(long version, OperatorType operatorType, List operands) implements Packet { - - public void accept(PacketVisitor packetVisitor) { - packetVisitor.enter(this); - for (final var subPacket : operands()) { - subPacket.accept(packetVisitor); - } - packetVisitor.exit(this); - } - - public long evaluate() { - return operatorType().evaluate(operands()); - } - } - - public interface PacketVisitor { - void visit(Literal literal); - - void enter(Operator operator); - - void exit(Operator operator); - } - - public static class PacketBuilder { - - private long version; - private long typeId; - private OptionalLong literalValue = OptionalLong.empty(); - private final List subPackets = new ArrayList<>(); - - public Packet readHex(final String hexString) { - final var hexDigits = hexString.toCharArray(); - final var bits = hexToBits(hexDigits); - read(bits, 0); - return toPacket(); - } - - public int read(final List bits, int transmissionCursor) { - final var versionBits = bits.subList(transmissionCursor, transmissionCursor + 3); - transmissionCursor += 3; - this.version = toLong(versionBits); - - final var typeBits = bits.subList(transmissionCursor, transmissionCursor + 3); - transmissionCursor += 3; - this.typeId = toLong(typeBits); - - // TODO consider adding methods to parse each type specifically - if (this.typeId == 4) { - boolean finalGroup = false; - final var literalBits = new ArrayList(); - while (!finalGroup) { - final var groupBits = bits.subList(transmissionCursor, transmissionCursor + 5); - transmissionCursor += 5; - finalGroup = groupBits.get(0) == 0; - literalBits.addAll(groupBits.subList(1, 5)); - } - if (literalBits.size() > 63) { - throw new IllegalArgumentException("Literal is too large for an long: " + literalBits.size()); - } - literalValue = OptionalLong.of(toLong(literalBits)); - return transmissionCursor; - } else { - final var lengthTypeIdBits = bits.subList(transmissionCursor, transmissionCursor + 1); - transmissionCursor += 1; - final var lengthTypeId = toLong(lengthTypeIdBits); - if (lengthTypeId == 0) { - final var lengthOfSubPacketsBits = bits.subList(transmissionCursor, transmissionCursor + 15); - transmissionCursor += 15; - final var lengthOfSubPackets = toLong(lengthOfSubPacketsBits); - int bitsRead = 0; - while (bitsRead < lengthOfSubPackets) { - final var subPacketBuilder = new PacketBuilder(); - final var newCursor = subPacketBuilder.read(bits, transmissionCursor); - final var subPacketSize = newCursor - transmissionCursor; // size of sub-packet in bits - transmissionCursor = newCursor; - - subPackets.add(subPacketBuilder.toPacket()); - bitsRead += subPacketSize; - } - return transmissionCursor; - } else if (lengthTypeId == 1) { - final var numSubPacketsBits = bits.subList(transmissionCursor, transmissionCursor + 11); - transmissionCursor += 11; - final var numSubPackets = toLong(numSubPacketsBits); - for (int packetsRead = 0; packetsRead < numSubPackets; packetsRead++) { - final var subPacketBuilder = new PacketBuilder(); - transmissionCursor = subPacketBuilder.read(bits, transmissionCursor); - subPackets.add(subPacketBuilder.toPacket()); - } - return transmissionCursor; - } else { - throw new IllegalArgumentException("Invalid length type ID: " + lengthTypeId); - } - } - } - - public Packet toPacket() { - if (typeId == 4) { - return new Literal(version, literalValue.orElseThrow()); - } else { - return new Operator(version, OperatorType.forId((int) typeId), subPackets); - } - } - - protected long toLong(final List bits) { - long result = 0; - for (int i = 0; i < bits.size(); i++) { - final var bit = bits.get(i); - if (bit == 1) { - final long shiftDistance = bits.size() - i - 1; - result |= 1L << shiftDistance; - } else if (bit != 0) { - throw new IllegalArgumentException("Invalid bit representation of an integer: " + bits); - } - } - return result; - } - - protected List hexToBits(final char[] hexDigits) { - final var result = new ArrayList(hexDigits.length * 4); - for (final var digit : hexDigits) { - final var bits = switch (digit) { - case '0' -> Arrays.asList((byte) 0, (byte) 0, (byte) 0, (byte) 0); - case '1' -> Arrays.asList((byte) 0, (byte) 0, (byte) 0, (byte) 1); - case '2' -> Arrays.asList((byte) 0, (byte) 0, (byte) 1, (byte) 0); - case '3' -> Arrays.asList((byte) 0, (byte) 0, (byte) 1, (byte) 1); - case '4' -> Arrays.asList((byte) 0, (byte) 1, (byte) 0, (byte) 0); - case '5' -> Arrays.asList((byte) 0, (byte) 1, (byte) 0, (byte) 1); - case '6' -> Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 0); - case '7' -> Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 1); - case '8' -> Arrays.asList((byte) 1, (byte) 0, (byte) 0, (byte) 0); - case '9' -> Arrays.asList((byte) 1, (byte) 0, (byte) 0, (byte) 1); - case 'A', 'a' -> Arrays.asList((byte) 1, (byte) 0, (byte) 1, (byte) 0); - case 'B', 'b' -> Arrays.asList((byte) 1, (byte) 0, (byte) 1, (byte) 1); - case 'C', 'c' -> Arrays.asList((byte) 1, (byte) 1, (byte) 0, (byte) 0); - case 'D', 'd' -> Arrays.asList((byte) 1, (byte) 1, (byte) 0, (byte) 1); - case 'E', 'e' -> Arrays.asList((byte) 1, (byte) 1, (byte) 1, (byte) 0); - case 'F', 'f' -> Arrays.asList((byte) 1, (byte) 1, (byte) 1, (byte) 1); - default -> throw new IllegalStateException("Unexpected value: " + digit); - }; - result.addAll(bits); - } - return Collections.unmodifiableList(result); - } - } - - @Test - public final void testParseLiteral() { - // given - final var input = "D2FE28"; - final var builder = new PacketBuilder(); - - // when - final var result = builder.readHex(input); - - // then - assertTrue(result instanceof Literal); - final var literal = (Literal) result; - assertEquals(2021, literal.value); - } - - @Test - public final void testOperatorWithTwoSubPackets() { - // given - final var input = "38006F45291200"; - final var builder = new PacketBuilder(); - - // when - final var result = builder.readHex(input); - - // then - assertTrue(result instanceof Operator); - final var operator = (Operator) result; - assertEquals(1, operator.version()); - assertEquals(OperatorType.LESS_THAN, operator.operatorType()); - assertEquals(2, operator.operands().size()); - final var a = (Literal) operator.operands().get(0); - assertEquals(10, a.value()); - final var b = (Literal) operator.operands().get(1); - assertEquals(20, b.value()); - } - - @Test - public final void part1() { - final var line = getInput().collect(Collectors.toList()).get(0); - final var builder = new PacketBuilder(); - final var packet = builder.readHex(line); - class VersionSummer implements PacketVisitor { - - int sum = 0; - - public void visit(Literal literal) { - sum += literal.version(); - } - - public void enter(Operator operator) { - } - - public void exit(Operator operator) { - sum += operator.version(); - } - } - final var summer = new VersionSummer(); - packet.accept(summer); - - System.out.println("Part 1: " + summer.sum); - } - - @Test - public final void part2() { - final var line = getInput().collect(Collectors.toList()).get(0); - final var builder = new PacketBuilder(); - final var packet = builder.readHex(line); - System.out.println("Part 2: " + packet.evaluate()); - } - - @Nested - public class PacketBuilderTest { - @Test - public void testToInt() { - // given - final var builder = new PacketBuilder(); - // when - // then - assertEquals(2021, builder.toLong(Arrays.asList((byte) 0, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 1, (byte) 0, (byte) 0, (byte) 1, (byte) 0, (byte) 1))); - } - - @Test - public final void testMaths() { - assertEquals(3, new PacketBuilder().readHex("C200B40A82").evaluate()); - assertEquals(54, new PacketBuilder().readHex("04005AC33890").evaluate()); - assertEquals(7, new PacketBuilder().readHex("880086C3E88112").evaluate()); - assertEquals(9, new PacketBuilder().readHex("CE00C43D881120").evaluate()); - assertEquals(1, new PacketBuilder().readHex("D8005AC2A8F0").evaluate()); - assertEquals(0, new PacketBuilder().readHex("F600BC2D8F").evaluate()); - assertEquals(0, new PacketBuilder().readHex("9C005AC2F8F0").evaluate()); - assertEquals(1, new PacketBuilder().readHex("9C0141080250320F1802104A08").evaluate()); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day17.java b/src/test/java/com/macasaet/Day17.java deleted file mode 100644 index 66dc6f1..0000000 --- a/src/test/java/com/macasaet/Day17.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.macasaet; - -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 17: Trick Shot --- - */ -public class Day17 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-17.txt"), - false); - } - - /** - * The target area in a large ocean trench - */ - public record Target(int minX, int maxX, int minY, int maxY) { - } - - /** - * A probe at a given point in time - */ - public record Probe(int xVelocity, int yVelocity, int x, int y) { - - /** - * Launch a probe from the origin - * - * @param xVelocity the starting horizontal velocity - * @param yVelocity the starting vertical velocity - * @return the initial state of the probe at the origin - */ - public static Probe launch(final int xVelocity, final int yVelocity) { - return new Probe(xVelocity, yVelocity, 0, 0); - } - - public Optional step() { - if(x > 0 && x + xVelocity < 0) { - return Optional.empty(); - } - if(y < 0 && y + yVelocity > 0) { - return Optional.empty(); - } - final int newX = x + xVelocity; - final int newY = y + yVelocity; - final int newXVelocity = xVelocity > 0 - ? xVelocity - 1 - : xVelocity < 0 - ? xVelocity + 1 - : xVelocity; - return Optional.of(new Probe(newXVelocity, - yVelocity - 1, - newX, - newY)); - } - - public Optional peak(final Target target) { - var peak = Integer.MIN_VALUE; - var p = Optional.of(this); - while (p.isPresent()) { - final var probe = p.get(); - peak = Math.max(peak, probe.y()); - if (probe.x() < target.minX() && probe.y() < target.minY()) { - // short - return Optional.empty(); - } else if (probe.x() > target.maxX()) { - // long - return Optional.empty(); - } else if (probe.x() >= target.minX() && probe.x() <= target.maxX() - && probe.y() >= target.minY() && probe.y() <= target.maxY()) { - return Optional.of(peak); - } - p = probe.step(); - } - return Optional.empty(); - } - - } - - @Test - public final void part1() { - final var line = getInput().collect(Collectors.toList()).get(0); - final var bounds = line.replaceFirst("target area: ", "").split(", "); - final var xBounds = bounds[0].replaceFirst("x=", "").split("\\.\\."); - final var yBounds = bounds[1].replaceFirst("y=", "").split("\\.\\."); - final int minX = Integer.parseInt(xBounds[0]); - final int maxX = Integer.parseInt(xBounds[1]); - final int minY = Integer.parseInt(yBounds[0]); - final int maxY = Integer.parseInt(yBounds[1]); - final var target = new Target(minX, maxX, minY, maxY); - - final var max = IntStream.range(0, 50) - .parallel() - .mapToObj(x -> IntStream.range(-50, 50) - .parallel() - .mapToObj(y -> Probe.launch(x, y)) - ).flatMap(probes -> probes) - .flatMapToInt(probe -> probe.peak(target) - .stream() - .mapToInt(peak -> peak)) - .max(); - - - System.out.println("Part 1: " + max.getAsInt()); - } - - @Test - public final void part2() { - final var line = getInput().collect(Collectors.toList()).get(0); - final var bounds = line.replaceFirst("target area: ", "").split(", "); - final var xBounds = bounds[0].replaceFirst("x=", "").split("\\.\\."); - final var yBounds = bounds[1].replaceFirst("y=", "").split("\\.\\."); - final int minX = Integer.parseInt(xBounds[0]); - final int maxX = Integer.parseInt(xBounds[1]); - final int minY = Integer.parseInt(yBounds[0]); - final int maxY = Integer.parseInt(yBounds[1]); - final var target = new Target(minX, maxX, minY, maxY); - int count = 0; - for (int x = 1; x <= 400; x++) { - for (int y = -400; y <= 400; y++) { - final var probe = Probe.launch(x, y); - if (probe.peak(target).isPresent()) { - count++; - } - } - } - - System.out.println("Part 2: " + count); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day18.java b/src/test/java/com/macasaet/Day18.java deleted file mode 100644 index afd2133..0000000 --- a/src/test/java/com/macasaet/Day18.java +++ /dev/null @@ -1,432 +0,0 @@ -package com.macasaet; - -import static com.macasaet.Day18.SnailfishNumber.parse; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * --- Day 18: Snailfish --- - */ -public class Day18 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-18.txt"), - false); - } - - /** - * An element of a {@link SnailfishNumber} - */ - public interface Token { - } - - /** - * A symbol in a {@link SnailfishNumber} - */ - public enum Symbol implements Token { - START_PAIR, - END_PAIR, - SEPARATOR, - - } - - /** - * An integer in a {@link SnailfishNumber} - */ - public record Number(int value) implements Token { - } - - /** - * "Snailfish numbers aren't like regular numbers. Instead, every snailfish number is a pair - an ordered list of - * two elements. Each element of the pair can be either a regular number or another pair." - */ - public record SnailfishNumber(List expression) { - - public SnailfishNumber(final String string) { - this(parse(string)); - } - - /** - * "The magnitude of a pair is 3 times the magnitude of its left element plus 2 times the magnitude of its right - * element. The magnitude of a regular number is just that number." - * - * @return the snailfish number distilled into a single value - */ - public int magnitude() { - var stack = new LinkedList(); - for (final var token : expression) { - if (token.equals(Symbol.START_PAIR)) { - } else if (token instanceof final Number number) { - stack.push(number.value()); - } else if (token.equals(Symbol.END_PAIR)) { - final var rightValue = stack.pop(); - final var leftValue = stack.pop(); - stack.push(leftValue * 3 + rightValue * 2); - } - } - if (stack.size() != 1) { - throw new IllegalStateException("Invalid stack: " + stack); - } - return stack.get(0); - } - - /** - * Repeatedly explode or split this snailfish number until those operations can no longer be performed. - * - * @return a representation of this snailfish number that cannot be reduced any further - */ - public SnailfishNumber reduce() { - var newExpression = expression; - while (true) { - var explosionIndex = getExplosionIndex(newExpression); - var splitIndex = getSplitIndex(newExpression); - if (explosionIndex > 0) { - newExpression = explode(newExpression, explosionIndex); - } else if (splitIndex > 0) { - newExpression = split(newExpression, splitIndex); - } else { - break; - } - } - return new SnailfishNumber(newExpression); - } - - /** - * Add a snailfish number. Note, this operation is *not commutative*: `x.add(y)` is not the same as `y.add(x)`. - * Also note that the process of addition may yield a snailfish number that needs to be reduced. - * - * @param addend the number to add to this snailfish number - * @return the sum of the snailfish numbers (may need to be reduced - * @see SnailfishNumber#reduce() - */ - public SnailfishNumber add(final SnailfishNumber addend) { - final var tokens = new ArrayList(); - tokens.add(Symbol.START_PAIR); - tokens.addAll(expression()); - tokens.add(Symbol.SEPARATOR); - tokens.addAll(addend.expression()); - tokens.add(Symbol.END_PAIR); - return new SnailfishNumber(Collections.unmodifiableList(tokens)); - } - - static List parse(final String expression) { - final var result = new ArrayList(); - for (int i = 0; i < expression.length(); i++) { - final var c = expression.charAt(i); - if (c == '[') { - result.add(Symbol.START_PAIR); - } else if (c == ']') { - result.add(Symbol.END_PAIR); - } else if (c == ',') { - result.add(Symbol.SEPARATOR); - } else if (c >= '0' && c <= '9') { - int endExclusive = i + 1; - while (endExclusive < expression.length()) { - final var d = expression.charAt(endExclusive); - if (d < '0' || d > '9') { - break; - } - endExclusive++; - } - final int value = Integer.parseInt(expression.substring(i, endExclusive)); - result.add(new Number(value)); - i = endExclusive - 1; - } - } - return Collections.unmodifiableList(result); - } - - /** - * Split a regular number. "To split a regular number, replace it with a pair; the left element of the pair - * should be the regular number divided by two and rounded down, while the right element of the pair should be - * the regular number divided by two and rounded up." - * - * @param expression a raw representation of a snailfish number - * @param index the index of a regular number to split. The caller is responsible for ensuring that this number - * can be split and that it is the most appropriate action to take. - * @return the reduced snailfish number in raw tokens - */ - List split(final List expression, final int index) { - final var result = new ArrayList(); - if (index > 0) { - result.addAll(expression.subList(0, index)); - } - final var regularNumber = (Number) expression.get(index); - - final var left = Math.floorDiv(regularNumber.value(), 2); - final var right = (int) Math.ceil(regularNumber.value() / 2.0d); - - result.add(Symbol.START_PAIR); - result.add(new Number(left)); - result.add(Symbol.SEPARATOR); - result.add(new Number(right)); - result.add(Symbol.END_PAIR); - if (index + 1 < expression.size()) { - result.addAll(expression.subList(index + 1, expression.size())); - } - return Collections.unmodifiableList(result); - } - - /** - * Determine whether any of the regular numbers can be split and if so, the highest-priority number to split. - * - * @param expression a raw representation of a snailfish number - * @return the index of the best regular number to split or -1 if none can be split - */ - int getSplitIndex(final List expression) { - for (int i = 0; i < expression.size(); i++) { - final var token = expression.get(i); - if (token instanceof final Number number) { - if (number.value() >= 10) { - return i; - - } - } - } - return -1; - } - - /** - * Explode the pair starting at `index`. "To explode a pair, the pair's left value is added to the first regular - * number to the left of the exploding pair (if any), and the pair's right value is added to the first regular - * number to the right of the exploding pair (if any). Exploding pairs will always consist of two regular - * numbers. Then, the entire exploding pair is replaced with the regular number 0." - * - * @param expression a raw representation of a snailfish number - * @param index the index of the opening brace of the pair to explode. The caller must ensure that an explosion - * operation is valid at the index and that the index represents the most appropriate pair to - * explode. - * @return the reduced expression in raw format - */ - List explode(final List expression, final int index) { - final var result = new ArrayList<>(expression); - final int leftNumberIndex = index + 1; - final int rightNumberIndex = index + 3; - final int left = ((Number) expression.get(leftNumberIndex)).value(); - final int right = ((Number) expression.get(rightNumberIndex)).value(); - int leftIndex = -1; - int rightIndex = -1; - - for (int i = index; --i >= 0; ) { - final var c = expression.get(i); - if (c instanceof Number) { - leftIndex = i; - break; - } - } - for (int i = rightNumberIndex + 1; i < expression.size(); i++) { - final var c = expression.get(i); - if (c instanceof Number) { - rightIndex = i; - break; - } - } - if (leftIndex < 0 && rightIndex < 0) { - throw new IllegalArgumentException("Cannot be exploded: " + expression); - } - // "the pair's left value is added to the first regular number to the left of the exploding pair (if any)" - if (leftIndex > 0) { - final int leftOperand = ((Number) expression.get(leftIndex)).value(); - final int replacement = leftOperand + left; - result.set(leftIndex, new Number(replacement)); - } - // "the pair's right value is added to the first regular number to the right of the exploding pair (if any)" - if (rightIndex > 0) { - final int rightOperand = ((Number) expression.get(rightIndex)).value(); - final int replacement = rightOperand + right; - result.set(rightIndex, new Number(replacement)); - } - // "Exploding pairs will always consist of two regular numbers. Then, the entire exploding pair is replaced - // with the regular number 0." - result.set(index, new Number(0)); - result.remove(index + 1); - result.remove(index + 1); - result.remove(index + 1); - result.remove(index + 1); - return Collections.unmodifiableList(result); - } - - /** - * @param expression a raw representation of a snailfish number - * @return the index of the most appropriate pair to explode (opening brace) or -1 if no explosion is appropriate - */ - int getExplosionIndex(final List expression) { - int depth = -1; - int maxDepth = Integer.MIN_VALUE; - int result = -1; - for (int i = 0; i < expression.size(); i++) { - final var token = expression.get(i); - if (token == Symbol.START_PAIR) { - depth++; - } else if (token == Symbol.END_PAIR) { - depth--; - } - if (depth > maxDepth) { - maxDepth = depth; - result = i; - } - } - return result > 3 ? result : -1; - } - - } - - @Nested - public class SnailfishNumberTest { - - @Test - public final void testAdd() { - assertEquals(new SnailfishNumber("[[[[0,7],4],[[7,8],[6,0]]],[8,1]]"), - new SnailfishNumber("[[[[4,3],4],4],[7,[[8,4],9]]]") - .add(new SnailfishNumber("[1,1]")) - .reduce()); - // either this example is broken or my bug is not triggered in the real puzzle input T_T -// assertEquals(new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]"), -// new SnailfishNumber("[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]]") -// .add(new SnailfishNumber("[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]]")) -// .reduce()); - } - - @Test - public final void testAddList() { - // given - final var lines = """ - [[[0,[4,5]],[0,0]],[[[4,5],[2,6]],[9,5]]] - [7,[[[3,7],[4,3]],[[6,3],[8,8]]]] - [[2,[[0,8],[3,4]]],[[[6,7],1],[7,[1,6]]]] - [[[[2,4],7],[6,[0,5]]],[[[6,8],[2,8]],[[2,1],[4,5]]]] - [7,[5,[[3,8],[1,4]]]] - [[2,[2,2]],[8,[8,1]]] - [2,9] - [1,[[[9,3],9],[[9,0],[0,7]]]] - [[[5,[7,4]],7],1] - [[[[4,2],2],6],[8,7]]"""; - final var list = Arrays.stream(lines.split("\n")) - .map(SnailfishNumber::new) - .collect(Collectors.toList()); - - // when - var sum = list.get(0); - for (final var addend : list.subList(1, list.size())) { - sum = sum.add(addend).reduce(); - } - - // then - assertEquals(new SnailfishNumber("[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]"), - sum); - } - - @Test - public final void testSplit() { - final var instance = new SnailfishNumber(Collections.emptyList()); - - assertEquals(parse("[5, 5]"), - instance.split(parse("10"), 0)); - assertEquals(parse("[5, 6]"), - instance.split(parse("11"), 0)); - assertEquals(parse("[6, 6]"), - instance.split(parse("12"), 0)); - } - - @Test - public final void testExplosionIndex() { - final var instance = new SnailfishNumber(Collections.emptyList()); - assertEquals(4, - instance.getExplosionIndex(parse("[[[[[9,8],1],2],3],4]"))); - assertEquals(12, - instance.getExplosionIndex(parse("[7,[6,[5,[4,[3,2]]]]]"))); - assertEquals(10, - instance.getExplosionIndex(parse("[[6,[5,[4,[3,2]]]],1]"))); - assertEquals(10, - instance.getExplosionIndex(parse("[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]"))); - assertEquals(24, - instance.getExplosionIndex(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"))); - } - - @Test - public final void testExplode() { - final var instance = new SnailfishNumber(Collections.emptyList()); - assertEquals(parse("[[[[0,9],2],3],4]"), - instance - .explode(parse("[[[[[9,8],1],2],3],4]"), 4)); - assertEquals(parse("[7,[6,[5,[7,0]]]]"), - instance - .explode(parse("[7,[6,[5,[4,[3,2]]]]]"), 12)); - assertEquals(parse("[[6,[5,[7,0]]],3]"), - instance - .explode(parse("[[6,[5,[4,[3,2]]]],1]"), 10)); - assertEquals(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"), - instance - .explode(parse("[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]"), 10)); - assertEquals(parse("[[3,[2,[8,0]]],[9,[5,[7,0]]]]"), - instance - .explode(parse("[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]"), 24)); - } - - @Test - public final void testMagnitude() { - assertEquals(29, new SnailfishNumber("[9,1]").magnitude()); - assertEquals(21, new SnailfishNumber("[1,9]").magnitude()); - assertEquals(143, new SnailfishNumber("[[1,2],[[3,4],5]]").magnitude()); - assertEquals(1384, new SnailfishNumber("[[[[0,7],4],[[7,8],[6,0]]],[8,1]]").magnitude()); - assertEquals(445, new SnailfishNumber("[[[[1,1],[2,2]],[3,3]],[4,4]]").magnitude()); - assertEquals(791, new SnailfishNumber("[[[[3,0],[5,3]],[4,4]],[5,5]]").magnitude()); - assertEquals(1137, new SnailfishNumber("[[[[5,0],[7,4]],[5,5]],[6,6]]").magnitude()); - assertEquals(3488, new SnailfishNumber("[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]").magnitude()); - assertEquals(3993, new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]").magnitude()); - } - - @Test - public final void verifyStableState() { - // given - final var original = new SnailfishNumber("[[[[7,8],[6,6]],[[6,0],[7,7]]],[[[7,8],[8,8]],[[7,9],[0,6]]]]"); - - // when - final var reduced = original.reduce(); - - // then - assertEquals(original, reduced); - } - } - - - @Test - public final void part1() { - final var list = - getInput().map(SnailfishNumber::new).collect(Collectors.toList()); - var sum = list.get(0); - for (final var addend : list.subList(1, list.size())) { - sum = sum.add(addend).reduce(); - } - System.out.println("Part 1: " + sum.magnitude()); - } - - @Test - public final void part2() { - final var list = - getInput().map(SnailfishNumber::new).collect(Collectors.toList()); - int max = Integer.MIN_VALUE; - for (final var x : list) { - for (final var y : list) { - if (x.equals(y)) { - continue; - } - final var sum = x.add(y).reduce(); - final var magnitude = sum.magnitude(); - if (magnitude > max) { - max = magnitude; - } - } - } - System.out.println("Part 2: " + max); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day19.java b/src/test/java/com/macasaet/Day19.java deleted file mode 100644 index a11c202..0000000 --- a/src/test/java/com/macasaet/Day19.java +++ /dev/null @@ -1,395 +0,0 @@ -package com.macasaet; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * --- Day 19: Beacon Scanner --- - */ -public class Day19 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-19.txt"), - false); - } - - protected List getScanners() { - final var list = getInput().toList(); - final var result = new ArrayList(); - var observations = new HashSet(); - int id = -1; - for (final var line : list) { - if (line.startsWith("--- scanner ")) { - id = Integer.parseInt(line - .replaceFirst("^--- scanner ", "") - .replaceFirst(" ---$", "")); - } else if (!line.isBlank()) { - observations.add(Position.parse(line)); - } else { // line is blank - result.add(new Scanner(id, Collections.unmodifiableSet(observations))); - observations = new HashSet<>(); - id = -1; - } - } - result.add(new Scanner(id, Collections.unmodifiableSet(observations))); - return Collections.unmodifiableList(result); - } - - public enum Direction { - POSITIVE_X { - public Position face(Position position) { - return position; - } - }, - NEGATIVE_X { - public Position face(Position position) { - return new Position(-position.x(), position.y(), -position.z()); - } - }, - POSITIVE_Y { - public Position face(Position position) { - return new Position(position.y(), -position.x(), position.z()); - } - }, - NEGATIVE_Y { - public Position face(Position position) { - return new Position(-position.y(), position.x(), position.z()); - } - }, - POSITIVE_Z { - public Position face(Position position) { - return new Position(position.z(), position.y(), -position.x()); - } - }, - NEGATIVE_Z { - public Position face(Position position) { - return new Position(-position.z(), position.y(), position.x()); - } - }; - - public abstract Position face(final Position position); - - } - - public enum Rotation { - r0 { - public Position rotate(final Position position) { - return position; - } - }, - r90 { - public Position rotate(Position position) { - return new Position(position.x(), -position.z(), position.y()); - } - }, - r180 { - public Position rotate(Position position) { - return new Position(position.x(), -position.y(), -position.z()); - } - }, - r270 { - public Position rotate(Position position) { - return new Position(position.x(), position.z(), -position.y()); - } - }; - - public abstract Position rotate(final Position position); - } - - public record Transformation(Direction direction, Rotation rotation) { - /** - * Look at a position from a specific orientation - * - * @param position a position relative to one point of view - * @return the same position relative to a different point of view - */ - public Position reorient(final Position position) { - return rotation.rotate(direction.face(position)); - } - - } - - public record Position(int x, int y, int z) { - public static Position parse(final String line) { - final var components = line.split(","); - return new Position(Integer.parseInt(components[0]), - Integer.parseInt(components[1]), - Integer.parseInt(components[2])); - } - - public Position plus(Position amount) { - return new Position(x() + amount.x(), y() + amount.y(), z() + amount.z()); - } - - public Position minus(final Position other) { - return new Position(x() - other.x(), y() - other.y(), z() - other.z()); - } - } - - public interface OverlapResult { - } - - public record Overlap(Position distance, Transformation transformation, - Set overlappingBeacons) implements OverlapResult { - } - - public record None() implements OverlapResult { - } - - public record Scanner(int id, Set observations) { - - public OverlapResult getOverlappingBeacons(final Scanner other) { - for (final var direction : Direction.values()) { - for (final var rotation : Rotation.values()) { - final var transformation = new Transformation(direction, rotation); - final var distances = observations().stream() - .flatMap(a -> other.observations() - .stream() - .map(transformation::reorient) - .map(a::minus)) - .collect(Collectors.toList()); - for (final var offset : distances) { - final var intersection = other.observations() - .stream() - .map(transformation::reorient) - .map(observation -> observation.plus(offset)) - .filter(observations()::contains) - .collect(Collectors.toUnmodifiableSet()); - if (intersection.size() >= 12) { - return new Overlap(offset, transformation, intersection); - } - } - } - } - return new None(); - } - - } - - @Nested - public class ScannerTest { - @Test - public final void testOverlapWithOrigin() { - // given - final var scanner0Observations = """ - 404,-588,-901 - 528,-643,409 - -838,591,734 - 390,-675,-793 - -537,-823,-458 - -485,-357,347 - -345,-311,381 - -661,-816,-575 - -876,649,763 - -618,-824,-621 - 553,345,-567 - 474,580,667 - -447,-329,318 - -584,868,-557 - 544,-627,-890 - 564,392,-477 - 455,729,728 - -892,524,684 - -689,845,-530 - 423,-701,434 - 7,-33,-71 - 630,319,-379 - 443,580,662 - -789,900,-551 - 459,-707,401 - """; - final var scanner1Observations = """ - 686,422,578 - 605,423,415 - 515,917,-361 - -336,658,858 - 95,138,22 - -476,619,847 - -340,-569,-846 - 567,-361,727 - -460,603,-452 - 669,-402,600 - 729,430,532 - -500,-761,534 - -322,571,750 - -466,-666,-811 - -429,-592,574 - -355,545,-477 - 703,-491,-529 - -328,-685,520 - 413,935,-424 - -391,539,-444 - 586,-435,557 - -364,-763,-893 - 807,-499,-711 - 755,-354,-619 - 553,889,-390 - """; - final var scanner0 = new Scanner(0, - Arrays.stream(scanner0Observations.split("\n")) - .map(Position::parse) - .collect(Collectors.toUnmodifiableSet())); - final var scanner1 = new Scanner(0, - Arrays.stream(scanner1Observations.split("\n")) - .map(Position::parse) - .collect(Collectors.toUnmodifiableSet())); - - // when - final var result = scanner0.getOverlappingBeacons(scanner1); - - // then - assertTrue(result instanceof Overlap); - final var overlap = (Overlap) result; - assertEquals(12, overlap.overlappingBeacons().size()); - } - - @Test - public final void testOverlapWithNonOrigin() { - // given - final var scanner1Observations = """ - 686,422,578 - 605,423,415 - 515,917,-361 - -336,658,858 - 95,138,22 - -476,619,847 - -340,-569,-846 - 567,-361,727 - -460,603,-452 - 669,-402,600 - 729,430,532 - -500,-761,534 - -322,571,750 - -466,-666,-811 - -429,-592,574 - -355,545,-477 - 703,-491,-529 - -328,-685,520 - 413,935,-424 - -391,539,-444 - 586,-435,557 - -364,-763,-893 - 807,-499,-711 - 755,-354,-619 - 553,889,-390 - """; - final var scanner4Observations = """ - 727,592,562 - -293,-554,779 - 441,611,-461 - -714,465,-776 - -743,427,-804 - -660,-479,-426 - 832,-632,460 - 927,-485,-438 - 408,393,-506 - 466,436,-512 - 110,16,151 - -258,-428,682 - -393,719,612 - -211,-452,876 - 808,-476,-593 - -575,615,604 - -485,667,467 - -680,325,-822 - -627,-443,-432 - 872,-547,-609 - 833,512,582 - 807,604,487 - 839,-516,451 - 891,-625,532 - -652,-548,-490 - 30,-46,-14 - """; - final var scanner1 = new Scanner(0, - Arrays.stream(scanner1Observations.split("\n")) - .map(Position::parse) - .collect(Collectors.toUnmodifiableSet())); - final var scanner4 = new Scanner(0, - Arrays.stream(scanner4Observations.split("\n")) - .map(Position::parse) - .collect(Collectors.toUnmodifiableSet())); - - // when - final var result = scanner1.getOverlappingBeacons(scanner4); - - // then - assertTrue(result instanceof Overlap); - final var overlap = (Overlap) result; - assertEquals(12, overlap.overlappingBeacons().size()); - } - } - - @Test - public final void part1() { - final var scanners = getScanners(); - final var knownBeacons = new HashSet(); - final var origin = new Scanner(-1, knownBeacons); - final var remaining = new ArrayList<>(scanners); - while (!remaining.isEmpty()) { - final var other = remaining.remove(0); - if (knownBeacons.isEmpty()) { - knownBeacons.addAll(other.observations()); - continue; - } - final var result = origin.getOverlappingBeacons(other); - if (result instanceof final Overlap overlap) { - knownBeacons.addAll(other.observations() - .stream() - .map(overlap.transformation()::reorient) - .map(observation -> observation.plus(overlap.distance())) - .collect(Collectors.toList())); - } else { - remaining.add(other); - } - } - System.out.println("Part 1: " + knownBeacons.size()); - } - - @Test - public final void part2() { - final var scanners = getScanners(); - final var knownBeacons = new HashSet(); - final var origin = new Scanner(-1, knownBeacons); - final var remaining = new ArrayList<>(scanners); - final var distances = new HashSet(); - while (!remaining.isEmpty()) { - final var other = remaining.remove(0); - if (knownBeacons.isEmpty()) { - knownBeacons.addAll(other.observations()); - continue; - } - final var result = origin.getOverlappingBeacons(other); - if (result instanceof final Overlap overlap) { - knownBeacons.addAll(other.observations() - .stream() - .map(overlap.transformation()::reorient) - .map(observation -> observation.plus(overlap.distance())) - .collect(Collectors.toList())); - distances.add(overlap.distance()); - } else { - remaining.add(other); - } - } - int maxDistance = Integer.MIN_VALUE; - for (final var x : distances) { - for (final var y : distances) { - final int distance = Math.abs(x.x() - y.x()) - + Math.abs(x.y() - y.y()) - + Math.abs(x.z() - y.z()); - maxDistance = Math.max(maxDistance, distance); - } - } - System.out.println("Part 2: " + maxDistance); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day20.java b/src/test/java/com/macasaet/Day20.java deleted file mode 100644 index 85115e9..0000000 --- a/src/test/java/com/macasaet/Day20.java +++ /dev/null @@ -1,233 +0,0 @@ -package com.macasaet; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.*; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * --- Day 20: Trench Map --- - */ -public class Day20 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-20.txt"), - false); - } - - public record ImageEnhancementAlgorithm(boolean[] map) { - - public boolean isPixelLit(final int code) { - return map()[code]; - } - - public static ImageEnhancementAlgorithm parse(final String line) { - final var map = new boolean[line.length()]; - for (int i = line.length(); --i >= 0; map[i] = line.charAt(i) == '#') ; - return new ImageEnhancementAlgorithm(map); - } - } - - protected record Coordinate(int x, int y) { - } - - public record Image(SortedMap> pixels, int minX, int maxX, int minY, - int maxY, boolean isEven) { - public Image enhance(final ImageEnhancementAlgorithm algorithm) { - final var enhancedPixelMap = new TreeMap>(); - int enhancedMinX = minX; - int enhancedMaxX = maxX; - int enhancedMinY = minY; - int enhancedMaxY = maxY; - for (int i = minX - 1; i <= maxX + 1; i++) { - final var targetRow = new TreeMap(); - for (int j = minY - 1; j <= maxY + 1; j++) { - final int replacementId = decode(i, j, !isEven && algorithm.isPixelLit(0)); - final var shouldLight = algorithm.isPixelLit(replacementId); - if (shouldLight) { - // save space by only storing an entry when lit - targetRow.put(j, true); - enhancedMinY = Math.min(enhancedMinY, j); - enhancedMaxY = Math.max(enhancedMaxY, j); - } - } - if (!targetRow.isEmpty()) { - // save space by only storing a row if at least one cell is lit - enhancedPixelMap.put(i, Collections.unmodifiableSortedMap(targetRow)); - enhancedMinX = Math.min(enhancedMinX, i); - enhancedMaxX = Math.max(enhancedMaxX, i); - } - } - return new Image(Collections.unmodifiableSortedMap(enhancedPixelMap), enhancedMinX, enhancedMaxX, enhancedMinY, enhancedMaxY, !isEven); - } - - int decode(final int x, final int y, boolean voidIsLit) { - final var list = getNeighbouringCoordinates(x, y).stream() - .map(coordinate -> isBitSet(coordinate, voidIsLit)) - .toList(); - int result = 0; - for (int i = list.size(); --i >= 0; ) { - if (list.get(i)) { - final int shiftDistance = list.size() - i - 1; - result |= 1 << shiftDistance; - } - } - if (result < 0 || result > 512) { - throw new IllegalStateException("Unable to decode pixel at " + x + ", " + y); - } - return result; - } - - boolean isBitSet(final Coordinate coordinate, boolean voidIsLit) { - final var row = pixels().get(coordinate.x()); - if((coordinate.x() < minX || coordinate.x() > maxX) && row == null) { - return voidIsLit; - } - else if(row == null) { - return false; - } - return row.getOrDefault(coordinate.y(), (coordinate.y() < minY || coordinate.y() > maxY) && voidIsLit); - } - - List getNeighbouringCoordinates(int x, int y) { - return Arrays.asList( - new Coordinate(x - 1, y - 1), - new Coordinate(x - 1, y), - new Coordinate(x - 1, y + 1), - new Coordinate(x, y - 1), - new Coordinate(x, y), - new Coordinate(x, y + 1), - new Coordinate(x + 1, y - 1), - new Coordinate(x + 1, y), - new Coordinate(x + 1, y + 1) - ); - } - - public long countLitPixels() { - return pixels().values() - .stream() - .flatMap(row -> row.values().stream()) - .filter(isLit -> isLit) - .count(); - } - - public int width() { - return maxX - minX + 1; - } - - public int height() { - return maxY - minY + 1; - } - - public String toString() { - final var builder = new StringBuilder(); - builder.append(width()).append('x').append(height()).append('\n'); - for (int i = minX; i <= maxX; i++) { - final var row = pixels.getOrDefault(i, Collections.emptySortedMap()); - for (int j = minY; j <= maxY; j++) { - final var value = row.getOrDefault(j, false); - builder.append(value ? '#' : '.'); - } - builder.append('\n'); - } - return builder.toString(); - } - - public static Image parse(final List lines) { - final var pixels = new TreeMap>(); - final int minX = 0; - final int minY = 0; - int maxX = 0; - int maxY = 0; - for (int i = lines.size(); --i >= 0; ) { - final var line = lines.get(i); - final var row = new TreeMap(); - for (int j = line.length(); --j >= 0; ) { - final var pixel = line.charAt(j); - row.put(j, pixel == '#'); - if (pixel == '#') { - row.put(j, true); - maxY = Math.max(maxY, j); - } - } - if (!row.isEmpty()) { - maxX = Math.max(maxX, i); - pixels.put(i, Collections.unmodifiableSortedMap(row)); - } - } - return new Image(Collections.unmodifiableSortedMap(pixels), minX, maxX, minY, maxY, true); - } - - } - - @Nested - public class ImageTest { - @Test - public final void testToInt() { - final var string = """ - #..#. - #.... - ##..# - ..#.. - ..### - """; - final var image = Image.parse(Arrays.asList(string.split("\n"))); - assertEquals(34, image.decode(2, 2, false)); - } - - @Test - public final void flipAllOn() { - final var template = "#........"; - final var imageString = """ - ... - ... - ... - """; - final var image = Image.parse(Arrays.asList(imageString.split("\n"))); - final var result = image.enhance(ImageEnhancementAlgorithm.parse(template)); - assertTrue(result.pixels().get(1).get(1)); - } - - @Test - public final void turnOffPixel() { - final var templateBuilder = new StringBuilder(); - for (int i = 511; --i >= 0; templateBuilder.append('#')) ; - templateBuilder.append('.'); - final var template = templateBuilder.toString(); - final var imageString = """ - ### - ### - ### - """; - final var image = Image.parse(Arrays.asList(imageString.split("\n"))); - final var result = image.enhance(ImageEnhancementAlgorithm.parse(template)); - final var middleRow = result.pixels().get(1); - assertFalse(middleRow.containsKey(1)); - } - } - - @Test - public final void part1() { - final var list = getInput().toList(); - final var algorithm = ImageEnhancementAlgorithm.parse(list.get(0)); - final var image = Image.parse(list.subList(2, list.size())) - .enhance(algorithm) - .enhance(algorithm); - System.out.println("Part 1: " + image.countLitPixels()); - } - - @Test - public final void part2() { - final var list = getInput().toList(); - final var algorithm = ImageEnhancementAlgorithm.parse(list.get(0)); - var image = Image.parse(list.subList(2, list.size())); - for(int _i = 50; --_i >= 0; image = image.enhance(algorithm)); - System.out.println("Part 2: " + image.countLitPixels()); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day21.java b/src/test/java/com/macasaet/Day21.java deleted file mode 100644 index 1223048..0000000 --- a/src/test/java/com/macasaet/Day21.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.macasaet; - -import java.math.BigInteger; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Test; - -/** - * --- Day 21: Dirac Dice --- - */ -public class Day21 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-21.txt"), - false); - } - - public static class DeterministicDie { - int value; - int totalRolls = 0; - - protected DeterministicDie(final int startingValue) { - this.value = startingValue; - } - - public DeterministicDie() { - this(1); - } - - public int roll() { - final int result = value; - value += 1; - if (value > 100) { - value -= 100; - } - totalRolls++; - return result; - } - } - - public record Pawn(int position, int score) { - public Pawn fork() { - return new Pawn(position(), score()); - } - - public Pawn move(final int distance) { - int newPosition = (position() + distance) % 10; - if (newPosition == 0) { - newPosition = 10; - } - final int newScore = score() + newPosition; - return new Pawn(newPosition, newScore); - } - - public Pawn takeTurn(final DeterministicDie die) { - final int distance = die.roll() + die.roll() + die.roll(); - return move(distance); - } - } - - public record Game(Pawn player1, Pawn player2, boolean playerOnesTurn) { - - } - - public record ScoreCard(BigInteger playerOneWins, BigInteger playerTwoWins) { - public ScoreCard add(final ScoreCard other) { - return new ScoreCard(playerOneWins().add(other.playerOneWins()), playerTwoWins().add(other.playerTwoWins())); - } - } - - public static class QuantumDie { - private final Map cache = new HashMap<>(); - - public ScoreCard play(final Game game) { - if (cache.containsKey(game)) { - return cache.get(game); - } - final var reverseScenario = new Game(game.player2(), game.player1(), !game.playerOnesTurn()); - if (cache.containsKey(reverseScenario)) { - final var reverseResult = cache.get(reverseScenario); - return new ScoreCard(reverseResult.playerTwoWins(), reverseResult.playerOneWins()); - } - - if (game.player1().score() >= 21) { - final var result = new ScoreCard(BigInteger.ONE, BigInteger.ZERO); - cache.put(game, result); - return result; - } else if (game.player2().score() >= 21) { - final var result = new ScoreCard(BigInteger.ZERO, BigInteger.ONE); - cache.put(game, result); - return result; - } - - var result = new ScoreCard(BigInteger.ZERO, BigInteger.ZERO); - for (int i = 1; i <= 3; i++) { - for (int j = 1; j <= 3; j++) { - for (int k = 1; k <= 3; k++) { - final int movementDistance = i + j + k; - final var forkResult = game.playerOnesTurn() - ? play(new Game(game.player1().move(movementDistance), game.player2(), false)) - : play(new Game(game.player1(), game.player2().fork().move(movementDistance), true)); - result = result.add(forkResult); - } - } - } - cache.put(game, result); - return result; - } - } - - @Test - public final void part1() { - final var lines = getInput().toList(); - final int playerOnePosition = - Integer.parseInt(lines.get(0).replaceAll("Player . starting position: ", "")); - final int playerTwoPosition = - Integer.parseInt(lines.get(1).replaceAll("Player . starting position: ", "")); - var playerOne = new Pawn(playerOnePosition, 0); - var playerTwo = new Pawn(playerTwoPosition, 0); - final var die = new DeterministicDie(); - while (true) { - // player 1 - playerOne = playerOne.takeTurn(die); - if (playerOne.score() >= 1000) { - break; - } - - // player 2 - playerTwo = playerTwo.takeTurn(die); - if (playerTwo.score() >= 1000) { - break; - } - } - int losingScore = Math.min(playerOne.score(), playerTwo.score()); - - System.out.println("Part 1: " + (losingScore * die.totalRolls)); - } - - @Test - public final void part2() { - final var lines = getInput().toList(); - final int playerOnePosition = - Integer.parseInt(lines.get(0).replaceAll("Player . starting position: ", "")); - final int playerTwoPosition = - Integer.parseInt(lines.get(1).replaceAll("Player . starting position: ", "")); - final var playerOne = new Pawn(playerOnePosition, 0); - final var playerTwo = new Pawn(playerTwoPosition, 0); - final var die = new QuantumDie(); - final var game = new Game(playerOne, playerTwo, true); - final var result = die.play(game); - final var winningScore = result.playerOneWins().max(result.playerTwoWins()); - System.out.println("Part 2: " + winningScore); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day22.java b/src/test/java/com/macasaet/Day22.java deleted file mode 100644 index 333be59..0000000 --- a/src/test/java/com/macasaet/Day22.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.macasaet; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.math.BigInteger; -import java.util.*; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -/** - * --- Day 22: Reactor Reboot --- - */ -public class Day22 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-22.txt"), - false); - } - - public record ReactorCore( - SortedMap>> cubes) { - - public long count(final long xMin, final long xMax, final long yMin, final long yMax, final long zMin, final long zMax) { - long sum = 0; - for (var i = xMin; i <= xMax; i++) { - final var xDimension = cubes().getOrDefault(i, Collections.emptySortedMap()); - for (var j = yMin; j <= yMax; j++) { - final var yDimension = xDimension.getOrDefault(j, Collections.emptySortedMap()); - for (var k = zMin; k <= zMax; k++) { - if (yDimension.getOrDefault(k, false)) { - sum++; - } - } - } - } - return sum; - } - - public void process(final Instruction instruction) { - final var block = instruction.block(); - final var on = instruction.on(); - for (var i = block.xMin(); i <= block.xMax(); i++) { - final var xDimension = cubes().computeIfAbsent(i, _key -> new TreeMap<>()); - for (var j = block.yMin(); j <= block.yMax(); j++) { - final var yDimension = xDimension.computeIfAbsent(j, _key -> new TreeMap<>()); - for (var k = block.zMin(); k <= block.zMax(); k++) { - yDimension.put(k, on); - } - } - } - } - - } - - public record Instruction(boolean on, Block block) { - public static Instruction parse(final String string) { - final var components = string.split(" "); - final boolean on = "on".equalsIgnoreCase(components[0]); - final var block = Block.parse(components[1]); - return new Instruction(on, block); - } - - } - - public record Block(long xMin, long xMax, long yMin, long yMax, long zMin, long zMax) { - - public BigInteger volume() { - return (BigInteger.valueOf(xMax).subtract(BigInteger.valueOf(xMin)).add(BigInteger.ONE)) - .multiply(BigInteger.valueOf(yMax).subtract(BigInteger.valueOf(yMin)).add(BigInteger.ONE)) - .multiply(BigInteger.valueOf(zMax).subtract(BigInteger.valueOf(zMin)).add(BigInteger.ONE)); - } - - public static Block parse(final String string) { - final var ranges = string.split(","); - final var xRange = ranges[0].split("\\.\\."); - final var xMin = Long.parseLong(xRange[0].replaceAll("x=", "")); - final var xMax = Long.parseLong((xRange[1])); - final var yRange = ranges[1].split("\\.\\."); - final var yMin = Long.parseLong((yRange[0].replaceAll("y=", ""))); - final var yMax = Long.parseLong((yRange[1])); - final var zRange = ranges[2].split("\\.\\."); - final var zMin = Long.parseLong((zRange[0].replaceAll("z=", ""))); - final var zMax = Long.parseLong((zRange[1])); - return new Block(xMin, xMax, yMin, yMax, zMin, zMax); - } - - public boolean overlaps(final Block other) { - return intersection(other).isPresent(); - } - - public Optional intersection(final Block other) { - if (xMin > other.xMax() || xMax < other.xMin() - || yMin > other.yMax() || yMax < other.yMin() - || zMin > other.zMax() || zMax < other.zMin()) { - return Optional.empty(); - } - final var result = new Block(Math.max(xMin, other.xMin()), Math.min(xMax, other.xMax()), - Math.max(yMin, other.yMin()), Math.min(yMax, other.yMax()), - Math.max(zMin, other.zMin()), Math.min(zMax, other.zMax())); - return Optional.of(result); - } - } - - @Nested - public class BlockTest { - @Test - public final void verifyEqualBlocksOverlap() { - // given - final var x = new Block(-2, 2, -2, 2, -2, 2); - final var y = new Block(-2, 2, -2, 2, -2, 2); - - // when - - // then - assertTrue(x.overlaps(y)); - assertTrue(y.overlaps(x)); - } - - @Test - public final void verifyNestedBlocksOverlap() { - final var inner = new Block(-2, 2, -2, 2, -2, 2); - final var outer = new Block(-4, 4, -4, 4, -4, 4); - - assertTrue(inner.overlaps(outer)); - assertTrue(outer.overlaps(inner)); - } - - @Test - public final void verifyIntersectingBlocksOverlap() { - final var x = new Block(10, 12, 10, 12, 10, 12); - final var y = new Block(11, 13, 11, 13, 11, 13); - - assertTrue(x.overlaps(y)); - assertTrue(y.overlaps(x)); - } - - @Test - public final void testIntersection() { - final var x = new Block(10, 12, 10, 12, 10, 12); - final var y = new Block(11, 13, 11, 13, 11, 13); - - assertTrue(x.intersection(y).isPresent()); - assertEquals(BigInteger.valueOf(8), x.intersection(y).orElseThrow().volume()); - assertTrue(y.intersection(x).isPresent()); - assertEquals(BigInteger.valueOf(8), y.intersection(x).orElseThrow().volume()); - assertEquals(x.intersection(y).orElseThrow(), y.intersection(x).orElseThrow()); - } - } - - @Test - public final void part1() { - final var core = new ReactorCore(new TreeMap<>()); - getInput().map(Instruction::parse).map(fullInstruction -> { - final var fullBlock = fullInstruction.block(); - final var truncatedBlock = new Block(Math.max(fullBlock.xMin(), -50), Math.min(fullBlock.xMax(), 50), - Math.max(fullBlock.yMin(), -50), Math.min(fullBlock.yMax(), 50), - Math.max(fullBlock.zMin(), -50), Math.min(fullBlock.zMax(), 50)); - return new Instruction(fullInstruction.on(), truncatedBlock); - }).forEach(core::process); - System.out.println("Part 1: " + core.count(-50, 50, -50, 50, -50, 50)); - } - - @Test - public final void part2() { - final var originalList = getInput().map(Instruction::parse).toList(); - final var appliedInstructions = new ArrayList(); - for (final var instruction : originalList) { - final var modifiedInstructions = new ArrayList(); - if (instruction.on()) { - // only add initial instructions that turn ON cubes - modifiedInstructions.add(instruction); - } - // override any previous instructions - for (final var previousInstruction : appliedInstructions) { - // add compensating instructions to handle the overlaps - instruction.block() - .intersection(previousInstruction.block()) - .map(intersection -> new Instruction(!previousInstruction.on(), intersection)) - .ifPresent(modifiedInstructions::add); - } - - appliedInstructions.addAll(modifiedInstructions); - } - - final var sum = appliedInstructions.stream() - .map(instruction -> instruction.block() - .volume() - .multiply(instruction.on() - ? BigInteger.ONE - : BigInteger.valueOf(-1))) - .reduce(BigInteger.ZERO, - BigInteger::add); - - System.out.println("Part 2: " + sum); - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day23.java b/src/test/java/com/macasaet/Day23.java deleted file mode 100644 index c545b82..0000000 --- a/src/test/java/com/macasaet/Day23.java +++ /dev/null @@ -1,900 +0,0 @@ -package com.macasaet; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.PriorityBlockingQueue; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * --- Day 23: Amphipod --- - */ -public class Day23 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-23.txt"), - false); - } - - protected Tile[][] parseGrid(List lines) { - final var result = new Tile[lines.size()][]; - for (int i = lines.size(); --i >= 0; ) { - final var line = lines.get(i); - result[i] = new Tile[line.length()]; - for (int j = line.length(); --j >= 0; ) { - final AmphipodType targetType = AmphipodType.forDestinationColumn(j); - final var c = line.charAt(j); - result[i][j] = switch (c) { - case '.' -> new Tile(new Point(i, j), null, null); - case 'A' -> new Tile(new Point(i, j), targetType, AmphipodType.AMBER); - case 'B' -> new Tile(new Point(i, j), targetType, AmphipodType.BRONZE); - case 'C' -> new Tile(new Point(i, j), targetType, AmphipodType.COPPER); - case 'D' -> new Tile(new Point(i, j), targetType, AmphipodType.DESERT); - default -> null; - }; - } - } - - return result; - } - - public enum AmphipodType { - AMBER(1, 3), - BRONZE(10, 5), - COPPER(100, 7), - DESERT(1000, 9); - - private final int energyPerStep; - private final int destinationColumn; - - AmphipodType(final int energyPerStep, final int destinationColumn) { - this.energyPerStep = energyPerStep; - this.destinationColumn = destinationColumn; - } - - public static AmphipodType forDestinationColumn(final int destinationColumn) { - for (final var candidate : values()) { - if (candidate.destinationColumn == destinationColumn) { - return candidate; - } - } - return null; - } - } - - public record Move(Point from, Point to) { - } - - public record BranchResult(Node node, int cost) { - } - - public record Node(Tile[][] tiles, Map, BranchResult> branchCache) { - - private static final Map estimatedDistanceCache = new ConcurrentHashMap<>(); - private static final Map> branchesCache = new ConcurrentHashMap<>(); - private static final Map solutionCache = new ConcurrentHashMap<>(); - - public static Node createInitialNode(final Tile[][] tiles) { - return new Node(tiles, new ConcurrentHashMap<>()); - } - - public BranchResult branch(final List moves) { - if (moves.size() == 0) { - System.err.println("How is this empty?"); - return new BranchResult(this, 0); - } - if (branchCache.containsKey(moves)) { - return branchCache.get(moves); - } - final var copy = new Tile[tiles.length][]; - for (int i = tiles.length; --i >= 0; ) { - final var row = new Tile[tiles[i].length]; - System.arraycopy(tiles[i], 0, row, 0, tiles[i].length); - copy[i] = row; - } - final var source = moves.get(0).from(); - final var destination = moves.get(moves.size() - 1).to(); - final var sourceTile = source.getTile(tiles); - final var destinationTile = destination.getTile(tiles); - final var amphipod = sourceTile.amphipodType(); - if (amphipod == null) { - System.err.println("source amphipod is missing :-("); - } - source.setTile(copy, sourceTile.updateType(null)); - destination.setTile(copy, destinationTile.updateType(amphipod)); - final var cost = moves.size() * amphipod.energyPerStep; - final var result = new BranchResult(new Node(copy, new ConcurrentHashMap<>()), cost); - branchCache.put(moves, result); - return result; - } - - boolean isSideRoom(final Point point) { - final var x = point.x(); - final var y = point.y(); - return x > 1 && (y == 3 || y == 5 || y == 7 || y == 9); - } - - boolean isCorridor(final Point point) { - return point.x() == 1; - } - - public int hashCode() { - // equality based on layout of the burrow regardless of how the amphipods got to that state - // FNV hash - long result = 2166136261L; - final Function rowHasher = row -> { - long rowHash = 2166136261L; - for (final var tile : row) { - rowHash = (16777619L * rowHash) ^ (tile == null ? 0L : (long) Objects.hashCode(tile.amphipodType())); - } - return rowHash; - }; - for (final var row : tiles()) { - result = (16777619L * result) ^ rowHasher.apply(row); - } - - return Long.hashCode(result); - // Bob Jenkins' One-at-a-Time hash -// int result = 0; -// final Function rowHasher = row -> { -// int rowHash = 0; -// for(final var tile : row) { -// final var tileHash = tile != null ? Objects.hashCode(tile.amphipodType()) : 0; -// rowHash += tileHash; -// rowHash += rowHash << 10; -// rowHash ^= rowHash >> 6; -// } -// rowHash += rowHash << 3; -// rowHash ^= rowHash >> 11; -// rowHash += rowHash << 15; -// return rowHash; -// }; -// for(final var row : tiles()) { -// result += rowHasher.apply(row); -// result += (result << 10); -// result ^= (result >> 6); -// } -// result += result << 3; -// result ^= result >> 11; -// result += result << 15; -// return result; - } - - public boolean equals(final Object o) { - if (o == null) { - return false; - } - if (this == o) { - return true; - } - try { - final var other = (Node) o; - // equality based on layout of the burrow regardless of how the amphipods got to that state - if (tiles().length != other.tiles().length) { - return false; - } - for (int i = tiles().length; --i >= 0; ) { - final var xRow = tiles()[i]; - final var yRow = other.tiles()[i]; - if (xRow.length != yRow.length) { - return false; - } - for (int j = xRow.length; --j >= 0; ) { - if (xRow[j] != yRow[j]) { - if (xRow[j] == null - || yRow[j] == null - || !Objects.equals(xRow[j].amphipodType(), yRow[j].amphipodType())) { - return false; - } - } - } - } - return true; - } catch (final ClassCastException cce) { - return false; - } - } - - public String toString() { - final var builder = new StringBuilder(); - for (final var row : tiles()) { - for (final var cell : row) { - if (cell == null) { - builder.append('#'); - } else if (cell.amphipodType() == null) { - builder.append('.'); - } else { - builder.append(switch (cell.amphipodType()) { - case AMBER -> 'A'; - case BRONZE -> 'B'; - case COPPER -> 'C'; - case DESERT -> 'D'; - }); - } - } - builder.append('\n'); - } - return builder.toString(); - } - - public boolean isSolution() { - if (solutionCache.containsKey(this)) { - return solutionCache.get(this); - } - final var result = Arrays.stream(tiles()) - .flatMap(Arrays::stream) - .filter(Objects::nonNull) - .allMatch(Tile::hasTargetType); - solutionCache.put(this, result); - return result; - } - - public Stream getBranches() { - if (branchesCache.containsKey(this)) { - return branchesCache.get(this).stream(); - } - final var branchResults = new Vector(); - return Arrays.stream(tiles()) - .parallel() - .flatMap(row -> Arrays.stream(row) - .parallel() - .filter(Objects::nonNull) - .filter(tile -> !tile.isVacant()) - .flatMap(this::getMoves)) - .map(this::branch) - .peek(branchResults::add) - .onClose(() -> branchesCache.put(this, Collections.unmodifiableList(branchResults))); - } - -// @Deprecated -// public Set> getMoves() { -// final var result = new HashSet>(); -// for (final var row : tiles()) { -// for (final var tile : row) { -// if (tile != null && !tile.isVacant()) { -// result.addAll(getMoves(tile).collect(Collectors.toSet())); -// } -// } -// } -// return Collections.unmodifiableSet(result); -// } - - /** - * Find all the actions (series of moves) that can be taken for a single amphipod. - * - * @param occupiedTile a tile with an amphipod - * @return All the moves that can be applied starting with occupiedTile that end in a valid temporary - * destination for the amphipod - */ - Stream> getMoves(final Tile occupiedTile) { - if (isSideRoom(occupiedTile.location()) && occupiedTile.hasTargetType()) { - var roomComplete = true; - for (var cursor = tiles[occupiedTile.location().x() + 1][occupiedTile.location().y()]; - cursor != null; - cursor = tiles[cursor.location().x() + 1][cursor.location().y()]) { - if (!cursor.hasTargetType()) { - // one of the amphipods in this room is destined elsewhere - // so the amphipod from the original tile will need to move out of the way - roomComplete = false; - break; - } - } - if (roomComplete) { - // all the amphipods in this room have this as their intended destination - return Stream.empty(); - } - } - - var paths = iterateThroughPaths(occupiedTile.amphipodType(), - occupiedTile, - Collections.singletonList(occupiedTile.location())); - if (isCorridor(occupiedTile.location())) { - /* - * "Once an amphipod stops moving in the hallway, it will stay in that spot until it can move into a - * room. (That is, once any amphipod starts moving, any other amphipods currently in the hallway are - * locked in place and will not move again until they can move fully into a room.)" - */ - paths = paths - // only select paths that end in a room - .filter(path -> isSideRoom(path.get(path.size() - 1) - .getTile(tiles()) - .location())); - } else { - // reduce the search space by only considering rooms from rooms into hallways - // prune any path that starts from a room and ends in a room -// paths = paths -// .filter(path -> isCorridor(path.get(path.size() - 1) -// .getTile(tiles()) -// .location())); - } - // convert tiles to moves - return paths - .filter(path -> path.size() > 1) // filter out paths in which the amphipod does not move - .map(points -> { - final var moves = new ArrayList(points.size() - 1); - for (int i = 1; i < points.size(); i++) { - moves.add(new Move(points.get(i - 1), points.get(i))); - } - return Collections.unmodifiableList(moves); - }); - } - - Stream> iterateThroughPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - // TODO store `pathSoFar` as a stack so checking for node becomes O(1) instead of O(n) - final int x = current.location.x(); - final int y = current.location.y(); - final var up = tiles[x - 1][y]; - final var down = tiles[x + 1][y]; - final var left = tiles[x][y - 1]; - final var right = tiles[x][y + 1]; - final var suppliers = new ArrayList>>>(4); - if (up != null && up.isVacant() && !pathSoFar.contains(up.location())) { - suppliers.add(() -> streamUpPaths(amphipodType, current, pathSoFar)); - } - if (down != null - && down.isVacant() - && !pathSoFar.contains(down.location()) - // don't enter side room unless it is the ultimate destination - && down.targetType == amphipodType) { - suppliers.add(() -> streamDownPaths(amphipodType, current, pathSoFar)); - } - if (left != null && left.isVacant() && !pathSoFar.contains(left.location())) { - suppliers.add(() -> streamLeftPaths(amphipodType, current, pathSoFar)); - } - if (right != null && right.isVacant() && !pathSoFar.contains(right.location())) { - suppliers.add(() -> streamRightPaths(amphipodType, current, pathSoFar)); - } - if (suppliers.isEmpty()) { - // dead end, emit the path so far - suppliers.add(() -> Stream.of(Collections.unmodifiableList(pathSoFar))); - } - - return suppliers.stream() - .flatMap(Supplier::get); - } - - Stream> streamUpPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - final int x = current.location.x(); - final int y = current.location.y(); - final var up = tiles[x - 1][y]; - // amphipod is in a side room - if (isSideRoom(up.location()) && current.targetType == amphipodType) { - // amphipod is in the back of the room in which it belongs, stop here - return Stream.of(pathSoFar); - } - final var incrementalPath = new ArrayList<>(pathSoFar); - incrementalPath.add(up.location()); - // whether "up" is the front of the room or the corridor outside the room, we have to keep moving - return iterateThroughPaths(amphipodType, up, incrementalPath); - } - - Stream> streamDownPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - final int x = current.location.x(); - final int y = current.location.y(); - final var down = tiles[x + 1][y]; - final var incrementalPath = new ArrayList<>(pathSoFar); - incrementalPath.add(down.location()); - if ((isCorridor(current.location()) && canEnterRoom(amphipodType, down)) || isSideRoom(current.location())) { - // go as for back into the room as possible, don't just stop at the entrance - return iterateThroughPaths(amphipodType, down, incrementalPath); - } - return Stream.empty(); - } - - Stream> streamLeftPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - final int x = current.location.x(); - final int y = current.location.y(); - final var left = tiles[x][y - 1]; - Stream> result = Stream.empty(); - final var incrementalPath = new ArrayList<>(pathSoFar); - incrementalPath.add(left.location()); - if (tiles[left.location().x() + 1][left.location().y()] == null || !isSideRoom(tiles[left.location().x() + 1][left.location().y()].location())) { - // this is not in front of a side room, - // we can stop here while other amphipods move - result = Stream.concat(result, Stream.of(Collections.unmodifiableList(incrementalPath))); - } - result = Stream.concat(result, iterateThroughPaths(amphipodType, left, incrementalPath)); - return result; - } - - Stream> streamRightPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { - final int x = current.location.x(); - final int y = current.location.y(); - final var right = tiles[x][y + 1]; - Stream> result = Stream.empty(); - final var incrementalPath = new ArrayList<>(pathSoFar); - incrementalPath.add(right.location()); - if (tiles[right.location().x() + 1][right.location().y()] == null || !isSideRoom(tiles[right.location().x() + 1][right.location().y()].location())) { - // this is not in front of a side room, - // we can stop here while other amphipods move - result = Stream.concat(result, Stream.of(Collections.unmodifiableList(incrementalPath))); - } - result = Stream.concat(result, iterateThroughPaths(amphipodType, right, incrementalPath)); - return result; - } - -// /** -// * Find all the paths an amphipod can take -// * -// * @param amphipodType the type of amphipod that is moving -// * @param current a tile through which the amphipod will take -// * @param pathSoFar the full path of the amphipod so far, *must* include _current_ -// * @return all the paths (start to finish) the amphipod can take -// */ -// @Deprecated -// List> getPaths(final AmphipodType amphipodType, final Tile current, final List pathSoFar) { -// final int x = current.location.x(); -// final int y = current.location.y(); -// final var up = tiles[x - 1][y]; -// final var down = tiles[x + 1][y]; -// final var left = tiles[x][y - 1]; -// final var right = tiles[x][y + 1]; -// -// final var result = new ArrayList>(); -// if (up != null && up.isVacant() && !pathSoFar.contains(up.location())) { -// // amphipod is in a side room -// if (isSideRoom(up.location()) && current.targetType == amphipodType) { -// // amphipod is in the back of the room in which it belongs, stop here -// return Collections.singletonList(pathSoFar); -// } -// final var incrementalPath = new ArrayList<>(pathSoFar); -// incrementalPath.add(up.location()); -// // whether "up" is the front of the room or the corridor outside the room, we have to keep moving -// result.addAll(getPaths(amphipodType, up, incrementalPath)); -// } -// if (down != null -// && down.isVacant() -// && !pathSoFar.contains(down.location()) -// // don't enter side room unless it is the ultimate destination -// && down.targetType == amphipodType) { -// // either entering a room or moving to the back of the room -// final var incrementalPath = new ArrayList<>(pathSoFar); -// incrementalPath.add(down.location()); -// if ((isCorridor(current.location()) && canEnterRoom(amphipodType, down)) || isSideRoom(current.location())) { -// // go as for back into the room as possible, don't just stop at the entrance -// result.addAll(getPaths(amphipodType, down, incrementalPath)); -// } -// } -// if (left != null && left.isVacant() && !pathSoFar.contains(left.location())) { -// final var incrementalPath = new ArrayList<>(pathSoFar); -// incrementalPath.add(left.location()); -// if (tiles[left.location().x() + 1][left.location().y()] == null || !isSideRoom(tiles[left.location().x() + 1][left.location().y()].location())) { -// // this is not in front of a side room, -// // we can stop here while other amphipods move -// result.add(Collections.unmodifiableList(incrementalPath)); -// } -// result.addAll(getPaths(amphipodType, left, incrementalPath)); -// } -// if (right != null && right.isVacant() && !pathSoFar.contains(right.location())) { -// final var incrementalPath = new ArrayList<>(pathSoFar); -// incrementalPath.add(right.location()); -// if (tiles[right.location().x() + 1][right.location().y()] == null || !isSideRoom(tiles[right.location().x() + 1][right.location().y()].location())) { -// // this is not in front of a side room, -// // we can stop here while other amphipods move -// result.add(Collections.unmodifiableList(incrementalPath)); -// } -// result.addAll(getPaths(amphipodType, right, incrementalPath)); -// } -// if (result.isEmpty() && pathSoFar.size() > 1) { -// // dead end, emit the path so far -// result.add(pathSoFar); -// } -// return Collections.unmodifiableList(result); -// } - - boolean canEnterRoom(final AmphipodType amphipodType, final Tile frontOfRoom) { - if (!isSideRoom(frontOfRoom.location())) { - throw new IllegalArgumentException("Not a side room: " + frontOfRoom); - } - if (frontOfRoom.targetType() != amphipodType) { - // this is not the destination room - return false; - } - // ensure all occupants have this as their destination - boolean hasOccupants = false; - for (var roomTile = tiles()[frontOfRoom.location().x() + 1][frontOfRoom.location().y()]; - roomTile != null; - roomTile = tiles()[roomTile.location().x() + 1][roomTile.location().y()]) { - if (roomTile.amphipodType() == null) { - if (hasOccupants) { - System.err.println("***There is a gap in the room***"); - return false; // there is a gap that shouldn't be here - } - continue; - } else { - hasOccupants = true; - } - if (!roomTile.hasTargetType()) { - return false; - } - } - return true; - } - - /** - * Estimate the energy required for all the amphipods to get to their side rooms. This strictly underestimates - * the amount of energy required. It assumes: each amphipod has an unobstructed path to their room, they only - * need to get to the front of the room, not all the way to the back, they do not need to take any detours to - * let other amphipods pass (e.g. they don't need to leave the room and then come back in). - * - * @return an underestimate of the energy required for the amphipods of the burrow to self-organise - */ - public int estimatedDistanceToSolution() { - if (estimatedDistanceCache.containsKey(this)) { - return estimatedDistanceCache.get(this); - } - int result = 0; - for (int i = tiles.length; --i >= 0; ) { - final var row = tiles[i]; - for (int j = row.length; --j >= 0; ) { - final var tile = row[j]; - if (tile != null && tile.amphipodType != null) { - final int horizontalDistance = - Math.abs(tile.amphipodType.destinationColumn - tile.location().y()); - int verticalDistance = 0; - if (horizontalDistance != 0) { - // get to the corridor - verticalDistance = tile.location().x() - 1; - // enter the side room - verticalDistance += 1; - } else if (isCorridor(tile.location())) { - // it's implied that horizontal distance is 0 - // we're right outside the target room - // enter the side room - verticalDistance = 1; - } - final int distance = verticalDistance + horizontalDistance; - result += distance * tile.amphipodType().energyPerStep; - } - } - } - estimatedDistanceCache.put(this, result); - return result; - } - - } - - protected record Point(int x, int y) { - - public Tile getTile(final Tile[][] tiles) { - return tiles[x()][y()]; - } - - public void setTile(final Tile[][] tiles, final Tile tile) { - if (tile.location().x() != x() || tile.location().y() != y()) { - throw new IllegalArgumentException("Tile and location do not match"); - } - tiles[x()][y()] = tile; - } - } - - public record Tile(Point location, AmphipodType targetType, AmphipodType amphipodType) { - - public Tile updateType(final AmphipodType newType) { - return new Tile(location, targetType, newType); - } - - public boolean isVacant() { - return amphipodType == null; - } - - public boolean hasTargetType() { - return Objects.equals(amphipodType(), targetType()); - } - - } - -// public int lwst(final Node start) { -// final var lowestCostToNode = new ConcurrentHashMap(); -// final var estimatedCostThroughNode = new ConcurrentHashMap(); -// final var openSet = new PriorityBlockingQueue(100000, Comparator.comparing(estimatedCostThroughNode::get)); -// -// // add the starting node, getting there is free -// lowestCostToNode.put(start, 0); -// estimatedCostThroughNode.put(start, start.estimatedDistanceToSolution()); -// openSet.add(start); -// -// final var executor = ForkJoinPool.commonPool(); -// final var stateModifiers = new LinkedBlockingDeque>(); -// final var complete = new AtomicBoolean(false); -// executor.execute(() -> { -// while (!complete.get()) { -// Thread.yield(); -// try { -// final var current = openSet.take(); -// if (current.isSolution()) { -// stateModifiers.addFirst(() -> lowestCostToNode.get(current)); -// } -// final var lowestCostToCurrent = lowestCostToNode.get(current); -// executor.execute(() -> current.getBranches().map(branchResult -> { -// final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); -// final var branchNode = branchResult.node(); -// final Supplier updater = () -> { -// if (tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE)) { -// // either we've never visited this node before, -// // or the last time we did, we took a more expensive route -// lowestCostToNode.put(branchNode, tentativeBranchCost); -// -// // update the cost through this branch -// // need to remove and re-add to get correct ordering in the open set -// estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); -// -// openSet.remove(branchNode); // O(n) -// openSet.add(branchNode); // O(log(n)) -// } -// return (Integer)null; -// }; -// return updater; -// }).forEach(stateModifiers::addLast)); -// } catch (InterruptedException e) { -// e.printStackTrace(); -// complete.set(true); -// Thread.currentThread().interrupt(); -// throw new RuntimeException(e.getMessage(), e); -// } -// } -// }); -// // process updates sequentially -// while (!complete.get()) { -// Thread.yield(); -// try { -// final var updater = stateModifiers.takeFirst(); -// final var cost = updater.get(); -// if (cost != null) { -// complete.set(true); -// return cost; -// } -// } catch (InterruptedException e) { -// e.printStackTrace(); -// complete.set(true); -// Thread.currentThread().interrupt(); -// throw new RuntimeException(e.getMessage(), e); -// } -// } -// throw new IllegalStateException("An error occurred"); -// } -// -// protected String id(Node node) { -// final var outputStream = new ByteArrayOutputStream(); -// outputStream.write(node.hashCode()); -// return Base64.getEncoder().encodeToString(outputStream.toByteArray()); -// } - - public int lowest(final Node start) { - final var lowestCostToNode = new ConcurrentHashMap(); - final var estimatedCostThroughNode = new ConcurrentHashMap(); - final var openSet = new PriorityBlockingQueue(100000, Comparator.comparing(estimatedCostThroughNode::get)); - - // add the starting node, getting there is free - lowestCostToNode.put(start, 0); - estimatedCostThroughNode.put(start, start.estimatedDistanceToSolution()); - openSet.add(start); - - while (!openSet.isEmpty()) { - if(Node.solutionCache.size() % 10000 == 0) { - System.err.println(openSet.size() + " branches left to check in the open set"); - System.err.println("Appraised the energy cost to " + lowestCostToNode.size() + " nodes."); - System.err.println("Estimated the energy cost through " + estimatedCostThroughNode.size() + " nodes."); - System.err.println("Lowest estimated cost so far: " + estimatedCostThroughNode.get(openSet.peek())); - } - final var current = openSet.poll(); // O(log(n)) - if (current.isSolution()) { - System.err.println("Found solution:\n" + current); - return lowestCostToNode.get(current); - } - final var lowestCostToCurrent = lowestCostToNode.get(current); - current.getBranches() - .parallel() - .filter(branchResult -> { - final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); - final var branchNode = branchResult.node(); - return tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE); - }) - .forEach(branchResult -> { - final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); - final var branchNode = branchResult.node(); - // either we've never visited this node before, - // or the last time we did, we took a more expensive route - lowestCostToNode.put(branchNode, tentativeBranchCost); - - // update the cost through this branch - // need to remove and re-add to get correct ordering in the open set -// openSet.remove(branchNode); // O(n) - openSet.removeIf(node -> node.equals(branchNode)); - estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); - openSet.add(branchNode); // O(log(n)) - }); -// current.getBranches().forEach(branchResult -> { -// final var tentativeBranchCost = lowestCostToCurrent + branchResult.cost(); -// final var branchNode = branchResult.node(); -// if (tentativeBranchCost < lowestCostToNode.getOrDefault(branchNode, Integer.MAX_VALUE)) { -// // either we've never visited this node before, -// // or the last time we did, we took a more expensive route -// lowestCostToNode.put(branchNode, tentativeBranchCost); -// -// // update the cost through this branch -// // need to remove and re-add to get correct ordering in the open set -// openSet.remove(branchNode); // O(n) -// estimatedCostThroughNode.put(branchNode, tentativeBranchCost + branchNode.estimatedDistanceToSolution()); -// openSet.add(branchNode); // O(log(n)) -// } -// }); - } - throw new IllegalStateException("Amphipods are gridlocked :-("); - } - - @Nested - public class NodeTest { - - @Test - public final void verifyEquality() { - // given - final var string = """ - ############# - #.....D.D.A.# - ###.#B#C#.### - #A#B#C#.# - ######### - """; - final var x = Node.createInitialNode(parseGrid(string.lines().toList())); - final var y = Node.createInitialNode(parseGrid(string.lines().toList())); - - // when - - // then - assertEquals(x.hashCode(), y.hashCode()); - assertEquals(x, y); - } - - @Test - public final void verifyBranchEquality() { - // given - final var string = """ - ############# - #.....D.D.A.# - ###.#B#C#.### - #A#B#C#.# - ######### - """; - final var original = Node.createInitialNode(parseGrid(string.lines().toList())); - - // when - final var x = original.branch(Collections.singletonList(new Move(new Point(2, 5), new Point(1, 2)))); - final var y = original.branch(Collections.singletonList(new Move(new Point(2, 5), new Point(1, 2)))); - - // then - assertEquals(x.hashCode(), y.hashCode()); - assertEquals(x, y); - } - - @Test - public final void verifyEstimatedDistanceIsZero() { - // given - final var string = """ - ############# - #...........# - ###A#B#C#D### - #A#B#C#D# - ######### - """; - final var initial = Node.createInitialNode(parseGrid(string.lines().toList())); - - // when - final var result = initial.estimatedDistanceToSolution(); - - // then - assertEquals(0, result); - } - - @Disabled - @Test - public final void verifyEstimationOrdering() { - // given - final var states = new String[]{ - """ - ############# - #...........# - ###B#C#B#D### - #A#D#C#A# - ######### - """, - """ - ############# - #...B.......# - ###B#C#.#D### - #A#D#C#A# - ######### - """, - """ - ############# - #...B.......# - ###B#.#C#D### - #A#D#C#A# - ######### - """, - """ - ############# - #.....D.....# - ###B#.#C#D### - #A#B#C#A# - ######### - """, - """ - ############# - #.....D.....# - ###.#B#C#D### - #A#B#C#A# - ######### - """, - """ - ############# - #.....D.D.A.# - ###.#B#C#.### - #A#B#C#.# - ######### - """, - """ - ############# - #.........A.# - ###.#B#C#D### - #A#B#C#D# - ######### - """, - """ - ############# - #...........# - ###A#B#C#D### - #A#B#C#D# - ######### - """ - }; - final var nodes = Arrays.stream(states) - .map(String::lines) - .map(Stream::toList) - .map(Day23.this::parseGrid) - .map(Node::createInitialNode) - .toList(); - - // when - - // then - for (int i = 1; i < nodes.size(); i++) { - final var previous = nodes.get(i - 1); - final var current = nodes.get(i); - assertTrue(previous.estimatedDistanceToSolution() >= current.estimatedDistanceToSolution(), - "Previous state has a lower estimated distance. Previous:\n" + previous + "\n(cost: " + previous.estimatedDistanceToSolution() + ")\nCurrent:\n" + current + "\n(cost: " + current.estimatedDistanceToSolution() + ")"); - } - } - } - - @Test - public final void part1() { - final var initial = Node.createInitialNode(parseGrid(getInput().toList())); - - System.out.println("Part 1: " + lowest(initial)); - } - - @Disabled - @Test - public final void part2() { - final var lines = getInput().collect(Collectors.toList()); - lines.add(3, " #D#B#A#C# "); - lines.add(3, " #D#C#B#A# "); - final var initial = Node.createInitialNode(parseGrid(lines)); - System.err.println("Initial state:\n" + initial); - - System.out.println("Part 2: " + lowest(initial)); - } - -} \ 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 100755 index 51eea06..0000000 --- a/src/test/java/com/macasaet/Day24.java +++ /dev/null @@ -1,284 +0,0 @@ -package com.macasaet; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import java.util.PrimitiveIterator; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * --- Day 24: Arithmetic Logic Unit --- - */ -public class Day24 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-24.txt"), - false); - } - - public static class ArithmeticLogicUnit { - public BigInteger getW() { - return w; - } - - public void setW(BigInteger w) { - this.w = w; - } - - public BigInteger getX() { - return x; - } - - public void setX(BigInteger x) { - this.x = x; - } - - public BigInteger getY() { - return y; - } - - public void setY(BigInteger y) { - this.y = y; - } - - public BigInteger getZ() { - return z; - } - - public void setZ(BigInteger z) { - this.z = z; - } - - private BigInteger w = BigInteger.ZERO, x = BigInteger.ZERO, y = BigInteger.ZERO, z = BigInteger.ZERO; - - public List getInstructions() { - return instructions; - } - - public void setInstructions(List instructions) { - this.instructions = instructions; - } - - private List instructions = new ArrayList<>(); - - public boolean isValid(final String modelNumber) { - final var iterator = modelNumber.chars().map(Character::getNumericValue).iterator(); - for (final var instruction : getInstructions()) { - instruction.evaluate(iterator); - } - return BigInteger.ZERO.equals(getZ()); - } - - public static ArithmeticLogicUnit parse(final Stream lines) { - final var result = new ArithmeticLogicUnit(); - final List instructions = lines.map(line -> { - final var components = line.split(" "); - - final Consumer resultSetter = switch (components[1]) { - case "w" -> result::setW; - case "x" -> result::setX; - case "y" -> result::setY; - case "z" -> result::setZ; - default -> throw new IllegalArgumentException("Invalid instruction, invalid l-value: " + line); - }; - final Supplier xSupplier = switch (components[1]) { - case "w" -> result::getW; - case "x" -> result::getX; - case "y" -> result::getY; - case "z" -> result::getZ; - default -> throw new IllegalArgumentException("Invalid instruction, invalid l-value: " + line); - }; - final Supplier ySupplier = components.length > 2 ? switch (components[2]) { - case "w" -> result::getW; - case "x" -> result::getX; - case "y" -> result::getY; - case "z" -> result::getZ; - default -> () -> new BigInteger(components[2]); - } : () -> { - throw new IllegalStateException(); - }; - return switch (components[0]) { - case "inp" -> new Input(resultSetter); - case "add" -> new Add(resultSetter, xSupplier, ySupplier); - case "mul" -> new Multiply(resultSetter, xSupplier, ySupplier); - case "div" -> new Divide(resultSetter, xSupplier, ySupplier); - case "mod" -> new Modulo(resultSetter, xSupplier, ySupplier); - case "eql" -> new Equals(resultSetter, xSupplier, ySupplier); - default -> throw new IllegalArgumentException("Invalid instruction: " + line); - }; - }).toList(); - result.setInstructions(instructions); - return result; - } - - public void reset() { - setW(BigInteger.ZERO); - setX(BigInteger.ZERO); - setY(BigInteger.ZERO); - setZ(BigInteger.ZERO); - } - } - - public interface Instruction { - void evaluate(PrimitiveIterator.OfInt input); - } - - public record Input(Consumer setter) implements Instruction { - - public void evaluate(PrimitiveIterator.OfInt input) { - setter.accept(BigInteger.valueOf(input.nextInt())); - } - } - - public record Add(Consumer resultSetter, Supplier xSupplier, - Supplier ySupplier) implements Instruction { - public void evaluate(PrimitiveIterator.OfInt _input) { - resultSetter.accept(xSupplier.get().add(ySupplier.get())); - } - } - - public record Multiply(Consumer resultSetter, Supplier xSupplier, - Supplier ySupplier) implements Instruction { - public void evaluate(PrimitiveIterator.OfInt _input) { - resultSetter.accept(xSupplier.get().multiply(ySupplier.get())); - } - } - - public record Divide(Consumer resultSetter, Supplier xSupplier, - Supplier ySupplier) implements Instruction { - public void evaluate(PrimitiveIterator.OfInt _input) { - resultSetter.accept(xSupplier.get().divide(ySupplier.get())); - } - } - - public record Modulo(Consumer resultSetter, Supplier xSupplier, - Supplier ySupplier) implements Instruction { - public void evaluate(PrimitiveIterator.OfInt _input) { - resultSetter.accept(xSupplier.get().mod(ySupplier.get())); - } - } - - public record Equals(Consumer resultSetter, Supplier xSupplier, - Supplier ySupplier) implements Instruction { - public void evaluate(PrimitiveIterator.OfInt _input) { - resultSetter.accept(xSupplier.get().equals(ySupplier.get()) ? BigInteger.ONE : BigInteger.ZERO); - } - } - - @Nested - public class ArithmeticLogicUnitTest { - @Disabled - @Test - public void testNegation() { - // given - final var input = """ - inp x - mul x -1 - """; - final var alu = ArithmeticLogicUnit.parse(input.lines()); - - // when - alu.isValid("7"); - - // then - assertEquals(-7, alu.getX()); - } - - @Disabled - @Test - public final void testThreeTimes() { - // given - final var input = """ - inp z - inp x - mul z 3 - eql z x - """; - final var alu = ArithmeticLogicUnit.parse(input.lines()); - - // when - alu.isValid("39"); - - // then - assertEquals(1, alu.getZ()); - } - - @Disabled - @Test - public final void testBinaryConversion() { - // given - final var input = """ - inp w - add z w - mod z 2 - div w 2 - add y w - mod y 2 - div w 2 - add x w - mod x 2 - div w 2 - mod w 2 - """; - final var alu = ArithmeticLogicUnit.parse(input.lines()); - - // when - alu.isValid("9"); - - // then - assertEquals(1, alu.getW()); - assertEquals(0, alu.getX()); - assertEquals(0, alu.getY()); - assertEquals(1, alu.getZ()); - } - - @Test - public final void testIsValid() { - // given - final var alu = ArithmeticLogicUnit.parse(getInput()); - - // when - final var result = alu.isValid("13579246899999"); - - // then - System.err.println("z=" + alu.getZ()); - } - } - - @Disabled - @Test - public final void part1() { - final var monad = getInput().toList(); - final var counter = new AtomicInteger(0); - final var result = Stream.iterate(new BigInteger("99999999999999"), previous -> previous.subtract(BigInteger.ONE)) - .parallel() - .filter(candidate -> { - final int count = counter.updateAndGet(previous -> previous + 1); - if(count % 10000 == 0) { - System.err.println("Testing: " + candidate); - } - final var alu = ArithmeticLogicUnit.parse(monad.stream()); - return alu.isValid(candidate.toString()); - }).findFirst(); - System.out.println("Part 1: " + result.orElseThrow()); - } - - @Disabled - @Test - public final void part2() { - - System.out.println("Part 2: " + null); - } - -} \ 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 100755 index d9e5335..0000000 --- a/src/test/java/com/macasaet/Day25.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.macasaet; - -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -/** - * - */ -public class Day25 { - - protected Stream getInput() { - return StreamSupport - .stream(new LineSpliterator("day-25.txt"), - false); - } - - public record Herd() { - - } - - public record SeaCucumber() { - - } - - public record OceanFloor(char[][] grid) { - public static OceanFloor parse(final Stream lines) { - final var list = lines.toList(); - final var grid = new char[list.size()][]; - for (int i = list.size(); --i >= 0; ) { - final var line = list.get(i); - grid[i] = new char[line.length()]; - for (int j = line.length(); --j >= 0; grid[i][j] = line.charAt(j)) ; - } - return new OceanFloor(grid); - } - - public OceanFloor step() { - return stepEast().stepSouth(); - } - - boolean isOccupied(final int x, final int y) { - return grid[x][y] != '.'; - } - - char[][] createBlankGrid() { - final char[][] result = new char[grid().length][]; - for (int i = grid().length; --i >= 0; ) { - final var originalRow = grid()[i]; - final var newRow = new char[originalRow.length]; - for (int j = originalRow.length; --j >= 0; newRow[j] = '.') ; - result[i] = newRow; - } - return result; - } - - OceanFloor stepEast() { - final char[][] copy = createBlankGrid(); - for (int i = grid().length; --i >= 0; ) { - final var originalRow = grid()[i]; - for (int j = originalRow.length; --j >= 0; ) { - final var nextIndex = (j + 1) % originalRow.length; - if (originalRow[j] == '>') { - if (!isOccupied(i, nextIndex)) { - copy[i][nextIndex] = '>'; - } else { - copy[i][j] = '>'; - } - } else if (originalRow[j] != '.') { - copy[i][j] = originalRow[j]; - } - } - } - return new OceanFloor(copy); - } - - OceanFloor stepSouth() { - final char[][] copy = createBlankGrid(); - for (int i = grid().length; --i >= 0; ) { - final var originalRow = grid()[i]; - final var nextIndex = (i + 1) % grid().length; - for (int j = originalRow.length; --j >= 0; ) { - if (originalRow[j] == 'v') { - if (!isOccupied(nextIndex, j)) { - copy[nextIndex][j] = 'v'; - } else { - copy[i][j] = 'v'; - } - } else if (originalRow[j] != '.') { - copy[i][j] = originalRow[j]; - } - } - } - return new OceanFloor(copy); - } - - public String toString() { - final var builder = new StringBuilder(); - for (final var row : grid()) { - builder.append(row).append('\n'); - } - return builder.toString(); - } - - public int hashCode() { - int result = 1; - for (final var row : grid()) { - result += 31 * result + Arrays.hashCode(row); - } - return result; - } - - public boolean equals(final Object o) { - if (o == null) { - return false; - } else if (this == o) { - return true; - } - try { - final OceanFloor other = (OceanFloor) o; - if (grid().length != other.grid().length) { - return false; - } - for (int i = grid.length; --i >= 0; ) { - final var mine = grid()[i]; - final var theirs = other.grid()[i]; - if (!Arrays.equals(mine, theirs)) { - return false; - } - } - return true; - } catch (final ClassCastException _cce) { - return false; - } - } - } - - @Test - public final void part1() { - var oceanFloor = OceanFloor.parse(getInput()); - for (int i = 1; ; i++) { - final var next = oceanFloor.step(); - if (next.equals(oceanFloor)) { - System.out.println("Part 1: " + i); - break; - } - oceanFloor = next; - } - } - - @Test - public final void part2() { - - System.out.println("Part 2: " + null); - } - -} \ No newline at end of file diff --git a/src/test/resources/sample/day-01.txt b/src/test/resources/sample/day-01.txt deleted file mode 100644 index 167e291..0000000 --- a/src/test/resources/sample/day-01.txt +++ /dev/null @@ -1,10 +0,0 @@ -199 -200 -208 -210 -200 -207 -240 -269 -260 -263 diff --git a/src/test/resources/sample/day-02.txt b/src/test/resources/sample/day-02.txt deleted file mode 100644 index b7172ac..0000000 --- a/src/test/resources/sample/day-02.txt +++ /dev/null @@ -1,6 +0,0 @@ -forward 5 -down 5 -forward 8 -up 3 -down 8 -forward 2 diff --git a/src/test/resources/sample/day-03.txt b/src/test/resources/sample/day-03.txt deleted file mode 100644 index a6366a8..0000000 --- a/src/test/resources/sample/day-03.txt +++ /dev/null @@ -1,12 +0,0 @@ -00100 -11110 -10110 -10111 -10101 -01111 -00111 -11100 -10000 -11001 -00010 -01010 diff --git a/src/test/resources/sample/day-04.txt b/src/test/resources/sample/day-04.txt deleted file mode 100644 index 669a51d..0000000 --- a/src/test/resources/sample/day-04.txt +++ /dev/null @@ -1,19 +0,0 @@ -7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1 - -22 13 17 11 0 - 8 2 23 4 24 -21 9 14 16 7 - 6 10 3 18 5 - 1 12 20 15 19 - - 3 15 0 2 22 - 9 18 13 17 5 -19 8 7 25 23 -20 11 10 24 4 -14 21 16 12 6 - -14 21 17 24 4 -10 16 15 9 19 -18 8 23 26 20 -22 11 13 6 5 - 2 0 12 3 7 diff --git a/src/test/resources/sample/day-05.txt b/src/test/resources/sample/day-05.txt deleted file mode 100644 index b258f68..0000000 --- a/src/test/resources/sample/day-05.txt +++ /dev/null @@ -1,10 +0,0 @@ -0,9 -> 5,9 -8,0 -> 0,8 -9,4 -> 3,4 -2,2 -> 2,1 -7,0 -> 7,4 -6,4 -> 2,0 -0,9 -> 2,9 -3,4 -> 1,4 -0,0 -> 8,8 -5,5 -> 8,2 diff --git a/src/test/resources/sample/day-06.txt b/src/test/resources/sample/day-06.txt deleted file mode 100644 index 55129f1..0000000 --- a/src/test/resources/sample/day-06.txt +++ /dev/null @@ -1 +0,0 @@ -3,4,3,1,2 diff --git a/src/test/resources/sample/day-07.txt b/src/test/resources/sample/day-07.txt deleted file mode 100644 index 18bd32a..0000000 --- a/src/test/resources/sample/day-07.txt +++ /dev/null @@ -1 +0,0 @@ -16,1,2,0,4,2,7,1,2,14 diff --git a/src/test/resources/sample/day-08.txt b/src/test/resources/sample/day-08.txt deleted file mode 100644 index c9f629b..0000000 --- a/src/test/resources/sample/day-08.txt +++ /dev/null @@ -1,10 +0,0 @@ -be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe -edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc -fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg -fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb -aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea -fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb -dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe -bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef -egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb -gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce diff --git a/src/test/resources/sample/day-09.txt b/src/test/resources/sample/day-09.txt deleted file mode 100644 index 6dee4a4..0000000 --- a/src/test/resources/sample/day-09.txt +++ /dev/null @@ -1,5 +0,0 @@ -2199943210 -3987894921 -9856789892 -8767896789 -9899965678 diff --git a/src/test/resources/sample/day-10.txt b/src/test/resources/sample/day-10.txt deleted file mode 100644 index b1518d9..0000000 --- a/src/test/resources/sample/day-10.txt +++ /dev/null @@ -1,10 +0,0 @@ -[({(<(())[]>[[{[]{<()<>> -[(()[<>])]({[<{<<[]>>( -{([(<{}[<>[]}>{[]{[(<()> -(((({<>}<{<{<>}{[]{[]{} -[[<[([]))<([[{}[[()]]] -[{[{({}]{}}([{[{{{}}([] -{<[[]]>}<{[{[{[]{()[[[] -[<(<(<(<{}))><([]([]() -<{([([[(<>()){}]>(<<{{ -<{([{{}}[<[[[<>{}]]]>[]] diff --git a/src/test/resources/sample/day-11.txt b/src/test/resources/sample/day-11.txt deleted file mode 100644 index 03743f6..0000000 --- a/src/test/resources/sample/day-11.txt +++ /dev/null @@ -1,10 +0,0 @@ -5483143223 -2745854711 -5264556173 -6141336146 -6357385478 -4167524645 -2176841721 -6882881134 -4846848554 -5283751526 diff --git a/src/test/resources/sample/day-12.txt b/src/test/resources/sample/day-12.txt deleted file mode 100644 index 6fd8c41..0000000 --- a/src/test/resources/sample/day-12.txt +++ /dev/null @@ -1,7 +0,0 @@ -start-A -start-b -A-c -A-b -b-d -A-end -b-end diff --git a/src/test/resources/sample/day-13.txt b/src/test/resources/sample/day-13.txt deleted file mode 100644 index 282114c..0000000 --- a/src/test/resources/sample/day-13.txt +++ /dev/null @@ -1,21 +0,0 @@ -6,10 -0,14 -9,10 -0,3 -10,4 -4,11 -6,0 -6,12 -4,1 -0,13 -10,12 -3,4 -3,0 -8,4 -1,10 -2,14 -8,10 -9,0 - -fold along y=7 -fold along x=5 diff --git a/src/test/resources/sample/day-14.txt b/src/test/resources/sample/day-14.txt deleted file mode 100644 index b5594dd..0000000 --- a/src/test/resources/sample/day-14.txt +++ /dev/null @@ -1,18 +0,0 @@ -NNCB - -CH -> B -HH -> N -CB -> H -NH -> C -HB -> C -HC -> B -HN -> C -NN -> C -BH -> H -NC -> B -NB -> B -BN -> B -BB -> N -BC -> B -CC -> N -CN -> C diff --git a/src/test/resources/sample/day-15.txt b/src/test/resources/sample/day-15.txt deleted file mode 100644 index ab80887..0000000 --- a/src/test/resources/sample/day-15.txt +++ /dev/null @@ -1,10 +0,0 @@ -1163751742 -1381373672 -2136511328 -3694931569 -7463417111 -1319128137 -1359912421 -3125421639 -1293138521 -2311944581 diff --git a/src/test/resources/sample/day-16.txt b/src/test/resources/sample/day-16.txt deleted file mode 100644 index 3f0eda1..0000000 --- a/src/test/resources/sample/day-16.txt +++ /dev/null @@ -1 +0,0 @@ -D2FE28 diff --git a/src/test/resources/sample/day-17.txt b/src/test/resources/sample/day-17.txt deleted file mode 100644 index a07e02d..0000000 --- a/src/test/resources/sample/day-17.txt +++ /dev/null @@ -1 +0,0 @@ -target area: x=20..30, y=-10..-5 diff --git a/src/test/resources/sample/day-18.txt b/src/test/resources/sample/day-18.txt deleted file mode 100644 index 1368dc4..0000000 --- a/src/test/resources/sample/day-18.txt +++ /dev/null @@ -1,10 +0,0 @@ -[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]] -[[[5,[2,8]],4],[5,[[9,9],0]]] -[6,[[[6,2],[5,6]],[[7,6],[4,7]]]] -[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]] -[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]] -[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]] -[[[[5,4],[7,7]],8],[[8,3],8]] -[[9,3],[[9,9],[6,[4,9]]]] -[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]] -[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]] diff --git a/src/test/resources/sample/day-19.txt b/src/test/resources/sample/day-19.txt deleted file mode 100644 index 4e496e9..0000000 --- a/src/test/resources/sample/day-19.txt +++ /dev/null @@ -1,136 +0,0 @@ ---- scanner 0 --- -404,-588,-901 -528,-643,409 --838,591,734 -390,-675,-793 --537,-823,-458 --485,-357,347 --345,-311,381 --661,-816,-575 --876,649,763 --618,-824,-621 -553,345,-567 -474,580,667 --447,-329,318 --584,868,-557 -544,-627,-890 -564,392,-477 -455,729,728 --892,524,684 --689,845,-530 -423,-701,434 -7,-33,-71 -630,319,-379 -443,580,662 --789,900,-551 -459,-707,401 - ---- scanner 1 --- -686,422,578 -605,423,415 -515,917,-361 --336,658,858 -95,138,22 --476,619,847 --340,-569,-846 -567,-361,727 --460,603,-452 -669,-402,600 -729,430,532 --500,-761,534 --322,571,750 --466,-666,-811 --429,-592,574 --355,545,-477 -703,-491,-529 --328,-685,520 -413,935,-424 --391,539,-444 -586,-435,557 --364,-763,-893 -807,-499,-711 -755,-354,-619 -553,889,-390 - ---- scanner 2 --- -649,640,665 -682,-795,504 --784,533,-524 --644,584,-595 --588,-843,648 --30,6,44 --674,560,763 -500,723,-460 -609,671,-379 --555,-800,653 --675,-892,-343 -697,-426,-610 -578,704,681 -493,664,-388 --671,-858,530 --667,343,800 -571,-461,-707 --138,-166,112 --889,563,-600 -646,-828,498 -640,759,510 --630,509,768 --681,-892,-333 -673,-379,-804 --742,-814,-386 -577,-820,562 - ---- scanner 3 --- --589,542,597 -605,-692,669 --500,565,-823 --660,373,557 --458,-679,-417 --488,449,543 --626,468,-788 -338,-750,-386 -528,-832,-391 -562,-778,733 --938,-730,414 -543,643,-506 --524,371,-870 -407,773,750 --104,29,83 -378,-903,-323 --778,-728,485 -426,699,580 --438,-605,-362 --469,-447,-387 -509,732,623 -647,635,-688 --868,-804,481 -614,-800,639 -595,780,-596 - ---- scanner 4 --- -727,592,562 --293,-554,779 -441,611,-461 --714,465,-776 --743,427,-804 --660,-479,-426 -832,-632,460 -927,-485,-438 -408,393,-506 -466,436,-512 -110,16,151 --258,-428,682 --393,719,612 --211,-452,876 -808,-476,-593 --575,615,604 --485,667,467 --680,325,-822 --627,-443,-432 -872,-547,-609 -833,512,582 -807,604,487 -839,-516,451 -891,-625,532 --652,-548,-490 -30,-46,-14 diff --git a/src/test/resources/sample/day-20.txt b/src/test/resources/sample/day-20.txt deleted file mode 100644 index 8fa4bd4..0000000 --- a/src/test/resources/sample/day-20.txt +++ /dev/null @@ -1,7 +0,0 @@ -..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..# - -#..#. -#.... -##..# -..#.. -..### diff --git a/src/test/resources/sample/day-21.txt b/src/test/resources/sample/day-21.txt deleted file mode 100644 index 3f69194..0000000 --- a/src/test/resources/sample/day-21.txt +++ /dev/null @@ -1,2 +0,0 @@ -Player 1 starting position: 4 -Player 2 starting position: 8 diff --git a/src/test/resources/sample/day-22.txt b/src/test/resources/sample/day-22.txt deleted file mode 100644 index 2790bed..0000000 --- a/src/test/resources/sample/day-22.txt +++ /dev/null @@ -1,60 +0,0 @@ -on x=-5..47,y=-31..22,z=-19..33 -on x=-44..5,y=-27..21,z=-14..35 -on x=-49..-1,y=-11..42,z=-10..38 -on x=-20..34,y=-40..6,z=-44..1 -off x=26..39,y=40..50,z=-2..11 -on x=-41..5,y=-41..6,z=-36..8 -off x=-43..-33,y=-45..-28,z=7..25 -on x=-33..15,y=-32..19,z=-34..11 -off x=35..47,y=-46..-34,z=-11..5 -on x=-14..36,y=-6..44,z=-16..29 -on x=-57795..-6158,y=29564..72030,z=20435..90618 -on x=36731..105352,y=-21140..28532,z=16094..90401 -on x=30999..107136,y=-53464..15513,z=8553..71215 -on x=13528..83982,y=-99403..-27377,z=-24141..23996 -on x=-72682..-12347,y=18159..111354,z=7391..80950 -on x=-1060..80757,y=-65301..-20884,z=-103788..-16709 -on x=-83015..-9461,y=-72160..-8347,z=-81239..-26856 -on x=-52752..22273,y=-49450..9096,z=54442..119054 -on x=-29982..40483,y=-108474..-28371,z=-24328..38471 -on x=-4958..62750,y=40422..118853,z=-7672..65583 -on x=55694..108686,y=-43367..46958,z=-26781..48729 -on x=-98497..-18186,y=-63569..3412,z=1232..88485 -on x=-726..56291,y=-62629..13224,z=18033..85226 -on x=-110886..-34664,y=-81338..-8658,z=8914..63723 -on x=-55829..24974,y=-16897..54165,z=-121762..-28058 -on x=-65152..-11147,y=22489..91432,z=-58782..1780 -on x=-120100..-32970,y=-46592..27473,z=-11695..61039 -on x=-18631..37533,y=-124565..-50804,z=-35667..28308 -on x=-57817..18248,y=49321..117703,z=5745..55881 -on x=14781..98692,y=-1341..70827,z=15753..70151 -on x=-34419..55919,y=-19626..40991,z=39015..114138 -on x=-60785..11593,y=-56135..2999,z=-95368..-26915 -on x=-32178..58085,y=17647..101866,z=-91405..-8878 -on x=-53655..12091,y=50097..105568,z=-75335..-4862 -on x=-111166..-40997,y=-71714..2688,z=5609..50954 -on x=-16602..70118,y=-98693..-44401,z=5197..76897 -on x=16383..101554,y=4615..83635,z=-44907..18747 -off x=-95822..-15171,y=-19987..48940,z=10804..104439 -on x=-89813..-14614,y=16069..88491,z=-3297..45228 -on x=41075..99376,y=-20427..49978,z=-52012..13762 -on x=-21330..50085,y=-17944..62733,z=-112280..-30197 -on x=-16478..35915,y=36008..118594,z=-7885..47086 -off x=-98156..-27851,y=-49952..43171,z=-99005..-8456 -off x=2032..69770,y=-71013..4824,z=7471..94418 -on x=43670..120875,y=-42068..12382,z=-24787..38892 -off x=37514..111226,y=-45862..25743,z=-16714..54663 -off x=25699..97951,y=-30668..59918,z=-15349..69697 -off x=-44271..17935,y=-9516..60759,z=49131..112598 -on x=-61695..-5813,y=40978..94975,z=8655..80240 -off x=-101086..-9439,y=-7088..67543,z=33935..83858 -off x=18020..114017,y=-48931..32606,z=21474..89843 -off x=-77139..10506,y=-89994..-18797,z=-80..59318 -off x=8476..79288,y=-75520..11602,z=-96624..-24783 -on x=-47488..-1262,y=24338..100707,z=16292..72967 -off x=-84341..13987,y=2429..92914,z=-90671..-1318 -off x=-37810..49457,y=-71013..-7894,z=-105357..-13188 -off x=-27365..46395,y=31009..98017,z=15428..76570 -off x=-70369..-16548,y=22648..78696,z=-1892..86821 -on x=-53470..21291,y=-120233..-33476,z=-44150..38147 -off x=-93533..-4276,y=-16170..68771,z=-104985..-24507 \ No newline at end of file diff --git a/src/test/resources/sample/day-23.txt b/src/test/resources/sample/day-23.txt deleted file mode 100644 index 6a7120d..0000000 --- a/src/test/resources/sample/day-23.txt +++ /dev/null @@ -1,5 +0,0 @@ -############# -#...........# -###B#C#B#D### - #A#D#C#A# - ######### diff --git a/src/test/resources/sample/day-24.txt b/src/test/resources/sample/day-24.txt deleted file mode 100755 index a41747d..0000000 --- a/src/test/resources/sample/day-24.txt +++ /dev/null @@ -1,2 +0,0 @@ -inp x -mul x -1 diff --git a/src/test/resources/sample/day-25.txt b/src/test/resources/sample/day-25.txt deleted file mode 100755 index 0f3c0cd..0000000 --- a/src/test/resources/sample/day-25.txt +++ /dev/null @@ -1,9 +0,0 @@ -v...>>.vv> -.vv>>.vv.. ->>.>v>...v ->>v>>.>.v. -v>v.vv.v.. ->.>>..v... -.vv..>.>v. -v.v..>>v.v -....v..v.> From e6d94df73ee37862d75eaea0e3821173fdf7934f Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Wed, 30 Nov 2022 21:28:53 -0800 Subject: [PATCH 26/44] Day 1 --- .github/workflows/ci.yml | 2 +- src/test/java/com/macasaet/Day01.java | 63 ++++++++++++++++++++------- src/test/resources/sample/day-01.txt | 14 ++++++ 3 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 src/test/resources/sample/day-01.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e789d7..007257b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: distribution: 'temurin' java-version: '19' check-latest: true - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: ~/.m2 key: m2-${{ runner.os }}-19-${{ hashFiles('**/pom.xml') }} diff --git a/src/test/java/com/macasaet/Day01.java b/src/test/java/com/macasaet/Day01.java index 7af31fc..d70e9ed 100644 --- a/src/test/java/com/macasaet/Day01.java +++ b/src/test/java/com/macasaet/Day01.java @@ -1,43 +1,76 @@ package com.macasaet; -import org.junit.jupiter.api.Disabled; 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.stream.StreamSupport; /** - * --- Day 1: --- + * --- Day 1: Calorie Counting --- */ public class Day01 { - /** - * - * - * @return - */ - protected List getInput() { + protected Iterator getInput() { return StreamSupport .stream(new LineSpliterator("day-01.txt"), false) - .collect(ArrayList::new, List::add, List::addAll); + .iterator(); + } + + protected List getElves() { + var calories = new ArrayList(); + final var elves = new ArrayList(); + for (final var i = getInput(); i.hasNext(); ) { + final var line = i.next(); + if (line.isBlank()) { + elves.add(new Elf(Collections.unmodifiableList(calories))); + calories = new ArrayList<>(); + } else { + calories.add(new BigInteger(line.strip())); + } + } + if (!calories.isEmpty()) { + elves.add(new Elf(Collections.unmodifiableList(calories))); + } + return Collections.unmodifiableList(elves); } - @Disabled @Test public final void part1() { - final var list = getInput(); + final var elves = getElves(); + final var elf = elves.stream() + .max(Comparator.comparing(Elf::totalCaloriesCarried)) + .get(); - System.out.println("Part 1: " + null); + System.out.println("Part 1: " + elf.totalCaloriesCarried()); } - @Disabled @Test public final void part2() { - final var list = getInput(); + final var elves = getElves(); + final var list = elves.stream() + .sorted(Comparator.comparing(Elf::totalCaloriesCarried).reversed()) + .toList(); - System.out.println("Part 2: " + null); + 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/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 From 64274d9e645cccfe5077dc496a11d57fb6eedc5d Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Thu, 1 Dec 2022 21:34:48 -0800 Subject: [PATCH 27/44] Day 2 --- src/test/java/com/macasaet/Day02.java | 177 ++++++++++++++++++++++++++ src/test/resources/sample/day-02.txt | 3 + 2 files changed, 180 insertions(+) create mode 100644 src/test/java/com/macasaet/Day02.java create mode 100644 src/test/resources/sample/day-02.txt diff --git a/src/test/java/com/macasaet/Day02.java b/src/test/java/com/macasaet/Day02.java new file mode 100644 index 0000000..875f433 --- /dev/null +++ b/src/test/java/com/macasaet/Day02.java @@ -0,0 +1,177 @@ +package com.macasaet; + +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 { + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-02.txt"), + false) + .map(line -> { + final var components = line.strip().split(" "); + return new Round(Shape.forChar(components[0].charAt(0)), + Shape.forChar(components[1].charAt(0)), + ResponseStrategy.forChar(components[1].charAt(0))); + }); + } + + @Test + public final void part1() { + final var result = getInput().mapToInt(Round::naiveScore).sum(); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var result = getInput().mapToInt(Round::score).sum(); + + System.out.println("Part 2: " + result); + } + + /** + * A shape that a contestant can play in a round + */ + public enum Shape { + Rock { + public int score() { + return 1; + } + + public Shape beatenBy() { + return Paper; + } + + public 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) { + switch (c) { + case 'X': + case 'A': + return Shape.Rock; + case 'Y': + case 'B': + return Shape.Paper; + case 'Z': + case 'C': + return Shape.Scissors; + } + throw new IllegalArgumentException(); + } + + /** + * @return the inherent value of this shape + */ + public abstract int score(); + + /** + * @return the shape that beats this one + */ + public abstract Shape beatenBy(); + + /** + * @return the shape this one beats + */ + public abstract Shape beats(); + } + + /** + * 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) { + switch (c) { + case 'X': + return Lose; + case 'Y': + return Draw; + case 'Z': + return Win; + } + throw new IllegalArgumentException(); + } + + 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(); + } + + /** + * @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/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 From d2f3c99dff39e387d8ce6c6d198c4ac321092581 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Thu, 1 Dec 2022 22:29:30 -0800 Subject: [PATCH 28/44] Day 2 - simplify switch statements --- src/test/java/com/macasaet/Day02.java | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/test/java/com/macasaet/Day02.java b/src/test/java/com/macasaet/Day02.java index 875f433..424b241 100644 --- a/src/test/java/com/macasaet/Day02.java +++ b/src/test/java/com/macasaet/Day02.java @@ -83,18 +83,19 @@ public Shape beats() { }; public static Shape forChar(final int c) { - switch (c) { + return switch (c) { case 'X': case 'A': - return Shape.Rock; + yield Shape.Rock; case 'Y': case 'B': - return Shape.Paper; + yield Shape.Paper; case 'Z': case 'C': - return Shape.Scissors; - } - throw new IllegalArgumentException(); + yield Shape.Scissors; + default: + throw new IllegalArgumentException("Invalid shape: " + c); + }; } /** @@ -134,15 +135,12 @@ public Shape respond(Shape opponent) { }; public static ResponseStrategy forChar(final char c) { - switch (c) { - case 'X': - return Lose; - case 'Y': - return Draw; - case 'Z': - return Win; - } - throw new IllegalArgumentException(); + return switch (c) { + case 'X' -> Lose; + case 'Y' -> Draw; + case 'Z' -> Win; + default -> throw new IllegalArgumentException("Invalid strategy: " + c); + }; } public abstract Shape respond(final Shape opponent); From 67f7de6ee5c7520964c9b19b6daed7d3fc3d49ab Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Fri, 2 Dec 2022 21:31:41 -0800 Subject: [PATCH 29/44] Day 3 --- src/test/java/com/macasaet/Day03.java | 117 ++++++++++++++++++++++++++ src/test/resources/sample/day-03.txt | 6 ++ 2 files changed, 123 insertions(+) create mode 100644 src/test/java/com/macasaet/Day03.java create mode 100644 src/test/resources/sample/day-03.txt diff --git a/src/test/java/com/macasaet/Day03.java b/src/test/java/com/macasaet/Day03.java new file mode 100644 index 0000000..af6e98a --- /dev/null +++ b/src/test/java/com/macasaet/Day03.java @@ -0,0 +1,117 @@ +package com.macasaet; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.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 { + + 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/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 From 21710bbe5a42786bf712c4f69139e48ece87e9c0 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sat, 3 Dec 2022 21:17:43 -0800 Subject: [PATCH 30/44] Day 4 --- src/test/java/com/macasaet/Day04.java | 69 +++++++++++++++++++++++++++ src/test/resources/sample/day-04.txt | 6 +++ 2 files changed, 75 insertions(+) create mode 100644 src/test/java/com/macasaet/Day04.java create mode 100644 src/test/resources/sample/day-04.txt diff --git a/src/test/java/com/macasaet/Day04.java b/src/test/java/com/macasaet/Day04.java new file mode 100644 index 0000000..8b3d13d --- /dev/null +++ b/src/test/java/com/macasaet/Day04.java @@ -0,0 +1,69 @@ +package com.macasaet; + +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 { + + /** + * 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/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 From bf35b0ff7c2174c138a4d6f2f50595cbc9eab975 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Tue, 6 Dec 2022 22:15:24 -0800 Subject: [PATCH 31/44] Days 5-7 --- src/test/java/com/macasaet/Day05.java | 139 +++++++++++++++ src/test/java/com/macasaet/Day06.java | 55 ++++++ src/test/java/com/macasaet/Day07.java | 241 ++++++++++++++++++++++++++ src/test/resources/sample/day-05.txt | 9 + src/test/resources/sample/day-06.txt | 1 + src/test/resources/sample/day-07.txt | 23 +++ 6 files changed, 468 insertions(+) create mode 100644 src/test/java/com/macasaet/Day05.java create mode 100644 src/test/java/com/macasaet/Day06.java create mode 100644 src/test/java/com/macasaet/Day07.java create mode 100644 src/test/resources/sample/day-05.txt create mode 100644 src/test/resources/sample/day-06.txt create mode 100644 src/test/resources/sample/day-07.txt diff --git a/src/test/java/com/macasaet/Day05.java b/src/test/java/com/macasaet/Day05.java new file mode 100644 index 0000000..97c43c4 --- /dev/null +++ b/src/test/java/com/macasaet/Day05.java @@ -0,0 +1,139 @@ +package com.macasaet; + +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 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; + } + } + } 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; + } + } + } 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); + + } + +} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day06.java b/src/test/java/com/macasaet/Day06.java new file mode 100644 index 0000000..769fb1b --- /dev/null +++ b/src/test/java/com/macasaet/Day06.java @@ -0,0 +1,55 @@ +package com.macasaet; + +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * --- Day 6: --- + * https://adventofcode.com/2022/day/6 + */ +public class Day06 { + + 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(set.size() >= 14) { + final var result = i; + System.out.println("Part 2: " + result); + return; + } + } + 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 new file mode 100644 index 0000000..5aee1bc --- /dev/null +++ b/src/test/java/com/macasaet/Day07.java @@ -0,0 +1,241 @@ +package com.macasaet; + +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 { + + static class Session { + private final Directory root = new Directory("/", new HashMap<>()); + private Directory workingDirectory = root; + private final Map parentMap = new HashMap<>(); + } + + static abstract class File { + abstract int size(); + } + + static class Directory extends File { + private final String name; + private final Map files; + + public Directory(String name, Map files) { + this.name = name; + this.files = files; + } + + int size() { + int result = 0; + for(final var file : files.values()) { + result += file.size(); + } + return result; + } + + public String toString() { + return "Directory{" + + "name='" + name + '\'' + + '}'; + } + + final Set findDirectoriesSmallerThan(final int maxSize) { + final var result = new HashSet(); + for(final var file : files.values()) { + try { + final var directory = (Directory)file; + result.addAll(directory.findDirectoriesSmallerThan(maxSize)); + } catch(final ClassCastException ignored) { + } + } + if(size() < maxSize) { // FIXME duplicated traversal + result.add(this); + } + return Collections.unmodifiableSet(result); + } + final Set findDirectoriesLargerThan(final int minSize) { + final var result = new HashSet(); + if(size() >= minSize) { + for (final var file : files.values()) { // FIXME duplicated traversal + try { + final var directory = (Directory) file; + result.addAll(directory.findDirectoriesLargerThan(minSize)); + } catch (final ClassCastException ignored) { + } + } + result.add(this); + } + return Collections.unmodifiableSet(result); + } + } + + static class Leaf extends File { + private final String name; + private final int size; + + public Leaf(String name, int size) { + this.name = name; + this.size = size; + } + + int size() { + return size; + } + + @Override + public String toString() { + return "Leaf{" + + "name='" + name + '\'' + + ", size=" + size + + '}'; + } + } + + static abstract class Line { + static Line parse(final String line) { + if(line.startsWith("$")) { + return Command.parse(line); + } + return Output.parse(line); + } + abstract void execute(Session session); + } + + static abstract class Command extends Line { + static Command parse(final String line) { + if(line.startsWith("$ cd")) { + return ChangeDirectory.parse(line); + } + return ListContents.parse(line); + } + } + + static class ListContents extends Command { + void execute(final Session session) { + } + static Command parse(final String ignored) { + return new ListContents(); + } + } + + static class ChangeDirectory extends Command { + private final String argument; + + public ChangeDirectory(String argument) { + this.argument = argument; + } + + void execute(Session session) { + if("..".equals(argument)) { + final var parent = session.parentMap.get(session.workingDirectory); + if(parent == null) { + throw new IllegalArgumentException("Working directory has no parent: " + session.workingDirectory); + } + session.workingDirectory = parent; + } else if( "/".equals(argument)) { + session.workingDirectory = session.root; + } else { + final var target = (Directory) session.workingDirectory.files.get(argument); + if(target == null) { + throw new IllegalArgumentException("No directory named \"" + argument + "\" inside \"" + session.workingDirectory + "\""); + } + session.workingDirectory = target; + } + } + + static ChangeDirectory parse(final String line) { + return new ChangeDirectory(line.split(" ")[2]); + } + } + + static abstract class Output extends Line { + static Output parse(final String line) { + if(line.startsWith("dir")) { + return DirectoryListing.parse(line); + } + 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/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 From a685df3bf0cd69b51be96c592122326a717ced63 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Wed, 7 Dec 2022 21:38:18 -0800 Subject: [PATCH 32/44] Day 8 --- src/test/java/com/macasaet/Day08.java | 161 ++++++++++++++++++++++++++ src/test/resources/sample/day-08.txt | 5 + 2 files changed, 166 insertions(+) create mode 100644 src/test/java/com/macasaet/Day08.java create mode 100644 src/test/resources/sample/day-08.txt diff --git a/src/test/java/com/macasaet/Day08.java b/src/test/java/com/macasaet/Day08.java new file mode 100644 index 0000000..98c498e --- /dev/null +++ b/src/test/java/com/macasaet/Day08.java @@ -0,0 +1,161 @@ +package com.macasaet; + +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 { + + 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++; + } + } + } + return result; + } + + 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; + } + } + int southScore = 0; + for(int i = x + 1; i < grid().length; i++) { + final var height = grid()[i][y]; + southScore += 1; + if(height >= treeHeight) { + break; + } + } + 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; + } + + 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; + } + + 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; + } + + private boolean isObstructedFromTheWest(int x, int y, int treeHeight) { + for(int j = y; --j >= 0; ) { + if(grid()[x][j] >= treeHeight) { + return true; + } + } + return false; + } + + 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; + } + + private boolean isObstructedFromTheNorth(int x, int y, int treeHeight) { + for(int i = x; --i >= 0; ) { + if(grid()[i][y] >= treeHeight) { + return true; + } + } + return false; + } + } + + protected Forest getInput() { + final var list = StreamSupport + .stream(new LineSpliterator("day-08.txt"), + false) + .map(line -> { + final var chars = line.toCharArray(); + final var row = new int[chars.length]; + for(int i = chars.length; --i >= 0; row[i] = chars[i] - '0'); + return row; + }) + .collect(Collectors.toList()); + final var grid = new int[list.size()][]; + for(int i = list.size(); --i >= 0; grid[i] = list.get(i)); + return new Forest(grid); + } + + @Test + public final void part1() { + final var 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/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 From bfe31dcc95148210198d9a1b0945742d7548a583 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Thu, 8 Dec 2022 22:09:50 -0800 Subject: [PATCH 33/44] Day 9 --- src/test/java/com/macasaet/Day09.java | 170 ++++++++++++++++++++++++++ src/test/resources/sample/day-09.txt | 8 ++ 2 files changed, 178 insertions(+) create mode 100644 src/test/java/com/macasaet/Day09.java create mode 100644 src/test/resources/sample/day-09.txt diff --git a/src/test/java/com/macasaet/Day09.java b/src/test/java/com/macasaet/Day09.java new file mode 100644 index 0000000..f334501 --- /dev/null +++ b/src/test/java/com/macasaet/Day09.java @@ -0,0 +1,170 @@ +package com.macasaet; + +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 { + + 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); + // NOT 7017 + } + + @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/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 From 2f0370287de6721e306714fabc716492e129b0d0 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Fri, 9 Dec 2022 22:18:23 -0800 Subject: [PATCH 34/44] Day 10 --- src/test/java/com/macasaet/Day09.java | 1 - src/test/java/com/macasaet/Day10.java | 154 ++++++++++++++++++++++++++ src/test/resources/sample/day-10.txt | 146 ++++++++++++++++++++++++ 3 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/macasaet/Day10.java create mode 100644 src/test/resources/sample/day-10.txt diff --git a/src/test/java/com/macasaet/Day09.java b/src/test/java/com/macasaet/Day09.java index f334501..b6bcb00 100644 --- a/src/test/java/com/macasaet/Day09.java +++ b/src/test/java/com/macasaet/Day09.java @@ -156,7 +156,6 @@ public final void part1() { getInput().forEach(rope::process); final var result = rope.countVisited(); System.out.println("Part 1: " + result); - // NOT 7017 } @Test diff --git a/src/test/java/com/macasaet/Day10.java b/src/test/java/com/macasaet/Day10.java new file mode 100644 index 0000000..2d0d47e --- /dev/null +++ b/src/test/java/com/macasaet/Day10.java @@ -0,0 +1,154 @@ +package com.macasaet; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * --- Day 10: Cathode-Ray Tube --- + * https://adventofcode.com/2022/day/10 + */ +public class Day10 { + + public enum Instruction { + noop { + public int cycles() { + return 0; + } + }, + addx { + public int cycles() { + return 2; + } + }; + + public abstract int cycles(); + + public static Instruction parse(final String string) { + return Instruction.valueOf(string); + } + } + + public record Operation(Instruction instruction, Integer argument) { + public List execute(int cycle, int register) { + return switch (instruction()) { + case noop -> Collections.singletonList(new CycleSnapshot(cycle + 1, register)); + case addx -> + List.of(new CycleSnapshot(cycle + 1, register), + new CycleSnapshot(cycle + 2, register + argument)); + }; + } + + public static 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); + } + } + + 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); + } + + public List execute(final Operation operation) { + final var result = operation.execute(cycle, register); + final var last = result.get(result.size() - 1); + cycle = last.cycle(); + register = last.register(); + return result; + } + } + + public static class Display { + final char[][] pixels = new char[6][]; + + { + for(int i = pixels.length; --i >= 0; pixels[i] = new char[40]); + } + + public void update(final CycleSnapshot snapshot) { + final var pixelIndex = snapshot.cycle() - 1; + final var spritePositions = + Arrays.asList(snapshot.register() - 1, snapshot.register(), snapshot.register() + 1); + final int row = pixelIndex / 40; + final int column = pixelIndex % 40; + if(row >= pixels.length) { + return; + } + if(spritePositions.contains(column)) { + pixels[row][column] = '#'; + } else { + pixels[row][column] = '.'; + } + } + 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)) { +// System.err.println("During cycle " + sideEffect.cycle() + ", register X has the value " + sideEffect.register() + ", so the signal strength is " + sideEffect.signalStrength()); + 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/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 From 4b56955f58ed282b898a0a2bbbad5961f1420ee2 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Fri, 9 Dec 2022 23:29:12 -0800 Subject: [PATCH 35/44] Day 10 - cleanup --- src/test/java/com/macasaet/Day10.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/macasaet/Day10.java b/src/test/java/com/macasaet/Day10.java index 2d0d47e..a289ed3 100644 --- a/src/test/java/com/macasaet/Day10.java +++ b/src/test/java/com/macasaet/Day10.java @@ -133,7 +133,6 @@ public final void part1() { final var sideEffects = state.execute(instruction); for(final var sideEffect : sideEffects) { if(interestingCycles.contains(sideEffect.cycle)) { -// System.err.println("During cycle " + sideEffect.cycle() + ", register X has the value " + sideEffect.register() + ", so the signal strength is " + sideEffect.signalStrength()); accumulator.addAndGet(sideEffect.signalStrength()); } } From 088a3c250b2d5842aa379c57eadfe7d60d477a17 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sat, 10 Dec 2022 23:28:18 -0800 Subject: [PATCH 36/44] Day 11 --- src/test/java/com/macasaet/Day11.java | 189 ++++++++++++++++++++++++++ src/test/resources/sample/day-11.txt | 27 ++++ 2 files changed, 216 insertions(+) create mode 100644 src/test/java/com/macasaet/Day11.java create mode 100644 src/test/resources/sample/day-11.txt diff --git a/src/test/java/com/macasaet/Day11.java b/src/test/java/com/macasaet/Day11.java new file mode 100644 index 0000000..45db617 --- /dev/null +++ b/src/test/java/com/macasaet/Day11.java @@ -0,0 +1,189 @@ +package com.macasaet; + +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 enum Operator implements BiFunction { + ADD { + public BigInteger apply(BigInteger x, BigInteger y) { + return x.add(y); + } + }, + MULTIPLY { + public BigInteger apply(BigInteger x, BigInteger y) { + return x.multiply(y); + } + }; + + public static Operator parse(final String string) { + return switch(string) { + case "*" -> MULTIPLY; + case "+" -> ADD; + default -> throw new IllegalArgumentException("Invalid operator: " + string); + }; + } + } + + public record Operation(Operator operator, + Function lValueSupplier, + Function rValueSupplier) implements Function { + + public BigInteger apply(final BigInteger oldValue) { + Objects.requireNonNull(oldValue); + final var lValue = lValueSupplier.apply(oldValue); + final var rValue = rValueSupplier.apply(oldValue); + return operator.apply(lValue, rValue); + } + + public static Operation parse(String line) { + line = line.strip(); + if(!line.trim().startsWith("Operation:")) { + throw new IllegalArgumentException("Not an operation: " + line); + } + final var components = line.split(" "); + final var lValueExpression = components[3]; + final Function lValueSupplier = "old".equalsIgnoreCase(lValueExpression) + ? old -> old + : ignored -> new BigInteger(lValueExpression); + final var operator = Operator.parse(components[4]); + final var rValueExpression = components[5]; + final Function rValueSupplier = "old".equalsIgnoreCase(rValueExpression) + ? old -> old + : ignored -> new BigInteger(rValueExpression); + return new Operation(operator, lValueSupplier, rValueSupplier); + } + } + + /** + * 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)); + } + + public BigInteger countItemsInspected() { + return itemsInspected.get(); + } + + 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); + } + + 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(); + } + + @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 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()); + } + } + } + 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/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 From 0927ae765a77c74f10e3e9696897865fd93bd64b Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sun, 11 Dec 2022 22:08:57 -0800 Subject: [PATCH 37/44] Day 12 --- src/test/java/com/macasaet/Day12.java | 164 ++++++++++++++++++++++++++ src/test/resources/sample/day-12.txt | 5 + 2 files changed, 169 insertions(+) create mode 100644 src/test/java/com/macasaet/Day12.java create mode 100644 src/test/resources/sample/day-12.txt diff --git a/src/test/java/com/macasaet/Day12.java b/src/test/java/com/macasaet/Day12.java new file mode 100644 index 0000000..3257259 --- /dev/null +++ b/src/test/java/com/macasaet/Day12.java @@ -0,0 +1,164 @@ +package com.macasaet; + +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 { + + 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); + } + } + } + } + return Integer.MAX_VALUE; + } + + public List getPotentialTrailHeads() { + final var list = new ArrayList(); + for(int i = this.grid().length; --i >= 0; ) { + final var row = this.grid()[i]; + for(int j = row.length; --j >= 0; ) { + if(row[j] == 0) { + list.add(new Coordinate(i, j)); + } + } + } + return Collections.unmodifiableList(list); + } + + 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); + } + + 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)); + } + + } + + 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); + } + + @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); + } + +} \ 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 From 8baf857c4e8fab3f7393ef4fbf186ebd323ca4a8 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Mon, 12 Dec 2022 22:00:00 -0800 Subject: [PATCH 38/44] Day 13 --- src/test/java/com/macasaet/Day13.java | 161 ++++++++++++++++++++++++++ src/test/resources/sample/day-13.txt | 23 ++++ 2 files changed, 184 insertions(+) create mode 100644 src/test/java/com/macasaet/Day13.java create mode 100644 src/test/resources/sample/day-13.txt diff --git a/src/test/java/com/macasaet/Day13.java b/src/test/java/com/macasaet/Day13.java new file mode 100644 index 0000000..457145f --- /dev/null +++ b/src/test/java/com/macasaet/Day13.java @@ -0,0 +1,161 @@ +package com.macasaet; + +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 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"); + } + return compareToLiteral((Literal) other); + } + } + } + + public record ListItem(List items) implements Item { + public static ListItem parse(final String string) { + final var stack = new ArrayDeque(); + StringBuilder numberBuffer = new StringBuilder(); + for(final char c : string.toCharArray()) { + if(c == '[') { + stack.push(new ListItem(new ArrayList<>())); + } else if(c == ']') { + if(!numberBuffer.isEmpty()) { + final var numberString = numberBuffer.toString(); + numberBuffer.delete(0, numberBuffer.length()); + final var number = Integer.parseInt(numberString); + stack.peek().items().add(new Literal(number)); + } + if(stack.size() > 1) { + final var completed = stack.pop(); + stack.peek().items().add(completed); + } + } else if(c == ',') { + if(!numberBuffer.isEmpty()) { + final var numberString = numberBuffer.toString(); + numberBuffer.delete(0, numberBuffer.length()); + final var number = Integer.parseInt(numberString); + stack.peek().items().add(new Literal(number)); + } + } else { + numberBuffer.append(c); + } + } + return stack.pop(); + } + + 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()); + } + + } + + public record Literal(int item) implements Item { + public int compareToList(ListItem other) { + return asList().compareToList(other); + } + + public int compareToLiteral(Literal other) { + return Integer.compare(item(), other.item()); + } + + public ListItem asList() { + return new ListItem(Collections.singletonList(this)); + } + } + + protected List getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-13.txt"), false) + .collect(Collectors.joining("\n")); + final var blocks = lines.split("\n\n"); + return Arrays.stream(blocks).map(block -> Pair.parse(block)).toList(); + } + + @Test + public final void part1() { + final var pairs = getInput(); + var result = 0; + for(int i = 0; i < pairs.size(); i++) { + final var pair = pairs.get(i); + if(pair.isInOrder()) { + result += i + 1; + } + } + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var 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/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 From 4aa6730baccf08d3da995f91175b9a5032ef01c5 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Tue, 13 Dec 2022 22:33:37 -0800 Subject: [PATCH 39/44] Day 14 --- src/test/java/com/macasaet/Day14.java | 212 ++++++++++++++++++++++++++ src/test/resources/sample/day-14.txt | 2 + 2 files changed, 214 insertions(+) create mode 100644 src/test/java/com/macasaet/Day14.java create mode 100644 src/test/resources/sample/day-14.txt diff --git a/src/test/java/com/macasaet/Day14.java b/src/test/java/com/macasaet/Day14.java new file mode 100644 index 0000000..7f7d777 --- /dev/null +++ b/src/test/java/com/macasaet/Day14.java @@ -0,0 +1,212 @@ +package com.macasaet; + +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.StreamSupport; + +/** + * --- Day 14: Regolith Reservoir --- + * https://adventofcode.com/2022/day/14 + */ +public class Day14 { + + 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; + } + } + } + } + + 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; + } + } + } + } + + 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); + } + } + if(current.verticalDepth() > maxDepth) { + maxDepth = current.verticalDepth(); + } + if(current.horizontalOffset() < minHorizontalOffset) { + minHorizontalOffset = current.horizontalOffset(); + } + if(current.horizontalOffset() > maxHorizontalOffset) { + maxHorizontalOffset = current.horizontalOffset(); + } + last = current; + } + } + return new Cave(grid, maxDepth, minHorizontalOffset, maxHorizontalOffset); + } + + static List parseRockPaths(final String line) { + return Arrays.stream(line.split(" -> ")).map(Coordinate::parse).toList(); + } + + @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(); + } + } + + protected Cave getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-14.txt"), false) + .toList(); + return Cave.parse(lines); + } + + @Test + public final void part1() { + final var cave = getInput(); + final var result = cave.pourSandIntoAbyss(); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var cave = getInput(); + final var result = cave.fillAperture(); + + System.out.println("Part 2: " + result); + } + +} \ No newline at end of file diff --git a/src/test/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 From 128d4217f45fa2bebda24224d427a84bcb1405e6 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Thu, 15 Dec 2022 18:01:19 -0800 Subject: [PATCH 40/44] Day 15 --- src/test/java/com/macasaet/Day15.java | 205 ++++++++++++++++++++++++++ src/test/resources/sample/day-15.txt | 14 ++ 2 files changed, 219 insertions(+) create mode 100644 src/test/java/com/macasaet/Day15.java create mode 100644 src/test/resources/sample/day-15.txt diff --git a/src/test/java/com/macasaet/Day15.java b/src/test/java/com/macasaet/Day15.java new file mode 100644 index 0000000..2fea62c --- /dev/null +++ b/src/test/java/com/macasaet/Day15.java @@ -0,0 +1,205 @@ +package com.macasaet; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.IntPredicate; +import java.util.stream.StreamSupport; + +/** + * --- Day 15: Beacon Exclusion Zone --- + * https://adventofcode.com/2022/day/15 + */ +public class Day15 { + + 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); + } + + 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; + } + } + } + } + throw new IllegalStateException("No uncovered point found"); + } + +} \ 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 From d6550291acaac5e2be90218d74bd5247c027d319 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Sun, 18 Dec 2022 13:32:27 -0800 Subject: [PATCH 41/44] Day 18 --- src/test/java/com/macasaet/Day18.java | 176 ++++++++++++++++++++++++++ src/test/resources/sample/day-18.txt | 13 ++ 2 files changed, 189 insertions(+) create mode 100644 src/test/java/com/macasaet/Day18.java create mode 100644 src/test/resources/sample/day-18.txt diff --git a/src/test/java/com/macasaet/Day18.java b/src/test/java/com/macasaet/Day18.java new file mode 100644 index 0000000..3c29e60 --- /dev/null +++ b/src/test/java/com/macasaet/Day18.java @@ -0,0 +1,176 @@ +package com.macasaet; + +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 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); + } + } + 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); + } + } + 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)); + } + 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; + } + } + +} \ 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 From 5cbe0aa9b2e2ffae22ddbe96937a96ca67ed7c38 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Mon, 19 Dec 2022 22:37:28 -0800 Subject: [PATCH 42/44] Day 20 --- src/test/java/com/macasaet/Day20.java | 106 ++++++++++++++++++++++++++ src/test/resources/sample/day-20.txt | 7 ++ 2 files changed, 113 insertions(+) create mode 100644 src/test/java/com/macasaet/Day20.java create mode 100644 src/test/resources/sample/day-20.txt diff --git a/src/test/java/com/macasaet/Day20.java b/src/test/java/com/macasaet/Day20.java new file mode 100644 index 0000000..4125a47 --- /dev/null +++ b/src/test/java/com/macasaet/Day20.java @@ -0,0 +1,106 @@ +package com.macasaet; + +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.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * --- Day 20: Grove Positioning System --- + * https://adventofcode.com/2022/day/20 + */ +public class Day20 { + + public record Number(int originalIndex, int value, BigInteger decryptedValue) { + Number(int originalIndex, int value) { + this(originalIndex, value, BigInteger.valueOf(value).multiply(BigInteger.valueOf(811_589_153))); + } + } + + protected static List getInput() { + final List numbers = StreamSupport.stream(new LineSpliterator("day-20.txt"), false) + .mapToInt(Integer::parseInt) + .collect(ArrayList::new, (x, y) -> x.add(y), (x, y) -> x.addAll(y)); + final var result = new ArrayList(numbers.size()); + for(int i = 0; i < numbers.size(); i++) { + result.add(new Number(i, numbers.get(i))); + } + return Collections.unmodifiableList(result); + } + + @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; + } + } + final var workingSet = new ArrayList<>(numbers); + + for(final var number : numbers) { + final var originalIndex = workingSet.indexOf(number); + workingSet.remove(originalIndex); + var newIndex = (originalIndex + number.value()) % (numbers.size() - 1); + if(newIndex < 0) { + newIndex += numbers.size() - 1; + } + workingSet.add(newIndex, number); + } + + 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 result = (long)x + (long)y + (long)z; + + System.out.println("Part 1: " + result); + } + + @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; + } + workingSet.add(newIndex, number); + } + } + + 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(); + + final var result = x.add(y).add(z); + + System.out.println("Part 2: " + result); + } + +} \ 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 From bc07f57b685f5f5982d37fe0264250462d904982 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Fri, 23 Dec 2022 19:27:47 -0800 Subject: [PATCH 43/44] Day 23 --- src/test/java/com/macasaet/Day23.java | 644 ++++++++++++++++++++++++++ src/test/resources/sample/day-23.txt | 7 + 2 files changed, 651 insertions(+) create mode 100644 src/test/java/com/macasaet/Day23.java create mode 100644 src/test/resources/sample/day-23.txt diff --git a/src/test/java/com/macasaet/Day23.java b/src/test/java/com/macasaet/Day23.java new file mode 100644 index 0000000..b701a77 --- /dev/null +++ b/src/test/java/com/macasaet/Day23.java @@ -0,0 +1,644 @@ +package com.macasaet; + +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 { + + 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); + } + }; + + public abstract Set relativeCoordinates(Coordinate reference); + + public abstract Coordinate adjacent(Coordinate reference); + } + + public static class Crater { + + private final Map> grid; + private int minX; + private int maxX; + private int minY; + private int maxY; + private List movementPriority = + new ArrayList<>(Arrays.asList(Direction.North, Direction.South, Direction.West, Direction.East)); + + public Crater(final Map> grid, int minX, int maxX, int minY, int maxY) { + this.grid = grid; + setMinX(minX); + setMinY(minY); + setMaxX(maxX); + setMaxY(maxY); + } + + public 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 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() { + 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/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 From afd3588095dc3660039e84f8c3fd56d66cc6cce4 Mon Sep 17 00:00:00 2001 From: Carlos Macasaet Date: Fri, 23 Dec 2022 19:37:29 -0800 Subject: [PATCH 44/44] Day 21 --- src/test/java/com/macasaet/Day21.java | 323 ++++++++++++++++++++++++++ src/test/resources/sample/day-21.txt | 15 ++ 2 files changed, 338 insertions(+) create mode 100644 src/test/java/com/macasaet/Day21.java create mode 100644 src/test/resources/sample/day-21.txt diff --git a/src/test/java/com/macasaet/Day21.java b/src/test/java/com/macasaet/Day21.java new file mode 100644 index 0000000..b2fbfc8 --- /dev/null +++ b/src/test/java/com/macasaet/Day21.java @@ -0,0 +1,323 @@ +package com.macasaet; + +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.StreamSupport; + +/** + * --- Day 21: Monkey Math --- + * https://adventofcode.com/2022/day/21 + */ +public class Day21 { + + public record Monkey(String name, Job job) { + public long yell(final Map monkeys, final Map results) { + return job().yell(monkeys, results); + } + + public Simplification simplify(final Map monkeys, final Map results) { + return job().simplify(monkeys, results); + } + + public static Monkey parse(final String line) { + final var components = line.split(": "); + final var name = components[0].trim(); + final var job = Job.parse(components[1].trim()); + return new Monkey(name, job); + } + } + + public interface Simplification { + Simplification simplify(); + } + + 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()); + } + // cannot simplify further + return this; + } else if (x() instanceof final Expression expression && y() instanceof final Value constant) { + // e.g. 5/x=2 or x/5=2 + final var inverse = expression.operation().inverse(); + if (expression.x() instanceof Value) { + // e.g. 5/x=2 or x/5=2 + if (expression.operation().isSymmetric()) { + // e.g. 2x=5 -> x=5*2 -> x=10 + final var rValue = new Expression(constant, inverse, expression.x()).simplify(); + return new Expression(expression.y(), Operation.IS_EQUAL, rValue); + } else { + // e.g. 5/x=2 -> 5=2x + final var rValue = new Expression(constant, inverse, expression.y()); + return new Expression(expression.x(), Operation.IS_EQUAL, rValue); + } + } else if (expression.y() instanceof Value) { + // e.g. x/5=2 -> x=2*5 -> x=10 + final var rValue = new Expression(constant, inverse, expression.y()).simplify(); + return new Expression(expression.x(), Operation.IS_EQUAL, rValue); + } + // cannot simplify further + return this; + } + } + return this; + } + + public String toString() { + return "(" + x() + ") " + operation() + " (" + y() + ")"; + } + } + + record Value(long value) implements Simplification { + public String toString() { + return "" + value(); + } + + public Simplification simplify() { + return this; + } + } + + 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/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