LeetCode 2019 - The Score of Students Solving Math Expression
The problem gives us a mathematical expression string s containing only: - Single digit numbers 0-9 - Addition operators + - Multiplication operators The expression is guaranteed to be valid.
Difficulty: 🔴 Hard
Topics: Array, Hash Table, Math, String, Dynamic Programming, Stack, Memoization
Solution
LeetCode 2019, The Score of Students Solving Math Expression
Problem Understanding
The problem gives us a mathematical expression string s containing only:
- Single digit numbers
0-9 - Addition operators
+ - Multiplication operators
*
The expression is guaranteed to be valid. For example:
3+5*2
7+3*1*2
Students were supposed to evaluate the expression using the correct precedence rules:
- Perform all multiplications from left to right
- Then perform all additions from left to right
This is standard arithmetic precedence where multiplication happens before addition.
We are also given an array answers, where each value represents a student's submitted answer. We must assign points according to the following grading rules:
- If the answer equals the correct result, award
5points - Otherwise, if the answer could result from a different but valid parenthesization where arithmetic itself is still correct, award
2points - Otherwise, award
0points
The important subtlety is understanding what counts as a "wrong order" answer.
Students may incorrectly evaluate the expression by choosing arbitrary parenthesizations, effectively changing operator precedence. However:
- They must still perform arithmetic correctly
- Only results obtainable from valid binary expression groupings count
- Intermediate and final values must stay within the allowed range
- We only care about results up to
1000
For example:
3+5*2
Correct evaluation:
3 + (5*2) = 13
Possible incorrect evaluation:
(3+5) * 2 = 16
So 16 earns 2 points.
The constraints are very important:
- Expression length is at most
31 - There are at most
15operators - Therefore, there are at most
16numbers
This is small enough for interval dynamic programming.
The main challenge is efficiently computing all possible results obtainable through incorrect parenthesizations.
Important edge cases include:
- Expressions where incorrect evaluation accidentally equals the correct answer
- Expressions containing
0 - Duplicate answers
- Many different parenthesizations producing the same value
- Intermediate results exceeding
1000, which should be discarded
Approaches
Brute Force Approach
A brute force solution would generate every possible parenthesization of the expression and evaluate each one.
This is equivalent to generating all binary expression trees. The number of parenthesizations grows according to Catalan numbers:
C(n) ≈ 4^n / (n^(3/2))
For 15 operators, this becomes extremely large.
The brute force algorithm would:
- Recursively split the expression at every operator
- Compute all possible results for the left and right subexpressions
- Combine them according to the operator
- Collect all generated values
- Compare student answers against the resulting set
Although this produces correct results, recomputing the same subexpressions repeatedly makes it far too slow.
Key Insight
The crucial observation is that many recursive calls solve the same subproblem repeatedly.
For example:
3+5*2+4
The subexpression 5*2 may appear in many recursive decompositions.
Instead of recomputing results every time, we can use interval dynamic programming with memoization.
For every substring representing a valid subexpression, we compute:
all possible values obtainable from that subexpression
Then:
- Store the results in a memo table
- Reuse them whenever needed
- Combine left and right results efficiently
Since there are only at most 16 numbers, the total number of subexpressions is manageable.
We also aggressively prune values greater than 1000, because student answers never exceed 1000.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | Exponential | Recomputes identical subexpressions repeatedly |
| Optimal | O(n³ × m²) | O(n² × m) | Interval DP with memoized result sets |
Here:
nis the number of numbers in the expressionmis the maximum number of distinct results per interval
Algorithm Walkthrough
Step 1: Parse the Expression
Separate the expression into:
- A list of numbers
- A list of operators
For example:
"3+5*2"
becomes:
nums = [3, 5, 2]
ops = ['+', '*']
This makes interval processing much easier.
Step 2: Compute the Correct Answer
We must evaluate the expression using standard precedence rules.
Because multiplication has higher precedence than addition, we can process the expression as follows:
- Maintain a running multiplication product
- Whenever we encounter
+, add the accumulated product to the total - Reset the product with the next number
For:
3+5*2
we compute:
3 + (5*2) = 13
This gives the official correct answer worth 5 points.
Step 3: Define the DP State
We define:
dp(l, r)
as:
all possible results obtainable from nums[l:r+1]
Each state returns a set of integers.
For example:
dp(0, 2)
for:
3+5*2
may produce:
{13, 16}
Step 4: Base Case
If the interval contains only one number:
l == r
then the only possible value is that number itself.
Example:
dp(1,1) = {5}
Step 5: Split at Every Operator
For every operator position k between l and r:
- Recursively compute all left results
- Recursively compute all right results
- Combine every left value with every right value
If the operator is +:
value = left + right
If the operator is *:
value = left * right
Step 6: Prune Large Values
The problem guarantees answers are between 0 and 1000.
Any intermediate result larger than 1000 can be ignored.
This dramatically reduces the number of stored states.
Step 7: Memoize Results
Many intervals repeat during recursion.
Memoization ensures each interval is computed only once.
Without memoization, runtime would explode exponentially.
Step 8: Grade Student Answers
Now we have:
correct_answerpossible_answers
For each student answer:
- If it equals
correct_answer, add5 - Else if it exists in
possible_answers, add2 - Otherwise add
0
Why it works
The recursion considers every possible binary partition of the expression. Every valid parenthesization corresponds to exactly one recursive decomposition tree, so every obtainable value is generated.
Memoization guarantees that each subexpression is solved once, while set storage prevents duplicate values from growing uncontrollably.
Because we combine all left and right possibilities for every split point, the DP enumerates exactly all valid incorrect evaluations.
Python Solution
from typing import List
from functools import lru_cache
class Solution:
def scoreOfStudents(self, s: str, answers: List[int]) -> int:
nums = []
ops = []
for i, ch in enumerate(s):
if i % 2 == 0:
nums.append(int(ch))
else:
ops.append(ch)
# Compute correct answer using standard precedence
correct = 0
current = nums[0]
for i, op in enumerate(ops):
if op == '*':
current *= nums[i + 1]
else:
correct += current
current = nums[i + 1]
correct += current
@lru_cache(None)
def dp(left: int, right: int):
if left == right:
return {nums[left]}
results = set()
for k in range(left, right):
left_values = dp(left, k)
right_values = dp(k + 1, right)
for a in left_values:
for b in right_values:
if ops[k] == '+':
value = a + b
else:
value = a * b
if value <= 1000:
results.add(value)
return results
possible = dp(0, len(nums) - 1)
score = 0
for ans in answers:
if ans == correct:
score += 5
elif ans in possible:
score += 2
return score
The implementation begins by parsing the expression into separate number and operator arrays. This simplifies interval handling because each subproblem can be represented by number indices instead of substring parsing.
The correct answer is computed using multiplication precedence. Instead of building a full parser, we maintain a running multiplication group and add completed groups whenever a + appears.
The recursive dp(left, right) function returns all possible values obtainable from the subexpression spanning indices left through right.
The memoization decorator is critical because many intervals repeat. Without caching, the recursion would recompute the same subexpressions many times.
For every operator position k, the function combines every left result with every right result according to the operator. This systematically explores every valid parenthesization.
The value <= 1000 pruning keeps the state space manageable and matches the problem constraints.
Finally, the grading loop checks whether each answer is exactly correct or merely obtainable through incorrect precedence.
Go Solution
package main
func scoreOfStudents(s string, answers []int) int {
nums := []int{}
ops := []byte{}
for i := 0; i < len(s); i++ {
if i%2 == 0 {
nums = append(nums, int(s[i]-'0'))
} else {
ops = append(ops, s[i])
}
}
// Compute correct answer
correct := 0
current := nums[0]
for i, op := range ops {
if op == '*' {
current *= nums[i+1]
} else {
correct += current
current = nums[i+1]
}
}
correct += current
type Key struct {
l int
r int
}
memo := map[Key]map[int]bool{}
var dp func(int, int) map[int]bool
dp = func(left int, right int) map[int]bool {
key := Key{left, right}
if val, exists := memo[key]; exists {
return val
}
results := map[int]bool{}
if left == right {
results[nums[left]] = true
memo[key] = results
return results
}
for k := left; k < right; k++ {
leftValues := dp(left, k)
rightValues := dp(k+1, right)
for a := range leftValues {
for b := range rightValues {
value := 0
if ops[k] == '+' {
value = a + b
} else {
value = a * b
}
if value <= 1000 {
results[value] = true
}
}
}
}
memo[key] = results
return results
}
possible := dp(0, len(nums)-1)
score := 0
for _, ans := range answers {
if ans == correct {
score += 5
} else if possible[ans] {
score += 2
}
}
return score
}
The Go implementation follows the same algorithmic structure as the Python version.
The primary difference is memoization handling. Since Go does not provide a built in memoization decorator like Python's lru_cache, we explicitly store computed intervals inside a map keyed by (left, right).
Sets are represented using:
map[int]bool
because Go does not have a native set type.
All arithmetic safely fits within Go's standard integer range under the given constraints.
Worked Examples
Example 1
s = "7+3*1*2"
answers = [20,13,42]
Step 1: Parse Expression
| Component | Value |
|---|---|
| nums | [7, 3, 1, 2] |
| ops | ['+', '', ''] |
Step 2: Correct Evaluation
Compute multiplication first:
3*1*2 = 6
7+6 = 13
Correct answer:
13
Step 3: DP Computation
Base intervals
| Interval | Results |
|---|---|
| dp(0,0) | {7} |
| dp(1,1) | {3} |
| dp(2,2) | {1} |
| dp(3,3) | {2} |
Length 2 intervals
| Interval | Computation | Results |
|---|---|---|
| dp(0,1) | 7+3 | {10} |
| dp(1,2) | 3*1 | {3} |
| dp(2,3) | 1*2 | {2} |
Length 3 intervals
For dp(0,2):
Split at +:
7 + (3*1) = 10
Split at *:
(7+3) * 1 = 10
Results:
{10}
For dp(1,3):
3 * (1*2) = 6
(3*1) * 2 = 6
Results:
{6}
Full interval
For dp(0,3):
Split at first operator:
7 + 6 = 13
Split at second operator:
10 * 2 = 20
Split at third operator:
10 * 2 = 20
Final possible values:
{13, 20}
Step 4: Grade Answers
| Answer | Result | Points |
|---|---|---|
| 20 | Incorrect but possible | 2 |
| 13 | Correct | 5 |
| 42 | Impossible | 0 |
Total:
7
Example 2
s = "3+5*2"
answers = [13,0,10,13,13,16,16]
Correct Answer
3 + (5*2) = 13
Possible DP Results
| Parenthesization | Value |
|---|---|
| 3 + (5*2) | 13 |
| (3+5) * 2 | 16 |
Possible set:
{13, 16}
Grading
| Answer | Points |
|---|---|
| 13 | 5 |
| 0 | 0 |
| 10 | 0 |
| 13 | 5 |
| 13 | 5 |
| 16 | 2 |
| 16 | 2 |
Total:
19
Example 3
s = "6+0*1"
answers = [12,9,6,4,8,6]
Correct Answer
6 + (0*1) = 6
Alternative Parenthesization
(6+0) * 1 = 6
Even incorrect precedence still gives 6.
According to the rules, exact correctness always gets 5 points.
Grading
| Answer | Points |
|---|---|
| 12 | 0 |
| 9 | 0 |
| 6 | 5 |
| 4 | 0 |
| 8 | 0 |
| 6 | 5 |
Total:
10
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n³ × m²) | For each interval and split, combine left and right result sets |
| Space | O(n² × m) | Memoized result sets for all intervals |
Here:
nis the number of numbers in the expressionmis the maximum number of distinct values stored per interval
The number of intervals is:
O(n²)
For each interval, we try all split positions:
O(n)
For every split, we combine two result sets, which contributes:
O(m²)
In practice, pruning values greater than 1000 keeps m relatively small, making the solution efficient enough for the constraints.
Test Cases
from typing import List
class Solution:
def scoreOfStudents(self, s: str, answers: List[int]) -> int:
from functools import lru_cache
nums = []
ops = []
for i, ch in enumerate(s):
if i % 2 == 0:
nums.append(int(ch))
else:
ops.append(ch)
correct = 0
current = nums[0]
for i, op in enumerate(ops):
if op == '*':
current *= nums[i + 1]
else:
correct += current
current = nums[i + 1]
correct += current
@lru_cache(None)
def dp(l, r):
if l == r:
return {nums[l]}
results = set()
for k in range(l, r):
for a in dp(l, k):
for b in dp(k + 1, r):
if ops[k] == '+':
val = a + b
else:
val = a * b
if val <= 1000:
results.add(val)
return results
possible = dp(0, len(nums) - 1)
score = 0
for ans in answers:
if ans == correct:
score += 5
elif ans in possible:
score += 2
return score
sol = Solution()
assert sol.scoreOfStudents("7+3*1*2", [20,13,42]) == 7 # provided example 1
assert sol.scoreOfStudents(
"3+5*2",
[13,0,10,13,13,16,16]
) == 19 # provided example 2
assert sol.scoreOfStudents(
"6+0*1",
[12,9,6,4,8,6]
) == 10 # provided example 3
assert sol.scoreOfStudents("1+2", [3,2,1]) == 5 # smallest valid expression
assert sol.scoreOfStudents("2*3", [6,5,4]) == 5 # multiplication only
assert sol.scoreOfStudents("1+2+3", [6,7]) == 5 # only one possible result
assert sol.scoreOfStudents("2*3+4", [10,14]) == 7 # different parenthesizations
assert sol.scoreOfStudents("0*0+0", [0]) == 5 # many parenthesizations same answer
assert sol.scoreOfStudents("9*9*9", [729,1000]) == 5 # large multiplication
assert sol.scoreOfStudents(
"1+2*3+4",
[11,13,21]
) == 7 # mix of correct and incorrect answers
assert sol.scoreOfStudents(
"1+1+1+1+1",
[5,4,3]
) == 5 # repeated additions only
assert sol.scoreOfStudents(
"2*2*2*2",
[16,8]
) == 5 # repeated multiplications only
Test Summary
| Test | Why |
|---|---|
"7+3*1*2" |
Validates mixed operators and alternative parenthesizations |
"3+5*2" |
Validates incorrect precedence answers |
"6+0*1" |
Validates overlapping correct and incorrect results |
"1+2" |
Smallest valid expression |
"2*3" |
Pure multiplication |
"1+2+3" |
Pure addition |
"2*3+4" |
Mixed precedence behavior |
"0*0+0" |
Zero handling |
"9*9*9" |
Large multiplication result |
"1+2*3+4" |
Multiple valid alternative values |
"1+1+1+1+1" |
Many additions |
"2*2*2*2" |
Many multiplications |
Edge Cases
Edge Case 1: Incorrect Evaluation Equals Correct Evaluation
Consider:
6+0*1
Correct precedence gives:
6 + (0*1) = 6
Incorrect precedence may give:
(6+0) * 1 = 6
Both produce the same value.
A buggy implementation might incorrectly award only 2 points because the value appears in the alternative result set.
Our implementation avoids this by always checking:
if ans == correct:
before checking whether it belongs to the alternative set.
Edge Case 2: Duplicate Student Answers
Students may submit the same answer multiple times.
For example:
answers = [13,13,13]
Each student must be graded independently.
A buggy implementation using frequencies incorrectly could undercount or overcount points.
Our solution iterates through every answer individually and adds points independently.
Edge Case 3: Exploding Number of Intermediate Results
Expressions with many operators can generate many possible parenthesizations.
Without pruning, result sets could grow extremely large.
For example:
9*9*9+9*9
can generate many combinations.
The constraint that student answers are at most 1000 allows us to safely discard larger values immediately:
if value <= 1000:
This dramatically limits memory usage and runtime while preserving correctness.