Minimum Time to Revert Word to Initial State II
HardPrompt
You are given a 0-indexed string word and an integer k.
At every second, you must perform the following operations:
- Remove the first
kcharacters ofword. - Add any
kcharacters to the end ofword.
Note that you do not necessarily need to add the same characters that you removed. However, you must perform both operations at every second.
Return the minimum time greater than zero required for word to revert to its initial state.
Example 1:
Input: word = "abacaba", k = 3
Output: 2
Explanation: At the 1st second, we remove characters "aba" from the prefix of word, and add characters "bac" to the end of word. Thus, word becomes equal to "cababac".
At the 2nd second, we remove characters "cab" from the prefix of word, and add "aba" to the end of word. Thus, word becomes equal to "abacaba" and reverts to its initial state.
It can be shown that 2 seconds is the minimum time greater than zero required for word to revert to its initial state.Example 2:
Input: word = "abacaba", k = 4
Output: 1
Explanation: At the 1st second, we remove characters "abac" from the prefix of word, and add characters "caba" to the end of word. Thus, word becomes equal to "abacaba" and reverts to its initial state.
It can be shown that 1 second is the minimum time greater than zero required for word to revert to its initial state.Example 3:
Input: word = "abcbabcd", k = 2
Output: 4
Explanation: At every second, we will remove the first 2 characters of word, and add the same characters to the end of word.
After 4 seconds, word becomes equal to "abcbabcd" and reverts to its initial state.
It can be shown that 4 seconds is the minimum time greater than zero required for word to revert to its initial state.
Constraints:
1 <= word.length <= 1061 <= k <= word.lengthwordconsists only of lowercase English letters.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem. We iterate through the number of operations t starting from 1. For each t, we calculate the number of characters removed from the prefix, which is i = t * k. We then check if the remaining suffix of the original word, word.substring(i), is identical to a prefix of the original word of the same length, word.substring(0, n-i). If they match, we've found the minimum time t. If we iterate through all possible t where t*k < n and find no match, the answer is the smallest t for which t*k >= n, which is ceil(n/k).
Algorithm
- Get the length of the word,
n. - Loop through time
tstarting from 1. - In each iteration, calculate the number of characters that would have been removed,
i = t * k. - If
iis greater than or equal ton, it means we have checked all possibilities where a non-empty suffix of the original word remains. The minimum time istin this case, so we returnt. - Otherwise, we check if the suffix of
wordstarting at indexiis equal to the prefix ofwordof the same length (n-i). We can useword.regionMatches(0, word, i, n - i)for an efficient comparison that avoids creating new string objects. - If they are equal, we have found the smallest
tthat works, so we returnt. - If the strings are not equal, we continue to the next value of
t.
Walkthrough
The core idea is to test each possible time t=1, 2, 3, ... sequentially. For a given time t, t*k characters are removed from the beginning of the word. The length of the word is n. The remaining part of the word is the suffix starting at index t*k, and its length is n - t*k. To revert to the initial state, this remaining suffix must be a prefix of the original word. So, we check if word[i...n-1] equals word[0...n-i-1]. The first t > 0 for which this condition holds is our answer. If no such t exists for t*k < n, it means we can only revert the word after removing all of its characters. The time for this is ceil(n/k), which can be calculated using integer arithmetic as (n + k - 1) / k. The loop for t will naturally arrive at this value if no earlier match is found.
class Solution { public int minimumTimeToInitialState(String word, int k) { int n = word.length(); for (int t = 1; ; ++t) { int i = t * k; if (i >= n) { return t; } // Check if suffix starting at i matches prefix of same length if (word.regionMatches(0, word, i, n - i)) { return t; } } }}Complexity
Time
O(N^2 / k). The loop for `t` runs up to `N/k` times. In each iteration, we compare strings of length `N - t*k`. The total number of character comparisons is the sum `(N-k) + (N-2k) + ...`, which is on the order of `O(N^2 / k)`. In the worst case, when `k=1`, the complexity is `O(N^2)`.
Space
O(1). We are not using any extra space that scales with the input size `n`. `regionMatches` compares parts of the existing string in-place.
Trade-offs
Pros
Simple to understand and implement.
It correctly solves the problem for small inputs.
Cons
The time complexity is high, making it unsuitable for large constraints.
It will likely result in a 'Time Limit Exceeded' error on platforms with strict time limits.
The brute-force approach is slow because of the repeated string comparisons inside the loop. We can significantly optimize this check using a standard string algorithm. The Z-algorithm is perfectly suited for this task. It preprocesses the string word in O(n) time to create a Z-array. The value z[i] in this array stores the length of the longest common prefix between word and the suffix of word starting at index i. This allows us to check our condition in O(1) time.
Algorithm
- First, compute the Z-array for the input
word. The Z-algorithm builds this array inO(n)time. - Iterate
tfrom 1 upwards. Leti = t * kbe the starting index of the suffix. - If
i >= n, it means we have checked all validtwhere a non-empty suffix could match a prefix. The answer is the currentt. Returnt. - For the current index
i, check the precomputed Z-valuez[i]. The conditionword.substring(i)is a prefix ofwordis equivalent toz[i] >= n - i. - If
z[i] >= n - i, the condition is met. We have found the minimum timet = i/k. Returnt. - If the condition is not met, continue to the next multiple of
k.
Walkthrough
The condition we need to check for each potential time t is whether the suffix word.substring(t*k) is a prefix of the original word. Let i = t*k. This is equivalent to checking if the Longest Common Prefix (LCP) of word and word.substring(i) has a length of at least n-i (the length of the suffix). The Z-array gives us this information directly: z[i] is the length of the LCP of word and word.substring(i). Therefore, the check simplifies to z[i] >= n - i.
By precomputing the Z-array for the entire word, we can then iterate through the relevant indices i = k, 2k, 3k, ... and perform this check in constant time for each.
class Solution { public int minimumTimeToInitialState(String word, int k) { int n = word.length(); int[] z = new int[n]; // Z-algorithm to compute the Z-array int l = 0, r = 0; for (int i = 1; i < n; i++) { if (i <= r) { z[i] = Math.min(r - i + 1, z[i - l]); } while (i + z[i] < n && word.charAt(z[i]) == word.charAt(i + z[i])) { z[i]++; } if (i + z[i] - 1 > r) { l = i; r = i + z[i] - 1; } } for (int t = 1; ; ++t) { int i = t * k; if (i >= n) { return t; } if (z[i] >= n - i) { return t; } } }}Complexity
Time
O(N). Computing the Z-array takes `O(N)`. The subsequent loop runs at most `N/k` times, and each check inside the loop is an `O(1)` array lookup. The total time is `O(N + N/k) = O(N)`.
Space
O(N) to store the Z-array.
Trade-offs
Pros
Highly efficient with a linear time complexity.
Guaranteed to pass within the time limits for large inputs.
Cons
Requires knowledge of the Z-algorithm, which is a non-trivial string algorithm.
The implementation of the Z-algorithm can be tricky to get right.
This approach reframes the problem in a way that perfectly aligns with the strengths of the Knuth-Morris-Pratt (KMP) algorithm's preprocessing step. We are looking for a state where the remaining suffix is also a prefix of the original word. This is equivalent to finding a prefix of word that is also a suffix of word. Let the length of such a prefix-suffix be L. The number of characters removed to reach this state is n-L. For this to be an achievable state, n-L must be a multiple of k. To find the minimum time t = (n-L)/k, we need to find the maximum possible L that satisfies this divisibility condition.
Algorithm
- Compute the Longest Proper Prefix Suffix (LPS) array for
wordusing the KMP preprocessing algorithm. This takesO(n)time. - Find the length of the longest proper prefix of
wordthat is also a suffix. This is given byL = lps[n-1]. - Start a loop with the current length
L. WhileL > 0: a. Check if the number of removed characters,n - L, is divisible byk. b. If(n - L) % k == 0, we have found the largestLthat works. This corresponds to the minimum timet = (n - L) / k. Return this value. c. If not divisible, find the next longest prefix-suffix by updatingLtolps[L-1]and continue the loop. - If the loop finishes (meaning
Lbecomes 0), no non-empty prefix-suffix satisfies the condition. The only way to revert is to remove characters until the word is empty. The time required isceil(n/k), which is(n + k - 1) / k. Return this value.
Walkthrough
The KMP preprocessing algorithm computes an LPS (Longest Proper Prefix Suffix) array. For a string S, lps[i] stores the length of the longest proper prefix of S[0...i] that is also a suffix of S[0...i]. The key insight is that all prefixes of word that are also suffixes can be found by starting with the longest one, L = lps[n-1], and then repeatedly chaining back via L = lps[L-1] until L becomes 0. We can iterate through this chain of lengths from longest to shortest, and the first one we find that satisfies (n - L) % k == 0 will give us our answer.
class Solution { public int minimumTimeToInitialState(String word, int k) { int n = word.length(); int[] lps = new int[n]; // KMP preprocessing to compute LPS array int length = 0; for (int i = 1; i < n; i++) { while (length > 0 && word.charAt(i) != word.charAt(length)) { length = lps[length - 1]; } if (word.charAt(i) == word.charAt(length)) { length++; } lps[i] = length; } int len = lps[n - 1]; while (len > 0) { if ((n - len) % k == 0) { return (n - len) / k; } len = lps[len - 1]; } // If no such prefix-suffix works, we must remove chunks until the word is empty. return (n + k - 1) / k; }}Complexity
Time
O(N). Computing the LPS array takes `O(N)`. The subsequent `while` loop to check lengths also takes at most `O(N)` in total over all its iterations, as `len` is strictly decreasing.
Space
O(N) to store the LPS array.
Trade-offs
Pros
Very efficient, with linear time complexity.
The logic is elegant and directly tackles the core structure of the problem (prefix-suffixes).
Potentially faster in practice than the Z-algorithm approach as it only checks lengths that are valid prefix-suffixes, which are often sparse.
Cons
Requires knowledge of the KMP algorithm's preprocessing step (LPS array construction).
The logic, while elegant, might be slightly less direct to derive compared to the Z-algorithm approach.
Solutions
Solution
class Hashing { private final long [] p ; private final long [] h ; private final long mod ; public Hashing ( String word , long base , int mod ) { int n = word . length (); p = new long [ n + 1 ]; h = new long [ n + 1 ]; p [ 0 ] = 1 ; this . mod = mod ; for ( int i = 1 ; i <= n ; i ++) { p [ i ] = p [ i - 1 ] * base % mod ; h [ i ] = ( h [ i - 1 ] * base + word . charAt ( i - 1 ) - 'a' ) % mod ; } } public long query ( int l , int r ) { return ( h [ r ] - h [ l - 1 ] * p [ r - l + 1 ] % mod + mod ) % mod ; } } class Solution { public int minimumTimeToInitialState ( String word , int k ) { Hashing hashing = new Hashing ( word , 13331 , 998244353 ); int n = word . length (); for ( int i = k ; i < n ; i += k ) { if ( hashing . query ( 1 , n - i ) == hashing . query ( i + 1 , n )) { return i / k ; } } return ( n + k - 1 ) / k ; } }class Hashing { private: vector < long long > p ; vector < long long > h ; long long mod ; public: Hashing ( string word , long long base , int mod ) { int n = word . size (); p . resize ( n + 1 ); h . resize ( n + 1 ); p [ 0 ] = 1 ; this -> mod = mod ; for ( int i = 1 ; i <= n ; i ++ ) { p [ i ] = ( p [ i - 1 ] * base ) % mod ; h [ i ] = ( h [ i - 1 ] * base + word [ i - 1 ] - 'a' ) % mod ; } } long long query ( int l , int r ) { return ( h [ r ] - h [ l - 1 ] * p [ r - l + 1 ] % mod + mod ) % mod ; } }; class Solution { public: int minimumTimeToInitialState ( string word , int k ) { Hashing hashing ( word , 13331 , 998244353 ); int n = word . size (); for ( int i = k ; i < n ; i += k ) { if ( hashing . query ( 1 , n - i ) == hashing . query ( i + 1 , n )) { return i / k ; } } return ( n + k - 1 ) / k ; } };class Hashing : __slots__ = [ "mod" , "h" , "p" ] def __init__ ( self , s : str , base : int , mod : int ): self . mod = mod self . h = [ 0 ] * ( len ( s ) + 1 ) self . p = [ 1 ] * ( len ( s ) + 1 ) for i in range ( 1 , len ( s ) + 1 ): self . h [ i ] = ( self . h [ i - 1 ] * base + ord ( s [ i - 1 ])) % mod self . p [ i ] = ( self . p [ i - 1 ] * base ) % mod def query ( self , l : int , r : int ) -> int : return ( self . h [ r ] - self . h [ l - 1 ] * self . p [ r - l + 1 ]) % self . mod class Solution : def minimumTimeToInitialState ( self , word : str , k : int ) -> int : hashing = Hashing ( word , 13331 , 998244353 ) n = len ( word ) for i in range ( k , n , k ): if hashing . query ( 1 , n - i ) == hashing . query ( i + 1 , n ): return i // k return ( n + k - 1 ) // kVideo 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.