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"]`.

  &nbsp;

  **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}`

    &nbsp;

    **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)

    &nbsp;

    **Step 3: Tokenise the expression**

    - Split by spaces to get tokens: numbers, variables, operators, parentheses
    - Create a map of evaluation values for variable substitution

    &nbsp;

    **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.

    &nbsp;

    **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.
