这道题做法很多,但是高效率的做法却要好好思考一下。那我们就从低效率的做法到高效率的做法来好好解读一下这道题,题目的意思就是说一个矩阵只有第一行的方块或者和方块直接相邻的四块方块只要有一块不会自动掉落,那这块方块就不会掉落,现在就是每次移除一块方块,问有几块方块会自动掉落?
初看这道题,第一个产生的想法就是搜索,大概思路就是考虑移除的这块方块的周围四块方块所在的连通块是否包含第一行的方块,如果包含那就不会掉落,如果不包含那就整个连通块全部掉落,就是先用DFS/BFS判断连通块是否包含第一行的方块,然后采用DFS的方法计数要掉落的方块并且把这个位置置为0即可。
代码如下:
class Solution {
public:
typedef pair<int, int> P;
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
int used[205][205];
int n, m;
vector<vector<int>> g;
int id;
vector<int> hitBricks(vector<vector<int>>& grid, vector<vector<int>>& hits) {
vector<int> res;
n = grid.size();
m = grid[0].size();
g.swap(grid);
id = 1;
for(int i = 0; i < hits.size(); ++i)
{
vector<int> pos = hits[i];
if(g[pos[0]][pos[1]] == 0)
{
res.push_back(0);
continue;
}
g[pos[0]][pos[1]] = 0;
int cnt = 0;
for(int j = 0; j < 4; ++j)
{
int nx = pos[0] + dx[j];
int ny = pos[1] + dy[j];
if(valid(nx, ny) && g[nx][ny] == 1 && dfs(nx, ny))
{
cnt += erase(nx, ny);
}
id += 1;
}
res.push_back(cnt);
}
return res;
}
bool valid(int x, int y)
{
return x >= 0 && x < n && y >= 0 && y < m;
}
bool dfs(int x, int y) //判断grid[x][y]是否掉落,true掉落,false不掉落
{
//if(used[x][y] == id) return true;
if(x == 0) return false;
used[x][y] = id;
for(int i = 0; i < 4; ++i)
{
int nx = x + dx[i];
int ny = y + dy[i];
if(valid(nx, ny) && g[nx][ny] == 1 && used[nx][ny] != id)
{
//used[x][y] = id;
if(!dfs(nx, ny)) return false;
}
}
return true;
}
int erase(int x, int y)
{
int res = 1;
g[x][y] = 0;
for(int i = 0; i < 4; ++i)
{
int nx = x + dx[i];
int ny = y + dy[i];
if(valid(nx, ny) && g[nx][ny] == 1)
{
res += erase(nx, ny);
}
}
return res;
}
};
计算一下复杂度就知道,复杂度为O(N**4),理论上是不可能通过的,但是奇怪的是只要dx,dy定义的好,竟然可以打个擦边球,尽管可以通过,但这绝对不是一个好的做法。
好的,现在来看一个O(N*N)的做法,就是使用并查集,不过要把这道题和并查集联系起来的确是有难度的,不容易想到,来看一下思路:首先得逆向思维,先把要移除的方块都在矩阵中移除,然后从后往前一块一块的方块往原矩阵中加,看能连通起多少块不能包含第一行方块的连通块的个数,这就是这次移除造成的自动掉落的方块数。那具体该如何实现呢?把所有要移除的方块从矩阵中移除后,首先把矩阵中剩下的方块中直接相邻的方块连接在一起,相当于在这两个节点之间加一条边(图的思维),处理的同时要注意记录每一个连通块已有的节点的个数,同时还要记录这个连通块是否包含第一行,然后就是从最后一次移除往前算,设要移除的方块为A,判断和A是否有直接相邻的方块B,如果这两块方块不属于同一个连通块,并且B所属连通块不包含第一行,那就记录这个连通块的个数,因为这个连通块通过添加A可能就包含第一行了,这也就是移除时会自动掉落的连通块,同时也把这两个节点之间连接一条边,同时更新记录,最后说记录的会自动掉落的连通块个数不一定就是结果,还要判断A所属连通块是不是包含第一行,如果不包含,那就是说明A是在之前就已经掉落的,那此次移除的结果就是0,最后再把原图中连通块A添上,然后处理下一个,OK,这道题这样就可以顺利解决了。
代码如下:
class Solution {
public:
vector<int> fa, pf, cnt;
int n, m;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int find(int x)
{
if(fa[x] == -1) return x;
return fa[x] = find(fa[x]);
}
bool valid(int x, int y)
{
return x >= 0 && x < n && y >= 0 && y < m;
}
vector<int> hitBricks(vector<vector<int>>& grid, vector<vector<int>>& hits) {
n = grid.size();
m = grid[0].size();
for(int i = 0; i < hits.size(); ++i)
{
if(grid[hits[i][0]][hits[i][1]]) grid[hits[i][0]][hits[i][1]] = 0;
else hits[i][0] = -1;
}
fa.assign(n * m, -1);
pf.assign(n * m, 0);
cnt.assign(n * m, 1);
for(int i = 0; i < m; ++i) pf[i] = 1;
for(int i = 0; i < n; ++i)
for(int j = 0; j < m; ++j)
{
if(grid[i][j])
{
int fx = find(i * m + j);
if(valid(i + 1, j) && grid[i + 1][j])
{
int fy = find((i + 1) * m + j);
if(fx != fy)
{
cnt[fx] += cnt[fy];
pf[fx] = pf[fy] = pf[fx] | pf[fy];
fa[fy] = fx;
}
}
if(valid(i, j + 1) && grid[i][j + 1])
{
int fy = find(i * m + j + 1);
if(fx != fy)
{
cnt[fx] += cnt[fy];
pf[fx] = pf[fy] = pf[fx] | pf[fy];
fa[fy] = fx;
}
}
}
}
vector<int> res = vector<int>(hits.size(), 0);
for(int i = hits.size() - 1; i >= 0; --i)
{
if(hits[i][0] == -1) continue;
int x = hits[i][0];
int y = hits[i][1];
int a = find(x * m + y);
int d = 0;
for(int j = 0; j < 4; ++j)
{
int nx = x + dx[j];
int ny = y + dy[j];
if(valid(nx, ny) && grid[nx][ny])
{
int b = find(nx * m + ny);
if(a == b) continue;
if(!pf[b]) d += cnt[b];
cnt[a] += cnt[b];
pf[a] = pf[b] = pf[a] | pf[b];
fa[b] = a;
}
}
grid[x][y] = 1;
if(pf[a]) res[i] = d;
}
return res;
}
};
看一下两种做法的时间对比:
第一种做法1169ms,第二种做法131ms,性能的提升是巨大的!
最后做个总结,之前用并查集都是在图论的题目中,这种能转化成并查集求解的题目做得比较少,不过再次领略了并查集的巨大魅力,这道题目还是值得好好思考的!