Facebook Pixel

1703. Minimum Adjacent Swaps for K Consecutive Ones

Problem Description

You have an array nums that contains only 0s and 1s. You can perform moves where each move allows you to swap two adjacent elements in the array.

Your goal is to rearrange the array so that there are k consecutive 1s somewhere in the array. You need to find the minimum number of moves required to achieve this.

For example, if you have nums = [1,0,0,1,0,1] and k = 2, you want to get at least one position in the array where two 1s are next to each other, like [0,1,1,0,0,1] or [1,0,0,0,1,1], etc. The problem asks for the minimum number of adjacent swaps needed to create such a configuration.

The key insight is that you're trying to gather k existing 1s from the array into consecutive positions, and you can only move elements by swapping adjacent pairs. The challenge is to determine which k ones to group together and where to group them to minimize the total number of swaps.

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

Intuition

The key observation is that we only care about the positions of 1s in the array since we need to group k of them together. The 0s are just obstacles that we need to move past.

First, let's extract all positions where 1s appear. If we have 1s at positions [1, 4, 7, 10] and want to group k=3 of them, we need to choose which 3 to group and where to place them.

A crucial insight: when we want to group k ones together into consecutive positions, the optimal strategy is to move them towards their median position. Why? Because the median minimizes the sum of distances. If we pick any k consecutive target positions [p, p+1, p+2, ..., p+k-1] and want to move k ones from positions [a₁, a₂, ..., aₖ] to these spots, the total moves needed is the sum of distances each 1 needs to travel.

Consider this: if we have ones at positions [2, 5, 9] and want them consecutive, moving them to positions around 5 (the median) like [4, 5, 6] requires fewer moves than moving them to positions around 2 or 9.

The calculation becomes: for each possible group of k ones, find their median position. Then calculate how many swaps are needed to bring all ones in this group to consecutive positions centered around the median. The ones to the left of the median need to move to positions [median-x+1, ..., median] and the ones to the right need to move to positions [median+1, ..., median+y].

Using prefix sums helps us quickly calculate the sum of positions for any subarray of ones, which we need to determine the total distance all ones must travel. The formula accounts for both the current positions (from prefix sums) and the target positions (arithmetic sequence around the median).

By trying all possible groups of k consecutive ones in our extracted positions array and choosing the one with minimum cost, we get our answer.

Learn more about Greedy, Prefix Sum and Sliding Window patterns.

Solution Approach

Let's walk through the implementation step by step:

Step 1: Extract positions of all 1s

arr = [i for i, x in enumerate(nums) if x]

We create an array arr containing all indices where nums[i] = 1. For example, if nums = [1,0,0,1,0,1], then arr = [0, 3, 5].

Step 2: Build prefix sum array

s = list(accumulate(arr, initial=0))

We compute the prefix sum array s where s[i] represents the sum of the first i elements in arr. This allows us to quickly calculate the sum of positions for any subarray of ones in O(1) time.

Step 3: Set up variables for median-based calculation

x = (k + 1) // 2  # Number of elements on the left (including median)
y = k - x         # Number of elements on the right

For a group of k ones, we split them around the median: x elements on the left (including the median itself) and y elements on the right.

Step 4: Enumerate all possible median positions

for i in range(x - 1, len(arr) - y):

We iterate through all valid median positions. The median must have at least x-1 elements before it and y elements after it in the arr array.

Step 5: Calculate movement costs for each median For each median position i:

  • j = arr[i] is the actual index of the median in the original nums array
  • ls = s[i+1] - s[i+1-x] is the sum of positions of the left x ones
  • rs = s[i+1+y] - s[i+1] is the sum of positions of the right y ones

The target positions are:

  • Left ones should move to positions [j-x+1, j-x+2, ..., j]
  • Right ones should move to positions [j+1, j+2, ..., j+y]

The cost calculations:

  • a = (j + j - x + 1) * x // 2 - ls: Cost to move left ones

    • (j + j - x + 1) * x // 2 is the sum of target positions (arithmetic sequence)
    • Subtract ls (current positions) to get the total distance
  • b = rs - (j + 1 + j + y) * y // 2: Cost to move right ones

    • (j + 1 + j + y) * y // 2 is the sum of target positions
    • rs minus this gives the total distance

