3707. Equal Score Substrings
Problem Description
You are given a string s consisting of lowercase English letters.
The score of a string is the sum of the positions of its characters in the alphabet, where 'a' = 1, 'b' = 2, ..., 'z' = 26. For example, the score of "abc" is 1 + 2 + 3 = 6.
Your task is to determine whether there exists an index i such that the string can be split into two non-empty substrings s[0..i] and s[(i + 1)..(n - 1)] whose scores are equal.
Return true if such a split exists, otherwise return false.
In other words, you need to check if there is a cutting point in the string where the total score of the left part exactly matches the total score of the right part. Both parts must contain at least one character, so the split cannot happen before the first character or after the last one.
How We Pick the Algorithm
Why Prefix Sums?
This problem maps to Prefix Sums through a short path in the full flowchart.
Maintaining running left and right scores as a prefix sum lets each split point be checked in O(1) time.
Open in FlowchartIntuition
A brute-force way to solve this would be: for every possible split point i, compute the score of the left part s[0..i] and the score of the right part s[(i+1)..(n-1)], then compare them. However, recomputing both sums from scratch at every split point wastes a lot of work, since adjacent split points share almost all of their characters.
The key observation is that the total score of the string never changes. If we call the total score total, then at any split point the left score l and the right score r always satisfy l + r = total.
This means we don't need two separate computations. We can compute total once upfront, then scan the string from left to right. As we pass each character, its value simply moves from the right side to the left side: we add it to l and subtract it from r. After each move, checking whether l == r takes constant time.
This is essentially the prefix sum idea: the left score is a running prefix sum, and the right score is just total - prefix. Each split point is checked in O(1) time, giving an overall O(n) solution with a single pass.
One small detail: we only iterate over the first n - 1 characters, because both parts must be non-empty — placing the last character on the left side would leave the right side empty, which is not a valid split.
Pattern Learn more about Prefix Sum patterns.
Solution Approach
The solution uses the Prefix Sum pattern with two running variables — no extra data structures are needed.
Step 1: Compute the total score
First, calculate the score of the entire string and store it in r, which initially represents the suffix score (the right part covering the whole string):
r = sum(ord(c) - ord("a") + 1 for c in s)
Here, ord(c) - ord("a") + 1 converts a character to its alphabet position: 'a' becomes 1, 'b' becomes 2, and so on up to 'z' which becomes 26.
We also initialize l = 0, representing the prefix score (the left part, which starts empty).
Step 2: Scan the split points
Traverse the first n - 1 characters from left to right using s[:-1]. We deliberately skip the last character because including it on the left side would leave the right side empty, violating the non-empty requirement.
For each character c:
- Compute its value
x = ord(c) - ord("a") + 1. - Move the character from the right side to the left side:
l += x— the prefix score grows.r -= x— the suffix score shrinks.
- Check the balance condition: if
l == r, a valid split exists at the current position, so returnTrueimmediately.
for c in s[:-1]:
x = ord(c) - ord("a") + 1
l += x
r -= x
if l == r:
return True
Step 3: No valid split found
If the loop finishes without l ever equaling r, no split point satisfies the condition, so return False.
Why this works
The invariant l + r = total holds at every step, since each character's value is transferred from r to l exactly once. Checking l == r at split point i is therefore equivalent to checking whether the prefix score equals total / 2 — but by maintaining both variables explicitly, we avoid any concerns about whether total is even and keep the logic straightforward.
Complexity Analysis
- Time complexity:
O(n)— one pass to compute the total score, and one pass to check all split points, wherenis the length ofs. - Space complexity:
O(1)— only two integer variableslandrare maintained, regardless of input size.
Example Walkthrough
Let's trace the algorithm with s = "dacb".
Setup — compute the total score:
The alphabet positions are: 'd' = 4, 'a' = 1, 'c' = 3, 'b' = 2.
So the total score is 4 + 1 + 3 + 2 = 10. We initialize:
l = 0(left/prefix score, starts empty)r = 10(right/suffix score, starts as the whole string)
Scanning split points:
We iterate over s[:-1] = "dac" — the last character 'b' is excluded, since putting it on the left would leave the right side empty.
| Step | Char | Value x | l (after l += x) | r (after r -= x) | l == r? | Split |
|---|---|---|---|---|---|---|
| 1 | 'd' | 4 | 4 | 6 | No | "d" | "acb" |
| 2 | 'a' | 1 | 5 | 5 | Yes ✅ | "da" | "cb" |
At step 2, the character 'a' moves from the right side to the left side, and we find l == r == 5. Verifying directly:
- Left part
"da"→4 + 1 = 5 - Right part
"cb"→3 + 2 = 5
The scores match, so the function returns True immediately — without even examining the third split point.
Notice the invariant l + r = 10 holds at every step (4 + 6, then 5 + 5), confirming that each character's value was transferred from r to l exactly once.
A quick negative case: for s = "ab", the total is 1 + 2 = 3. The only split point gives l = 1, r = 2, which never balances — and indeed an odd total can never be split evenly — so the function returns False after the loop ends.
Solution Implementation
1class Solution:
2 def scoreBalance(self, s: str) -> bool:
3 # left_score: cumulative score of the prefix processed so far
4 left_score = 0
5 # right_score: total score of the string, will shrink as characters
6 # move from the right part to the left part
7 right_score = sum(ord(ch) - ord("a") + 1 for ch in s)
8
9 # Iterate over every possible split point; exclude the last character
10 # so that the right part is never empty
11 for ch in s[:-1]:
12 # Score of the current character (a=1, b=2, ..., z=26)
13 char_score = ord(ch) - ord("a") + 1
14 # Move the current character from the right part to the left part
15 left_score += char_score
16 right_score -= char_score
17 # Check whether both parts have equal scores
18 if left_score == right_score:
19 return True
20
21 # No valid split point found
22 return False
231class Solution {
2 public boolean scoreBalance(String s) {
3 int length = s.length();
4
5 // leftScore: sum of character scores on the left part
6 // rightScore: sum of character scores on the right part
7 int leftScore = 0;
8 int rightScore = 0;
9
10 // Initially, treat the entire string as the right part:
11 // compute the total score, where 'a' = 1, 'b' = 2, ..., 'z' = 26
12 for (int i = 0; i < length; ++i) {
13 int charScore = s.charAt(i) - 'a' + 1;
14 rightScore += charScore;
15 }
16
17 // Try every possible split point.
18 // Move characters one by one from the right part to the left part.
19 // The loop stops at length - 1 so that the right part is never empty.
20 for (int i = 0; i < length - 1; ++i) {
21 int charScore = s.charAt(i) - 'a' + 1;
22 leftScore += charScore;
23 rightScore -= charScore;
24
25 // If both parts have equal scores, the string can be balanced
26 if (leftScore == rightScore) {
27 return true;
28 }
29 }
30
31 // No valid split point found
32 return false;
33 }
34}
351class Solution {
2public:
3 bool scoreBalance(string s) {
4 // leftScore: sum of scores of the prefix processed so far
5 // rightScore: sum of scores of the remaining suffix
6 int leftScore = 0, rightScore = 0;
7
8 // Compute the total score of the string first,
9 // treating it initially as the entire "right" part
10 for (char ch : s) {
11 int score = ch - 'a' + 1;
12 rightScore += score;
13 }
14
15 // Try every possible split point i, where the prefix is s[0..i]
16 // and the suffix is s[i+1..n-1]. Both parts must be non-empty,
17 // so the split index goes up to s.size() - 2.
18 for (int i = 0; i + 1 < (int) s.size(); ++i) {
19 int score = s[i] - 'a' + 1;
20 // Move the current character's score from the right part to the left part
21 leftScore += score;
22 rightScore -= score;
23 // Check whether the two parts have equal scores
24 if (leftScore == rightScore) {
25 return true;
26 }
27 }
28
29 // No valid split point found
30 return false;
31 }
32};
331/**
2 * Determines whether the string can be split into two non-empty parts
3 * such that the sum of the character values (a = 1, b = 2, ..., z = 26)
4 * of the left part equals that of the right part.
5 *
6 * @param s - The input string consisting of lowercase English letters.
7 * @returns `true` if such a balanced split exists, otherwise `false`.
8 */
9function scoreBalance(s: string): boolean {
10 // `leftSum` tracks the score of the left part of the split.
11 // `rightSum` initially holds the total score of the entire string.
12 let leftSum: number = 0;
13 let rightSum: number = 0;
14
15 // Compute the total score of the string.
16 // Each character's value is its position in the alphabet (a = 1, ..., z = 26).
17 for (const ch of s) {
18 rightSum += ch.charCodeAt(0) - 96;
19 }
20
21 // Try every split point, moving one character at a time from the
22 // right part to the left part. The split must keep both parts
23 // non-empty, hence iterating up to `s.length - 1`.
24 for (let i = 0; i < s.length - 1; ++i) {
25 const value: number = s[i].charCodeAt(0) - 96;
26 leftSum += value;
27 rightSum -= value;
28
29 // A balanced split is found when both parts have equal scores.
30 if (leftSum === rightSum) {
31 return true;
32 }
33 }
34
35 // No valid split point produces equal scores.
36 return false;
37}
38Time and Space Complexity
-
Time complexity:
O(n), wherenis the length of the strings. The code first computes the total scorerby iterating over all characters once, which takesO(n). It then iterates overs[:-1]a second time, performing constant-time operations (updatinglandr, and comparing them) at each step, which also takesO(n). Therefore, the overall time complexity isO(n) + O(n) = O(n). -
Space complexity:
O(1). The code only uses a fixed number of integer variables (l,r, andx), regardless of the input size. The generator expression insidesum(...)does not build an intermediate list, so no extra space proportional tonis consumed. Hence, the extra space used is constant.
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Allowing an empty right part by iterating over the entire string
A very common mistake is to loop over all characters of s instead of stopping one character early:
# WRONG
for ch in s: # iterates over every character
char_score = ord(ch) - ord("a") + 1
left_score += char_score
right_score -= char_score
if left_score == right_score:
return True
When the loop reaches the last character, the entire string has been moved to the left side, leaving the right side empty (right_score == 0). If the string happens to have a total score of 0 on the right at that moment — which always occurs at the final iteration — the check left_score == right_score can never be true there unless the total is 0, but more subtle bugs appear with variations of this pattern (e.g., comparing against total // 2, where left_score == total and right_score == 0 would incorrectly pass for total == 0-style edge logic). More importantly, the split s[0..n-1] | "" is simply not a valid split per the problem statement: both parts must be non-empty.
Solution: Iterate only over s[:-1] (or use index range range(len(s) - 1)), guaranteeing the right part always retains at least one character:
# CORRECT for ch in s[:-1]: ...
This also gracefully handles a single-character string: s[:-1] is empty, the loop body never runs, and the function correctly returns False since no valid split can exist.
Pitfall 2: Using 0-based character values ('a' = 0)
It is tempting to write the familiar ord(ch) - ord("a") and forget the + 1:
# WRONG
char_score = ord(ch) - ord("a") # 'a' = 0, 'b' = 1, ...
This is not a harmless re-scaling — it changes the answer. With 0-based values, every character contributes one less than it should, so a left part of length L loses L points and a right part of length R loses R points. The two sides lose different amounts whenever L != R, so the equality check compares the wrong quantities. For example, s = "aab": correct scores allow the split "aa" | "b" (1+1 == 2), but with 0-based values the comparison becomes 0+0 == 1, returning False incorrectly.
Solution: Always map characters with ord(ch) - ord("a") + 1 so that 'a' = 1 through 'z' = 26, exactly as the problem defines.
Pitfall 3: Checking the balance condition before updating the scores
Another ordering bug is testing left_score == right_score before moving the current character to the left side:
# WRONG
for ch in s[:-1]:
if left_score == right_score: # checked too early
return True
char_score = ord(ch) - ord("a") + 1
left_score += char_score
right_score -= char_score
On the very first iteration this compares an empty left part (left_score = 0) against the full string — an invalid split. Simultaneously, it never evaluates the last legitimate split point (left part = s[0..n-2]), because the loop exits before that comparison happens. Both a false positive and a missed valid answer are possible.
Solution: Update left_score and right_score first, then perform the equality check, ensuring every comparison corresponds to a real split with two non-empty parts.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapWhich of the following is a min heap? 
Recommended Readings
Prefix Sum Technique Explained Prefix Sum A prefix sum transforms an array into a new array of running totals For an input array arr define prefix k as the sum of all elements before index k prefix 0 0 prefix 1 arr 0 prefix 2 arr 0 arr 1 and so on The power
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!