May 2024
This page has been viewed 1122 times.
Recently, I was invited to Google's secret coding interview, Foobar Challange. From Markov chains to
cellular automata, this challenge was the hardest and most fun coding experience I have ever had. I have
decided to share my solutions with the animations I have made for questions, which make them easier to
understand.
TL;DR? Check the GitHub repository.
Go back to the blogsOne day, while I was working on my projects, a message appeared at the top of my Google search results. I don't exactly remember what I was searching for, but I guess it was about multi-threading.
After clicking the message, I was redirected to a website, and the challenge has started. The challenge consists of 5 levels and 9 questions in total.
3.1. Prepare the Bunnies' Escape
4.2. Bringing a Gun to a Trainer Fight
For context, we are in a space station where Commander Lambda is on charge, and we are trying to help the bunnies used as workers.
There's some unrest in the minion ranks: minions with ID numbers like "1", "42", and other "good" numbers have been lording it over the poor minions who are stuck with more boring IDs. To quell the unrest, Commander Lambda has tasked you with reassigning everyone new random IDs based on a Completely Foolproof Scheme.
Commander Lambda has concatenated the prime numbers in a single long string: "2357111317192329...". Now every minion must draw a number from a hat. That number is the starting index in that string of primes, and the minion's new ID number will be the next five digits in the string. So if a minion draws "3", their ID number will be "71113".
Help the Commander assign these IDs by writing a function solution(n) which takes in the starting index n of Lambda's string of all primes, and returns the next five digits in the string. Commander Lambda has a lot of minions, so the value of n will always be between 0 and 10000.
Test cases
Input:
solution.solution(0)
Output:
23571
Input:
solution.solution(3)
Output:
71113
This one is pretty straight-forward. Since the question states that the value of n will always be between 0 and 10000, we can simply create a string to store the prime numbers until the length of the string is 10005. Then we can ask for the n value and return the next 5 digits from the string.
To find the prime numbers, we can use a for loop. Starting from 2, we can check if the number is divisible by any other number. If it is not divisible by any number, then it is a prime number. We can add this number to our string.
It is not possible to calculate prime numbers without brute forcing (for now, 2024), so if you come up with a formula to find the prime numbers, congratulations on your Nobel Prize.
It is also possible to use i/2
as the upper limit of the range in the for loop for better
optimization, since a number is not divisible by any number greater than half of itself.
# Creating a string of prime numbers
prime_string = ""
# Finding the primes until the length of the string is 10005
i = 2
while len(prime_string) < 10005:
for j in range(2, i):
if i % j == 0:
break
else:
prime_string += str(i)
i += 1
def solution(n):
# Returning the next five digits in the string
return prime_string[n:n+5]
Commander Lambda uses an automated algorithm to assign minions randomly to tasks, in order to keep minions on their toes. But you've noticed a flaw in the algorithm -- it eventually loops back on itself, so that instead of assigning new minions as it iterates, it gets stuck in a cycle of values so that the same minions end up doing the same tasks over and over again. You think proving this to Commander Lambda will help you make a case for your next promotion.
You have worked out that the algorithm has the following process:
1) Start with a random minion ID n, which is a nonnegative integer of length k in base b
2) Define x and y as integers of length k. x has the digits of n in descending order, and y has the
digits of n in ascending order
3) Define z = x - y. Add leading zeros to z to maintain length k if necessary
4) Assign n = z to get the next minion ID, and go back to step 2
For example, given minion ID n = 1211, k = 4, b = 10, then x = 2111, y = 1112 and z = 2111 - 1112 = 0999. Then the next minion ID will be n = 0999 and the algorithm iterates again: x = 9990, y = 0999 and z = 9990 - 0999 = 8991, and so on.
Depending on the values of n, k (derived from n), and b, at some point the algorithm reaches a cycle, such as by reaching a constant value. For example, starting with n = 210022, k = 6, b = 3, the algorithm will reach the cycle of values [210111, 122221, 102212] and it will stay in this cycle no matter how many times it continues iterating. Starting with n = 1211, the routine will reach the integer 6174, and since 7641 - 1467 is 6174, it will stay as that value no matter how many times it iterates.
Given a minion ID as a string n representing a nonnegative integer of length k in base b, where 2 <= k <= 9 and 2 <= b <= 10, write a function solution(n, b) which returns the length of the ending cycle of the algorithm above starting with n. For instance, in the example above, solution(210022, 3) would return 3, since iterating on 102212 would return to 210111 when done in base 3. If the algorithm reaches a constant, such as 0, then the length is 1.
Test cases
Input:
solution.solution('1211', 10)
Output:
1
Input:
solution.solution('210022', 3)
Output:
3
My approach to this question is to create a list to store the values of z and check if the new value of z is in the list after each iteration. If it is, then we can return the length of the list minus the index of the new value of z. If it is not, then we can add the new value of z to the list and continue the iteration.
To find the value of z, we can convert the string n to a list of integers, sort it in descending order to find x, sort it in ascending order to find y, and then find z by subtracting y from x. We can convert the list of integers back to a string and add leading zeros if necessary to maintain the length of k.
Python's int
function has a base
parameter that can be used to convert a string to
an
integer, with the base parameter specifying the base of the number in the string.
print(int('210111', 3)) # Prints 580
However, Python doesn't have a built-in function to convert an integer to a string with a specific base. We
can create a
function to do this by using the divmod
function to find the quotient and remainder of the
division.
def to_base_b(n, b):
if n == 0:
return '0'
numbers = []
while n:
n, r = divmod(n, b)
numbers.append(str(r))
return ''.join(reversed(numbers))
We know that 580 in base 3 is 210111. Let's test our function with this example.
print(to_base_b(580, 3)) # Prints 210111
Now we can implement our algorithm to find the length of the ending cycle of the algorithm.
def to_base_b(n, b):
"""
Converts an integer n to a string in base b.
"""
if n == 0:
return '0'
numbers = []
while n:
n, r = divmod(n, b)
numbers.append(str(r))
return ''.join(reversed(numbers))
def solution(n, b):
list_of_z = []
while True:
# Converting n to a string
n = str(n)
# Creating x by sorting n in descending order
x = ''.join(sorted(n, reverse=True))
# Creating y by sorting n in ascending order
y = ''.join(sorted(n))
# Converting x and y to base b
x = int(x, b)
y = int(y, b)
# Calculating z
z = x - y
# Converting z to base b
z = to_base_b(z, b)
# Adding leading zeros to z to maintain length k if necessary
while len(z) < len(n):
z = '0' + z
# Checking if z is in the list of z's
if z in list_of_z:
return len(list_of_z) - list_of_z.index(z)
else:
list_of_z.append(z)
n = z
You've been assigned the onerous task of elevator maintenance -- ugh! It wouldn't be so bad, except that all the elevator documentation has been lying in a disorganized pile at the bottom of a filing cabinet for years, and you don't even know what elevator version numbers you'll be working on.
Elevator versions are represented by a series of numbers, divided up into major, minor and revision integers. New versions of an elevator increase the major number, e.g. 1, 2, 3, and so on. When new features are added to an elevator without being a complete new version, a second number named "minor" can be used to represent those new additions, e.g. 1.0, 1.1, 1.2, etc. Small fixes or maintenance work can be represented by a third number named "revision", e.g. 1.1.1, 1.1.2, 1.2.0, and so on. The number zero can be used as a major for pre-release versions of elevators, e.g. 0.1, 0.5, 0.9.2, etc (Commander Lambda is careful to always beta test her new technology, with her loyal henchmen as subjects!).
Given a list of elevator versions represented as strings, write a function solution(l) that returns the same list sorted in ascending order by major, minor, and revision number so that you can identify the current elevator version. The versions in list l will always contain major numbers, but minor and revision numbers are optional. If the version contains a revision number, then it will also have a minor number.
For example, given the list l as ["1.1.2", "1.0", "1.3.3", "1.0.12", "1.0.2"], the function solution(l) would return the list ["1.0", "1.0.2", "1.0.12", "1.1.2", "1.3.3"]. If two or more versions are equivalent but one version contains more numbers than the others, then these versions must be sorted ascending based on how many numbers they have, e.g ["1", "1.0", "1.0.0"]. The number of elements in the list l will be at least 1 and will not exceed 100.
Test cases
Input:
solution.solution(["1.11", "2.0.0", "1.2", "2", "0.1", "1.2.1", "1.1.1", "2.0"])
Output:
0.1,1.1.1,1.2,1.2.1,1.11,2,2.0,2.0.0
Input:
solution.solution(["1.1.2", "1.0", "1.3.3", "1.0.12", "1.0.2"])
Output:
1.0,1.0.2,1.0.12,1.1.2,1.3.3
Surprisingly easy question. Python's sort
method has a key
parameter that can be
used to sort a list of strings based on a custom key function. We can define a custom key function that
splits the string on the '.' character, converts each part of the split string to an integer, and returns
the list of integer parts. This way, we can sort the list of strings based on the major, minor, and revision
numbers.
def sort_key(s):
"""
Splits the string on the '.' character,
converts each part of the split string to an integer,
and returns the list of integer parts.
"""
# Splitting the string on the '.' character
split_string = s.split('.')
# Converting each part of the split string to an integer
integer_parts = map(int, split_string)
return list(integer_parts)
def solution(l):
# Sorting the list using the defined key function
l.sort(key=sort_key)
# Returning the sorted list
return l
You're awfully close to destroying the LAMBCHOP doomsday device and freeing Commander Lambda's bunny workers, but once they're free of the work duties the bunnies are going to need to escape Lambda's space station via the escape pods as quickly as possible. Unfortunately, the halls of the space station are a maze of corridors and dead ends that will be a deathtrap for the escaping bunnies. Fortunately, Commander Lambda has put you in charge of a remodeling project that will give you the opportunity to make things a little easier for the bunnies. Unfortunately (again), you can't just remove all obstacles between the bunnies and the escape pods - at most you can remove one wall per escape pod path, both to maintain structural integrity of the station and to avoid arousing Commander Lambda's suspicions.
You have maps of parts of the space station, each starting at a work area exit and ending at the door to an escape pod. The map is represented as a matrix of 0s and 1s, where 0s are passable space and 1s are impassable walls. The door out of the station is at the top left (0,0) and the door into an escape pod is at the bottom right (w-1,h-1).
Write a function solution(map) that generates the length of the shortest path from the station door to the escape pod, where you are allowed to remove one wall as part of your remodeling plans. The path length is the total number of nodes you pass through, counting both the entrance and exit nodes. The starting and ending positions are always passable (0). The map will always be solvable, though you may or may not need to remove a wall. The height and width of the map can be from 2 to 20. Moves can only be made in cardinal directions; no diagonal moves are allowed.
Test cases
Input:
solution.solution([[0, 1, 1, 0], [0, 0, 0, 1], [1, 1, 0, 0], [1, 1, 1, 0]])
Output:
7
Input:
solution.solution([[0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1], [0,
1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0]])
Output:
11
Here is where things get interesting. After this question, the difficulty of the questions increases rapidly, which makes them more fun to solve.
My approach to this question is to create every single possible maze and use brute force. I defined a
function called wall_breaker
, which creates mazes by replacing a different 1 with 0 every step.
Then we can
feed the astar
function, which solves the mazes by using the astar algorithm. After that we can
return the
shortest one.
At first, I tried solving the mazes using the BFS algorithm, but it failed some of the test cases. Later, I noticed that BFS fails to solve the mazes unless the path is some sort of tunnel.
def astar(map):
"""
A* algorithm to find the shortest path in a binary maze,
starting from the top left and ending at the bottom right.
"""
start = (0, 0)
goal = (len(map) - 1, len(map[0]) - 1)
# Defining movement directions: up, down, left, right
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# Initializing open and closed lists
open_list = []
closed_set = set()
# Adding start node to open list
open_list.append((0, start, 0)) # (total_cost, node, g_cost)
while open_list:
open_list.sort(key=lambda x: x[0]) # Sort by total_cost
total_cost, current_node, g_cost = open_list.pop(0)
closed_set.add(current_node)
# Checking if current node is goal node
if current_node == goal:
return g_cost + 1
# Generating successors (neighbors) of current node
for dr, dc in directions:
neighbor_row, neighbor_col = current_node[0] + dr, current_node[1] + dc
# Checking if neighbor is within bounds and not a wall
if 0 <= neighbor_row < len(map) and 0 <= neighbor_col < len(map[0]) and map[neighbor_row][neighbor_col] == 0:
neighbor_node = (neighbor_row, neighbor_col)
# Calculating heuristic (h value)
heuristic = abs(neighbor_row - goal[0]) + abs(neighbor_col - goal[1])
# Calculating total cost (f value)
total_cost = g_cost + 1 + heuristic # Cost to move to neighbor is 1
# Checking if neighbor is already in closed set
if neighbor_node in closed_set:
continue
# Checking if neighbor is already in open list and has a lower total cost
for i, (tc, n, _) in enumerate(open_list):
if n == neighbor_node and total_cost >= tc:
break
else:
open_list.append((total_cost, neighbor_node, g_cost + 1))
# If no path found
return len(map) * len(map[0])
def wall_breaker(map):
"""
For a given map, generates all possible maps with
breaking one wall at a time (Converting 1 to 0) and returns the dictionary
of the newly generated maps.
"""
new_maps = {}
# Before creating new maps, we append the original map to the dictionary
new_maps["original_map"] = map
for i in range(len(map)):
for j in range(len(map[0])):
if map[i][j] == 1:
new_map = [row[:] for row in map]
new_map[i][j] = 0
new_maps["map_{0}_{1}".format(i, j)] = new_map
return new_maps
def solution(map):
# Get all possible maps by breaking one wall at a time
new_maps = wall_breaker(map)
# Finding the shortest path for each map
shortest_paths = {}
for key, value in new_maps.items():
shortest_paths[key] = astar(value)
# Returning the minimum path length
return min(shortest_paths.values())
Making fuel for the LAMBCHOP's reactor core is a tricky process because of the exotic matter involved. It starts as raw ore, then during processing, begins randomly changing between forms, eventually reaching a stable form. There may be multiple stable forms that a sample could ultimately reach, not all of which are useful as fuel.
Commander Lambda has tasked you to help the scientists increase fuel creation efficiency by predicting the end state of a given ore sample. You have carefully studied the different structures that the ore can take and which transitions it undergoes. It appears that, while random, the probability of each structure transforming is fixed. That is, each time the ore is in 1 state, it has the same probabilities of entering the next state (which might be the same state). You have recorded the observed transitions in a matrix. The others in the lab have hypothesized more exotic forms that the ore can become, but you haven't seen all of them.
Write a function solution(m) that takes an array of array of nonnegative ints representing how many times that state has gone to the next state and return an array of ints for each terminal state giving the exact probabilities of each terminal state, represented as the numerator for each state, then the denominator for all of them at the end and in simplest form. The matrix is at most 10 by 10. It is guaranteed that no matter which state the ore is in, there is a path from that state to a terminal state. That is, the processing will always eventually end in a stable state. The ore starts in state 0. The denominator will fit within a signed 32-bit integer during the calculation, as long as the fraction is simplified regularly.
For example, consider the matrix m:
[
[0,1,0,0,0,1], # s0, the initial state, goes to s1 and s5 with equal probability
[4,0,0,3,2,0], # s1 can become s0, s3, or s4, but with different probabilities
[0,0,0,0,0,0], # s2 is terminal, and unreachable (never observed in practice)
[0,0,0,0,0,0], # s3 is terminal
[0,0,0,0,0,0], # s4 is terminal
[0,0,0,0,0,0], # s5 is terminal
]
So, we can consider different paths to terminal states, such as:
s0 -> s1 -> s3
s0 -> s1 -> s0 -> s1 -> s0 -> s1 -> s4
s0 -> s1 -> s0 -> s5
Tracing the probabilities of each, we find that
s2 has probability 0
s3 has probability 3/14
s4 has probability 1/7
s5 has probability 9/14
So, putting that together, and making a common denominator, gives an answer in the form of
[s2.numerator, s3.numerator, s4.numerator, s5.numerator, denominator] which is
[0, 3, 2, 9, 14].
Test cases
Input:
solution.solution([[0, 2, 1, 0, 0], [0, 0, 0, 3, 4], [0, 0, 0, 0, 0], [0, 0, 0, 0,0], [0, 0, 0, 0, 0]])
Output:
[7, 6, 8, 21]
Input:
solution.solution([[0, 1, 0, 0, 0, 1], [4, 0, 0, 3, 2, 0], [0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0], [0,
0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]])
Output:
[0, 3, 2, 9, 14]
This question reminds me of my stochastic differential equation lectures from my university, which makes it my favorite question in this challenge. Aside from fancy stuff, the problem can be reduced to an absorbing Markov chain. Watch this video series to learn how to solve the equation.
Let's look at the absorbing Markov chain in our example. The given matrix is:
\begin{bmatrix} 0 & 1 & 0 & 0 & 0 & 1 \\ 4 & 0 & 0 & 3 & 2 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ \end{bmatrix}
We can get the probabilities by dividing each number by the sum of the rows. Visualization can make it easier to grasp.
We need to convert this matrix to a transition matrix with probabilities. In the code,
the create_transition_matrix
function handles this conversion. Notice the terminal states are
in the first rows.
\begin{bmatrix} 1 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1/2 & 0 & 1/2 \\ 0 & 1/3 & 2/9 & 0 & 4/9 & 0 \\ \end{bmatrix}
Now we have I, $0$, R, and Q matrices.
\[ I = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix} % , % \:\: % 0 = % \begin{bmatrix} 0 & 0 \\ 0 & 0 \\ 0 & 0 \\ 0 & 0 \\ \end{bmatrix} \] \[ R = \begin{bmatrix} 0 & 0 & 0 & 1/2 \\ 0 & 1/3 & 2/9 & 0 \\ \end{bmatrix} % , % \:\: % Q = % \begin{bmatrix} 0 & 1/2 \\ 4/9 & 0 \\ \end{bmatrix} \]
We can calculate the matrix F.
\begin{align} F &= (I - Q)^{-1} = \left( \begin{bmatrix} 1 & 0 \\ 0 & 1 \\ \end{bmatrix} - \begin{bmatrix} 0 & 1/2 \\ 4/9 & 0 \\ \end{bmatrix} \right)^{-1} \\ &= \begin{bmatrix} 1 & -1/2 \\ -4/9 & 1 \\ \end{bmatrix}^{-1} = \begin{bmatrix} 9/7 & 9/14 \\ 4/7 & 9/7 \\ \end{bmatrix} \end{align}
Finally, we can calculate the absorbing matrix FR.
\begin{align} FR = F \cdot R &= \begin{bmatrix} 9/7 & 9/14 \\ 4/7 & 9/7 \\ \end{bmatrix} \cdot \begin{bmatrix} 0 & 0 & 0 & 1/2 \\ 0 & 1/3 & 2/9 & 0 \\ \end{bmatrix} \\ &= \begin{bmatrix} 0 & 3/14 & 1/7 & 9/14 \\ 0 & 3/7 & 2/7 & 2/7 \\ \end{bmatrix} \\ \end{align}
Check out the first row of the FR matrix. These are the probabilities we are looking for. Making a common denominator gives us the answer in the form of [s2.numerator, s3.numerator, s4.numerator, s5.numerator, denominator], which is [0, 3, 2, 9, 14].
You will also notice the greatest_common_divisor
and least_common_multiple
functions in the code, which are used to find the common denominator.
from fractions import Fraction
def create_transition_matrix(m):
"""
Converts the given matrix to a transition matrix with probabilities.
Creates the form
I | 0
--|--
R | Q
where I is the identity matrix, 0 is the zero matrix, R and Q are the submatrices.
To achieve the desired form, we need to do the following:
1. Convert the given numbers to probabilities by dividing each number by the sum of the row.
2. Determine the terminal states (rows with all zeros).
3. Convert the 0 in the self directed positions to 1.
4. Reorder the columns and rows where the terminal states are first.
"""
# Convert the given numbers to probabilities
for i in range(len(m)):
row_sum = sum(m[i])
for j in range(len(m[i])):
m[i][j] /= row_sum if row_sum != 0 else 1
# Determine the terminal states
terminal_states = []
non_terminal_states = []
for i in range(len(m)):
if sum(m[i]) == 0:
terminal_states.append(i)
else:
non_terminal_states.append(i)
# Convert the 0 in the self directed positions to 1
for i in range(len(m)):
if i in terminal_states:
m[i][i] = 1
# Reorder the columns and rows where the terminal states are first
new_m = []
for i in terminal_states:
new_m.append([m[i][j] for j in terminal_states + non_terminal_states])
for i in non_terminal_states:
new_m.append([m[i][j] for j in terminal_states + non_terminal_states])
# Return the transition matrix and the number of terminal states
return new_m, len(terminal_states)
def inverse_matrix(matrix):
"""
Returns the inverse of the given matrix using the Gauss-Jordan elimination method.
"""
size = len(matrix)
inverse = [[0 if row != column else 1 for column in range(size)] for row in range(size)]
for i in range(size):
if matrix[i][i] == 0:
for j in range(i + 1, size):
if matrix[j][i] != 0:
matrix[i], matrix[j] = matrix[j], matrix[i]
inverse[i], inverse[j] = inverse[j], inverse[i]
break
else:
raise ValueError("Matrix is not invertible")
for j in range(i + 1, size):
ratio = matrix[j][i] / matrix[i][i]
for k in range(size):
matrix[j][k] -= ratio * matrix[i][k]
inverse[j][k] -= ratio * inverse[i][k]
for i in range(size - 1, -1, -1):
for j in range(i - 1, -1, -1):
ratio = matrix[j][i] / matrix[i][i]
for k in range(size):
matrix[j][k] -= ratio * matrix[i][k]
inverse[j][k] -= ratio * inverse[i][k]
div = matrix[i][i]
for k in range(size):
matrix[i][k] /= div
inverse[i][k] /= div
return inverse
def calculate_fr(transition_matrix, terminal_states):
"""
Calculates the fundamental matrix and the absorbing matrix from the given transition matrix.
"""
# Split the transition matrix into the fundamental matrix and the absorbing matrix
Q = []
R = []
# The matrix is in the form of
# I | 0
# --|--
# R | Q
# so R is on the bottom left and Q is on the bottom right
# Q is the submatrix of transition_matrix containing transitions between non-terminal states
# R is the submatrix of transition_matrix containing transitions from non-terminal states to terminal states
for i in range(terminal_states, len(transition_matrix)):
Q.append(transition_matrix[i][terminal_states:])
R.append(transition_matrix[i][:terminal_states])
# Calculate the fundamental matrix
I = []
for i in range(len(Q)):
I.append([0] * len(Q))
I[i][i] = 1
# F = (I - Q)^-1
F = []
for i in range(len(Q)):
F.append([I[i][j] - Q[i][j] for j in range(len(Q))])
F = inverse_matrix(F)
# Calculate the absorbing matrix which is F*R
FR = []
for i in range(len(F)):
FR.append([0] * len(R[0]))
for j in range(len(R[0])):
for k in range(len(R)):
FR[i][j] += F[i][k] * R[k][j]
return FR
def greates_common_divisor(list_of_numbers):
"""
Returns the greatest common divisor of the given numbers.
"""
if len(list_of_numbers) == 2:
a, b = list_of_numbers
while b:
a, b = b, a % b
return a
else:
return greates_common_divisor([list_of_numbers[0], greates_common_divisor(list_of_numbers[1:])])
def least_common_multiple(list_of_numbers):
"""
Returns the least common multiple of the given numbers.
"""
lcm = list_of_numbers[0]
for i in list_of_numbers[1:]:
lcm = lcm * i // greates_common_divisor([lcm, i])
return lcm
def solution(m):
if len(m) == 1:
return [1, 1]
# Convert the given matrix to a transition matrix with probabilities
transition_matrix, terminal_states_count = create_transition_matrix(m)
# Calculate the fundamental matrix and the absorbing matrix from the given transition matrix
FR = calculate_fr(transition_matrix, terminal_states_count)
# Converting the probabilities to fractions
FR = [[Fraction(x).limit_denominator() for x in FR[i]] for i in range(len(FR))]
# Since the starting state is 0, we can directly return the first row of the absorbing matrix
probabilities = FR[0]
# Now we need to find the least common multiple of the denominators
denominators = [x.denominator for x in probabilities]
lcm = least_common_multiple(denominators)
# Return the result
result = [x.numerator * (lcm // x.denominator) for x in probabilities] + [lcm]
return result
As a henchman on Commander Lambda's space station, you're expected to be resourceful, smart, and a quick thinker. It's not easy building a doomsday device and ordering the bunnies around at the same time, after all! In order to make sure that everyone is sufficiently quick-witted, Commander Lambda has installed new flooring outside the henchman dormitories. It looks like a chessboard, and every morning and evening you have to solve a new movement puzzle in order to cross the floor. That would be fine if you got to be the rook or the queen, but instead, you have to be the knight. Worse, if you take too much time solving the puzzle, you get "volunteered" as a test subject for the LAMBCHOP doomsday device!
To help yourself get to and from your bunk every day, write a function called solution(src, dest) which takes in two parameters: the source square, on which you start, and the destination square, which is where you need to land to solve the puzzle. The function should return an integer representing the smallest number of moves it will take for you to travel from the source square to the destination square using a chess knight's moves (that is, two squares in any direction immediately followed by one square perpendicular to that direction, or vice versa, in an "L" shape). Both the source and destination squares will be an integer between 0 and 63, inclusive, and are numbered like the example chessboard below:
-------------------------
| 0| 1| 2| 3| 4| 5| 6| 7|
-------------------------
| 8| 9|10|11|12|13|14|15|
-------------------------
|16|17|18|19|20|21|22|23|
-------------------------
|24|25|26|27|28|29|30|31|
-------------------------
|32|33|34|35|36|37|38|39|
-------------------------
|40|41|42|43|44|45|46|47|
-------------------------
|48|49|50|51|52|53|54|55|
-------------------------
|56|57|58|59|60|61|62|63|
-------------------------
Test cases
Input:
solution.solution(0, 1)
Output:
3
Input:
solution.solution(19, 36)
Output:
1
I have played a lot of chess. I mean, a lot. After reading the question, I grabbed my chessboard and the knight piece and tried to figure out a pattern, if there was any. Then I came up with the following table:
What you are seeing here is the minimum number of moves to reach that square from the square that the knight is on. You may notice the size of the table is greater than 8x8, which is the size of a chessboard. This is because I thought it would be easier to move the board on this map instead of recalculating the moves every time with different knight placements.
Then I noticed an exception in the pattern. If the knight is placed on the corner, the nearest corner becomes reachable after 4 moves.
And another exception is If the knight is placed on one diagonal away from the corner, the closest corner square becomes reachable in 4 moves.
Changing the grid according to these exceptions, I wrote the solution
function. You will also
notice the helper functions get_directions
, is_valid_move
, move
, and
navigator
in the code. These are used to find the directions from the source to the destination
and to move the knight on the grid. I am certain that there are more efficient ways to solve the navigation
part,
but I didn't want to dwell on it too much.
grid = [
[6,5,4,5,4,5,4,5,4,5,4,5,4,5,6,],
[5,4,5,4,3,4,3,4,3,4,3,4,5,4,5,],
[4,5,4,3,4,3,4,3,4,3,4,3,4,5,4,],
[5,4,3,4,3,2,3,2,3,2,3,4,3,4,5,],
[4,3,4,3,2,3,2,3,2,3,2,3,4,3,4,],
[5,4,3,2,3,4,1,2,1,4,3,2,3,4,5,],
[4,3,4,3,2,1,2,3,2,1,2,3,4,3,4,],
[5,4,3,2,3,2,3,0,3,2,3,2,3,4,5,],
[4,3,4,3,2,1,2,3,2,1,2,3,4,3,4,],
[5,4,3,2,3,4,1,2,1,4,3,2,3,4,5,],
[4,3,4,3,2,3,2,3,2,3,2,3,4,3,4,],
[5,4,3,4,3,2,3,2,3,2,3,4,3,4,5,],
[4,5,4,3,4,3,4,3,4,3,4,3,4,5,4,],
[5,4,5,4,3,4,3,4,3,4,3,4,5,4,5,],
[6,5,4,5,4,5,4,5,4,5,4,5,4,5,6,],
]
def get_directions(start, end, grid_size=8):
"""
Returns the directions from start to end in the grid.
"""
# Calculate row and column for start and end
start_row, start_col = divmod(start, grid_size)
end_row, end_col = divmod(end, grid_size)
# Calculate the number of steps in each direction
steps_right = end_col - start_col
steps_down = end_row - start_row
# Generate the directions based on the steps
directions = (
"R" * max(steps_right, 0)
+ "L" * max(-steps_right, 0)
+ "D" * max(steps_down, 0)
+ "U" * max(-steps_down, 0)
)
return directions
def is_valid_move(x, y, grid):
"""
Returns True if the move is valid, False otherwise.
"""
return 0 <= x < len(grid) and 0 <= y < len(grid[0])
def move(x, y, direction):
"""
Returns the new position after moving in the given direction.
"""
directions = {'U': (-1, 0), 'D': (1, 0), 'L': (0, -1), 'R': (0, 1)}
dx, dy = directions[direction]
new_x, new_y = x + dx, y + dy
return new_x, new_y
def navigator(path, grid):
"""
Returns the value of the destination cell after following the given path.
"""
current_x, current_y = 7, 7 # Starting position
for move_direction in path:
current_x, current_y = move(current_x, current_y, move_direction)
if not is_valid_move(current_x, current_y, grid):
return None # Invalid move, return None
return grid[current_x][current_y]
def solution(src, dest):
# There are few exceptions where grid is changed depending on the knight's position
# The knight being in the corners
if src in [0, 7, 56, 63]:
grid[6][6] = 4
grid[6][8] = 4
grid[8][6] = 4
grid[8][8] = 4
# The knight being in one diagonal away from the corners
elif src == 9:
grid[6][6] = 4
elif src == 14:
grid[6][8] = 4
elif src == 49:
grid[8][6] = 4
elif src == 54:
grid[8][8] = 4
else:
pass
# Finding directions from src to dest
directions = get_directions(src, dest)
min_moves = navigator(directions, grid)
return min_moves
You need to free the bunny workers before Commander Lambda's space station explodes! Unfortunately, the Commander was very careful with the highest-value workers -- they all work in separate, maximum-security work rooms. The rooms are opened by putting keys into each console, then pressing the open button on each console simultaneously. When the open button is pressed, each key opens its corresponding lock on the work room. So, the union of the keys in all of the consoles must be all of the keys. The scheme may require multiple copies of one key given to different minions.
The consoles are far enough apart that a separate minion is needed for each one. Fortunately, you have already relieved some bunnies to aid you - and even better, you were able to steal the keys while you were working as Commander Lambda's assistant. The problem is, you don't know which keys to use at which consoles. The consoles are programmed to know which keys each minion had, to prevent someone from just stealing all of the keys and using them blindly. There are signs by the consoles saying how many minions had some keys for the set of consoles. You suspect that Commander Lambda has a systematic way to decide which keys to give to each minion such that they could use the consoles.
You need to figure out the scheme that Commander Lambda used to distribute the keys. You know how many minions had keys, and how many consoles are by each work room. You know that Command Lambda wouldn't issue more keys than necessary (beyond what the key distribution scheme requires), and that you need as many bunnies with keys as there are consoles to open the work room.
Given the number of bunnies available and the number of locks required to open a work room, write a function solution(num_buns, num_required) which returns a specification of how to distribute the keys such that any num_required bunnies can open the locks, but no group of (num_required - 1) bunnies can.
Each lock is numbered starting from 0. The keys are numbered the same as the lock they open (so for a duplicate key, the number will repeat, since it opens the same lock). For a given bunny, the keys they get is represented as a sorted list of the numbers for the keys. To cover all of the bunnies, the final solution is represented by a sorted list of each individual bunny's list of keys. Find the lexicographically least such key distribution - that is, the first bunny should have keys sequentially starting from 0.
num_buns will always be between 1 and 9, and num_required will always be between 0 and 9 (both
inclusive). For example, if you had 3 bunnies and required only 1 of them to open the cell, you would
give each bunny the same key such that any of the 3 of them would be able to open it, like so:
[
[0],
[0],
[0],
]
If you had 2 bunnies and required both of them to open the cell, they would receive different keys
(otherwise they wouldn't both actually be required), and your solution would be as follows:
[
[0],
[1],
]
Finally, if you had 3 bunnies and required 2 of them to open the cell, then any 2 of the 3 bunnies
should have all of the keys necessary to open the cell, but no single bunny would be able to do it.
Thus, the solution would be:
[
[0, 1],
[0, 2],
[1, 2],
]
Test cases
Input:
solution.solution(4, 4)
Output:
[[0], [1], [2], [3]]
Input:
solution.solution(5, 3)
Output:
[[0, 1, 2, 3, 4, 5], [0, 1, 2, 6, 7, 8], [0, 3, 4, 6, 7, 9], [1, 3, 5, 6, 8, 9], [2, 4, 5, 7, 8, 9]]
Input:
solution.solution(2, 1)
Output:
[[0], [0]]
I think this question was the most challenging one for me in this challenge. I spent most of my time trying to understand what the question was asking. But it can be summarized like this:
Randomly select the required bunnies from the total bunnies. Selected bunnies should have all the unique keys together.
Say we have 5 bunnies in total and 3 required bunnies. If we select any 3 bunnies, they should have all the keys (from 0 to 9) together.
How do we find the number of unique keys? It is the number of combinations of the total bunnies with the required bunnies. In this case, it is $5 \choose 3$.
I have found these patterns by trial and error:
Assuming that we have 5 bunnies in total, we can get the following table using the formulas above:
Bunnies | Required bunnies | Total keys | Key per bunny | Copy per key | Unique key |
---|---|---|---|---|---|
5 | 1 | 5 | 1 | 5 | 1 |
5 | 2 | 20 | 4 | 4 | 5 |
5 | 3 | 30 | 6 | 3 | 10 |
5 | 4 | 20 | 4 | 2 | 10 |
5 | 5 | 5 | 1 | 1 | 5 |
But how do we assign the keys to the bunnies? After brainstorming for a long time, it clicked. We shouldn't
assign keys to bunnies; we should assign bunnies to key pairs. We can find the key pairs using the
combinations
function from the itertools
module.
from itertools import combinations
num_buns = 5
num_required = 3
replicas = num_buns - num_required + 1
print(list(combinations(range(num_buns), replicas))
# Output
# [(0, 1, 2), (0, 1, 3), (0, 1, 4), (0, 2, 3), (0, 2, 4), (0, 3, 4), (1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]
Then we can create a dictionary to store the keys for each bunny.
keys = {}
for i in range(num_buns):
keys[i] = []
print(keys)
# Output
# {0: [], 1: [], 2: [], 3: [], 4: []}
After that, we can assign the keys to the bunnies.
for i in range(len(key_pairs)):
for j in key_pairs[i]:
print('Assigning key', i, 'to bunny', j)
keys[j].append(i)
# Output
# Assigning key 0 to bunny 0
# Assigning key 0 to bunny 1
# Assigning key 0 to bunny 2
# Assigning key 1 to bunny 0
# Assigning key 1 to bunny 1
# Assigning key 1 to bunny 3
# Assigning key 2 to bunny 0
# Assigning key 2 to bunny 1
# Assigning key 2 to bunny 4
# Assigning key 3 to bunny 0
# Assigning key 3 to bunny 2
# Assigning key 3 to bunny 3
# Assigning key 4 to bunny 0
# Assigning key 4 to bunny 2
# Assigning key 4 to bunny 4
# Assigning key 5 to bunny 0
# Assigning key 5 to bunny 3
# Assigning key 5 to bunny 4
# Assigning key 6 to bunny 1
# Assigning key 6 to bunny 2
# Assigning key 6 to bunny 3
# Assigning key 7 to bunny 1
# Assigning key 7 to bunny 2
# Assigning key 7 to bunny 4
# Assigning key 8 to bunny 1
# Assigning key 8 to bunny 3
# Assigning key 8 to bunny 4
# Assigning key 9 to bunny 2
# Assigning key 9 to bunny 3
# Assigning key 9 to bunny 4
from itertools import combinations
def solution(num_buns, num_required):
# Tricky part is to figure out how many copies of each key to give to the bunnies
replicas = num_buns - num_required + 1
key_pairs = list(combinations(range(num_buns), replicas))
# Creating a dictionary to store the keys for each bunny
keys = {}
for i in range(num_buns):
keys[i] = []
# Assigning the keys to the bunnies
for i in range(len(key_pairs)):
for j in key_pairs[i]:
keys[j].append(i)
# Creating a list of the keys for each bunny
key_list = []
for i in range(num_buns):
key_list.append(keys[i])
return key_list
Uh-oh -- you've been cornered by one of Commander Lambdas elite bunny trainers! Fortunately, you grabbed a beam weapon from an abandoned storeroom while you were running through the station, so you have a chance to fight your way out. But the beam weapon is potentially dangerous to you as well as to the bunny trainers: its beams reflect off walls, meaning you'll have to be very careful where you shoot to avoid bouncing a shot toward yourself!
Luckily, the beams can only travel a certain maximum distance before becoming too weak to cause damage. You also know that if a beam hits a corner, it will bounce back in exactly the same direction. And of course, if the beam hits either you or the bunny trainer, it will stop immediately (albeit painfully).
Write a function solution(dimensions, your_position, trainer_position, distance) that gives an array of 2 integers of the width and height of the room, an array of 2 integers of your x and y coordinates in the room, an array of 2 integers of the trainer's x and y coordinates in the room, and returns an integer of the number of distinct directions that you can fire to hit the elite trainer, given the maximum distance that the beam can travel.
The room has integer dimensions [1 < x_dim <=1250, 1 < y_dim <=1250]. You and the elite trainer are both positioned on the integer lattice at different distinct positions (x, y) inside the room such that [0 < x < x_dim, 0 < y < y_dim]. Finally, the maximum distance that the beam can travel before becoming harmless will be given as an integer 1 < distance <=10000.
For example, if you and the elite trainer were positioned in a room with dimensions [3, 2], your_position [1, 1], trainer_position [2, 1], and a maximum shot distance of 4, you could shoot in seven different directions to hit the elite trainer (given as vector bearings from your location): [1, 0], [1, 2], [1, -2], [3, 2], [3, -2], [-3, 2], and [-3, -2]. As specific examples, the shot at bearing [1, 0] is the straight line horizontal shot of distance 1, the shot at bearing [-3, -2] bounces off the left wall and then the bottom wall before hitting the elite trainer with a total shot distance of sqrt(13), and the shot at bearing [1, 2] bounces off just the top wall before hitting the elite trainer with a total shot distance of sqrt(5).
Test cases
Input:
solution.solution([300,275], [150,150], [185,100], 500)
Output:
9
Input:
solution.solution([3,2], [1,1], [2,1], 4)
Output:
7
Thanks to the optics lectures in high school, figuring this out took me only a few minutes, but coding the solution wasn't that easy. The idea is to find the reflection points of the trainer and, depending on the range of the beam, find the possible shots to hit the trainer.
Like in the example given in the question, assume that you are on the point (1, 1), the trainer is on the point (2, 1), and the room is 3x2. When you find the reflections and draw a circle around yourself with a radius of 4, you will notice that there are 9 reflection points of the trainer inside the circle.
One thing to consider here is if there are any reflections of yourself between you and the trainer's reflection points. If there are, you should eliminate those shots, since the beam will hit you before hitting the trainer by bouncing off the walls.
Another one is to check if there are any reflections behind the trainer and the trainer's reflection points. The question is asking how many distinct directions you can fire to hit the trainer, so counting the reflections behind the trainer or the trainer's reflection points leads to the wrong results.
In our example, there are 2 reflection points that satisfy the conditions above. So the answer is 9-2=7.
I spent almost all of my time on this question, optimizing my approach. You will notice the helper functions
calculate_angle_and_distance
and calculate_reflection_points
in the code, which
serve the purpose explained
above.
from math import sqrt, atan2
def calculate_angle_and_distance(point):
'''
Returns the angle and distance of the point from the origin.
'''
x, y = point
return atan2(y, x), sqrt(x**2 + y**2)
def calculate_reflection_points(the_point, dimensions, loop_count):
'''
Returns the reflection points of the given point depending on the dimensions and the loop count.
Dimensions are the width and height of the room, assumed to have mirrored walls.
Loop count is the number of times the room is mirrored.
'''
reflection_points = {tuple(the_point)}
max_x, max_y = dimensions
new_reflection_points = set()
for _ in range(loop_count):
for point in reflection_points:
x, y = point
reflections = {(x, 2 * max_y - y), (2 * max_x - x, y), (2 * max_x - x, 2 * max_y - y)}
new_reflection_points.update(reflections - reflection_points)
max_x *= 2
max_y *= 2
reflection_points.update(new_reflection_points)
new_reflection_points.clear()
final_reflection_points = set()
for point in reflection_points:
x, y = point
final_reflection_points.update({(x, y), (-x, y), (x, -y), (-x, -y)})
return final_reflection_points
def solution(dimensions, your_position, trainer_position, distance):
loop_count = min(distance // min(dimensions), 9)
trainer_reflection_points = calculate_reflection_points(trainer_position, dimensions, loop_count)
vectors = [(point[0] - your_position[0], point[1] - your_position[1]) for point in trainer_reflection_points]
possible_shots = [(x, y) for x, y in vectors if sqrt(x**2 + y**2) < distance]
# Calculate angle and distance for each shot
shots_with_angle_and_distance = [(calculate_angle_and_distance(shot), shot) for shot in possible_shots]
# Sort shots by angle and then by distance
shots_with_angle_and_distance.sort()
# Remove shots that have the same angle and a greater distance
previous_angle = None
shots_to_keep = []
for (angle, distance), shot in shots_with_angle_and_distance:
if angle != previous_angle:
shots_to_keep.append(shot)
previous_angle = angle
possible_shots = shots_to_keep
your_reflection_points = calculate_reflection_points(your_position, dimensions, loop_count)
your_reflection_vectors = [(point[0] - your_position[0], point[1] - your_position[1]) for point in your_reflection_points]
max_x = max(point[0] for point in possible_shots)
min_x = min(point[0] for point in possible_shots)
max_y = max(point[1] for point in possible_shots)
min_y = min(point[1] for point in possible_shots)
your_reflection_vectors = [vector for vector in your_reflection_vectors if min_x < vector[0] < max_x and min_y < vector[1] < max_y]
origin = (0, 0)
if origin in your_reflection_vectors:
your_reflection_vectors.remove(origin)
reflection_angles_distances = [calculate_angle_and_distance(reflection) for reflection in your_reflection_vectors]
valid_shots = []
for point in possible_shots:
shot_angle, shot_distance = calculate_angle_and_distance(point)
if any(angle == shot_angle and distance < shot_distance for angle, distance in reflection_angles_distances):
continue
valid_shots.append(point)
possible_shots = valid_shots
return len(possible_shots)
You've escaped Commander Lambda's exploding space station along with numerous escape pods full of bunnies. But -- oh no! -- one of the escape pods has flown into a nearby nebula, causing you to lose track of it. You start monitoring the nebula, but unfortunately, just a moment too late to find where the pod went. However, you do find that the gas of the steadily expanding nebula follows a simple pattern, meaning that you should be able to determine the previous state of the gas and narrow down where you might find the pod.
From the scans of the nebula, you have found that it is very flat and distributed in distinct patches, so you can model it as a 2D grid. You find that the current existence of gas in a cell of the grid is determined exactly by its 4 nearby cells, specifically, (1) that cell, (2) the cell below it, (3) the cell to the right of it, and (4) the cell below and to the right of it. If, in the current state, exactly 1 of those 4 cells in the 2x2 block has gas, then it will also have gas in the next state. Otherwise, the cell will be empty in the next state.
For example, let's say the previous state of the grid (p) was:
.O..
..O.
...O
O...
To see how this grid will change to become the current grid (c) over the next time step, consider the
2x2 blocks of cells around each cell. Of the 2x2 block of [p[0][0], p[0][1], p[1][0], p[1][1]], only
p[0][1] has gas in it, which means this 2x2 block would become cell c[0][0] with gas in the next time
step:
.O -> O
..
Likewise, in the next 2x2 block to the right consisting of [p[0][1], p[0][2], p[1][1], p[1][2]], two of
the containing cells have gas, so in the next state of the grid, c[0][1] will NOT have gas:
O. -> .
.O
Following this pattern to its conclusion, from the previous state p, the current state of the grid c
will be:
O.O
.O.
O.O
Note that the resulting output will have 1 fewer row and column, since the bottom and rightmost cells do not have a cell below and to the right of them, respectively.
Write a function solution(g) where g is an array of array of bools saying whether there is gas in each cell (the current scan of the nebula), and return an int with the number of possible previous states that could have resulted in that grid after 1 time step. For instance, if the function were given the current state c above, it would deduce that the possible previous states were p (given above) as well as its horizontal and vertical reflections, and would return 4. The width of the grid will be between 3 and 50 inclusive, and the height of the grid will be between 3 and 9 inclusive. The solution will always be less than one billion (10^9).
Test cases
Input:
solution.solution([[True, True, False, True, False, True, False, True, True, False], [True, True, False,
False, False, False, True, True, True, False], [True, True, False, False, False, False, False, False,
False, True], [False, True, False, False, False, False, True, True, False, False]])
Output:
11567
Input:
solution.solution([[True, False, True], [False, True, False], [True, False, True]])
Output:
4
Input:
solution.solution([[True, False, True, False, False, True, True, True], [True, False, True, False,
False, False, True, False], [True, True, True, False, False, False, True, False], [True, False, True,
False, False, False, True, False], [True, False, True, False, False, True, True, True]])
Output:
254
This is a nondeterministic cellular automaton problem, meaning there is no direct way to find the previous states of a given grid. The problem with the classical brute force approach is that the number of possible previous states is huge. For a given 3x3 grid, there are $2^{16}=65536$ possible previous states and that is an optimistic scenario. Your RAM is not going to like this. So we need to do this search with pruning.
If a cell has gas, its previous state has 4 possibilities: 1 cell has gas, others are empty.
If a cell is empty, its previous state has 12 possibilities, there is no gas, 2 cells have gas, 3 cells have gas, and 4 cells have gas.
true_predecessors = [
# One true
[[False, False], [False, True]],
[[False, False], [True, False]],
[[True, False], [False, False]],
[[False, True], [False, False]],
]
false_predecessors = [
# All false
[[False, False], [False, False]],
# Two false
[[False, True], [True, False]],
[[False, True], [False, True]],
[[True, False], [True, False]],
[[True, False], [False, True]],
[[True, True], [False, False]],
[[False, False], [True, True]],
# One false
[[False, True], [True, True]],
[[True, False], [True, True]],
[[True, True], [False, True]],
[[True, True], [True, False]],
# All true
[[True, True], [True, True]],
]
Let's assume that given grid is:
$$ \begin{bmatrix} O & . & O \\ . & O & . \\ O & . & O \\ \end{bmatrix} $$
Let's convert it to a boolean for simplicity:
$$ \begin{bmatrix} True & False & True \\ False & True & False \\ True & False & True \\ \end{bmatrix} $$
The grid is 3x3, so we will have a 4x4 grid for the previous state.
$$ \begin{bmatrix} - & - & - & - \\ - & - & - & - \\ - & - & - & - \\ - & - & - & - \\ \end{bmatrix} $$
The first element of the given grid is True
, we can start generating the previous states from
there. Since
there are 4 possibilities for the first element, we will have 4 different grids.
\[ 1: \begin{bmatrix} False & False & - & - \\ False & True & - & - \\ - & - & - & - \\ - & - & - & - \\ \end{bmatrix} % , % \:\: % 2: % \begin{bmatrix} False & False & - & - \\ True & False & - & - \\ - & - & - & - \\ - & - & - & - \\ \end{bmatrix} \] \[ 3: \begin{bmatrix} True & False & - & - \\ False & False & - & - \\ - & - & - & - \\ - & - & - & - \\ \end{bmatrix} % , % \:\: % 4: % \begin{bmatrix} False & True & - & - \\ False & False & - & - \\ - & - & - & - \\ - & - & - & - \\ \end{bmatrix} \]
Let's go one cell to the bottom of the given grid. The value is False
, so it should have 12
different possibilities. This is where the pruning comes in. If we check the first row of the possible
previous states and match them with the generated grids, we can eliminate some of the possibilities.
Applying this logic until the last row, we get 8 possibilities. Without restricting the search like this, there would be $2^{8}=256$ grids that we would need to check. List of the possible previous states at the moment if you are curious:
[
[[False, False, None, None], [False, True, None, None], [True, False, None, None], [False, False, None, None]],
[[False, False, None, None], [False, True, None, None], [False, True, None, None], [False, False, None, None]],
[[False, False, None, None], [True, False, None, None], [True, False, None, None], [False, False, None, None]],
[[False, False, None, None], [True, False, None, None], [False, True, None, None], [False, False, None, None]],
[[True, False, None, None], [False, False, None, None], [False, False, None, None], [False, True, None, None]],
[[True, False, None, None], [False, False, None, None], [False, False, None, None], [True, False, None, None]],
[[False, True, None, None], [False, False, None, None], [False, False, None, None], [False, True, None, None]],
[[False, True, None, None], [False, False, None, None], [False, False, None, None], [True, False, None, None]]
]
With the same idea but with a twist on the algorithm, now we need to check the second column while generating the first two rows of the third column.
\begin{bmatrix} - & (check \: this) & (while \: generating \: this) & - \\ - & (check \: this) & (while \: generating \: this) & - \\ - & - & - & - \\ - & - & - & - \\ \end{bmatrix}
Moving on to the third row, since we have three already generated neighbors, we only need to generate the remaining one with respect to its neighbors.
\begin{bmatrix} - & - & - & - \\ - & (check \: this) & (check \: this) & - \\ - & (check \: this) & (while \: generating \: this) & - \\ - & - & - & - \\ \end{bmatrix}
Looping through the columns and rows, we can find the possible previous states of the given grid. Returning the length of the list will give us the answer.
true_predecessors = [
# One true
[[False, False], [False, True]],
[[False, False], [True, False]],
[[True, False], [False, False]],
[[False, True], [False, False]],
]
false_predecessors = [
# All false
[[False, False], [False, False]],
# Two false
[[False, True], [True, False]],
[[False, True], [False, True]],
[[True, False], [True, False]],
[[True, False], [False, True]],
[[True, True], [False, False]],
[[False, False], [True, True]],
# One false
[[False, True], [True, True]],
[[True, False], [True, True]],
[[True, True], [False, True]],
[[True, True], [True, False]],
# All true
[[True, True], [True, True]],
]
def solution(g):
current_state = g
# Creating the array for the previous state, where the dimensions are the same as the current state's dimensions + 1
previous_state = []
for i in range(len(current_state) + 1):
previous_state.append([None] * (len(current_state[0]) + 1))
# Now we will check the first column of the current state and fill the first two columns of the previous state accordingly
possible_previous_states = []
if current_state[0][0]:
for predecessor in true_predecessors:
previous_state[0][0] = predecessor[0][0]
previous_state[1][0] = predecessor[1][0]
previous_state[0][1] = predecessor[0][1]
previous_state[1][1] = predecessor[1][1]
possible_previous_states.append([list(sublist) for sublist in previous_state])
else:
for predecessor in false_predecessors:
previous_state[0][0] = predecessor[0][0]
previous_state[1][0] = predecessor[1][0]
previous_state[0][1] = predecessor[0][1]
previous_state[1][1] = predecessor[1][1]
possible_previous_states.append([list(sublist) for sublist in previous_state])
# Now we check current_state[1][0] and depending on the values in the possible_previous_states we will make a decision from predessors
for i in range(1, len(current_state)):
new_possible_previous_states = []
for previous_state in possible_previous_states:
common_row = [previous_state[i][0], previous_state[i][1]]
if current_state[i][0]:
for predecessor in true_predecessors:
if predecessor[0] == common_row:
new_state = [list(sublist) for sublist in previous_state]
new_state[i + 1][0] = predecessor[1][0]
new_state[i + 1][1] = predecessor[1][1]
new_possible_previous_states.append(new_state)
else:
for predecessor in false_predecessors:
if predecessor[0] == common_row:
new_state = [list(sublist) for sublist in previous_state]
new_state[i + 1][0] = predecessor[1][0]
new_state[i + 1][1] = predecessor[1][1]
new_possible_previous_states.append(new_state)
possible_previous_states = new_possible_previous_states
# Now we will do the same thing until we reach the last column of the current state
# We need to do this without hardcoding the indexes, since the dimensions of the current state can change
# But there is a trick here,
# While generating the third column, if we are genearting the first step (first two rows), checking the common column will be enough
# But if we are generating the other rows we need to check the other three
for i in range(len(current_state[0])):
for j in range(len(current_state)):
new_possible_previous_states = []
if j == 0:
for previous_state in possible_previous_states:
common_column = [previous_state[j][i], previous_state[j+1][i]]
if current_state[j][i]:
for predecessor in true_predecessors:
if predecessor[0][1] == common_column[0] and predecessor[1][1] == common_column[1]:
new_state = [list(sublist) for sublist in previous_state]
new_state[j][i+1] = predecessor[0][0]
new_state[j+1][i+1] = predecessor[1][0]
new_possible_previous_states.append(new_state)
else:
for predecessor in false_predecessors:
if predecessor[0][1] == common_column[0] and predecessor[1][1] == common_column[1]:
new_state = [list(sublist) for sublist in previous_state]
new_state[j][i+1] = predecessor[0][0]
new_state[j+1][i+1] = predecessor[1][0]
new_possible_previous_states.append(new_state)
possible_previous_states = new_possible_previous_states
else:
for previous_state in possible_previous_states:
common_elements = [previous_state[j][i], previous_state[j+1][i], previous_state[j][i+1]]
if current_state[j][i]:
for predecessor in true_predecessors:
if predecessor[0][1] == common_elements[0] and predecessor[1][1] == common_elements[1] and predecessor[0][0] == common_elements[2]:
new_state = [list(sublist) for sublist in previous_state]
new_state[j][i+1] = predecessor[0][0]
new_state[j+1][i+1] = predecessor[1][0]
new_possible_previous_states.append(new_state)
else:
for predecessor in false_predecessors:
if predecessor[0][1] == common_elements[0] and predecessor[1][1] == common_elements[1] and predecessor[0][0] == common_elements[2]:
new_state = [list(sublist) for sublist in previous_state]
new_state[j][i+1] = predecessor[0][0]
new_state[j+1][i+1] = predecessor[1][0]
new_possible_previous_states.append(new_state)
possible_previous_states = new_possible_previous_states
# Removing the states appearing more than once
possible_previous_states = list(set([tuple([tuple(sublist) for sublist in result]) for result in possible_previous_states]))
return len(possible_previous_states)
This challenge was a great experience for me. I learned a lot of things, and I had a lot of fun. I hope you did too.
If you like these kinds of challenges, I recommend you check out Project Euler and Codeforces. They are great platforms to improve your problem-solving skills.
If you have any questions or suggestions, please feel free to contact me (you can find my email on the home page). I will be happy to help you.