| from __future__ import annotations |
|
|
| import math |
| import re |
| from fractions import Fraction |
| from math import comb |
| from typing import List, Optional, Tuple |
|
|
| from models import SolverResult |
|
|
|
|
| |
| |
| |
|
|
| COLOR_WORDS = [ |
| "red", "blue", "green", "white", "black", "yellow", "gray", "grey", |
| "orange", "purple", "pink", "brown" |
| ] |
|
|
| PROBABILITY_WORDS = [ |
| "probability", "chance", "likely", "likelihood", "odds", |
| "random", "at random", "equally likely", |
| "coin", "coins", "head", "heads", "tail", "tails", |
| "die", "dice", |
| "card", "cards", "deck", |
| "marble", "marbles", "ball", "balls", "urn", "bag", |
| "without replacement", "with replacement", |
| "committee", "chosen", "select", "selected", "draw", "drawn", |
| "exactly", "at least", "at most", "no more than", "no fewer than", |
| "independent", "mutually exclusive", |
| "rain", "success", "failure" |
| ] |
|
|
|
|
| def _clean(text: str) -> str: |
| return re.sub(r"\s+", " ", (text or "").strip()).lower() |
|
|
|
|
| def _nums(text: str) -> List[int]: |
| return [int(x) for x in re.findall(r"-?\d+", text)] |
|
|
|
|
| def _fraction_str(x: float) -> str: |
| try: |
| f = Fraction(x).limit_denominator() |
| if f.denominator == 1: |
| return str(f.numerator) |
| return f"{f.numerator}/{f.denominator}" |
| except Exception: |
| return f"{x:.6g}" |
|
|
|
|
| def _safe_decimal_str(x: float) -> str: |
| return f"{x:.6g}" |
|
|
|
|
| def _make_result( |
| *, |
| internal_answer: Optional[str], |
| steps: List[str], |
| solved: bool = True, |
| ) -> SolverResult: |
| return SolverResult( |
| domain="quant", |
| solved=solved, |
| topic="probability", |
| answer_value=None, |
| internal_answer=internal_answer, |
| steps=steps, |
| ) |
|
|
|
|
| def _contains_probability_language(lower: str) -> bool: |
| strong_markers = [ |
| "probability", "chance", "odds", "at random", "equally likely", |
| "without replacement", "with replacement", |
| "coin", "coins", "die", "dice", "card", "cards", "deck", |
| "marble", "marbles", "ball", "balls", "urn", "bag" |
| ] |
| return any(w in lower for w in strong_markers) |
|
|
|
|
| def _contains_any(lower: str, words: List[str]) -> bool: |
| return any(w in lower for w in words) |
|
|
|
|
| def _has_without_replacement(lower: str) -> bool: |
| return "without replacement" in lower or "not replaced" in lower |
|
|
|
|
| def _has_with_replacement(lower: str) -> bool: |
| return "with replacement" in lower or "replaced" in lower |
|
|
|
|
| def _extract_percent_value(text: str) -> Optional[float]: |
| m = re.search(r"(\d+(?:\.\d+)?)\s*%", text) |
| if m: |
| return float(m.group(1)) / 100.0 |
| return None |
|
|
|
|
| def _extract_probability_value(text: str) -> Optional[float]: |
| """ |
| Tries to pull a direct probability from text: |
| - 40% |
| - 0.4 |
| - 1/3 |
| """ |
| pct = _extract_percent_value(text) |
| if pct is not None: |
| return pct |
|
|
| frac = re.search(r"\b(\d+)\s*/\s*(\d+)\b", text) |
| if frac: |
| a = int(frac.group(1)) |
| b = int(frac.group(2)) |
| if b != 0: |
| return a / b |
|
|
| dec = re.search(r"\b0\.\d+\b", text) |
| if dec: |
| return float(dec.group(0)) |
|
|
| return None |
|
|
|
|
| def _extract_named_counts(lower: str) -> List[Tuple[str, int]]: |
| """ |
| Picks up structures like: |
| '10 green and 90 white marbles' |
| '1 gray, 2 white and 4 green balls' |
| '5 red, 3 blue' |
| """ |
| pairs = [] |
| for m in re.finditer(r"(\d+)\s+([a-z]+)", lower): |
| n = int(m.group(1)) |
| word = m.group(2) |
| if word in COLOR_WORDS or word in { |
| "odd", "even", "prime", "composite", |
| "boys", "girls", "men", "women", |
| "married", "single" |
| }: |
| pairs.append((word, n)) |
| return pairs |
|
|
|
|
| def _extract_color_counts(lower: str) -> List[Tuple[str, int]]: |
| return [(name, n) for name, n in _extract_named_counts(lower) if name in COLOR_WORDS] |
|
|
|
|
| def _extract_set_contents(lower: str) -> List[List[int]]: |
| """ |
| Extracts {1,3,6,7,8} style sets. |
| """ |
| sets = [] |
| for m in re.finditer(r"\{([^{}]+)\}", lower): |
| raw = m.group(1) |
| vals = [int(x) for x in re.findall(r"-?\d+", raw)] |
| if vals: |
| sets.append(vals) |
| return sets |
|
|
|
|
| def _is_fair_coin(lower: str) -> bool: |
| return "coin" in lower or "coins" in lower |
|
|
|
|
| def _is_die_problem(lower: str) -> bool: |
| return "die" in lower or "dice" in lower |
|
|
|
|
| def _is_card_problem(lower: str) -> bool: |
| return "card" in lower or "cards" in lower or "deck" in lower |
|
|
|
|
| def _is_draw_problem(lower: str) -> bool: |
| return any(w in lower for w in ["marble", "marbles", "ball", "balls", "urn", "bag", "card", "cards", "deck"]) |
|
|
|
|
| def _probability_of_card_event(lower: str) -> Optional[Tuple[float, List[str]]]: |
| """ |
| Basic single-card deck facts. |
| """ |
| if not _is_card_problem(lower): |
| return None |
|
|
| total = 52 |
| event = None |
| count = None |
|
|
| if "ace" in lower: |
| event, count = "ace", 4 |
| elif "king" in lower: |
| event, count = "king", 4 |
| elif "queen" in lower: |
| event, count = "queen", 4 |
| elif "jack" in lower: |
| event, count = "jack", 4 |
| elif "heart" in lower: |
| event, count = "heart", 13 |
| elif "spade" in lower: |
| event, count = "spade", 13 |
| elif "club" in lower: |
| event, count = "club", 13 |
| elif "diamond" in lower: |
| event, count = "diamond", 13 |
| elif "face card" in lower or ("face" in lower and "card" in lower): |
| event, count = "face card", 12 |
| elif "red card" in lower or ("red" in lower and "card" in lower): |
| event, count = "red card", 26 |
| elif "black card" in lower or ("black" in lower and "card" in lower): |
| event, count = "black card", 26 |
|
|
| if count is None: |
| return None |
|
|
| p = count / total |
| steps = [ |
| "Treat a standard deck as 52 equally likely cards unless the question says otherwise.", |
| f"Count how many cards satisfy the requested property ({event}).", |
| "Use probability = favorable outcomes ÷ total outcomes.", |
| ] |
| return p, steps |
|
|
|
|
| def _extract_trial_counts(lower: str) -> Optional[Tuple[int, int]]: |
| """ |
| Extract exactly k in n style language. |
| """ |
| n = None |
| k = None |
|
|
| m_n = re.search(r"\b(?:in|over|during)\s+a?\s*(\d+)[- ](?:day|trial|toss|flip|roll|time|times|period)\b", lower) |
| if m_n: |
| n = int(m_n.group(1)) |
|
|
| if n is None: |
| m_n = re.search(r"\b(\d+)\s+(?:times|trials|days|flips|tosses|rolls)\b", lower) |
| if m_n: |
| n = int(m_n.group(1)) |
|
|
| m_k = re.search(r"\bexactly\s+(\d+)\b", lower) |
| if m_k: |
| k = int(m_k.group(1)) |
|
|
| return (k, n) if k is not None and n is not None else None |
|
|
|
|
| def _is_sequence_ordered(lower: str) -> bool: |
| ordered_markers = [ |
| "first", "second", "third", |
| "then", "followed by", "on the first day", "on the second day" |
| ] |
| return any(m in lower for m in ordered_markers) |
|
|
|
|
| |
| |
| |
|
|
| def _solve_simple_favorable_total(lower: str) -> Optional[SolverResult]: |
| m = re.search(r"(\d+)\s+out of\s+(\d+)", lower) |
| if m: |
| fav = int(m.group(1)) |
| total = int(m.group(2)) |
| if total == 0: |
| return None |
| p = fav / total |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "This is a direct favorable-over-total setup.", |
| "Count the outcomes that satisfy the condition.", |
| "Divide by the total number of equally likely outcomes.", |
| ], |
| ) |
|
|
| m = re.search(r"probability.*?(\d+).*?(?:possible|total)", lower) |
| if m: |
| nums = _nums(lower) |
| if len(nums) >= 2 and nums[-1] != 0: |
| fav = nums[0] |
| total = nums[-1] |
| p = fav / total |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "Use probability = favorable outcomes ÷ total equally likely outcomes.", |
| "Make sure the denominator is the full sample space.", |
| ], |
| ) |
| return None |
|
|
|
|
| def _solve_single_coin_or_die(lower: str) -> Optional[SolverResult]: |
| if _is_fair_coin(lower): |
| if "head" in lower or "heads" in lower or "tail" in lower or "tails" in lower: |
| if not any(w in lower for w in ["twice", "two", "three", "4 times", "5 times", "6 times"]): |
| p = 1 / 2 |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "A fair coin has 2 equally likely outcomes.", |
| "Identify the one outcome that matches the event.", |
| "Use favorable ÷ total.", |
| ], |
| ) |
|
|
| if _is_die_problem(lower): |
| if "even" in lower: |
| p = 3 / 6 |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "A fair die has 6 equally likely outcomes.", |
| "The even outcomes are 2, 4, and 6.", |
| "Use favorable ÷ total.", |
| ], |
| ) |
| if "odd" in lower: |
| p = 3 / 6 |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "A fair die has 6 equally likely outcomes.", |
| "The odd outcomes are 1, 3, and 5.", |
| "Use favorable ÷ total.", |
| ], |
| ) |
| m = re.search(r"(?:at least|greater than or equal to)\s+(\d+)", lower) |
| if m: |
| k = int(m.group(1)) |
| fav = len([x for x in range(1, 7) if x >= k]) |
| if 0 <= fav <= 6: |
| p = fav / 6 |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "List the die outcomes that satisfy the condition.", |
| "Count how many are favorable.", |
| "Divide by 6.", |
| ], |
| ) |
| m = re.search(r"(?:at most|less than or equal to)\s+(\d+)", lower) |
| if m: |
| k = int(m.group(1)) |
| fav = len([x for x in range(1, 7) if x <= k]) |
| if 0 <= fav <= 6: |
| p = fav / 6 |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "List the die outcomes that satisfy the condition.", |
| "Count how many are favorable.", |
| "Divide by 6.", |
| ], |
| ) |
| return None |
|
|
|
|
| def _solve_single_card(lower: str) -> Optional[SolverResult]: |
| data = _probability_of_card_event(lower) |
| if data is None: |
| return None |
| p, steps = data |
| return _make_result(internal_answer=_fraction_str(p), steps=steps) |
|
|
|
|
| def _solve_basic_draw_ratio(lower: str) -> Optional[SolverResult]: |
| """ |
| One draw from marbles/balls/cards with named categories. |
| """ |
| if not _is_draw_problem(lower): |
| return None |
|
|
| color_counts = _extract_color_counts(lower) |
| if len(color_counts) >= 2 and not _is_sequence_ordered(lower): |
| total = sum(n for _, n in color_counts) |
| if total == 0: |
| return None |
|
|
| for color, count in color_counts: |
| if color in lower: |
| p = count / total |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "This is a single-draw favorable-over-total problem.", |
| "Count how many objects have the requested property.", |
| "Divide by the total number of objects.", |
| ], |
| ) |
| return None |
|
|
|
|
| def _solve_independent_ordered_events(lower: str) -> Optional[SolverResult]: |
| """ |
| Handles ordered independent sequences like: |
| - heads and a 4 |
| - rain first day but not second |
| """ |
| if "heads and a \"4\"" in lower or ("head" in lower and "4" in lower and _is_die_problem(lower)): |
| p = (1 / 2) * (1 / 6) |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "Identify the events in order.", |
| "Because the events are independent, multiply their probabilities.", |
| "Use product rule for 'and' with independent events.", |
| ], |
| ) |
|
|
| if "rain" in lower and _is_sequence_ordered(lower): |
| p_rain = _extract_probability_value(lower) |
| if p_rain is not None: |
| |
| if ("first day" in lower and "second day" in lower) and ( |
| "but not" in lower or "not on the second" in lower or "sunshine on the second" in lower |
| ): |
| p = p_rain * (1 - p_rain) |
| return _make_result( |
| internal_answer=_fraction_str(p), |
| steps=[ |
| "Translate the wording into an ordered sequence of events.", |
| "Use the given probability for rain and its complement for no rain.", |
| "Because days are treated as independent here, multiply the stage probabilities.", |
| ], |
| ) |
| return None |
|
|
|
|
| def _solve_complement_at_least_one(lower: str) -> Optional[SolverResult]: |
| """ |
| At least one success in n independent trials. |
| """ |
| if "at least one" not in lower: |
| return None |
|
|
| p = _extract_probability_value(lower) |
| n = None |
|
|
| m = re.search(r"\b(\d+)\s+(?:times|days|trials|flips|tosses|rolls)\b", lower) |
| if m: |
| n = int(m.group(1)) |
|
|
| if n is None: |
| m = re.search(r"\bin a[n]?\s+(\d+)[- ](?:day|trial|flip|toss|roll|period)\b", lower) |
| if m: |
| n = int(m.group(1)) |
|
|
| if p is None and _is_fair_coin(lower): |
| p = 1 / 2 |
|
|
| if p is None or n is None: |
| return None |
|
|
| ans = 1 - (1 - p) ** n |
| return _make_result( |
| internal_answer=_fraction_str(ans), |
| steps=[ |
| "For 'at least one', the complement is usually easiest.", |
| "Compute the probability of zero successes.", |
| "Subtract that from 1.", |
| ], |
| ) |
|
|
|
|
| def _solve_exactly_k_in_n(lower: str) -> Optional[SolverResult]: |
| """ |
| Binomial-type: |
| exactly k successes in n independent trials with probability p. |
| """ |
| if "exactly" not in lower: |
| return None |
|
|
| kn = _extract_trial_counts(lower) |
| if not kn: |
| return None |
| k, n = kn |
|
|
| p = _extract_probability_value(lower) |
| if p is None and _is_fair_coin(lower): |
| p = 1 / 2 |
|
|
| if p is None or n is None or k is None: |
| return None |
| if k < 0 or n < 0 or k > n: |
| return None |
|
|
| ans = comb(n, k) * (p ** k) * ((1 - p) ** (n - k)) |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "This is an 'exactly k successes in n independent trials' structure.", |
| "Count how many different arrangements produce k successes.", |
| "Multiply arrangements by the probability of one such arrangement.", |
| ], |
| ) |
|
|
|
|
| def _solve_without_replacement_two_draws(lower: str) -> Optional[SolverResult]: |
| """ |
| Two-draw color/object probability, with or without replacement. |
| Recognises: |
| - both red |
| - two red |
| - one of each |
| - at least one red |
| """ |
| if not _is_draw_problem(lower): |
| return None |
|
|
| counts = _extract_color_counts(lower) |
| if len(counts) < 2: |
| return None |
|
|
| total = sum(n for _, n in counts) |
| if total <= 0: |
| return None |
|
|
| lookup = {name: n for name, n in counts} |
| replace = _has_with_replacement(lower) and not _has_without_replacement(lower) |
|
|
| target_color = None |
| for c in COLOR_WORDS: |
| if c in lower: |
| target_color = c |
| break |
|
|
| |
| if target_color and any(phrase in lower for phrase in [f"both {target_color}", f"two {target_color}", f"{target_color} both"]): |
| if target_color not in lookup: |
| return None |
| good = lookup[target_color] |
|
|
| if replace: |
| ans = (good / total) ** 2 |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "This is a repeated-draw problem with replacement.", |
| "The probability stays the same from draw to draw.", |
| "For two required successes, multiply the stage probabilities.", |
| ], |
| ) |
| else: |
| if good < 2 or total < 2: |
| return None |
| ans = (good / total) * ((good - 1) / (total - 1)) |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "This is a repeated-draw problem without replacement.", |
| "After the first successful draw, both the favorable count and total count change.", |
| "Multiply the updated stage probabilities.", |
| ], |
| ) |
|
|
| |
| m = re.search(r"one\s+([a-z]+)\s+and\s+one\s+([a-z]+)", lower) |
| if m: |
| c1 = m.group(1) |
| c2 = m.group(2) |
| if c1 in lookup and c2 in lookup and c1 != c2: |
| a = lookup[c1] |
| b = lookup[c2] |
|
|
| if replace: |
| ans = 2 * (a / total) * (b / total) |
| else: |
| ans = (a / total) * (b / (total - 1)) + (b / total) * (a / (total - 1)) |
|
|
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "For 'one of each', consider both possible orders unless order is fixed.", |
| "Compute each valid order.", |
| "Add the mutually exclusive orders.", |
| ], |
| ) |
|
|
| |
| if target_color and f"at least one {target_color}" in lower and target_color in lookup: |
| good = lookup[target_color] |
| bad = total - good |
| if total < 2: |
| return None |
|
|
| if replace: |
| ans = 1 - (bad / total) ** 2 |
| else: |
| if bad < 2: |
| ans = 1.0 |
| else: |
| ans = 1 - (bad / total) * ((bad - 1) / (total - 1)) |
|
|
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "For 'at least one', the complement is often easier.", |
| "First compute the probability of getting none of the target color.", |
| "Subtract from 1.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_combination_probability(lower: str) -> Optional[SolverResult]: |
| """ |
| Committee / selection style combinatorial probability. |
| """ |
|
|
| |
| m = re.search( |
| r"there are (\d+) .*? if (\d+) .*? randomly chosen .*? probability .*? includes both ([a-z]+) and ([a-z]+)", |
| lower |
| ) |
| if m: |
| total_people = int(m.group(1)) |
| choose_n = int(m.group(2)) |
| if choose_n == 2 and total_people >= 2: |
| ans = 1 / comb(total_people, 2) |
| return _make_result( |
| internal_answer=_fraction_str(ans), |
| steps=[ |
| "This is a committee-selection probability problem.", |
| "Count all possible committees of the required size.", |
| "Count how many committees satisfy the condition, then divide favorable by total.", |
| ], |
| ) |
|
|
| |
| m = re.search(r"(\d+)\s+red.*?(\d+)\s+blue.*?choose\s+2.*?both red", lower) |
| if m: |
| red = int(m.group(1)) |
| blue = int(m.group(2)) |
| total = red + blue |
| if red >= 2 and total >= 2: |
| ans = comb(red, 2) / comb(total, 2) |
| return _make_result( |
| internal_answer=_fraction_str(ans), |
| steps=[ |
| "Use combinations when order does not matter.", |
| "Count favorable selections.", |
| "Count total selections.", |
| ], |
| ) |
|
|
| |
| m = re.search( |
| r"(\d+)\s+married couples.*?select .*?(\d+)\s+people.*?probability that none of them are married to each other", |
| lower |
| ) |
| if m: |
| couples = int(m.group(1)) |
| choose_n = int(m.group(2)) |
| total_people = 2 * couples |
| if 0 <= choose_n <= couples: |
| favorable = comb(couples, choose_n) * (2 ** choose_n) |
| total = comb(total_people, choose_n) |
| ans = favorable / total |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "This is a combinatorial selection problem with a restriction.", |
| "Choose which couples are represented.", |
| "Then choose one person from each selected couple.", |
| "Divide by the total number of unrestricted selections.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_set_based_odd_even(lower: str) -> Optional[SolverResult]: |
| """ |
| Example: choose one integer from each set, probability both odd. |
| """ |
| sets = _extract_set_contents(lower) |
| if len(sets) >= 2 and ("odd" in lower or "even" in lower): |
| target = "odd" if "odd" in lower else "even" |
|
|
| probs = [] |
| for s in sets[:2]: |
| if not s: |
| return None |
| if target == "odd": |
| good = sum(1 for x in s if x % 2 != 0) |
| else: |
| good = sum(1 for x in s if x % 2 == 0) |
| probs.append(good / len(s)) |
|
|
| if "two odd integers" in lower or "both odd" in lower or "both even" in lower: |
| ans = probs[0] * probs[1] |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "Treat each selection as its own favorable-over-total probability.", |
| "Then multiply because the selections come from separate sets.", |
| ], |
| ) |
| return None |
|
|
|
|
| def _solve_or_probability(lower: str) -> Optional[SolverResult]: |
| """ |
| Handles explicit P(A)=..., P(B)=..., mutually exclusive / overlap cases. |
| """ |
| if " or " not in lower and "either" not in lower: |
| return None |
|
|
| probs = [] |
| for m in re.finditer(r"p\([^)]+\)\s*=\s*(\d+/\d+|\d+%|0\.\d+)", lower): |
| probs.append(m.group(1)) |
|
|
| def parse_prob(token: str) -> Optional[float]: |
| token = token.strip() |
| if token.endswith("%"): |
| return float(token[:-1]) / 100.0 |
| if "/" in token: |
| a, b = token.split("/") |
| a, b = int(a), int(b) |
| if b == 0: |
| return None |
| return a / b |
| if token.startswith("0."): |
| return float(token) |
| return None |
|
|
| if len(probs) >= 2: |
| p_a = parse_prob(probs[0]) |
| p_b = parse_prob(probs[1]) |
| if p_a is None or p_b is None: |
| return None |
|
|
| if "mutually exclusive" in lower or "cannot occur at the same time" in lower: |
| ans = p_a + p_b |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "For mutually exclusive events, there is no overlap.", |
| "So the probability of 'A or B' is the sum of their probabilities.", |
| ], |
| ) |
|
|
| m_overlap = re.search(r"p\(a and b\)\s*=\s*(\d+/\d+|\d+%|0\.\d+)", lower) |
| if m_overlap: |
| p_ab = parse_prob(m_overlap.group(1)) |
| if p_ab is not None: |
| ans = p_a + p_b - p_ab |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "For overlapping events, use the addition rule.", |
| "Add the two event probabilities.", |
| "Subtract the overlap once so it is not double-counted.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| def _solve_conditional_probability(lower: str) -> Optional[SolverResult]: |
| """ |
| P(A|B) = P(A and B) / P(B) |
| """ |
| if "given that" not in lower and "|" not in lower: |
| return None |
|
|
| tokens = [] |
| for m in re.finditer(r"(\d+/\d+|\d+%|0\.\d+)", lower): |
| tokens.append(m.group(1)) |
|
|
| def parse_prob(token: str) -> Optional[float]: |
| if token.endswith("%"): |
| return float(token[:-1]) / 100.0 |
| if "/" in token: |
| a, b = token.split("/") |
| a, b = int(a), int(b) |
| if b == 0: |
| return None |
| return a / b |
| if token.startswith("0."): |
| return float(token) |
| return None |
|
|
| if len(tokens) >= 2: |
| p_ab = parse_prob(tokens[0]) |
| p_b = parse_prob(tokens[1]) |
| if p_ab is not None and p_b not in (None, 0): |
| ans = p_ab / p_b |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "This is a conditional probability structure.", |
| "Restrict the sample space to the given condition.", |
| "Then divide the joint probability by the probability of the condition.", |
| ], |
| ) |
| return None |
|
|
|
|
| def _solve_symmetry_probability(lower: str) -> Optional[SolverResult]: |
| """ |
| Symmetry shortcuts like: |
| Bob left of Rachel -> 1/2 |
| """ |
| if "left of" in lower or "right of" in lower: |
| if "always left to" in lower or "left of" in lower: |
| ans = 1 / 2 |
| return _make_result( |
| internal_answer=_fraction_str(ans), |
| steps=[ |
| "This is a symmetry situation.", |
| "For every arrangement where one named person is left of the other, there is a mirrored arrangement where the order reverses.", |
| "So the desired probability is one half of all arrangements.", |
| ], |
| ) |
| return None |
|
|
|
|
| def _solve_tree_style_two_stage(lower: str) -> Optional[SolverResult]: |
| """ |
| Handles multi-branch two-stage without-replacement wording loosely. |
| This is intentionally conservative and only triggers when the structure is clear. |
| """ |
| if "without replacement" not in lower: |
| return None |
|
|
| counts = _extract_color_counts(lower) |
| if len(counts) < 2: |
| return None |
|
|
| total = sum(n for _, n in counts) |
| lookup = {name: n for name, n in counts} |
| if total < 2: |
| return None |
|
|
| |
| |
| |
| |
| if ( |
| "wins if" in lower |
| and "first" in lower |
| and "second" in lower |
| and ("or if" in lower or "or" in lower) |
| ): |
| parts = [] |
|
|
| |
| for color in COLOR_WORDS: |
| if f"first is {color}" in lower or f"first {color}" in lower: |
| if color in lookup: |
| parts.append(lookup[color] / total) |
| break |
|
|
| |
| for c1 in COLOR_WORDS: |
| for c2 in COLOR_WORDS: |
| phrase1 = f"first {c1} and second {c2}" |
| phrase2 = f"first ball is {c1} and the second ball is {c2}" |
| if phrase1 in lower or phrase2 in lower: |
| if c1 in lookup and c2 in lookup: |
| n1 = lookup[c1] |
| n2 = lookup[c2] |
| if c1 == c2: |
| if n1 >= 2: |
| parts.append((n1 / total) * ((n1 - 1) / (total - 1))) |
| else: |
| parts.append((n1 / total) * (n2 / (total - 1))) |
|
|
| |
| for color in COLOR_WORDS: |
| if f"two {color}" in lower: |
| if color in lookup and lookup[color] >= 2: |
| n = lookup[color] |
| parts.append((n / total) * ((n - 1) / (total - 1))) |
| break |
|
|
| if parts: |
| ans = sum(parts) |
| return _make_result( |
| internal_answer=_safe_decimal_str(ans), |
| steps=[ |
| "This is a multi-branch probability-tree style problem.", |
| "Break the win condition into separate valid paths.", |
| "Find the probability of each path.", |
| "Add the mutually exclusive winning paths.", |
| ], |
| ) |
|
|
| return None |
|
|
|
|
| |
| |
| |
|
|
| def _explanation_only_result(lower: str) -> Optional[SolverResult]: |
| if not _contains_probability_language(lower): |
| return None |
|
|
| steps = [ |
| "Identify what counts as a successful outcome.", |
| "Decide whether the problem is favorable-over-total, multiplication ('and'), addition ('or'), complement, or counting-based.", |
| "Check whether order matters and whether draws are with replacement or without replacement.", |
| "If the wording says 'at least one', try the complement first.", |
| "If the wording says 'exactly k times in n trials', think binomial structure.", |
| ] |
|
|
| if _has_without_replacement(lower): |
| steps.append("Without replacement means the probabilities change after each draw.") |
| if "mutually exclusive" in lower: |
| steps.append("Mutually exclusive events are added because they cannot happen together.") |
| if "independent" in lower: |
| steps.append("Independent events are multiplied because one does not change the other.") |
|
|
| return _make_result( |
| internal_answer=None, |
| steps=steps, |
| solved=False, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def solve_probability(text: str) -> Optional[SolverResult]: |
| lower = _clean(text) |
| if not _contains_probability_language(lower): |
| return None |
|
|
| solvers = [ |
| _solve_simple_favorable_total, |
| _solve_single_coin_or_die, |
| _solve_single_card, |
| _solve_basic_draw_ratio, |
| _solve_independent_ordered_events, |
| _solve_complement_at_least_one, |
| _solve_exactly_k_in_n, |
| _solve_without_replacement_two_draws, |
| _solve_combination_probability, |
| _solve_set_based_odd_even, |
| _solve_or_probability, |
| _solve_conditional_probability, |
| _solve_symmetry_probability, |
| _solve_tree_style_two_stage, |
| ] |
|
|
| for solver in solvers: |
| try: |
| result = solver(lower) |
| if result is not None: |
| return result |
| except Exception: |
| continue |
|
|
| return _explanation_only_result(lower) |