Step 6: Track minimum cost

ans = min(ans, a + b)

We keep track of the minimum total cost across all possible median positions.

The algorithm efficiently finds the optimal grouping of k ones by leveraging the median property and prefix sums to achieve O(n) time complexity after the initial setup.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with nums = [1,0,0,0,1,0,1,1] and k = 3.

Step 1: Extract positions of 1s

  • The 1s appear at indices: arr = [0, 4, 6, 7]
  • We have 4 ones total, and need to group 3 of them consecutively

Step 2: Build prefix sum array

  • s = [0, 0, 4, 10, 17] (cumulative sums of positions)
  • This helps us quickly calculate sum of positions for any subarray

Step 3: Set up median split

  • x = (3 + 1) // 2 = 2 (elements on left including median)
  • y = 3 - 2 = 1 (elements on right of median)

Step 4 & 5: Try each possible median

Option 1: Median at index 1 (position 4 in original array)

  • Group ones at positions [0, 4, 6]
  • Median position j = 4
  • Left ones (positions 0, 4) should move to positions [3, 4]
  • Right one (position 6) should move to position [5]
  • Cost calculation:
    • ls = s[2] - s[0] = 4 - 0 = 4 (sum of left positions)
    • rs = s[3] - s[2] = 10 - 4 = 6 (sum of right positions)
    • Left cost: (4 + 3) * 2 / 2 - 4 = 7 - 4 = 3 moves
    • Right cost: 6 - 5 = 1 move
    • Total: 3 + 1 = 4 moves

Option 2: Median at index 2 (position 6 in original array)

  • Group ones at positions [4, 6, 7]
  • Median position j = 6
  • Left ones (positions 4, 6) should move to positions [5, 6]
  • Right one (position 7) should move to position [7]
  • Cost calculation:
    • ls = s[3] - s[1] = 10 - 0 = 10 (sum of left positions)
    • rs = s[4] - s[3] = 17 - 10 = 7 (sum of right positions)
    • Left cost: (6 + 5) * 2 / 2 - 10 = 11 - 10 = 1 move
    • Right cost: 7 - 7 = 0 moves
    • Total: 1 + 0 = 1 move

Step 6: Return minimum

  • Minimum cost is 1 move (Option 2)
  • This corresponds to moving the 1 at position 4 to position 5
  • Final array: [1,0,0,0,0,1,1,1] with 3 consecutive 1s at the end

The algorithm correctly identifies that grouping the last three 1s requires minimal movement, as they're already close together.

Solution Implementation

