1. 程式人生 > >[LeetCode] Bricks Falling When Hit 碰撞時磚頭掉落

[LeetCode] Bricks Falling When Hit 碰撞時磚頭掉落

We have a grid of 1s and 0s; the 1s in a cell represent bricks.  A brick will not drop if and only if it is directly connected to the top of the grid, or at least one of its (4-way) adjacent bricks will not drop.

We will do some erasures sequentially. Each time we want to do the erasure at the location (i, j), the brick (if it exists) on that location will disappear, and then some other bricks may drop because of that erasure.

Return an array representing the number of bricks that will drop after each erasure in sequence.

Example 1:
Input: 
grid = [[1,0,0,0],[1,1,1,0]]
hits = [[1,0]]
Output: [2]
Explanation: 
If we erase the brick at (1, 0), the brick at (1, 1) and (1, 2) will drop. So we should return 2.
Example 2:
Input: 
grid = [[1,0,0,0],[1,1,0,0]]
hits = [[1,1],[1,0]]
Output: [0,0]
Explanation: 
When we erase the brick at (1, 0), the brick at (1, 1) has already disappeared due to the last move. So each erasure will cause no bricks dropping.  Note that the erased brick (1, 0) will not be counted as a dropped brick.

Note:

  • The number of rows and columns in the grid will be in the range [1, 200].
  • The number of erasures will not exceed the area of the grid.
  • It is guaranteed that each erasure will be different from any other erasure, and located inside the grid.
  • An erasure may refer to a location with no brick - if it does, no bricks drop.

這道題給了我們一個由0和1組成的grid,說是1代表磚頭,當磚頭連著頂端的時候,就不會掉落,當某個磚頭連著不會掉落的磚頭時,其本身也不會掉落。然後我們要去掉一些磚頭,當去掉某個磚頭時,與其相連的磚頭可能也會同時掉落。所以這裡讓我們求同時掉落的磚頭個數。博主書讀的不少,不會受騙,這尼瑪不就是泡泡龍遊戲麼。其中泡泡龍的一大技巧就是掛葡萄,當關鍵節點處的泡泡被打掉後,這整個一串都可以直接掉下來。這裡也是一樣啊,grid的頂端就是遊戲介面的頂端,然後磚頭就是泡泡,去掉磚頭就是消掉某個地方的泡泡,然後掉落的磚頭就是掉下的泡泡啦。遊戲玩的6,不代表題會做,其實這道題還是蠻有難度的,花了博主很長的時間。

首先我們來想,我們肯定要統計出當前沒有掉落的磚頭數量,當去掉某個磚頭後,我們可以統計當前還連著的磚頭數量,二者做差值就是掉落的磚頭數量。那麼如何來統計不會掉落的磚頭數量呢,由於頂層的磚頭時不會掉落的,那麼跟頂層相連的所有磚頭肯定也不會掉落,我們就可以使用DFS來遍歷,我們可以把不會掉落的磚頭位置存入一個HashSet中,這樣通過比較不同狀態下HashSet中元素的個數,我們就知道掉落了多少磚頭。然後我們再來想一個問題,在沒有去除任何磚頭的時候,我們DFS查詢會遍歷所有的磚頭,當某個磚頭去除後,可能沒有連帶其他的磚頭,那麼如果我們再來遍歷一遍所有相連的磚頭,相當於又把整個陣列搜尋了一遍,這樣並不是很高效。我們可以試著換一個思路,如果我們先把要去掉的所有磚頭都先去掉,這樣我們遍歷所有相連的磚頭就是最終還剩下的磚頭,然後我們從最後一個磚頭開始往回加,每加一個磚頭,我們就以這個磚頭為起點,DFS遍歷其周圍相連的磚頭,加入HashSet中,那麼只會遍歷那些會掉的磚頭,那麼增加的這些磚頭就是會掉的磚頭數量了,然後再不停的在增加前面的磚頭,直到把hits中所有的磚頭都添加回來了,那麼我們也就計算出了每次會掉的磚頭的個數。

