3029. Minimum Time to Revert Word to Initial State I
Problem Description
You have a string word
and an integer k
. Starting from the original string, you need to perform operations every second until the string returns to its initial state.
Each operation consists of two mandatory steps:
- Remove the first
k
characters fromword
- Add any
k
characters to the end ofword
The key insight is that while you must remove exactly the first k
characters, you have complete freedom in choosing which k
characters to append at the end. Your goal is to strategically choose these characters to restore the string to its original form as quickly as possible.
For example, if word = "abcabc"
and k = 2
:
- After 1 second: Remove "ab", leaving "cabc". You could append "ab" to get "cabcab"
- After 2 seconds: Remove "ca", leaving "bcab". You could append "ca" to get "bcabca"
- After 3 seconds: Remove "bc", leaving "abca". You could append "bc" to get "abcabc" (back to original)
The solution works by checking if after removing k*i
characters (where i
is the number of operations), the remaining suffix of the original word matches a prefix of the original word. If word[k*i:]
equals word[:n-k*i]
, then we can restore the string in i
operations by appending the appropriate characters.
The function returns the minimum number of seconds (operations) needed to restore the string to its initial state. If no partial match is found, it returns the ceiling of n/k
, which represents the case where we need to completely replace all characters.
Intuition
The key realization is that after each operation, we're essentially shifting the string left by k
positions and filling the end with characters of our choice. Since we want to restore the original string, we need to think about when the remaining part of the original string can help us reconstruct the whole.
Let's trace through what happens after each operation:
- After 1 operation: We've removed the first
k
characters, so we haveword[k:]
remaining - After 2 operations: We've removed the first
2k
characters, so we haveword[2k:]
remaining - After
i
operations: We've removed the firsti*k
characters, so we haveword[i*k:]
remaining
The crucial insight is that if word[i*k:]
happens to be the same as the beginning of the original word word[:n-i*k]
, then we're in luck! This means the suffix that remains after i
operations matches a prefix of the original word. In this case, we can simply append the missing part word[n-i*k:]
to complete the restoration.
For example, consider word = "abcabc"
and k = 2
:
- After removing 2 characters:
"cabc"
remains - After removing 4 characters:
"bc"
remains - Notice that
"bc"
doesn't matchword[:2]
which is"ab"
- But if we had
word = "ababab"
and removed 2 characters, we'd get"abab"
, which does match the first 4 characters of the original!
This pattern-matching approach works because strings often have repeating patterns or self-similar structures. The algorithm exploits this by checking if any suffix (after removing multiples of k
characters) forms a valid prefix of the original string.
If no such match is found throughout the string, it means we need to completely cycle through all characters, which takes ceiling(n/k)
operations - essentially replacing the entire string piece by piece.
Solution Approach
The implementation follows a straightforward enumeration approach based on our pattern-matching insight.
We iterate through all possible multiples of k
starting from k
itself up to the length of the string n
. For each position i
that is a multiple of k
, we check if the suffix starting at position i
matches the prefix of the original string.
Here's how the algorithm works step by step:
-
Initialize the search: Start with
i = k
(after one operation) and increment byk
each time (representing successive operations). -
Check suffix-prefix match: For each position
i
, compareword[i:]
withword[:-i]
.word[i:]
represents the remaining suffix after removing the firsti
charactersword[:-i]
represents the prefix of lengthn-i
of the original word- If these match, it means we can restore the original string in
i // k
operations
-
Return on first match: As soon as we find a matching suffix-prefix pair, we return
i // k
, which gives us the minimum number of operations needed. -
Handle no match case: If we iterate through all valid positions without finding a match, it means we need to completely replace the string. This takes
(n + k - 1) // k
operations, which is the ceiling division ofn
byk
.
The time complexity is O(n²)
in the worst case, as we might need to check up to n/k
positions, and each string comparison takes O(n)
time. The space complexity is O(1)
as we only use string slicing which creates temporary strings but doesn't require additional data structures.
The elegance of this solution lies in its simplicity - we directly check the condition that allows restoration rather than simulating the actual operations, making the code both efficient and easy to understand.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with word = "abacaba"
and k = 2
.
Initial Setup:
- String length
n = 7
- We need to check positions that are multiples of
k
: 2, 4, 6
Step 1: Check position i = 2 (after 1 operation)
- Suffix at position 2:
word[2:]
="acaba"
- Prefix to match:
word[:5]
="abaca"
- Compare:
"acaba"
≠"abaca"
❌ - No match, continue
Step 2: Check position i = 4 (after 2 operations)
- Suffix at position 4:
word[4:]
="aba"
- Prefix to match:
word[:3]
="aba"
- Compare:
"aba"
="aba"
✓ - Match found!
Since we found a match at position 4, we can restore the string in 4 // 2 = 2
operations:
- Operation 1: Remove
"ab"
, get"acaba"
, append"ca"
→"acabaca"
- Operation 2: Remove
"ac"
, get"abaca"
, append"ba"
→"abacaba"
(original restored!)
The algorithm returns 2 as the minimum number of operations needed.
Why this works: After removing 4 characters total (2 operations × 2 characters each), the remaining suffix "aba"
matches the first 3 characters of our original word. This means we can strategically append the right characters (word[3:]
= "caba"
split across two operations) to rebuild the original string.
Solution Implementation
1class Solution:
2 def minimumTimeToInitialState(self, word: str, k: int) -> int:
3 """
4 Find the minimum number of operations to return a string to its initial state.
5 Each operation removes the first k characters and appends k characters to the end.
6
7 Args:
8 word: The input string
9 k: Number of characters to remove/add in each operation
10
11 Returns:
12 Minimum number of operations needed
13 """
14 n = len(word)
15
16 # Try each possible number of operations
17 # After i characters are removed, check if the remaining suffix
18 # matches the prefix of the original word
19 for i in range(k, n, k):
20 # Check if the suffix starting at position i matches
21 # the prefix of length (n - i) of the original word
22 if word[i:] == word[:n - i]:
23 # If they match, we need i // k operations
24 return i // k
25
26 # If no match found, we need to remove all characters
27 # Calculate ceiling division: (n + k - 1) // k
28 return (n + k - 1) // k
29
1class Solution {
2 /**
3 * Finds the minimum number of operations to return a string to its initial state.
4 * In each operation, we remove the first k characters and append k characters to the end.
5 * The string returns to initial state when the remaining suffix matches the corresponding prefix.
6 *
7 * @param word The input string to process
8 * @param k The number of characters to remove/append in each operation
9 * @return The minimum number of operations needed
10 */
11 public int minimumTimeToInitialState(String word, int k) {
12 int wordLength = word.length();
13
14 // Try each possible number of operations (1, 2, 3, ...)
15 // After i characters are removed, check if remaining suffix matches the prefix
16 for (int removedChars = k; removedChars < wordLength; removedChars += k) {
17 // Get the suffix starting from position 'removedChars'
18 String suffix = word.substring(removedChars);
19
20 // Get the prefix of length equal to the suffix
21 String prefix = word.substring(0, wordLength - removedChars);
22
23 // If suffix matches prefix, the string can return to initial state
24 if (suffix.equals(prefix)) {
25 return removedChars / k; // Number of operations performed
26 }
27 }
28
29 // If no match found, we need to remove all characters
30 // Calculate ceiling division: (wordLength + k - 1) / k
31 return (wordLength + k - 1) / k;
32 }
33}
34
1class Solution {
2public:
3 int minimumTimeToInitialState(string word, int k) {
4 int n = word.size();
5
6 // Try each possible shift position that is a multiple of k
7 for (int shiftPos = k; shiftPos < n; shiftPos += k) {
8 // Check if the suffix starting at shiftPos matches the prefix of the original word
9 // If word[shiftPos...n-1] equals word[0...n-shiftPos-1], we found a match
10 if (word.substr(shiftPos) == word.substr(0, n - shiftPos)) {
11 // Return the number of operations needed (each operation removes k characters)
12 return shiftPos / k;
13 }
14 }
15
16 // If no matching suffix-prefix found, we need to remove all characters
17 // Calculate ceiling division: (n + k - 1) / k
18 return (n + k - 1) / k;
19 }
20};
21
1/**
2 * Finds the minimum number of operations to return a word to its initial state
3 * by removing k characters from the end and appending k characters to the end
4 * @param word - The input string to process
5 * @param k - The number of characters to remove/append in each operation
6 * @returns The minimum number of operations needed
7 */
8function minimumTimeToInitialState(word: string, k: number): number {
9 // Get the length of the input word
10 const wordLength: number = word.length;
11
12 // Try each possible number of characters to remove (in multiples of k)
13 for (let removedChars: number = k; removedChars < wordLength; removedChars += k) {
14 // Check if the remaining suffix matches the prefix of the original word
15 // word.slice(removedChars) gets the suffix after removing 'removedChars' characters
16 // word.slice(0, -removedChars) gets the prefix of length (wordLength - removedChars)
17 if (word.slice(removedChars) === word.slice(0, -removedChars)) {
18 // If they match, return the number of operations performed
19 return Math.floor(removedChars / k);
20 }
21 }
22
23 // If no match found, return the ceiling of wordLength divided by k
24 // This represents removing all characters and rebuilding the word
25 return Math.floor((wordLength + k - 1) / k);
26}
27
Time and Space Complexity
Time Complexity: O(n²)
The algorithm iterates through positions at intervals of k
starting from k
up to n
(where n
is the length of the word). In the worst case, this results in approximately n/k
iterations. For each iteration at position i
, the string comparison word[i:] == word[:-i]
is performed. This comparison involves creating two substrings and comparing them character by character, which takes O(n-i)
time in the worst case. Since i
ranges from k
to potentially close to n
, and each comparison can take up to O(n)
time, the overall time complexity is O(n/k * n) = O(n²/k)
. However, since k
is at least 1 and could be a constant, the worst-case time complexity is O(n²)
.
Space Complexity: O(n)
During each string comparison word[i:] == word[:-i]
, Python creates two new substring objects. The slicing operations word[i:]
and word[:-i]
each create new strings that store copies of the respective portions of the original string. In the worst case, these substrings can be of length up to n-k
, which is O(n)
. Although these temporary strings are created and discarded in each iteration, the space required at any given moment is bounded by the size of these substring copies, resulting in a space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Ceiling Division Formula
A common mistake is using n // k
instead of (n + k - 1) // k
for the ceiling division when no match is found. This leads to incorrect results when n
is not perfectly divisible by k
.
Incorrect:
return n // k # This gives floor division, not ceiling
Correct:
return (n + k - 1) // k # Proper ceiling division
2. Off-by-One Error in String Slicing
Developers might confuse the slicing indices and write word[:-i]
instead of word[:n-i]
, or use word[i:]
with word[:i]
. While word[:-i]
and word[:n-i]
are equivalent when i > 0
, using explicit length makes the logic clearer.
Incorrect:
if word[i:] == word[:i]: # Wrong comparison return i // k
Correct:
if word[i:] == word[:n - i]: # Compares suffix with prefix of correct length return i // k
3. Starting Loop from Wrong Index
Starting the loop from 0 instead of k
would cause unnecessary checks and potential errors, as checking position 0 would mean no characters have been removed yet.
Incorrect:
for i in range(0, n, k): # Starting from 0 is meaningless
if word[i:] == word[:n - i]:
return i // k
Correct:
for i in range(k, n, k): # Start from k (after first operation)
if word[i:] == word[:n - i]:
return i // k
4. Forgetting Edge Cases
Not handling the case where k >= n
properly. When k
is greater than or equal to the string length, the entire string is removed in one operation, and we can restore it immediately.
Enhanced Solution:
def minimumTimeToInitialState(self, word: str, k: int) -> int:
n = len(word)
# Edge case: if k >= n, we need only 1 operation
if k >= n:
return 1
for i in range(k, n, k):
if word[i:] == word[:n - i]:
return i // k
return (n + k - 1) // k
5. Using Wrong Division for Return Value
Returning just i
instead of i // k
when a match is found. Remember that i
represents the number of characters removed, not the number of operations.
Incorrect:
if word[i:] == word[:n - i]: return i # Wrong: this returns characters removed, not operations
Correct:
if word[i:] == word[:n - i]: return i // k # Correct: number of operations
How does merge sort divide the problem into subproblems?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!