j-js commited on
Commit
a28bd44
·
verified ·
1 Parent(s): 9526549

Update solver_factorial.py

Browse files
Files changed (1) hide show
  1. solver_factorial.py +416 -40
solver_factorial.py CHANGED
@@ -2,69 +2,445 @@ from __future__ import annotations
2
 
3
  import math
4
  import re
5
- from typing import Optional
6
 
7
  from models import SolverResult
8
 
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  def solve_factorial(text: str) -> Optional[SolverResult]:
11
  raw = text or ""
12
- lower = raw.lower().strip()
13
 
14
- if not raw:
15
  return None
16
 
17
- has_factorial_notation = re.search(r"(\d+)\s*!", raw) is not None
18
- has_factorial_word = "factorial" in lower
19
- has_trailing_zero_phrase = "trailing zero" in lower or "trailing zeros" in lower
20
-
21
- if not (has_factorial_notation or has_factorial_word or has_trailing_zero_phrase):
22
  return None
23
 
24
- # trailing zeros in n!
25
- trailing_match = re.search(
 
 
 
 
 
 
26
  r"trailing zeros?.*?(?:in|of)?\s*(\d+)\s*!",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  lower
28
  )
29
- if trailing_match:
30
- n = int(trailing_match.group(1))
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- count = 0
33
- power = 5
34
- while power <= n:
35
- count += n // power
36
- power *= 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- return SolverResult(
39
- domain="quant",
40
- solved=True,
41
- topic="factorial_trailing_zeros",
42
- answer_value=None,
43
- internal_answer=str(count),
44
- steps=[
45
- "Trailing zeros come from factors of 10.",
46
- "Each factor of 10 is made from one 2 and one 5.",
47
- "In a factorial, there are usually more 2s than 5s, so count the number of factors of 5.",
48
- "Count multiples of 5, then add extra 5s from numbers like 25, 125, and so on.",
49
- ],
50
- )
51
 
52
- # direct factorial value
53
- factorial_match = re.search(r"(\d+)\s*!", raw)
54
- if factorial_match:
55
- n = int(factorial_match.group(1))
56
- result = math.factorial(n)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
 
 
 
 
 
 
58
  return SolverResult(
59
  domain="quant",
60
- solved=True,
61
  topic="factorial",
62
  answer_value=None,
63
- internal_answer=str(result),
64
  steps=[
65
- "A factorial means multiply descending whole numbers from that number down to 1.",
66
- "Write the expression as repeated multiplication.",
67
- "Then simplify carefully step by step.",
 
68
  ],
69
  )
70
 
 
2
 
3
  import math
4
  import re
5
+ from typing import Optional, List
6
 
7
  from models import SolverResult
8
 
9
 
