diff --git a/README.md b/README.md index 9603ba6..bde4b1c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ - [Day 13 - Claw Contraption](https://github.com/Flashky/advent-of-code-2024/tree/master/src/main/java/com/adventofcode/flashk/day13) - [Day 14 - Restroom Redoubt](https://github.com/Flashky/advent-of-code-2024/tree/master/src/main/java/com/adventofcode/flashk/day14) - [Day 15 - Warehouse Woes](https://github.com/Flashky/advent-of-code-2024/tree/master/src/main/java/com/adventofcode/flashk/day15) -- [Day 16](https://github.com/Flashky/advent-of-code-2024/tree/master/src/main/java/com/adventofcode/flashk/day16) +- [Day 16 - Reindeer Maze](https://github.com/Flashky/advent-of-code-2024/tree/master/src/main/java/com/adventofcode/flashk/day16) - [Day 17](https://github.com/Flashky/advent-of-code-2024/tree/master/src/main/java/com/adventofcode/flashk/day17) - [Day 18 - RAM Run](https://github.com/Flashky/advent-of-code-2024/tree/master/src/main/java/com/adventofcode/flashk/day18) - [Day 19 - Linen Layout](https://github.com/Flashky/advent-of-code-2024/tree/master/src/main/java/com/adventofcode/flashk/day19) diff --git a/src/main/java/com/adventofcode/flashk/common/Vector2.java b/src/main/java/com/adventofcode/flashk/common/Vector2.java index 2653a91..7576192 100644 --- a/src/main/java/com/adventofcode/flashk/common/Vector2.java +++ b/src/main/java/com/adventofcode/flashk/common/Vector2.java @@ -64,7 +64,11 @@ public static Vector2 transform(Vector2 start, Vector2 end) { return new Vector2(x,y); } - + + public static Vector2 multiply(Vector2 vector, int scalar) { + return new Vector2(vector.x * scalar, vector.y * scalar); + } + /** * Substracts the right operand vector to the left operand vector, applying absolute value to the result. * diff --git a/src/main/java/com/adventofcode/flashk/day16/ReindeerMaze.java b/src/main/java/com/adventofcode/flashk/day16/ReindeerMaze.java index 44822db..fe790f9 100644 --- a/src/main/java/com/adventofcode/flashk/day16/ReindeerMaze.java +++ b/src/main/java/com/adventofcode/flashk/day16/ReindeerMaze.java @@ -2,8 +2,11 @@ import com.adventofcode.flashk.common.Vector2; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashSet; import java.util.PriorityQueue; +import java.util.Queue; import java.util.Set; public class ReindeerMaze { @@ -60,7 +63,6 @@ public long solveA2() { return dijkstra(startTile, Vector2.right()); } - // TODO add also start direction private long dijkstra(Tile start, Vector2 direction) { reset(); @@ -68,9 +70,6 @@ private long dijkstra(Tile start, Vector2 direction) { // Initialize start tile start.setScore(0); start.setDirection(direction); - //if(start.getDirection() == null) { - // start.setDirection(Vector2.right()); - //} // Execute dijkstra PriorityQueue tiles = new PriorityQueue<>(); @@ -141,87 +140,115 @@ private long dijkstra(Tile start, Vector2 direction) { - 11 steps - 11 steps - 11 steps */ - public long solveB(){ - // Idea: - - // 1. Primero aplicamos un dijkstra normal para encontrar el camino más corto. - // 2, Metemos en una lista los tiles del camino más corto. - - // Ahora recorremos cada tile del camino más corto - // 1. Calculamos los adyacentes de un tile. - // 2. Si la distancia del tile adyacente al inicio + la distancia del tile al final es igual que la distancia - // del camino más corto, entonces estamos ante un camino alternativo. - - long score = dijkstra(startTile, Vector2.right()); - paint(); - // Parte 1, encontrar las intersecciones que hay en el camino más corto - - Set intersections = findPathIntersections(); - //Set intersections = findPathSections(); - - for(Tile intersection : intersections) { - - // calcular dijkstra para adyacentes - Set adjacents = getAdjacentsIncludingVisited(intersection); - // Add also direction - - for(Tile adjacent : adjacents) { - //if(adjacent.isPath()) { - // continue; - //} - //Vector2 direction = Vector2.substract(intersection.getPosition(), adjacent.getPosition()); - Vector2 direction = Vector2.substract(adjacent.getPosition(),intersection.getPosition()); - dijkstra(adjacent, direction); - long adjacentScore = endTile.getScore() + startTile.getScore(); - if(adjacentScore == score) { - fillPath(adjacent, endTile); - fillPath(adjacent, startTile); - paint(); - System.out.println(); + + public long solveB2() { + long result = 0; + dijkstra(startTile, Vector2.right()); + reverseDijkstra(); + for(int row = 0; row < rows; row++) { + for(int col = 0; col < cols; col++) { + if(map[row][col].isPath()) { + result++; } } } - // Pasada 2, buscamos caminos alternativos en cada una de las intersecciones. - - startTile.setPath(true); - endTile.setPath(true); - paint(); - return countPathTiles(); + return result; } - private long countPathTiles() { - long count = 0; - for(int row = 0; row < rows; row++) { - for (int col = 0; col < cols; col++) { - if (map[row][col].isPath()) { - count++; + /// Executes a reverse dijkstra that starts at the end node and attempts to reach the start node. + /// + /// This method must be executed after [dijkstra(Tile start, Vector2 direction)]() + private void reverseDijkstra() { + + // Initialize start tile + endTile.setScoreToEnd(0); + + Deque tiles = new ArrayDeque<>(); + tiles.add(endTile); + + while(!tiles.isEmpty()) { + + Tile tile = tiles.poll(); + tile.setVisitedReverse(true); + tile.setPath(true); + + // Calcular adyacentes. + Set adjacentTiles = getAdjacentsReverse(tile); + for(Tile adjacentTile : adjacentTiles) { + + // Tiles should be added to the queue only if they can meet the following formula: + // d(S,E) == estimated score + // estimated score = d(S,A,E) = d(S,A) + d(A,B) + d(B,E) + + // Where: + // S: Starting tile + // E: Ending tile + // A: Adjacent tile + // B: Tile + // Where nodes have this order: S -> A -> B -> E + + // Known parameters: + // d(S,E): endTile.getScore() + // d(S,A): adjacentTile.getScore() + // d(A,B): 1 if A and B directions are the same, 1001 if A and B directions is different. + // d(B,E): tile.getScoreToEnd() + // + // Also adjacent tile score must be modified by 1000 when taking corners. + + Vector2 newDirection = Vector2.substract(adjacentTile.getPosition(), tile.getPosition()); + + // d(S,A) + long scoreAdjacentTile = calculateAdjacentTileScore(adjacentTile, newDirection); + + // d(A,B) + long scoreAdjacentToTile = calculateAdjacentTileToTileScore(adjacentTile, tile, newDirection); + + // d(B,E) + long scoreTileToEnd = tile.getScoreToEnd(); + + // estimated score = d(S,A) + d(A,B) + d(B,E) + long estimatedScore = scoreAdjacentTile + scoreAdjacentToTile + scoreTileToEnd; + if(estimatedScore == endTile.getScore()) { + // Adjacent tile has a path that is the same as the best score + + // d(A,E) = d(A,B) + d(B,E) + adjacentTile.setScoreToEnd(scoreAdjacentToTile+scoreTileToEnd); + adjacentTile.setDirectionReverse(newDirection); + + tiles.add(adjacentTile); + } + } } - return count; - } + //paint(); - private Set findPathIntersections() { - Set intersections = new HashSet<>(); + } - Tile end = map[endPos.getY()][endPos.getX()]; - Tile start = map[startPos.getY()][startPos.getX()]; + private long calculateAdjacentTileScore(Tile adjacentTile, Vector2 newDirection) { - Tile current = end; - do { - current.setPath(true); - if(getAdjacentsIncludingVisited(current).size() > 2) { - intersections.add(current); - } - current = current.getParent(); - } while(current != start); + long scoreAdjacentTile = adjacentTile.getScore(); - start.setPath(true); + // If there is a corner, adjacent tile must modify its score + Vector2 reversedNewDirection = Vector2.multiply(newDirection,-1); + if(!reversedNewDirection.equals(adjacentTile.getDirection())) { + scoreAdjacentTile += 1000; + } - return intersections; + return scoreAdjacentTile; } + private long calculateAdjacentTileToTileScore(Tile adjacentTile, Tile tile, Vector2 newDirection) { + long scoreAdjacentToTile; + if(tile == endTile) { + scoreAdjacentToTile = 1; + } else { + scoreAdjacentToTile = newDirection.equals(tile.getDirectionReverse()) ? 1 : 1001; + } + + return scoreAdjacentToTile; + } private void reset() { for(int row = 0; row < rows; row++) { @@ -229,24 +256,10 @@ private void reset() { map[row][col].setVisited(false); map[row][col].setScore(Long.MAX_VALUE); map[row][col].setParent(null); - //map[row][col].setPath(false); } } } - private void fillPath(Tile start, Tile end) { - - Tile current = end; - while(current != start) { - current = current.getParent(); - current.setPath(true); - } - - start.setPath(true); - end.setPath(true); // just in case - - } - private Set getAdjacents(Tile parent) { Set adjacents = new HashSet<>(); for(Vector2 dir : directions) { @@ -257,15 +270,14 @@ private Set getAdjacents(Tile parent) { } } return adjacents; - } - private Set getAdjacentsIncludingVisited(Tile parent) { + private Set getAdjacentsReverse(Tile parent) { Set adjacents = new HashSet<>(); for(Vector2 dir : directions) { Vector2 newPos = Vector2.transform(parent.getPosition(), dir); Tile newTile = map[newPos.getY()][newPos.getX()]; - if(!newTile.isWall()) { + if(!newTile.isWall() && !newTile.isVisitedReverse()) { adjacents.add(newTile); } } diff --git a/src/main/java/com/adventofcode/flashk/day16/Tile.java b/src/main/java/com/adventofcode/flashk/day16/Tile.java index b06bd77..1810534 100644 --- a/src/main/java/com/adventofcode/flashk/day16/Tile.java +++ b/src/main/java/com/adventofcode/flashk/day16/Tile.java @@ -18,11 +18,18 @@ public class Tile implements Comparable{ private final Vector2 position; private final char value; + // Dijkstra values private long score = Long.MAX_VALUE; private long partialScore; - private Tile parent; private boolean visited = false; private Vector2 direction; + private Tile parent; + + // Reversed Dijkstra values + private long scoreToEnd = Long.MAX_VALUE; + private boolean visitedReverse = false; + private Vector2 directionReverse; + private boolean path = false; public Tile(Vector2 position, char value) { diff --git a/src/test/java/com/adventofcode/flashk/day16/Day16Test.java b/src/test/java/com/adventofcode/flashk/day16/Day16Test.java index 2ecd0a4..49af091 100644 --- a/src/test/java/com/adventofcode/flashk/day16/Day16Test.java +++ b/src/test/java/com/adventofcode/flashk/day16/Day16Test.java @@ -20,8 +20,7 @@ @DisplayName(TestDisplayName.DAY_16) @TestMethodOrder(OrderAnnotation.class) -@Disabled // TODO Remove comment when implemented -public class Day16Test extends PuzzleTest { +class Day16Test extends PuzzleTest { private static final String INPUT_FOLDER = TestFolder.DAY_16; @@ -160,7 +159,7 @@ public void testSolvePart2Sample() { ReindeerMaze reindeerMaze = new ReindeerMaze(inputs); // Me da - assertEquals(45L, reindeerMaze.solveB()); + assertEquals(45L, reindeerMaze.solveB2()); } @Test @@ -168,13 +167,13 @@ public void testSolvePart2Sample() { @Tag(TestTag.PART_2) @Tag(TestTag.SAMPLE) @DisplayName(TestDisplayName.PART_2_SAMPLE) - public void testSolvePart2Sample2() { + void testSolvePart2Sample2() { // Read input file char[][] inputs = Input.read2DCharArray(INPUT_FOLDER, TestFilename.INPUT_FILE_SINGLE_SAMPLE_2); ReindeerMaze reindeerMaze = new ReindeerMaze(inputs); - // Me da - assertEquals(64L, reindeerMaze.solveB()); + + assertEquals(64L, reindeerMaze.solveB2()); } @Test @@ -187,33 +186,55 @@ public void testSolvePart2Input() { // Read input file char[][] inputs = Input.read2DCharArray(INPUT_FOLDER, TestFilename.INPUT_FILE); ReindeerMaze reindeerMaze = new ReindeerMaze(inputs); - System.out.println("Solution: "+reindeerMaze.solveB()); - // 512 -> too low - // 513 -> That's not the right answer; your answer is too low. - // Curiously, it's the right answer for someone else; - // you might be logged in to the wrong account or just unlucky. - // 514 -> too low - - assertEquals(0L,0L); + assertEquals(554L,reindeerMaze.solveB2()); } - // Alternate test cases: - // https://www.reddit.com/r/adventofcode/comments/1hfhgl1/2024_day_16_part_1_alternate_test_case/ @Test @Order(6) @Tag(TestTag.PART_2) @Tag(TestTag.INPUT) @DisplayName(TestDisplayName.PART_2_DEBUG) - public void testSolvePart2Debug() { + void testSolvePart2Debug() { // Read input file char[][] inputs = Input.read2DCharArray(INPUT_FOLDER, "debug_edge_1.input"); ReindeerMaze reindeerMaze = new ReindeerMaze(inputs); - assertEquals(149L,reindeerMaze.solveB()); + // Source: https://www.reddit.com/r/adventofcode/comments/1hfhgl1/2024_day_16_part_1_alternate_test_case/ + + // Should fill: + // ########################### + // #######################..O# + // ######################..#O# + // #####################..##O# + // ####################..###O# + // ###################..##OOO# + // ##################..###O### + // #################..####OOO# + // ################..#######O# + // ###############..##OOOOOOO# + // ##############..###O####### + // #############..####OOOOOOO# + // ############..###########O# + // ###########..##OOOOOOOOOOO# + // ##########..###O########### + // #########..####OOOOOOOOOOO# + // ########..###############O# + // #######..##OOOOOOOOOOOOOOO# + // ######..###O############### + // #####..####OOOOOOOOOOOOOOO# + // ####..###################O# + // ###..##OOOOOOOOOOOOOOOOOOO# + // ##..###O################### + // #..####OOOOOOOOOOOOOOOOOOO# + // #.#######################O# + // #OOOOOOOOOOOOOOOOOOOOOOOOO# + // ########################### + + assertEquals(149L,reindeerMaze.solveB2()); } @@ -222,7 +243,10 @@ public void testSolvePart2Debug() { @Tag(TestTag.PART_2) @Tag(TestTag.INPUT) @DisplayName(TestDisplayName.PART_2_DEBUG_2) - public void testSolvePart2Debug2() { + @Disabled + void testSolvePart2Debug2() { + + // DISCLAIMER: This test is disabled as it takes 3 seconds to finish, enable it only for debugging purposes // Read input file char[][] inputs = Input.read2DCharArray(INPUT_FOLDER, "debug_edge_2.input"); @@ -239,7 +263,7 @@ public void testSolvePart2Debug2() { // #...O#...OOOOOO#...OOOOOO#...OOOOOO#...OOOOOO#...OOOOOO# // #OOOO#.........#.........#.........#.........#.........# // ######################################################## - // + // Camino inicial: // 21 giros // 110 pasos @@ -267,7 +291,7 @@ public void testSolvePart2Debug2() { ReindeerMaze reindeerMaze = new ReindeerMaze(inputs); - assertEquals(264L,reindeerMaze.solveB()); + assertEquals(264L,reindeerMaze.solveB2()); } } diff --git a/src/test/resources/inputs b/src/test/resources/inputs index 14d33f2..a44ad79 160000 --- a/src/test/resources/inputs +++ b/src/test/resources/inputs @@ -1 +1 @@ -Subproject commit 14d33f2ac95351ce6b310aa1f09435c2971236ea +Subproject commit a44ad7906ce5456bbf9bbf48e627b1be3bf423ce