1. 程式人生 > >[LeetCode] Minimum Number of K Consecutive Bit Flips 連續K位翻轉的最小次數

[LeetCode] Minimum Number of K Consecutive Bit Flips 連續K位翻轉的最小次數

位翻轉 () javac 另一個 -o oss 所有 -s ace



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. 1 <= A.length <=?30000
  2. 1 <= K <= A.length



這道題博主最先在某家公司的 OA 上看到過(OA 出 Hard 題,也真是給跪了~),給了一個只有0和1的數組,又給了一個數字K,說是每次可以翻轉任意連續K個位置的數字,問我們多少步可以把所有的0都翻轉為1。這是道挺有意思的題目,背景可以隨便換,比如什麽烙煎餅啊,翻煎雞蛋啊,都是一樣的問題。註意我們翻轉的時候,一旦選定了起始點,那麽只能翻連著的K個位置,由於無法影響到前面的0的,所以需要在遇到0的時候,就進行翻轉,這就是明顯的貪婪算法的特征,這要一遇到0,就立馬翻連續K個位置,比如對於數組 [0,1,0],K = 2,遇到位置0上的0,翻轉,變為 [1,0,0],此時遇到位置1上的0,翻轉,變為 [1,1,1],操作完成。想法有了,於是就吭哧吭哧的寫好了代碼,交給 OJ 大人審閱,結果被駁回,Time Limit Exceeded,什麽鬼?那麽仔細想一下吧,上面的這種解法到底哪裏最費時,當然是翻轉K個位置了,若K非常大,而且若需要翻轉的位置很多的話,將非常的不高效。所以當務之急就是想辦法代替真實的翻轉,最簡單的方法就是記錄起始翻轉的位置,我們使用一個長度相同的數組 isFlipped,其中 isFlipped[i] 表示在原數組i位置上進行了K個連續翻轉。現在雖然知道了其實翻轉的位置,但是由於是連續翻轉K個,之後的位置也被翻轉,比如 [0,1,0] 在位置0翻轉後,變為了 [1,0,0],那麽位置1就從之前的1變為0了,此時位置1也需要翻轉了,那麽我們怎麽知道非起始翻轉位置的數字當前是0還是1呢,這時就要引入另一個變量 curFlipped 了,表示當前位置的數字跟原數組相比是否被翻轉了。所以一旦我們決定對當前位置進行翻轉,那麽需要將 isFlipped[i] 標記為1,並且翻轉 curFlipped,方法是‘異或‘上1,舉個例子來說對於數組 [0,1,0,1], K=3 來說,當我們對位置0進行翻轉,數組變為 [1,0,1,0],那麽在位置1的時候,curFlipped 是1,表示此時0相對於原來的1是翻轉了,那麽若我們在為1時再次進行翻轉,數組變為 [1,1,0,1],此時 curFlipped 變為0了,表明位置2上的0對於原數組的位置2上的0來說沒有翻轉(雖然實際上我們翻轉了兩次),那麽我們怎麽知道某個位置應不應該翻轉呢?需要根據 curFlipped 的值和原數組 A[i] 的值進行比較來判斷,此時有兩種情況:

  • 當 curFlipped 為0,表示沒有翻轉,且原數組 A[i] 為0,此時就需要翻轉i位置。

  • 當 curFlipped 為1,表示翻轉過了,而原數組 A[i] 為1,表示雖然原來是1,但是當前位置受之前翻轉的影響變成了0,此時就需要翻轉回來。

仔細觀察上面兩種情況可以發現 curFlipped 和 A[i] 同奇同偶的時候就需要翻轉,那麽兩種情況可以合成一個表達式,curFlipped%2 等於 A[i] 時翻轉。還需要弄清楚當什麽時候就無法翻轉了,當 i+K 大於n的時候,比如 [1,1,0,1], k=3 時,在位置2的時候 i+k>n(2+3>4)了,表示無法翻轉了,直接返回 -1 即可。最後需要註意的是,curFlipped 受影響的位置只有在大小為K的窗口中,一旦超過了,是需要還原 curFlipped 的狀態的。比如 [0,0,0,1], K=2 時,在位置0翻轉後變為 [1,1,0,1],那麽到位置2的時候,由於已經不受位置0翻轉的影響了,此時的 curFlipped 應該變回0,所以只要‘異或‘上 isFlipped[0] 進行翻轉,所以我們在循環開始前,首先檢測 i>=K 是否成立,成立的話就讓 curFlipped ‘異或‘上 isFlipped[i-K] 進行狀態還原,即便是位置 i-K 未進行過翻轉,‘異或‘上0也不會改變 curFLipped 的狀態,參見代碼如下:



