AlgoMaster Logo

Number of Distinct Substrings in a String

mediumFrequencyUpdated September 21, 2026

Understanding the Problem

We need to count how many unique substrings exist in the given string. A substring is any contiguous portion of the string, and we only want to count each unique one once. The empty string does not count.

For example, in "aba", the substrings are: "a", "b", "a", "ab", "ba", "aba". But "a" appears twice, so the distinct count is 5 (not 6).

The total number of substrings in a string of length n is n*(n+1)/2 (picking a start and end position). The challenge is counting how many of those are duplicates. We can enumerate all substrings and add them to a set, or we can use a Trie or rolling hash to avoid creating string objects at all.

Key Constraints:

  • 1 <= s.length <= 500 → With n up to 500, the total number of substrings is at most 500 * 501 / 2 = 125,250. An O(n^2) approach that processes each substring is fine, so the cost that matters is the per-substring work of hashing or comparison.
  • s consists of lowercase English letters → Only 26 possible characters, so a Trie node needs at most 26 children, which makes a fixed-size array per node practical.

Approach 1: Brute Force (HashSet)

Intuition

Generate every possible substring, add it to a HashSet, and let the set handle deduplication. At the end, the size of the set is the answer.

For a string of length n, we pick every starting index i (0 to n-1) and every ending index j (i+1 to n), extract the substring s[i..j], and add it to the set. Duplicates collapse to a single entry.

The cost is in the substring extraction. Each call creates a new string object, and hashing that string takes time proportional to its length. So while the two loops are O(n^2), the work per substring can be up to O(n), making this O(n^3) overall.

Algorithm

  1. Create an empty HashSet of strings.
  2. For each starting index i from 0 to n-1:
    • For each ending index j from i+1 to n:
      • Extract the substring s[i..j] and add it to the set.
  3. Return the size of the set.

Visualization and Code

Loading animation...

The next approach avoids rebuilding and rehashing strings by reusing the work already done for shorter substrings.

Approach 2: Trie (Optimal)

Intuition

Every substring of s is a prefix of some suffix of s. In "abc", the substring "ab" is a prefix of the suffix "abc", and "bc" is a prefix of the suffix "bc". So if we insert all n suffixes of s into a Trie, the set of all root-to-node paths covers exactly the set of substrings of s.

That turns the count into a node-counting problem: insert every suffix into a Trie, and each time inserting a character creates a new node, that node is one distinct substring not seen before. The total number of nodes created equals the number of distinct substrings.

This avoids creating string objects. We walk character by character, either following an existing edge or creating a new node.

Algorithm

  1. Create an empty Trie (just a root node with an array of 26 children).
  2. Initialize a counter to 0.
  3. For each starting index i from 0 to n-1 (this represents inserting the suffix starting at i):
    • Set current to the root of the Trie.
    • For each character s[j] where j goes from i to n-1:
      • Compute the index: c = s[j] - 'a'.
      • If current.children[c] is null, create a new node and increment the counter.
      • Move current to current.children[c].
  4. Return the counter.

Visualization and Code

Loading animation...

The Trie uses O(n^2 * 26) space because each node stores an array of 26 children. The next approach represents each substring as a single number instead of a node, which lowers the per-entry memory.

Approach 3: Rolling Hash

Intuition

Represent each substring by a hash instead of by a node, and store the hashes in a set. The number of distinct hashes equals the number of distinct substrings, as long as no two different substrings collide to the same hash.

A polynomial rolling hash makes extending a substring cheap. Fixing the start index i and growing the end index j by one, the hash of s[i..j+1] is hash * base + s[j+1], computed from the previous hash in O(1). So for each start position we sweep the end position once, hashing all n*(n+1)/2 substrings in O(n^2) total time.

The risk is a hash collision: two distinct substrings mapping to the same value would be counted once and undercount the answer. To make that improbable, we hash each substring under two independent base-modulus pairs and treat a substring as seen only when both hashes match.

Algorithm

  1. Choose two large primes mod1 and mod2, and two bases base1 and base2.
  2. Create a HashSet of (hash1, hash2) pairs.
  3. For each starting index i from 0 to n-1:
    • Initialize hash1 = 0 and hash2 = 0.
    • For each ending index j from i to n-1:
      • Update: hash1 = (hash1 * base1 + s[j]) % mod1.
      • Update: hash2 = (hash2 * base2 + s[j]) % mod2.
      • Add the pair (hash1, hash2) to the set.
  4. Return the size of the set.

Visualization and Code

Loading animation...