Sean McLemon | Advent of Code

Home | Czech | Blog | GitHub | Advent Of Code | Notes


2019-12-22 - Slam Shuffle

(original .ipynb)

Day 22 puzzle input is an set of instructions on how to shuffle a set of cards (mine is here). Part 1 involves performing the shuffle according to these instructions and finding the card at index 2019. Part 2 involved performing this shuffle for an insanely huge deck, many times over. This involved something I had never seen before, so I ended up consulting the reddit and to be honest I still don't properly get it.

def new_deck(size):
    return [ c for c in range(size) ]

def deal_new_stack(deck):
    deck = [ c for c in reversed(deck) ]
    return deck

def cut(deck, n):
    cut_cards = []
    return deck[n:] + deck[:n]

def deal_with_increment(deck, increment):
    new_deck = [ None for d in deck ]
    position = 0
    
    while deck:
        card = deck.pop(0)
        new_deck[position] = card
        position = (position + increment) % len(new_deck)
        
    return new_deck    


def parse_deal_instruction(instruction_str):
    
    deal_increment_instr = "deal with increment "
    cut_instr = "cut "
    deal_new_stack_instr = "deal into new stack"
    
    if instruction_str.startswith(deal_increment_instr):
        increment = int(instruction_str[len(deal_increment_instr):])        
        return lambda deck: deal_with_increment(deck, increment)
    elif instruction_str.startswith(cut_instr):
        cut_index = int(instruction_str[len(cut_instr):])
        return lambda deck: cut(deck, cut_index)
    elif instruction_str.startswith(deal_new_stack_instr):
        return lambda deck: deal_new_stack(deck)
    else:
        raise Exception(f"couldn't parse instruction {instruction_str}")
# part 1

puzzle_input = open("puzzle_input/day22.txt", "r").read()
stack = new_deck(10007)

for instruction_str in puzzle_input.split("\n"):
    instruction = parse_deal_instruction(instruction_str)
#     print(instruction_str)
    stack = instruction(stack)
    
print(stack.index(2019))
6526
# part 2 - what is in position 2020 after doing this for 119315717514047 cards
#          a total of 101741582076661 times.
#
# So obviously we cannot model this directly - we'd need to do 100 transforms of
# about ~100 trillion numbers 101741582076661 times which would be ridiculous.
#
# I thought this might also be possible to implement using generators which are all
# chained together, starting with range(), then next()-ing 2020 times. I don't think
# this is possible though - "deal with increment" interleaves the cards which I think 
# complicates all of this.
#
# After hammering on this problem it turns out this requires "modular arithmatic" 
# which I never learned :-/ I read a couple of articles (https://codeforces.com/blog/entry/72593
# and https://work.njae.me.uk/2020/01/12/advent-of-code-2019-day-22) but mostly 
# followed along someone else's solution. I don't know this mathematics and I honestly
# probably won't :(

def deal_with_increment_modular(deck_size, a, b, increment):
    z = pow(increment, deck_size - 2, deck_size)
    a = a * z % deck_size
    b = b * z % deck_size
    return (a, b)

def cut_modular(deck_size, a, b, cut_index):
    b = (b + cut_index) % deck_size
    return (a, b)

def deal_new_stack_modular(deck_size, a, b):
    a = -a
    b = deck_size - b - 1
    return (a, b)

def parse_deal_instruction_modular(deck_size, instruction_str):    
    deal_increment_instr = "deal with increment "
    cut_instr = "cut "
    deal_new_stack_instr = "deal into new stack"
    
    if instruction_str.startswith(deal_increment_instr):
        increment = int(instruction_str[len(deal_increment_instr):])        
        return lambda a, b: deal_with_increment_modular(deck_size, a, b, increment)
    elif instruction_str.startswith(cut_instr):
        cut_index = int(instruction_str[len(cut_instr):])
        return lambda a, b: cut_modular(deck_size, a, b, cut_index)
    elif instruction_str.startswith(deal_new_stack_instr):
        return lambda a, b: deal_new_stack_modular(deck_size, a, b)
    else:
        raise Exception(f"couldn't parse instruction {instruction_str}")

def polypow(a, b, m, n):
    if m == 0:
        return 1,0
    if m % 2 == 0:
        return polypow(a * a % n, (a * b + b) % n, m // 2, n)
    else:
        c, d = polypow(a, b, m - 1, n)
        return (a * c % n, (a * d + b) % n)
        
deck_size = 119315717514047
shuffle_count = 101741582076661

a = 1
b = 0
deal_actions = [ parse_deal_instruction_modular(deck_size, instruction_str) for instruction_str in puzzle_input.split("\n") ]
for deal_action in reversed(deal_actions):
    a, b = deal_action(a, b)

a, b = polypow(a, b, shuffle_count, deck_size)
print((2020 * a + b) % deck_size)
79855812422607