From 1f506f120261bd7f8839a11be209a05a402023fc Mon Sep 17 00:00:00 2001 From: Michael D Date: Mon, 5 Oct 2020 18:50:50 +0200 Subject: [PATCH 1/4] Project Euler problem 191 solution --- project_euler/problem_191/__init__.py | 0 project_euler/problem_191/sol1.py | 83 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 project_euler/problem_191/__init__.py create mode 100644 project_euler/problem_191/sol1.py diff --git a/project_euler/problem_191/__init__.py b/project_euler/problem_191/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/project_euler/problem_191/sol1.py b/project_euler/problem_191/sol1.py new file mode 100644 index 000000000000..c5dc2e5f628c --- /dev/null +++ b/project_euler/problem_191/sol1.py @@ -0,0 +1,83 @@ +""" +Prize Strings +Problem 191 + +A particular school offers cash rewards to children with good attendance and +punctuality. If they are absent for three consecutive days or late on more +than one occasion then they forfeit their prize. + +During an n-day period a trinary string is formed for each child consisting +of L's (late), O's (on time), and A's (absent). + +Although there are eighty-one trinary strings for a 4-day period that can be +formed, exactly forty-three strings would lead to a prize: + +OOOO OOOA OOOL OOAO OOAA OOAL OOLO OOLA OAOO OAOA +OAOL OAAO OAAL OALO OALA OLOO OLOA OLAO OLAA AOOO +AOOA AOOL AOAO AOAA AOAL AOLO AOLA AAOO AAOA AAOL +AALO AALA ALOO ALOA ALAO ALAA LOOO LOOA LOAO LOAA +LAOO LAOA LAAO + +How many "prize" strings exist over a 30-day period? +""" + + +def solution(days=30): + """Returns the number of possible prize strings for a particular number + of days, using a simple recursive function with caching to speed it up. + + >>> solution() + 1918080160 + >>> solution(4) + 43 + """ + + # a cache to speed up calculation + cache = {} + + # we will be using a simple recursive function + def calculate(days, absent, late): + # if we are absent twice, or late 3 consecutive days, + # no further prize strings are possible + if late == 3 or absent == 2: + return 0 + + # if we have no days left, and have not failed any other rules, + # we have a prize string + if days == 0: + return 1 + + # No easy solution, so now we need to do the recursive calculation + + # First, check if the combination is already in the cache, and + # if yes, return the stored value from there since we already + # know the number of possible prize strings from this point on + key = (days, absent, late) + if key in cache: + return cache[key] + + # if we are late (but not absent), the "absent" counter stays as + # it is, but the "late" counter increases by one + state_late = calculate(days - 1, absent, late + 1) + + # if we are absent, the "absent" counter increases by 1, and the + # "late" counter resets to 0 + state_absent = calculate(days - 1, absent + 1, 0) + + # if we are on time, this resets the "late" counter and keeps the + # absent counter + state_ontime = calculate(days - 1, absent, 0) + + # the total number of prize strings is the sum of these three + # possible states + prizestrings = state_late + state_absent + state_ontime + + # now store the value we calculated in the cache + cache[key] = prizestrings + return prizestrings + + return calculate(days, 0, 0) + + +if __name__ == "__main__": + print(solution()) From 26a66e0812a81450cc62ab9e1f892fd1a4ffe85c Mon Sep 17 00:00:00 2001 From: Michael D Date: Tue, 6 Oct 2020 21:48:50 +0200 Subject: [PATCH 2/4] Add type hints and reference links --- project_euler/problem_191/sol1.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/project_euler/problem_191/sol1.py b/project_euler/problem_191/sol1.py index c5dc2e5f628c..83282958d285 100644 --- a/project_euler/problem_191/sol1.py +++ b/project_euler/problem_191/sol1.py @@ -19,10 +19,14 @@ LAOO LAOA LAAO How many "prize" strings exist over a 30-day period? + +References: + - The original Project Euler project page: + https://projecteuler.net/problem=191 """ -def solution(days=30): +def solution(days: int = 30) -> int: """Returns the number of possible prize strings for a particular number of days, using a simple recursive function with caching to speed it up. @@ -36,7 +40,7 @@ def solution(days=30): cache = {} # we will be using a simple recursive function - def calculate(days, absent, late): + def calculate(days: int, absent: int, late: int) -> int: # if we are absent twice, or late 3 consecutive days, # no further prize strings are possible if late == 3 or absent == 2: From a44e1934804eabc2c83826932244c5cd7e42ec60 Mon Sep 17 00:00:00 2001 From: Michael D Date: Sat, 10 Oct 2020 19:33:13 +0200 Subject: [PATCH 3/4] Address requested changes - update documentation - split out helper function but mark it with an underscore - remove redundant comments or make them more explicit/helpful --- project_euler/problem_191/sol1.py | 106 ++++++++++++++++++------------ 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/project_euler/problem_191/sol1.py b/project_euler/problem_191/sol1.py index 83282958d285..cc7b9d47ae2e 100644 --- a/project_euler/problem_191/sol1.py +++ b/project_euler/problem_191/sol1.py @@ -26,61 +26,85 @@ """ -def solution(days: int = 30) -> int: - """Returns the number of possible prize strings for a particular number - of days, using a simple recursive function with caching to speed it up. +# a cache to speed up calculation +cache = {} - >>> solution() - 1918080160 - >>> solution(4) + +def _calculate(days: int, absent: int, late: int) -> int: + """ + A small helper function for the recursion, mainly to have + a clean interface for the solution() function below. + + It should get called with the number of days (corresponding + to the desired length of the 'prize strings'), and the + initial values for the number of consecutive absent days and + number of total late days. + + >>> _calculate(days=4, absent=0, late=0) 43 + >>> _calculate(days=30, absent=2, late=0) + 0 + >>> _calculate(days=30, absent=1, late=0) + 98950096 """ - # a cache to speed up calculation - cache = {} + # if we are absent twice, or late 3 consecutive days, + # no further prize strings are possible + if late == 3 or absent == 2: + return 0 + + # if we have no days left, and have not failed any other rules, + # we have a prize string + if days == 0: + return 1 - # we will be using a simple recursive function - def calculate(days: int, absent: int, late: int) -> int: - # if we are absent twice, or late 3 consecutive days, - # no further prize strings are possible - if late == 3 or absent == 2: - return 0 + # No easy solution, so now we need to do the recursive calculation - # if we have no days left, and have not failed any other rules, - # we have a prize string - if days == 0: - return 1 + # First, check if the combination is already in the cache, and + # if yes, return the stored value from there since we already + # know the number of possible prize strings from this point on + key = (days, absent, late) + if key in cache: + return cache[key] - # No easy solution, so now we need to do the recursive calculation + # now we calculate the three possible ways that can unfold from + # this point on, depending on our attendance today - # First, check if the combination is already in the cache, and - # if yes, return the stored value from there since we already - # know the number of possible prize strings from this point on - key = (days, absent, late) - if key in cache: - return cache[key] + # 1) if we are late (but not absent), the "absent" counter stays as + # it is, but the "late" counter increases by one + state_late = _calculate(days - 1, absent, late + 1) - # if we are late (but not absent), the "absent" counter stays as - # it is, but the "late" counter increases by one - state_late = calculate(days - 1, absent, late + 1) + # 2) if we are absent, the "absent" counter increases by 1, and the + # "late" counter resets to 0 + state_absent = _calculate(days - 1, absent + 1, 0) - # if we are absent, the "absent" counter increases by 1, and the - # "late" counter resets to 0 - state_absent = calculate(days - 1, absent + 1, 0) + # 3) if we are on time, this resets the "late" counter and keeps the + # absent counter + state_ontime = _calculate(days - 1, absent, 0) - # if we are on time, this resets the "late" counter and keeps the - # absent counter - state_ontime = calculate(days - 1, absent, 0) + prizestrings = state_late + state_absent + state_ontime - # the total number of prize strings is the sum of these three - # possible states - prizestrings = state_late + state_absent + state_ontime + # keep the value we calculated in the cache, so that later we do not + # need to recurse again. This is the main improvement which brings + # runtime down from seconds/minutes to usually below 1 second. + cache[key] = prizestrings + return prizestrings - # now store the value we calculated in the cache - cache[key] = prizestrings - return prizestrings - return calculate(days, 0, 0) +def solution(days: int = 30) -> int: + """ + Returns the number of possible prize strings for a particular number + of days, using a simple recursive function with caching to speed it up. + + >>> solution() + 1918080160 + >>> solution(4) + 43 + """ + + # we call the calculate function above, with initially + # no absent days and no late days. + return _calculate(days, absent=0, late=0) if __name__ == "__main__": From 210786412ec92974bc75e09b8a442203ca6323de Mon Sep 17 00:00:00 2001 From: Michael D Date: Sun, 11 Oct 2020 10:45:30 +0200 Subject: [PATCH 4/4] Address requested changes --- project_euler/problem_191/sol1.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/project_euler/problem_191/sol1.py b/project_euler/problem_191/sol1.py index cc7b9d47ae2e..38325b363b89 100644 --- a/project_euler/problem_191/sol1.py +++ b/project_euler/problem_191/sol1.py @@ -26,7 +26,6 @@ """ -# a cache to speed up calculation cache = {} @@ -84,9 +83,6 @@ def _calculate(days: int, absent: int, late: int) -> int: prizestrings = state_late + state_absent + state_ontime - # keep the value we calculated in the cache, so that later we do not - # need to recurse again. This is the main improvement which brings - # runtime down from seconds/minutes to usually below 1 second. cache[key] = prizestrings return prizestrings @@ -102,8 +98,6 @@ def solution(days: int = 30) -> int: 43 """ - # we call the calculate function above, with initially - # no absent days and no late days. return _calculate(days, absent=0, late=0)