1class Solution:
2    def minMoves(self, nums: List[int], k: int) -> int:
3        # Extract indices where nums[i] == 1
4        ones_indices = [i for i, value in enumerate(nums) if value == 1]
5      
6        # Create prefix sum array for quick range sum calculations
7        # prefix_sums[i] contains sum of first i elements from ones_indices
8        prefix_sums = list(accumulate(ones_indices, initial=0))
9      
10        min_moves = float('inf')
11      
12        # Split k into left and right parts around a center position
13        # left_count: number of ones to collect on the left side (including center if k is odd)
14        # right_count: number of ones to collect on the right side
15        left_count = (k + 1) // 2
16        right_count = k - left_count
17      
18        # Try each possible center position for collecting k ones
19        for center_idx in range(left_count - 1, len(ones_indices) - right_count):
20            # Get the actual position in nums array for the center
21            center_pos = ones_indices[center_idx]
22          
23            # Calculate sum of positions for left group using prefix sums
24            # Elements from index (center_idx - left_count + 1) to center_idx
25            left_sum = prefix_sums[center_idx + 1] - prefix_sums[center_idx + 1 - left_count]
26          
27            # Calculate sum of positions for right group using prefix sums
28            # Elements from index (center_idx + 1) to (center_idx + right_count)
29            right_sum = prefix_sums[center_idx + 1 + right_count] - prefix_sums[center_idx + 1]
30          
31            # Calculate moves needed for left group
32            # Target positions: center_pos - left_count + 1, ..., center_pos
33            # Sum of target positions using arithmetic sequence formula
34            left_target_sum = (center_pos + center_pos - left_count + 1) * left_count // 2
35            left_moves = left_target_sum - left_sum
36          
37            # Calculate moves needed for right group
38            # Target positions: center_pos + 1, ..., center_pos + right_count
39            # Sum of target positions using arithmetic sequence formula
40            right_target_sum = (center_pos + 1 + center_pos + right_count) * right_count // 2
41            right_moves = right_sum - right_target_sum
42          
43            # Update minimum moves
44            total_moves = left_moves + right_moves
45            min_moves = min(min_moves, total_moves)
46      
47        return min_moves
48
1class Solution {
2    public int minMoves(int[] nums, int k) {
3        // Step 1: Collect all indices where nums[i] == 1
4        List<Integer> onePositions = new ArrayList<>();
5        int n = nums.length;
6        for (int i = 0; i < n; i++) {
7            if (nums[i] != 0) {
8                onePositions.add(i);
9            }
10        }
11      
12        // Step 2: Build prefix sum array for quick range sum queries
13        int totalOnes = onePositions.size();
14        int[] prefixSum = new int[totalOnes + 1];
15        for (int i = 0; i < totalOnes; i++) {
16            prefixSum[i + 1] = prefixSum[i] + onePositions.get(i);
17        }
18      
19        // Step 3: Find minimum cost using sliding window approach
20        long minCost = 1L << 60; // Initialize with a very large value
21      
22        // Split k into left and right parts around median
23        int leftCount = (k + 1) / 2;  // Number of ones to the left (including median)
24        int rightCount = k - leftCount; // Number of ones to the right of median
25      
26        // Iterate through all possible median positions
27        for (int medianIndex = leftCount - 1; medianIndex < totalOnes - rightCount; medianIndex++) {
28            int medianPosition = onePositions.get(medianIndex);
29          
30            // Calculate sum of positions for left group
31            int leftSum = prefixSum[medianIndex + 1] - prefixSum[medianIndex + 1 - leftCount];
32          
33            // Calculate sum of positions for right group
34            int rightSum = prefixSum[medianIndex + 1 + rightCount] - prefixSum[medianIndex + 1];
35          
36            // Calculate cost to move left group to consecutive positions ending at median
37            // Target positions: [medianPosition - leftCount + 1, ..., medianPosition]
38            long leftCost = (medianPosition + medianPosition - leftCount + 1L) * leftCount / 2 - leftSum;
39          
40            // Calculate cost to move right group to consecutive positions starting after median
41            // Target positions: [medianPosition + 1, ..., medianPosition + rightCount]
42            long rightCost = rightSum - (medianPosition + 1L + medianPosition + rightCount) * rightCount / 2;
43          
44            // Update minimum cost
45            minCost = Math.min(minCost, leftCost + rightCost);
46        }
47      
48        return (int) minCost;
49    }
50}
51
1class Solution {
2public:
3    int minMoves(vector<int>& nums, int k) {
4        // Step 1: Extract positions of all 1s in the array
5        vector<int> onesPositions;
6        for (int i = 0; i < nums.size(); ++i) {
7            if (nums[i] == 1) {
8                onesPositions.push_back(i);
9            }
10        }
11      
12        int totalOnes = onesPositions.size();
13      
14        // Step 2: Build prefix sum array for quick range sum queries
15        // Note: prefixSum[0] is initialized to 0 (was 1 in original, likely a bug)
16        long prefixSum[totalOnes + 1];
17        prefixSum[0] = 0;
18        for (int i = 0; i < totalOnes; ++i) {
19            prefixSum[i + 1] = prefixSum[i] + onesPositions[i];
20        }
21      
22        // Step 3: Initialize answer with a large value
23        long minCost = 1L << 60;
24      
25        // Step 4: Calculate split points for k elements
26        // leftCount: number of elements to the left of (and including) the median
27        // rightCount: number of elements to the right of the median
28        int leftCount = (k + 1) / 2;
29        int rightCount = k - leftCount;
30      
31        // Step 5: Try each possible median position
32        // The median will be at index i in onesPositions array
33        for (int medianIdx = leftCount - 1; medianIdx < totalOnes - rightCount; ++medianIdx) {
34            int medianPos = onesPositions[medianIdx];
35          
36            // Calculate sum of positions for left group (including median)
37            long leftSum = prefixSum[medianIdx + 1] - prefixSum[medianIdx + 1 - leftCount];
38          
39            // Calculate sum of positions for right group
40            long rightSum = prefixSum[medianIdx + 1 + rightCount] - prefixSum[medianIdx + 1];
41          
42            // Calculate cost to move left elements to consecutive positions
43            // Target positions: medianPos - leftCount + 1, ..., medianPos
44            // Cost formula: sum of (target position - actual position)
45            long leftCost = (medianPos + medianPos - leftCount + 1L) * leftCount / 2 - leftSum;
46          
47            // Calculate cost to move right elements to consecutive positions  
48            // Target positions: medianPos + 1, ..., medianPos + rightCount
49            // Cost formula: sum of (actual position - target position)
50            long rightCost = rightSum - (medianPos + 1L + medianPos + rightCount) * rightCount / 2;
51          
52            // Update minimum cost
53            minCost = min(minCost, leftCost + rightCost);
54        }
55      
56        return minCost;
57    }
58};
59
1function minMoves(nums: number[], k: number): number {
2    // Step 1: Extract positions of all 1s in the array
3    const onesPositions: number[] = [];
4    for (let i = 0; i < nums.length; i++) {
5        if (nums[i] === 1) {
6            onesPositions.push(i);
7        }
8    }
9  
10    const totalOnes = onesPositions.length;
11  
12    // Step 2: Build prefix sum array for quick range sum queries
13    // prefixSum[i] stores the sum of first i elements from onesPositions
14    const prefixSum: number[] = new Array(totalOnes + 1);
15    prefixSum[0] = 0;
16    for (let i = 0; i < totalOnes; i++) {
17        prefixSum[i + 1] = prefixSum[i] + onesPositions[i];
18    }
19  
20    // Step 3: Initialize answer with a large value
21    let minCost = Number.MAX_SAFE_INTEGER;
22  
23    // Step 4: Calculate split points for k elements
24    // leftCount: number of elements to the left of (and including) the median
25    // rightCount: number of elements to the right of the median
26    const leftCount = Math.floor((k + 1) / 2);
27    const rightCount = k - leftCount;
28  
29    // Step 5: Try each possible median position
30    // The median will be at index medianIdx in onesPositions array
31    for (let medianIdx = leftCount - 1; medianIdx < totalOnes - rightCount; medianIdx++) {
32        const medianPos = onesPositions[medianIdx];
33      
34        // Calculate sum of positions for left group (including median)
35        const leftSum = prefixSum[medianIdx + 1] - prefixSum[medianIdx + 1 - leftCount];
36      
37        // Calculate sum of positions for right group
38        const rightSum = prefixSum[medianIdx + 1 + rightCount] - prefixSum[medianIdx + 1];
39      
40        // Calculate cost to move left elements to consecutive positions
41        // Target positions: medianPos - leftCount + 1, ..., medianPos
42        // Cost formula: sum of (target position - actual position)
43        const leftCost = (medianPos + medianPos - leftCount + 1) * leftCount / 2 - leftSum;
44      
45        // Calculate cost to move right elements to consecutive positions
46        // Target positions: medianPos + 1, ..., medianPos + rightCount
47        // Cost formula: sum of (actual position - target position)
48        const rightCost = rightSum - (medianPos + 1 + medianPos + rightCount) * rightCount / 2;
49      
50        // Update minimum cost
51        minCost = Math.min(minCost, leftCost + rightCost);
52    }
53  
54    return minCost;
55}
56

