Find Substring With Given Hash Value
HardPrompt
The hash of a 0-indexed string s of length k, given integers p and m, is computed using the following function:
hash(s, p, m) = (val(s[0]) * p0 + val(s[1]) * p1 + ... + val(s[k-1]) * pk-1) mod m.
Where val(s[i]) represents the index of s[i] in the alphabet from val('a') = 1 to val('z') = 26.
You are given a string s and the integers power, modulo, k, and hashValue. Return sub, the first substring of s of length k such that hash(sub, power, modulo) == hashValue.
The test cases will be generated such that an answer always exists.
A substring is a contiguous non-empty sequence of characters within a string.
Example 1:
Input: s = "leetcode", power = 7, modulo = 20, k = 2, hashValue = 0
Output: "ee"
Explanation: The hash of "ee" can be computed to be hash("ee", 7, 20) = (5 * 1 + 5 * 7) mod 20 = 40 mod 20 = 0.
"ee" is the first substring of length 2 with hashValue 0. Hence, we return "ee".Example 2:
Input: s = "fbxzaad", power = 31, modulo = 100, k = 3, hashValue = 32
Output: "fbx"
Explanation: The hash of "fbx" can be computed to be hash("fbx", 31, 100) = (6 * 1 + 2 * 31 + 24 * 312) mod 100 = 23132 mod 100 = 32.
The hash of "bxz" can be computed to be hash("bxz", 31, 100) = (2 * 1 + 24 * 31 + 26 * 312) mod 100 = 25732 mod 100 = 32.
"fbx" is the first substring of length 3 with hashValue 32. Hence, we return "fbx".
Note that "bxz" also has a hash of 32 but it appears later than "fbx".
Constraints:
1 <= k <= s.length <= 2 * 1041 <= power, modulo <= 1090 <= hashValue < modulosconsists of lowercase English letters only.- The test cases are generated such that an answer always exists.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach involves a straightforward, brute-force check of every possible substring. We iterate through all substrings of length k, calculate their hash value individually, and compare it to the target hashValue. The first substring that provides a match is the one we are looking for.
Algorithm
- Iterate through the string
swith an indexifrom0tos.length() - k. - For each
i, consider the substring starting atiof lengthk. - Calculate the hash of this substring using the given formula.
- To do this, loop from
j = 0tok-1. - Maintain a variable for
power^j, updating it in each step of the inner loop. - Sum up
val(char) * power^jfor all characters in the substring, taking the modulo at each step to prevent overflow.
- To do this, loop from
- If the calculated hash matches
hashValue, return the current substrings.substring(i, i + k). - Since an answer is guaranteed to exist, the loop will always find a match.
Walkthrough
The algorithm iterates through the string s from the first possible starting position 0 up to the last possible one, n-k. For each starting position i, it considers the substring of length k. A nested loop is then used to compute the hash of this substring. Inside the nested loop, we iterate through the k characters of the substring. For each character at index j within the substring, we calculate its value ('a'=1, 'b'=2, etc.) and multiply it by power^j. We maintain a running currentPower variable, which starts at 1 and is multiplied by power (modulo modulo) in each iteration. The term (val * currentPower) is added to a running hash total, which is also kept within the modulo range. If, after processing all k characters, the final hash equals hashValue, we have found our answer and can return the substring immediately. All calculations involving large numbers should use a 64-bit integer type (long in Java) to avoid overflow before applying the modulo operation.
class Solution { public String subStrHash(String s, int power, int modulo, int k, int hashValue) { int n = s.length(); long p = power; long m = modulo; for (int i = 0; i <= n - k; i++) { long currentHash = 0; long p_pow = 1; // Calculate hash for substring s[i...i+k-1] for (int j = 0; j < k; j++) { long val = s.charAt(i + j) - 'a' + 1; currentHash = (currentHash + (val * p_pow)) % m; p_pow = (p_pow * p) % m; } if (currentHash == hashValue) { return s.substring(i, i + k); } } return ""; // Should not be reached as per problem statement }}Complexity
Time
O(n * k), where `n` is the length of `s` and `k` is the length of the substring. The outer loop runs `n-k+1` times, and the inner loop for hash calculation runs `k` times.
Space
O(k) if we create a new substring in each iteration. This can be optimized to O(1) auxiliary space by accessing characters directly from the input string.
Trade-offs
Pros
Simple to understand and implement.
Correctly solves the problem for small inputs.
Cons
The time complexity of O(n*k) is too slow for the given constraints and will likely result in a 'Time Limit Exceeded' error on larger test cases.
This approach uses the rolling hash technique to achieve a linear time complexity. Instead of recomputing the hash for each substring from scratch, we calculate the hash of a new substring based on the hash of the previous one in constant time. The given hash function has powers of p increasing from left to right. This structure makes it difficult to slide the window from left to right without using modular inverse (which is not always possible). However, by sliding the window from right to left, we can devise an update formula that only uses multiplication, addition, and subtraction, making it efficient and always applicable.
Algorithm
- Let
nbe the length ofs. - Pre-compute
power^k mod modulo(let's call itpowerK) using modular exponentiation. This will be needed for the update step. - Calculate the hash of the rightmost substring
s[n-k..n-1]from scratch. This takes O(k) time. - Check if this initial hash matches
hashValue. If so, storen-kas the potential answer index. - Iterate with an index
ifromn-k-1down to0. This indexirepresents the start of the new window. - In each iteration, update the hash from the previous window
s[i+1..i+k]to the current windows[i..i+k-1]in O(1) time using the derived update formula:h_new = (p * h_old - p^k * val_old + val_new) mod m. h_oldis the hash of the previous window,val_oldis the value of the character leaving the window (s[i+k]), andval_newis the value of the character entering the window (s[i]).- If the new hash matches
hashValue, update the answer index toi. Since we iterate from right to left, we always update the index to find the leftmost match. - After the loop, return the substring starting at the final answer index.
Walkthrough
The core of this method is the O(1) hash update. Let h_i be the hash of s[i..i+k-1] and h_{i+1} be the hash of s[i+1..i+k]. The relationship is h_i = (power * h_{i+1} - power^k * val(s[i+k]) + val(s[i])) mod modulo.
The algorithm works as follows:
- We first compute
power^k mod modulousing modular exponentiation (O(log k) time). - We then compute the hash of the last substring
s[n-k..n-1]from scratch (O(k) time). - We check if this hash matches
hashValueand initialize our answer index if it does. - We then loop from
i = n-k-1down to0. In each step,iis the starting index of the new window. We use the hash of the windows[i+1..i+k](which we have from the previous step) to calculate the hash of the current windows[i..i+k-1]using the update formula. This step takes O(1) time. - During the update, we must be careful with subtractions in modular arithmetic, using
(a - b + m) % mto handle potential negative results. All intermediate calculations should uselongto prevent overflow. - If the hash of the current window matches
hashValue, we update our answer index toi. Because we are iterating from right to left, the last index we find will be the smallest, corresponding to the first substring in the original string. - Finally, we return the substring identified by the answer index.
class Solution { public String subStrHash(String s, int power, int modulo, int k, int hashValue) { int n = s.length(); long p = power; long m = modulo; long targetHash = hashValue; long powerK = 1; for (int i = 0; i < k; i++) { powerK = (powerK * p) % m; } long currentHash = 0; long p_pow = 1; // Calculate hash of the last window s[n-k...n-1] for (int j = 0; j < k; j++) { long val = s.charAt(n - k + j) - 'a' + 1; currentHash = (currentHash + val * p_pow) % m; p_pow = (p_pow * p) % m; } int ansIdx = -1; if (currentHash == targetHash) { ansIdx = n - k; } // Slide window from right to left // i is the start of the new window for (int i = n - k - 1; i >= 0; i--) { // currentHash is for window s[i+1..i+k] // We want to compute hash for s[i..i+k-1] long oldCharVal = s.charAt(i + k) - 'a' + 1; long newCharVal = s.charAt(i) - 'a' + 1; // Update hash using formula: h_i = (p * h_{i+1} - p^k * val(s[i+k]) + val(s[i])) mod m currentHash = (p * currentHash) % m; long termToRemove = (oldCharVal * powerK) % m; currentHash = (currentHash - termToRemove + m) % m; currentHash = (currentHash + newCharVal) % m; if (currentHash == targetHash) { ansIdx = i; } } return s.substring(ansIdx, ansIdx + k); }}Complexity
Time
O(n), where `n` is the length of `s`. The initial hash calculation takes O(k), and the rolling hash part takes O(n-k). The total time is dominated by O(n). (Note: `power^k` calculation takes O(log k), which is negligible).
Space
O(1) auxiliary space, as we only need a few variables to store the running hash and powers.
Trade-offs
Pros
Highly efficient with a linear time complexity, making it suitable for large inputs.
Optimal solution for this problem.
Cons
The implementation is more complex than the brute-force approach.
The derivation of the correct rolling hash update formula for this specific hash function can be tricky and error-prone.
Solutions
Solution
class Solution { public String subStrHash ( String s , int power , int modulo , int k , int hashValue ) { long h = 0 , p = 1 ; int n = s . length (); for ( int i = n - 1 ; i >= n - k ; -- i ) { int val = s . charAt ( i ) - 'a' + 1 ; h = (( h * power % modulo ) + val ) % modulo ; if ( i != n - k ) { p = p * power % modulo ; } } int j = n - k ; for ( int i = n - k - 1 ; i >= 0 ; -- i ) { int pre = s . charAt ( i + k ) - 'a' + 1 ; int cur = s . charAt ( i ) - 'a' + 1 ; h = (( h - pre * p % modulo + modulo ) * power % modulo + cur ) % modulo ; if ( h == hashValue ) { j = i ; } } return s . substring ( j , j + k ); } }/** * @param {string} s * @param {number} power * @param {number} modulo * @param {number} k * @param {number} hashValue * @return {string} */ var subStrHash = function ( s , power , modulo , k , hashValue ) { power = BigInt ( power ); modulo = BigInt ( modulo ); hashValue = BigInt ( hashValue ); const n = s . length ; let pk = 1 n ; let ac = 0 n ; class Solution { public: string subStrHash ( string s , int power , int modulo , int k , int hashValue ) { long long h = 0 , p = 1 ; int n = s . size (); for ( int i = n - 1 ; i >= n - k ; -- i ) { int val = s [ i ] - 'a' + 1 ; h = (( h * power % modulo ) + val ) % modulo ; if ( i != n - k ) { p = p * power % modulo ; } } int j = n - k ; for ( int i = n - k - 1 ; i >= 0 ; -- i ) { int pre = s [ i + k ] - 'a' + 1 ; int cur = s [ i ] - 'a' + 1 ; h = (( h - pre * p % modulo + modulo ) * power % modulo + cur ) % modulo ; if ( h == hashValue ) { j = i ; } } return s . substr ( j , k ); } };class Solution : def subStrHash ( self , s : str , power : int , modulo : int , k : int , hashValue : int ) -> str : h , n = 0 , len ( s ) 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 j = n - k for i in range ( n - 1 - k , - 1 , - 1 ): pre = ord ( s [ i + k ]) - ord ( "a" ) + 1 cur = ord ( s [ i ]) - ord ( "a" ) + 1 h = (( h - pre * p ) * power + cur ) % modulo if h == hashValue : j = i return s [ j : j + k ]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.