2262. Total Appeal of A String
Problem Description
The appeal of a string is defined as the number of distinct characters found in that string.
For example:
- The appeal of
"abbca"
is3
because it contains 3 distinct characters:'a'
,'b'
, and'c'
.
Given a string s
, you need to find the total appeal of all of its substrings.
A substring is a contiguous sequence of characters within a string. This means you need to:
- Find all possible substrings of the given string
- Calculate the appeal (number of distinct characters) for each substring
- Return the sum of all these appeal values
For instance, if s = "abc"
, the substrings would be:
"a"
with appeal = 1"b"
with appeal = 1"c"
with appeal = 1"ab"
with appeal = 2"bc"
with appeal = 2"abc"
with appeal = 3
The total appeal sum would be 1 + 1 + 1 + 2 + 2 + 3 = 10
.
The challenge is to efficiently calculate this sum for potentially long strings, as the naive approach of generating all substrings and counting distinct characters for each would be computationally expensive.
Intuition
Instead of calculating the appeal for each substring individually (which would be inefficient), let's think about how the total appeal changes as we build substrings incrementally.
Consider what happens when we process each character position i
in the string. At position i
, we're looking at all substrings that end at this position. These substrings are:
s[0...i]
s[1...i]
s[2...i]
- ...
s[i]
(just the character itself)
The key insight is that when we move from position i-1
to position i
, we're essentially adding character s[i]
to the end of all substrings that ended at position i-1
, plus creating a new substring with just s[i]
itself.
Now, how does adding s[i]
affect the appeal of these substrings?
Case 1: Character s[i]
hasn't appeared before
- Every substring ending at
i-1
gets its appeal increased by 1 (since we're adding a new distinct character) - There are exactly
i
such substrings (starting from positions 0, 1, 2, ..., i-1) - Plus the single character substring
s[i]
itself has appeal 1 - So the total contribution increases by
i + 1
Case 2: Character s[i]
has appeared before at position j
- Substrings that already contain this character (those starting at or before position
j
) don't get their appeal increased - Only substrings starting after position
j
will see their appeal increase by 1 - There are
i - j - 1
such substrings (starting from positions j+1, j+2, ..., i-1) - Plus the single character substring
s[i]
itself has appeal 1 - So the total contribution increases by
i - j
This leads us to maintain:
- A running sum
t
that tracks the total appeal of all substrings ending at the current position - An array
pos
that stores the last seen position of each character (initialized to -1)
At each position i
, we update t
by adding i - pos[c]
where c
is the current character. This formula works for both cases above (when pos[c] = -1
for unseen characters, we get i - (-1) = i + 1
).
Learn more about Dynamic Programming patterns.
Solution Approach
Based on our intuition, we'll implement a solution that processes each character position and maintains the cumulative appeal sum efficiently.
Data Structures:
ans
: Accumulates the total appeal sum of all substringst
: Tracks the sum of appeals for all substrings ending at the current positionpos
: An array of size 26 (for lowercase English letters) that stores the last occurrence position of each character, initialized to -1
Algorithm Steps:
-
Initialize variables:
ans = t = 0 pos = [-1] * 26 # Last position of each character ('a' to 'z')
-
Process each character in the string: For each position
i
with characters[i]
:a. Convert character to index:
c = ord(s[i]) - ord('a') # Maps 'a'->0, 'b'->1, ..., 'z'->25
b. Update the appeal sum for substrings ending at position i:
t += i - pos[c]
- If character hasn't appeared before (
pos[c] = -1
), this addsi + 1
- If character appeared at position
j
(pos[c] = j
), this addsi - j
- This represents the contribution of character
s[i]
to all substrings ending at positioni
c. Add current position's total to answer:
ans += t
This accumulates the appeal of all substrings ending at position
i
d. Update last occurrence position:
pos[c] = i
- If character hasn't appeared before (
-
Return the total appeal sum:
return ans
Example Walkthrough:
For s = "abc"
:
- Position 0 ('a'):
t = 0 + (0 - (-1)) = 1
,ans = 1
, updatepos[0] = 0
- Position 1 ('b'):
t = 1 + (1 - (-1)) = 3
,ans = 1 + 3 = 4
, updatepos[1] = 1
- Position 2 ('c'):
t = 3 + (2 - (-1)) = 6
,ans = 4 + 6 = 10
, updatepos[2] = 2
The final answer is 10, which matches our manual calculation from the problem description.
Time Complexity: O(n)
where n is the length of the string, as we process each character once.
Space Complexity: O(1)
as we use a fixed-size array of 26 elements regardless of input size.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "abbca"
to see how the algorithm efficiently calculates the total appeal.
Initial Setup:
ans = 0
(total appeal sum)t = 0
(appeal sum for substrings ending at current position)pos = [-1, -1, -1, ..., -1]
(26 elements, all initialized to -1)
Position 0: character 'a'
- Character index:
c = ord('a') - ord('a') = 0
- Update t:
t = 0 + (0 - (-1)) = 1
- This means substring "a" has appeal 1
- Update ans:
ans = 0 + 1 = 1
- Update pos:
pos[0] = 0
(mark 'a' seen at position 0)
Position 1: character 'b'
- Character index:
c = ord('b') - ord('a') = 1
- Update t:
t = 1 + (1 - (-1)) = 3
- Substrings ending here: "b" (appeal 1), "ab" (appeal 2)
- Total appeal = 1 + 2 = 3
- Update ans:
ans = 1 + 3 = 4
- Update pos:
pos[1] = 1
(mark 'b' seen at position 1)
Position 2: character 'b'
- Character index:
c = 1
- Update t:
t = 3 + (2 - 1) = 4
- 'b' was last seen at position 1
- Only substring "b" (starting after position 1) gets +1 appeal
- Substrings: "b" (appeal 1), "bb" (appeal 1), "abb" (appeal 2)
- Total appeal = 1 + 1 + 2 = 4
- Update ans:
ans = 4 + 4 = 8
- Update pos:
pos[1] = 2
(update 'b' position to 2)
Position 3: character 'c'
- Character index:
c = ord('c') - ord('a') = 2
- Update t:
t = 4 + (3 - (-1)) = 8
- 'c' hasn't appeared before, so all 4 substrings get +1 appeal
- Substrings: "c" (appeal 1), "bc" (appeal 2), "bbc" (appeal 2), "abbc" (appeal 3)
- Total appeal = 1 + 2 + 2 + 3 = 8
- Update ans:
ans = 8 + 8 = 16
- Update pos:
pos[2] = 3
(mark 'c' seen at position 3)
Position 4: character 'a'
- Character index:
c = 0
- Update t:
t = 8 + (4 - 0) = 12
- 'a' was last seen at position 0
- Substrings starting after position 0 get +1 appeal
- Substrings: "a" (appeal 1), "ca" (appeal 2), "bca" (appeal 3), "bbca" (appeal 3), "abbca" (appeal 3)
- Total appeal = 1 + 2 + 3 + 3 + 3 = 12
- Update ans:
ans = 16 + 12 = 28
- Update pos:
pos[0] = 4
(update 'a' position to 4)
Final Result: 28
The algorithm correctly computes that the total appeal of all substrings of "abbca" is 28, processing each character only once in O(n) time.
Solution Implementation
1class Solution:
2 def appealSum(self, s: str) -> int:
3 # Total sum of appeals across all substrings
4 total_appeal_sum = 0
5
6 # Current contribution to appeal from substrings ending at current position
7 current_contribution = 0
8
9 # Track the last seen position of each character (a-z)
10 # Initialize with -1 to indicate character hasn't been seen yet
11 last_position = [-1] * 26
12
13 # Iterate through each character in the string
14 for current_index, char in enumerate(s):
15 # Convert character to index (0-25 for a-z)
16 char_index = ord(char) - ord('a')
17
18 # Calculate new substrings that include current character
19 # These are all substrings from (last occurrence of char + 1) to current_index
20 # Each adds 1 to the appeal count if char wasn't in that substring before
21 current_contribution += current_index - last_position[char_index]
22
23 # Add current contribution to total
24 # This represents appeal sum of all substrings ending at current_index
25 total_appeal_sum += current_contribution
26
27 # Update last seen position of current character
28 last_position[char_index] = current_index
29
30 return total_appeal_sum
31
1class Solution {
2 public long appealSum(String s) {
3 // Total sum of appeal values for all substrings
4 long totalAppealSum = 0;
5
6 // Running sum of appeal values ending at current position
7 long currentAppealSum = 0;
8
9 // Array to store the last seen position of each character ('a' to 'z')
10 // Initialize all positions to -1 (character not seen yet)
11 int[] lastPositionOfChar = new int[26];
12 Arrays.fill(lastPositionOfChar, -1);
13
14 // Iterate through each character in the string
15 for (int currentIndex = 0; currentIndex < s.length(); ++currentIndex) {
16 // Convert character to index (0-25 for 'a'-'z')
17 int charIndex = s.charAt(currentIndex) - 'a';
18
19 // Calculate contribution of current character to appeal sum
20 // The number of new substrings that include this character
21 // equals (currentIndex - lastPositionOfChar[charIndex])
22 currentAppealSum += currentIndex - lastPositionOfChar[charIndex];
23
24 // Add current appeal sum to total
25 // This represents appeal values of all substrings ending at currentIndex
26 totalAppealSum += currentAppealSum;
27
28 // Update the last seen position of current character
29 lastPositionOfChar[charIndex] = currentIndex;
30 }
31
32 return totalAppealSum;
33 }
34}
35
1class Solution {
2public:
3 long long appealSum(string s) {
4 long long totalAppealSum = 0; // Sum of appeal values for all substrings
5 long long currentContribution = 0; // Contribution of substrings ending at current position
6
7 // Track the last occurrence position of each character ('a' to 'z')
8 // Initialize to -1 to indicate character hasn't appeared yet
9 vector<int> lastPosition(26, -1);
10
11 // Process each character in the string
12 for (int i = 0; i < s.size(); ++i) {
13 // Convert character to index (0-25 for 'a'-'z')
14 int charIndex = s[i] - 'a';
15
16 // Calculate new substrings containing current character
17 // All substrings from (lastPosition[charIndex] + 1) to i will have this character
18 // This adds (i - lastPosition[charIndex]) new unique character contributions
19 currentContribution += i - lastPosition[charIndex];
20
21 // Add current contribution to total sum
22 // currentContribution represents appeal sum of all substrings ending at position i
23 totalAppealSum += currentContribution;
24
25 // Update last occurrence position of current character
26 lastPosition[charIndex] = i;
27 }
28
29 return totalAppealSum;
30 }
31};
32
1/**
2 * Calculates the sum of appeal of all substrings of the given string.
3 * The appeal of a string is the number of distinct characters in it.
4 *
5 * @param s - The input string containing only lowercase English letters
6 * @returns The sum of appeal values of all substrings
7 */
8function appealSum(s: string): number {
9 // Array to track the last occurrence position of each character (a-z)
10 // Initialize all positions to -1 (character not yet seen)
11 const lastPositions: number[] = Array(26).fill(-1);
12
13 // Length of the input string
14 const stringLength: number = s.length;
15
16 // Total sum of appeal values across all substrings
17 let totalAppealSum: number = 0;
18
19 // Running sum of appeal values for substrings ending at current position
20 let currentContribution: number = 0;
21
22 // Iterate through each character in the string
23 for (let i = 0; i < stringLength; ++i) {
24 // Convert character to index (0-25 for a-z)
25 const charIndex: number = s.charCodeAt(i) - 97;
26
27 // Update contribution: number of substrings where this character adds to appeal
28 // This equals the number of positions between current and last occurrence
29 currentContribution += i - lastPositions[charIndex];
30
31 // Add current contribution to total sum
32 totalAppealSum += currentContribution;
33
34 // Update the last seen position for this character
35 lastPositions[charIndex] = i;
36 }
37
38 return totalAppealSum;
39}
40
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. The algorithm iterates through the string once with a single for loop, and within each iteration, it performs constant-time operations: calculating the character index, updating the running sum t
, adding to the answer ans
, and updating the position array pos[c]
.
The space complexity is O(|Σ|)
or equivalently O(26)
which simplifies to O(1)
, where |Σ|
represents the size of the character set. In this problem, the character set consists of lowercase English letters, so |Σ| = 26
. The algorithm uses a fixed-size array pos
of length 26 to track the last occurrence position of each character, regardless of the input string length.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Contribution Update Logic
Pitfall: Many developers struggle to understand why we use t += i - pos[c]
instead of simply counting distinct characters for each substring. The confusion often leads to incorrect implementations that try to maintain a set of characters or recalculate appeals from scratch.
Why it happens: The formula i - pos[c]
represents how many new substrings ending at position i
will have character c
contributing to their appeal. This is counterintuitive because we're not directly counting distinct characters.
Solution: Understand that when character c
appears at position i
:
- All substrings starting from positions
pos[c] + 1
toi
and ending ati
will havec
as a new distinct character - The count of such substrings is exactly
i - pos[c]
- These substrings inherit the appeal contributions from previous characters plus this new contribution
2. Incorrect Initialization of Last Position Array
Pitfall: Initializing the last_position
array with 0
instead of -1
:
# Wrong last_position = [0] * 26 # Correct last_position = [-1] * 26
Why it happens: Developers might think position indices start at 0, so they initialize with 0. However, this causes incorrect calculations for the first occurrence of each character.
Solution: Always initialize with -1
to indicate "not yet seen". This ensures that when a character appears for the first time at position i
, the contribution calculation i - (-1) = i + 1
correctly accounts for all i + 1
substrings that start from position 0 to i
and end at i
.
3. Forgetting to Accumulate the Running Sum
Pitfall: Directly adding the contribution to the final answer without maintaining the running sum:
# Wrong approach
for i, char in enumerate(s):
char_index = ord(char) - ord('a')
total_appeal_sum += i - last_position[char_index] # Missing accumulation
last_position[char_index] = i
Why it happens: The algorithm requires tracking two separate values: the cumulative contribution (current_contribution
) and the total appeal sum. The contribution at each position builds upon the previous position's contribution.
Solution: Maintain both variables:
current_contribution
: Accumulates the appeal sum for all substrings ending at the current positiontotal_appeal_sum
: Accumulates the total appeal across all substrings The relationship is:current_contribution
grows incrementally, and we add it tototal_appeal_sum
at each step.
4. Character Index Calculation Errors
Pitfall: Incorrect character-to-index conversion, especially when dealing with uppercase letters or mixed cases:
# Wrong for lowercase only
char_index = ord(char) - ord('A') # Uses 'A' instead of 'a'
# Wrong for mixed case without handling
char_index = ord(char) - ord('a') # Fails for uppercase letters
Why it happens: The problem typically specifies lowercase English letters, but developers might accidentally use the wrong base character or forget to handle case sensitivity if the problem allows mixed cases.
Solution:
- For lowercase only: Use
ord(char) - ord('a')
- For uppercase only: Use
ord(char) - ord('A')
- For mixed case: Convert to lowercase first with
char.lower()
or handle both cases separately with a larger position array (size 52)
5. Off-by-One Errors in Position Tracking
Pitfall: Confusion about whether to update the position before or after calculating the contribution:
# Wrong order last_position[char_index] = current_index # Updated too early current_contribution += current_index - last_position[char_index] # Now always 0!
Why it happens: The order of operations matters. We need the previous position to calculate the contribution before updating it.
Solution: Always follow this order:
- Calculate the contribution using the old position:
current_contribution += i - last_position[char_index]
- Add to total:
total_appeal_sum += current_contribution
- Update the position for next iteration:
last_position[char_index] = i
What's the relationship between a tree and a graph?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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 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
Want a Structured Path to Master System Design Too? Don’t Miss This!