A .gitignore => .gitignore +2 -0
@@ 0,0 1,2 @@
+**/__pycache__/
+
A Makefile => Makefile +10 -0
@@ 0,0 1,10 @@
+express:
+ ./main.py
+
+classic:
+ python -m classic
+
+clean:
+ rm -rf classic/__pycache__/
+
+.PHONY: clean express classic
A classic/README.md => classic/README.md +65 -0
@@ 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`.
+
A classic/__init__.py => classic/__init__.py +0 -0
A classic/__main__.py => classic/__main__.py +33 -0
@@ 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()
+
A classic/main.py => classic/main.py +85 -0
@@ 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()
+
A classic/test.py => classic/test.py +70 -0
@@ 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()
+