2020/23.py
def part2():
    """
    The first key insight is that there are three operations for each iteration that all
    need to be constant time.

    1. Remove the three elements to the right of current.
    2. Finding the destination element.
    3. Inserting the three picked elements once the destination is found.

    At first I attempted to implement all of this using a dequeue (double-ended queue)
    because it can do both (1) and (3) in O(1) time. However, this doesn't work because,
    (2) is still O(n), which isn't good enough.

    The second key insight is that the three above properties can be achieved via a
    linked list, *if you can find implement a way to find the destination element in
    constant time*. This can be accomplished if you have a mapping of label to node.

    My solution uses an implicit linked-list (implemented using a dictionary) that
    allows me to accomplish the same thing conceptually, without an explicit extra
    mapping.
    """
    MAX_2 = 1_000_000
    cups = deepcopy(CUPS) + [i for i in range(MAX_CUP + 1, MAX_2 + 1)]

    # Initialize the "linked list" with the initial "pointers" from each cup to the next
    # one.
    linked_list = {}  # cup_label -> cup_label
    for x, y in zip(cups, cups[1:]):
        linked_list[x] = y
    linked_list[cups[-1]] = cups[0]  # wrap-around from the end to the beginning

    # The current cup is whatever is at the head of the cups linked list.
    current = cups[0]

    for i in range(10_000_000):
        if debug and i % 10_000 == 0:
            print(i)

        # Because each node of the linked list is in the dictionary, we can actually
        # find the element within the linked list by just looking it up in the
        # dictionary.

        # Here, we get the element that the current element points to.
        # For example, here let's say our linked list looks like this:
        #   1 -> 5 -> 6 -> 7 -> 9 -> 4 -> 8 -> 2 -> 3
        # If "current" is 5, then
        picked1 = linked_list[current]  # will give 6
        picked2 = linked_list[picked1]  # thus this will give 7
        picked3 = linked_list[picked2]  # and this will give 9
        first_not_picked = linked_list[picked3]  # and this will give 4
        picked = (picked1, picked2, picked3)

        # First, remove the next 3 by bypassing them with a pointer change
        linked_list[current] = first_not_picked
        #   1 -> 5 -.  6 -> 7 -> 9 -> 4 -> 8 -> 2 -> 3
        #           |                 ^
        #           '-----------------'

        # Compute the destination node (same logic as part 1)
        destination = current - 1
        if destination < MIN_CUP:
            destination = MAX_2
        while destination in picked:
            destination -= 1
            if destination < MIN_CUP:
                destination = MAX_2

        # Find the element after the destination. In the example case, the destination
        # is going to be 4. We need to splice in the picked elements back into the list
        # right after the destination. The first step is to point the end of the picked
        # list at the node that the destination points to.
        linked_list[picked3] = linked_list[destination]
        #   1 -> 5 -.  6 -> 7 -> 9 -.  4 -> 8 -> 2 -> 3
        #           |               |  ^    ^
        #           '---------------+--'    |
        #                           '-------'

        # The next step in spiciling in the list is to point the destination at the
        # beginning of the picked list.
        linked_list[destination] = picked1
        #   1 -> 5 -.  6 -> 7 -> 9 -.  4 -.  8 -> 2 -> 3
        #           |  ^            |  ^  |  ^
        #           |  |            |  |  |  |
        #           '--+------------+--'  |  |
        #              |            '-----+--'
        #              '------------------'

        # If we unravel this, then we get:
        #   1 -> 5 -> 4 -> 6 -> 7 -> 9 -> 8 -> 2 -> 3

        # Finally, all we do is set the new current node to the node that is directly to
        # the right of the current node. It is guaranteed that this will be the first
        # node that wasn't spliced out.
        current = first_not_picked

    print(linked_list[1], linked_list[linked_list[1]])
    return linked_list[1] * linked_list[linked_list[1]]

    The first key insight is that there are three operations for each iteration that all
    need to be constant time.

    1. Remove the three elements to the right of current.
    2. Finding the destination element.
    3. Inserting the three picked elements once the destination is found.

    At first I attempted to implement all of this using a dequeue (double-ended queue)
    because it can do both (1) and (3) in O(1) time. However, this doesn't work because,
    (2) is still O(n), which isn't good enough.

    The second key insight is that the three above properties can be achieved via a
    linked list, *if you can find implement a way to find the destination element in
    constant time*. This can be accomplished if you have a mapping of label to node.

    My solution uses an implicit linked-list (implemented using a dictionary) that
    allows me to accomplish the same thing conceptually, without an explicit extra
    mapping.
    
The first key insight is that there are three operations for each iteration that all
need to be constant time.

1. Remove the three elements to the right of current.
2. Finding the destination element.
3. Inserting the three picked elements once the destination is found.

At first I attempted to implement all of this using a dequeue (double-ended queue)
because it can do both (1) and (3) in O(1) time. However, this doesn't work because,
(2) is still O(n), which isn't good enough.

The second key insight is that the three above properties can be achieved via a
linked list, *if you can find implement a way to find the destination element in
constant time*. This can be accomplished if you have a mapping of label to node.

