[LeetCode] 995. Minimum Number of K Consecutive Bit Flips
In an array A
containing only 0s and 1s, a K
-bit flip consists of choosing a (contiguous) subarray of length K
and simultaneously changing every 0 in the subarray to 1, and every 1 in the subarray to 0.
Return the minimum number of K
-bit flips required so that there is no 0 in the array. If it is not possible, return -1
.
Example 1:
Input: A = [0,1,0], K = 1
Output: 2
Explanation: Flip A[0], then flip A[2].
Example 2:
Input: A = [1,1,0], K = 2
Output: -1
Explanation: No matter how we flip subarrays of size 2, we can't make the array become [1,1,1].
Example 3:
Input: A = [0,0,0,1,0,1,1,0], K = 3
Output: 3
Explanation:
Flip A[0],A[1],A[2]: A becomes [1,1,1,1,0,1,1,0]
Flip A[4],A[5],A[6]: A becomes [1,1,1,1,1,0,0,0]
Flip A[5],A[6],A[7]: A becomes [1,1,1,1,1,1,1,1]
Note:
1 <= A.length <= 30000
1 <= K <= A.length
Formation and proof of greedy algorithm: Intuitively, we know if A[0] is 0, then we must flip from the left side since there is only this way to flip A[0]. So the following greedy algorithm may be the right algorithm here. But we must prove it is the correct algorithm.
Greedy Algorithm: from left to right, after considering the previous flips, if A[i] is 0, flip subarray A[i] to A[i + K - 1]. Otherwise don't flip. If A[i] needs to be flipped but the remaining subarray does not have K elements to operate flip on (A.length - i < K), return -1.
Proof of correctness: The above algorithm gives a solution S by greedily flipping 0s from left to right, considering previous flips. We claim to if A[i] is already 1, then we never flip it, else always flip subarray starting from A[i]. Now we'll prove the following solutions does not yield a solution that is better than S.
1. Without loss of generosity, let the i-th bit be the first 0. There is a solution S' that does a flip at A[j], j < i. Since A[j] == 1, a flip will set A[j] to 0. Eventually A[j] must be flipped back to 1, and this can only happen for flips whose start index is in [j - K + 1, j] range. Obviously we can eliminate start index j as that is doing the same flip starting from A[j] twice, essentially wasting 2 flips and achieving nothing. So we know in order to flip A[j] back to 1, a flip that starts at a smaller index j must occur. Because all the bits that come ahead index j are also 1s, we can induct the same reasoning: to flip back a bit that was originally 1, we can only resort to another flip that starts at an smaller index until we reach A[0]. At this point, we can only flip at A[0], wasting 2 flips and achieves nothing. So this proves that from left to right, skipping 1s is always better than flipping 1s.
2. Assume we don't start from left to right, but at some middle index i.
2a. A[i] == 1, based on the case 1 argument, we know skipping A[i] is better.
2b. A[i] == 0. There are two possible cases. 1. If there is no 0s to the left of A[i], then this is the greedy algorithm we formed. 2. If there are 0s to the left of A[i], let j be the index of the first 0 from A[0...i - 1]. Again by case 1 argument, we know that we simply flip A[j]. The same argument applies to rest of 0s in A[0.... i - 1]. As a result, we know starting in the middle does not yield a better result that starting from left.
Solution 1. Naive Greedy Algorithm. O(nK) runtime, O(1) space, original input changed.
class Solution { public int minKBitFlips(int[] A, int K) { int count = 0; for(int i = 0; i < A.length; i++) { if(A[i] == 0 && i + K <= A.length) { for(int j = 0; j < K; j++) { A[i + j] = A[i + j] == 0 ? 1 : 0; } count++; } else if(A[i] == 0) { return -1; } } return count; } }
Solution 2. Sliding window greedy algorithm. O(n) runtime, O(K) space, input not changed.
The bottleneck of the naive solution is that it does not use the previous flip information. For a given index i, whether there needs to be a flip starting from A[i] or not depends the original A[i] value and the impacts of possible flips starting from index [i - K + 1, i - 1]. Flips that occur at index smaller than i - K + 1 do not have any impact on A[i]'s flip decision. To save/update the previous K indices's flip events, we need a sliding window of size K.
1. Use an ArrayDeque of size K to keep the indices of bits that are flipped and a count variable currentEffectiveFlips to keep the flips that occur previously that are also still effective on the current bit A[i].
2. If the earliest flip event saved in the deque is no longer effective, remove it and decrement currentEffectiveFlips by 1.
3. At this point, we have A[i]'s value and the effective previous flip counts, we do the following:
3a. If A[i] has been flipped even number times and A[i] is originally 0, we know A[i] is still 0 so flip it.
3b. or if A[i] has been flipped odd number times and A[i] is originally 1, we know A[i] has been flipped to 0 so flip it.
class Solution { public int minKBitFlips(int[] A, int K) { int totalFlips = 0, currentEffectiveFlips = 0; Deque<Integer> flipIndices = new ArrayDeque<>(); for(int i = 0; i < A.length; i++) { if(flipIndices.size() > 0 && flipIndices.peekFirst() + K == i) { flipIndices.remove(); currentEffectiveFlips--; } if(A[i] == 0 && currentEffectiveFlips % 2 == 0 || A[i] == 1 && currentEffectiveFlips % 2 != 0) { if(i + K > A.length) { return -1; } flipIndices.addLast(i); currentEffectiveFlips++; totalFlips++; } } return totalFlips; } }