Time and Space Complexity

Time Complexity: O(n + m), which simplifies to O(n) since m ≤ n

  • Creating the arr list requires iterating through all n elements of nums to find positions of 1s: O(n)
  • Building the prefix sum array s using accumulate on arr of length m: O(m)
  • The main loop iterates m - k + 1 times (from x - 1 to len(arr) - y - 1), which is O(m)
  • Each iteration performs constant time operations: O(1)
  • Overall: O(n) + O(m) + O(m) = O(n + m) = O(n)

Space Complexity: O(m)

  • The arr list stores indices of all 1s in nums, requiring O(m) space where m is the number of 1s
  • The prefix sum array s has length m + 1: O(m)
  • Other variables (ans, x, y, i, j, etc.) use constant space: O(1)
  • Overall: O(m) + O(m) + O(1) = O(m)

Here, n is the length of the array nums and m is the number of 1s in the array nums.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Off-by-One Errors in Index Calculations

One of the most common mistakes is incorrectly calculating the range boundaries when iterating through possible center positions or when computing prefix sum ranges.

Pitfall Example:

# Incorrect: Missing the -1 adjustment
for center_idx in range(left_count, len(ones_indices) - right_count):
    # This starts one position too late

Correct Approach:

# Start from left_count - 1 because we need left_count elements total,
# and the center itself is included in that count
for center_idx in range(left_count - 1, len(ones_indices) - right_count):

