HASHING › HASHING WITH PREFIX SUM
Open — the attempt gate is not wired up yet
This editorial is meant to unlock after you have run the problem at least once, with the worked solution behind one further deliberate click. That needs per-learner unlock state nothing stores today, so for now the whole article is open.
Try it yourself first →Give each vowel a value of +1 and each consonant a value of -1. For a substring, the sum of these values is zero exactly when its vowel count equals its consonant count. If both counts are x, the substring has length 2x and its product condition becomes x squared divisible by k.
The useful number-theoretic fact is that there is a smallest positive integer d such that d squared is divisible by k, and every valid x is a multiple of d. Therefore a substring is beautiful exactly when its prefix-difference sum is zero and its length is divisible by 2d.
Let prefix[i] be the difference after the first i characters. A substring from i to j has equal vowel and consonant counts when prefix[i] equals prefix[j]. Its length is j - i, so requiring divisibility by 2d is the same as requiring i and j to have the same remainder modulo 2d. Each prefix position can therefore be grouped by two facts: its difference and its position remainder.
| MEASURE | BOUND | WHY |
|---|---|---|
| Time | O(n + sqrt(k)) | Factoring k tries divisors up to sqrt(k), and the string scan performs one constant-time map lookup and insertion per character. No substring is examined more than once because each pair is counted when its later prefix position is processed. |
| Space | O(n) extra | The map stores at most one entry for each scanned prefix position. The returned count is required output and is excluded from working space; the extra bound remains O(n) in the worst case when all prefix keys are distinct. |
#include <string>
#include <unordered_map>
using namespace std;
class Solution {
public:
long long beautifulSubstrings(string s, int k) {
int d = 1;
int value = k;
for (int prime = 2; prime * prime <= value; ++prime) {
if (value % prime != 0) continue;
int exponent = 0;
while (value % prime == 0) {
value /= prime;
++exponent;
}
for (int power = 0; power < (exponent + 1) / 2; ++power)
d *= prime;
}
if (value > 1)
d *= value;
int mod = 2 * d;
unordered_map<long long, long long> frequency;
long long answer = 0;
int difference = 0;
auto keyOf = [n = static_cast<int>(s.size()), mod](int diff, int position) {
return static_cast<long long>(diff + n) * mod + position % mod;
};
frequency[keyOf(0, 0)] = 1;
for (int i = 0; i < static_cast<int>(s.size()); ++i) {
char c = s[i];
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u')
++difference;
else
--difference;
int position = i + 1;
long long key = keyOf(difference, position);
answer += frequency[key];
++frequency[key];
}
return answer;
}
};The factorization computes d prime by prime. If k contains a prime p to exponent e, x squared contains p to exponent 2 times the exponent of p in x, so x needs at least ceiling of e divided by 2 copies of p. The key packs the difference and the position remainder into one integer; adding n shifts every possible difference into a non-negative range before packing.