Day 19 - Linen Layout

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

  • EnEnCode@programming.dev
    link
    fedilink
    English
    arrow-up
    1
    ·
    19 hours ago

    Rust

    Definitely not Aho–Corasick cool or genius, but does get ~11ms on both parts. Gains over the other Rust solves are partly a less naïve approach to towel checking and partly getting it to work with 0 String allocations, which involves both an instructive lifetime annotation and the clever use of a niche standard library function.

    Code
    pub fn day19(input: &str) -> (u64, u64) {
        fn recur_helper<'a>(pat: &'a str, towels: &[&str], map: &mut HashMap<&'a str, u64>) -> u64 {
            let mut hits = 0;
            let mut towel_subset = towels;
            for l in 1..=pat.len() {
                let test_pat = &pat[0..l];
                match towel_subset.binary_search(&test_pat) {
                    Ok(idx) => {
                        if pat[l..].is_empty() {
                            return hits + 1;
                        } else if let Some(&v) = map.get(&pat[l..]) {
                            hits += v;
                        } else {
                            let v = recur_helper(&pat[l..], towels, map);
                            map.insert(&pat[l..], v);
                            hits += v;
                        }
    
                        towel_subset = &towel_subset[idx..];
                        let end = towel_subset.partition_point(|&x| x.starts_with(test_pat));
                        towel_subset = &towel_subset[..end];
                        if towel_subset.is_empty() {
                            return hits;
                        }
                    }
                    Err(idx) => {
                        towel_subset = &towel_subset[idx..];
                        let end = towel_subset.partition_point(|&x| x.starts_with(test_pat));
                        towel_subset = &towel_subset[..end];
                        if towel_subset.is_empty() {
                            return hits;
                        }
                    }
                }
            }
            hits
        }
        let mut part1 = 0;
        let mut part2 = 0;
        let mut lines = input.lines();
        let mut towels = lines.next().unwrap().split(", ").collect::<Vec<_>>();
        towels.sort();
        let mut map = HashMap::new();
        for pat in lines.skip(1) {
            let tmp = recur_helper(pat, &towels, &mut map);
            part2 += tmp;
            if tmp > 0 {
                part1 += 1;
            }
        }
        (part1, part2)
    }
    
    • Acters@lemmy.world
      link
      fedilink
      arrow-up
      1
      ·
      edit-2
      1 hour ago

      nice job! now do it without recursion. something I always attempt to shy away from as I think it is dirty to do, makes me feel like it is the “lazy man’s” loop.

      Aho-Corasick is definitely meant for this kind of challenge that requires finding all occurrences of multiple patterns, something worth reading up on! If you are someone who builds up a util library for future AoC or other projects then that would likely come in to use sometimes.

      Another algorithm that is specific to finding one pattern is the Boyer-Moore algorithm. something to mull over: https://softwareengineering.stackexchange.com/a/183757

      have to remember we are all discovering new interesting ways to solve similar or same challenges. I am still learning lots, hope to see you around here and next year!

  • Acters@lemmy.world
    link
    fedilink
    arrow-up
    2
    ·
    1 day ago

    Python3

    Solver uses trie just like the other python solve here, I try to not look at solve threads until after it is solved. yet, I somehow got a solve that only takes ~7 ms. previously I would rebuild the trie every time and made it take ~55 ms.

    Code
    from collections import deque
    
    def profiler(method):
        from time import perf_counter_ns
        def wrapper_method(*args: any, **kwargs: any) -> any:
            start_time = perf_counter_ns()
            ret = method(*args, **kwargs)
            stop_time = perf_counter_ns() - start_time
            time_len = min(9, ((len(str(stop_time))-1)//3)*3)
            time_conversion = {9: 'seconds', 6: 'milliseconds', 3: 'microseconds', 0: 'nanoseconds'}
            print(f"Method {method.__name__} took : {stop_time / (10**time_len)} {time_conversion[time_len]}")
            return ret
    
        return wrapper_method
    
    def build_aho_corasick(towels):
        # Each node: edges dict, failure link, output (which towels end here)
        trie = [{'fail': 0, 'out': []}]
        
        # Build trie of towels
        for index, word in enumerate(towels):
            node = 0
            for char in word:
                if char not in trie[node]:
                    trie[node][char] = len(trie)
                    trie.append({'fail': 0, 'out': []})
                node = trie[node][char]
            trie[node]['out'].append(len(word))  # store length of matched towel
    
        # Build fail links (BFS)
        queue = deque()
        # Initialize depth-1 fail links
        for c in trie[0]:
            if c not in ('fail', 'out'):
                nxt = trie[0][c]
                trie[nxt]['fail'] = 0
                queue.append(nxt)
    
        # BFS build deeper fail links
        while queue:
            r = queue.popleft()
            for c in trie[r]:
                if c in ('fail', 'out'):
                    continue
                nxt = trie[r][c]
                queue.append(nxt)
                f = trie[r]['fail']
                while f and c not in trie[f]:
                    f = trie[f]['fail']
                trie[nxt]['fail'] = trie[f][c] if c in trie[f] else 0
                trie[nxt]['out'] += trie[trie[nxt]['fail']]['out']
        return trie
    
    def count_possible_designs_aho(trie, design):
        n = len(design)
        dp = [0] * (n + 1)
        dp[0] = 1
    
        # State in the trie automaton
        state = 0
    
        for i, char in enumerate(design, 1):
            # Advance in trie
            while state and char not in trie[state]:
                state = trie[state]['fail']
            if char in trie[state]:
                state = trie[state][char]
            else:
                state = 0
            
            # For every towel match that ends here:
            for length_matched in trie[state]['out']:
                dp[i] += dp[i - length_matched]
        
        return dp[n]
    
    @profiler
    def main(input_data):
        towels,desired_patterns = ( sorted(x.split(), key=len, reverse=True) for x in input_data.replace(',', ' ').split('\n\n') )
        part1 = 0
        part2 = 0
        
        # Build Aho-Corasick structure
        trie = build_aho_corasick(towels)
        
        for pattern in desired_patterns:
            count = count_possible_designs_aho(trie, pattern)
            if count:
                part1 += 1
                part2 += count
        
        return part1,part2
    
    if __name__ == "__main__":
        with open('input', 'r') as f:
            input_data = f.read().replace('\r', '').strip()
        result = main(input_data)
        print("Part 1:", result[0], "\nPart 2:", result[1])
    
    
  • Pyro@programming.dev
    link
    fedilink
    arrow-up
    1
    ·
    edit-2
    9 days ago

    Python

    Approach: Recursive memoized backtracking with a Trie

    I get to use one of my favorite data structures here, a Trie! It helps us figure out whether a prefix of the design is a valid pattern in linear time.

    I use backtracking to choose potential component patterns (using the Trie), kicking off matching the rest of the design down the stack. We can continue matching longer patterns immediately after the recursion stack unwinds.
    In addition, I use global memoization to keep track of the feasibility (part 1) or the number of combinations (part 2) for designs and sub-designs. This way, work done for earlier designs can help speed up later ones too.

    I ended up combining part 1 and 2 solutions into a single function because part 1 is a simpler variant of part 2 where we count all designs with the number of possible pattern combinations > 0.

    Reading Input
    import os
    
    here = os.path.dirname(os.path.abspath(__file__))
    
    # read input
    def read_data(filename: str):
        global here
    
        filepath = os.path.join(here, filename)
        with open(filepath, mode="r", encoding="utf8") as f:
            return f.read()
    
    
    Trie Implementation
    class Trie:
        class TrieNode:
            def __init__(self) -> None:
                self.children = {}  # connections to other TrieNode
                self.end = False  # whether this node indicates an end of a pattern
    
        def __init__(self) -> None:
            self.root = Trie.TrieNode()
    
        def add(self, pattern: str):
            node = self.root
            # add the pattern to the trie, one character at a time
            for color in pattern:
                if color not in node.children:
                    node.children[color] = Trie.TrieNode()
                node = node.children[color]
            # mark the node as the end of a pattern
            node.end = True
    
    
    Solution
    def soln(filename: str):
        data = read_data(filename)
        patterns, design_data = data.split("\n\n")
    
        # build the Trie
        trie = Trie()
        for pattern in patterns.split(", "):
            trie.add(pattern)
    
        designs = design_data.splitlines()
    
        # saves the design / sub-design -> number of component pattern combinations
        memo = {}
    
        def backtrack(design: str):
            nonlocal trie
    
            # if design is empty, we have successfully 
            #   matched the caller design / sub-design
            if design == "":
                return 1
            # use memo if available
            if design in memo:
                return memo[design]
    
            # start matching a new pattern from here
            node = trie.root
            # number of pattern combinations for this design
            pattern_comb_count = 0
            for i in range(len(design)):
                # if design[0 : i+1] is not a valid pattern,
                #   we are done matching characters
                if design[i] not in node.children:
                    break
                # move along the pattern
                node = node.children[design[i]]
                # we reached the end of a pattern
                if node.end:
                    # get the pattern combinations count for the rest of the design / sub-design
                    # all of them count for this design / sub-design
                    pattern_comb_count += backtrack(design[i + 1 :])
    
            # save the pattern combinations count for this design / sub-design
            memo[design] = pattern_comb_count
            return pattern_comb_count
    
        pattern_comb_counts = []
        for design in designs:
            pattern_comb_counts.append(backtrack(design))
        return pattern_comb_counts
    
    
    assert sum(1 for dc in soln("sample.txt") if dc > 0) == 6
    print("Part 1:", sum(1 for dc in soln("input.txt") if dc > 0))
    
    assert sum(soln("sample.txt")) == 16
    print("Part 2:", sum(soln("input.txt")))