RosettaCodeData/Task/100-prisoners/Python/100-prisoners-3.py

178 lines
5.1 KiB
Python

'''100 Prisoners'''
from random import randint, sample
# allChainedPathsAreShort :: Int -> IO (0|1)
def allChainedPathsAreShort(n):
'''1 if none of the index-chasing cycles in a shuffled
sample of [1..n] cards are longer than half the
sample size. Otherwise, 0.
'''
limit = n // 2
xs = range(1, 1 + n)
shuffled = sample(xs, k=n)
# A cycle of boxes, drawn from a shuffled
# sample, which includes the given target.
def cycleIncluding(target):
boxChain = [target]
v = shuffled[target - 1]
while v != target:
boxChain.append(v)
v = shuffled[v - 1]
return boxChain
# Nothing if the target list is empty, or if the cycle which contains the
# first target is larger than half the sample size.
# Otherwise, just a cycle of enchained boxes containing the first target
# in the list, tupled with the residue of any remaining targets which
# fall outside that cycle.
def boxCycle(targets):
if targets:
boxChain = cycleIncluding(targets[0])
return Just((
difference(targets[1:])(boxChain),
boxChain
)) if limit >= len(boxChain) else Nothing()
else:
return Nothing()
# No cycles longer than half of total box count ?
return int(n == sum(map(len, unfoldr(boxCycle)(xs))))
# randomTrialResult :: RandomIO (0|1) -> Int -> (0|1)
def randomTrialResult(coin):
'''1 if every one of the prisoners finds their ticket
in an arbitrary half of the sample. Otherwise 0.
'''
return lambda n: int(all(
coin(x) for x in range(1, 1 + n)
))
# TEST ----------------------------------------------------
# main :: IO ()
def main():
'''Two sampling techniques constrasted with 100 drawers
and 100 prisoners, over 100,000 trial runs.
'''
halfOfDrawers = randomRInt(0)(1)
def optimalDrawerSampling(x):
return allChainedPathsAreShort(x)
def randomDrawerSampling(x):
return randomTrialResult(halfOfDrawers)(x)
# kSamplesWithNBoxes :: Int -> Int -> String
def kSamplesWithNBoxes(k):
tests = range(1, 1 + k)
return lambda n: '\n\n' + fTable(
str(k) + ' tests of optimal vs random drawer-sampling ' +
'with ' + str(n) + ' boxes: \n'
)(fName)(lambda r: '{:.2%}'.format(r))(
lambda f: sum(f(n) for x in tests) / k
)([
optimalDrawerSampling,
randomDrawerSampling,
])
print(kSamplesWithNBoxes(10000)(10))
print(kSamplesWithNBoxes(10000)(100))
print(kSamplesWithNBoxes(100000)(100))
# ------------------------DISPLAY--------------------------
# fTable :: String -> (a -> String) ->
# (b -> String) -> (a -> b) -> [a] -> String
def fTable(s):
'''Heading -> x display function -> fx display function ->
f -> xs -> tabular string.
'''
def go(xShow, fxShow, f, xs):
ys = [xShow(x) for x in xs]
w = max(map(len, ys))
return s + '\n' + '\n'.join(map(
lambda x, y: y.rjust(w, ' ') + ' -> ' + fxShow(f(x)),
xs, ys
))
return lambda xShow: lambda fxShow: lambda f: lambda xs: go(
xShow, fxShow, f, xs
)
# fname :: (a -> b) -> String
def fName(f):
'''Name bound to the given function.'''
return f.__name__
# ------------------------GENERIC -------------------------
# Just :: a -> Maybe a
def Just(x):
'''Constructor for an inhabited Maybe (option type) value.
Wrapper containing the result of a computation.
'''
return {'type': 'Maybe', 'Nothing': False, 'Just': x}
# Nothing :: Maybe a
def Nothing():
'''Constructor for an empty Maybe (option type) value.
Empty wrapper returned where a computation is not possible.
'''
return {'type': 'Maybe', 'Nothing': True}
# difference :: Eq a => [a] -> [a] -> [a]
def difference(xs):
'''All elements of xs, except any also found in ys.'''
return lambda ys: list(set(xs) - set(ys))
# randomRInt :: Int -> Int -> IO () -> Int
def randomRInt(m):
'''The return value of randomRInt is itself
a function. The returned function, whenever
called, yields a a new pseudo-random integer
in the range [m..n].
'''
return lambda n: lambda _: randint(m, n)
# unfoldr(lambda x: Just((x, x - 1)) if 0 != x else Nothing())(10)
# -> [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
# unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
def unfoldr(f):
'''Dual to reduce or foldr.
Where catamorphism reduces a list to a summary value,
the anamorphic unfoldr builds a list from a seed value.
As long as f returns Just(a, b), a is prepended to the list,
and the residual b is used as the argument for the next
application of f.
When f returns Nothing, the completed list is returned.
'''
def go(v):
xr = v, v
xs = []
while True:
mb = f(xr[0])
if mb.get('Nothing'):
return xs
else:
xr = mb.get('Just')
xs.append(xr[1])
return xs
return lambda x: go(x)
# MAIN ---
if __name__ == '__main__':
main()