Sesame Street teaches us to easily count up to ten using our fingers. A computer doesn't have fingers, but it too can use brute force, enumerating things one by one, to easily count up to a billion or so.
So in that sense, a billion is a small number. For really big numbers, like 10100, we need a better strategy: counting by calculating the number of things rather than enumerating them. This notebook describes some strategies for counting by calculating.
We'll start with a really simple counting problem:
Given a cube with *n* sub-cubes on a side, how many total sub-cubes are there?
Here are two example cubes:
n = 3 | n = 9 |
---|---|
![]() |
![]() |
For the n = 3 cube, we could use the enumeration strategy, pointing to each sub-cube with our finger and counting 1, 2, 3, ... 27. That wouldn't work as well for the n = 9 cube, and it would be completely infeasible for an n = 1000 cube. How can we count with a computer?
First let's get some Python preliminaries out of the way: imports, and three utility functions:
cat
concatenates strings into one big stringquantify
(from the itertools
module recipes) counts how many things a predicate is true for.totalcount
totals up all the values in a Counter.# Python 3.x imports, and some utilities
from collections import Counter
from functools import lru_cache
from itertools import product, permutations, combinations
from math import factorial, inf # infinity
import re
cat = ''.join
def quantify(iterable, predicate=bool) -> int:
"Count the number of items in iterable for which the predicate is true."
return sum(map(predicate, iterable))
def totalcount(counter) -> int:
"The sum of all the values in a Counter (or mapping)."
return sum(counter.values())
Here is an enumeration strategy and a calculation strategy for sub-cube counting:
def enumerate_subcubes(n) -> int:
return quantify(1 for width in range(n)
for depth in range(n)
for height in range(n))
def calculate_subcubes(n) -> int:
return n ** 3
The calculation strategy is much faster:
%time enumerate_subcubes(300)
CPU times: user 6.4 s, sys: 79.7 ms, total: 6.48 s Wall time: 7.77 s
27000000
%time calculate_subcubes(300)
CPU times: user 5 µs, sys: 1 µs, total: 6 µs Wall time: 9.06 µs
27000000
A more difficult problem:
Students at a school must check in with the guidance counselor if they have two total absences, or three consecutive late days. Each student's attendance record consists of a string of 'A' for absent, 'L' for late, or 'P' for present. For example: "LAPLPA" requires a meeting (because there are two absences), and "LAPLPL" is OK (there are three late days, but they are not consecutive). How many attendance records of length n are OK?
The enumeration strategy says: define what it means for a record to be ok
, define all the strings of length n, and count how many of them are ok
:
def ok(record: str) -> bool:
"Is this student record OK? (Not 2 abscences, nor 3 consecutive lates.)"
return not re.search(r'A.*A|LLL', record)
def all_strings(alphabet, n):
"All length-n strings over the given alphabet."
return map(cat, product(alphabet, repeat=n))
def enumerate_ok(n) -> int:
"How many attendance records of length n are ok?"
return quantify(all_strings('LAP', n), ok)
{n: enumerate_ok(n) for n in range(11)}
{0: 1, 1: 3, 2: 8, 3: 19, 4: 43, 5: 94, 6: 200, 7: 418, 8: 861, 9: 1753, 10: 3536}
This looks good, but for large values of n I will need an efficient calculation strategy. Here's how I think about it:
ok
strings of length n-1, and use that to quickly (in constant time) compute a summary of the ok
strings of length n.ok
strings is too much. A simple count of the number of ok
strings is not enough. Instead, I need several different counts, for several different classes of strings. Each class is defined by the number of 'A'
characters in the string, and the number of consecutive 'L'
characters at the end of the string (because these are the two things that determine whether the string will be ok
or not ok
when I add a letter to the end). So the summary can be represented as a Counter
of the form {(A, L): count, ...}
.For n = 0, the summary is {(0, 0): 1}
, because there is one string of length zero, the empty string, which has no 'A'
nor 'L'
in it. From there we can proceed in a "bottom-up" fashion to compute the total number of ok
strings for the next value of n
, using the function next_summary
, which says that we can add an 'A'
to any string that doesn't already have one; we can add an L
to any string that doesn't already end in 2 'L'
s; and we can add a 'P'
to any string.
summary0 = Counter({(0, 0): 1})
def next_summary(prev_summary: Counter) -> Counter:
"Given a summary of the form {(A, L): count}, return summary for one letter more."
summary = Counter()
for (A, L), count in prev_summary.items():
if not A: summary[A+1, 0] += count # add an 'A'
if L < 2: summary[A, L+1] += count # add an 'L'
summary[A, 0] += count # add a 'P'
return summary
next_summary(summary0)
Counter({(0, 0): 1, (0, 1): 1, (1, 0): 1})
next_summary(_)
Counter({(0, 0): 2, (0, 1): 1, (0, 2): 1, (1, 0): 3, (1, 1): 1})
next_summary(_)
Counter({(0, 0): 4, (0, 1): 2, (0, 2): 1, (1, 0): 8, (1, 1): 3, (1, 2): 1})
Here's an implementation of calculate_ok
, and a demonstration that it gets the same results as enumerate_ok
:
def calculate_ok(n) -> int:
"How many strings of length n are ok?"
summary = summary0
for _ in range(n):
summary = next_summary(summary)
return totalcount(summary)
{n: calculate_ok(n) for n in range(11)}
{0: 1, 1: 3, 2: 8, 3: 19, 4: 43, 5: 94, 6: 200, 7: 418, 8: 861, 9: 1753, 10: 3536}
But we can go way beyond what we could do with enumerate_ok
:
calculate_ok(500)
744860152388838641467766615047636640123287960024109564468341118067976387620845421483777039844936500522445420732911099270409841757052659
iterate
¶This pattern of repeatedly calling next_summary
n times will be common in this notebook; we can encapsulate it in a function called iterate
(after the Haskell function of that name) allowing us to implement a version of calculate_ok
in one line:
def calculate_ok2(N): return totalcount(iterate(next_summary, summary0, N))
def iterate(f, x, n):
"Return f^n(x); for example, iterate(f, x, 3) == f(f(f(x)))."
for _ in range(n):
x = f(x)
return x
calculate_ok2(500)
744860152388838641467766615047636640123287960024109564468341118067976387620845421483777039844936500522445420732911099270409841757052659
As a third alternative, instead of working up from 0 to n
we can work down from n
to 0 (although we may run up against Python's recursion limit):
def calculate_ok3(n) -> int: return totalcount(nth_summary(n))
def nth_summary(n) -> Counter:
"The {(A, L): count} summary for strings of length n."
return (summary0 if n == 0 else next_summary(nth_summary(n - 1)))
calculate_ok3(500)
744860152388838641467766615047636640123287960024109564468341118067976387620845421483777039844936500522445420732911099270409841757052659
A comparison:
n = 500
%timeit calculate_ok(n)
%timeit calculate_ok2(n)
%timeit calculate_ok3(n)
float(calculate_ok(n))
The slowest run took 4.34 times longer than the fastest. This could mean that an intermediate result is being cached. 100 loops, best of 3: 6.26 ms per loop 100 loops, best of 3: 9.52 ms per loop 100 loops, best of 3: 5.52 ms per loop
7.448601523888386e+134
There are over 10134 ok strings of length 500, but it only takes a couple milliseconds to count them. Any of the three methods works equally well; use the approach you feel comfortable with.
Here's another problem:
How many strings of length k can be formed such that the first occurrences of each character in the string forms a prefix of the alphabet?
Let's first make sure we understand the problem, because the statement is a bit abstract and tricky. Given a string we have to pick out the first occurrences of characters (letters). For example, for the string "abba"
, we read left-to-right, recording each time we see a letter for the first time; that gives us "ab"
(the subsequent occurrences of 'a'
and 'b'
don't matter). Is that a prefix of the alphabet, "abcd..."
? Yes it is.
Here are some test cases—pairs of strings along with their first occurrences—labelled with True
for valid strings and False
for invalid:
alpha_test_cases = {
True: {('abba', 'ab'),
('', ''),
('a', 'a'),
('aaa', 'a'),
('abc', 'abc'),
('abac', 'abc'),
('abbacabbadabba', 'abcd')},
False: {('b', 'b'), # 'a' is missing
('cab', 'cab'), # 'c' is before 'ab'
('abd', 'abd'), # 'c' is missing
('aback', 'abck'), # 'k' is before 'd-j'
('badcafe','badcfe'), # 'b' is before 'a', etc.
('abecedarian', 'abecdrin')}} # 'e' is before 'cd', etc.
Now to implement the code that can run these tests. Since k could be arbitrarily large, I will by default assume an alphabet consisting of the non-negative integers, not the letters. Here are the key concepts:
integers = range(10 ** 100)
letters = 'abcdefghijklmnopqrstuvwxyz'
def is_alpha_firsts(s, alphabet=integers) -> bool:
"Do the first occurrences of `s` form a prefix of the alphabet?"
pre = firsts(s)
return pre == list(alphabet[:len(pre)])
def firsts(s) -> list:
"The first occurrences of each character, in the order they appear."
return sorted(set(s), key=lambda x: s.index(x))
I'll test this code on the examples I showed above by defining the function alpha_test
:
def alpha_test(cases=alpha_test_cases):
"Verify all the test cases."
for validity in cases:
for (s, s_firsts) in cases[validity]:
assert firsts(s) == list(s_firsts)
assert is_alpha_firsts(s, letters) == validity
return 'pass'
alpha_test()
'pass'
Now I can count the number of valid strings by enumerating all possible strings in the alphabet and checking each one with is_alpha_firsts
. The complexity of this algorithm is $O(k^{k+1})$, because there are $k^k$ strings, and to validate a string requires looking at all $k$ characters:
def enumerate_alpha_firsts(k) -> int:
"""Enumerate all possible strings and count the number of valid ones."""
all_strings = product(range(k), repeat=k)
return quantify(all_strings, is_alpha_firsts)
{k: enumerate_alpha_firsts(k) for k in range(7)}
{0: 1, 1: 1, 2: 2, 3: 5, 4: 15, 5: 52, 6: 203}
Now let's think about a calculation strategy that would be faster than the enumeration strategy.
As with previous problems, I need a summary of the relevant information for strings of length k, to help me calculate the relevant information for length k+1. I know that if I have a valid string of length k with m distinct characters in it, then I can extend it by one character in m ways by repeating any of those m characters, or I can introduce a first occurrence of character number m+1 (but I can do that in just 1 way). I can't validly introduce any other character. So a good summary would be a Counter of {
m:
count}
, and we get this:
def calculate_alpha_firsts(k):
"Count the number of strings of length k that have alphabetic first character occurrences."
return totalcount(iterate(next_alpha_summary, {0: 1}, k))
def next_alpha_summary(prev_summary) -> Counter:
"Given a summary of the form {m: count}, return summary for one character more."
summary = Counter()
for m, c in prev_summary.items():
summary[m] += c * m # Add any of the m previously-used characters
summary[m + 1] += c # Add character number m+1
return summary
{k: calculate_alpha_firsts(k) for k in range(7)}
{0: 1, 1: 1, 2: 2, 3: 5, 4: 15, 5: 52, 6: 203}
calculate_alpha_firsts(1000)
29899013356824084214804223538976464839473928098212305047832737888945413625123259596641165872540391578300639147082986964028021802248993382881013411276574829121155811755170830666039838837273971971676782389800810361809319250755399325279656765435255999301529770267107281619733800281695881540007577899106878679451165492535930459233713316342551545242815802367257284852612201081016386308535990145447341800455472334713864080523978960296365736999295932080550928561633025800627524911700149562106895897725047744775812241800937310491797818107578233924187312824632629095993832334781713007323483688294825326897450386817327410532925074613888321264138083842196202242956001314953449497244271843922741908252107652201346933889741070435350690242062001522697855278356012055718392851567813397125419144780476479197990921602015873703820769182603836788465785093563686025690269802153802436873530877006737154523895273029510238745997356292232631282773748762989386003970214423843947094021177989737557020369751561595003372955621411858485959813344799967960196238368337022346946771703060269288691694028444791203978533454759410587065022546491518871238421560825907135885619221776405898771057270555581449229994215739476758785884545723062263992367750091319644861547658472282284005892044371587560711880627741139497818835632120761570174928529697397267899554407350161283097123211048049269727655279783900702416095132827766428865017653366696304131436690232979453876337599721772897049270230544262611264917393374756384152784943607952408782612639220380791445272655004475989064276373713608901650681165467490310898804916827069427310961109285035545084791339423266482359955663377201515204340817580915468489969181643341007197836481461051798995640789292580146918580703759556634019451731530034209189203377522668309771129566108101617727442045637098112678864654309987785463307376544339506878267267349348171320834971956806668304099159992067385998690820326902473886782781499414773179
That's a big number.
Another way to keep track of the number of valid strings of length k with m distinct characters is with a recursive counting function, which I will call C(k, m)
. There are three cases:
C(0, 0)
is 1, because the empty string is valid (and contains no distinct characters).C(k, m)
is 0 when k
is negative (because there is no such thing as a negative length string) or less than m
(because a string can't contain more distinct characters than its length).m
ways to add an existing letter to each of the strings of length k - 1
, and there is one way to add a new letter.@lru_cache(None)
def C(k, m) -> int:
"Count the number of valid strings of length k, that use m distinct characters."
return (1 if (k == m == 0) else
0 if (k < 0 or k < m) else
m * C(k - 1, m) + C(k - 1, m - 1))
Note that I used lru_cache
, to avoid having to re-compute intermediate results.
Now I can define calculate_alpha_firsts2(k)
as the sum of C(k, m)
over all values of m
:
def calculate_alpha_firsts2(k) -> int: return sum(C(k, m) for m in range(k + 1))
Let's make sure the two calculations gives the same answers as the enumeration, at least for small values of $k$:
all(enumerate_alpha_firsts(k) == calculate_alpha_firsts(k) == calculate_alpha_firsts2(k)
for k in range(7))
True
Consider the barcode pictured below. A valid barcode consists of alternating black and white stripes, where each stripe is either 1, 2, or 3 units wide. The question is: for a box that is n units wide, how many different valid barcodes are there?
We'll represent a barcode as a string, such as 'BBWWWBBW'
. Each of the n characters in the string can be either 'B'
or 'W'
, but the string can't have 4 of the same character in a row. We'll start with the familiar enumerate-and-test strategy:
def valid_barcode(code) -> bool: return 'BBBB' not in code and 'WWWW' not in code
def all_barcodes(n): return map(cat, product('BW', repeat=n))
def enumerate_barcodes(n) -> int: return quantify(all_barcodes(n), valid_barcode)
Here's a table of counts for small values of n:
{n: enumerate_barcodes(n) for n in range(13)}
{0: 1, 1: 2, 2: 4, 3: 8, 4: 14, 5: 26, 6: 48, 7: 88, 8: 162, 9: 298, 10: 548, 11: 1008, 12: 1854}
We can see that the table starts out with the powers of two: 1, 2, 4, 8. That makes sense: each of the n positions in the barcode could be either black or white, so that's 2n barcodes. But barcodes with 4 or more of the same color in a row are invalid, so for n = 4, 'BBBB'
and 'WWWW'
are invalid, and we get 14 (not 16) valid barcodes. For n = 5 and up, the difference between 2n and the count of valid barcodes becomes larger.
Now for a fast calculation. As before, we need a representation that summarizes the valid barcodes of length n. The key thing we need to know is how many barcode units of the same color are at the end of the barcode: if it is 1 or 2, then we can add another instance of the same color to the end; no matter what it is we can always add the opposite color (and then the barcode will end in just one unit of the same color).
def calculate_barcodes(k): return totalcount(iterate(next_barcode_summary, {0: 1}, k))
def next_barcode_summary(prev_summary) -> Counter:
"Given a summary of the form {end: count}, return summary for one unit more."
summary = Counter()
for end, c in prev_summary.items():
if end < 3:
summary[end + 1] += c
summary[1] += c
return summary
{k: calculate_barcodes(k) for k in range(13)}
{0: 1, 1: 2, 2: 4, 3: 8, 4: 14, 5: 26, 6: 48, 7: 88, 8: 162, 9: 298, 10: 548, 11: 1008, 12: 1854}
Verify that enumeration and calculation do the same thing for small values of n:
assert all(enumerate_barcodes(n) == calculate_barcodes(n) for n in range(20))
Demonstrate that we can compute big numbers:
calculate_barcodes(10000)
3861431277625007961956955484353530119634001892816040917233932945064320273497370215771811960744678098449553175356862760450029708838175822242262792740296878964074883622671541479265048463512360941352413049264274485743297728357502930085506419846119753743423275462450713981257123756695327534235952507322555045959925039572403245743061549541274972562526816439217931608999532601457740681763142591939324781110768274782850152125981385364637513839024687081770052346957401052189529936883629883775724870785833452510126377097128195647948735625551805771200981065390268401761158588370204299868440790417559363818139283430755197453196664541025472104658523804014518931760254828135638415413408780736125999685589725526874318196976263624936793335541955083569139572617638693840543637407782446933562063756941909207810703824222697116352937601482868529114899390708691493432262019965964035273813939182009029538539757438413668036430833988535147478776959903569999055599703304516221455076523484352182404159659661240973630499557250950477386781288167303525408434907471599633608826344342446869378392685927230076723348547049789748491137890119623879128231595262082532286126660730784998797003448161418183918024344043613986269574364002761796194720086663633818066518383357158900007727428966795190669943187944515210398817044521469661682433819382541570464313514353180507280999817655346753846288267575488232309682752190042235927387740555053797529717247933281707578234957297940957985028202675009617273305705968728942732545640071819447202821044438874136038990729299397246489481363094787623333269036228204295737689912072742922999093173704662567170601571800655678495878408411165386708197339087266092115780445248022350264528021426918011958029075557108406192634108388793426193565093359228830473466263938925653882623387099940055081548579900911968982480432787324594136156465497071249090902373689488770079828664684036332439902542305885855063306802357476735193730805776865978477463443216064890685565824039494431583860044254937057151591772329166180799218273949130368000609439119706131185925513231524378443124711002954768565310478734594199963723940004083846148186188631535346393565417946059827530465095391420721324513264443555308798011729355327067365885381721113676766083967129134309202588682281325857855165574371421168078423586430302604691681703448297310856061359072019641696782664700526740970642410648738647199882852824774480069658314840218244137906558931392021855430239022442215072063184370394666755160595637651648014842470092745788026900633378764049390511584307920491613532339464196985013369306439276245475602674051266814071448421271626238787893306176669816773478303604652043427279626683524358370
This problem is covered in depth in another notebook, so here I present just the part that has to do with counting things:
Say you’re given the following challenge: create a set of five rectangles that have sides of length 1, 2, 3, 4, 5, 6, 7, 8, 9 and 10 units. You can combine sides in a variety of ways: for example, you could create a set of rectangles with dimensions 1 x 3, 2 x 4, 5 x 7, 6 x 8 and 9 x 10. How many different sets of five rectangles are possible?
This is a basic combinatorics problem. I will present three methods to calculate the number of sets. If all goes well they will give the same answer. The example set of rectangles given in the problem was
{1 × 3, 2 × 4, 5 × 7, 6 × 8, 9 × 10}
and in general it would be
{A × B, C × D, E × F, G × H, I × J}
The question is: how many distinct ways can we assign the integers 1 through 10 to the variables A through J?
Method 1: Count all permutations and divide by repetitions: There are 10 variables to be filled, so there are 10! = 3,628,800 permutations. But if we fill the first two variables with 1 × 3, that is the same rectangle as 3 × 1. So divide 10! by 25 to account for the fact that each of 5 rectangles can appear 2 ways. Similarly, if we fill A and B with 1 × 3, that yields the same set as if we filled C and D with 1 × 3. So divide again by 5! (the number of permutations of 5 things) to account for this. That gives us:
factorial(10) / 2 ** 5 / factorial(5)
945.0
(It is always a relief when this "count and divide" method comes out to a whole number.)
Method 2: Count without repetitions: in each rectangle of the example set the smaller component is listed first, and in each set, the rectangles with smaller first components are listed first. Without loss of generality, let's assume that all rectangle sets must be of this form, and count how many sets there are that respect this ordering. We'll work from left to right. How many choices are there for variable A? Only one! A must always be 1, because we agreed that the smallest number comes first. Then, given A, there are 9 remaining choices for B. For C, given A and B, there is again only one choice: C must be the smallest of the remaining 8 numbers. That leaves 7 choices for D, 5 for F, 3 for H and 1 for J. So:
9 * 7 * 5 * 3 * 1
945
(It is always a relief when two methods give the same answer.)
Method 3: Write a program to enumerate and check: We'll generate all permutations of sides, and then check which ones are valid rectangle sets: they must have the first element of each pair less than the second (i.e. A < B, C < D, ...) and the pairs must be sorted, which is equivalent to saying their first elements are sorted (i.e. A < C < E < G < I).
def valid_rectangle_set(sides):
A,B, C,D, E,F, G,H, I,J = sides
return A < B and C < D and E < F and G < H and I < J and A < C < E < G < I
quantify(permutations(range(1, 11)), valid_rectangle_set)
945
(It is a relief that once again we get the same answer.)
In a grid, how many paths are there from the upper left to the lower right corner, making only rightward or downward moves?
Here is an example 11 × 6 grid, with three of the possible paths:
----------+ |.......... --+........
..........| |.......... ..+-+......
..........| +--+....... ....+-+....
..........| ...|....... ......+-+..
..........| ...|....... ........|..
..........| ...+------- ........+--
We can use the same three methods as the previous problem:
Method 1: Count all permutations and divide by repetitions: Any path must consist of 10 right and 5 down moves, but they can appear in any order. Arranging 15 things in any order gives 15! = 1,307,674,368,000 possible paths. But that counts all the moves as being distinct, when actually the 10 right moves are indistinguishable, as are the 5 down moves, so we need to divide by the number of ways that they can be arranged. That gives us:
factorial(15) // (factorial(10) * factorial(5))
3003
Method 2: Count without repetitions: Another way to look at it is that there will be 15 total moves, so start with all 15 being "right" moves and then choose 5 of them to become "down" moves. So the answer is (15 choose 5), which happens to lead to the same formula we just used:
def choose(n, k) -> int: return factorial(n) // (factorial(n - k) * factorial(k))
choose(15, 5)
3003
Method 3: Write a program to count the paths: The function calculate_paths(start, goal)
counts the number of paths from the start location to the goal location, where a location is an (x, y)
pair of integers.
In general, the number of paths to the goal is the number of paths to the location just to the left of the goal, plus the number of paths to the location just above the goal. But there are two special cases: there is only one path (the empty path) when the start is equal to the goal, and there are zero paths when the goal is an illegal destination (one with a negative coordinate).
@lru_cache(None)
def calculate_paths(start=(0, 0), goal=(5, 10)):
"Number of paths to goal, using only 'right' and 'down' moves."
(x, y) = goal
return (1 if goal == start else
0 if x < 0 or y < 0 else
calculate_paths(start, (x - 1, y)) +
calculate_paths(start, (x, y - 1)))
calculate_paths()
3003
Even though calculate_paths
is slower than the choose
calculation, it can still handle reasonably large grids:
N = 100
assert calculate_paths(goal=(N, N)) == choose(2 * N, N)
calculate_paths(goal=(N, N))
90548514656103281165404177077484163874504589675413336841320
Why bother with the recursive function calculate_paths
when the choose
formula works so well? Good question. One reason is that the two different approaches validate each other by giving the same answer. Another reason is that we can modify calculate_paths
to handle things that are hard to do with just the formula. For example, what if we have a grid with some obstacles in it? I'll define a Grid
constructor, which adopts the convention that the input is a string of rows, where a '.'
character within a row is a passable square, and all other (non-whitespace) characters are impassible barriers. Then calculate_grid_paths
finds the number of paths on a grid from start to goal (by default, from upper left to lower right):
def Grid(text): return tuple(text.split())
@lru_cache(None)
def calculate_grid_paths(grid, start=(0, 0), goal=None):
"Number of paths from start to goal on grid, using only 'right' and 'down' moves."
if goal is None: goal = (len(grid[-1]) - 1, len(grid) - 1) # bottom right
(x, y) = goal
return (1 if goal == start else
0 if x < 0 or y < 0 or grid[y][x] != '.' else
calculate_grid_paths(grid, start, (x - 1, y)) +
calculate_grid_paths(grid, start, (x, y - 1)))
We can verify that we get the same answer on the 11 by 6 empty grid:
calculate_grid_paths(Grid("""
...........
...........
...........
...........
...........
...........
"""))
3003
Here's a grid where there are only two paths around the walls:
calculate_grid_paths(Grid("""
...........
.........|.
.........|.
.........|.
.--------+.
...........
"""))
2
If we put a hole in the wall, there should be many paths (but less than 3003 because most of the wall remains):
calculate_grid_paths(Grid("""
...........
.........|.
.........|.
.........|.
.-------.+.
...........
"""))
167
Here are a couple of bigger examples, courtesy of ascii-art-generator.org:
calculate_grid_paths(Grid("""
....................................................................................................
...................................NK0OkdddolcccccccccclloddxkO0XN..................................
............................X0kdlc:cccccclodxllxxxkkxdloddolccccccccodkKN...........................
.......................N0xl:cccllcco0.XXXXXX.l,O.NNNNo,kXXXXXXXNklllolccccokKN......................
..................X.0dc::loool:,XXXdXXXXXXXXN:.X''X''X.dXXXXXXX.KcXX';coooolcclkX...................
.................Kd.:cooo:'X.....'O.XXXXXXXXX;.........oXXXXXXXXXNo......X,lddoc:ckX................
...............Oc,c.oc'..........l.XXXXXXXXXOX.........:NXXXXXXXXX0'.........X;oxoc;oK..............
.............0c,lxl..............,O.XXXXXXXK;..........XlNXXXXXXXXlX............X;dxc,oX............
...........Nx':xlX................X;dOKXX0d'.............,d0XK0xl'.................,xx;;O...........
...........dXlk,......................XXXX.................XXX......................Xl0:xO..........
..........OX:0:.......................................................................d0x;X.........
..........lXdOX.......................................................................;XlXk.........
..........lXdOX.......................................................................;XlXk.........
.........XkX:0:...............XX....................................XX................dK,;K.........
..........NoXlk,...........XoOKK0xc'.....X'..............XXX....X;okKXKO:X..........Xl0:Xk..........
...........XoX:xcX.........o.XXXXXXNk:X,kNNO:..........Xl0NKoX'oK.XXXXXXX:.........'xk;,k...........
...........X.k;'lo;X.......l.XXXXXXXX.XXXXXXNxX.......,O.XXX.XNXXXXXXXXXK,.......'oxc'cK............
.............XNx;,:lc,.....Xd.XXXXXXXXXXXXXXX.O'.....cXXXXXXXXXXXXXXXXXX:.....Xcoo:,cO..............
...............XNOl;;:c:;X..XlXXXXXXXXXXXXXXXXX0,..XoNXXXXXXXXXXXXXXX.O,..X;c.lc;:dK................
..................XNkl:;;:::::oXXXXXXXXXXXXXXXXX0:'oNXXXXXXXXXXXXXXXNkc:cccc:..:d0..................
......................NKko:;;;:lxOKX.XXXXXXXXXXXXNN.XXXXXXXXXXX..X0Odc::::cdOX......................
...........................N0kdl:;;:;;:clodxxkkkkOOOOkkxxddolc::::;:cldOK...........................
..................................XKOkxdolcc:;;::;;:::;::cllodxk0KN.................................
....................................................................................................
"""))
626386545674738
calculate_grid_paths(Grid(r"""
.....................................................................................................
.................WXK0kxdd.oooooooooodxOXW............................................................
..........W0xdoooooooodxk.00KKKKK00OxdoolokXW........................................................
..........Wk',xKXNW....................WXkollOW......................................................
............0:c0W..........WNN.............NkccOW....................................................
.............Xd:dN.........Nkooodk0XW........Nk:lKW..................................................
..............W0lckN........WNKOxooooodk0NW....Xo:ckW................................................
................WOccxX.............WX0xdoooodOKNWkc;dN.WKOK..........................................
..................WOocoON................WXOxoododd:.ld:..cX.........................................
.....................XxlloxKN..................NOl:'....;dKW.........................................
.......................WXkoloooxO0XNNWWWNXK0k.oll;'...;ON............................................
...........................WN0kdooooooooooooo.kKO;...dN......WN0kdocc.WW.clodk0NW....................
....................NX0OkkkO0XWW...WWNNNW......Xc...xW...WXko;.....',.Nd.;'.....;lkKW................
...............WXOo.;;::ccc::::cokKW..........Wx...oN..Nkc...,cdOKXNW....WNXKOdl;...;o0N.............
.............Nkc,;c.OXW.....WX0xl:;:lkXW......X;.,d0.Xd'..;d0N..................WKkl'..,dKW..........
..........WKo,,lONW..............WXkl:;cxKW......oNXd'..cON.........................NOl...lKW........
.........Kl',xX......................W0dc::lkKo..cd;..c0W.............................WXd'..oX.......
.......Nd''xN...........................WXOoc:'.....'kW..................................Xo..,.W.....
......K:.lK.................................WNx'...;K.....................................W0;..xW....
....W0,.xN..................................Nx...o0X........................................X:..xW...
....0,.kW...................................WOcl.W...........................................X:.'O...
...K;.xW.........................................................................................cN..
..Nl.lN.......................................................................................Wl..k..
..k.,K.........................................................................................k..oW.
.Nc.oW............................................................................................cN.
.K,.O..........................................................................................K,.:X.
.k.;X..........................................................................................K,.:N.
.x.:N..........................................................................................0'.lW.
.d.cN..........................................................................................k.....
.d.cN.........................................................................................Wo..O..
.x.;X.........................................................................................K,.:X..
.O.'0........................................................................................Wx..dW..
.K,.x........................................................................................X;.,K...
.Wl.cN......................................................................................Wd..dW...
..O.'O......................................................................................0,.:X....
..Nc.lN....................................................................................Nl..O.....
...O'.k...................................................................................Wx..oW.....
...Wd.;K..................................................................................0,.:X......
....Xc.cN................................................................................X:.'0.......
.....K,.oN..............................................................................No..xW.......
......0,.dN............................................................................Wx..oW........
.......0,.oN..............................................................................cN.........
........0;.cX.........................................................................0,.;K..........
.........Kc.;0W......................................................................X;..0...........
..........Nd..dN....................................................................Xc...............
...........W0;.;OW.................................................................Xc..kW............
.............Nd'.c0W..............................................................K:..kW.............
...............Xo..:ON..........................................................Nk'.;0W..............
.................Xd'.,dKW.....................................................NO:..oX................
...................Xx:..:d0N........................WNXK0000KKXNW.........WXO....c0W.................
.....................WKx:'.,cdOKNW...........WNKOxol;'........'',;:cclllc.;......KW..................
........................WXOo:,..';cloodddoolc;'..',;ldk00K000Okxoolc:::::.ldOXW......................
.............................NKOxolc:;;,,;:ccodk0NWW.................................................
.....................................WWWWW...........................................................
.....................................................................................................
"""))
11468451846417028993973305727890751485
Can you verify that these last three answers are correct?
In this variant of chess, the pieces are set up in a random but restricted fashion. The pawns are in their regular positions, and the major white pieces are placed randomly on the first rank, with two restrictions: the bishops must be placed on opposite-color squares, and the king must be placed between the rooks. The black pieces are set up to mirror the white pieces. How many starting positions are there?
We can answer by generating all distinct permutations of the eight pieces and quantifying (counting) the number of permutations that are legal:
from statistics import median
def legal_position(pieces):
"Legal if bishops are on different colors, and king is between rooks."
B, R, K = map(pieces.index, 'BRK')
b, r = map(cat(pieces).rindex, 'BR')
return (B % 2 != b % 2) and R < K < r or r < K < R
quantify(set(permutations('RNBKQBNR')), legal_position)
960
[Note: initially I wrote pieces.rindex
instead of cat(pieces).rindex
, because I forgot that while tuples, lists and strings all have an index
method, only strings have rindex
. How annoying! In Ruby, both strings and arrays have index
and rindex
. In Java and Javascript, both strings and lists/arrays have both indexOf
and lastIndexOf
. What's wrong with Python?]
How many ways are there to select coins that add up to a specified amount of money? For example, to make 10 cents with an unlimited number of coins of denomination 10, 5, and 1 cents, there are four ways: {10, 5+5, 5+1+1+1+1+1, 1+1+1+1+1+1+1+1+1+1}.
For this problem there is no sense of advanccing from $k$ to $k$+1; instead we will need a recursive breakdown. Here are the cases:
@lru_cache(None)
def calculate_change(amount, denominations=(100, 50, 25, 10, 5, 1)):
return (1 if amount == 0 else
0 if amount < 0 or not denominations else
calculate_change(amount - denominations[0], denominations) +
calculate_change(amount, denominations[1:]))
For example:
calculate_change(11)
4
calculate_change(100)
293
calculate_change(9999)
139599978000
The above assumed that you had a limitless supply of each denomination. What if you don't?
I'll define calculate_limited_change
, where, instead of specifying denominations, you specify actual coins, repeating the ones you have more than one of. We use the same strategy, and if you skip a denomination, you can never use another coin of the same denomination.
@lru_cache(None)
def calculate_limited_change(amount, coins):
return (1 if amount == 0 else
0 if amount < 0 or not coins else
calculate_limited_change(amount - coins[0], coins[1:]) +
calculate_limited_change(amount, tuple(c for c in coins if c != coins[0])))
calculate_limited_change(10, (10, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
4
calculate_limited_change(10, (50, 25, 10, 10, 5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
4
calculate_limited_change(10, (10, 5, 1, 1, 1, 1))
1
calculate_limited_change(25, (25, 10, 10, 5, 5, 5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
9
calculate_limited_change(25, (25, 10, 5, 5, 5, 5, 1, 1, 1, 1))
2
The July 20, 2018 Riddler poses this problem (which has a Wikipedia article and a journal article):
I was recently traveling in Europe and struck by the number of coins the euro uses. They have 2 euro, 1 euro, 50 cent, 20 cent, 10 cent, 5 cent, 2 cent and 1 cent coins. This got me thinking: If Riddler Nation needed to make change (anywhere from 0.01 to 0.99) and was establishing its own denomination, what values of coins would be ideal to yield the smallest number of coins in any transaction? When picking values, let’s say we’re ditching the Europeans and limiting our denomination to four different coin denominations — replacing the current common American ones of penny, nickel, dime and quarter.
This is an optimization problem, not a counting problem, but it is related to the other coin/denomination problems here. Here's how I address the problem:
totalcoins(denominations)
will give the total number of coins (taken from those denominations) that arerequired to make each amount of change from 1 to 99 cents (assuming optimal change choices).
mincoins(amount, denominations)
computes this optimal number of coins for a given amount, or returns infinity if the amount cannot be made.def totalcoins(denominations) -> int:
"The total number of coins needed to make change for all amounts up to 99 cents."
return sum(mincoins(a, denominations) for a in range(1, 100))
@lru_cache(None)
def mincoins(amount, denominations) -> int:
"The minimum number of coins, taken from denominations, that add to amount."
return (0 if amount == 0 else
inf if not denominations or amount < min(denominations) else
min(mincoins(amount, denominations[1:]),
mincoins(amount - denominations[0], denominations) + 1))
candidates = ((L, M, S, 1) for S, M, L in combinations(range(2, 100), 3))
%time min(candidates, key=totalcoins)
CPU times: user 1min 40s, sys: 5.93 s, total: 1min 46s Wall time: 1min 55s
(25, 18, 5, 1)
Interesting! This is almost the US system of coins; we just need to trade in the dime for an 18 cent piece.