Facebook Pixel

2156. Find Substring With Given Hash Value

HardStringSliding WindowHash FunctionRolling Hash
Leetcode Link

Problem Description

You are given a string s and need to find a specific substring based on a hash function. The hash function for a string of length k is calculated as:

hash(s, p, m) = (val(s[0]) * p^0 + val(s[1]) * p^1 + ... + val(s[k-1]) * p^(k-1)) mod m

Where val(s[i]) represents the position of character s[i] in the alphabet: val('a') = 1, val('b') = 2, ..., val('z') = 26.

Given:

  • A string s (0-indexed)
  • Integer power (the base p in the hash formula)
  • Integer modulo (the modulus m in the hash formula)
  • Integer k (the length of the substring to find)
  • Integer hashValue (the target hash value)

Your task is to find and return the first substring of s with length k whose hash value equals hashValue.

For example, if we have a substring "abc" with k=3, power=2, and modulo=100, the hash would be: hash("abc", 2, 100) = (1 * 2^0 + 2 * 2^1 + 3 * 2^2) = (1 + 4 + 12) = 17

The problem guarantees that at least one valid substring exists. A substring is defined as a contiguous sequence of characters within the string.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The naive approach would be to check every possible substring of length k by calculating its hash value and comparing it with the target hashValue. However, recalculating the hash from scratch for each substring would be inefficient.

The key insight is to use a sliding window technique where we can update the hash value incrementally as we move the window. But there's a challenge: when sliding the window forward (left to right), removing the leftmost character from the hash requires division by a power of p, which is problematic with modular arithmetic.

Consider how the hash changes when sliding forward:

  • Current window: s[i], s[i+1], ..., s[i+k-1]
  • Next window: s[i+1], s[i+2], ..., s[i+k]

To get the new hash, we need to:

  1. Remove contribution of s[i]: subtract val(s[i]) * p^0
  2. Shift all remaining terms down by one power (divide by p)
  3. Add the new character: add val(s[i+k]) * p^(k-1)

The division operation in step 2 is complicated with modular arithmetic.

The clever solution is to traverse the string in reverse. When moving the window backward:

  • Current window: s[i], s[i+1], ..., s[i+k-1]
  • Previous window: s[i-1], s[i], ..., s[i+k-2]

Now the transformation becomes:

  1. Remove the rightmost character: subtract val(s[i+k-1]) * p^(k-1)
  2. Multiply everything by p (shift up by one power)
  3. Add the new leftmost character: add val(s[i-1]) * p^0

This only involves multiplication and addition, which work nicely with modular arithmetic! We start from the last k characters, calculate their hash, then slide the window backward, updating the hash incrementally until we find a match.

Learn more about Sliding Window patterns.

Solution Approach

Following the reverse traversal strategy, here's how we implement the solution:

Step 1: Initialize and calculate the hash for the last k characters

We start by calculating the hash value for the last k characters of the string (positions n-k to n-1):

h = 0
p = 1
for i in range(n - 1, n - 1 - k, -1):
    val = ord(s[i]) - ord("a") + 1
    h = ((h * power) + val) % modulo
    if i != n - k:
        p = p * power % modulo
  • We traverse from right to left within this window
  • For each character, we multiply the current hash by power and add the character's value
  • We also calculate p = power^(k-1) % modulo which represents the highest power coefficient we'll need for removing characters

Step 2: Slide the window backward through the string

Now we slide the window from right to left through the entire string:

j = n - k  # Track the starting position of valid substring
for i in range(n - 1 - k, -1, -1):
    pre = ord(s[i + k]) - ord("a") + 1  # Character being removed
    cur = ord(s[i]) - ord("a") + 1      # Character being added
    h = ((h - pre * p) * power + cur) % modulo

The hash update formula works as follows:

  1. Remove the rightmost character's contribution: h - pre * p
  2. Shift all terms up by multiplying by power: (h - pre * p) * power
  3. Add the new leftmost character with coefficient p^0 = 1: + cur