好,我們使用一個HashSet來儲存不會掉落的磚頭,然後先遍歷hits陣列,把要掉落的磚頭位置的值都減去一個1,這裡有個需要注意的地方,hits裡的掉落位置實際上在grid中不一定有磚頭,就是說可能是本身為0的位置,那麼我們減1後,陣列中也可能會有-1,沒有太大的影響,不過需要注意一下,這裡不能使用 if (grid[i][j]) 來直接判斷其是否為1,因為非0值-1也會返回true。然後我們對第一行的磚頭都呼叫遞迴函式,因為頂端的磚頭不會掉落,跟頂端的磚頭相連的磚頭也不會掉落,所以要遍歷所有相連的磚頭,將位置都存入noDrop。然後就是從最後一個位置往前加磚頭,先記錄noDrop當前的元素個數,然後grid中對應的值自增1,之後增加後的值為1了,才說明這塊之前是有磚頭的,然後我們看其上下左右位置,若有磚頭,則對當前位置呼叫遞迴,還有一種情況是當前是頂層的話,還是要呼叫遞迴。遞迴呼叫完成後二者的差值再減去1就是掉落的磚頭數,減1的原因是去掉的磚頭不算掉落的磚頭數中,參見程式碼如下:

解法一:

class Solution {
public:
    vector<int> hitBricks(vector<vector<int>>& grid, vector<vector<int>>& hits) {
        int m = grid.size(), n = grid[0].size(), k = hits.size();
        vector<int> res(k);
        unordered_set<int> noDrop;
        for (int i = 0; i < k; ++i) grid[hits[i][0]][hits[i][1]] -= 1;
        for (int i = 0; i < n; ++i) {
            if (grid[0][i] == 1) check(grid, 0, i, noDrop);
        }
        for (int i = k - 1; i >= 0; --i) {
            int oldSize = noDrop.size(), x = hits[i][0], y = hits[i][1];
            if (++grid[x][y] != 1) continue;
            if ((x - 1 >= 0 && noDrop.count((x - 1) * n + y)) 
                || (x + 1 < m && noDrop.count((x + 1) * n + y))
                || (y - 1 >= 0 && noDrop.count(x * n + y - 1))
                || (y + 1 < n && noDrop.count(x * n + y + 1))
                || x == 0) {
                check(grid, x, y, noDrop);
                res[i] = noDrop.size() - oldSize - 1;
            }
        }
        return res;
    }
    void check(vector<vector<int>>& grid, int i, int j, unordered_set<int>& noDrop) {
        int m = grid.size(), n = grid[0].size();
        if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != 1 || noDrop.count(i * n + j)) return;
        noDrop.insert(i * n + j);
        check(grid, i - 1, j, noDrop);
        check(grid, i + 1, j, noDrop);
        check(grid, i, j - 1, noDrop);
        check(grid, i, j + 1, noDrop);
    }
};

