19th December, 2024

The Towel Trials

695 words
4 min.
Post preview image
Advent of Code 2024: Day 19

Today's task may look like a problem of its own, but it turns out it's fundamentally yet another "first find the length of the best path, then find all possible paths with that length" algorithm, to the delight of those who can't take it anymore ;)

Allured by the possibility of free access to the hot springs, we want to help the staff arrange towels by pattern of colored stripes (because why not). Such stripes can be white (w), blue (u), black (b), red (r), or green (g), and the first part of the puzzle input is a list of all available patterns that can be use to assemble a towel:

r, wr, b, g, bwu, rb, gb, br

brwrr
bggr
gbbr
rrbgbr
ubwu
bwurrg
brgr
bbrgwb

The second part on the other hand is a series of towels we need to try and reproduce. Not all of the towels will be possible with the available list of patterns (for example ubwu above won't be, since we don't have any u pattern to use), but some will be, such as:

- brwrr can be made with a br towel, then a wr towel, and finally an r towel.

- bggr can be made with a b towel, two g towels, and then an r towel.

The goal for now is to find how many of the towels in the list are indeed possible. For this purpose, I created an isDesignValid function that addresses the question one towel at the time. The key points for this are:

- We check if the given design can be fully broken down into valid substrings, where each substring is part of the provided allPatterns list.

- Only substrings up to the length of the longest pattern are considered, reducing unnecessary computations.

- A queue is used to explore all possible substring combinations systematically, ensuring all potential solutions are checked.

- Substrings starting at the current position in the design are evaluated dynamically, with only valid substrings added to the next exploration states.

- The function stops immediately and returns true if a valid breakdown of the full design is found.

function isDesignValid(design: string, allPatterns: string[]) {
    let longestPattern = allPatterns.reduce(
        (maxLength, str) => Math.max(maxLength, str.length),
        0
    );

    const queue: State[] = [{ patternList: [], index: 0 }];
    const visited = new Set<number>();

    while (queue.length > 0) {
        const { patternList, index } = queue.pop()!;

        if (index === design.length) {
            return true;
        }

        if (visited.has(index)) {
            continue;
        }
        visited.add(index);

        const substrings = new Set<string>();
        for (let i = 1; i <= longestPattern; i++) {
            const substring = design.slice(index, index + i);
            if (allPatterns.includes(substring)) {
                substrings.add(substring);
            }
        }

        [...substrings].forEach((substring) => {
            queue.push({
                index: index + substring.length,
                patternList: [...patternList, substring],
            });
        });
    }

    return false;
}

As I anticipated, part 2 is to find all the possible way each towel can be composed using the available patterns… Basically a design like rrbgbr can be made 6 different ways:

  • r, r, b, g, b, r
  • r, r, b, g, br
  • r, r, b, gb, r
  • r, rb, g, b, r
  • r, rb, g, br
  • r, rb, gb, r

Time for some DFS now: we need to count all possible ways to completely break down the design string into valid substrings from the allPatterns list. Additionally, memoization allows us to efficiently explore all substring combinations while avoiding redundant calculations for overlapping subproblems.

In coding terms, this is what the idea above translated into:

function findAllDesignCombinations(
    allPatterns: string[],
    design: string
): number {
    const longestPattern = allPatterns.reduce(
        (maxLength, str) => Math.max(maxLength, str.length),
        0
    );
    const memo: Map<number, number> = new Map();

    function dfs(index: number): number {
        if (index === design.length) {
            return 1; // One valid combination
        }

        if (memo.has(index)) {
            return memo.get(index)!;
        }

        let count = 0;

        for (let i = 1; i <= longestPattern; i++) {
            const substring = design.slice(index, index + i);
            if (allPatterns.includes(substring)) {
                count += dfs(index + i);
            }
        }

        memo.set(index, count);
        return count;
    }

    return dfs(0);
}

The Matrix strikes back, I guess
Mario Kart: 100 Picoseconds Edition