Three egg proble​m - algorithm

I was just reading The Two Egg Problem:
The Two Egg Problem
You are given two eggs, and access to a 100-storey building. Both eggs are identical. The aim is to find out the highest floor from which an egg will not break when dropped out of a window from that floor. If an egg is dropped and does not break, it is undamaged and can be dropped again. However, once an egg is broken, that’s it for that egg.
If an egg breaks when dropped from floor n, then it would also have broken from any floor above that. If an egg survives a fall, then it will survive any fall shorter than that.
The question is: What strategy should you adopt to minimize the number egg drops it takes to find the solution?. (And what is the worst case for the number of drops it will take?)
I was following along until the "Look see I can do three" section. The author states that after the first egg breaks it degrades into the 2-egg problem and can be solved recursively.
That's great, but wouldn't we want to choose larger step-sizes when using 3 eggs instead of 2 (for the first egg)? From which floor do we throw the first egg?
With 1 egg, we have to start at floor 1.
With 2 eggs, we solve for n(n+1)/2=k and round up, where n is the starting floor, and k is the number of floors.
With 3... I'm having trouble coming up with a formula.
Thinking about this a bit more, with 2 eggs, the maximum number of drops is equal to the floor number that we drop our first egg from. For example, with 2 eggs and 100 floors, the solution is 14, which means we drop the first egg from floor 14, and if it breaks, we have to drop up to 13 more times, for floors 1-13.
With 3 eggs, the solution is 9 (as shown in the chart). But we wouldn't want to throw the first egg at floor 9, we can throw it higher, because we don't have to iterate by 1s in-between.
If we throw from floor 14 again, and it breaks, then we recurse. n(n+1)/2=k where k is now 13... but that gives us 4.815, if we ceil and that and add our previous drop we get 6, which is lower than the actual solution, so something here is wrong...

