Advent of Code - Day 3: Gear Ratios


2023-12-03-generated.png

You and the Elf eventually reach a gondola lift station; he says the gondola lift will take you up to the water source, but this is as far as he can bring you. You go inside.

It doesn’t take long to find the gondolas, but there seems to be a problem: they’re not moving.

-- Day 3 - Advent of Code 2023

Solution in Java

Full source can be found: in GitHub

Part 1

For today I decided to model the assignment into some data structures to aid in the calculations later on. This consists out of Position implementation, allowing both the NumberWithPos and the SymbolPos specifications.

1private sealed interface Position permits NumberWithPos, SymbolPos {}  
2
3private record NumberWithPos(int number, int row, int startPos, int endPos) implements Position {}
4
5record SymbolPos(char c, int position) implements Position {  
6    boolean isGear() {  
7        return c == '*';  
8    }  
9}

To parse the input lines into Position instances the following method is added.

 1private static final Set<Character> SYMBOLS = Set.of('!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+', '=', '|', '~', '`', '{', '}', '[', ']', ':', ';', '"', '\'', '<', '>', ',', '?', '/', '\\');
 2private List<Position> parseLine(int rowNr, String line) {  
 3    var currentRow = new ArrayList<Position>();  
 4  
 5    var digit = "";  
 6    for (int i = 0; i < line.length(); i++) {  
 7        char c = line.charAt(i);  
 8  
 9        if (Character.isDigit(c)) {  
10            digit += c;  
11        } else {  
12            if (!digit.isEmpty()) {  
13                currentRow.add(new NumberWithPos(  
14                        Integer.parseInt(digit),  
15                        rowNr,  
16                        Math.max(0, i - digit.length() - 1),  
17                        i));  
18                digit = "";  
19            }  
20            if (SYMBOLS.contains(c)) {  
21                currentRow.add(new SymbolPos(c, i));  
22            }  
23        }  
24    }  
25  
26    if (!digit.isEmpty()) {  
27        currentRow.add(new NumberWithPos(  
28                Integer.parseInt(digit),  
29                rowNr,  
30                Math.max(0, line.length() - digit.length() - 1),  
31                line.length()));  
32    }  
33  
34    return currentRow;  
35}

Now it can be glued together using an algorithm that processes each input line. Then we look if each number is touching one of the SYMBOLS in the grid.

 1public void part1() {  
 2    int rowNr = 0;  
 3    Scanner scanner = inputLoader.scanner();  
 4    Queue<List<Position>> queue = new LinkedList<>();  
 5    Set<NumberWithPos> relevantNumbers = new HashSet<>();  
 6    while (scanner.hasNext()) {  
 7        var currentRow = parseLine(rowNr, scanner.nextLine());  
 8        queue.add(currentRow);  
 9  
10        // find the relevant numbers  
11        var iterationList = queue.stream().toList();  
12        for (var i = 0; i < iterationList.size(); i++) {  
13            var currentIdx = i;  
14            iterationList.get(i)  
15                    .stream()  
16                    .filter(pos -> pos instanceof SymbolPos)  
17                    .map(pos -> (SymbolPos) pos)  
18                    .forEach(pos -> {  
19                        if (currentIdx > 0) {  
20                            relevantNumbers.addAll(  
21                                    touching(iterationList.get(currentIdx - 1), pos.position()));  
22                        }  
23                        if (currentIdx < iterationList.size() - 1) {  
24                            relevantNumbers.addAll(  
25                                    touching(iterationList.get(currentIdx + 1), pos.position()));  
26                        }  
27  
28                        relevantNumbers.addAll(  
29                                touching(iterationList.get(currentIdx), pos.position()));  
30                    });  
31        }  
32  
33        // keep at most 3 rows in the cache  
34        if (queue.size() > 3) {  
35            queue.poll();  
36        }  
37        rowNr++;  
38    }  
39  
40    var answer = relevantNumbers.stream()  
41            .mapToLong(NumberWithPos::number)  
42            .sum();  
43    validator.part1(answer);  
44}

Part 2

For the second part I first added the GearRatio record

1record GearRatio(SymbolPos gear, NumberWithPos left, NumberWithPos right) {}

Using the newly added class the algorithm for the second part can be written.

 1public void part2() {  
 2    var rowNr = 0;  
 3    var scanner = inputLoader.scanner();  
 4    var queue = new LinkedList<List<Position>>();  
 5    var gears = new HashSet<GearRatio>();  
 6    while (scanner.hasNext()) {  
 7        var currentRow = parseLine(rowNr, scanner.nextLine());  
 8        queue.add(currentRow);  
 9  
10        // The missing part wasn't the only issue - one of the gears in the engine is wrong.  
11        // A gear is any * symbol that is adjacent to exactly two part numbers. Its gear
12        // ratio is the result of multiplying those two numbers together.
13		// This time, you need to find the gear ratio of every gear and add them all up
14		// so that the engineer can figure out which gear needs to be replaced.
15		var iterationList = queue.stream().toList();  
16        for (var i = 0; i < iterationList.size(); i++) {  
17            var currentIdx = i;  
18            iterationList.get(i)  
19                    .stream()  
20                    .filter(pos -> pos instanceof SymbolPos)  
21                    .map(SymbolPos.class::cast)  
22                    .filter(SymbolPos::isGear)  
23                    .forEach(pos -> {  
24                        var touching = new ArrayList<>(touching(iterationList.get(currentIdx), pos.position()));  
25                        if (currentIdx < iterationList.size() - 1) {  
26                            touching.addAll(touching(iterationList.get(currentIdx + 1), pos.position()));  
27                        }  
28                        if (currentIdx > 0) {  
29                            touching.addAll(touching(iterationList.get(currentIdx - 1), pos.position()));  
30                        }  
31  
32                        if (touching.size() == 2) {  
33                            gears.add(  
34                                    new GearRatio(pos, touching.get(0), touching.get(1)));  
35                        }  
36                    });  
37        }  
38  
39        if (queue.size() > 3) {  
40            queue.poll();  
41        }  
42  
43        rowNr++;  
44    }  
45  
46    var answer = gears.stream()  
47            .mapToLong(gear -> (long) gear.left().number() * gear.right().number())  
48            .sum();  
49  
50    validator.part2(answer);  
51}  
52  
53private Set<NumberWithPos> touching(List<Position> inputRow, int position) {  
54    return inputRow.stream()  
55            .filter(pos -> pos instanceof NumberWithPos)  
56            .map(NumberWithPos.class::cast)  
57            .filter(pos -> pos.endPos() >= position && pos.startPos() <= position)  
58            .collect(toSet());  
59}

See also