2111. Minimum Operations to Make the Array K-Increasing
Problem Description
You have an array arr
of positive integers (0-indexed) and a positive integer k
. Your task is to determine the minimum number of operations needed to make the array "K-increasing".
An array is K-increasing if for every valid index i
(where k ≤ i ≤ n-1
), the condition arr[i-k] ≤ arr[i]
holds true. In other words, if you look at elements that are exactly k
positions apart, the later element must be greater than or equal to the earlier one.
To understand this better, imagine dividing the array into k
subsequences:
- Subsequence 0: elements at indices 0, k, 2k, 3k, ...
- Subsequence 1: elements at indices 1, k+1, 2k+1, 3k+1, ...
- Subsequence 2: elements at indices 2, k+2, 2k+2, 3k+2, ...
- And so on...
For the array to be K-increasing, each of these subsequences must be non-decreasing.
Example walkthrough:
Given arr = [4, 1, 5, 2, 6, 2]
and k = 2
:
- Elements at distance 2:
arr[0]
andarr[2]
: 4 ≤ 5 ✓arr[1]
andarr[3]
: 1 ≤ 2 ✓arr[2]
andarr[4]
: 5 ≤ 6 ✓arr[3]
andarr[5]
: 2 ≤ 2 ✓
- This array is K-increasing for
k = 2
But for k = 1
, we'd need arr[0] ≤ arr[1]
, which means 4 ≤ 1, which is false.
In one operation, you can select any index and change its value to any positive integer. The goal is to find the minimum number of such operations needed to make the array K-increasing.
The solution approach leverages the fact that making each subsequence non-decreasing is equivalent to finding the longest non-decreasing subsequence within each group and changing the remaining elements. This is achieved by finding the minimum elements to change in each of the k
subsequences formed by taking every k-th element starting from positions 0 through k-1.
Intuition
The key insight is recognizing that the K-increasing property creates independent subsequences within the array. When we look at indices that are k
apart, they form chains that don't interact with each other.
Consider indices 0, k, 2k, 3k, ... - these form one chain. Similarly, indices 1, k+1, 2k+1, ... form another chain. In total, we have exactly k
such chains, and each chain must be non-decreasing for the array to be K-increasing.
Here's the crucial observation: changing an element in one chain doesn't affect the other chains. This means we can solve the problem independently for each chain and sum up the results.
For each chain, we need to make it non-decreasing with minimum changes. This is a classic problem: given a sequence, what's the minimum number of elements to change to make it non-decreasing?
The answer lies in finding the longest non-decreasing subsequence (not necessarily contiguous) within that chain. Why? Because:
- The longest non-decreasing subsequence represents the maximum number of elements we can keep unchanged
- All other elements must be modified to fit into this non-decreasing pattern
- Therefore, minimum changes = total elements - longest non-decreasing subsequence length
For finding the longest non-decreasing subsequence efficiently, we use a clever binary search technique. We maintain an array t
where t[i]
represents the smallest ending value of all non-decreasing subsequences of length i+1
. When we encounter a new element x
:
- We find where
x
fits usingbisect_right
(finding the rightmost position where we can insertx
while maintaining sorted order) - If
x
is larger than all elements int
, we've found a longer subsequence - Otherwise, we update
t
to keep track of the best (smallest) ending value for subsequences of that length
The final answer is the sum of minimum changes needed across all k
chains, which the solution computes as sum(lis(arr[i::k]) for i in range(k))
where arr[i::k]
extracts the i-th chain.
Learn more about Binary Search patterns.
Solution Approach
The implementation consists of two main parts: a helper function to find the minimum changes needed for a single subsequence, and the main logic that applies this to all k
subsequences.
Helper Function - Finding Minimum Changes (lis
function):
The lis
function calculates how many elements need to be changed to make a sequence non-decreasing:
-
Initialize an empty array
t
: This array will maintain the smallest ending values of non-decreasing subsequences of different lengths. -
Process each element
x
in the array:- Use
bisect_right(t, x)
to find the rightmost position wherex
can be inserted while keepingt
sorted - This position tells us the length of the longest non-decreasing subsequence ending with a value ≤
x
- Use
-
Update the tracking array:
- If
idx == len(t)
, it meansx
is larger than or equal to all elements int
, so we can extend the longest subsequence by appendingx
- Otherwise, we update
t[idx] = x
to maintain the invariant thatt[i]
stores the smallest ending value for subsequences of lengthi+1
- If
-
Calculate minimum changes: Return
len(arr) - len(t)
, wherelen(t)
represents the length of the longest non-decreasing subsequence that can be kept unchanged.
Main Solution Logic:
The main function leverages the independence of the k
subsequences:
return sum(lis(arr[i::k]) for i in range(k))
This line does the following:
- For each starting position
i
from0
tok-1
- Extract the subsequence using slice notation
arr[i::k]
, which gets elements at indicesi
,i+k
,i+2k
, etc. - Apply the
lis
function to find minimum changes needed for that subsequence - Sum all the minimum changes across all
k
subsequences
Time Complexity: O(n log n)
where n
is the length of the array. Each element is processed once, and for each element, we perform a binary search operation.
Space Complexity: O(n)
in the worst case for storing the tracking array t
and the subsequences.
The elegance of this solution lies in decomposing the problem into independent subproblems and using an efficient algorithm (longest non-decreasing subsequence with binary search) to solve each subproblem optimally.
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 arr = [5, 4, 3, 2, 1]
and k = 2
.
Step 1: Identify the k subsequences
With k = 2
, we have 2 independent subsequences:
- Subsequence 0 (indices 0, 2, 4):
[5, 3, 1]
- Subsequence 1 (indices 1, 3):
[4, 2]
Step 2: Process Subsequence 0: [5, 3, 1]
Using the lis
function to find the longest non-decreasing subsequence:
- Process element 5:
t = [5]
(start with 5) - Process element 3:
bisect_right([5], 3) = 0
, so updatet[0] = 3
, givingt = [3]
- Process element 1:
bisect_right([3], 1) = 0
, so updatet[0] = 1
, givingt = [1]
The longest non-decreasing subsequence has length 1 (we can keep just [1]
).
Minimum changes needed: 3 - 1 = 2
elements must be changed.
Step 3: Process Subsequence 1: [4, 2]
Using the lis
function:
- Process element 4:
t = [4]
(start with 4) - Process element 2:
bisect_right([4], 2) = 0
, so updatet[0] = 2
, givingt = [2]
The longest non-decreasing subsequence has length 1 (we can keep just [2]
).
Minimum changes needed: 2 - 1 = 1
element must be changed.
Step 4: Sum the results
Total minimum operations = 2 + 1 = 3
Verification of the result:
One possible K-increasing array after 3 changes could be [1, 2, 3, 4, 5]
:
- Changed indices 0, 2, and 3
- Now checking K-increasing property:
arr[0] ≤ arr[2]
: 1 ≤ 3 ✓arr[1] ≤ arr[3]
: 2 ≤ 4 ✓arr[2] ≤ arr[4]
: 3 ≤ 5 ✓
The algorithm correctly identifies that we need 3 operations to make the array K-increasing.
Solution Implementation
1from typing import List
2import bisect
3
4class Solution:
5 def kIncreasing(self, arr: List[int], k: int) -> int:
6 """
7 Find minimum operations to make array k-increasing.
8 A k-increasing array means arr[i] <= arr[i+k] for all valid i.
9
10 Args:
11 arr: Input array of integers
12 k: The step size for k-increasing property
13
14 Returns:
15 Minimum number of elements to change
16 """
17
18 def find_min_changes_to_non_decreasing(subsequence: List[int]) -> int:
19 """
20 Calculate minimum changes needed to make a sequence non-decreasing.
21 Uses Longest Non-Decreasing Subsequence (LIS variant) approach.
22
23 Args:
24 subsequence: Array to make non-decreasing
25
26 Returns:
27 Number of elements that need to be changed
28 """
29 # Build the longest non-decreasing subsequence using binary search
30 # tails[i] stores the smallest tail element for non-decreasing subsequence of length i+1
31 tails = []
32
33 for num in subsequence:
34 # Find the rightmost position where num can be placed
35 # bisect_right allows equal elements (non-decreasing property)
36 insertion_pos = bisect.bisect_right(tails, num)
37
38 if insertion_pos == len(tails):
39 # num is larger than or equal to all elements, extend the sequence
40 tails.append(num)
41 else:
42 # Replace the element at insertion_pos to maintain smallest possible tail
43 tails[insertion_pos] = num
44
45 # Return number of elements that need to be changed
46 # (total length - longest non-decreasing subsequence length)
47 return len(subsequence) - len(tails)
48
49 # Process k independent subsequences
50 # Each subsequence consists of elements at positions i, i+k, i+2k, ...
51 total_changes = 0
52 for start_index in range(k):
53 # Extract subsequence starting at start_index with step k
54 subsequence = arr[start_index::k]
55 # Add minimum changes needed for this subsequence
56 total_changes += find_min_changes_to_non_decreasing(subsequence)
57
58 return total_changes
59
1class Solution {
2 /**
3 * Finds the minimum number of elements to change to make the array k-increasing.
4 * An array is k-increasing if arr[i] <= arr[i+k] for all valid indices.
5 *
6 * @param arr the input array
7 * @param k the step size for k-increasing property
8 * @return minimum number of elements to change
9 */
10 public int kIncreasing(int[] arr, int k) {
11 int n = arr.length;
12 int totalChanges = 0;
13
14 // Process each of the k independent subsequences
15 for (int startIndex = 0; startIndex < k; startIndex++) {
16 // Extract elements at positions startIndex, startIndex+k, startIndex+2k, ...
17 List<Integer> subsequence = new ArrayList<>();
18 for (int j = startIndex; j < n; j += k) {
19 subsequence.add(arr[j]);
20 }
21
22 // Add minimum changes needed for this subsequence
23 totalChanges += findMinimumChanges(subsequence);
24 }
25
26 return totalChanges;
27 }
28
29 /**
30 * Finds the minimum number of changes needed to make the sequence non-decreasing.
31 * Uses the Longest Non-Decreasing Subsequence (LIS variant) approach.
32 *
33 * @param sequence the input sequence
34 * @return minimum number of elements to change
35 */
36 private int findMinimumChanges(List<Integer> sequence) {
37 // Build the longest non-decreasing subsequence using patience sorting
38 List<Integer> tailElements = new ArrayList<>();
39
40 for (int value : sequence) {
41 // Find the leftmost position where value can be placed
42 int insertPosition = binarySearchUpperBound(tailElements, value);
43
44 if (insertPosition == tailElements.size()) {
45 // Value is larger than or equal to all elements, append it
46 tailElements.add(value);
47 } else {
48 // Replace the element at insertPosition with current value
49 tailElements.set(insertPosition, value);
50 }
51 }
52
53 // Return the number of elements that need to be changed
54 return sequence.size() - tailElements.size();
55 }
56
57 /**
58 * Binary search to find the leftmost position where x can be inserted
59 * to maintain non-decreasing order (upper bound search).
60 *
61 * @param sortedList the sorted list to search in
62 * @param x the value to search for
63 * @return the index where x should be inserted
64 */
65 private int binarySearchUpperBound(List<Integer> sortedList, int x) {
66 int left = 0;
67 int right = sortedList.size();
68
69 while (left < right) {
70 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
71
72 if (sortedList.get(mid) > x) {
73 // x is smaller, search in left half
74 right = mid;
75 } else {
76 // sortedList.get(mid) <= x, search in right half
77 left = mid + 1;
78 }
79 }
80
81 return left;
82 }
83}
84
1class Solution {
2public:
3 int kIncreasing(vector<int>& arr, int k) {
4 int totalOperations = 0;
5 int n = arr.size();
6
7 // Process each of the k subsequences independently
8 for (int startIndex = 0; startIndex < k; ++startIndex) {
9 // Extract elements at positions startIndex, startIndex+k, startIndex+2k, ...
10 vector<int> subsequence;
11 for (int j = startIndex; j < n; j += k) {
12 subsequence.push_back(arr[j]);
13 }
14
15 // Add the minimum operations needed for this subsequence
16 totalOperations += findMinOperations(subsequence);
17 }
18
19 return totalOperations;
20 }
21
22private:
23 int findMinOperations(vector<int>& arr) {
24 // Find longest non-decreasing subsequence using patience sorting
25 vector<int> lis; // Stores the smallest tail element for each length
26
27 for (int num : arr) {
28 // Find the first element greater than num (for non-decreasing sequence)
29 auto position = upper_bound(lis.begin(), lis.end(), num);
30
31 if (position == lis.end()) {
32 // num is larger than or equal to all elements, extend the sequence
33 lis.push_back(num);
34 } else {
35 // Replace the found element with num to maintain optimal tails
36 *position = num;
37 }
38 }
39
40 // Return number of elements that need to be changed
41 // (total elements - longest non-decreasing subsequence length)
42 return arr.size() - lis.size();
43 }
44};
45
1function kIncreasing(arr: number[], k: number): number {
2 let totalOperations = 0;
3 const n = arr.length;
4
5 // Process each of the k subsequences independently
6 for (let startIndex = 0; startIndex < k; startIndex++) {
7 // Extract elements at positions startIndex, startIndex+k, startIndex+2k, ...
8 const subsequence: number[] = [];
9 for (let j = startIndex; j < n; j += k) {
10 subsequence.push(arr[j]);
11 }
12
13 // Add the minimum operations needed for this subsequence
14 totalOperations += findMinOperations(subsequence);
15 }
16
17 return totalOperations;
18}
19
20function findMinOperations(arr: number[]): number {
21 // Find longest non-decreasing subsequence using patience sorting
22 const lis: number[] = []; // Stores the smallest tail element for each length
23
24 for (const num of arr) {
25 // Find the first element greater than num (for non-decreasing sequence)
26 const position = upperBound(lis, num);
27
28 if (position === lis.length) {
29 // num is larger than or equal to all elements, extend the sequence
30 lis.push(num);
31 } else {
32 // Replace the found element with num to maintain optimal tails
33 lis[position] = num;
34 }
35 }
36
37 // Return number of elements that need to be changed
38 // (total elements - longest non-decreasing subsequence length)
39 return arr.length - lis.length;
40}
41
42// Helper function to find the first element greater than target
43function upperBound(arr: number[], target: number): number {
44 let left = 0;
45 let right = arr.length;
46
47 while (left < right) {
48 const mid = Math.floor((left + right) / 2);
49 if (arr[mid] <= target) {
50 left = mid + 1;
51 } else {
52 right = mid;
53 }
54 }
55
56 return left;
57}
58
Time and Space Complexity
Time Complexity: O(k * (n/k) * log(n/k))
= O(n * log(n/k))
The algorithm processes k
separate subsequences, where each subsequence is formed by taking every k-th element starting from position i
(for i
from 0 to k-1). Each subsequence has approximately n/k
elements.
For each subsequence:
- We iterate through
n/k
elements - For each element, we perform a binary search using
bisect_right
on arrayt
, which takesO(log(len(t)))
time - In the worst case,
len(t)
can be up ton/k
- Therefore, processing one subsequence takes
O((n/k) * log(n/k))
time
Since we process k
such subsequences, the total time complexity is:
k * O((n/k) * log(n/k))
= O(n * log(n/k))
Space Complexity: O(n/k)
The space is dominated by:
- The temporary array
t
in thelis
function, which in the worst case can store all elements of a subsequence, requiringO(n/k)
space - The slicing operation
arr[i::k]
creates a new array of size approximatelyn/k
- Since we process subsequences one at a time (not simultaneously), we only need
O(n/k)
space at any given moment
Therefore, the overall space complexity is O(n/k)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using bisect_left
instead of bisect_right
The Pitfall:
A common mistake is using bisect_left
instead of bisect_right
when finding the insertion position. This subtle difference can lead to incorrect results because we need to handle equal elements properly for a non-decreasing sequence.
Why it matters:
bisect_left(tails, x)
returns the leftmost position wherex
can be insertedbisect_right(tails, x)
returns the rightmost position wherex
can be inserted- For non-decreasing sequences, we need to allow equal consecutive elements
Example of the bug:
# Incorrect implementation
def find_min_changes_incorrect(subsequence):
tails = []
for num in subsequence:
# WRONG: Using bisect_left
insertion_pos = bisect.bisect_left(tails, num)
if insertion_pos == len(tails):
tails.append(num)
else:
tails[insertion_pos] = num
return len(subsequence) - len(tails)
# Test case: [1, 2, 2, 3]
# With bisect_left: Would incorrectly replace the first 2 with the second 2
# With bisect_right: Correctly extends the sequence to include both 2s
The Solution:
Always use bisect_right
for non-decreasing sequences and bisect_left
for strictly increasing sequences.
2. Confusing K-increasing with every K-th element
The Pitfall: Misunderstanding the problem statement and thinking that only every k-th element needs to be checked (like checking arr[0], arr[k], arr[2k] only), rather than understanding that ALL elements at distance k must satisfy the condition.
Wrong interpretation:
# INCORRECT: Only checking one subsequence
def wrong_approach(arr, k):
# This only checks elements at indices 0, k, 2k, ...
subsequence = arr[::k]
return find_min_changes(subsequence)
Correct interpretation:
# CORRECT: Checking all k subsequences
def correct_approach(arr, k):
total = 0
for i in range(k):
# Each subsequence: arr[i], arr[i+k], arr[i+2k], ...
subsequence = arr[i::k]
total += find_min_changes(subsequence)
return total
3. Not handling edge cases properly
The Pitfall: Failing to consider edge cases like:
- When
k >= len(arr)
: No elements need to satisfy the k-increasing property - When
k = 1
: The entire array must be non-decreasing - Empty subsequences when array length is small
The Solution:
def kIncreasing(self, arr: List[int], k: int) -> int:
# Edge case handling
if not arr or k >= len(arr):
return 0
total_changes = 0
for start_index in range(k):
subsequence = arr[start_index::k]
if subsequence: # Only process non-empty subsequences
total_changes += find_min_changes_to_non_decreasing(subsequence)
return total_changes
4. Memory inefficiency with large arrays
The Pitfall: Creating all k subsequences at once can consume excessive memory for large arrays.
Inefficient approach:
# Creates all subsequences in memory at once
subsequences = [arr[i::k] for i in range(k)]
Memory-efficient solution: Process one subsequence at a time as shown in the original solution, or use generators if needed for extremely large datasets.
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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
Want a Structured Path to Master System Design Too? Don’t Miss This!