题目地址:
https://www.lintcode.com/problem/maximum-connected-area/description
给定一个二维 0 − 1 0-1 0−1矩阵 A A A,允许将其中的某一个 0 0 0改为 1 1 1,问能得到的最大的 1 1 1连通块的面积(面积定义为 1 1 1的个数)。
法1:并查集。遍历矩阵 A A A,先将所有的 1 1 1的位置做union,同时算出森林里每棵树的节点个数(也就是 1 1 1连通块的 1 1 1的个数)。接着再遍历一次 A A A,关注那些值为 0 0 0的位置,看一个四个方向的邻居的parent是多少(这里的parent指的是它们在并查集中的parent,当然四个邻居的parent有可能有 4 4 4个,也有可能少于 4 4 4个,因为也许有若干邻居是共享parent的),再将这些parent一个个取出来,算出它们所在树的size,再加上将 0 0 0改为 1 1 1多出来的 1 1 1个单位的面积,这就是将当前 0 0 0改为 1 1 1之后,其所在的 1 1 1连通块的面积,可以用此面积来更新答案。最后还需要注意一个问题,如果 A A A里全是 1 1 1,这时是找不到 0 0 0的,则返回 A A A的面积即可。代码如下:
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class Solution {
class UnionFind {
int m, n;
int[] parent, size;
public UnionFind(int m, int n) {
parent = new int[m * n];
for (int i = 0; i < m * n; i++) {
parent[i] = i;
}
size = new int[m * n];
Arrays.fill(size, 1);
this.m = m;
this.n = n;
}
public void union(int x1, int y1, int x2, int y2) {
union(transform(x1, y1), transform(x2, y2));
}
public void union(int a, int b) {
int pa = find(a), pb = find(b);
if (pa == pb) {
return;
}
parent[pa] = pb;
size[pb] += size[pa];
}
public int find(int x, int y) {
return find(transform(x, y));
}
public int find(int a) {
if (parent[a] != a) {
parent[a] = find(parent[a]);
}
return parent[a];
}
public int getSize(int x, int y) {
return size[transform(x, y)];
}
public int getSize(int a) {
return size[a];
}
// 将二维坐标与一维坐标做映射
private int transform(int x, int y) {
return x * n + y;
}
}
/**
* @param matrix: the matrix for calculation.
* @return: return the max area after operation at most once.
*/
public int maxArea(int[][] matrix) {
// write your code here.
int m = matrix.length, n = matrix[0].length;
UnionFind uf = new UnionFind(m, n);
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 1) {
int[] d = {1, 0, -1, 0, 1};
for (int k = 0; k < 4; k++) {
int nextX = i + d[k], nextY = j + d[k + 1];
if (inBound(nextX, nextY, matrix) && matrix[nextX][nextY] == 1) {
uf.union(i, j, nextX, nextY);
}
}
}
}
}
int res = 0;
// 记录有没有找到0
boolean found = false;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
// 找到0了,标记一下
found = true;
int[] d = {1, 0, -1, 0, 1};
// 存matrix[i][j]的四个邻居的parent,为了去重这里用哈希表
Set<Integer> set = new HashSet<>();
for (int k = 0; k < 4; k++) {
int nextX = i + d[k], nextY = j + d[k + 1];
if (inBound(nextX, nextY, matrix) && matrix[nextX][nextY] == 1) {
set.add(uf.find(nextX, nextY));
}
}
int curArea = 1;
for (int num : set) {
curArea += uf.getSize(num);
}
res = Math.max(res, curArea);
}
}
}
// 如果不存在0,则返回整个矩阵的面积
return found ? res : m * n;
}
private boolean inBound(int x, int y, int[][] matrix) {
return 0 <= x && x < matrix.length && 0 <= y && y < matrix[0].length;
}
}
时间复杂度 O ( m n log ∗ ( m n ) ) O(mn\log ^* (mn)) O(mnlog∗(mn)),空间 O ( m n ) O(mn) O(mn)。
注解:这里的并查集用HashMap来写可能更精简一些,如果用二维映射到一维的方法的话,速度会快些,但写起来十分啰嗦。
法2:DFS + 标记连通块编号。为了统计面积的时候防止重复,我们可以对每个 1 1 1连通块做编号。开一个二维数组 i d id id,使得 i d [ i ] [ j ] id[i][j] id[i][j]是 A [ i ] [ j ] A[i][j] A[i][j]所在的 1 1 1连通块的编号,具体方法是DFS的flood fill算法。同时在DFS的时候,还可以顺便算出某个编号对应的 1 1 1连通块的面积,存入哈希表中。编号结束后,再对 A A A进行遍历,遇到 0 0 0的时候,就将其邻居的 1 1 1所在的 1 1 1连通块的面积加个总,然后更新答案。根据 i d id id数组就能避免重复统计。最后仍然要注意 A A A里没有 0 0 0的情况。代码如下:
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class Solution {
/**
* @param matrix: the matrix for calculation.
* @return: return the max area after operation at most once.
*/
public int maxArea(int[][] matrix) {
// write your code here.
int m = matrix.length, n = matrix[0].length;
boolean[][] visited = new boolean[m][n];
int[][] id = new int[m][n];
int idx = 1;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (!visited[i][j] && matrix[i][j] == 1) {
map.put(idx, dfs(i, j, matrix, id, idx, visited));
idx++;
}
}
}
int res = 0;
boolean found = false;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
found = true;
Set<Integer> ids = new HashSet<>();
int[] d = {1, 0, -1, 0, 1};
int curArea = 1;
for (int k = 0; k < 4; k++) {
int nextX = i + d[k], nextY = j + d[k + 1];
if (inBound(nextX, nextY, matrix) && matrix[nextX][nextY] == 1) {
int curId = id[nextX][nextY];
if (!ids.contains(curId)) {
ids.add(curId);
curArea += map.get(curId);
}
}
}
res = Math.max(res, curArea);
}
}
}
return found ? res : m * n;
}
// 将(x, y)所在的1连通块的所有位置在id数组里标记其编号为idx,同时返回该连通块面积
private int dfs(int x, int y, int[][] matrix, int[][] id, int idx, boolean[][] visited) {
visited[x][y] = true;
id[x][y] = idx;
int[] d = {1, 0, -1, 0, 1};
int area = 1;
for (int i = 0; i < 4; i++) {
int nextX = x + d[i], nextY = y + d[i + 1];
if (inBound(nextX, nextY, matrix) && matrix[nextX][nextY] == 1 && !visited[nextX][nextY]) {
area += dfs(nextX, nextY, matrix, id, idx, visited);
}
}
return area;
}
private boolean inBound(int x, int y, int[][] matrix) {
return 0 <= x && x < matrix.length && 0 <= y && y < matrix[0].length;
}
}
时空复杂度 O ( m n ) O(mn) O(mn)。
注解:在上面并查集的方法里,事实上parent就扮演了这里的id的作用。