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 pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy