From 0216807bec87546055061031b3ebb14870072800 Mon Sep 17 00:00:00 2001 From: Dominic Ricottone Date: Thu, 12 Jan 2023 14:11:48 -0600 Subject: [PATCH] Solution to classic riddler --- .gitignore | 2 ++ Makefile | 10 ++++++ classic/README.md | 65 ++++++++++++++++++++++++++++++++++ classic/__init__.py | 0 classic/__main__.py | 33 ++++++++++++++++++ classic/main.py | 85 +++++++++++++++++++++++++++++++++++++++++++++ classic/test.py | 70 +++++++++++++++++++++++++++++++++++++ 7 files changed, 265 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 classic/README.md create mode 100644 classic/__init__.py create mode 100644 classic/__main__.py create mode 100755 classic/main.py create mode 100755 classic/test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e91862f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**/__pycache__/ + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f59852d --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +express: + ./main.py + +classic: + python -m classic + +clean: + rm -rf classic/__pycache__/ + +.PHONY: clean express classic diff --git a/classic/README.md b/classic/README.md new file mode 100644 index 0000000..86b309c --- /dev/null +++ b/classic/README.md @@ -0,0 +1,65 @@ +# Class Riddler + +## The problem + +``` +$ python -m classic help +The astronomers of Planet Xiddler are back in action! Unfortunately, +this time they have used their telescopes to spot an armada of +hostile alien warships on a direct course for Xiddler. The armada +will be arriving in exactly 100 days. (Recall that, like Earth, +there are 24 hours in a Xiddler day.) + +Fortunately, Xiddler’s engineers have just completed construction of +the planet’s first assembler, which is capable of producing any +object. An assembler can be used to build a space fighter to defend +the planet, which takes one hour to produce. An assembler can also +be used to build another assembler (which, in turn, can build other +space fighters or assemblers). However, building an assembler is +more time-consuming, requiring six whole days. Also, you cannot use +multiple assemblers to build one space fighter or assembler in a +shorter period of time. + +What is the greatest number of space fighters the Xiddlerian fleet +can have when the alien armada arrives? +``` + +## My assumption + +It is always preferable to front-load assembler production. +While an assembler could be produced at any point in time, it is always +preferable to gain the multiplicative effect earlier. +If an assembler is set to produce fighters, it should never produce +assemblers again, because it would have been more efficient to produce that +assembler earlier. +Furthermore, if an assembler to set to produce fighters, in the next cycle +all assemblers will be set to produce fighters. +Essentially, the problem is *when to switch production from assemblers to +fighters*. +This also reduces the complexity of the problem. Instead of *hour-by-hour* +choices, the strategies will be formed by *blocks of 6 days* (the time +required to produce an assembler). +As an example, there are two strategies to produce 4 assemblers. + 1. One assembler spends (18 * 24 =) 432 hours producing assemblers. + The second and third assemblers will, upon their independent creation at + the 144 and 288 hours marks, begin producing fighters. + That is 288 and 144 fighters respectively, for a sum of 432. + The yield is 4 assemblers and 432 fighters in 432 hours. + 2. The first assembler spends (12 * 24 =) 288 hours producing assemblers + and, upon creation at the 144 hour mark, the second assembler spends 144 + hours producing assemblers. + The yield is 4 assemblers and 0 fighters in 288 hours. +However, strategy #2 is trivially superior to #1. In the unconsumed 144 hours +4 assemblers will produce (4 * 6 * 24 =) 576 fighters. + +## My solution + +``` +$ python -m classic +Built 7864320 fighters with 32768 assemblers + * 0 fighters were built concurrent to the assemblers + * 7864320 fighters were built in the unconsumed 240 hours +``` + +See also `python -m classic test` and `python -m classic dump`. + diff --git a/classic/__init__.py b/classic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/classic/__main__.py b/classic/__main__.py new file mode 100644 index 0000000..ca12394 --- /dev/null +++ b/classic/__main__.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import sys + +import classic.main as main +import classic.test as test + +if len(sys.argv) > 1 and sys.argv[1] == "test": + test.main() +elif len(sys.argv) > 1 and sys.argv[1] == "help": + print( + "The astronomers of Planet Xiddler are back in action! Unfortunately,\n" + "this time they have used their telescopes to spot an armada of \n" + "hostile alien warships on a direct course for Xiddler. The armada\n" + "will be arriving in exactly 100 days. (Recall that, like Earth,\n" + "there are 24 hours in a Xiddler day.)\n\n" + "Fortunately, Xiddler’s engineers have just completed construction of\n" + "the planet’s first assembler, which is capable of producing any\n" + "object. An assembler can be used to build a space fighter to defend\n" + "the planet, which takes one hour to produce. An assembler can also\n" + "be used to build another assembler (which, in turn, can build other\n" + "space fighters or assemblers). However, building an assembler is\n" + "more time-consuming, requiring six whole days. Also, you cannot use\n" + "multiple assemblers to build one space fighter or assembler in a\n" + "shorter period of time.\n\n" + "What is the greatest number of space fighters the Xiddlerian fleet\n" + "can have when the alien armada arrives?" + ) +elif len(sys.argv) > 1 and sys.argv[1] == "dump": + main.dump() +else: + main.main() + diff --git a/classic/main.py b/classic/main.py new file mode 100755 index 0000000..3b12546 --- /dev/null +++ b/classic/main.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +from typing import Generator, NamedTuple + +HOURS_TO_PRODUCE_ASSEMBLER = 6 * 24 # 6 days * 24 hours/day = 144 hours + +# equivalent constant, but this name is more legible in certain contexts +FIGHTERS_PRODUCED_IN_ONE_UNIT = HOURS_TO_PRODUCE_ASSEMBLER + +class Strategy(NamedTuple): + available_hours: int + produced_fighters: int + +class MaxStrategy(NamedTuple): + available_hours: int + produced_fighters: int + produced_assemblers: int + +def assembler_schedule(available_hours) -> dict[int, Strategy]: + """A schedule of quantities of assemblers that are possible within + `available_hours`. + + Index the schedule by a target quantity of assemblers. This provides the + optimal strategy to produce that quantity. A strategy is represented by a + tuple of available hours and number of fighters produced incidentally. + + The first assembler is given; there is no strategy for 0 assemblers. + + If there is no strategy to produce a quantity given `available_hours`, the + schedule will not be indexable by that quantity. + """ + # initialize the 'do nothing' strategy + schedule = { 1: Strategy(available_hours, 0) } + + while available_hours >= HOURS_TO_PRODUCE_ASSEMBLER: + # consume 6 days + available_hours -= HOURS_TO_PRODUCE_ASSEMBLER + + # find the maximal number of assemblers at this hour + assemblers = max(schedule.keys()) + + # with N assemblers, for n in 1..N, n will produce assemblers + for i in range(1, assemblers + 1): + produced_assemblers = assemblers + i + produced_fighters = (assemblers - i) * HOURS_TO_PRODUCE_ASSEMBLER + schedule[produced_assemblers] = Strategy(available_hours, produced_fighters) + + return schedule + +def evaluate_fighters(strategy: Strategy, assemblers: int) -> int: + """Helper function to calculate final production of fighters.""" + return strategy.produced_fighters + (assemblers * strategy.available_hours) + +def evaluate_schedule(schedule: dict[int, Strategy]) -> Generator[tuple[int, int, Strategy], None, None]: + """For each strategy, determine how many fighters would be produced by + setting all assemblers to produce fighters for the availabe hours. + """ + for assemblers in schedule.keys(): + fighters = evaluate_fighters(schedule[assemblers], assemblers) + yield (fighters, assemblers, schedule[assemblers]) + +def max_schedule(schedule: dict[int, Strategy]) -> MaxStrategy: + """Find the maximal strategy within a schedule. + + A tuple of hours remaining, fighters proeduced, and assemblers produced. + """ + # initialize with the 'do nothing' strategy + return max(tuple(evaluate_schedule(schedule)), key=lambda x: x[0]) + +def dump(): + schedule = assembler_schedule(100 * 24) + print("FinalFighters,ProducedAssemblers,ProducedFighters,UnconsumedHours") + for fighters, assemblers, strategy in evaluate_schedule(schedule): + print(f"{fighters}\t{assemblers}\t{strategy.produced_fighters}\t{strategy.available_hours}") + +def main(): + schedule = assembler_schedule(100 * 24) + maximum = max_schedule(schedule) + print(f"Built {maximum[0]} fighters with {maximum[1]} assemblers") + print(f" * {maximum[2].produced_fighters} fighters were built concurrent to the assemblers") + print(f" * {maximum[0] - maximum[2].produced_fighters} fighters were built in the unconsumed {maximum[2].available_hours} hours") + +if __name__ == "__main__": + main() + diff --git a/classic/test.py b/classic/test.py new file mode 100755 index 0000000..ea1f427 --- /dev/null +++ b/classic/test.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +from .main import * + +def _test_schedule(generated_schedule, literal_schedule): + """Helper function for `test_schedules`.""" + diff = set(generated_schedule.keys()) - set(literal_schedule.keys()) + try: + assert len(diff) == 0 + except: + print(f"Generated schedule contains an impossible quantity of assemblers: {diff}") + + for n in sorted(literal_schedule.keys()): + try: + assert generated_schedule[n][0] == literal_schedule[n][0] + assert generated_schedule[n][1] == literal_schedule[n][1] + except: + print(f"Generated schedule is wrong for {n} assemblers") + print(f"Is: {generated_schedule[n]}") + print(f"Should be: {literal_schedule[n]}") + +def test_schedules(): + # check that only one strategy exists for schedules with too few hours + for i in range(HOURS_TO_PRODUCE_ASSEMBLER): + res = assembler_schedule(i) + _test_schedule(res, { 1: (i, 0) }) + + # check that complex strategies can be formed + res = assembler_schedule(HOURS_TO_PRODUCE_ASSEMBLER) + _test_schedule(res, { + 1: (HOURS_TO_PRODUCE_ASSEMBLER, 0), + 2: (0, 0), + }) + + # check handling of remaining hours without fighter production potential + res = assembler_schedule(HOURS_TO_PRODUCE_ASSEMBLER + 1) + _test_schedule(res, { + 1: (HOURS_TO_PRODUCE_ASSEMBLER + 1, 0), + 2: (1, 0), + }) + + # check handling of remaining hours with fighter production potential + res = assembler_schedule(HOURS_TO_PRODUCE_ASSEMBLER * 2) + _test_schedule(res, { + 1: (HOURS_TO_PRODUCE_ASSEMBLER * 2, 0), + 2: (HOURS_TO_PRODUCE_ASSEMBLER, 0), + 3: (0, FIGHTERS_PRODUCED_IN_ONE_UNIT), + 4: (0, 0), + }) + + # check that non-trivial ranges are bounded correctly + res = assembler_schedule(HOURS_TO_PRODUCE_ASSEMBLER * 3) + _test_schedule(res, { + 1: (HOURS_TO_PRODUCE_ASSEMBLER * 3, 0), + 2: (HOURS_TO_PRODUCE_ASSEMBLER * 2, 0), + 3: (HOURS_TO_PRODUCE_ASSEMBLER, FIGHTERS_PRODUCED_IN_ONE_UNIT), + 4: (HOURS_TO_PRODUCE_ASSEMBLER, 0), + 5: (0, FIGHTERS_PRODUCED_IN_ONE_UNIT*3), + 6: (0, FIGHTERS_PRODUCED_IN_ONE_UNIT*2), + 7: (0, FIGHTERS_PRODUCED_IN_ONE_UNIT), + 8: (0, 0), + }) + +def main(): + print("Running test suite...") + test_schedules() + +if __name__ == "__main__": + main() + -- 2.45.2