Step 3: Check for matches and track the answer

if h == hashValue:
    j = i

Whenever the current window's hash equals the target hashValue, we update j to store this position. Since we're traversing backward, the last match we find will actually be the first occurrence in the string.

Step 4: Return the result

return s[j : j + k]

Return the substring starting at position j with length k.

Time Complexity: O(n) where n is the length of the string, as we process each character at most twice.

Space Complexity: O(1) as we only use a constant amount of extra space.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to understand how the solution works.

Given:

  • s = "fbxzaad"
  • power = 31
  • modulo = 100
  • k = 3
  • hashValue = 32

We need to find a substring of length 3 whose hash equals 32.

Step 1: Calculate hash for the last k characters ("aad")

Starting with the last 3 characters (indices 4, 5, 6):

  • Initialize h = 0, p = 1
  • Process 'd' (index 6):
    • val('d') = 4
    • h = (0 * 31 + 4) % 100 = 4
  • Process 'a' (index 5):
    • val('a') = 1
    • h = (4 * 31 + 1) % 100 = 125 % 100 = 25
    • p = 31 (power for position 0)
  • Process 'a' (index 4):
    • val('a') = 1
    • h = (25 * 31 + 1) % 100 = 776 % 100 = 76
    • p = 31 * 31 % 100 = 961 % 100 = 61 (power^(k-1))

Hash of "aad" = 76, which ≠ 32, so we continue.

Step 2: Slide window backward

Window at indices [3,4,5] - "zaa":

  • Remove 'd' (index 6): val('d') = 4
  • Add 'z' (index 3): val('z') = 26
  • h = ((76 - 4 * 61) * 31 + 26) % 100
  • h = ((76 - 244) * 31 + 26) % 100
  • h = ((-168) * 31 + 26) % 100
  • h = (-5208 + 26) % 100 = -5182 % 100 = 18

Hash of "zaa" = 18, which ≠ 32, continue.

Window at indices [2,3,4] - "xza":

  • Remove 'a' (index 5): val('a') = 1
  • Add 'x' (index 2): val('x') = 24
  • h = ((18 - 1 * 61) * 31 + 24) % 100
  • h = ((18 - 61) * 31 + 24) % 100
  • h = ((-43) * 31 + 24) % 100
  • h = (-1333 + 24) % 100 = -1309 % 100 = 91

Hash of "xza" = 91, which ≠ 32, continue.

Window at indices [1,2,3] - "bxz":

  • Remove 'a' (index 4): val('a') = 1
  • Add 'b' (index 1): val('b') = 2
  • h = ((91 - 1 * 61) * 31 + 2) % 100
  • h = ((91 - 61) * 31 + 2) % 100
  • h = (30 * 31 + 2) % 100
  • h = (930 + 2) % 100 = 932 % 100 = 32

Hash of "bxz" = 32, which equals our target! Update j = 1.

Window at indices [0,1,2] - "fbx":

  • Remove 'z' (index 3): val('z') = 26
  • Add 'f' (index 0): val('f') = 6
  • h = ((32 - 26 * 61) * 31 + 6) % 100
  • h = ((32 - 1586) * 31 + 6) % 100
  • h = ((-1554) * 31 + 6) % 100
  • h = (-48174 + 6) % 100 = -48168 % 100 = 32

Hash of "fbx" = 32, which also equals our target! Update j = 0.

Step 3: Return the result

Since we traversed backward and the last matching position was j = 0, we return s[0:3] = "fbx".

This demonstrates why reverse traversal is clever: it naturally gives us the first occurrence of a matching substring, and the rolling hash update only requires multiplication and addition (no division needed).

Solution Implementation