10
+ # ----------------------------
11
+ # Helpers
12
+ # ----------------------------
13
+
14
+ def _clean(text: str) -> str:
15
+ return " ".join((text or "").strip().split())
16
+
17
+
18
+ def _has_factorial_signal(raw: str, lower: str) -> bool:
19
+ return any(
20
+ [
21
+ re.search(r"\b\d+\s*!", raw) is not None,
22
+ "factorial" in lower,
23
+ "trailing zero" in lower,
24
+ "trailing zeros" in lower,
25
+ "zeros at the end" in lower,
26
+ "power of" in lower and "!" in raw,
27
+ "highest power" in lower and "!" in raw,
28
+ "divide" in lower and "!" in raw,
29
+ ]
30
+ )
31
+
32
+
33
+ def _is_prime(n: int) -> bool:
34
+ if n < 2:
35
+ return False
36
+ if n in (2, 3):
37
+ return True
38
+ if n % 2 == 0 or n % 3 == 0:
39
+ return False
40
+ f = 5
41
+ while f * f <= n:
42
+ if n % f == 0 or n % (f + 2) == 0:
43
+ return False
44
+ f += 6
45
+ return True
46
+
47
+
48
+ def _valuation_in_factorial(n: int, p: int) -> int:
49
+ """
50
+ Exponent of prime p in n! using Legendre's formula:
51
+ floor(n/p) + floor(n/p^2) + ...
52
+ """
53
+ total = 0
54
+ power = p
55
+ while power <= n:
56
+ total += n // power
57
+ power *= p
58
+ return total
59
+
60
+
61
+ def _factorial_steps() -> List[str]:
62
+ return [
63
+ "A factorial means multiply consecutive positive integers down to 1.",
64
+ "Rewrite the factorial in expanded form if needed.",
65
+ "Then simplify carefully, often by canceling common factors first.",
66
+ ]
67
+
68
+
69
+ def _trailing_zero_steps() -> List[str]:
70
+ return [
71
+ "Trailing zeros come from factors of 10.",
72
+ "Each factor of 10 is made from one 2 and one 5.",
73
+ "In a factorial, there are usually more 2s than 5s, so the limiting factor is the number of 5s.",
74
+ "Count factors of 5 using multiples of 5, then add extra 5s from 25, 125, and so on.",
75
+ ]
76
+
77
+
78
+ def _prime_power_steps(p: int) -> List[str]:
79
+ return [
80
+ f"To find the power of {p} in n!, count how many times {p} appears in the factorial product.",
81
+ f"Use repeated floor division: floor(n/{p}) + floor(n/{p**2}) + floor(n/{p**3}) + ...",
82
+ "Stop when the next power of the prime is larger than n.",
83
+ "That total gives the exponent of the prime in the factorial.",
84
+ ]
85
+
86
+
87
+ def _ratio_steps() -> List[str]:
88
+ return [
89
+ "Expand only the larger factorial until cancellation is possible.",
90
+ "Do not expand both factorials fully unless the numbers are very small.",
91
+ "Cancel the common descending product.",
92
+ "Then simplify the remaining factors.",
93
+ ]
94
+
95
+
96
+ def _equation_steps() -> List[str]:
97
+ return [
98
+ "Use factorial growth: 1!, 2!, 3!, 4!, ... gets large quickly.",
99
+ "Compare nearby factorial values rather than expanding everything at once.",
100
+ "Match the factorial expression to the target value or simplified form.",
101
+ ]
102
+
103
+
104
+ def _normalize_factorial_expr(expr: str) -> str:
105
+ expr = expr.replace("^", "**")
106
+ expr = expr.replace("×", "*")
107
+ expr = expr.replace("÷", "/")
108
+ expr = expr.replace("–", "-")
109
+ expr = expr.replace("—", "-")
110
+ expr = expr.strip()
111
+ return expr
112
+
113
+
114
+ def _factorial_to_python(expr: str) -> str:
115
+ """
116
+ Convert occurrences like 7! into math.factorial(7).
117
+ Only supports numeric factorial arguments.
118
+ """
119
+ expr = _normalize_factorial_expr(expr)
120
+
121
+ def repl(match: re.Match) -> str:
122
+ num = match.group(1)
123
+ return f"math.factorial({num})"
124
+
125
+ return re.sub(r"(\d+)\s*!", repl, expr)
126
+
127
+
128
+ def _safe_eval_numeric_factorial_expr(expr: str) -> Optional[int]:
129
+ """
130
+ Evaluate a numeric factorial expression like:
131
+ 8!/5!
132
+ (7!*3!)/(6!)
133
+ Only for controlled numeric inputs after conversion.
134
+ """
135
+ try:
136
+ py_expr = _factorial_to_python(expr)
137
+ if not re.fullmatch(r"[0-9\(\)\s\+\-\*/,.a-zA-Z_]+", py_expr):
138
+ return None
139
+ value = eval(py_expr, {"__builtins__": {}}, {"math": math})
140
+ if isinstance(value, (int, float)) and abs(value - round(value)) < 1e-9:
141
+ return int(round(value))
142
+ return None
143
+ except Exception:
144
+ return None
145
+
146
+
147
+ def _extract_smallest_positive_solution_for_factorial_value(target: int) -> Optional[int]:
148
+ """
149
+ Solve n! = target for integer n if possible.
150
+ """
151
+ if target < 1:
152
+ return None
153
+ n = 1
154
+ fact = 1
155
+ while fact < target and n <= 50:
156
+ n += 1
157
+ fact *= n
158
+ if fact == target:
159
+ return n
160
+ return None
161
+
162
+
163
+ def _extract_numbers_from_text(lower: str) -> List[int]:
164
+ return [int(x) for x in re.findall(r"\b\d+\b", lower)]
165
+
166
+
167
+ # ----------------------------
168
+ # Main solver
169
+ # ----------------------------
170
+
171
  def solve_factorial(text: str) -> Optional[SolverResult]:
