• 1 Post
  • 13 Comments
Joined 11 months ago
cake
Cake day: August 8th, 2023

help-circle
  • This felt ... too simple. I think the hardest part of part two for me was reading comprehension. My errors were typically me not reading exactly was there.

    Python
    import re
    import math
    import argparse
    import itertools
    
    def int_hash(string:str) -> int:
        hash = 0
        for c in [*string]:
            hash += ord(c)
            hash *= 17
            hash = hash % 256
        return hash
    
    class Instruction:
        def __init__(self,string:str) -> None:
            label,action,strength = re.split('([-=])',string)
            self.label = label
            self.action = action
            if not strength:
                strength = 0
            self.strength = int(strength)
        
        def __repr__(self) -> str:
            return f"Instruction(l={self.label}, a={self.action}, s={self.strength})"
        
        def __str__(self) -> str:
            stren = str(self.strength if self.strength > 0 else '')
            return f"{self.label}{self.action}{stren}"
    
    
    class Lens:
        def __init__(self,label:str,focal_length:int) -> None:
            self.label:str = label
            self.focal_length:int = focal_length
    
        def __repr__(self) -> str:
            return f"Lens(label:{self.label},focal_length:{self.focal_length})"
        
        def __str__(self) -> str:
            return f"[{self.label} {self.focal_length}]"
    
    def main(line_list:str,part:int):
        init_sequence = line_list.splitlines(keepends=False)[0].split(',')
        sum = 0
        focal_array = dict[int,list[Lens]]()
        for i in range(0,256):
            focal_array[i] = list[Lens]()
        for s in init_sequence:
            hash_value = int_hash(s)
            sum += hash_value
    
            # part 2 stuff
            action = Instruction(s)
            position = int_hash(action.label)
            current_list = focal_array[position]
            existing_lens = list(filter(lambda x:x.label == action.label,current_list))
            if len(existing_lens) > 1:
                raise Exception("multiple of same lens in box, what do?")
            match action.action:
                case '-':
                    if len(existing_lens) == 1:
                        current_list.remove(existing_lens[0])
                case '=':
                    if len(existing_lens) == 0:
                        current_list.append(Lens(action.label,action.strength))
                    if len(existing_lens) == 1:
                        existing_lens[0].focal_length = action.strength
                case _:
                    raise Exception("unknown action")
    
        print(f"Part1: {sum}")
        #print(focal_array)
    
        sum2 = 0
        for i,focal_box in focal_array.items():
            for l,lens in enumerate(focal_box):
                sum2 += ( (i+1) * (l+1) * lens.focal_length )
    
        print(f"Part2: {sum2}")
    
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description="template for aoc solver")
        parser.add_argument("-input",type=str)
        parser.add_argument("-part",type=int)
        args = parser.parse_args()
        filename = args.input
        if filename == None:
            parser.print_help()
            exit(1)
        part = args.part
        file = open(filename,'r')
        main(file.read(),part)
        file.close()
    

  • When you substitute the first two question marks with ##, the answer already doesn’t match the input string, so you can throw away 1M of the combinations that don’t fit.

    You know I figured I was already doing that, but printing your example shows I was not. I also added some logic to the other end since 1,2,2 needs a space of 7 and if the check is all dots and I only have 6 chars left, I know it can't fit.

    still taking a long time for the real data so it must be something inefficient in my code then, rather than the method.

    Also, while you’re at it, avoid generic type annotations

    Good point. Recently figured that one out, still not automatic as you can see.

    Thanks for the pointers.




  • I saw that coming, but decided to do it the naive way for part 1, then fixed that up for part 2. Thanks to AoC I can also recognise a Manhattan distance written in a complex manner.

    Python
    from __future__ import annotations
    
    import re
    import math
    import argparse
    import itertools
    
    def print_sky(sky:list):
        for r in sky:
            print("".join(r))
    
    class Point:
        def __init__(self,x:int,y:int) -> None:
            self.x = x
            self.y = y
    
        def __repr__(self) -> str:
            return f"Point({self.x},{self.y})"
    
        def distance(self,point:Point):
            # Manhattan dist
            x = abs(self.x - point.x)
            y = abs(self.y - point.y)
            return x + y
    
    def expand_galaxies(galaxies:list,position:int,amount:int,index:str):
        for g in galaxies:
            if getattr(g,index) > position:
                c = getattr(g,index)
                setattr(g,index, c + amount)
    
    def main(line_list:list,part:int):
        ## list of lists is the plan for init idea
    
        expand_value = 2 -1
        if part == 2:
            expand_value = 1e6 -1
        if part > 2:
            expand_value = part -1
    
        sky = list()
        for l in line_list:
            row_data = [*l]
            sky.append(row_data)
        
        print_sky(sky)
        
        # get galaxies
        gal_list = list()
        for r in range(0,len(sky)):
            for c in range(0,len(sky[r])):
                if sky[r][c] == '#':
                    gal_list.append(Point(r,c))
    
        print(gal_list)
    
        col_indexes = list(reversed(range(0,len(sky))))
        # expand rows
        for i in col_indexes:
            if not '#' in sky[i]:
                expand_galaxies(gal_list,i,expand_value,'x')
    
        # check for expanding columns
        for i in reversed( range(0, len( sky[0] )) ):
            col = [sky[x][i] for x in col_indexes]
            if not '#' in col:
                expand_galaxies(gal_list,i,expand_value,'y')
    
        print(gal_list)
    
        # find all unique pair distance sum, part 1
        sum = 0
        for i in range(0,len(gal_list)):
            for j in range(i+1,len(gal_list)):
                sum += gal_list[i].distance(gal_list[j])
    
        print(f"Sum distances: {sum}")
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description="template for aoc solver")
        parser.add_argument("-input",type=str)
        parser.add_argument("-part",type=int)
        args = parser.parse_args()
        filename = args.input
        if filename == None:
            parser.print_help()
            exit(1)
        part = args.part
        file = open(filename,'r')
        main([line.rstrip('\n') for line in file.readlines()],part)
        file.close()
    

  • I always felt I was one fix away from the solution, which was both nice and bad.

    Walking the path was fine, and part 2 looked easy until I missed the squeezed pipes. I for some silly reason thought I only had to expand the grid by x2 instead of x3 and had to re-do that. Fill is hyper bad but works for <1 minute.

    Python
    import re
    import math
    import argparse
    import itertools
    from enum import Flag,Enum
    
    class Connection(Flag):
        Empty = 0b0000
        North = 0b0001
        South = 0b0010
        East = 0b01000
        West = 0b10000
    
    def connected_directions(first:Connection,second:Connection) -> bool:
        return bool(((first.value >> 1) &amp; second.value) or
                ((first.value &lt;&lt; 1) &amp; second.value))
    
    def opposite_direction(dir:Connection) -> Connection:
        if dir.value &amp; 0b00011:
            return Connection(dir.value ^ 0b00011)
        if dir.value &amp; 0b11000:
            return Connection(dir.value ^ 0b11000)
        return Connection(0)
    
    class PipeElement:
        def __init__(self,symbol:chr) -> None:
            self.symbol = symbol
            self.connection = Connection.Empty
            if symbol in [*'|LJS']:
                self.connection |= Connection.North
            if symbol in [*'|7FS']:
                self.connection |= Connection.South
            if symbol in [*'-LFS']:
                self.connection |= Connection.East
            if symbol in [*'-J7S']:
                self.connection |= Connection.West
            if self.connection == Connection.Empty:
                self.symbol = '.'
    
        def __repr__(self) -> str:
            return f"Pipe({self.connection})"
        
        def __str__(self) -> str:
            return self.symbol
    
        def connected_to(self,pipe,direction:Connection) -> bool:
            if not (self.connection &amp; direction):
                return False
            
            if self.connection &amp; direction and pipe.connection &amp; opposite_direction(direction):
                return True
            
            return False
            
    class Navigator:
        def __init__(self,list:list,width) -> None:
            self.list = list
            self.width = width
    
        def at(self,position):
            return self.list[position]
        
        def neighbor(self,position,direction:Connection) -> tuple | None:
            match direction:
                case Connection.North:
                    return self.prev_row(position)
                case Connection.South:
                    return self.next_row(position)
                case Connection.East:
                    return self.next(position)
                case Connection.West:
                    return self.prev(position)
            raise Exception(f"Direction not found: {direction}")
    
        def prev_row(self,position) -> tuple | None:
            p = position - self.width
            if p &lt; 0:
                return None
            return (p,self.list[p])
    
        def next_row(self,position) -> tuple | None:
            p = position + self.width
            if p >= len(self.list):
                return None
            return (p,self.list[p])
        
        def prev(self,position) -> tuple | None:
            p = position - 1
            if p &lt; 0:
                return None
            return (p,self.list[p])
    
        def next(self,position) -> tuple | None:
            p = position + 1
            if p >= len(self.list):
                return None
            return (p,self.list[p])
        
        def all_neighbors(self,position) -> list:
            l = list()
            for f in [self.next, self.prev, self.next_row,self.prev_row]:
                t = f(position)
                if t != None:
                    l.append(t)
            return l
        
        def find_connected(self,position,exclude=Connection.Empty) -> tuple | None:
            for dir in [Connection.East,Connection.West,Connection.North,Connection.South]:
                if dir == exclude:
                    continue
    
                n = self.neighbor(position,dir)
                if n == None:
                    continue
    
                if self.at(position).connected_to(n[1],dir):
                    return (*n,dir)
            return None
    
    class TileType(Enum):
        Inside = 1
        Outside = 0
        Pipe = 2
        PlaceHolder = 3
    
    def pipe_to_tile_expand(pipe:PipeElement) -> list:
        s = str(pipe)
        expansions = {
            '.': '.PP'+ 'PPP' + 'PPP',
            '-': 'PPP'+ '---' + 'PPP',
            '|': 'P|P'+ 'P|P' + 'P|P',
            'F': 'PPP'+ 'PF-' + 'P|P',
            '7': 'PPP'+ '-7P' + 'P|P',
            'J': 'P|P'+ '-JP' + 'PPP',
            'L': 'P|P'+ 'PL-' + 'PPP',
            'S': 'P|P'+ '-S-' + 'P|P'
            }
        l = expansions[s]
        return [pipe_to_tile(x) for x in [*l]]
    def pipe_to_tile(pipe:str) -> TileType:
        expansions = {
            '.': TileType.Inside,
            '-': TileType.Pipe,
            '|': TileType.Pipe,
            'F': TileType.Pipe,
            '7': TileType.Pipe,
            'J': TileType.Pipe,
            'L': TileType.Pipe,
            'S': TileType.Pipe,
            'P': TileType.PlaceHolder
            }
        return expansions[pipe]
    
    def chunks(lst, n):
        """Yield successive n-sized chunks from lst."""
        for i in range(0, len(lst), n):
            yield lst[i:i + n]
    
    def print_tiles(tile_list:list,width:int):
        for c in chunks(tile_list,width):
            print("".join([str(t.value) for t in c]))
    
    def print_pipes(tile_list:list,width:int):
        for c in chunks(tile_list,width):
            print("".join([str(t) for t in c]))
    
    def main(line_list:list,part:int):
        width = None
    
        pipe_list = list()
        tile_list = list()
        start_o = None
        for line in line_list:
            line = line + ' ' # stops east/west joining over new lines
            if width == None:
                width = len(line)
            for c in [*line]:
                o = PipeElement(c)
                pipe_list.append(o)
                tile_list.append(TileType.Inside)
                if c == 'S':
                    start_o = o
        #print(pipe_list)
        start_pos = pipe_list.index(start_o)
        start_co = (start_pos // width, start_pos % width)
        print(f"starting index: {start_pos}: {start_co}")
    
        nav = Navigator(pipe_list,width)
    
        cur_pos = None
        last_dir = Connection.Empty
        steps = 0
        while cur_pos != start_pos:
            if cur_pos == None:
                cur_pos = start_pos
            
            pipe = nav.find_connected(cur_pos,exclude=opposite_direction(last_dir))
            if pipe == None:
                raise Exception(f"end of pipe at: {cur_pos}, {nav.at(cur_pos)}")
            cur_pos = pipe[0]
            last_dir = pipe[2]
            steps += 1
            #print(f"{cur_pos}->",end="")
    
            tile_list[cur_pos] = TileType.Pipe
        
        print(f"end: {cur_pos}, steps: {steps}")
    
        clean_pipe = list()
        for i in range(0,len(pipe_list)):
            if tile_list[i] == TileType.Pipe:
                clean_pipe.append(pipe_list[i])
            else:
                clean_pipe.append(PipeElement('.'))
    
        print_pipes(clean_pipe,width)
        print(f"part 1: {steps/2}")
    
        # part 2 outputs
        #print("start tile:")
        #print_tiles(tile_list,width)
    
        # add outsides to edge of map
        tile_list2 = list()
        #first row
        expanded_width = (width*3)+2
        for i in range(0,expanded_width):
            tile_list2.append(TileType.Outside)
        for row in chunks(clean_pipe, width):
            ## we need to expand this to 2x size tiles
            t_rows = [ list() for x in range(0,3)]
            [ x.append(TileType.Outside) for x in t_rows]
            for r in row:
                parts = pipe_to_tile_expand(r)
                [ t_rows[x].extend( parts[x*3:(x*3)+3] ) for x in range(0,3)]
            [ x.append(TileType.Outside) for x in t_rows]
            [ tile_list2.extend(x) for x in t_rows]
        for i in range(0,expanded_width):
            tile_list2.append(TileType.Outside)
    
        #print("with o tile:")
        #print_tiles(tile_list2,width+2)
    
        tilenav = Navigator(tile_list2,expanded_width)
        changes = True
        while changes == True:
            changes = False
            count_in = 0
            
            for i in range(0,len(tile_list2)):
                t = tilenav.at(i)
                if t == TileType.Inside or t == TileType.PlaceHolder:
                    n = tilenav.all_neighbors(i)
                    if any([x[1] == TileType.Outside for x in n]):
                        tilenav.list[i] = TileType.Outside
                        changes = True
                        continue
                    if t == TileType.Inside:
                        count_in += 1
    
        print("with outside tile:")
        print_tiles(tile_list2,expanded_width)
        print(count_in)
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description="template for aoc solver")
        parser.add_argument("-input",type=str)
        parser.add_argument("-part",type=int)
        args = parser.parse_args()
        filename = args.input
        if filename == None:
            parser.print_help()
            exit(1)
        part = args.part
        file = open(filename,'r')
        main([line.rstrip('\n') for line in file.readlines()],part)
        file.close()
    

  • Using a class here actually made part 2 super simple, just copy and paste a function. Initially I was a bit concerned about what part 2 would be, but looking at the lengths of the input data, there looked to be a resonable limit to how many additional rows there could be.

    python
    import re
    import math
    import argparse
    import itertools
    
    #https://stackoverflow.com/a/1012089
    def iter_item_and_next(iterable):
        items, nexts = itertools.tee(iterable, 2)
        nexts = itertools.chain(itertools.islice(nexts, 1, None), [None])
        return zip(items, nexts)
    
    class Sequence:
        def __init__(self,sequence:list) -> None:
            self.list = sequence
            if all([x == sequence[0] for x in sequence]):
                self.child:Sequence = ZeroSequence(len(sequence)-1)
                return
            
            child_sequence = list()
            for cur,next in iter_item_and_next(sequence):
                if next == None:
                    continue
                child_sequence.append(next - cur)
    
            if len(child_sequence) > 1:
                self.child:Sequence = Sequence(child_sequence)
                return
            
            # can't do diff on single item, use zero list
            self.child:Sequence = ZeroSequence(1)
    
        def __repr__(self) -> str:
            return f"Sequence([{self.list}], Child:{self.child})"
    
        def getNext(self) -> int:
            if self.child == None:
                new = self.list[-1]
            else: 
                new = self.list[-1] + self.child.getNext()
    
            self.list.append(new)
            return new
        
        def getPrevious(self) -> int:
            if self.child == None:
                new = self.list[0]
            else: 
                new = self.list[0] - self.child.getPrevious()
    
            self.list.insert(0,new)
            return new
    
    class ZeroSequence(Sequence):
        def __init__(self,count) -> None:
            self.list = [0]*count
            self.child = None
    
        def __repr__(self) -> str:
            return f"ZeroSequence(length={len(self.list)})"
    
        def getNext(self) -> int:
            self.list.append(0)
            return 0
        
        def getPrevious(self) -> int:
            self.list.append(0)
            return 0
    
    def parse_line(string:str) -> list:
        return [int(x) for x in string.split(' ')]
    
    def main(line_list):
        data = [Sequence(parse_line(x)) for x in line_list]
        print(data)
    
        # part 1
        total = 0
        for d in data:
            total += d.getNext()
        print("Part 1 After:")
        print(data)
        print(f"part 1 total: {total}")
    
        # part 2
        total = 0
        for d in data:
            total += d.getPrevious()
        print("Part 2 After:")
        print(data)
        print(f"part 2 total: {total}")
    
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description="day 1 solver")
        parser.add_argument("-input",type=str)
        parser.add_argument("-part",type=int)
        args = parser.parse_args()
        filename = args.input
        if filename == None:
            parser.print_help()
            exit(1)
        file = open(filename,'r')
        main([line.rstrip('\n') for line in file.readlines()])
        file.close()
    

  • This wasn't too bad. Had a worried moment when the part 2 solution took more than half a second. Maybe a better solution that brute forcing all the joker combinations, but it worked.

    Python
    import re
    import argparse
    import itertools
    from enum import Enum
    
    rule_jokers_enabled = False
    
    class CardType(Enum):
        HighCard = 1
        OnePair = 2
        TwoPair = 3
        ThreeOfAKind = 4
        FullHouse = 5
        FourOfAKind = 6
        FiveOfAKind = 7
    
    class Hand:
        def __init__(self,cards:str,bid:int) -> None:
            self.cards = cards
            self.bid = int(bid)
            if rule_jokers_enabled:
                self.type = self._find_type_joker(cards)
            else:
                self.type = self._find_type(cards)
    
        def _find_type(self,cards:str) -> CardType:
            # group cards based on card counts
            card_list = [*cards]
            card_list.sort()
            grouping = itertools.groupby(card_list,lambda x:x)
            lengths = [len(list(x[1])) for x in grouping]
            if 5 in lengths:
                return CardType.FiveOfAKind
            if 4 in lengths:
                return CardType.FourOfAKind
            if 3 in lengths and 2 in lengths:
                return CardType.FullHouse
            if 3 in lengths:
                return CardType.ThreeOfAKind
            if len([x for x in lengths if x == 2]) == 2:
                return CardType.TwoPair
            if 2 in lengths:
                return CardType.OnePair
            return CardType.HighCard
        
        def _find_type_joker(self,cards:str) -> CardType:
            try:
                joker_i = cards.index("J") 
            except ValueError:
                return self._find_type(cards)
            
            current_value = CardType.HighCard
            for new_card in [*(valid_card_list())]:
                if new_card == "J":
                    continue
                test_cards = list(cards)
                test_cards[joker_i] = new_card
                new_value = self._find_type_joker("".join(test_cards))
                if new_value.value > current_value.value:
                    current_value = new_value
            
            return current_value
    
        
        def sort_string(self):
            v = str(self.type.value) + ":" + "".join(["abcdefghijklmnoZ"[card_value(x)] for x in [*self.cards]])
            return v
        
        def __repr__(self) -> str:
            return f""
    
    
    
    def valid_card_list() -> str:
        if rule_jokers_enabled:
            return "J23456789TQKA"
        return "23456789TJQKA"
    
    def card_value(char:chr):
        return valid_card_list().index(char)
    
    def main(line_list: list):
        hand_list = list()
        for l in line_list:
            card,bid = re.split(' +',l)
            hand = Hand(card,bid)
            hand_list.append(hand)
            #print(hand.sort_string())
        
        hand_list.sort(key=lambda x: x.sort_string())
        print(hand_list)
    
        rank_total = 0
        rank = 1
        for single_hand in hand_list:
            rank_total += rank * single_hand.bid
            rank += 1
        
        print(f"total {rank_total}")
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description="day 1 solver")
        parser.add_argument("-input",type=str)
        parser.add_argument("-part",type=int)
        args = parser.parse_args()
    
        if args.part == 2:
            rule_jokers_enabled = True
    
        filename = args.input
        if filename == None:
            parser.print_help()
            exit(1)
        file = open(filename,'r')
        main([line.rstrip('\n') for line in file.readlines()])
        file.close()
    

  • That was so much better than yesterday. Went with algebra but looks like brute force would have worked.

    python
    import re
    import argparse
    import math
    
    # i feel doing this with equations is probably the
    # "fast" way.
    
    # we can re-arange stuff so we only need to find the point
    # the line crosses the 0 line
    
    # distance is speed * (time less time holding the button (which is equal to speed)):
    # -> d = v * (t - v)
    # -> v^2 -vt +d = 0
    
    # -> y=0 @ v = t +- sqrt( t^2 - 4d) / 2
    
    def get_cross_points(time:int, distance:int) -> list | None:
        pre_root = time**2 - (4 * distance)
        if pre_root &lt; 0:
            # no solutions
            return None
        if pre_root == 0:
            # one solution
            return [(float(time)/2)]
        sqroot = math.sqrt(pre_root)
        v1 = (float(time) + sqroot)/2
        v2 = (float(time) - sqroot)/2
        return [v1,v2]
    
    def float_pair_to_int_pair(a:float,b:float):
        # if floats are equal to int value, then we need to add one to value
        # as we are looking for values above 0 point
        
        if a > b:
            # lower a and up b
            if a == int(a):
                a -= 1
            if b == int(b):
                b += 1
    
            return [math.floor(a),math.ceil(b)]
        if a &lt; b:
            if a == int(a):
                a += 1
            if b == int(b):
                b -= 1
            return [math.floor(b),math.ceil(a)]
    
    def main(line_list: list):
        time_section,distance_section = line_list
        if (args.part == 1):
            time_list = filter(None , re.split(' +',time_section.split(':')[1]))
            distance_list = filter(None ,  re.split(' +',distance_section.split(':')[1]))
            games = list(zip(time_list,distance_list))
        if (args.part == 2):
            games = [ [time_section.replace(' ','').split(':')[1],distance_section.replace(' ','').split(':')[1]] ]
        print (games)
        total = 1
        for t,d in games:
            cross = get_cross_points(int(t),int(d))
            cross_int = float_pair_to_int_pair(*cross)
            print (cross_int)
            total *= cross_int[0] - cross_int[1] +1
        
        print(f"total: {total}")
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description="day 6 solver")
        parser.add_argument("-input",type=str)
        parser.add_argument("-part",type=int)
        args = parser.parse_args()
        filename = args.input
        if filename == None:
            parser.print_help()
            exit(1)
        file = open(filename,'r')
        main([line.rstrip('\n') for line in file.readlines()])
        file.close()
    

  • Finally. :cries:

    Part 1 was fine, I was figuring I might be able to practice classes.

    Part 2 told me that nope, memory management required for you. In the end instead of calculating seeds, I resolved the whole thing down to a single mapping of seeds to locations. Then I could just sort by location ranges and try to see if they were a seed. Code is full of old parts from failed solutions but I've had enough of day 5, so I no longer care to clean it up.