解法一:

class Solution {
public:
    int minKBitFlips(vector<int>& A, int K) {
        int res = 0, n = A.size(), curFlipped = 0;
        vector<int> isFlipped(n);
        for (int i = 0; i < n; ++i) {
            if (i >= K) curFlipped ^= isFlipped[i - K];
            if (curFlipped % 2 == A[i]) {
                if (i + K > n) return -1;
                isFlipped[i] = 1;
                curFlipped ^= 1;
                ++res;
            }
        }
        return res;
    }
};



根據上面的分析,對於每個位置i,我們其實只關心 i-K 位置是否是起始翻轉的位置,之前什麽情況並不 care,那麽就沒必要用整個數組來保存所有的起始翻轉位置,只需要維護一個大小為K的窗口,將在此窗口內的起始翻轉位置保存即可,超出範圍的就扔掉。那麽我們可以使用一個 queue 來保存窗口內的起始位置,在遍歷的過程中,首先檢測隊首元素是否超出了範圍,是的話就扔掉,否則就接著判斷當前位置是否需要翻轉,判斷的方法是看此窗口中起始翻轉的個數是否跟 A[i] 同奇同偶,推導方式跟上面解法中類似,這裏就不過多講解了,之後就判斷所剩位置是否還有K個,沒有就返回 -1。然後將當前位置加入隊列,結果 res 自增1即可,參見代碼如下:



解法二:

class Solution {
public:
    int minKBitFlips(vector<int>& A, int K) {
        int res = 0, n = A.size();
        queue<int> q;
        for (int i = 0; i < n; ++i) {
            if (!q.empty() && i >= (q.front() + K)) q.pop();
            if (q.size() % 2 == A[i]) {
                if (i + K > n) return -1;
                q.push(i);
                ++res;
            }
        }
        return res;
    }
};



我們還可以進一步的優化空間,連隊列 queue 都不需要,直接在原數組上修改,從而達到標記的目的,在解法一中,我們是新建了一個數組,對於起始翻轉位置標記為1,其余為0。這裏由於原數組已經使用了0和1,我們可以對於起始位置,將 A[i] 加上2,這樣起始翻轉位置的值就成了2或者3,跟原來的0和1就區分開了,那麽只要將 A[i] 除以2,根據商是1還是0,就可以知道該位置是否是起始翻轉位置,其余的地方基本相同,不過這裏的 flipped 更新並沒有用異或,而是直接用的加減,為了 diversity 也是拼了,參見代碼如下:



解法三:

class Solution {
public:
    int minKBitFlips(vector<int>& A, int K) {
        int res = 0, n = A.size(), flipped = 0;
        for (int i = 0; i < n; ++i) {
            if (i >= K) flipped -= A[i - K] / 2;
            if (flipped % 2 == A[i]) {
                if (i + K > n) return -1;
                A[i] += 2;
                ++flipped;
                ++res;
            }
        }
        return res;
    }
};



類似題目:

Bulb Switcher



參考資料:

https://leetcode.com/problems/minimum-number-of-k-consecutive-bit-flips/

https://leetcode.com/problems/minimum-number-of-k-consecutive-bit-flips/discuss/239284/C%2B%2B-greedy-stack-and-O(1)-memory

https://leetcode.com/problems/minimum-number-of-k-consecutive-bit-flips/discuss/239117/Java-O(n)-Sliding-Window-Solution-using-Queue

https://leetcode.com/problems/minimum-number-of-k-consecutive-bit-flips/discuss/238609/JavaC%2B%2BPython-One-Pass-and-O(1)-Space



LeetCode All in One 題目講解匯總(持續更新中...)

[LeetCode] Minimum Number of K Consecutive Bit Flips 連續K位翻轉的最小次數