If we throw from floor 14 again, and it breaks, then we recurse. n(n+1)/2=k where k is now 13... but that gives us 4.815, if we ceil and that and add our previous drop we get 6, which is lower than the actual solution, so something here is wrong...
What if it doesn't break? Then you have a three-egg problem with 86 floors, which takes maybe one drop less to solve than the 100-floor problem.
Say you drop the first egg from the 50th floor. If it breaks, you have a two-egg problem with 49 floors, which takes up to 10 additional drops. So that would give you a worst-case of 11 drops (since if it doesn't break, the 50-floor three-egg problem takes at most 7 additional drops).
If you choose the 37th floor for the first drop, if it breaks, you have a 36-floor two-egg problem, needing up to 8 additional drops. If it doesn't break, you have a 63-floor three-egg problem left. You want to solve that problem with at most 8 drops, so if the next drop breaks the egg, the remaining two-egg problem should be solvable in at most 7 drops, thus the highest floor you can choose for the second drop is 37 + 28 + 1 = 66, since 28 floors is the highest you can solve with at most 7 drops and two eggs. If the egg doesn't break, you have a 34-floor three-egg problem with 7 drops left. The highest you can certainly solve with the remaining 6 drops if the egg breaks is 21 (6*7/2), so you can choose floor 66 + 21 + 1 = 88. If the egg doesn't break, you have 12 floors left with 6 drops, which is already doable with only two eggs.
Systematically, the highest number of floors you can certainly solve with d drops and e eggs is
/ 1, if d == 1
F(e,d) = | d, if e == 1
\ F(e-1,d-1) + 1 + F(e,d-1), if e > 1 and d > 1
If you have only one drop, you have no choice but to choose the lowest floor of which you do not yet know that the egg doesn't break. If that breaks it, and you tried a higher floor, you don't know the first floor to break the egg.
If you have only one egg, you have to check every floor in order until the egg breaks or you run out of drops.
Otherwise, if the first drop is from a floor higher than F(e-1,d-1) + 1, you possibly can't find the first breaking floor if the egg breaks. If the first drop is from a lower floor, you can't reach as high with d-1 drops if the egg doesn't break, so the first drop should be from floor F(e-1,d-1) + 1. If it breaks, you can solve with the remaining e-1 eggs and d-1 drops by assumption. If not, you can solve for the next F(e,d-1) floors with the remaining drops and eggs.
Conversely, to find how many drops you may need for f floors with e eggs, you have to find
D(e,f) = min { d | F(e,d) >= f }
You can find that by calculating the F(e,d) matrix, or you can use dynamic programming:
If you choose floor s for the first drop, if the egg breaks, you need up to D(e-1,s-1) drops to determine the floor. If the egg doesn't break, you need up to D(e,f-s) drops to determine the floor.
So the worst case for choosing floor s for the first drop is
WC(s,e,f) = 1 + max { D(e-1,s-1), D(e,f-s) }
and the best of the worst case is
D(e,f) = minimum { WC(s,e,f) | 1 <= s <= f }
(where of course D(e,0) = 0).

This problem can be solved with following 3 approaches (that I know) :
Dynamic Programming
Solution using Binary Search Tree
Solution by obtaining the direct mathematical formula for maximum number of floors that can be tested or covered with given number of eggs and given number of drops
Let me first define some symbols as follows:
e = number of eggs
f = number of floors in building
n = number of egg drops
Fmax(e, n) = maximum number of floors that can be tested with e eggs and n drops
The crux for dynamic programming approach lies in following recursive formula for Fmax:
Fmax(e, n) = 1 + Fmax(e-1, n-1) + fmax(e, n-1)
And the crux for obtaining the direct mathematical formula for Fmax lies in following recursive formula for Fmax:
Fmax(e, n) = { ∑Fmax(e-1,i) for i = 1 to n } - Fmax(e-1, n) + n
Alternative solution using Binary Search Tree (BST) is also possible for this problem. In order to facilitate our analysis, let us draw BST with slight modifications as follows:
1. If egg breaks then child node is drawn on left down side
2. If egg does not break then child node is drawn straight down side
If we draw BST with above kind of representation then width of the BST (i.e. number of vertical columns in BST) represents the number of eggs.
Any BST with f number of nodes, drawn with above kind of representation and subjected to the constraint width of BST <= e (number of eggs) is a solution but it may not be the optimal solution.
Hence obtaining the optimal solution is equivalent to obtaining the arrangement of nodes in BST with minimum height subjected to the constraint: width of BST <= e
For more details about all the above 3 approaches, check my blog at: 3 approaches for solving generalized egg drop problem

This is the same answer that I provided at Egg Drop Printing Solutions. I am providing it here for anyone who wants to see the whole decision tree and reasoning laid out.
# This uses dynamic programming to find the basic information.
def optimal_solution(floors, eggs):
# dp[drops][eggs] = max_floors
dp = []
# With no drops, we can do no floors
dp.append([0 for x in range(eggs+1)])
# With one drop and any eggs, we can do 1 floor
one_drop = [1 for _ in range(eggs+1)]
one_drop[0] = 0 # Except no eggs is still no floors
dp.append(one_drop)
# Keep adding drops until we have our answer
# Note, in python array element -1 is shorthand for the end of the array.
while dp[-1][eggs] < floors:
# 0 floors for 0 eggs. 1 more for one egg
next_drop = [0, dp[-1][1] + 1]
for i in range(2, eggs+1): # Python for 2..eggs
# The best we can do is drop at floor dp[-1][i-1].
# If the egg breaks, we can find the answer using that solution.
# If the egg holds, we can find another dp[-1][i] floors.
next_drop.append(dp[-1][i] + dp[-1][i-1])
dp.append(next_drop)
return dp
# This turns that optimal solution into a decision tree.
def dp_to_decision_tree(dp, floors, start_floor=None, eggs=None, drops=None):
# Figure out defaults if needed.
if start_floor is None:
start_floor = 0
if drops is None:
drops = len(dp) - 1
if eggs is None:
eggs = len(dp[0]) - 1
# Are we done?
if floors == start_floor:
return start_floor
elif dp[drops][eggs] < floors - start_floor:
return None
# Do we need all of our drops?
while floors - start_floor < dp[drops-1][eggs]:
drops -= 1
drop_at = start_floor + dp[drops-1][eggs-1]
if eggs == 1:
drop_at = start_floor + 1
if floors < drop_at:
drop_at = floors
return [
drop_at,
dp_to_decision_tree(dp, floors, drop_at, eggs, drops-1),
dp_to_decision_tree(dp, drop_at-1, start_floor, eggs-1, drops-1),
{'eggs': eggs, 'floor_range': (start_floor, floors)}
]
# This prints the decision tree in a human readable format.
def print_decision_tree(tree, indent="", label="start"):
if tree is None:
print(f"{indent}{label}: ?")
elif isinstance(tree, int):
print(f"{indent}{label}: {tree} found")
else:
print(f"{indent}{label}: {tree[0]} {tree[3]}")
print_decision_tree(tree[1], indent + " ", label="held")
print_decision_tree(tree[2], indent + " ", label="broke")
# And this calls the previous functions.
def print_optimal_decisions(floors, eggs):
print_decision_tree(
dp_to_decision_tree(
optimal_solution(floors, eggs), floors))
# And now we can try it.
print_optimal_decisions(36, 3)

You can use simple dynamic programming solution.
n - number of floors.
k - number of eggs.
D[n,k] = you answer (The number of minimum throws).
D[j,1] = n-1 for each 1 <= j <= n.
The main idea for calculate D[n,k] for k>1:
D[n,k] = maximum 1 <= j <= n-1 { maximum{ D[j,k-1]+1, D[n-j,k]+1 }.

The blog here explains the mathematical approach for 3 eggs with 1000 floors:
https://sankalpiitr.wordpress.com/2012/03/02/the-2-eggs-problem-extended-to-3-eggs/
According to this blog formula would be
P = N + ( N * ( N + 1 ) * ( N – 1 ) ) /6
where P is the number of floors and N is the minimum drops required.
So solving for P=100, we get N as 8.2

Related

FireHose (S3) from CCC

This grade 11 problem has been bothering me since 2010 and I still can't figure out/find a solution even after university.
Problem Description
There is a very unusual street in your neighbourhood. This street
forms a perfect circle, and the circumference of the circle is
1,000,000. There are H (1 ≤ H ≤ 1000) houses on the street. The
address of each house is the clockwise arc-length from the
northern-most point of the circle. The address of the house at the
northern-most point of the circle is 0. You also have special firehoses
which follow the curve of the street. However, you wish to keep the
length of the longest hose you require to a minimum. Your task is to
place k (1 ≤ k ≤ 1000) fire hydrants on this street so that the maximum
length of hose required to connect a house to a fire hydrant is as
small as possible.
Input Specification
The first line of input will be an integer H, the number of houses. The
next H lines each contain one integer, which is the address of that
particular house, and each house address is at least 0 and less than
1,000,000. On the H + 2nd line is the number k, which is the number of
fire hydrants that can be placed around the circle. Note that a fire
hydrant can be placed at the same position as a house. You may assume
that no two houses are at the same address. Note: at least 40% of the
marks for this question have H ≤ 10.
Output Specification
On one line, output the length of hose required
so that every house can connect to its nearest fire hydrant with that
length of hose.
Sample Input
4
0
67000
68000
77000
2
Output for Sample Input
5000
Link to original question
I can't even come up with a brutal force algorithm since the placement might be float number. For example if the houses are located in 1 and 2, then the hydro should be placed at 1.5 and the distance would be 0.5
Here is quick outline of an answer.
First write a function that can figures out whether you can cover all of the houses with a given maximum length per hydrant. (The maximum hose will be half that length.) It just starts at a house, covers all of the houses it can, jumps to the next, and ditto, and sees whether you stretch. If you fail it tries starting at the next house instead until it has gone around the circle. This will be a O(n^2) function.
Second create a sorted list of the pairwise distances between houses. (You have to consider it going both ways around for a single hydrant, you can only worry about the shorter way if you have 2+ hydrants.) The length covered by a hydrant will be one of those. This takes O(n^2 log(n)).
Now do a binary search to find the shortest length that can cover all of the houses. This will require O(log(n)) calls to the O(n^2) function that you wrote in the first step.
The end result is a O(n^2 log(n)) algorithm.
And here is working code for all but the parsing logic.
#! /usr/bin/env python
def _find_hoses_needed (circle_length, hose_span, houses):
# We assume that houses is sorted.
answers = [] # We can always get away with one hydrant per house.
for start in range(len(houses)):
needed = 1
last_begin = start
current_house = start + 1 if start + 1 < len(houses) else 0
while current_house != start:
pos_begin = houses[last_begin]
pos_end = houses[current_house]
length = pos_end - pos_begin if pos_begin <= pos_end else circle_length + pos_begin - pos_end
if hose_span < length:
# We need a new hose.
needed = needed + 1
last_begin = current_house
current_house = current_house + 1
if len(houses) <= current_house:
# We looped around the circle.
current_house = 0
answers.append(needed)
return min(answers)
def find_min_hose_coverage (circle_length, hydrant_count, houses):
houses = sorted(houses)
# First we find all of the possible answers.
is_length = set()
for i in range(len(houses)):
for j in range(i, len(houses)):
is_length.add(houses[j] - houses[i])
is_length.add(houses[i] - houses[j] + circle_length)
possible_answers = sorted(is_length)
# Now we do a binary search.
lower = 0
upper = len(possible_answers) - 1
while lower < upper:
mid = (lower + upper) / 2 # Note, we lose the fraction here.
if hydrant_count < _find_hoses_needed(circle_length, possible_answers[mid], houses):
# We need a strictly longer coverage to make it.
lower = mid + 1
else:
# Longer is not needed
upper = mid
return possible_answers[lower]
print(find_min_hose_coverage(1000000, 2, [0, 67000, 68000, 77000])/2.0)

Calculate the winning strategy of a subtraction game

Problem:
Given 100 stones, two players alternate to take stones out. One can take any number from 1 to 15; however, one cannot take any number that was already taken. If in the end of the game, there is k stones left, but 1 through k have all been previously taken, one can take k stones. The one who takes the last stone wins. How can the first player always win?
My Idea
Use recursion (or dynamic programming). Base case 1, where player 1 has a winning strategy.
Reducing: for n stones left, if palyer 1 takes m1 stones, he has to ensure that for all options player 2 has (m2), he has a winning strategy. Thus the problem is reduced to (n - m1 - m2).
Follow Up Question:
If one uses DP, the potential number of tables to be filled is large (2^15), since the available options left depend on the history, which has 2^15 possibilities.
How can you optimize?
Assuming that the set of numbers remaining can be represented as R, the highest number remaining after your selection can be represented by RH and the lowest number remaining can be RL, the trick is to use your second-to-last move to raise the number to <100-RH, but >100-RH-RL. That forces your opponent to take a number that will put you in winning range.
The final range of winning, with the total number that you create with your second-to-last move, is:
N < 100-RH
N > 100-RH-RL
By observation I noted that RH can be as high as 15 and as low as 8. RL can be as low as 1 and as high as 13. From this range I evaluated the equations.
N < 100-[8:15] => N < [92:85]
N > 100-[8:15]-[1:13] => N > [92:85] - [1:13] => N > [91:72]
Other considerations can narrow this gap. RL, for instance, is only 13 in an edge circumstance that always results in a loss for Player A, so the true range is between 72 and 91. There is a similar issue with RH and the low end of it, so the final ranges and calculations are:
N < 100-[9:15] => N < [91:85]
N > 100-[9:15]-[1:12] => N > [91:85] - [1:12] => N > [90:73]
[90:73] < N < [91:85]
Before this, however, the possibilities explode. Remember, this is AFTER you choose your second-to-last number, not before. At this point they are forced to choose a number that will allow you to win.
Note that 90 is not a valid choice to win with, even though it might exist. Thus, the maximum it can be is 89. The real range of N is:
[88:73] < N < [90:85]
It is, however, possible to calculate the range of the number that you're using to put your opponent in a no-win situation. In the situation you find yourself in, the lowest number or the highest number might be the one you chose, so if RHc is the highest number you can pick and RLc is the lowest number you can pick, then
RHc = [9:15]
RLc = [1:12]
With this information, I can begin constructing a relative algorithm starting from the end of the game.
N*p* - RHp - RLp < Np < N*p* - RHp, where p = iteration and *p* = iteration + 1
RHp = [8+p:15]
RLp = [1:13-p]
p = -1 is your winning move
p = 0 is your opponent's helpless move
p = 1 is your set-up move
Np is the sum of that round.
Thus, solving the algorithm for your set-up move, p=1, you get:
N*p* - [9:15] - [1:12] < Np < N*p* - [9:15]
100 <= N*p* <= 114
I'm still working out the math for this, so expect adjustments. If you see an error, please let me know and I'll adjust appropriately.
Here is a simple, brute force Python code:
# stoneCount: number of stones to start the game with
# possibleMoves: which numbers of stones may be removed? (*sorted* list of integers)
# return value: signals if winning can be forced by first player;
# if True, the winning move is attached
def isWinningPosition(stoneCount, possibleMoves):
if stoneCount == 0:
return False
if len(possibleMoves) == 0:
raise ValueError("no moves left")
if stoneCount in possibleMoves or stoneCount < possibleMoves[0]:
return True,stoneCount
for move in possibleMoves:
if move > stoneCount:
break
remainingMoves = [m for m in possibleMoves if m != move]
winning = isWinningPosition(stoneCount - move, remainingMoves)
if winning == False:
return True,move
return False
For the given problem size this function returns in less than 20 seconds on an Intel i7:
>>> isWinningPosition(100, range(1,16))
False
(So the first play cannot force a win in this situation. Whatever move he makes, it will result in a winning position for the second player.)
Of course, there is a lot of room for run time optimization. In the above implementation many situations are reached and recomputed again and again (e.g. when the first play takes one stone and the second player takes two stones this will put the first player into the same situation as when the number of stones taken by each player are reversed). So the first (major) improvement is to memorize already computed situations. Then one could go for more efficient data structures (e.g. encoding the list of possible moves as bit pattern).

Understanding Egg Drop Algorithm for more than two eggs.

http://datagenetics.com/blog/july22012/index.html
I am using the mentioned link to understand the eggdrop problem. I have also looked at code online which I understand to decent extent (the recursion is a bit confusing) but I can't seem to understand a few main things about this egg drop problem.
Suppose we have 100 floors
For 2 eggs we say we start at 14 and then go to 14 + (14-1). I understand why we do this to keep the worse case time uniform and all. However, where will we start for three eggs? The formula shows that 3 eggs will have a max of 9 tries in the worst case. Obviously we don't start from 9 because going 9 + ( 9 - 1 ) doesn't give us a consisten 9 trials across 100 so where do we start for 3? Not only that but how do we figure this out?
It seems like for 3 eggs, we run a few trials until the problem degenerates into 2 eggs and x amount of floors. Conceptually this makes sense but I don't understand how to visualize or implement this
I have to find the sequence of tries in a worst case scenario and implement it which is why I want to visualize the tries.
I hope this is clear. It is the first bullet of mine that is my main issue. Please let me know if I'm missing out any info and I'll edit it.
The equation n(n+1)/2 = F can only be used to solve the min worst case number of drops in the case of 2 eggs. Here also happens to be one of the floors you can drop the 1st egg from because of the uniformity you mentioned. You know you can drop from the 14th floor because if it cracks you'll have, at worst, 13 more drops. But if it doesn't crack and you go up 13 floors and it cracks, you will have at worst 12 more drops to go with 2 under your belt already. With this pattern you can see that by dropping the egg from nth floor, then n+(n-1) floor, then n+(n-1)+(n-2) floor your worst case is staying the same at each threshold.
This is what you want to achieve regardless of how many eggs you start with, but finding an optimal floor (n) which makes this condition true (which actually can be expressed as a range as #Amit pointed out) can't be calculated with a closed series like it can for 2 eggs. It is important to note that even in the case of (n+1)n = F, n is just one of many answers for the possible first floor value. We conveniently say it is the answer, perhaps carelessly, because we can prove it to be true using a relatively simple series.
So let's use a more general approach to estimate the minimum worst case number of drops and then see at which floors we know that this can be achieved. Let's say we have function, g(floors, eggs), which returns the minimum worst case egg drops needed for a particular amount of floors. We can say with confidence that if eggs = 1, the worst case scenario is that we will have to drop the egg from every floor to find the threshold, so g(floors, 1) = floors is true for any value of floors. We also know that if we have 1 floor to test it will always only require one drop so g(1, eggs) = 1. Beyond these two cases we know the following:
g(floors, eggs) = Min(n = 1->floors : 1 + max(g(n-1,e-1),g(floors-n, e)))
Why does it work? Well given an amount of total floors, one must go through every single one and see what the worst case is for cracking the egg at each. This is done by comparing worst case if it cracks, or g([current floor]-1, eggs-1), with worst case if it doesn't crack, or g(floors-[current floor], eggs).
The maximum of these two values will be the worst case scenario for that particular floor. If we track a global minimum of each of these maxima, we will find the lowest drops required for the worst case. The floor(s) at which this happens is the optimal floor to drop the egg at. Now let's plug in eggs = 2 in that function to get a better feeling as to why this works, and why we can also represent the min worst case number of drops when starting with 2 eggs as a series as well.
When we have precisely 2 eggs we will always be comparing g([current floor]-1, 1) with g(floors-[current floor], 2). This makes things a bit easier because we know exactly what the worst case is if we crack the egg at current floor:
*worst case drops required* = g(cf-1, 1) + 1 = 1 + (cf-1)
note-here we add the 1 because we have already done 1 drop by the time we can test the remaining floors below.
We also know that the two functions we are comparing at each floor (cf) are monotonically going in two different directions for any fixed number of total floors F. This must be true because:
g(cf+d, 1) > g(cf, 1) for any positive cf and d so this function is increasing as you increase cf.
g(F-(cf+d),2) < g(F-cf,2), thus this function is always decreasing as you increase cf.
The above is important because it makes us realize that the minimum max of these two functions will happen at a floor (let's call it optimal floor, of) where they return values that are closest to one another, one could even dare to say equal to each other! Using this we can approximate that the minimum occurs when:
g(of1-1, 1) ~= of1-1 ~= g(F- of1, 2) ~= 1+(of2-1) ~= 1+ g(F- of1 - of2, 2) ~= 2+(of3-1) ~=.....~= of2 --> the value furthest on the right represents the worst case if the egg doesn't crack at any of the optimal thresholds, then we will have used up of2 number of drops to get to that point, not counting the very first drop.
where of1 is the theoretical floor where the first drop can occur to minimize the worst case and of1+of2 is where the second drop must occur (given failure to crack at of1), all the way up until of1 + of2...+ofn = F. Let's now examine the relationship between of1 and of2:
(of1-1) = 1+(of2-1), so of2 = of1-1
similarly
of3 = of2 -1 = of1 - 2
finally we can say that in general
ofn = of1-(n-1)
We know that if we have gone through all threshold floors except for the last, and none cracked the egg, then we are on our of1th drop.
ofof1 = of1-(of1-1) =1 => this element is the last in our series
The series, of1 + of2...+ofn can be written as of1 + (of1-1) + (of1-2) +....+1 = F which we know can be expressed as (of1)(of1+1)/2 = F. This makes finding both the minimum worst case number of drops and the optimal floor to drop the first egg a simple exercise of plugging in F into this formula.
Okay now let's use the same function when eggs equals three! Well unfortunately it turns out that you hit a wall in the very first step. Remember when we were still comparing g([current floor]-1, eggs-1) with g(floors-[current floor], eggs)? Well let's say eggs = 3 now so you are comparing g(floors-[current floor], 3) and g([current floor]-1, 2).
We cannot reduce any of these functions into a series with a closed form solution as the function
g(F - cf, 3)
requires at least one level of recursion to solve so whatever we reduce it to will always have a term with g function in it. On the other hand if we try to utilize the fact that for any
g(f-1, 2)
there exists an n where (n+1)n/2 = f-1, where n is the minimum worst case number of drops.
If we rearrange (n+1)n/2 = f-1 to n = 1/2(sqrt(8f-7)-1) = g(f-1, 2)
we could potentially try to set g(of1-1, 2) equal to 1+g(of2-1, 2) to find of2 as a function of of1, similarly to how we found of2 expressed in terms of of1 when were starting with 2 eggs. If you recall we put all "optimum" floors for drops, expressed in terms of the optimum floor for the first drop, in a series that happened to have a closed form solution. With 3 eggs we run out of luck as this results in an "ugly" series with no way of solving without recursion. This is unlike in the case where we were starting with 2 eggs because we were able to reduce g(cf-1, 1) + 1 into just 1 + (cf-1). This helped us build the series which did have a closed form solution.
Therefore there is no nifty derivation to use to find the optimal first drop like there is in the case of 2 eggs. Below I wrote a short algo that outputs both min worst case number of drops and optimal floor for first drop (often times it can be more than one but I always return the last). Once you put in a value other than 2 for e you can notice that these will not necessarily equal one another.
var optimumFloorForFirstDrop = 0;
var F = 24;
console.log("Minimum worst case number of drops: " +findBestWorstCase(F,3) + ", optimum floor for first drop: "+ optimumFloorForFirstDrop);
function findBestWorstCase(n, e){
//if we have 0 or 1 floors or one egg then return the number of floor...this is pretty intuitive
if(n < 2 || e == 1) return n;
//we want to go through every floor and keep track of the minimum for a particular n of the max function below
var min = n;
for(var i = 1; i <= n; i++){
var tmpMax = 1 + Math.max(findBestWorstCase(i-1, e-1),findBestWorstCase(n-i, e));
if(tmpMax <= min){
min = tmpMax;
if(n==F) optimumFloorForFirstDrop = i;
}
}
return min;
}
Let's examine a few examples first:
If you have 1 egg, how many throws do you need for a 2 floors building? 3 floors? 4?
Obviously that's simple. You need 2, 3 and 4 throws, respectively, and generally, n throws.
What if you have 2 eggs? How many throws for 2 floors? 3? 4?
Obviously 2 throws for 2 floors... 3 is interesting though. If you throw an egg from the 2nd floor, and it doesn't break, you throw it from the 3rd floor and you know the answer (it either breaks on the 3rd floor, or doesn't at all). If the first egg breaks on the 2nd floor throw, you throw the other egg from the first floor and again you know the answer (it either breaks on the 1st floor and that's the answer, or it doesn't and the answer is the 2nd floor). Ha... so only 2 throws for 3 floors. But that doesn't work for the 4th floor and we need another throw. That extra throw can "buy us" more than just one floor. It will actually get us to the 6th floor (we'll soon see how that works).
It also turns out that having more eggs won't make any difference for a 6 floor building or less.
Suppose we know how many floors we can cover with m-1 eggs and n-1 throws, let's call that h. If we have m eggs and n throws, our optimal strategy would be to through the first egg from the h+1th floor - that's the highest floor we can go for. If the egg breaks, we have enough eggs (m-1) and enough throws (n-1) to find the answer in the remaining h floors. Our next move if the egg doesn't break is to go up enough floors so that we're covered by (m, n-1) and keep doing that till we have no throws left. This way we will achieve the maximum coverage with any combination of eggs and throws.
That explains our optimal strategy, but we haven’t defined how many floors will (m, n) cover. That’s quite simple though: (m-1, n-1) will cover some h floors, the (m, n) throw itself will account for the h+1 floor, and the remaining throws will allow for additional (m, n-1) floors.
The modeling of the problem should be quite clear by now, and we can define a simple recursive function to calculate the maximal covered height:
function maxHeightByEggThrows(eggs, throws) {
if(eggs === 0 || throws === 0)
return 0;
return maxHeightByEggThrows(eggs - 1, throws - 1) + 1 +
maxHeightByEggThrows(eggs, throws - 1);
}
And this works flawlessly, but it’s a poor, ineffective implementation. Let’s try DP:
function maxHeightByEggThrowsDP(eggs, throws) {
let eggThrows = [[]];
for(let i = 0; i < throws; i++) {
// A single egg can cover has many floors has throws are allowed
eggThrows[0].push(i + 1);
}
for(let i = 1; i < eggs; i++) {
// Any number of eggs can only cover 1 floor with a single throw
eggThrows.push([1]);
}
for(let i = 1; i < throws; i++) {
for(let j = 1; j < eggs; j++) {
eggThrows[j][i] = eggThrows[j - 1][i - 1] + eggThrows[j][i - 1] + 1;
}
}
return eggThrows[eggs - 1][throws - 1];
}
This doesn't look as nice, but it's much better for performance reasons, and, we can use such an implementation to return the whole table and display it:
function maxHeightByEggThrowsDP(eggs, throws) {
let eggThrows = [[]];
for (let i = 0; i < throws; i++) {
// A single egg can cover has many floors has throws are allowed
eggThrows[0].push(i + 1);
}
for (let i = 1; i < eggs; i++) {
// Any number of eggs can only cover 1 floor with a single throw
eggThrows.push([1]);
}
for (let i = 1; i < throws; i++) {
for (let j = 1; j < eggs; j++) {
eggThrows[j][i] = eggThrows[j - 1][i - 1] + eggThrows[j][i - 1] + 1;
}
}
return eggThrows;
}
const eggs = 10;
const throws = 15;
let eggThrows = maxHeightByEggThrowsDP(eggs, throws);
// display our data (boilerplate code)
// add a "row header" so we can read the egg count
eggThrows.forEach((row, i) => row.unshift(i + 1));
d3.select('#eggThrows>tbody').selectAll('tr').data(eggThrows).enter().append('tr').selectAll('td').data(d => d).enter().append('td').text(t => t);
d3.select('#throwsHeader').attr('colSpan', throws);
#eggThrows > thead td {
padding: 5px;
background-color: #404040;
color: white;
}
#eggThrows > tbody td {
padding: 5px;
background-color: #C0C0C0;
}
#eggThrows > tbody td:first-child {
background-color: #C0FF30;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<table id="eggThrows"><thead><tr><td>Eggs</td><td id="throwsHeader">Throws</td></tr></thead><tbody></tbody></table>
So we know the maximum height, but we wanted the first throw floor... It turns out there are multiple options. For example, we can now see that in the classic "2 eggs 14 throws" case, we can actually get an answer for 105 floors, not just 100. We also know that 2 eggs and 13 throws covers 91 floors, so we could throw the first egg from the 9th floor and still manage to find a solution within 14 throws.
And now we can answer the question:
The first floor to throw from is not higher than (maxHeightByEggThrows(m-1, n-1) + 1) and not lower than ([building height] - maxHeightByEggThrows(m, n-1))
(for 3 eggs and 100 floors, this is between the 8th and 37th floors)
This problem can be solved with following 3 approaches (that I know) :
Dynamic Programming
Solution using Binary Search Tree
Solution by obtaining the direct mathematical formula for maximum number of floors that can be tested or covered with given number of eggs and given number of drops
Let me first define some symbols that are used in analysis done afterwards :
e = number of eggs
f = number of floors in building
n = number of egg drops
Fmax(e, n) = maximum number of floors that can be tested or covered with e eggs and n drops
The crux for dynamic programming approach lies in following recursive formula for Fmax:
Fmax(e, n) = 1 + Fmax(e-1, n-1) + fmax(e, n-1)
And the crux for obtaining the direct mathematical formula for Fmax lies in following recursive formula for Fmax:
Fmax(e, n) = { ∑Fmax(e-1,i) for i = 1 to n } - Fmax(e-1, n) + n
Alternative solution using Binary Search Tree (BST) is also possible for this problem. In order to facilitate our analysis, let us draw BST with slight modifications as follows:
1. If egg breaks then child node is drawn on left down side
2. If egg does not break then child node is drawn straight down side
If we draw BST with above kind of representation then width of the BST represents the number of eggs.
Any BST with f number of nodes, drawn with above kind of representation and subjected to the constraint width of BST <= e (number of eggs) is a solution but it may not be the optimal solution.
Hence obtaining the optimal solution is equivalent to obtaining the arrangement of nodes in BST with minimum height subjected to the constraint: width of BST <= e
For more details about all the above 3 approaches, check my blog at: 3 approaches for solving generalized egg drop problem
Here is the algorithm which have implemented in swift(This includes concept of all previous algorithm):-
//Here n is number of eggs and k is number of floors
func eggDrop(_ n: Int, _ k: Int) -> Int {
var eggFloor: [[Int]] = Array(repeating:Array(repeating: 0, count: k+1),count: n+1)
if k == 1 {
return k
}
for i in 1...n {
eggFloor[i][1] = 1
eggFloor[i][0] = 0
}
for j in 1...k {
eggFloor[1][j] = j
}
for i in 2...n {
for j in 2...k {
eggFloor[i][j] = Int(INT_MAX)
for x in 1...j {
let attempts = 1 + max(eggFloor[i-1][x-1], eggFloor[i][j-x])
if attempts < eggFloor[i][j] {
eggFloor[i][j] = attempts
}
}
}
}
return eggFloor[n][k]
}

