Facebook Pixel

3707. Equal Score Substrings

EasyStringPrefix Sum
LeetCode ↗

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Prefix Sums?

This problem maps to Prefix Sums through a short path in the full flowchart.

Tree orgraph?noPrefixsums /range sum?yesPrefix Sums

Maintaining running left and right scores as a prefix sum lets each split point be checked in O(1) time.

Open in Flowchart

Intuition

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:

  1. Compute its value x = ord(c) - ord("a") + 1.
  2. Move the character from the right side to the left side:
    • l += x — the prefix score grows.
    • r -= x — the suffix score shrinks.
  3. Check the balance condition: if l == r, a valid split exists at the current position, so return True immediately.
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, where n is the length of s.
  • Space complexity: O(1) — only two integer variables l and r are 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.

StepCharValue xl (after l += x)r (after r -= x)l == r?Split
1'd'446No"d" | "acb"
2'a'155Yes"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
23
1class 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}
35
1class 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};
33
1/**
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}
38

Time and Space Complexity

  • Time complexity: O(n), where n is the length of the string s. The code first computes the total score r by iterating over all characters once, which takes O(n). It then iterates over s[:-1] a second time, performing constant-time operations (updating l and r, and comparing them) at each step, which also takes O(n). Therefore, the overall time complexity is O(n) + O(n) = O(n).

  • Space complexity: O(1). The code only uses a fixed number of integer variables (l, r, and x), regardless of the input size. The generator expression inside sum(...) does not build an intermediate list, so no extra space proportional to n is 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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which of the following is a min heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More