1class Solution:
2    def subStrHash(
3        self, s: str, power: int, modulo: int, k: int, hashValue: int
4    ) -> str:
5        """
6        Find the substring of length k whose hash equals hashValue.
7        Hash formula: (val(s[0]) * power^0 + val(s[1]) * power^1 + ... + val(s[k-1]) * power^(k-1)) % modulo
8        where val(c) = ord(c) - ord('a') + 1
9        """
10        # Initialize variables
11        current_hash = 0
12        string_length = len(s)
13        power_k_minus_1 = 1  # Will store power^(k-1) % modulo
14      
15        # Calculate hash for the last k characters (rightmost substring)
16        # Building hash from right to left within the window
17        for i in range(string_length - 1, string_length - 1 - k, -1):
18            char_value = ord(s[i]) - ord("a") + 1
19            current_hash = (current_hash * power + char_value) % modulo
20          
21            # Calculate power^(k-1) % modulo for later use in rolling hash
22            if i != string_length - k:
23                power_k_minus_1 = (power_k_minus_1 * power) % modulo
24      
25        # Initialize result position with the last possible substring
26        result_position = string_length - k
27      
28        # Roll the hash window from right to left through the string
29        for i in range(string_length - 1 - k, -1, -1):
30            # Remove the rightmost character from current window
31            removed_char_value = ord(s[i + k]) - ord("a") + 1
32            # Add the new leftmost character to current window
33            new_char_value = ord(s[i]) - ord("a") + 1
34          
35            # Update rolling hash: remove old character contribution, shift left, add new character
36            current_hash = ((current_hash - removed_char_value * power_k_minus_1) * power + new_char_value) % modulo
37          
38            # Check if current hash matches target
39            if current_hash == hashValue:
40                result_position = i
41      
42        # Return the substring starting at result_position with length k
43        return s[result_position : result_position + k]
44
1class Solution {
2    public String subStrHash(String s, int power, int modulo, int k, int hashValue) {
3        // Initialize hash value and power^(k-1) for rolling hash
4        long currentHash = 0;
5        long powerK = 1;  // Will store power^(k-1) mod modulo
6        int stringLength = s.length();
7      
8        // Calculate initial hash for the rightmost window of length k
9        // Hash formula: val[0] * power^0 + val[1] * power^1 + ... + val[k-1] * power^(k-1)
10        for (int i = stringLength - 1; i >= stringLength - k; i--) {
11            int charValue = s.charAt(i) - 'a' + 1;  // Convert character to value (a=1, b=2, ...)
12            currentHash = ((currentHash * power % modulo) + charValue) % modulo;
13          
14            // Calculate power^(k-1) mod modulo for later use in rolling hash
15            if (i != stringLength - k) {
16                powerK = powerK * power % modulo;
17            }
18        }
19      
20        // Track the starting index of substring with matching hash
21        int resultIndex = stringLength - k;
22      
23        // Slide the window from right to left to find all substrings with target hash
24        for (int i = stringLength - k - 1; i >= 0; i--) {
25            // Remove the rightmost character from current window
26            int removedCharValue = s.charAt(i + k) - 'a' + 1;
27            // Add the new leftmost character to current window
28            int addedCharValue = s.charAt(i) - 'a' + 1;
29          
30            // Update rolling hash: remove old character contribution and add new character
31            // Formula: newHash = (oldHash - removedChar * power^(k-1)) * power + addedChar
32            currentHash = ((currentHash - removedCharValue * powerK % modulo + modulo) * power % modulo + addedCharValue) % modulo;
33          
34            // Update result index if current hash matches target
35            if (currentHash == hashValue) {
36                resultIndex = i;
37            }
38        }
39      
40        // Return the substring starting at the found index with length k
41        return s.substring(resultIndex, resultIndex + k);
42    }
43}
44
1class Solution {
2public:
3    string subStrHash(string s, int power, int modulo, int k, int hashValue) {
4        // Initialize rolling hash and power multiplier
5        long long rollingHash = 0;
6        long long powerK = 1;  // Will store power^(k-1) % modulo
7        int stringLength = s.size();
8      
9        // Calculate initial hash for the last k characters (rightmost window)
10        // Hash formula: val[0] * power^(k-1) + val[1] * power^(k-2) + ... + val[k-1]
11        for (int i = stringLength - 1; i >= stringLength - k; --i) {
12            int charValue = s[i] - 'a' + 1;  // Convert character to value (a=1, b=2, ...)
13            rollingHash = ((rollingHash * power % modulo) + charValue) % modulo;
14          
15            // Calculate power^(k-1) % modulo for later use in rolling hash
16            if (i != stringLength - k) {
17                powerK = powerK * power % modulo;
18            }
19        }
20      
21        // Track the position of substring with matching hash
22        int matchPosition = stringLength - k;
23      
24        // Slide the window from right to left
25        for (int windowStart = stringLength - k - 1; windowStart >= 0; --windowStart) {
26            // Remove the rightmost character from current window
27            int removedCharValue = s[windowStart + k] - 'a' + 1;
28            // Add the new leftmost character to the window
29            int addedCharValue = s[windowStart] - 'a' + 1;
30          
31            // Update rolling hash: remove old character contribution and add new character
32            // Formula: newHash = (oldHash - removed * power^(k-1)) * power + added
33            rollingHash = ((rollingHash - removedCharValue * powerK % modulo + modulo) * power % modulo + addedCharValue) % modulo;
34          
35            // Check if current window's hash matches the target
36            if (rollingHash == hashValue) {
37                matchPosition = windowStart;
38            }
39        }
40      
41        // Return the substring with matching hash
42        return s.substr(matchPosition, k);
43    }
44};
45
1/**
2 * Finds a substring of length k whose polynomial rolling hash equals the given hashValue
3 * Uses rolling hash technique with modular arithmetic to efficiently check all substrings
4 * 
5 * @param s - The input string containing only lowercase English letters
6 * @param power - The base for polynomial hash calculation
7 * @param modulo - The modulus for hash calculation
8 * @param k - The length of the substring to find
9 * @param hashValue - The target hash value to match
10 * @returns The first substring (leftmost if multiple exist) with the matching hash value
11 */
12function subStrHash(
13    s: string,
14    power: number,
15    modulo: number,
16    k: number,
17    hashValue: number,
18): string {
19    // Initialize hash value and power multiplier for rolling hash
20    let currentHash: bigint = BigInt(0);
21    let powerMultiplier: bigint = BigInt(1);
22  
23    const stringLength: number = s.length;
24    const moduloBigInt: bigint = BigInt(modulo);
25  
26    // Calculate initial hash for the last k characters (rightmost substring)
27    // Hash formula: sum of (char_value * power^position) mod modulo
28    for (let i: number = stringLength - 1; i >= stringLength - k; --i) {
29        const charValue: bigint = BigInt(s.charCodeAt(i) - 'a'.charCodeAt(0) + 1);
30        currentHash = (((currentHash * BigInt(power)) % moduloBigInt) + charValue) % moduloBigInt;
31      
32        // Calculate power^(k-1) for later use in rolling hash
33        if (i !== stringLength - k) {
34            powerMultiplier = (powerMultiplier * BigInt(power)) % moduloBigInt;
35        }
36    }
37  
38    // Track the position of substring with matching hash
39    let matchPosition: number = stringLength - k;
40  
41    // Roll the hash window from right to left through the string
42    for (let i: number = stringLength - k - 1; i >= 0; --i) {
43        // Character leaving the window (rightmost of current window)
44        const removedCharValue: bigint = BigInt(s.charCodeAt(i + k) - 'a'.charCodeAt(0) + 1);
45        // Character entering the window (leftmost of new window)
46        const addedCharValue: bigint = BigInt(s.charCodeAt(i) - 'a'.charCodeAt(0) + 1);
47      
48        // Update rolling hash: remove old character contribution, shift, add new character
49        currentHash = ((((currentHash - ((removedCharValue * powerMultiplier) % moduloBigInt) + moduloBigInt) * BigInt(power)) % moduloBigInt) + addedCharValue) % moduloBigInt;
50      
51        // Check if current window hash matches target
52        if (Number(currentHash) === hashValue) {
53            matchPosition = i;
54        }
55    }
56  
57    // Return the substring at the found position
58    return s.substring(matchPosition, matchPosition + k);
59}
60