2. Misunderstanding the Median Split for Odd vs Even k

When k is odd, the median element is included in the left group, but developers often forget this asymmetry.

Pitfall Example:

# Incorrect: Treating left and right symmetrically
left_count = k // 2
right_count = k // 2
if k % 2 == 1:
    # Forgot to handle the middle element

Correct Approach:

left_count = (k + 1) // 2  # Includes median for odd k
right_count = k - left_count  # Automatically handles both cases

3. Incorrect Target Position Calculation

Calculating the sum of target positions using arithmetic sequences can be error-prone, especially with the boundary values.

Pitfall Example:

# Incorrect: Wrong arithmetic sequence formula
left_target_sum = (center_pos - left_count + center_pos) * left_count // 2
# Missing the +1 adjustment in the first term

Correct Approach:

# For positions [center_pos - left_count + 1, ..., center_pos]
# First term: center_pos - left_count + 1
# Last term: center_pos
left_target_sum = (center_pos - left_count + 1 + center_pos) * left_count // 2

4. Not Handling Edge Cases

Failing to check if there are enough 1s in the array or if k equals the total number of 1s.

Pitfall Example:

def minMoves(self, nums: List[int], k: int) -> int:
    ones_indices = [i for i, value in enumerate(nums) if value == 1]
    # Directly proceed without checking if len(ones_indices) >= k

Correct Approach:

def minMoves(self, nums: List[int], k: int) -> int:
    ones_indices = [i for i, value in enumerate(nums) if value == 1]
  
    # Handle edge cases
    if len(ones_indices) < k:
        return -1  # Or handle according to problem constraints
    if k == 1:
        return 0  # Already have a consecutive group of 1

5. Confusion Between Index in ones_indices vs Original Array

Mixing up the index within the ones_indices array and the actual position in the original nums array.

Pitfall Example:

for center_idx in range(left_count - 1, len(ones_indices) - right_count):
    # Incorrect: Using center_idx directly as position
    left_moves = calculate_moves(center_idx, ...)  # Wrong!

Correct Approach:

for center_idx in range(left_count - 1, len(ones_indices) - right_count):
    # Get actual position in original array
    center_pos = ones_indices[center_idx]
    # Use center_pos for position calculations
    left_moves = calculate_moves(center_pos, ...)

Prevention Strategy:

  1. Draw out small examples with k=2, k=3 to verify index calculations
  2. Use descriptive variable names to distinguish between indices and positions
  3. Add assertions during development to verify intermediate calculations
  4. Test with edge cases like k=1, k=len(ones_indices), and arrays with all 1s clustered at boundaries
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

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

Load More