Question: Count redundant substrings by vowel and consonant weights
Companies: Amazon
Difficulty: Medium
Problem statement
Write a function that counts the redundant substrings of a word.
A string s is redundant when its length equals a * V + b * C, where V is the number of vowels in s, C is the number of consonants in s, and a and b are given integers. The vowels are a, e, i, o, u; every other letter (including y) is a consonant.
def count_redundant_substrings(word: str, a: int, b: int) -> int: ...
Return how many of the n(n+1)/2 non-empty contiguous substrings of word are redundant. Substrings at different positions count separately, even if their text is the same.
Constraints
-
0 <= len(word) <= 10^5, lowercase English letters only. -
-10^5 <= a, b <= 10^5. - The answer can exceed 2^31.
Example 1
word = "code", a = 2, b = 0 -> 4
The rule becomes V + C = 2V, so V == C. The redundant substrings are "co", "od", "de" and "code".
Example 2
word = "queue", a = 1, b = 0 -> 10
The rule becomes V + C = V, so C == 0. Only substrings made entirely of vowels count, and "ueue" has 10 of them.
An O(n^2) solution is too slow for the largest inputs; aim for O(n).
Approach
A substring has length V + C, where V counts its vowels and C counts its consonants. It is redundant when
V + C = a*V + b*C
Move everything to one side:
(1 - a)*V + (1 - b)*C = 0
So give every character a weight: a vowel weighs 1 - a, a consonant weighs 1 - b. A substring is redundant exactly when the sum of its weights is zero. The question becomes "count the subarrays with sum zero", a standard prefix-sum problem.
Brute force. Try every start i. Extend the end j one character at a time and keep running counts of vowels and consonants. Check j - i + 1 == a*V + b*C at each step. That is O(n^2) time and O(1) space. It is correct and worth saying first, but with n up to about 10^5 it times out.
Optimal: prefix sums and a hash map.
- Let
P[0] = 0andP[k] = weight(word[0]) + ... + weight(word[k-1]). - The substring
word[i:j]has weightP[j] - P[i]. It is redundant whenP[j] == P[i]withi < j. - Walk left to right. Keep a counter of how many times each prefix value has appeared so far. At each new prefix value
p, every earlier equal prefix closes one redundant substring, so addseen[p]to the answer, then incrementseen[p].
Each pair i < j is counted exactly once, when the scan reaches j. Seeding seen[0] = 1 (for P[0]) counts substrings that start at index 0.
Why it is correct. The equation is linear in V and C, and both counts are additive over concatenation. That is what lets one integer per prefix stand in for the pair (V, C). If the condition were not linear (for example V * C == length), this trick would not apply.
Special cases fall out for free.
-
a = 1andb = 1: both weights are 0, every prefix is 0, and the answer isn(n+1)/2. Every substring is redundant. -
a = 1only: vowels weigh 0, so a substring is redundant exactly when it has no consonants. - Weights can be negative or zero; the hash map does not care.
Invariants.
- After processing
kcharacters,prefix == P[k]andseen[x]is the number of indicest <= kwithP[t] == x. -
countequals the number of redundant substrings that end at or before indexk - 1.
Code
from collections import defaultdict
from typing import Dict
VOWELS = frozenset("aeiou")
def count_redundant_substrings(word: str, a: int, b: int) -> int:
"""Count substrings s of word with len(s) == a * vowels(s) + b * consonants(s).
word holds lowercase English letters. 'y' counts as a consonant.
"""
vowel_weight = 1 - a
consonant_weight = 1 - b
seen: Dict[int, int] = defaultdict(int)
seen[0] = 1 # the empty prefix P[0]
prefix = 0
count = 0
for ch in word:
prefix += vowel_weight if ch in VOWELS else consonant_weight
count += seen[prefix]
seen[prefix] += 1
return count
def count_redundant_brute(word: str, a: int, b: int) -> int:
"""O(n^2) reference: extend every start and test the length rule."""
n = len(word)
total = 0
for i in range(n):
v = c = 0
for j in range(i, n):
if word[j] in VOWELS:
v += 1
else:
c += 1
if j - i + 1 == a * v + b * c:
total += 1
return total
if __name__ == "__main__":
import random
# Equal vowels and consonants: "co", "od", "de", "code".
assert count_redundant_substrings("code", 2, 0) == 4
# a = b = 1: every substring counts.
assert count_redundant_substrings("rhythm", 1, 1) == 21
# a = 1, b = 0: substrings made only of vowels.
assert count_redundant_substrings("queue", 1, 0) == 10
assert count_redundant_substrings("", 3, -2) == 0
assert count_redundant_substrings("z", 0, 1) == 1
assert count_redundant_substrings("z", 0, 0) == 0
rng = random.Random(7)
for _ in range(2000):
w = "".join(rng.choice("aeioubcdxyz") for _ in range(rng.randint(0, 12)))
a, b = rng.randint(-3, 3), rng.randint(-3, 3)
assert count_redundant_substrings(w, a, b) == count_redundant_brute(w, a, b)
print("all tests passed")
Complexity
-
Optimal: O(n) time for one pass with O(1) hash map operations. O(n) space in the worst case for the map of distinct prefix values. When
|1 - a|and|1 - b|are small, prefix values lie in[-n*w, n*w]and a plain array offset byn*wcan replace the hash map. - Brute force: O(n^2) time, O(1) space.
Test cases
-
word = "code", a = 2, b = 0→4. Weights are -1 for vowels and +1 for consonants, so a substring counts when its vowels equal its consonants:"co","od","de","code". -
word = "rhythm", a = 1, b = 1→21. All weights are 0; every one of the 6*7/2 substrings counts. -
word = "queue", a = 1, b = 0→10. Vowels weigh 0 and consonants weigh 1, so only all-vowel substrings count. The vowel run"ueue"has length 4, giving 4*5/2 = 10. -
word = "", a = 3, b = -2→0. -
word = "z", a = 0, b = 1→1. The consonant weighs 0, so the single-letter substring counts. -
word = "z", a = 0, b = 0→0. Length 1 never equals 0. -
word = "aab", a = 3, b = -1→1. Vowels weigh -2, consonants weigh 2; only"ab"sums to 0. - Large input,
n = 10^5of one letter witha = b = 1→5000050000. Checks that the count does not overflow in languages with 32-bit ints.
Follow-ups
-
Return the substrings, or the longest one. Store the first index of each prefix value instead of a count. The longest redundant substring ending at
jstarts atfirst[P[j]]. Listing all of them is O(n + output) by keeping each prefix value's index list. -
Answer many
(a, b)queries on the same word. Each query needs its own weights, so it is O(n) per query. If the queries shareaandbonly through the ratio(1 - a) : (1 - b), scale the weights by their gcd and reuse one pass per distinct ratio. -
Streaming input. The algorithm is already one pass. Keep
prefix,countandseenas state and reportcountafter each character. Memory stays O(distinct prefixes). -
Generalize the rule to
length = a*V + b*C + k. Rewrite it assum(weights) = k. Look upseen[prefix - k]instead ofseen[prefix], the same trick as "subarray sum equals k". -
Count only substrings of length at least
L. Delay insertingP[t]intoseenuntil the scan reaches indext + L, so only starts far enough back are counted.
Interviewer expectations
Mid-level (L4)
- Writes the O(n^2) brute force correctly, then finds the algebra that turns the rule into a zero-sum condition.
- Implements the prefix-sum counter with the
seen[0] = 1seed and explains why it is needed. - Tests the empty string and the
a = b = 1case.
Senior (L5)
- Goes straight to the reduction and states why linearity makes it valid.
- Handles negative and zero weights and calls out the 64-bit count for large inputs.
- Adapts quickly to "sum equals k" and "longest substring" variants.
Staff (L6+)
- Frames the problem as a family (weighted zero-sum subarrays) and says which rule changes would break the reduction.
- Discusses array versus hash map storage, memory for streaming input, and batching many
(a, b)queries. - Writes a brute-force cross-check or property test without being asked.