There's Only So Much "Time & Space" For Your Algorithms.
On the need to write efficient algorithms.
Konnichiwa (hello) fellow programmers! ๐๐ฝ
This article is a wee bit different from the others in my Algos in Plain English series, where I explain the thought process behind solving an algorithm. Here I'll mainly explain the differences between my initial solution to the algorithm challenge and thereafter provide one of many efficient solutions inspired by the discussion forums on HackerRank.
If this is your first time reading my data structures and algorithms article, please consider reading more in my Algos in Plain English series.๐
Before I get into today's problem, I'd like to remind myself and others like me, that the whole point of algorithms is to solve a problem (with more emphasis on an efficient solution and not necessarily a readable one).
While solving this particular challenge, I found myself telling a story in my code, a story of how I arrived at the solution. Having failed 10/12 test cases due to runtime errors (because my code took too long to run for the extremes of the input constraints), I've been painfully reminded about the true point of algorithms๐ .
With that being said, I'll post my initial story-telling algorithm and the final solution after scouring through the discussion forums. So let's get to it!
Problem
A jail has a number of prisoners and a number of treats to pass out to them. Their jailer decides the fairest way to divide the treats is to seat the prisoners around a circular table in sequentially numbered chairs. A chair number will be drawn from a hat. Beginning with the prisoner in that chair, one candy will be handed to each prisoner sequentially around the table until all have been distributed.
The jailer is playing a little joke, though. The last piece of candy looks like all the others, but it tastes awful. Determine the chair number occupied by the prisoner who will receive that candy.
Example
n = 4
m = 6
s = 2
There are 4
prisoners, 6
pieces of candy and distribution starts at chair 2
. The prisoners arrange themselves in seats numbered 1
to 4
. Prisoners receive candy at positions 2, 3, 4, 1, 2, 3. The prisoner to be warned sits in chair number 3
.
Function Description
Complete the saveThePrisoner
function in the editor below. It should return an integer representing the chair number of the prisoner to warn.
saveThePrisoner has the following parameter(s):
- int
n
: the number of prisoners - int
m
: the number of sweets - int
s
: the chair number to begin passing out sweets from
Returns int: the chair number of the prisoner to warn
Feel free to read the full problem on HackerRank.
Why My "Story-telling" Solution Failed 10/12 Test Cases.
In an attempt to write code that humans can read, I thought of the most readable implementation of the solution without any regard for efficiency and complexity. As a result, my solution suffered from a scalability problem measured by a metric called space complexity.
Space complexity (like Time Complexity) is a measure of the memory required for an algorithm to run.
Consider the following code:
def multiply(a, b):
return a * b
To compute this simple algorithm, python needs to allocate space for the two input parameters, a
and b
, as well as space for the return value. The actual memory size allocated internally by python depends on the implementation details and where the code is running, but whatever the case, it'll be a fixed amount of memory even for very large input values.
Now consider this example:
all_numbers = []
for number in range(0, n):
numbers.append(number)
This algorithm creates an empty list called all_numbers
to store n
numbers added at every iteration.
The larger the value of n
, the longer the list will be and thus more space will be required to store the list in memory. Since the space increases proportionally with the input value, the space complexity of the algorithm is linear and the Big O notation is O(n).
With that out of the way, let's look at my solution now.
These are the steps taken to arrive at a solution.
# Loop through all prisoners [n], starting from the prisoners at chair [s].
# Add a prisoner who receives a candy to a [prisoners_with_candy] array.
# Repeat the for loop while there are still candies available.
# Start subsequent iterations at the first chair.
# Repeat the iteration while there are candies left.
And here's the code:
def saveThePrisoner(n, m, s):
# initially, remaining candies is equal to total number of candies
remaining_candies = m
# array to track what prisoners receive candies.
prisoners_with_candy = []
# loop through all prisoners [n], starting from the prisoners at
# chair [s].
# add a prisoner who receives a candy to [prisoners_with_candy]
for prisoner in range(s, n + 1):
prisoners_with_candy.append(prisoner)
remaining_candies -= 1
# stop sharing candies when the jailer runs out
if remaining_candies == 0:
break
# Start subsequent iterations at the first chair.
else:
# repeat the for loop while there are candies left
while remaining_candies > 0:
for prisoner in range(1, n + 1):
prisoners_with_candy.append(prisoner)
remaining_candies -= 1
# stop sharing candies when the jailer runs out
if remaining_candies == 0:
break
prisoner_with_last_candy = prisoners_with_candy[-1]
return prisoner_with_last_candy
By now you should have noticed what I meant by a "story-telling" algorithm.
What's wrong with this algorithm?
- The original problem is only concerned with getting the id of the last prisoner.
- This means iterating through all prisoners, giving each one a candy, adding prisoners with candies to an array, reducing the number of candies, and finally returning the last prisoner with candies are all steps that do not play a role in getting a solution.
- The time and space required to run this algorithm depends on the
n
, the total number of prisoners.
Extra note: Removing the extra step of "adding prisoners with candies to the array" also created a new problem. This modified solution was not able to compile within the allotted time for a test case because the program still spent a lot of time iterating through every prisoner, resulting in a compiler timeout.
One of many efficient solutions.
def saveThePrisoner(n, m, s):
return ( ((s - 1) + (m - 1)) % n ) + 1
The above code block solves the same problem in only 1 line of code.
Thoughts behind this solution.๐ค๐ญ
A. (s - 1)
translates a prisoner ID to an equivalent index.
- E.g. 4 prisoners (
n = 4
) are represented as0, 1, 2, 3
instead of1, 2, 3, 4
as defined in the problem. This is done because%
effectively deals with indexes like0..n-1
.
B. (m - 1)
handles the fact that the first prisoner to get candy is not counted when giving them away.
For E.g. Assuming any number of prisoners, say 20 prisoners (
n = 20
).if the jailer is giving away 1 candy (
m = 1
) and you start at the prisoner in chair 12 (s = 12
), it is the prisoner in chair 12 (12 + 1 - 1) who should be warned because he's the only one who gets candy.
- If the jailer is giving away 2 candies (
m = 2
) it is the prisoner in chair 13 (12 + 2 - 1) who should be warned.
C. % n
handles the wrapping around the circular table based on the index of the prisoners. This means we are not interested in repetitions around the table but the remainder at the last rotation.
D. + 1
brings us back to dealing with prisoners IDs instead of indices.
So we get:
( (12 - 1) + (1 - 1) % 20) + 1) # ( ((s - 1) + (m - 1)) % n ) + 1
# 12: prisoner with ID 12.
( (12 - 1) + (2 - 1) % 20) + 1) # ( ((s - 1) + (m - 1)) % n ) + 1
# 13: prisoner with ID 13.
You'll notice that this algorithm does not depend on the number of prisoners, n
, like the "story-telling" algorithm from earlier. It therefore has a constant time and space requirement for compiling. The Big O notation for this algorithm is O(1).
That's it! Thanks for reading! ๐
Hope this article helped with understanding this challenge. Happy Coding!๐จ๐ฝโ๐ป
Nonsocchi๐ฅท๐ฝ