172
  raw = text or ""
173
+ lower = _clean(raw).lower()
174
 
175
+ if not raw.strip():
176
  return None
177
 
178
+ if not _has_factorial_signal(raw, lower):
 
 
 
 
179
  return None
180
 
181
+ # --------------------------------------------------
182
+ # 1) Trailing zeros in n!
183
+ # Covers:
184
+ # - trailing zeros in/of 32!
185
+ # - zeros at the end of 100!
186
+ # - how many zeros are at the end of 50 factorial
187
+ # --------------------------------------------------
188
+ trailing_patterns = [
189
  r"trailing zeros?.*?(?:in|of)?\s*(\d+)\s*!",
190
+ r"zeros? at the end.*?(?:in|of)?\s*(\d+)\s*!",
191
+ r"how many zeros?.*?(?:end|trailing).*?(\d+)\s*!",
192
+ r"(\d+)\s*!\s*.*?trailing zeros?",
193
+ r"(\d+)\s*factorial.*?trailing zeros?",
194
+ r"zeros? at the end of\s*(\d+)\s*factorial",
195
+ ]
196
+ for pattern in trailing_patterns:
197
+ m = re.search(pattern, lower)
198
+ if m:
199
+ n = int(m.group(1))
200
+ if n < 0:
201
+ return None
202
+
203
+ count = _valuation_in_factorial(n, 5)
204
+
205
+ return SolverResult(
206
+ domain="quant",
207
+ solved=True,
208
+ topic="factorial_trailing_zeros",
209
+ answer_value=None,
210
+ internal_answer=str(count),
211
+ steps=_trailing_zero_steps(),
212
+ )
213
+
214
+ # --------------------------------------------------
215
+ # 2) Power of a prime p in n!
216
+ # Covers:
217
+ # - power of 2 in 25!
218
+ # - exponent of 3 in 100!
219
+ # - highest power of 5 dividing 80!
220
+ # - how many times does 7 divide 50!
221
+ # --------------------------------------------------
222
+ prime_power_patterns = [
223
+ r"power of\s+(\d+)\s+in\s+(\d+)\s*!",
224
+ r"exponent of\s+(\d+)\s+in\s+(\d+)\s*!",
225
+ r"highest power of\s+(\d+)\s+(?:that\s+)?divides\s+(\d+)\s*!",
226
+ r"how many times does\s+(\d+)\s+divide\s+(\d+)\s*!",
227
+ r"factor of\s+(\d+)\s+in\s+(\d+)\s*!",
228
+ ]
229
+ for pattern in prime_power_patterns:
230
+ m = re.search(pattern, lower)
231
+ if m:
232
+ p = int(m.group(1))
233
+ n = int(m.group(2))
234
+
235
+ if n < 0 or not _is_prime(p):
236
+ return None
237
+
238
+ count = _valuation_in_factorial(n, p)
239
+
240
+ return SolverResult(
241
+ domain="quant",
242
+ solved=True,
243
+ topic="factorial_prime_power",
244
+ answer_value=None,
245
+ internal_answer=str(count),
246
+ steps=_prime_power_steps(p),
247
+ )
248
+
249
+ # --------------------------------------------------
250
+ # 3) Numeric factorial ratio / product evaluation
251
+ # Covers:
252
+ # - 8!/5!
253
+ # - (9!)/(7!)
254
+ # - 6! / (3! 2!) [if user types with *]
255
+ # - (10! * 3!) / 8!
256
+ #
257
+ # We only trigger if the expression contains at least
258
+ # two factorials or one factorial plus arithmetic signs.
259
+ # --------------------------------------------------
260
+ numeric_factorials = re.findall(r"\d+\s*!", raw)
261
+ if numeric_factorials:
262
+ factorial_count = len(numeric_factorials)
263
+ has_arithmetic = any(sym in raw for sym in ["/", "*", "+", "-", "(", ")"])
264
+
265
+ if factorial_count >= 2 or (factorial_count >= 1 and has_arithmetic):
266
+ expr_candidate = raw.strip()
267
+
268
+ # Try to isolate a math-looking substring if wrapped in words
269
+ expr_match = re.search(r"([\d\s!\(\)\+\-\*/×÷^]+)", raw)
270
+ if expr_match:
271
+ maybe_expr = expr_match.group(1).strip()
272
+ if "!" in maybe_expr:
273
+ expr_candidate = maybe_expr
274
+
275
+ value = _safe_eval_numeric_factorial_expr(expr_candidate)
276
+ if value is not None:
277
+ return SolverResult(
278
+ domain="quant",
279
+ solved=True,
280
+ topic="factorial_expression",
281
+ answer_value=None,
282
+ internal_answer=str(value),
283
+ steps=_ratio_steps(),
284
+ )
285
+
286
+ # --------------------------------------------------
287
+ # 4) Factorial equations: n! = k
288
+ # Covers:
289
+ # - n! = 120
290
+ # - which value of n satisfies n! = 720
291
+ # --------------------------------------------------
292
+ eq_match = re.search(
293
+ r"(?:n|x)\s*!\s*=\s*(\d+)|(?:which value of|find)\s+(?:n|x).*?(?:n|x)\s*!\s*=\s*(\d+)",
294
  lower
295
  )
