Files
codetutor/backend/data/questions/basic-calculator-iv.yaml

442 lines
19 KiB
YAML

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:
- backtracking
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}`
&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.