Facebook Pixel

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] and arr[2]: 4 ≤ 5 ✓
    • arr[1] and arr[3]: 1 ≤ 2 ✓
    • arr[2] and arr[4]: 5 ≤ 6 ✓
    • arr[3] and arr[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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 using bisect_right (finding the rightmost position where we can insert x while maintaining sorted order)
  • If x is larger than all elements in t, 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:

  1. Initialize an empty array t: This array will maintain the smallest ending values of non-decreasing subsequences of different lengths.

  2. Process each element x in the array:

    • Use bisect_right(t, x) to find the rightmost position where x can be inserted while keeping t sorted
    • This position tells us the length of the longest non-decreasing subsequence ending with a value ≤ x
  3. Update the tracking array:

    • If idx == len(t), it means x is larger than or equal to all elements in t, so we can extend the longest subsequence by appending x
    • Otherwise, we update t[idx] = x to maintain the invariant that t[i] stores the smallest ending value for subsequences of length i+1
  4. Calculate minimum changes: Return len(arr) - len(t), where len(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 from 0 to k-1
  • Extract the subsequence using slice notation arr[i::k], which gets elements at indices i, 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 Evaluator

Example 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 update t[0] = 3, giving t = [3]
  • Process element 1: bisect_right([3], 1) = 0, so update t[0] = 1, giving t = [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 update t[0] = 2, giving t = [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 array t, which takes O(log(len(t))) time
  • In the worst case, len(t) can be up to n/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 the lis function, which in the worst case can store all elements of a subsequence, requiring O(n/k) space
  • The slicing operation arr[i::k] creates a new array of size approximately n/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 where x can be inserted
  • bisect_right(tails, x) returns the rightmost position where x 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More