2

I've been trying to solve MIT's Mathematics for Computer Science problem sets and here's one of the problems:

Each monk entering the Temple of Forever is given a bowl with 15 red beads and 12 green beads. Each time the Gong of Time rings, a monk must do one of two things:

  1. Exchange: If he has 3 red beads in his bowl, then he may exchange 3 red beads for 2 green beads.
  2. Swap: He may replace each green bead in his bowl with a red bead and replace each red bead in his bowl with a green bead. That is, if he starts with i red beads and j green beads, then after he performs this operation, he will have j red beads and i green beads.

A monk may leave the Temple of Forever only when he has exactly 5 red beads and 5 green beads in his bowl.

There are sub-problems to this:

  1. Prove that no one ever leaves the temple. This I proved using mathematical induction.
  2. Prove that there are only a finite number of states that can be reached by this problem. This proof was also done by using mathematical induction and proving that the sum of number of red beads and green beads can only ever reduce or stay the same.
  3. (This is where I am stuck) What is the true maximal number of unique states a monk can visit in any execution of the Temple of Forever machine?

After spending a considerable amount of time trying to think of sub-problem #3, I gave up and decided to write a program to count the number of unique states.

class Node:
    def __init__(self, r, g):
        self.r = r
        self.g = g

    def swap(self):
        if self.g <= 0 or self.r <= 0:
            return None
        return Node(self.g, self.r)

    def exchange(self):
        if self.r >= 3:
            return Node(self.r - 3, self.g + 2)
        return None

    def __hash__(self):
        return hash((self.r, self.g))

    def __eq__(self, other):
        if self is None and other is None:
            return True
        if self is None or other is None:
            return False
        return self.r == other.r and self.g == other.g

    def __repr__(self):
        return "({}, {})".format(self.r, self.g)

start = Node(15, 12)
queue = []
graph = []
visited = set()
queue.append(start)

while (queue):
    curr = queue.pop(0)
    visited.add(curr)
    graph.append(curr)
    s = curr.swap()
    e = curr.exchange()

    for el in [s, e]:
        if el != None:
            if el not in visited:
                queue.append(el)

print(visited, len(visited))

The answer I get from my program is

{(6, 9), (16, 9), (0, 7), (2, 5), (8, 5), (5, 8), (10, 8), (10, 7), (16, 3), (5, 18), (0, 17), (14, 1), (8, 15), (10, 13), (4, 16), (9, 16), (7, 5), (14, 2), (13, 10), (3, 1), (6, 13), (20, 3), (3, 11), (4, 12), (10, 3), (6, 14), (7, 15), (18, 5), (3, 6), (8, 6), (4, 1), (9, 7), (6, 4), (11, 4), (16, 4), (5, 17), (11, 9), (0, 18), (14, 6), (13, 6), (19, 2), (18, 6), (1, 19), (15, 7), (0, 8), (4, 11), (3, 5), (4, 6), (9, 2), (5, 7), (4, 17), (11, 3), (7, 4), (14, 12), (12, 4), (19, 1), (3, 15), (1, 3), (5, 13), (3, 21), (11, 14), (12, 9), (18, 1), (15, 12), (2, 19), (3, 10), (1, 14), (8, 10), (9, 11), (3, 16), (8, 16), (11, 13), (0, 22), (17, 5), (6, 18), (7, 14), (12, 14), (19, 6), (15, 3), (2, 20), (1, 4), (0, 12), (1, 9), (4, 2), (2, 14), (9, 6), (5, 3), (6, 8), (11, 8), (16, 8), (14, 7), (13, 5), (1, 18), (2, 4), (9, 12), (4, 7), (9, 1), (12, 5), (15, 8), (0, 3), (2, 9), (8, 1), (5, 12), (3, 20), (10, 12), (6, 3), (9, 17), (7, 10), (12, 10), (13, 11), (1, 13), (8, 11), (2, 10), (0, 23), (17, 4), (6, 19), (14, 11), (12, 15), (7, 9), (13, 1), (17, 9), (15, 2), (20, 2), (0, 13), (21, 3), (1, 8), (2, 15), (5, 2), (10, 2)} 129