My solution uses an implicit linked-list (implemented using a dictionary) that
allows me to accomplish the same thing conceptually, without an explicit extra
mapping.
The first key insight is that there are three operations for each iteration that all need to be constant time . 1 . Remove the three elements to the right of current . 2 . Finding the destination element . 3 . Inserting the three picked elements once the destination is found . At first I attempted to implement all of this using a dequeue ( double - ended queue ) because it can do both ( 1 ) and ( 3 ) in O ( 1 ) time . However this doesn ' t work because ( 2 ) is still O ( n ) which isn ' t good enough . The second key insight is that the three above properties can be achieved via a linked list * if you can find implement a way to find the destination element in constant time * . This can be accomplished if you have a mapping of label to node . My solution uses an implicit linked - list ( implemented using a dictionary ) that allows me to accomplish the same thing conceptually without an explicit extra mapping .
def part2():
    MAX_2 = 1_000_000
    cups = deepcopy(CUPS) + [i for i in range(MAX_CUP + 1, MAX_2 + 1)]
    linked_list = {}  
    for x, y in zip(cups, cups[1:]):
        linked_list[x] = y
    linked_list[cups[-1]] = cups[0]  
    current = cups[0]
    for i in range(10_000_000):
        if debug and i % 10_000 == 0:
            print(i)
        picked1 = linked_list[current]  
        picked2 = linked_list[picked1]  
        picked3 = linked_list[picked2]  
        first_not_picked = linked_list[picked3]  
        picked = (picked1, picked2, picked3)
        linked_list[current] = first_not_picked
        destination = current - 1
        if destination < MIN_CUP:
            destination = MAX_2
        while destination in picked:
            destination -= 1
            if destination < MIN_CUP:
                destination = MAX_2
        linked_list[picked3] = linked_list[destination]
        linked_list[destination] = picked1
        current = first_not_picked
    print(linked_list[1], linked_list[linked_list[1]])
    return linked_list[1] * linked_list[linked_list[1]]
def part2 ( ) : MAX_2 = 1_000_000 cups = deepcopy ( CUPS ) + [ i for i in range ( MAX_CUP + 1 , MAX_2 + 1 ) ] linked_list = { } for x , y in zip ( cups , cups [ 1 : ] ) : linked_list [ x ] = y linked_list [ cups [ - 1 ] ] = cups [ 0 ] current = cups [ 0 ] for i in range ( 10_000_000 ) : if debug and i % 10_000 == 0 : print ( i ) picked1 = linked_list [ current ] picked2 = linked_list [ picked1 ] picked3 = linked_list [ picked2 ] first_not_picked = linked_list [ picked3 ] picked = ( picked1 , picked2 , picked3 ) linked_list [ current ] = first_not_picked destination = current - 1 if destination < MIN_CUP : destination = MAX_2 while destination in picked : destination -= 1 if destination < MIN_CUP : destination = MAX_2 linked_list [ picked3 ] = linked_list [ destination ] linked_list [ destination ] = picked1 current = first_not_picked print ( linked_list [ 1 ] , linked_list [ linked_list [ 1 ] ] ) return linked_list [ 1 ] * linked_list [ linked_list [ 1 ] ]
The first key insight is that there are three operations for each iteration that all
need to be constant time.
The first key insight is that there are three operations for each iteration that all need to be constant time .
"""
    The first key insight is that there are three operations for each iteration that all
    need to be constant time.

    1. Remove the three elements to the right of current.
    2. Finding the destination element.
    3. Inserting the three picked elements once the destination is found.

    At first I attempted to implement all of this using a dequeue (double-ended queue)
    because it can do both (1) and (3) in O(1) time. However, this doesn't work because,
    (2) is still O(n), which isn't good enough.

    The second key insight is that the three above properties can be achieved via a
    linked list, *if you can find implement a way to find the destination element in
    constant time*. This can be accomplished if you have a mapping of label to node.

    My solution uses an implicit linked-list (implemented using a dictionary) that
    allows me to accomplish the same thing conceptually, without an explicit extra
    mapping.
    """ # Initialize the "linked list" with the initial "pointers" from each cup to the next # one. # cup_label -> cup_label # wrap-around from the end to the beginning # The current cup is whatever is at the head of the cups linked list. # Because each node of the linked list is in the dictionary, we can actually # find the element within the linked list by just looking it up in the # dictionary. # Here, we get the element that the current element points to. # For example, here let's say our linked list looks like this: #   1 -> 5 -> 6 -> 7 -> 9 -> 4 -> 8 -> 2 -> 3 # If "current" is 5, then # will give 6 # thus this will give 7 # and this will give 9 # and this will give 4 # First, remove the next 3 by bypassing them with a pointer change #   1 -> 5 -.  6 -> 7 -> 9 -> 4 -> 8 -> 2 -> 3 #           |                 ^ #           '-----------------' # Compute the destination node (same logic as part 1) # Find the element after the destination. In the example case, the destination # is going to be 4. We need to splice in the picked elements back into the list # right after the destination. The first step is to point the end of the picked # list at the node that the destination points to. #   1 -> 5 -.  6 -> 7 -> 9 -.  4 -> 8 -> 2 -> 3 #           |               |  ^    ^ #           '---------------+--'    | #                           '-------' # The next step in spiciling in the list is to point the destination at the # beginning of the picked list. #   1 -> 5 -.  6 -> 7 -> 9 -.  4 -.  8 -> 2 -> 3 #           |  ^            |  ^  |  ^ #           |  |            |  |  |  | #           '--+------------+--'  |  | #              |            '-----+--' #              '------------------' # If we unravel this, then we get: #   1 -> 5 -> 4 -> 6 -> 7 -> 9 -> 8 -> 2 -> 3 # Finally, all we do is set the new current node to the node that is directly to # the right of the current node. It is guaranteed that this will be the first # node that wasn't spliced out.