Skip to content

Commit 237b99e

Browse files
committed
feat(algorithms, dynamic-programming): appeal of a string
1 parent 13e626e commit 237b99e

File tree

3 files changed

+181
-0
lines changed

3 files changed

+181
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Total Appeal of A String
2+
3+
The appeal of a string is the number of distinct characters found in the string.
4+
5+
- For example, the appeal of "abbca" is 3 because it has 3 distinct characters: 'a', 'b', and 'c'.
6+
7+
Given a string s, return the total appeal of all of its substrings.
8+
9+
A substring is a contiguous sequence of characters within a string.
10+
11+
## Examples
12+
13+
Example 1:
14+
```text
15+
Input: s = "abbca"
16+
Output: 28
17+
Explanation: The following are the substrings of "abbca":
18+
- Substrings of length 1: "a", "b", "b", "c", "a" have an appeal of 1, 1, 1, 1, and 1 respectively. The sum is 5.
19+
- Substrings of length 2: "ab", "bb", "bc", "ca" have an appeal of 2, 1, 2, and 2 respectively. The sum is 7.
20+
- Substrings of length 3: "abb", "bbc", "bca" have an appeal of 2, 2, and 3 respectively. The sum is 7.
21+
- Substrings of length 4: "abbc", "bbca" have an appeal of 3 and 3 respectively. The sum is 6.
22+
- Substrings of length 5: "abbca" has an appeal of 3. The sum is 3.
23+
The total sum is 5 + 7 + 7 + 6 + 3 = 28.
24+
```
25+
26+
Example 2:
27+
```text
28+
Input: s = "code"
29+
Output: 20
30+
Explanation: The following are the substrings of "code":
31+
- Substrings of length 1: "c", "o", "d", "e" have an appeal of 1, 1, 1, and 1 respectively. The sum is 4.
32+
- Substrings of length 2: "co", "od", "de" have an appeal of 2, 2, and 2 respectively. The sum is 6.
33+
- Substrings of length 3: "cod", "ode" have an appeal of 3 and 3 respectively. The sum is 6.
34+
- Substrings of length 4: "code" has an appeal of 4. The sum is 4.
35+
The total sum is 4 + 6 + 6 + 4 = 20.
36+
```
37+
38+
## Constraints
39+
40+
- 1 <= s.length <= 10^5
41+
- s consists of lowercase English letters.
42+
43+
## Topics
44+
45+
- Hash Table
46+
- String
47+
- Dynamic Programming
48+
49+
## Hints
50+
51+
- Consider the set of substrings that end at a certain index i. Then, consider a specific alphabetic character. How do
52+
you count the number of substrings ending at index i that contain that character?
53+
- The number of substrings that contain the alphabetic character is equivalent to 1 plus the index of the last occurrence
54+
of the character before index i + 1.
55+
- The total appeal of all substrings ending at index i is the total sum of the number of substrings that contain each
56+
alphabetic character.
57+
- To find the total appeal of all substrings, we simply sum up the total appeal for each index.
58+
59+
## Solution
60+
61+
The key intuition for solving this problem efficiently is to focus on the contribution of each character to the total
62+
appeal, rather than processing all possible substrings. In any substring, each character contributes only once to the
63+
total appeal, regardless of how many times it appears. Therefore, we can consider only the first occurrence of each
64+
character in any substring as contributing to the total appeal. To achieve this, we keep track of the last index of each
65+
character, which helps us identify when a character appears again, allowing us to avoid counting it multiple times.
66+
67+
To implement this, we calculate the following two values for each character c in the string:
68+
69+
- The number of substrings that end at c. This is calculated by finding the distance from the current index to the last
70+
occurrence of c.
71+
- The number of substrings that start from c and extend to the end of the string. This is simply the total length of the
72+
string minus the current index.
73+
74+
The product of these two values gives us the contribution of c to the total appeal. The first value counts the substrings
75+
containing an occurrence of c, and each of these substrings can be concatenated with the substrings formed using the rest
76+
of the characters to create new substrings.
77+
78+
Using the above intuition, the solution can be implemented as follows:
79+
1. Create a hash map, track, to store the last index where each character appeared in the string. It's initialized to −1
80+
for characters that haven't been seen yet. The maximum number of values that this hash map will store is 26.
81+
2. Create a variable, appeal, to accumulate the total appeal sum.
82+
3. For each character c at index i in the string, calculate the contribution of c to the total appeal.
83+
- Determine how many new substrings can be formed that end with the current character c. We do this using i - track[c].
84+
- If track[c] is −1 (meaning c hasn't appeared before), then i - track[c] is simply i+1 (all substrings from the
85+
start up to index i).
86+
- If track[c] is a valid index, this expression gives the count of substrings that include the current position but
87+
exclude previous occurrences of c.
88+
- Calculate how many substrings can start from the current index i to the end of the string, which can be calculated
89+
using n - i.
90+
- The product of these two values gives the total number of substrings contributed by the current character c. Add
91+
the result of this product to appeal.
92+
4. After calculating the contribution, update the track dictionary to record that the last occurrence of character c is
93+
now at index i.
94+
5. After processing all characters, return the accumulated result appeal, which is the total appeal sum.
95+
96+
### Time Complexity
97+
98+
The algorithm’s time complexity is O(n), where n is the length of the input string s.
99+
100+
### Space Complexity
101+
102+
The algorithm’s space complexity is O(1), because the maximum number of values that can be stored in the hash map is 26
103+
equivalent to the lowercase English letters.
104+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from collections import defaultdict
2+
3+
4+
def appeal_sum_dp(s: str) -> int:
5+
# Total sum of appeals across all substrings
6+
total_appeal_sum = 0
7+
# Current contribution to appeal from substrings ending at current position
8+
current_contribution = 0
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+
for idx, char in enumerate(s):
14+
# Convert character to index (0-25 for a-z)
15+
char_index = ord(char) - ord('a')
16+
# Calculate new substrings that include current character
17+
# These are all substrings from (last occurrence of char + 1) to current_index
18+
# Each adds 1 to the appeal count if char wasn't in that substring before
19+
current_contribution += idx - last_position[char_index]
20+
# Add current contribution to total
21+
# This represents appeal sum of all substrings ending at current_index
22+
total_appeal_sum += current_contribution
23+
24+
# Update last seen position of current character
25+
last_position[char_index] = idx
26+
27+
return total_appeal_sum
28+
29+
def appeal_sum_hash_table(s: str) -> int:
30+
# Create a hash map to track the last occurrence index of each character
31+
track = defaultdict(lambda: -1)
32+
33+
# Initialize result variable to accumulate the total appeal sum
34+
appeal = 0
35+
36+
# Get the length of the string
37+
n = len(s)
38+
39+
# Iterate through each character in the string with its index
40+
for i, c in enumerate(s):
41+
# Calculate the contribution of the current character to the appeal sum
42+
# (i - track[c]) gives the number of substrings ending with c
43+
# (n - i) gives the number of substrings starting from index i to the end of the string
44+
appeal += (i - track[c]) * (n - i)
45+
46+
# Update the last occurrence index of the c to the current index
47+
track[c] = i
48+
49+
# Return the total appeal sum
50+
return appeal
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import unittest
2+
from parameterized import parameterized
3+
from algorithms.dynamic_programming.total_appeal_of_a_string import appeal_sum_dp, appeal_sum_hash_table
4+
5+
APPEAL_SUM_TEST_CASES = [
6+
("abbca", 28),
7+
("code", 20),
8+
("asd", 10),
9+
("bbb", 6),
10+
("q", 1),
11+
("madam", 30),
12+
("hippopotamus", 279),
13+
]
14+
15+
class AppealSumTestCase(unittest.TestCase):
16+
@parameterized.expand(APPEAL_SUM_TEST_CASES)
17+
def test_appeal_sum_dp(self, s: str, expected: int):
18+
actual = appeal_sum_dp(s)
19+
self.assertEqual(expected, actual)
20+
21+
@parameterized.expand(APPEAL_SUM_TEST_CASES)
22+
def test_appeal_sum_hash_table(self, s: str, expected: int):
23+
actual = appeal_sum_hash_table(s)
24+
self.assertEqual(expected, actual)
25+
26+
if __name__ == '__main__':
27+
unittest.main()

0 commit comments

Comments
 (0)