以下为力扣的官方题解
题目
在仅包含 0 0 0 和 1 1 1 的数组 A A A 中,一次 K K K 位翻转包括选择一个长度为 K K K 的(连续)子数组,同时将子数组中的每个 0 0 0 更改为 1 1 1,而每个 1 1 1 更改为 0。
返回所需的 K K K 位翻转的最小次数,以便数组没有值为 0 0 0 的元素。如果不可能,返回 − 1 -1 −1。
示例1
输入: A = [ 0 , 1 , 0 ] , K = 1 A = [0,1,0], K = 1 A=[0,1,0],K=1
输出: 2 2 2
解释:先翻转 A [ 0 ] A[0] A[0],然后翻转 A [ 2 ] A[2] A[2]。
示例2
输入: A = [ 1 , 1 , 0 ] , K = 2 A = [1,1,0], K = 2 A=[1,1,0],K=2
输出: − 1 -1 −1
解释:无论我们怎样翻转大小为 2 2 2 的子数组,我们都不能使数组变为 [ 1 , 1 , 1 ] [1,1,1] [1,1,1]。
示例3
输入: A = [ 0 , 0 , 0 , 1 , 0 , 1 , 1 , 0 ] , K = 3 A = [0,0,0,1,0,1,1,0], K = 3 A=[0,0,0,1,0,1,1,0],K=3
输出: 3 3 3
解释:
翻转 A [ 0 ] , A [ 1 ] , A [ 2 ] : A[0],A[1],A[2]: A[0],A[1],A[2]: A A A 变成 [ 1 , 1 , 1 , 1 , 0 , 1 , 1 , 0 ] [1,1,1,1,0,1,1,0] [1,1,1,1,0,1,1,0]
翻转 A [ 4 ] , A [ 5 ] , A [ 6 ] : A[4],A[5],A[6]: A[4],A[5],A[6]: A A A 变成 [ 1 , 1 , 1 , 1 , 1 , 0 , 0 , 0 ] [1,1,1,1,1,0,0,0] [1,1,1,1,1,0,0,0]
翻转 A [ 5 ] , A [ 6 ] , A [ 7 ] : A[5],A[6],A[7]: A[5],A[6],A[7]: A A A 变成 [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] [1,1,1,1,1,1,1,1] [1,1,1,1,1,1,1,1]
提示
- 1 < = A . l e n g t h < = 30000 1 <= A.length <= 30000 1<=A.length<=30000
- 1 < = K < = A . l e n g t h 1 <= K <= A.length 1<=K<=A.length
官方题解
方法一 差分数组
由于对同一个子数组执行两次翻转操作不会改变该子数组,所以对每个长度为 K K K 的子数组,应该至多执行一次翻转操作。
对于若干个 K K K 位翻转操作,改变先后顺序并不影响翻转的结果。不妨从 A [ 0 ] A[0] A[0] 开始考虑,若 A [ 0 ] = 0 A[0] = 0 A[0]=0,则必定要翻转从位置 0 0 0 开始的子数组;若 A [ 0 ] = 1 A[0] = 1 A[0]=1,则不翻转从位置 0 0 0 开始的子数组。
按照这一策略,我们从左到右地执行这些翻转操作。由于翻转操作是唯一的,若最终数组元素均为 1 1 1,则执行的翻转次数就是最小的。
用 N N N 表示数组 A A A 的长度。若直接模拟上述过程,复杂度将会是 O ( N K ) O(NK) O(NK)。如何优化呢?
考虑不去翻转数字,而是统计每个数字需要翻转的次数。对于一次翻转操作,相当于把子数组中所有数字的翻转次数加 1 1 1。
这启发我们用差分数组的思想来计算当前数字需要翻转的次数。我们可以维护一个差分数组 d i f f diff diff,其中 d i f f [ i ] diff[i] diff[i] 表示两个相邻元素 A [ i − 1 ] A[i-1] A[i−1] 和 A [ i ] A[i] A[i] 的翻转次数的差,对于区间 [ l , r ] [l, r] [l,r],将其元素全部加 1 1 1,只会影响到 l l l 和 r + 1 r+1 r+1 处的差分值,故 d i f f [ l ] diff[l] diff[l] 增加 1 1 1, d i f f [ r + 1 ] diff[r+1] diff[r+1] 减少 1 1 1。
通过累加差分数组可以得到当前位置需要翻转的次数,我们用变量 r e v C n t revCnt revCnt 来表示这一累加值。
遍历到 A [ i ] A[i] A[i] 时,若 A [ i ] + r e v C n t A[i]+revCnt A[i]+revCnt 是偶数,则说明当前元素的实际值为 0 0 0,需要翻转区间 [ i , i + K − 1 ] [i, i+K-1] [i,i+K−1],我们可以直接将 r e v C n t revCnt revCnt 增加 1 1 1, d i f f [ i + K ] diff[i+K] diff[i+K] 减少 1 1 1。
注意到若 i + K > n i+K \gt n i+K>n,则无法执行翻转操作,此时应返回 − 1 -1 −1。
代码
class Solution {
public int minKBitFlips(int[] A, int K) {
int n = A.length;
int[] diff = new int[n+1];
int ans = 0, revCnt = 0;
for (int i=0; i<n; i++)
{
revCnt += diff[i];
if ((A[i]+revCnt) % 2 == 0)
{
if (i+K > n)
return -1;
ans ++;
revCnt ++;
diff[i+K] --;
}
}
return ans;
}
}
由于模 2 2 2 意义下的加减法与异或等价,我们也可以用异或改写上面的代码。
class Solution {
public int minKBitFlips(int[] A, int K) {
int n = A.length;
int[] diff = new int[n+1];
int ans = 0, revCnt = 0;
for (int i=0; i<n; i++)
{
revCnt ^= diff[i];
if (A[i] == revCnt) // A[i] ^ revCnt == 0
{
if (i+K > n)
return -1;
ans ++;
revCnt ^= 1;
diff[i+K] ^=1;
}
}
return ans;
}
}
复杂度分析
- 时间复杂度: O ( N ) O(N) O(N),其中 N N N 是数组 A A A 的长度。需要对数组 A A A 遍历一次。
- 空间复杂度: O ( N ) O(N) O(N),其中 N N N 是数组 A A A 的长度。需要创建一个长度为 N + 1 N+1 N+1 的差分数组 d i f f diff diff。
方法二 滑动窗口
能否将空间复杂度优化至 O ( 1 ) O(1) O(1) 呢?
回顾方法一的代码,当遍历到位置 i i i 时,若能知道位置 i − K i-K i−K 上发生了翻转操作,便可以直接修改 r e v C n t revCnt revCnt,从而去掉 d i f f diff diff 数组。
注意到 0 ≤ A [ i ] ≤ 1 0 \leq A[i] \leq 1 0≤A[i]≤1,我们可以用 A [ i ] A[i] A[i] 范围之外的数来表达「是否翻转过」的含义。
具体来说,若要翻转从位置 i i i 开始的子数组,可以将 A [ i ] A[i] A[i] 加 2 2 2,这样当遍历到位置 i ′ i' i′ 时,若有 A [ i ′ − K ] > 1 A[i'-K] \gt 1 A[i′−K]>1,则说明在位置 i ′ − K i'-K i′−K 上发生了翻转操作。
代码
class Solution {
public int minKBitFlips(int[] A, int K) {
int n = A.length;
int ans = 0, revCnt = 0;
for (int i=0; i<n; i++)
{
if (i>=K && A[i-K]>1)
{
revCnt ^= 1;
A[i-K] -= 2; // 复原数组元素,若允许修改数组 A,则可以省略
}
if (A[i] == revCnt)
{
if (i+K > n)
return -1;
ans ++;
revCnt ^= 1;
A[i] +=2;
}
}
return ans;
}
}
复杂度分析
- 时间复杂度: O ( N ) O(N) O(N)。其中 N N N 是数组 A A A 的长度,需要对数组 A A A 遍历一次。
- 空间复杂度: O ( 1 ) O(1) O(1)。