我們再來看一種使用並查集Union Find的方法來做的,接觸過並查集題目的童鞋應該有印象,是專門處理群組問題的一種演算法。最典型的就是島嶼問題(例如Number of Islands II),很適合使用Union Find來做,LeetCode中有很多道可以使用這個方法來做的題,比如Friend CirclesGraph Valid TreeNumber of Connected Components in an Undirected Graph,和Redundant Connection等等。都是要用一個root陣列,每個點開始初始化為不同的值,如果兩個點屬於相同的組,就將其中一個點的root值賦值為另一個點的位置,這樣只要是相同組裡的兩點,通過find函式得到相同的值。當然初始化的時候也不用都賦為不同的值,如果表示的是座標的話,我們也可以都初始化為-1,在find函式稍稍改動一下,也是可以的,這裡就把判斷 root[x] == x 改為 root[x] == -1 即可。這道題要稍稍複雜一些,我們不光需要並查集陣列root,還需要知道每個位置右方和下方跟其相連的磚頭個數陣列count,還有標記每個位置是否相連且不會墜落的狀態陣列t,第一行各個位置的t值初始化為1。跟上面的方法類似,我們還是從最後一個磚頭開始往回加,那麼我們還是要把hits中所有的位置在grid中對應的值減1。然後我們要建立並查集的關係,我們遍歷每一個位置,如果是磚頭,那麼我們對其右邊和下邊的位置進行檢測,如果是磚頭,我們就進行經典的並查集的操作,分別對當前位置和右邊位置呼叫find函式,如果兩個值不同,說明目前屬於兩個不同的群組,我們要連結上這兩個位置,這裡有個小問題需要注意一下,一般來說,我們連結群組的時候,root[x] = y 或 root[y] = x 都是可以的,但是這裡我們需要使用第二種,為了跟後面的 count[x] += count[y] 對應起來,因為這裡的y是在x的右邊,所以count[x]要大於count[y],這裡x和y我們都使用x的群組號,這樣能保證後面加到正確的相連的磚頭個數。還有我們的t[x] 和 t[y] 也需要更新,因為兩個位置要相連,所以只有其中有一方是跟頂端相連的,那麼二者的t值都應該為1。初始化完成後,我們就從hits陣列末尾開始往回加磚頭,跟之前的方法一樣,首先要判斷之前是有磚頭的,然後遍歷周圍四個新位置,如果位置合法且有磚頭的話,再呼叫並查集的經典操作,對老位置和新位置分別呼叫find函式,如果不在同一個群組的話,我們需要一個變數cnt來記錄可以掉落的磚頭個數,如果新位置的t值為0,說明其除了當前位置之外不跟其他位置相連,我們將其count值加入cnt。然後就是連結兩個群組,通知更新老位置的count值,新老位置的t值等等。當週圍位置遍歷完成後,如果當前位置的t值為1,則將cnt值存入結果res的對應位置,參見程式碼如下:

解法二:

class Solution {
public:
    vector<int> hitBricks(vector<vector<int>>& grid, vector<vector<int>>& hits) {
        int m = grid.size(), n = grid[0].size(), k = hits.size();
        vector<int> res(k), root(m * n, -1), count(m * n, 1), t(m * n, 0);
        vector<vector<int>> dirs{{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
        for (int i = 0; i < k; ++i) grid[hits[i][0]][hits[i][1]] -= 1;
        for (int i = 0; i < n; ++i) t[i] = 1;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] != 1) continue;
                if (i + 1 < m && grid[i + 1][j] == 1) {
                    int x = find(root, i * n + j), y = find(root, (i + 1) * n + j);
                    if (x != y) {root[y] = x; count[x] += count[y]; t[x] = t[y] = (t[x] | t[y]);}
                }
                if (j + 1 < n && grid[i][j + 1] == 1) {
                    int x = find(root, i * n + j), y = find(root, i * n + j + 1);
                    if (x != y) {root[y] = x; count[x] += count[y]; t[x] = t[y] = (t[x] | t[y]);}
                }
            }
        }
        for (int i = k - 1; i >= 0; --i) {
            int x = hits[i][0], y = hits[i][1], a = find(root, x * n + y), cnt = 0;
            if (++grid[x][y] != 1) continue;
            for (auto dir : dirs) {
                int newX = x + dir[0], newY = y + dir[1];
                if (newX < 0 || newX >= m || newY < 0 || newY >= n || grid[newX][newY] != 1) continue;
                int b = find(root, newX * n + newY);
                if (a == b) continue;
                if (!t[b]) cnt += count[b];
                root[b] = a; count[a] += count[b]; t[a] = t[b] = (t[a] | t[b]);
            }
            if (t[a]) res[i] = cnt;
        }
        return res;
    }
    int find(vector<int>& root, int x) {
        return (root[x] == -1) ? x : find(root, root[x]);
    }
};

參考資料: