3031. Minimum Time to Revert Word to Initial State II
Problem Description
You start with a string word
and an integer k
. Every second, you must perform exactly two operations:
- Remove the first
k
characters fromword
- Add any
k
characters to the end ofword
The goal is to find the minimum number of seconds (greater than 0) needed to transform word
back to its original state.
For example, if word = "abcdef"
and k = 2
:
- After 1 second: Remove "ab", leaving "cdef". You could add "ab" to get "cdefab"
- After 2 seconds: Remove "cd", leaving "efab". You could add "cd" to get "efabcd"
- After 3 seconds: Remove "ef", leaving "abcd". You could add "ef" to get "abcdef" (original string)
The solution uses rolling hash to efficiently check if a suffix of the original string matches a prefix. At each step i
(where i
is a multiple of k
), it checks if word[i:]
equals word[:n-i]
. If they match, we know we can restore the original string in i/k
seconds by:
- Removing the first
i
characters overi/k
operations - Adding back the appropriate characters to restore the original string
The algorithm iterates through positions k, 2k, 3k, ...
and uses the Hashing
class to compare substrings in O(1) time. The hash function uses polynomial rolling hash with base 13331 and modulo 998244353 to avoid collisions. If no early match is found, the answer is ceiling(n/k)
, which represents completely cycling through the entire string.
Intuition
The key insight is that after removing the first k
characters and adding k
characters to the end, we're essentially performing a circular shift of the string. After t
seconds, we will have removed the first t*k
characters from the original string.
For the string to return to its initial state, the remaining portion of the original string (after removing t*k
characters) must match the beginning of the original string. In other words, if we've removed i = t*k
characters, then word[i:]
must equal word[:n-i]
for us to be able to reconstruct the original string by adding the appropriate characters.
Why does this work? Consider what happens after t
seconds:
- We've removed characters at positions
[0, t*k)
from the original string - The current string starts with characters from position
t*k
of the original - To get back to the original, we need the suffix
word[t*k:]
to match the prefixword[:n-t*k]
This is because if they match, we can add the removed characters word[:t*k]
to the end, and the string becomes word[t*k:] + word[:t*k]
, which equals the original word
when the suffix-prefix condition holds.
The rolling hash technique is used to efficiently compare these substrings in O(1) time rather than O(n) for direct string comparison. We check every possible position that's a multiple of k
(since we can only remove k
characters at a time), starting from k
and going up to n
. The first position where the suffix-prefix condition holds gives us our minimum time.
If no such position exists before reaching the end, it means we need to cycle through the entire string, which takes ceiling(n/k)
seconds.
Solution Approach
The solution implements a rolling hash algorithm to efficiently check substring equality. Here's how the implementation works:
Rolling Hash Class (Hashing
)
The Hashing
class precomputes hash values for all prefixes of the string:
h[i]
stores the hash value of the prefixs[0:i]
p[i]
storesbase^i mod mod
for efficient hash computation- The hash formula used is:
hash(s) = (s[0]*base^(n-1) + s[1]*base^(n-2) + ... + s[n-1]) mod mod
The query(l, r)
method returns the hash of substring s[l-1:r]
in O(1) time using the formula:
hash(s[l-1:r]) = (h[r] - h[l-1] * p[r-l+1]) mod mod
Main Algorithm (minimumTimeToInitialState
)
-
Initialize the hashing object with base 13331 and modulo 998244353 (large prime to minimize collisions)
-
Iterate through all positions that are multiples of
k
:for i in range(k, n, k):
At each position
i
, we check if removing the firsti
characters would allow us to restore the original string. -
For each position
i
, compare two substrings using hash values:hashing.query(1, n - i)
: Hash ofword[0:n-i]
(the prefix we need to match)hashing.query(i + 1, n)
: Hash ofword[i:n]
(the suffix that remains after removal)
Note: The query uses 1-based indexing, so we add 1 to convert from 0-based.
-
If the hashes match, it means
word[0:n-i] == word[i:n]
. This indicates we can restore the original string ini // k
seconds by:- Removing
k
charactersi/k
times - Adding back the appropriate characters to complete the original string
- Removing
-
If no match is found in the loop, return
(n + k - 1) // k
, which is the ceiling division representing the time needed to completely cycle through the entire string.
The time complexity is O(n) for preprocessing the hash values and O(n/k) for checking positions, resulting in O(n) overall. The space complexity is O(n) for storing the hash arrays.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with word = "abacaba"
and k = 3
.
Step 1: Initialize Rolling Hash
- Create hash arrays for the string "abacaba" (length n = 7)
- The hash will help us compare substrings in O(1) time
Step 2: Check positions that are multiples of k
Position i = 3 (after 1 second):
- We check if removing the first 3 characters allows restoration
- Need to compare:
word[0:4]
("abac") withword[3:7]
("caba") - Using hash:
query(1, 4)
vsquery(4, 7)
- "abac" ≠ "caba", so we can't restore in 1 second
Position i = 6 (after 2 seconds):
- We check if removing the first 6 characters allows restoration
- Need to compare:
word[0:1]
("a") withword[6:7]
("a") - Using hash:
query(1, 1)
vsquery(7, 7)
- "a" = "a", so we CAN restore in 2 seconds!
Why does this work? After 2 seconds of operations:
- Second 1: Remove "aba" → "caba", add any 3 chars (say "xyz") → "cabaxyz"
- Second 2: Remove "cab" → "axyz", add "aba" → "axyzaba"
- But wait, since word[0:1] = word[6:7] = "a", we can be smarter:
- Second 1: Remove "aba" → "caba", add "aba" → "cabaaba"
- Second 2: Remove "cab" → "aaba", add "cab" → "aabacab"
- Actually, let me reconsider...
- Second 1: Remove "aba" → "caba", we need to add 3 chars that will help us get back
- Second 2: Remove "cab" → "a" + (our 3 added chars)
- Since word[6:7] = "a" matches word[0:1] = "a", we can add "bacaba" over the 2 operations to restore "abacaba"
The answer is 2 seconds.
If no match was found at any position, we would return ⌈7/3⌉ = 3
seconds.
Solution Implementation
1class Hashing:
2 """Rolling hash implementation for efficient substring comparison."""
3
4 __slots__ = ["mod", "h", "p"]
5
6 def __init__(self, s: str, base: int, mod: int) -> None:
7 """
8 Initialize the rolling hash for string s.
9
10 Args:
11 s: Input string to be hashed
12 base: Base value for polynomial rolling hash
13 mod: Modulo value to prevent overflow
14 """
15 self.mod = mod
16 # h[i] stores the hash value of prefix s[0:i]
17 self.h = [0] * (len(s) + 1)
18 # p[i] stores base^i mod mod
19 self.p = [1] * (len(s) + 1)
20
21 # Build prefix hash and power arrays
22 for i in range(1, len(s) + 1):
23 # Calculate hash of prefix ending at position i
24 self.h[i] = (self.h[i - 1] * base + ord(s[i - 1])) % mod
25 # Calculate base^i for later use
26 self.p[i] = (self.p[i - 1] * base) % mod
27
28 def query(self, l: int, r: int) -> int:
29 """
30 Get the hash value of substring s[l-1:r].
31
32 Args:
33 l: Starting position (1-indexed)
34 r: Ending position (1-indexed, inclusive)
35
36 Returns:
37 Hash value of the substring
38 """
39 # Extract hash of substring using prefix hash difference
40 return (self.h[r] - self.h[l - 1] * self.p[r - l + 1]) % self.mod
41
42
43class Solution:
44 def minimumTimeToInitialState(self, word: str, k: int) -> int:
45 """
46 Find minimum time to return string to initial state by removing k characters
47 from the beginning and appending them to the end.
48
49 Args:
50 word: Input string
51 k: Number of characters to remove/append in each operation
52
53 Returns:
54 Minimum number of operations needed
55 """
56 # Initialize rolling hash with chosen base and prime modulo
57 hashing = Hashing(word, base=13331, mod=998244353)
58 n = len(word)
59
60 # Try each possible number of k-sized chunks removed
61 for i in range(k, n, k):
62 # Check if remaining suffix matches the prefix of same length
63 # Compare word[0:n-i] with word[i:n]
64 if hashing.query(1, n - i) == hashing.query(i + 1, n):
65 return i // k
66
67 # If no match found, need to process entire string
68 # Return ceiling division of n by k
69 return (n + k - 1) // k
70
1/**
2 * Polynomial rolling hash implementation for string hashing.
3 * Uses polynomial hash function: h(s) = (s[0] * base^(n-1) + s[1] * base^(n-2) + ... + s[n-1]) mod prime
4 */
5class Hashing {
6 private final long[] powers; // powers[i] = base^i mod prime
7 private final long[] prefixHash; // prefixHash[i] = hash of substring [0, i)
8 private final long prime; // prime modulus for hash function
9
10 /**
11 * Constructs a hashing object for the given string.
12 * @param word the string to hash
13 * @param base the base for polynomial hashing
14 * @param prime the prime modulus
15 */
16 public Hashing(String word, long base, int prime) {
17 int length = word.length();
18 powers = new long[length + 1];
19 prefixHash = new long[length + 1];
20 powers[0] = 1;
21 this.prime = prime;
22
23 // Precompute powers of base and prefix hashes
24 for (int i = 1; i <= length; i++) {
25 // Calculate base^i mod prime
26 powers[i] = powers[i - 1] * base % prime;
27
28 // Calculate hash for prefix [0, i)
29 // Hash formula: h[i] = (h[i-1] * base + char_value) mod prime
30 prefixHash[i] = (prefixHash[i - 1] * base + word.charAt(i - 1) - 'a') % prime;
31 }
32 }
33
34 /**
35 * Queries the hash value of substring [left, right] (1-indexed).
36 * @param left the starting position (1-indexed)
37 * @param right the ending position (1-indexed)
38 * @return the hash value of the substring
39 */
40 public long query(int left, int right) {
41 // Extract hash of substring using prefix hashes
42 // Formula: hash[l,r] = (hash[r] - hash[l-1] * base^(r-l+1)) mod prime
43 // Add prime before final mod to handle negative values
44 return (prefixHash[right] - prefixHash[left - 1] * powers[right - left + 1] % prime + prime) % prime;
45 }
46}
47
48/**
49 * Solution class for minimum time to initial state problem.
50 */
51class Solution {
52 /**
53 * Finds the minimum number of operations to return the string to its initial state.
54 * In each operation, remove the first k characters and append k characters to the end.
55 * @param word the initial string
56 * @param k the number of characters to remove/append in each operation
57 * @return the minimum number of operations needed
58 */
59 public int minimumTimeToInitialState(String word, int k) {
60 // Initialize hashing with base 13331 and prime 998244353
61 Hashing hashing = new Hashing(word, 13331, 998244353);
62 int wordLength = word.length();
63
64 // Check each possible number of operations
65 // After i characters are removed, check if remaining prefix matches the suffix
66 for (int removedChars = k; removedChars < wordLength; removedChars += k) {
67 // Compare hash of prefix [1, n-i] with suffix [i+1, n]
68 // If they match, the string can return to initial state after removedChars/k operations
69 if (hashing.query(1, wordLength - removedChars) ==
70 hashing.query(removedChars + 1, wordLength)) {
71 return removedChars / k;
72 }
73 }
74
75 // If no early match found, return ceiling of wordLength/k
76 // This represents completely cycling through the string
77 return (wordLength + k - 1) / k;
78 }
79}
80
1class Hashing {
2private:
3 vector<long long> power; // power[i] stores base^i mod modulus
4 vector<long long> hash; // hash[i] stores hash value of prefix [0, i)
5 long long modulus; // modulus for hash computation
6
7public:
8 /**
9 * Constructor: Builds polynomial rolling hash for the given string
10 * @param word: Input string to hash
11 * @param base: Base for polynomial hash (should be prime)
12 * @param mod: Modulus for hash computation (should be prime)
13 */
14 Hashing(string word, long long base, int mod) {
15 int n = word.size();
16 power.resize(n + 1);
17 hash.resize(n + 1);
18 power[0] = 1;
19 this->modulus = mod;
20
21 // Precompute powers of base and prefix hashes
22 for (int i = 1; i <= n; i++) {
23 power[i] = (power[i - 1] * base) % mod;
24 // Hash formula: h[i] = h[i-1] * base + char_value
25 hash[i] = (hash[i - 1] * base + word[i - 1] - 'a') % mod;
26 }
27 }
28
29 /**
30 * Query hash value of substring [l, r] (1-indexed)
31 * @param l: Left boundary (1-indexed)
32 * @param r: Right boundary (1-indexed)
33 * @return: Hash value of substring word[l-1...r-1]
34 */
35 long long query(int l, int r) {
36 // Extract substring hash using prefix difference
37 // Add modulus to handle negative values
38 return (hash[r] - hash[l - 1] * power[r - l + 1] % modulus + modulus) % modulus;
39 }
40};
41
42class Solution {
43public:
44 /**
45 * Find minimum time to return string to initial state
46 * @param word: Input string
47 * @param k: Number of characters removed in each operation
48 * @return: Minimum number of operations needed
49 */
50 int minimumTimeToInitialState(string word, int k) {
51 // Initialize hashing with base 13331 and prime modulus
52 Hashing hashing(word, 13331, 998244353);
53 int n = word.size();
54
55 // Check each possible position after removing k characters at a time
56 for (int i = k; i < n; i += k) {
57 // Check if prefix of length (n-i) matches suffix starting at position i
58 // This means after i/k operations, the string can return to initial state
59 if (hashing.query(1, n - i) == hashing.query(i + 1, n)) {
60 return i / k;
61 }
62 }
63
64 // If no match found, return ceiling of n/k
65 return (n + k - 1) / k;
66 }
67};
68
1// Global variables for hashing
2let power: bigint[] = []; // power[i] stores base^i mod modulus
3let hash: bigint[] = []; // hash[i] stores hash value of prefix [0, i)
4let modulus: bigint; // modulus for hash computation
5
6/**
7 * Builds polynomial rolling hash for the given string
8 * @param word - Input string to hash
9 * @param base - Base for polynomial hash (should be prime)
10 * @param mod - Modulus for hash computation (should be prime)
11 */
12function initHashing(word: string, base: bigint, mod: bigint): void {
13 const n = word.length;
14 power = new Array(n + 1);
15 hash = new Array(n + 1);
16 power[0] = 1n;
17 hash[0] = 0n;
18 modulus = mod;
19
20 // Precompute powers of base and prefix hashes
21 for (let i = 1; i <= n; i++) {
22 // Calculate base^i mod modulus
23 power[i] = (power[i - 1] * base) % mod;
24
25 // Hash formula: h[i] = h[i-1] * base + char_value
26 // Convert character to numeric value (a=0, b=1, ...)
27 const charValue = BigInt(word.charCodeAt(i - 1) - 'a'.charCodeAt(0));
28 hash[i] = (hash[i - 1] * base + charValue) % mod;
29 }
30}
31
32/**
33 * Query hash value of substring [l, r] (1-indexed)
34 * @param l - Left boundary (1-indexed)
35 * @param r - Right boundary (1-indexed)
36 * @returns Hash value of substring word[l-1...r-1]
37 */
38function queryHash(l: number, r: number): bigint {
39 // Extract substring hash using prefix difference
40 // Formula: hash(l,r) = hash[r] - hash[l-1] * base^(r-l+1)
41 // Add modulus to handle negative values from subtraction
42 const substringHash = (hash[r] - hash[l - 1] * power[r - l + 1] % modulus + modulus) % modulus;
43 return substringHash;
44}
45
46/**
47 * Find minimum time to return string to initial state
48 * @param word - Input string
49 * @param k - Number of characters removed in each operation
50 * @returns Minimum number of operations needed
51 */
52function minimumTimeToInitialState(word: string, k: number): number {
53 // Initialize hashing with base 13331 and prime modulus 998244353
54 initHashing(word, 13331n, 998244353n);
55 const n = word.length;
56
57 // Check each possible position after removing k characters at a time
58 // Starting from position k, check every k-th position
59 for (let i = k; i < n; i += k) {
60 // Check if prefix of length (n-i) matches suffix starting at position i
61 // If prefix[0...n-i-1] equals suffix[i...n-1], the string can return to initial state
62 // This happens after i/k operations
63 if (queryHash(1, n - i) === queryHash(i + 1, n)) {
64 return Math.floor(i / k);
65 }
66 }
67
68 // If no match found, return ceiling of n/k
69 // This represents the worst case where we need to remove all characters
70 return Math.ceil(n / k);
71}
72
Time and Space Complexity
Time Complexity: O(n)
The analysis breaks down as follows:
- The
Hashing.__init__
method performs a single loop from 1 tolen(s) + 1
, which takesO(n)
time wheren
is the length of the string. - The
Hashing.query
method performs constant time operations (arithmetic and modulo operations), so it takesO(1)
time. - In the
minimumTimeToInitialState
method:- Creating the
Hashing
object takesO(n)
time. - The for loop iterates from
k
ton
with stepk
, resulting in at mostn/k
iterations. - Each iteration calls
hashing.query
twice, each takingO(1)
time. - Therefore, the loop takes
O(n/k)
time.
- Creating the
- Since
k ≥ 1
, the loop complexityO(n/k)
is bounded byO(n)
. - The overall time complexity is
O(n) + O(n/k) = O(n)
.
Space Complexity: O(n)
The space analysis:
- The
Hashing
class stores two arraysh
andp
, each of sizelen(s) + 1 = n + 1
. - These arrays take
O(n)
space. - The
Solution
class uses only a constant amount of additional space for variables likei
andn
. - Therefore, the total space complexity is
O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Hash Collision Leading to False Positives
The most critical pitfall in this rolling hash solution is assuming that equal hash values always mean equal strings. Hash collisions can occur even with a good hash function, leading to incorrect results.
Problem Example:
# Even with base=13331 and mod=998244353, collisions are possible # The algorithm might incorrectly identify a match when hashes are equal # but the actual substrings are different
Solution: Add a verification step when hash values match to ensure the substrings are actually equal:
class Solution:
def minimumTimeToInitialState(self, word: str, k: int) -> int:
hashing = Hashing(word, base=13331, mod=998244353)
n = len(word)
for i in range(k, n, k):
# First check hash equality
if hashing.query(1, n - i) == hashing.query(i + 1, n):
# Verify actual string equality to avoid hash collision
if word[:n-i] == word[i:]:
return i // k
return (n + k - 1) // k
2. Integer Overflow in Hash Calculation
Without proper modulo operations, the hash values can overflow, especially for long strings.
Problem Example:
# Incorrect: Missing modulo in intermediate calculations
self.h[i] = self.h[i - 1] * base + ord(s[i - 1]) # Can overflow!
Solution: Always apply modulo operations at each step:
# Correct: Apply modulo at each operation
self.h[i] = (self.h[i - 1] * base + ord(s[i - 1])) % mod
self.p[i] = (self.p[i - 1] * base) % mod
3. Off-by-One Errors in Index Conversion
The Hashing
class uses 1-based indexing while Python uses 0-based indexing, making it easy to make indexing errors.
Problem Example:
# Incorrect indexing - forgetting to adjust for 1-based indexing if hashing.query(0, n - i - 1) == hashing.query(i, n - 1): # Wrong!
Solution: Document the indexing convention clearly and be consistent:
# Correct: Convert 0-based to 1-based indexing # To check word[0:n-i] == word[i:n] if hashing.query(1, n - i) == hashing.query(i + 1, n): return i // k
4. Using Double Hashing for Collision Resistance
For production code where correctness is critical, use double hashing with different bases and moduli:
class Solution:
def minimumTimeToInitialState(self, word: str, k: int) -> int:
# Use two different hash functions
hash1 = Hashing(word, base=13331, mod=998244353)
hash2 = Hashing(word, base=31, mod=10**9 + 7)
n = len(word)
for i in range(k, n, k):
# Check both hashes match
if (hash1.query(1, n - i) == hash1.query(i + 1, n) and
hash2.query(1, n - i) == hash2.query(i + 1, n)):
return i // k
return (n + k - 1) // k
This dramatically reduces the probability of collision from approximately 1/mod to 1/(mod1 * mod2).
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
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!