Whole Tablet, Half Tablet
Posted on Thu 31 August 2017 in misc
Five Thirty Eight - Riddler Express (August 25th)¶
You take half of a vitamin every morning. The vitamins are sold in a bottle of 100 (whole) tablets, so at first you have to cut the tablets in half. Every day you randomly pull one thing from the bottle — if it’s a whole tablet, you cut it in half and put the leftover half back in the bottle. If it’s a half-tablet, you take the vitamin. You just bought a fresh bottle. How many days, on average, will it be before you pull a half-tablet out of the bottle?
Extra credit: What if the halves are less likely to come up than the full tablets? They are smaller, after all.
The problem is screaming for Markov modelling but I felt it could be solved using basic probability concepts alone and went ahead with the following approach.
Algebraic Solution¶
from sympy.abc import i, j, k, n, z
from sympy import Product, factorial, Sum, oo, evalf, factor
from sympy import *
init_printing()
Average number of days before one pulls a half-tablet out is $\sum_{k=2}^{101}\;k.P(\;First\;Ever\;🚦 🚥_{k^{th} day})$
And $P(\;First\;Ever\;🚦 🚥_{i^{th} day})\;=\;P(\;💊\;_{1^{st} day})P(\;💊\;_{2^{nd} day})....P( \;💊\;_{i-1^{th} day})P(\;🚦 🚥_{i^{th} day})$
After a few simplifications, the required answer can be expressed as
simplify(expectedDays)
This is probably the point where something cool can be done, say for example, using inequalities to get upper and lower bounds or conjuring up some distribution / arrangement that maps to these coefficients. I chose the boring method of letting the computer handle these calculations though. I was more interested in implementing a simulation for this problem and wanted the get the answer algebraically so that I can compare it with the number the simulation spits out.
expectedDays = Sum(Product(1-i/100, (i, 0, j-2) )*j*(j-1)/100, (j, 2, 99))
expectedDays.evalf()
Bonus Credit Solution¶
In case, a whole-tablet is $\lambda$ times more likely than a half-tablet to be picked
def modifiedExpectedDays(Lambda):
return Sum(Product((Lambda*(100-i)/(Lambda*(100-i) + i)),
(i, 0, j) )*(j+1)*(j+2)/(Lambda*(100-j-1) + j+1),
(j, 1, 99)).evalf()
factor(modifiedExpectedDays(z))
modifiedExpectedDays(2) # If the whole-tablet is twice as likely to be picked
How the expected number of days varies with Lambda¶
b = [int(modifiedExpectedDays(factor)) for factor in range(1,1000,100)]
import plotly.plotly as py
import plotly.graph_objs as go
data = [go.Scatter(
x=[1,101,201,301,401,501,601,701,801,901],
y=b,
mode='lines+markers',
line=dict(shape='spline')
)]
py.iplot(data, filename='spline-interpolation')
modifiedExpectedDays(2000000000000000)
Converges to 101 just as expected
Simulation Solution¶
import numpy as np
import plotly
import random
import sympy
from sympy import *
init_printing()
import plotly
Bottle, Day Zero¶
bottle = np.ones(100)
Tablet eject logic¶
def takeMedicine(sizeMatters=False, verbose=False):
global bottle
if sizeMatters:
glottle = bottle[bottle > 0.5]
pill = random.choice(np.concatenate((bottle,glottle)))
else:
pill = random.choice(bottle)
if pill == 1.0:
bottle[np.argmax(bottle==1.0)] = 0.5
if verbose:
print("Whole Tablets {}, Half Tablets {} 💊".format(currentStatus()[0], currentStatus()[1]))
return 1
else:
bottle = np.delete(bottle, np.argmax(bottle==0.5) )
if verbose:
print("Whole Tablets {}, Half Tablets {} 🚦 🚥".format(currentStatus()[0], currentStatus()[1]))
return 0
Days taken to pull a half-tablet out¶
def daysTaken(sizeMatters=False, verbose=False):
global bottle
days = 0
for day in range(1,201):
if takeMedicine(sizeMatters, verbose):
days += 1
else:
bottle = np.ones(100)
break
return days + 1
Average of a million first half-tablet sightings¶
def averageDays(sizeMatters=False, verbose=False, numberOfRuns=1000000):
totalDays = 0
for element in range(numberOfRuns):
totalDays += daysTaken(sizeMatters, verbose)
return totalDays/numberOfRuns
Results¶
Extra: Entire State History¶
def completeRun():
global bottle
days = 0
for day in range(1,201):
days += 1
print("On Day {}: ".format(days))
takeMedicine(1, 1)
bottle = np.ones(100)
def currentStatus(noHalfPills=True, noMorePills=False):
global bottle
unique, counts = np.unique(bottle, return_counts=True)
uniqueCounts = dict(zip(unique, counts))
try:
numFullPills = uniqueCounts[1.0]
except KeyError:
numFullPills = 0
try:
numHalfPills = uniqueCounts[0.5]
except KeyError:
numHalfPills = 0
return numFullPills, numHalfPills
completeRun()