GeeksforGeeks Hard

Number of Distinct Substrings

Count the distinct substrings of a string (the empty substring excluded).

Constraints
  • 1 <= |s| <= 10³
  • s consists of lowercase English letters
  • The empty substring is not counted
triestringsuffix
Open on GeeksforGeeks ↗
02

Intuition

Number of distinct substrings counts how many different substrings a string has, excluding the empty one. Generating all of them and putting them in a set is correct but costs O(n³) once the copying of each substring is counted, since there are about n²/2 substrings averaging n/2 characters each. The reframing that fixes it is a small observation with a large consequence: - Every substring is a prefix of some suffix — the substring s[i..j] is a prefix of the suffix starting at i. So the set of all substrings is exactly the set of all prefixes of all suffixes. And a trie is precisely the structure whose nodes are prefixes. Insert all n suffixes into a trie. Each node below the root corresponds to one distinct substring, spelled out by the path from the root to it. Duplicate substrings arrive along paths that already exist, so they create no new nodes — the de-duplication happens structurally, with no set and no comparison anywhere. The answer is therefore just the node count, excluding the root. Worth being honest about the cost: this is O(n²) in both time and space, since the total length of all suffixes is n(n+1)/2. That is a real improvement over the naive O(n³), but for very long strings a suffix automaton achieves O(n), which is the direction to mention if asked to go further.

How to spot this pattern

Every substring is a prefix of some suffix — so inserting all suffixes into a trie and counting the new nodes created counts distinct substrings exactly. Each node corresponds to one unique substring, and the trie collapses shared prefixes automatically. Reframing "substrings" as "prefixes of suffixes" is the whole unlock.

03

Approach

Try it first

Before reading on: price up what enumerating every case costs here, then ask what the shared prefixes let you avoid storing twice. Aim for O(n²) time and O(n²) space.

1

See substrings as prefixes of suffixes

Any substring s[i..j] is a prefix of the suffix beginning at i. This equivalence is the entire idea — it converts an unstructured collection into something a trie indexes naturally.

2

Insert every suffix into a trie

For each start index, walk the suffix from the root, creating nodes only where the path does not already exist. Repeated substrings follow existing paths and add nothing.

3

Count nodes rather than insertions

Each non-root node represents exactly one distinct substring. No end-of-word flags and no de-duplication step are needed — the trie's structure enforces uniqueness by construction.

4

Increment as nodes are created

Keep a running counter incremented each time a new node is allocated. Traversing the finished trie to count also works, but counting during insertion avoids a second pass.

5

Exclude the root

The root corresponds to the empty substring, which the problem excludes. Counting only created nodes handles this automatically, since the root is never created during an insertion.

6

Cost and the better alternative

The suffixes total n(n+1)/2 characters, so this is O(n²) time and space — a solid gain over the naive O(n³). For very long strings, a suffix automaton counts distinct substrings in O(n), which is the answer to a follow-up asking for better.

04

Solution & live demo

▶1class Solution:
▶2 def countDistinctSubstrings(self, s):
▶3 root = {}
▶4 count = 0
▶5 for i in range(len(s)):
▶6 node = root
▶7 for ch in s[i:]:
▶8 if ch not in node:
▶9 node[ch] = {}
▶10 count += 1 # a substring never seen before
▶11 node = node[ch]
▶12 return count
05

Common pitfalls

Generating substrings into a set

✗ Wrong
return len({s[i:j] for i in range(n) for j in range(i+1, n+1)})
✓ Right
for i in range(len(s)):
    node = root
    for ch in s[i:]:
        if ch not in node:
            node[ch] = {}; count += 1

Correct but stores O(n²) substrings of average length O(n) — O(n³) memory. The trie shares prefixes, so equal substrings occupy the same nodes and are never duplicated.

Counting nodes visited rather than nodes created

✗ Wrong
node = node[ch]
count += 1
✓ Right
if ch not in node:
    node[ch] = {}
    count += 1

Walking an existing path means that substring was already counted from an earlier suffix. Only a newly created node represents a substring never seen before.

Inserting only whole suffixes without walking each character

✗ Wrong
for i in range(len(s)):
    root[s[i:]] = True
✓ Right
for ch in s[i:]:
    ...

That stores n suffixes as opaque keys and counts n, not the number of distinct substrings. The character-by-character descent is what makes every prefix of every suffix its own node.

06

Edge cases

All characters identical, e.g. "aaa"

Only n substrings exist ("a", "aa", "aaa") — the trie is one straight path of n nodes.

All characters distinct

Nothing merges, so the count reaches the maximum n(n+1)/2.

Empty substring

Excluded by construction, since the root is not counted.

07

Complexity

Time
O(n²)
Space
O(n²)
n suffixes of length up to n. A suffix automaton reaches O(n), but the trie makes the 'prefix of a suffix' argument visible.