On a 2D plane, we place n stones at some integer coordinate points. Each coordinate point may have at most one stone.
A stone can be removed if it shares either the same row or the same column as another stone that has not been removed.
Given an array stones of length n where stones[i] = [xi, yi] represents the location of the ith stone, return the largest possible number of stones that can be removed.
Example 1:
Input: stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
Output: 5
Explanation: One way to remove 5 stones is as follows:
- Remove stone [2,2] because it shares the same row as [2,1].
- Remove stone [2,1] because it shares the same column as [0,1].
- Remove stone [1,2] because it shares the same row as [1,0].
- Remove stone [1,0] because it shares the same column as [0,0].
- Remove stone [0,1] because it shares the same row as [0,0].
Stone [0,0] cannot be removed since it does not share a row/column with another stone still on the plane.
Constraints:
1 <= stones.length <= 1000
0 <= xi, yi <= 104
No two stones are at the same coordinate point.
给一个2D数组,其中的元素代表2D的位置,同一位置只会有一个石头,同行或同列的石头能被移走(前提是存在与它同行或同列的石头,如果只有它自己就不能移走),问最多可移走多少个石头。
思路:
问题转换:同行同列的石头阵可以被移走直到只剩下一个石头。那么把同行同列的石头全都归到一个group,这个group的个数就是最后剩下的石头数量。则被移走的石头数量=石头总数量 - group个数
任务就变成了把同行同列的石头归到一个group里,两种方法,DFS和Union-Find
1) DFS
可把 (row, col) 看作一条边,建立无向图
但是注意一个问题,就是(1,2) 和 (2,1)是两个位置,但会被认为是同一条边,这时需要把这两种边区分开,题中有这样一个限制条件0 <= xi, yi <= 104 ,所以可定义0~10000是row的区间,10001~20001是col的范围。也就是把col + 10001,但是实际上col+10000也通过了。
这样做就可以访问一个位置(row,col)时,把row行和col列的位置全都标记为访问过,再递归标记它们关联的行和列,为一个组。下一次再从未访问过的位置重新标记新的一组。这样就可找出一共有多少个组。
可能行和列的概念容易混淆,可以认为列也是要遍历的“行”,只不过列的index是从10000开始的。
这里标记访问过不是一个位置一个位置地标,而是标整行整列,表示这一行或列已经访问过(可把列理解为index从10000开始的“行”)。
//19ms
public int removeStones(int[][] stones) {
int groups = 0;
HashMap<Integer, List<Integer>> graph = new HashMap<>();
boolean[] visited = new boolean[20001];
for(int[] stone : stones) {
int row = stone[0];
int col = stone[1] + 10000;
if(!graph.containsKey(row)) {
graph.put(row, new ArrayList<Integer>());
}
graph.get(row).add(col);
if(!graph.containsKey(col)) {
graph.put(col, new ArrayList<Integer>());
}
graph.get(col).add(row);
}
for(Integer key : graph.keySet()) {
if(visited[key]) continue;
dfs(key, graph, visited);
groups ++;
}
return stones.length - groups;
}
void dfs(int rowCol, HashMap<Integer, List<Integer>> graph, boolean[] visited) {
if(visited[rowCol]) return;
visited[rowCol] = true;
//行的话找同一列,列的话找同一行的stone,把同行或同列的标记为访问,为同一group
for(Integer next : graph.get(rowCol)) {
if(visited[next]) continue;
dfs(next, graph, visited);
}
}
如果DFS深度过深,担心StackOverflowError,可用stack版DFS,和BFS差不多,只不过BFS用的queue储存节点,先进先访问。DFS用stack储存节点,后进的先访问,直到到达尽头。
public int removeStones(int[][] stones) {
int groups = 0;
HashMap<Integer, List<Integer>> graph = new HashMap<>();
boolean[] visited = new boolean[20001];
for(int[] stone : stones) {
int row = stone[0];
int col = stone[1] + 10000;
if(!graph.containsKey(row)) {
graph.put(row, new ArrayList<Integer>());
}
graph.get(row).add(col);
if(!graph.containsKey(col)) {
graph.put(col, new ArrayList<Integer>());
}
graph.get(col).add(row);
}
for(Integer key : graph.keySet()) {
if(visited[key]) continue;
Stack<Integer> stack = new Stack<>();
stack.push(key);
visited[key] = true;
while(!stack.isEmpty()) {
int rowCol = stack.pop();
for(Integer next : graph.get(rowCol)) {
if(visited[next]) continue;
visited[next] = true;
stack.push(next);
}
}
groups ++;
}
return stones.length - groups;
}
2)Union-Find
思路和上面一样,也是把同行同列的归为一个group,最后用石头总数-group个数。
还是把(row, col)看成一条边,也可理解为它们是关联的数字。还是row是0~10000区间,col是10001~20001区间。但是这里加了一个情况,就是一个点还没有被访问的时候,它的parent是0,访问过的点要么parent是它自己,要么是其他点。所以为了和0区别开,row的范围移到1~10001,col移到10002~20002。
所以row要加1,col要加10002。
parent:上面还有其他parent。
root:parent是它自己(最上层parent)。
统一把col的root设为row的root。这样做有什么用处?row和col有同一个root,当同一列的位置来的时候,可通过col找到这个root,当同一行的位置来的时候,可通过row找到同一root,这样就可达到把同行和同列的石头都归为一个group的效果。
当row和col未访问过,即parent==0时,把row的parent标记为它自己,col的parent标记为row。一个访问过一个没访问过时,标记root为访问过的root。
当有不同的root时,把col的root,重点强调是root,而不是简单的parent,标记为row的root
//1ms~7ms
class Solution {
int group = 0;
public int removeStones(int[][] stones) {
int[] root = new int[20003];
for(int[] stone : stones) {
union(root, stone[0] + 1, stone[1] + 10002);
}
return stones.length - group;
}
void union(int[] root, int row, int col) {
int rootR = find(root, row);
int rootC = find(root, col);
//已经是一组(不需要处理),或者都没被访问过
if(rootR == rootC) {
if(rootR == 0) { //都没被访问过,新增group
group ++;
root[row] = row;
root[col] = row;
}
} else { //一个没被访问过(设定root,group不变),或者行列不在一组(改root为一组,减去一个group)
if(rootR == 0) { //row没被访问过
root[row] = rootC;
} else if(rootC == 0) {
root[col] = rootR;
} else {
root[rootC] = rootR; //从源头上改root,不要只改最下面节点
group --;
}
}
}
int find(int[] root, int node) {
int p = node;
while(p != root[p]) {
p = root[p];
root[p] = root[root[p]];
}
return p;
}
}