questions B (backspace - burst-balloons)
This commit is contained in:
417
backend/data/questions/basic-calculator-iv.yaml
Normal file
417
backend/data/questions/basic-calculator-iv.yaml
Normal file
@@ -0,0 +1,417 @@
|
||||
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
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user