3137. Minimum Number of Operations to Make Word K-Periodic
Problem Description
You have a string word
of length n
and an integer k
where k
divides n
evenly.
You can perform operations to modify the string. In each operation:
- Pick any two positions
i
andj
that are divisible byk
(meaning positions 0, k, 2k, 3k, etc.) - Replace the substring of length
k
starting at positioni
with the substring of lengthk
starting at positionj
- In other words, replace
word[i..i+k-1]
withword[j..j+k-1]
Your goal is to make the string k-periodic using the minimum number of operations.
A string is k-periodic if it consists of some pattern s
of length k
repeated multiple times. For example:
"ababab"
is 2-periodic because it's the pattern"ab"
repeated 3 times"abcabcabc"
is 3-periodic because it's the pattern"abc"
repeated 3 times
The solution approach divides the string into chunks of length k
and counts how many times each unique chunk appears. Since we want all chunks to be the same (to make it k-periodic), we keep the most frequent chunk and replace all others. The minimum operations needed equals the total number of chunks (n/k
) minus the count of the most frequent chunk.
For example, if word = "ababcaba"
and k = 2
:
- The chunks are:
"ab"
,"ab"
,"ca"
,"ba"
"ab"
appears 2 times (most frequent)- Total chunks = 4
- Minimum operations = 4 - 2 = 2 (replace
"ca"
and"ba"
with"ab"
)
Intuition
To make the string k-periodic, we need all segments of length k
to be identical. Let's think about what happens when we divide the string into consecutive chunks of length k
.
For example, if we have word = "abcdefabcxyz"
and k = 3
, we get chunks: ["abc", "def", "abc", "xyz"]
.
The key insight is that each operation allows us to copy one chunk over another. Since we can only replace chunks (not create new ones), we must work with the chunks we already have.
To minimize operations, we should:
- Find which chunk appears most frequently (this will be our target pattern)
- Keep all occurrences of this chunk unchanged
- Replace all other chunks with this most frequent one
Why does this work? Consider that if we have m
total chunks and the most frequent chunk appears f
times, then:
- We need to change
m - f
chunks to match the most frequent one - Each operation replaces exactly one chunk
- Therefore, we need exactly
m - f
operations
This greedy approach is optimal because:
- We must make all chunks identical (that's what k-periodic means)
- We minimize work by choosing the chunk that already appears most often
- Any other choice would require more replacements
The formula n/k - max(counter.values())
directly implements this logic:
n/k
gives us the total number of chunksmax(counter.values())
gives us the frequency of the most common chunk- Their difference is the minimum number of operations needed
Solution Approach
The implementation uses a counting approach with Python's Counter
from the collections module.
Step-by-step breakdown:
-
Calculate the total length: Store
n = len(word)
for the string length. -
Generate all k-length chunks: Use a list comprehension with
range(0, n, k)
to iterate through the string at intervals ofk
. For each starting positioni
, extract the substringword[i : i + k]
. -
Count chunk frequencies: The
Counter
takes the list of all chunks and creates a frequency map. For example, if chunks are["ab", "cd", "ab", "ef"]
, the Counter becomes{"ab": 2, "cd": 1, "ef": 1}
. -
Find the maximum frequency: Use
.values()
to get all frequency counts, thenmax()
to find the highest frequency. This represents how many chunks we can keep unchanged. -
Calculate minimum operations: The formula
n // k - max(Counter(...).values())
computes:n // k
: Total number of chunks in the string- Subtract the maximum frequency to get chunks that need replacement
Example walkthrough:
If word = "abcabdabc"
and k = 3
:
- Chunks extracted:
["abc", "abd", "abc"]
- Counter result:
{"abc": 2, "abd": 1}
- Maximum frequency:
2
- Total chunks:
9 // 3 = 3
- Minimum operations:
3 - 2 = 1
The solution efficiently handles the problem in O(n)
time complexity for string traversal and O(n/k)
space complexity for storing the unique chunks in the Counter.
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 walk through the solution with word = "abcdabefabcd"
and k = 4
.
Step 1: Divide into chunks
- Start at positions 0, 4, 8 (multiples of k=4)
- Extract chunks of length 4:
- Position 0:
"abcd"
- Position 4:
"abef"
- Position 8:
"abcd"
- Position 0:
Step 2: Count chunk frequencies
- Create frequency map:
"abcd"
appears 2 times"abef"
appears 1 time
Step 3: Identify the most frequent chunk
- Maximum frequency = 2 (for chunk
"abcd"
) - This will be our target pattern
Step 4: Calculate minimum operations
- Total chunks = 12 ÷ 4 = 3
- Chunks to keep unchanged = 2 (the
"abcd"
chunks) - Chunks to replace = 3 - 2 = 1
- Answer: 1 operation needed
Verification:
- We need to replace
"abef"
at position 4 with"abcd"
- After 1 operation:
"abcdabcdabcd"
(k-periodic with pattern"abcd"
)
This demonstrates why keeping the most frequent chunk minimizes operations - we only need to change 1 chunk instead of 2 if we had chosen "abef"
as our target.
Solution Implementation
1from collections import Counter
2from typing import List
3
4class Solution:
5 def minimumOperationsToMakeKPeriodic(self, word: str, k: int) -> int:
6 # Get the length of the input word
7 n = len(word)
8
9 # Split the word into k-length substrings
10 # Create a list of all k-length substrings starting at positions 0, k, 2k, etc.
11 k_length_substrings = [word[i:i + k] for i in range(0, n, k)]
12
13 # Count the frequency of each k-length substring
14 substring_counter = Counter(k_length_substrings)
15
16 # Find the most frequent k-length substring
17 max_frequency = max(substring_counter.values())
18
19 # Calculate minimum operations needed
20 # Total number of k-length blocks minus the frequency of the most common block
21 # This gives us the number of blocks that need to be changed
22 total_blocks = n // k
23 min_operations = total_blocks - max_frequency
24
25 return min_operations
26
1class Solution {
2 public int minimumOperationsToMakeKPeriodic(String word, int k) {
3 // Map to store frequency of each k-length substring
4 Map<String, Integer> frequencyMap = new HashMap<>();
5
6 // Get the total length of the word
7 int wordLength = word.length();
8
9 // Track the maximum frequency among all k-length substrings
10 int maxFrequency = 0;
11
12 // Iterate through the word in steps of k to get all k-length segments
13 for (int i = 0; i < wordLength; i += k) {
14 // Extract substring of length k starting at position i
15 String substring = word.substring(i, i + k);
16
17 // Update frequency count for this substring and get the new count
18 // merge() adds 1 to existing count or sets to 1 if not present
19 int currentFrequency = frequencyMap.merge(substring, 1, Integer::sum);
20
21 // Update maximum frequency if current is higher
22 maxFrequency = Math.max(maxFrequency, currentFrequency);
23 }
24
25 // Calculate minimum operations needed
26 // Total segments minus the most frequent segment count
27 // (we keep the most frequent segment and replace all others)
28 return wordLength / k - maxFrequency;
29 }
30}
31
1class Solution {
2public:
3 int minimumOperationsToMakeKPeriodic(string word, int k) {
4 // Map to store frequency of each k-length substring
5 unordered_map<string, int> substringFrequency;
6
7 int wordLength = word.size();
8 int maxFrequency = 0;
9
10 // Iterate through the string in steps of k to get all k-length substrings
11 for (int i = 0; i < wordLength; i += k) {
12 // Extract substring of length k starting at position i
13 string currentSubstring = word.substr(i, k);
14
15 // Increment frequency count and update maximum frequency
16 substringFrequency[currentSubstring]++;
17 maxFrequency = max(maxFrequency, substringFrequency[currentSubstring]);
18 }
19
20 // Total number of k-length blocks minus the most frequent block count
21 // gives the minimum operations needed to make all blocks identical
22 return (wordLength / k) - maxFrequency;
23 }
24};
25
1/**
2 * Finds the minimum number of operations to make a string k-periodic.
3 * A string is k-periodic if it can be formed by concatenating the same substring of length k.
4 *
5 * @param word - The input string to make k-periodic
6 * @param k - The period length
7 * @returns The minimum number of operations needed
8 */
9function minimumOperationsToMakeKPeriodic(word: string, k: number): number {
10 // Map to store frequency of each substring of length k
11 const substringFrequencyMap: Map<string, number> = new Map<string, number>();
12
13 // Total length of the input word
14 const wordLength: number = word.length;
15
16 // Track the maximum frequency among all substrings
17 let maxFrequency: number = 0;
18
19 // Iterate through the word in steps of k to extract all k-length substrings
20 for (let startIndex = 0; startIndex < wordLength; startIndex += k) {
21 // Extract substring of length k starting at current position
22 const currentSubstring: string = word.slice(startIndex, startIndex + k);
23
24 // Update frequency count for this substring
25 const currentFrequency: number = (substringFrequencyMap.get(currentSubstring) || 0) + 1;
26 substringFrequencyMap.set(currentSubstring, currentFrequency);
27
28 // Update maximum frequency if current is higher
29 maxFrequency = Math.max(maxFrequency, currentFrequency);
30 }
31
32 // Total number of k-length segments minus the most frequent one
33 // gives us the minimum operations needed (replace all others with the most frequent)
34 return Math.floor(wordLength / k) - maxFrequency;
35}
36
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string word
. The algorithm iterates through the string once with a step size of k
using range(0, n, k)
, which creates n/k
iterations. For each iteration, it extracts a substring of length k
using word[i:i+k]
, which takes O(k)
time. Therefore, the total time for creating all substrings is O(n/k * k) = O(n)
. The Counter
construction processes these n/k
substrings in O(n/k)
time with O(k)
time for hashing each substring, resulting in O(n)
total time. Finding the maximum value in the counter takes O(n/k)
time. Overall, the time complexity is O(n)
.
The space complexity is O(n)
. The Counter
stores at most n/k
unique substrings, each of length k
. In the worst case where all substrings are unique, the total space required is O(n/k * k) = O(n)
. The generator expression word[i:i+k] for i in range(0, n, k)
doesn't create all substrings at once but the Counter
still needs to store them, resulting in O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Empty Input or Edge Cases with Counter
The code assumes the Counter will always have values to take the maximum from. If the input string is empty or if there's an issue with chunk generation, max(substring_counter.values())
will fail with a ValueError
because max()
cannot operate on an empty sequence.
Solution: Add a guard clause to handle empty strings or validate that chunks were generated:
def minimumOperationsToMakeKPeriodic(self, word: str, k: int) -> int:
n = len(word)
# Handle edge case of empty string
if n == 0:
return 0
k_length_substrings = [word[i:i + k] for i in range(0, n, k)]
# Ensure we have substrings to work with
if not k_length_substrings:
return 0
substring_counter = Counter(k_length_substrings)
max_frequency = max(substring_counter.values())
return n // k - max_frequency
2. Assuming k Always Divides n Evenly
While the problem states that k divides n evenly, in a real implementation you might encounter cases where this isn't true. The current code would create a final chunk of length less than k, leading to incorrect results.
Solution: Add validation to ensure k divides n evenly:
def minimumOperationsToMakeKPeriodic(self, word: str, k: int) -> int:
n = len(word)
# Validate that k divides n evenly
if n % k != 0:
raise ValueError(f"k={k} does not divide n={n} evenly")
# Rest of the implementation...
3. Integer Division vs Float Division
Using n / k
instead of n // k
would return a float, which could cause issues in the subtraction operation or if the result needs to be strictly an integer.
Solution:
Always use integer division (//
) when calculating the number of blocks:
total_blocks = n // k # Correct: integer division # NOT: total_blocks = n / k # Would return float
4. Memory Inefficiency with Large Strings
Creating a list of all substrings at once with list comprehension stores all chunks in memory simultaneously. For very large strings, this could cause memory issues.
Solution: Use a generator expression with Counter directly to process chunks on-the-fly:
def minimumOperationsToMakeKPeriodic(self, word: str, k: int) -> int:
n = len(word)
# Use generator expression instead of list comprehension
substring_counter = Counter(word[i:i + k] for i in range(0, n, k))
# Alternative: process incrementally
# substring_counter = Counter()
# for i in range(0, n, k):
# substring_counter[word[i:i + k]] += 1
max_frequency = max(substring_counter.values())
return n // k - max_frequency
5. Not Handling k = 0 or Negative k
If k is 0 or negative, the range function would either cause an infinite loop or behave unexpectedly.
Solution: Validate k at the beginning of the function:
def minimumOperationsToMakeKPeriodic(self, word: str, k: int) -> int:
if k <= 0:
raise ValueError(f"k must be positive, got k={k}")
n = len(word)
# Rest of the implementation...
Which two pointer techniques do you use to check if a string is a palindrome?
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!