all_sums.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import itertools, math
  2. from sys import argv
  3. from functools import cache
  4. # Goal
  5. # ============================================
  6. #
  7. # Given an integer `n` and a step size `size`,
  8. # find all combinations
  9. # of integers greater than or equal to `size`
  10. # that sum to `n`, ignoring order.
  11. #
  12. # E.g. If n = 4 and step = 1, return
  13. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1)}
  14. #
  15. # Method
  16. # ============================================
  17. #
  18. # Take n and find the set of pairs summing to it.
  19. # For example, if n = 4 and step = 1,
  20. # the pairs summing to 4 are:
  21. #
  22. # {(3, 1), (2, 2)}
  23. #
  24. # Then, go through each number in each tuple in that set...
  25. #
  26. # {(3, 1), (2, 2)}
  27. # ^
  28. # Start
  29. #
  30. # If the selected number is greater than 2 × step,
  31. # get all the tuples that sum to it
  32. # by using the method described here.
  33. #
  34. # Note: Recursion can feel like cheating,
  35. # {(3, 1), (2, 2)} but notice that finding everything that sums to 3
  36. # ^ is easier than finding everything that sums to 4.
  37. # / \ Finding everything that sums to 2 or 1 is trivial.
  38. # (2,1) (1,1,1) <-- The problem gets easier on each nested reccurance
  39. # until it no longer requires going down further.
  40. #
  41. # Then, for each resulting tuple,
  42. # Find the tuple that results from
  43. # replacing the originally selected number
  44. # with the tuple that sums to it.
  45. #
  46. #
  47. # {(3, 1), (2, 2)}
  48. # ^
  49. # / \
  50. # (2,1) (1,1,1)
  51. # | |
  52. # | (1,1,1) replaces 3 in (3, 1) = (1, 1, 1, 1)
  53. # |
  54. # (2,1) replaces 3 with (3, 1) = (2, 1, 1)
  55. # /
  56. # /
  57. # Then, take the resulting tuples /
  58. # and add them to the original set /
  59. # /
  60. # +++++++++++++++++++++++
  61. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1)}
  62. # ^ +++++++++++++++++++++++
  63. #
  64. # Then move to the next number and continue the process.
  65. #
  66. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1)}
  67. # ^
  68. #
  69. # If the select number is less than 2 × step, just move on...
  70. #
  71. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1)}
  72. # ^
  73. #
  74. # If the selected number is _equal to_ 2 × step,
  75. # Get the tuple that results from replacing it
  76. # with a tuple containing `n` elements of value `step`.
  77. #
  78. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1)}
  79. # ^
  80. # (1,1)
  81. # | Then replace the selected number
  82. # | with the resulting tuple
  83. # (1,1,2)
  84. #
  85. # Then add it to the result set:
  86. # +++++++++
  87. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1), (1, 1, 2)}
  88. # ^ +++++++++
  89. #
  90. # Except actually, don't do that
  91. # if it's the same as a number already in the set,
  92. # only in a different order.
  93. #
  94. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1), (1, 1, 2)}
  95. # ^ ********* *********
  96. #
  97. # Instead just move on.
  98. #
  99. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1)}
  100. # ^
  101. # This is the same as before.
  102. #
  103. #
  104. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1)}
  105. # ^ **********
  106. # This results in (1, 1, 1, 1), which we already have.
  107. #
  108. #
  109. # {(3, 1), (2, 2), (2, 1, 1), (1, 1, 1, 1)}
  110. # ^ ^ ^ ^ ^ ^
  111. # And we can skip all of the 1s, so we're done.
  112. def main():
  113. n = int(argv[1]) if len(argv) > 1 else 10
  114. step = int(argv[2]) if len(argv) > 2 else 1
  115. for e in sorted(list(all_tuples_summing_to(n, step))):
  116. print(e)
  117. @cache
  118. def all_tuples_summing_to(n: int, step: int) -> set[tuple[int]]:
  119. """
  120. >>> all_tuples_summing_to(5, 1)
  121. {(1, 1, 1, 1, 1), (1, 1, 1, 2), (1, 1, 3), (1, 2, 2), (1, 4), (2, 3)}
  122. """
  123. # e.g. n = 5, step = 1
  124. sum_tuples = all_pairs_summing_to(n, step) # e.g. {(1, 4), (2, 3)}
  125. for sum_tuple in sum_tuples: # e.g. (1, 4)
  126. for e in sum_tuple: # e.g. 4
  127. if e >= 2*step:
  128. # Take the sum tuple and replace e
  129. # with each tuple summing to e.
  130. sum_tuple_dropping_e = list(sum_tuple)
  131. del sum_tuple_dropping_e[sum_tuple_dropping_e.index(e)]
  132. sum_tuple_dropping_e = tuple(sum_tuple_dropping_e)
  133. # e.g. (1, 4) dropping 4 = (1)
  134. all_tuples_summing_to_e = (
  135. all_tuples_summing_to(e, step) if e > 2*step else
  136. # If e is less than 2 * step,
  137. # then only remaining combination is the one containing only `step`s.
  138. # For example, if step = 1 and e = 2,
  139. # then the only remaining combo is (1, 1).
  140. # This is the base case preventing infinite recursion.
  141. {tuple([step] * e)}
  142. )
  143. # e.g. {(3, 1), (2, 2), (2, 1, 1)}
  144. for tuple_summing_to_e in all_tuples_summing_to_e: # e.g. (3, 1)
  145. sum_tuples = sum_tuples.union({ tuple(sorted(
  146. sum_tuple_dropping_e + tuple_summing_to_e
  147. # e.g. (1) + (3, 1)
  148. # ... (1) + (2, 2)
  149. # ... (1) + (2, 1, 1)
  150. )) })
  151. return sum_tuples
  152. @cache
  153. def all_pairs_summing_to(n: int, step: int) -> set[tuple[int]]:
  154. """
  155. >>> all_pairs_summing_to(5, 1)
  156. {(1, 4), (2, 3)}
  157. """
  158. return set([
  159. tuple(sorted((i*step, n - (i*step))))
  160. for i in range(1, math.floor(n/step))
  161. ])
  162. main()