296
+ if eq_match:
297
+ target = eq_match.group(1) or eq_match.group(2)
298
+ if target is not None:
299
+ target_int = int(target)
300
+ sol = _extract_smallest_positive_solution_for_factorial_value(target_int)
301
+ if sol is not None:
302
+ return SolverResult(
303
+ domain="quant",
304
+ solved=True,
305
+ topic="factorial_equation",
306
+ answer_value=None,
307
+ internal_answer=str(sol),
308
+ steps=_equation_steps(),
309
+ )
310
 
311
+ # --------------------------------------------------
312
+ # 5) Direct small factorial value
313
+ # Covers:
314
+ # - 6!
315
+ # - value of 7!
316
+ # - factorial of 5
317
+ #
318
+ # Keep this after richer patterns so it does not steal
319
+ # trailing-zero / prime-power questions.
320
+ # --------------------------------------------------
321
+ direct_patterns = [
322
+ r"^\s*(\d+)\s*!\s*$",
323
+ r"value of\s+(\d+)\s*!",
324
+ r"what is\s+(\d+)\s*!",
325
+ r"factorial of\s+(\d+)",
326
+ r"value of\s+(\d+)\s+factorial",
327
+ ]
328
+ for pattern in direct_patterns:
329
+ m = re.search(pattern, lower)
330
+ if m:
331
+ n = int(m.group(1))
332
+ if n < 0:
333
+ return None
334
 