Time and Space Complexity

Time Complexity: O(n), where n is the length of the string s.

The algorithm consists of two main loops:

  1. The first loop runs from n-1 to n-k, iterating exactly k times to compute the initial hash value of the last substring of length k. This takes O(k) time.
  2. The second loop runs from n-1-k to 0, iterating n-k times. In each iteration, it performs constant time operations:
    • Computing the hash of the new substring using the rolling hash technique by removing the contribution of the character going out of the window (s[i+k]) and adding the contribution of the new character (s[i])
    • Comparing the hash value with the target hashValue

Since k ≤ n, the total time complexity is O(k) + O(n-k) = O(n).

Space Complexity: O(1)

The algorithm uses only a constant amount of extra space:

  • Variables h, n, p, j to store the hash value, string length, power value, and result index
  • Variables pre, cur to store character values during the rolling hash computation

The space used does not depend on the input size, making the space complexity O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Negative Modulo Values

The most critical pitfall in this solution is handling negative values during the modulo operation. When we compute (current_hash - removed_char_value * power_k_minus_1) * power % modulo, the subtraction can result in a negative number.

Why this happens:

  • In Python, the modulo operation with negative numbers can produce unexpected results
  • For example, if current_hash = 5, removed_char_value * power_k_minus_1 = 10, and modulo = 7, then (5 - 10) % 7 gives -5 in intermediate calculation

