title: Basic Calculator IV slug: basic-calculator-iv difficulty: hard leetcode_id: 770 leetcode_url: https://leetcode.com/problems/basic-calculator-iv/ categories: - strings - hash-tables - recursion - stack - math patterns: - slug: backtracking is_optimal: true function_signature: "def basic_calculator_iv(expression: str, evalvars: list[str], evalints: list[int]) -> list[str]:" test_cases: visible: - input: { expression: "e + 8 - a + 5", evalvars: ["e"], evalints: [1] } expected: ["-1*a", "14"] - input: { expression: "e - 8 + temperature - pressure", evalvars: ["e", "temperature"], evalints: [1, 12] } expected: ["-1*pressure", "5"] - input: { expression: "(e + 8) * (e - 8)", evalvars: [], evalints: [] } expected: ["1*e*e", "-64"] hidden: - input: { expression: "0", evalvars: [], evalints: [] } expected: [] - input: { expression: "a * b", evalvars: [], evalints: [] } expected: ["1*a*b"] - input: { expression: "a * b * c", evalvars: ["a"], evalints: [2] } expected: ["2*b*c"] - input: { expression: "(a + b) * (a - b)", evalvars: [], evalints: [] } expected: ["1*a*a", "-1*b*b"] - input: { expression: "a + b + c", evalvars: ["a", "b", "c"], evalints: [1, 2, 3] } expected: ["6"] - input: { expression: "a * a * a - a * a + a - 1", evalvars: [], evalints: [] } expected: ["1*a*a*a", "-1*a*a", "1*a", "-1"] description: | Given an expression such as `expression = "e + 8 - a + 5"` and an evaluation map such as `{"e": 1}` (given in terms of `evalvars = ["e"]` and `evalints = [1]`), return a list of tokens representing the **simplified expression**, such as `["-1*a","14"]`. An expression alternates chunks and symbols, with a space separating each chunk and symbol. A chunk is either an expression in parentheses, a variable, or a non-negative integer. A variable is a string of lowercase letters (not including digits). Note that variables can be multiple letters, and note that variables never have a leading coefficient or unary operator like `"2x"` or `"-x"`. Expressions are evaluated in the usual order: brackets first, then multiplication, then addition and subtraction. For example, `expression = "1 + 2 * 3"` has an answer of `["7"]`.   **Output Format:** - For each term of free variables with a non-zero coefficient, write the free variables within a term in **sorted order lexicographically** (e.g., `"a*b*c"`, never `"b*a*c"`) - Terms have degrees equal to the number of free variables being multiplied, counting multiplicity. Write the **largest degree terms first**, breaking ties by lexicographic order ignoring the leading coefficient - The leading coefficient is placed directly to the left with an asterisk separating it from the variables. A leading coefficient of `1` is still printed - Terms with coefficient `0` are not included An example of a well-formatted answer is `["-2*a*a*a", "3*a*a*b", "3*b*b", "4*a", "5*c", "-6"]`. constraints: | - `1 <= expression.length <= 250` - `expression` consists of lowercase English letters, digits, `'+'`, `'-'`, `'*'`, `'('`, `')'`, `' '` - `expression` does not contain any leading or trailing spaces - All tokens in `expression` are separated by a single space - `0 <= evalvars.length <= 100` - `1 <= evalvars[i].length <= 20` - `evalvars[i]` consists of lowercase English letters - `evalints.length == evalvars.length` - `-100 <= evalints[i] <= 100` examples: - input: 'expression = "e + 8 - a + 5", evalvars = ["e"], evalints = [1]' output: '["-1*a","14"]' explanation: "Substituting e = 1 gives: 1 + 8 - a + 5 = 14 - a. Rearranged with highest degree first: -1*a + 14." - input: 'expression = "e - 8 + temperature - pressure", evalvars = ["e", "temperature"], evalints = [1, 12]' output: '["-1*pressure","5"]' explanation: "Substituting e = 1 and temperature = 12 gives: 1 - 8 + 12 - pressure = 5 - pressure." - input: 'expression = "(e + 8) * (e - 8)", evalvars = [], evalints = []' output: '["1*e*e","-64"]' explanation: "No substitutions. Using the difference of squares formula: (e + 8)(e - 8) = e² - 64." explanation: intuition: | This problem combines **expression parsing** with **polynomial arithmetic** — two challenging concepts rolled into one. Think of it like building a symbolic calculator that can handle algebraic expressions. When you type `(x + 2) * (x - 3)` into such a calculator, it expands this to `x² - x - 6`. That's essentially what we're implementing here. The key insight is to separate concerns: 1. **Parsing**: Convert the string expression into a structure we can evaluate, respecting operator precedence (parentheses > multiplication > addition/subtraction) 2. **Polynomial representation**: Instead of computing a single number, we track a *polynomial* — a collection of terms where each term is a coefficient times some product of variables (like `3*a*b` or `-5*x*x`) 3. **Polynomial arithmetic**: Define how to add, subtract, and multiply polynomials together The clever representation is to use a **map (dictionary)** where the key is a sorted tuple of variables (like `("a", "b")` or `("x", "x")`) and the value is the coefficient. This makes combining like terms trivial — just add coefficients for matching keys! approach: | We solve this using **recursive descent parsing** combined with a **polynomial class** that handles arithmetic operations. **Step 1: Define the Polynomial representation** - Use a dictionary mapping `tuple[str, ...]` → `int` - The tuple contains variables in sorted order (e.g., `("a", "b")` for term `a*b`) - Empty tuple `()` represents a constant term - Example: `3*a*b - 5` is `{("a", "b"): 3, (): -5}`   **Step 2: Implement polynomial operations** - **Addition**: Merge dictionaries, adding coefficients for matching keys - **Subtraction**: Same as addition but negate the second polynomial's coefficients - **Multiplication**: For each pair of terms, multiply coefficients and merge variable tuples (keeping them sorted)   **Step 3: Tokenise the expression** - Split by spaces to get tokens: numbers, variables, operators, parentheses - Create a map of evaluation values for variable substitution   **Step 4: Recursive descent parsing** - `parse_expression()`: Handles `+` and `-` (lowest precedence) - `parse_term()`: Handles `*` (higher precedence) - `parse_factor()`: Handles parentheses, variables, and numbers (highest precedence) Each function returns a Polynomial, and we combine results using our polynomial operations.   **Step 5: Format the output** - Filter out terms with zero coefficient - Sort by: degree (descending), then lexicographically by variables - Format each term as `"coefficient*var1*var2*..."` or just `"coefficient"` for constants common_pitfalls: - title: Ignoring Operator Precedence description: | A naive left-to-right evaluation of `1 + 2 * 3` gives `9` instead of the correct `7`. The expression `a + b * c` must parse `b * c` first, then add `a`. This requires either: - Recursive descent parsing with separate functions for each precedence level - Shunting-yard algorithm to convert to postfix notation - Operator precedence climbing Without respecting precedence, your results will be mathematically incorrect. wrong_approach: "Evaluate operators left to right as encountered" correct_approach: "Use recursive descent with precedence levels or convert to postfix" - title: Incorrect Term Ordering in Output description: | The output requires a specific ordering: 1. Higher degree terms first 2. Ties broken by lexicographic order of variables For `3*a + 2*b*b + 5`, the correct output is `["2*b*b", "3*a", "5"]` not `["3*a", "2*b*b", "5"]`. The term `2*b*b` has degree 2, while `3*a` has degree 1, so `b*b` comes first despite `a < b` lexicographically. wrong_approach: "Sort terms alphabetically or in evaluation order" correct_approach: "Sort by (-degree, variables_tuple) to get correct ordering" - title: Forgetting to Combine Like Terms description: | When you multiply `(a + b) * (a + b)`, you get `a*a + a*b + b*a + b*b`. The terms `a*b` and `b*a` are actually the same term and must be combined into `2*a*b`. This is why we sort variables within each term — `a*b` and `b*a` both become the key `("a", "b")`, allowing us to add their coefficients. Missing this step produces duplicate or incorrect terms. wrong_approach: "Keep a*b and b*a as separate terms" correct_approach: "Always sort variables in each term; same variable set = same term" - title: Not Handling Zero Coefficients description: | After operations like `(a - a)`, you'll have terms with coefficient `0`. These must be excluded from output. Similarly, the expression `"0"` should return `[]`, not `["0"]`. Always filter the polynomial before formatting output. wrong_approach: "Include all terms in output regardless of coefficient" correct_approach: "Filter out zero-coefficient terms before formatting" key_takeaways: - "**Recursive descent parsing** is a powerful technique for evaluating expressions with operator precedence — each precedence level gets its own parsing function" - "**Polynomials as dictionaries** with sorted variable tuples as keys makes combining like terms automatic" - "**Separation of concerns**: parsing logic is independent of polynomial arithmetic — define clean interfaces between them" - "This pattern extends to building interpreters, compilers, and symbolic math systems" time_complexity: "O(2^n + m) where n is the number of distinct variables and m is the expression length. In the worst case, multiplying polynomials can create exponentially many terms, though practical cases are much smaller." space_complexity: "O(2^n) to store all possible polynomial terms in the worst case. Each term requires space proportional to the number of variables it contains." solutions: - approach_name: Recursive Descent with Polynomial Class is_optimal: true code: | from collections import Counter class Poly: """Polynomial represented as {variable_tuple: coefficient}""" def __init__(self, terms=None): # terms maps (var1, var2, ...) -> coefficient # Empty tuple () represents constant term self.terms = Counter(terms) if terms else Counter() @staticmethod def from_const(c: int) -> 'Poly': """Create polynomial from a constant""" return Poly({(): c} if c else {}) @staticmethod def from_var(var: str) -> 'Poly': """Create polynomial from a single variable""" return Poly({(var,): 1}) def __add__(self, other: 'Poly') -> 'Poly': """Add two polynomials""" result = Poly(self.terms) result.terms.update(other.terms) # Remove zero coefficients return Poly({k: v for k, v in result.terms.items() if v}) def __sub__(self, other: 'Poly') -> 'Poly': """Subtract two polynomials""" result = Poly(self.terms) for k, v in other.terms.items(): result.terms[k] -= v return Poly({k: v for k, v in result.terms.items() if v}) def __mul__(self, other: 'Poly') -> 'Poly': """Multiply two polynomials""" result = Counter() for vars1, coef1 in self.terms.items(): for vars2, coef2 in other.terms.items(): # Merge and sort variables new_vars = tuple(sorted(vars1 + vars2)) result[new_vars] += coef1 * coef2 return Poly({k: v for k, v in result.items() if v}) def to_list(self) -> list[str]: """Convert to output format""" # Sort by: degree descending, then lexicographically sorted_terms = sorted( self.terms.items(), key=lambda x: (-len(x[0]), x[0]) ) result = [] for variables, coef in sorted_terms: if coef == 0: continue if variables: # Has variables: "coef*var1*var2*..." result.append(f"{coef}*" + "*".join(variables)) else: # Constant term result.append(str(coef)) return result class Solution: def basicCalculatorIV( self, expression: str, evalvars: list[str], evalints: list[int] ) -> list[str]: # Build evaluation map eval_map = dict(zip(evalvars, evalints)) # Tokenise tokens = expression.replace('(', ' ( ').replace(')', ' ) ').split() self.pos = 0 self.tokens = tokens self.eval_map = eval_map # Parse and return result result = self.parse_expression() return result.to_list() def parse_expression(self) -> Poly: """Parse addition and subtraction (lowest precedence)""" result = self.parse_term() while self.pos < len(self.tokens) and self.tokens[self.pos] in '+-': op = self.tokens[self.pos] self.pos += 1 right = self.parse_term() if op == '+': result = result + right else: result = result - right return result def parse_term(self) -> Poly: """Parse multiplication (higher precedence)""" result = self.parse_factor() while self.pos < len(self.tokens) and self.tokens[self.pos] == '*': self.pos += 1 right = self.parse_factor() result = result * right return result def parse_factor(self) -> Poly: """Parse parentheses, variables, numbers (highest precedence)""" token = self.tokens[self.pos] if token == '(': # Parenthesised expression self.pos += 1 result = self.parse_expression() self.pos += 1 # Skip ')' return result elif token.lstrip('-').isdigit(): # Number self.pos += 1 return Poly.from_const(int(token)) else: # Variable self.pos += 1 if token in self.eval_map: # Substitute with known value return Poly.from_const(self.eval_map[token]) else: # Keep as free variable return Poly.from_var(token) explanation: | **Time Complexity:** O(2^n * m) where n is the number of distinct free variables and m is the expression length. Polynomial multiplication can create exponentially many terms. **Space Complexity:** O(2^n) for storing polynomial terms. The solution uses recursive descent parsing to handle operator precedence correctly. Each level of precedence (expression → term → factor) is a separate method. The `Poly` class encapsulates polynomial arithmetic, making the parsing code clean and readable. - approach_name: Stack-Based Parsing is_optimal: false code: | from collections import Counter def basic_calculator_iv( expression: str, evalvars: list[str], evalints: list[int] ) -> list[str]: """Alternative using explicit stacks instead of recursion""" eval_map = dict(zip(evalvars, evalints)) def make_poly(token: str) -> Counter: """Convert token to polynomial""" if token.lstrip('-').isdigit(): c = int(token) return Counter({(): c}) if c else Counter() elif token in eval_map: c = eval_map[token] return Counter({(): c}) if c else Counter() else: return Counter({(token,): 1}) def combine(poly1: Counter, poly2: Counter, op: str) -> Counter: """Combine two polynomials with given operation""" if op == '+': result = poly1.copy() result.update(poly2) elif op == '-': result = poly1.copy() for k, v in poly2.items(): result[k] -= v else: # '*' result = Counter() for v1, c1 in poly1.items(): for v2, c2 in poly2.items(): key = tuple(sorted(v1 + v2)) result[key] += c1 * c2 # Remove zeros return Counter({k: v for k, v in result.items() if v}) def apply_ops(polys: list, ops: list, min_prec: int): """Apply operations with precedence >= min_prec""" prec = {'+': 1, '-': 1, '*': 2} while ops and ops[-1] != '(' and prec.get(ops[-1], 0) >= min_prec: op = ops.pop() right = polys.pop() left = polys.pop() polys.append(combine(left, right, op)) # Tokenise tokens = expression.replace('(', ' ( ').replace(')', ' ) ').split() polys = [] # Operand stack ops = [] # Operator stack prec = {'+': 1, '-': 1, '*': 2} for token in tokens: if token == '(': ops.append(token) elif token == ')': # Apply all ops until matching '(' while ops[-1] != '(': op = ops.pop() right = polys.pop() left = polys.pop() polys.append(combine(left, right, op)) ops.pop() # Remove '(' elif token in prec: # Apply ops with higher/equal precedence apply_ops(polys, ops, prec[token]) ops.append(token) else: # Number or variable polys.append(make_poly(token)) # Apply remaining operators apply_ops(polys, ops, 0) # Format output result = polys[0] if polys else Counter() sorted_terms = sorted(result.items(), key=lambda x: (-len(x[0]), x[0])) output = [] for variables, coef in sorted_terms: if coef == 0: continue if variables: output.append(f"{coef}*" + "*".join(variables)) else: output.append(str(coef)) return output explanation: | **Time Complexity:** O(2^n * m) — same as recursive approach. **Space Complexity:** O(2^n + m) — polynomial storage plus explicit stacks. This approach uses the shunting-yard algorithm concept with explicit operand and operator stacks. It's equivalent to recursive descent but uses iteration instead. Some find this easier to reason about; others prefer the recursive version's clarity.