335
+ result = math.factorial(n)
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
+ return SolverResult(
338
+ domain="quant",
339
+ solved=True,
340
+ topic="factorial",
341
+ answer_value=None,
342
+ internal_answer=str(result),
343
+ steps=_factorial_steps(),
344
+ )
345
+
346
+ # --------------------------------------------------
347
+ # 6) “Last non-zero digit” / “units digit” in n!
348
+ # GMAT-style edge coverage:
349
+ # - last non-zero digit of 10!
350
+ # - units digit of 7!
351
+ #
352
+ # Note: for n! with n >= 5, the units digit is 0.
353
+ # For last non-zero digit, strip zeros.
354
+ # --------------------------------------------------
355
+ units_match = re.search(r"units digit.*?(\d+)\s*!|(\d+)\s*!\s*.*?units digit", lower)
356
+ if units_match:
357
+ n_str = units_match.group(1) or units_match.group(2)
358
+ if n_str is not None:
359
+ n = int(n_str)
360
+ value = math.factorial(n)
361
+ units = value % 10
362
+ return SolverResult(
363
+ domain="quant",
364
+ solved=True,
365
+ topic="factorial_units_digit",
366
+ answer_value=None,
367
+ internal_answer=str(units),
368
+ steps=[
369
+ "Compute or reason about the final digit of the factorial product.",
370
+ "For factorials with n ≥ 5, a factor of 10 appears, so the units digit becomes 0.",
371
+ "For smaller factorials, expand directly.",
372
+ ],
373
+ )
374
+
375
+ last_nonzero_match = re.search(
376
+ r"last non-?zero digit.*?(\d+)\s*!|(\d+)\s*!\s*.*?last non-?zero digit",
377
+ lower,
378
+ )
379
+ if last_nonzero_match:
380
+ n_str = last_nonzero_match.group(1) or last_nonzero_match.group(2)
381
+ if n_str is not None:
382
+ n = int(n_str)
383
+ value = math.factorial(n)
384
+ while value % 10 == 0:
385
+ value //= 10
386
+ last_nonzero = value % 10
387
+
388
+ return SolverResult(
389
+ domain="quant",
390
+ solved=True,
391
+ topic="factorial_last_nonzero_digit",
392
+ answer_value=None,
393
+ internal_answer=str(last_nonzero),
394
+ steps=[
395
+ "Find the factorial value or reason from its prime-factor structure.",
396
+ "Remove trailing zeros first.",
397
+ "Then take the final remaining digit.",
398
+ ],
399
+ )
400
+
401
+ # --------------------------------------------------
402
+ # 7) Divisibility-style basic recognition without a full parse
403
+ # Example:
404
+ # - Is 6! divisible by 9?
405
+ # - greatest integer k such that 2^k divides 25!
406
+ #
407
+ # We only solve simple p^k-divides-n! patterns when p is prime.
408
+ # --------------------------------------------------
409
+ power_divides_match = re.search(
410
+ r"greatest integer\s+(?:k|n).*?(\d+)\s*\^\s*(?:k|n)\s+divides\s+(\d+)\s*!",
411
+ lower
412
+ )
413
+ if power_divides_match:
414
+ base = int(power_divides_match.group(1))
415
+ n = int(power_divides_match.group(2))
416
+ if _is_prime(base):
417
+ count = _valuation_in_factorial(n, base)
418
+ return SolverResult(
419
+ domain="quant",
420
+ solved=True,
421
+ topic="factorial_prime_power",
422
+ answer_value=None,
423
+ internal_answer=str(count),
424
+ steps=_prime_power_steps(base),
425
+ )
426
 
427
+ # --------------------------------------------------
428
+ # 8) Fallback recognition for factorial questions that are real
429
+ # but not yet fully parsed. This is still useful because it gives
430
+ # structured guidance without pretending to solve incorrectly.
431
+ # --------------------------------------------------
432
+ if "!" in raw or "factorial" in lower:
433
  return SolverResult(
434
  domain="quant",
435
+ solved=False,
436
  topic="factorial",
437
  answer_value=None,
438
+ internal_answer=None,
439
  steps=[
440
+ "Identify whether the question is asking for direct evaluation, cancellation, trailing zeros, or prime-factor counting.",
441
+ "If it is a ratio, expand only enough terms to cancel.",
442
+ "If it is about zeros or divisibility, convert the question into counting prime factors inside the factorial.",
443
+ "Then simplify step by step without expanding more than necessary.",
444
  ],
445
  )
446