Solution: Add modulo before taking the final modulo to ensure a positive result:

current_hash = ((current_hash - removed_char_value * power_k_minus_1 % modulo + modulo) * power + new_char_value) % modulo

2. Integer Overflow in Power Calculation

When calculating power_k_minus_1, repeated multiplication can cause integer overflow for large values of k and power.

Solution: Apply modulo operation at each multiplication step:

if i != string_length - k:
    power_k_minus_1 = (power_k_minus_1 * power) % modulo  # Already correct in the code

3. Incorrect Hash Calculation Direction

A common mistake is calculating the initial hash from left to right instead of right to left within the window, which would give incorrect power coefficients.

Why this matters:

  • The hash formula requires s[0] * power^0 + s[1] * power^1 + ...
  • Building from right to left naturally gives us the correct coefficients when we multiply by power at each step

Solution: Ensure the initial hash calculation traverses from right to left:

for i in range(string_length - 1, string_length - 1 - k, -1):
    current_hash = (current_hash * power + char_value) % modulo

4. Off-by-One Errors in Window Boundaries

It's easy to make mistakes with index boundaries when sliding the window, especially when working backward.

Common mistakes:

  • Starting the sliding window at wrong position
  • Incorrectly calculating which character to remove/add

Solution: Carefully track indices:

  • Window starts at position i and ends at position i + k - 1
  • Character being removed is at position i + k
  • Character being added is at position i

5. Forgetting to Apply Modulo Consistently

Missing modulo operations at any step can lead to incorrect hash values or overflow.

Solution: Apply modulo after every arithmetic operation that could exceed the modulo value:

current_hash = ((current_hash - removed_char_value * power_k_minus_1 % modulo + modulo) % modulo * power % modulo + new_char_value) % modulo

Corrected Critical Section:

The most important fix to apply to the original code:

# Original (potentially buggy)
current_hash = ((current_hash - removed_char_value * power_k_minus_1) * power + new_char_value) % modulo

# Corrected (handles negative values properly)
current_hash = ((current_hash - removed_char_value * power_k_minus_1 % modulo + modulo) % modulo * power + new_char_value) % modulo
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More