Towers of Hanoi - Bellman equation solution

I have to implement an algorithm that solves the Towers of Hanoi game for k pods and d rings in a limited number of moves (let's say 4 pods, 10 rings, 50 moves for example) using Bellman dynamic programming equation (if the problem is solvable of course).
Now, I understand the logic behind the equation:
where V^T is the objective function at time T, a^0 is the action at time 0, x^0 is the starting configuration, H_0 is cumulative gain f(x^0, a^0)=x^1.
The cardinality of the state space is $k^d$ and I get that a good representation for a state is a number in base k: d digits that can go from 0 to k-1. Each digit represents a ring and the digit can go from 0 to k-1, that are the labels of the k rings.
I want to minimize the number of moves for going from the initial configuration (10 rings on the first pod) to the end one (10 rings on the last pod).
What I don't get is: how do I write my objective function?
The first you need to do is choose a reward function H_t(s,a) which will define you goal. Once this function is chosen, the (optimal) value function is defined and all you have to do is compute it.
The idea of dynamic programming for the Bellman equation is that you should compute V_t(s) bottom-up: you start with t=T, then t=T-1 and so on until t=0.
The initial case is simply given by:
V_T(s) = 0, ∀s
You can compute V_{T-1}(x) ∀x from V_T:
V_{T-1}(x) = max_a [ H_{T-1}(x,a) ]
Then you can compute V_{T-2}(x) ∀s from V_{T-1}:
V_{T-2}(x) = max_a [ H_{T-2}(x,a) + V_{T-1}(f(x,a)) ]
And you keep on computing V_{t-1}(x) ∀s from V_{t}:
V_{t-1}(x) = max_a [ H_{t-1}(x,a) + V_{t}(f(x,a)) ]
until you reach V_0.
Which gives the algorithm:
forall x:
V[T](x) ← 0
for t from T-1 to 0:
forall x:
V[t](x) ← max_a { H[t](x,a) + V[t-1](f(x,a)) }
What actually was requested was this:
def k_hanoi(npods,nrings):
if nrings == 1 and npods > 1: #one remaining ring: just one move
return 1
if npods == 3:
return 2**nrings - 1 #optimal solution with 3 pods take 2^d -1 moves
if npods > 3 and nrings > 0:
sol = []
for pivot in xrange(1, nrings): #loop on all possible pivots
sol.append(2*k_hanoi(npods, pivot)+k_hanoi(npods-1, nrings-pivot))
return min(sol) #minimization on pivot
k = 4
d = 10
print k_hanoi(k, d)
I think it is the Frame algorithm, with optimization on the pivot chosen to divide the disks in two subgroups. I also think someone demonstrated this is optimal for 4 pegs (in 2014 or something like that? Not sure btw) and conjectured to be optimal for more than 4 pegs. The limitation on the number of moves can be implemented easily.
The value function in this case was the number of steps needed to go from the initial configuration to the ending one and it needed be minimized. Thank you all for the contribution.

How to efficiently detect a tie early in m,n,k-game (generalized tic-tac-toe)?

I'm implementing an m,n,k-game, a generalized version of tic-tac-toe, where m is the number of rows, n is the number of columns and k is the number of pieces that a player needs to put in a row to win. I have implemented a check for a win, but I haven't figured out a satisfactory way to check before the board is full of pieces, if no player can win the game. In other words, there might be empty slots on the board, but they cannot be filled in such a way that one player would win.
My question is, how to check this efficiently? The following algorithm is the best that I can think of. It checks for two conditions:
A. Go over all board positions in all 4 directions (top to bottom, right to left, and both diagonal directions). If say k = 5, and 4 (= k-1) consecutive empty slots are found, stop checking and report "no tie". This doesn't take into account for example the following situation:
OX----XO (Example 1)
where a) there are 4 empty consecutive slots (-) somewhere between two X's, b) next it is O's turn, c) there are less than four other empty positions on the board and no player can win by putting pieces to those, and d) it is not possible to win in any other direction than horizontally in the shown slots either. Now we know that it is a tie because O will eventually block the last winning possibility, but erroneously it is not reported yet because there are four consecutive empty slots. That would be ok (but not great). Checking this condition gives a good speed-up at the beginning when the checking algorithm usually finds such a case early, but it gets slower as more pieces are put on the board.
B. If this k-1-consecutive-empty-slots-condition isn't met, the algorithm would check the slots again consecutively in all 4 directions. Suppose we are currently checking from left to right. If at some point an X is encountered and it was preceded by an O or - (empty slot) or a board border, then start counting the number of consecutive X's and empty slots, counting in this first encountered X. If one can count to 5, then one knows it is possible for X to win, and "no tie" is reported. If an O preceded by an X is encountered before 5 consecutive X's, then X cannot win in those 5 slots from left to right starting from where we started counting. For example:
X-XXO (Example 2)
12345
Here we started checking at position 1, counted to 4, and encountered an O. In this case, one would continue from the encountered O in the same way, trying to find 5 consecutive O's or empty slots this time. In another case when counting X's or empty slots, an O preceded by one or more empty slots is encountered, before counting to 5. For example:
X-X-O (Example 3)
12345
In this case we would again continue from the O at position 5, but add to the new counter (of consecutive O's or empty slots) the number of consecutive empty slots that preceded O, here 1, so that we wouldn't miss for example this possible winning position:
X-X-O---X (Example 4)
In this way, in the worst case, one would have to go through all positions 4 times (4 directions, and of course diagonals whose length is less than k can be skipped), giving running time O(mn).
The best way I could think of was doing these two described checks, A and B, in one pass. If the checking algorithm gets through all positions in all directions without reporting "no tie", it reports a tie.
Knowing that you can check a win just by checking in the vicinity of the last piece that was added with running time O(k), I was wondering if there were quicker ways to do an early check for a tie. Doesn't have to be asymptotically quicker. I'm currently keeping the pieces in a two-dimensional array. Is there maybe a data structure that would allow an efficient check? One approach: what is the highest threshold of moves that one can wait the players to make before running any checks for a tie at all?
There are many related questions at Stack Overflow, for example this, but all discussions I could find either only pointed out the obvious tie condition, where the number of moves made is equal to the size of the board (or they checked if the board is full), or handled only the special case where the board is square: m = n. For example this answer claims to do the check for a tie in constant time, but only works when m = n = k. I'm interested in reporting the tie as early as possible and for general m,n and k. Also if the algorithm works for more than two players, that would be neat.
I would reduce the problem of determining a tie to the easier sub-problem:
Can player X still win?
If the answer is 'no' for all players, it is a tie.
To find out whether Player X can win:
fill all blank spaces with virtual 'X'-pieces
are there k 'X'-pieces in a row anywhere?
if there are not --> Player X cannot win. return false.
if there are, find the row of k stones with the least amount of virtual pieces. Count the number of virtual pieces in it.
count the number of moves player X has left, alternating with all other players, until the board is completely full.
if the number of moves is less than the amount of virtual pieces required to win, player X cannot win. return false.
otherwise, player X can still win. return true.
(This algorithm will report a possible win for player X even in cases where the only winning moves for X would have another player win first, but that is ok, since that would not be a tie either)
If, as you said, you can check a win just by checking in the vicinity of the last piece that was added with running time O(k), then I think you can run the above algorithm in O(k * Number_of_empty_spots): Add all virtual X-Piece, note any winning combinations in the vicinity of the added pieces.
The number of empty slots can be large, but as long as there is at least one empty row of size k and player X has still k moves left until the board is filled, you can be sure that player X can still win, so you do not need to run the full check.
This should work with any number of players.
Actually the constant time solution you referenced only works when k = m = n as well. If k is smaller then I don't see any way to adapt the solution to get constant time, basically because there are multiple locations on each row/column/diagonal where a winning consecutive k 0's or 1's may occur.
However, maintaining auxiliary information for each row/column/diagonal can give a speed up. For each row/column/diagonal, you can store the start and end locations for consecutive occurrences of 1's and blanks as possible winning positions for player 1, and similarly store start and end locations of consecutive occurrences of 0's and blanks as possible winning positions for player 0. Note that for a given row/column/diagonal, intervals for player 0 and 1 may overlap if they contain blanks. For each row/column/diagonal, store the intervals for player 1 in sorted order in a self-balancing binary tree (Note you can do this because the intervals are disjoint). Similarly store the intervals for player 0 sorted in a tree. When a player makes a move, find the row/column/diagonals that contain the move location and update the intervals containing the move in the appropriate row column and diagonal trees for the player that did not make the move. For the player that did not make a move, this will split an interval (if it exists) into smaller intervals that you can replace the old interval with and then rebalance the tree. If an interval ever gets to length less than k you can delete it. If a tree ever becomes empty then it is impossible for that player to win in that row/column/diagonal. You can maintain a counter of how many rows/columns/diagonals are impossible to win for each player, and if the counter ever reaches the total number of rows/columns/diagonals for both players then you know you have a tie. The total running time for this is O(log(n/k) + log(m/k)) to check for a tie per move, with O(mn/k) extra space.
You can similarly maintain trees that store consecutive intervals of 1's (without spaces) and update the trees in O(log n + log m) time when a move is made, basically searching for the positions before and after the move in your tree and updating the interval(s) found and merging two intervals if two intervals (before and after) are found. Then you report a win if an interval is ever created/updated and obtains length greater than or equal to k. Similarly for player 0. Total time to check for a win is O(log n + log m) which may be better than O(k) depending on how large k is. Extra space is O(mn).
Let's look at one row (or column or diagonal, it doesn't matter) and count the number of winning lines of length k ("k-line") it's possible to make, at each place in the row, for player X. This solution will keep track of that number over the course of the game, checking fulfillment of the winning condition on each move as well as detecting a tie.
1 2 3... k k k k... 3 2 1
There is one k-line including an X in the leftmost slot, two with the second slot from the left, and so on. If an opposing player, O or otherwise, plays in this row, we can reduce the k-line possibility counts for player X in O(k) time at the time of the move. (The logic for this step should be straightforward after doing an example, needing no other data structure, but any method involving checking each of the k rows of k from will do. Going left to right, only k operations on the counts is needed.) An enemy piece should set the possibility count to -1.
Then, a detectably tied game is one where no cell has a non-zero k-line possibility count for any player. It's easy to check this by keeping track of the index of the first non-zero cell. Maintaining the structure amounts to O(k*players) work on each move. The number of empty slots is less than those filled, for positions that might be tied, so the other answers are good for checking a position in isolation. However, at least for reasonably small numbers of players, this problem is intimately linked with checking the winning condition in the first place, which at minimum you must do, O(k), on every move. Depending on your game engine there may be a better structure that is rich enough to find good moves as well as detect ties. But the possibility counting structure has the nice property that you can check for a win whilst updating it.
If space isn't an issue, I had this idea:
For each player maintain a structure sized (2mn + (1 - k)(m + n) + 2(m - k + 1)(n - k + 1) + 2(sum 1 to (m - k))) where each value represents if one of another player's moves are in one distinct k-sized interval. For example for a 8-8-4 game, one element in the structure could represent row 1, cell 0 to 3; another row 1, cell 1 to 4; etc.
In addition, one variable per player will represent how many elements in their structure are still unset. Only one move is required to set an element, showing that that k-interval can no longer be used to win.
An update of between O(k) and O(4k) time per player seems needed per move. A tie is detected when the number of players exceeds the number of different elements unset.
Using bitsets, the number of bytes needed for each player's structure would be the structure size divided by 8. Notice that when k=m=n, the structure size is 4*k and update time O(4). Less than half a megabyte per player would be needed for a 1000,1000,5 game.
Below is a JavaScript example.
var m = 1000, n = 1000, k = 5, numberOfPlayers = 2
, numberOfHorizontalKIs = m * Math.max(n - k + 1,0)
, numberOfverticalKIs = n * Math.max(m - k + 1,0)
, horizontalVerticalKIArraySize = Math.ceil((numberOfHorizontalKIs + numberOfverticalKIs)/31)
, horizontalAndVerticalKIs = Array(horizontalVerticalKIArraySize)
, numberOfUnsetKIs = horizontalAndVerticalKIs
, upToM = Math.max(0,m - k) // southwest diagonals up to position m
, upToMSum = upToM * (upToM + 1) / 2
, numberOfSouthwestKIs = 2 * upToMSum //sum is multiplied by 2 to account for bottom-right-corner diagonals
+ Math.max(0,n - m + 1) * (m - k + 1)
, diagonalKIArraySize = Math.ceil(2 * numberOfSouthwestKIs/31)
, diagonalKIs = Array(diagonalKIArraySize)
, numberOfUnsetKIs = 2 * numberOfSouthwestKIs + numberOfHorizontalKIs + numberOfverticalKIs
function checkTie(move){
var row = move[0], column = move[1]
//horizontal and vertical
for (var rotate=0; rotate<2; rotate++){
var offset = Math.max(k - n + column, 0)
column -= offset
var index = rotate * numberOfHorizontalKIs + (n - k + 1) * row + column
, count = 0
while (column >= 0 && count < k - offset){
var KIArrayIndex = Math.floor(index / 31)
, bitToSet = 1 << index % 31
if (!(horizontalAndVerticalKIs[KIArrayIndex] & bitToSet)){
horizontalAndVerticalKIs[KIArrayIndex] |= bitToSet
numberOfUnsetKIs--
}
index--
column--
count++
}
//rotate board to log vertical KIs
var mTmp = m
m = n
n = mTmp
row = move[1]
column = move[0]
count = 0
}
//rotate board back
mTmp = m
m = n
n = mTmp
// diagonals
for (var rotate=0; rotate<2; rotate++){
var diagonalTopColumn = column + row
if (diagonalTopColumn < k - 1 || diagonalTopColumn >= n + m - k){
continue
} else {
var offset = Math.max(k - m + row, 0)
row -= offset
column += offset
var dBeforeM = Math.min (diagonalTopColumn - k + 1,m - k)
, dAfterM = n + m - k - diagonalTopColumn
, index = dBeforeM * (dBeforeM + 1) / 2
+ (m - k + 1) * Math.max (Math.min(diagonalTopColumn,n) - m + 1,0)
+ (diagonalTopColumn < n ? 0 : upToMSum - dAfterM * (dAfterM + 1) / 2)
+ (diagonalTopColumn < n ? row : n - 1 - column)
+ rotate * numberOfSouthwestKIs
, count = 0
while (row >= 0 && column < n && count < k - offset){
var KIArrayIndex = Math.floor(index / 31)
, bitToSet = 1 << index % 31
if (!(diagonalKIs[KIArrayIndex] & bitToSet)){
diagonalKIs[KIArrayIndex] |= bitToSet
numberOfUnsetKIs--
}
index--
row--
column++
count++
}
}
//mirror board
column = n - 1 - column
}
if (numberOfUnsetKIs < 1){
return "This player cannot win."
} else {
return "No tie."
}
}

Resources