So, 129. But when I look at the solution of the problem set (for sub-problem #3), here is what it states

Each move in the sequence must be either an exchange or swap, since these are the only legal moves. Now, whenever the monk performs an exchange operation, the sum r + g decreases by one.

(r - 3) + (g + 2) = (r + g) - 1

In contrast, swaps do not have any effect on the sum. Furthermore, we know that the sum r + g must be at least 3 to perform an exchange operation. Therefore, there can be at most 25 exchange operations in the sequence.

Now consider swap operations: between each pair of exchanges in the sequence, there may be an unlimited number of swaps. However, only a single swap can take the monk to a new state: if at step k the monk is in state (r, g), then at step k + 2, he will return to the same state. Therefore, an upper bound on the number of unique states in any execution of the machine is 25 + 26 + 1 = 52 (if swaps are inserted at both the beginning and end of the sequence).

Where did my program go wrong? Is my understanding of the problem statement incorrect (wrt the program I've written)? Also, I don't really understand the solution they've given. Is there a better way to explain it? For example, one of the issues/things I don't understand about it is, the solution states that the sum of beads reduces by 1 with every exchange operation. And therefore we can get 25 new states with exchange operations. But every sum at each level of the graph can be expressed in multiple ways, yes? So there must be more states created from exchange operations? Here's a link to the full problem set and it's solution.

Arne
  • 9,990
  • 2
  • 18
  • 28
  • I'm unsure if this goes on here or math stackexchange. If required, I can delete it here and post it there. – Apoorva Anand Apr 12 '20 at 18:26
  • "And therefore we can get 25 new states with exchange operations." No, the solution does not say this. Be careful to distinguish between the sequence of visited states and the set of unique states. – Arne Apr 12 '20 at 22:28
  • You're right. The solution states "there can be AT MOST 25 exchange operations in the sequence", but then towards the end they also state "An upper bound on the number of unique states in ANY execution of the machine is 25 + 25 + 1 = 52", unless I'm misunderstanding this as well, doesn't this statement tell us that the number of elements in the set of unique states is at most 52? But my program, with a BFS like execution of the state machine gives me a set of 129 unique states? – Apoorva Anand Apr 12 '20 at 22:35
  • Yes, I understand there is a contradiction between the given solution and your program's results. All I'm saying is the quoted proof looks convincing to me, but I don't know where you went wrong in your code. – Arne Apr 12 '20 at 22:40
  • Maybe some visual intuition can help: Think of the states as points in an x-y-grid. x is the number of red beads, y is the number of green beads. Then the only possible state changes are 'mirror across the diagonal' (swap) or 'go left 3 and up 2' (exchange). With this in mind it may be easier to follow the proof you quoted. – Arne Apr 12 '20 at 22:47
  • Every swap that creates a new state can create it's own set of unique states that are possible through exchange operations, no? I find it hard to believe that there can be only 52 unique states. – Apoorva Anand Apr 12 '20 at 22:57
  • I do too. I wrote a [simple program](https://repl.it/repls/WorthlessWarmAbstractions) and got 140. I don't think your program is quite correct, but 52 seems impossible by any count. – ggorlen Apr 12 '20 at 23:24
  • My program gives the same result if I allow swaps when either r or g is 0. If I disallow that, I get my result 129. – Apoorva Anand Apr 12 '20 at 23:33
  • Yeah I noticed that that's the difference taking a closer look at your code. There must be some misunderstanding of the problem, because 52 is too fundamentally different. – ggorlen Apr 12 '20 at 23:40

2 Answers2

1

Edit: In the light of OP's own answer and our discussion in the comments, this was the key issue:

We have to distinguish between two different numbers:

  1. The maximum number n of visited states in any one path the monk can take;
  2. The overall number N of states the monk could reach.

Note that N is the cardinality of the union (taken over all possible paths) of the sets of states visited in any one path. This implies n <= N, and it's easy to see that these numbers are not equal. The MIT question asked about n, whereas OP's original code was designed to find N.


The quoted proof is correct, so "an upper bound on [n] is 25 + 26 + 1 = 52".

I tried a Monte Carlo approach to approximate N: Decide randomly whether to exchange or swap whenever there is a choice, repeat until the process oscillates between (2, 0) and (0, 2), and repeat all of this a huge number of times, while keeping track of all unique visited states.

However, this does not appear to be practical, because the number of possible paths is too large, so the number we get does not come close to N with any feasible number of iterations. The following code already took about 15 minutes on my machine.

import random

def swap(i, j):
    i, j = j, i
    return i, j

def exchange(i, j):
    i, j = i - 3, j + 2
    return i, j

x, y = 15, 12
visited = {(x, y)}

for run in range(1_000_000_000):
    while x + y > 2:
        if x < 3:
            x, y = swap(x, y)
        else:
            coinflip = random.randint(0, 1)
            if coinflip == 0:
                x, y = swap(x, y)
            else:
                x, y = exchange(x, y)
        visited = visited.union({(x, y)})

    x, y = swap(x, y)    
    visited = visited.union({(x, y)})

print('Visited states:', visited)
print('Number of visited states:', len(visited))

Visited states: {(18, 0), (4, 7), (1, 3), (3, 0), (0, 2), (4, 12), (11, 14), (2, 5), (0, 3), (8, 5), (5, 8), (15, 12), (8, 1), (16, 3), (5, 18), (1, 14), (14, 1), (3, 16), (8, 16), (4, 1), (12, 14), (2, 20), (0, 18), (2, 10), (1, 4), (1, 19), (4, 2), (17, 4), (5, 3), (14, 11), (4, 6), (15, 2), (20, 2), (16, 8), (4, 17), (11, 3), (3, 1), (7, 4), (14, 12), (1, 8), (12, 4), (2, 0), (19, 1), (5, 2), (2, 4), (10, 2)}

Number of visited states: 46

Update: Here's a plot of the full state space. N = 140

full state space

And here's a path visiting 52 states. The green X is the starting point, and every blue circle marks a visited state. Since we know from the quoted proof that n <= 52, this proves n = 52.

52 states visited in one path

Arne
  • 9,990
  • 2
  • 18
  • 28
  • Arne! Thank you very much. Your answer, although you say "is not a complete answer" made me immediately realize where I was going wrong. There's nothing wrong with my code, simply in my understanding of the question. – Apoorva Anand Apr 13 '20 at 01:34
  • @Apoorva Anand, great, please share the result if you can make your code fit the question. – Arne Apr 13 '20 at 01:42
1

The graph being built by the code in the question connects EVERY POSSIBLE state and thus counts the total number of states possible by the state machine but NOT the maximal number of unique states a monk can visit in ANY execution of the Temple of Forever machine.

By counting the nodes in a BFS-like fashion, I make the assumption that it is possible to reach every state through any other state. This is not so. After an exchange operation creates a new state, it is not possible to go back to it's "parent" state through a swap (obviously), and thus once the choice has been made at the ring of a gong, there isn't any going back.

So, what we really have to do is build the graph and do a DFS from the starting node, and keep track of the most depth we've achieved during the DFS traversal. This maximum depth is the solution we want.

Here's the code:

class Node:
    def __init__(self, r, g):
        self.r = r
        self.g = g

    def swap(self):
        ## if self.g <= 0 or self.r <= 0:
            ## return None
        ## Swaps with 0 allowed (only by commenting
        ## out the code above I get 52)
        return Node(self.g, self.r)

    def exchange(self):
        if self.r >= 3:
            return Node(self.r - 3, self.g + 2)
        return None

    def neighbours(self):
        s = self.swap()
        e = self.exchange()
        n = []
        for el in [s, e]:
            if el is not None:
                n.append(el)
        return n

    def __hash__(self):
        return hash((self.r, self.g))

    def __eq__(self, other):
        if self is None and other is None:
            return True
        if self is None or other is None:
            return False
        return self.r == other.r and self.g == other.g

    def __repr__(self):
        return "({}, {})".format(self.r, self.g)

start = Node(15, 12)
dfs_visited = set()
max_len = [0]  ## need a mutable data structure

def dfs(s, curr_len):
    for n in s.neighbours():
        if curr_len > max_len[0]:
            max_len[0] = curr_len
        if n not in dfs_visited:
            dfs_visited.add(n)
            dfs(n, curr_len+1)
    return max_len[0]

print(dfs(start, 0))

52