442 lines
19 KiB
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}`
|
|
|
|
|
|
|
|
**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.
|