n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。
如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头。
给你一个长度为 n 的数组 stones ,其中 stones[i] = [xi, yi] 表示第 i 块石头的位置,返回 可以移除的石子 的最大数量。
示例 1:
输入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
输出:5
解释:一种移除 5 块石头的方法如下所示:
- 移除石头 [2,2] ,因为它和 [2,1] 同行。
- 移除石头 [2,1] ,因为它和 [0,1] 同列。
- 移除石头 [1,2] ,因为它和 [1,0] 同行。
- 移除石头 [1,0] ,因为它和 [0,0] 同列。
- 移除石头 [0,1] ,因为它和 [0,0] 同行。
石头 [0,0] 不能移除,因为它没有与另一块石头同行/列。
示例 2:
输入:stones = [[0,0],[0,2],[1,1],[2,0],[2,2]]
输出:3
解释:一种移除 3 块石头的方法如下所示:
- 移除石头 [2,2] ,因为它和 [2,0] 同行。
- 移除石头 [2,0] ,因为它和 [0,0] 同列。
- 移除石头 [0,2] ,因为它和 [0,0] 同行。
石头 [0,0] 和 [1,1] 不能移除,因为它们没有与另一块石头同行/列。
示例 3:
输入:stones = [[0,0]]
输出:0
解释:[0,0] 是平面上唯一一块石头,所以不可以移除它。
提示:
1 <= stones.length <= 1000
0 <= xi, yi <= 104
不会有两块石头放在同一个坐标点上
分析:
本题最关键的是理解 题目中“最多”的含义,以及理解移除石头的规则。
因为不可能在同一个坐标上出现两个石头,我们可以把二维坐标上的石头想象称为图的顶点,如果两个石头的横坐标、或者纵坐标相同,则在它们之间形成一条边,就会形成一个连通图
如果一个石头的同行或者同列有其他石头存在,就可以移除这块石头,这说明在一个连通图里面的所有顶点可以根据这个规则删除到只剩下一个顶点。
所以有:最多可以移除的石头的个数 = 所有石头的个数-连同分量的个数
因为题目中没有要求给出具体移除石头的方案,可以考虑使用并查集。
合并的语义:所有横坐标为x的石头和所有纵坐标为y的石头都属于同一个连通分量。例如(3,4)这个石头,所有横坐标为3的石头,和所有纵坐标为4的石头都和(3,4)属于同一个连同分量。
方法:删除到最后,留在图中的顶点一定位于不同的行和列,因为,并查集中的元素是描述 横坐标和纵坐标的数值。因为我们需要遍历所有的stone,将每个stone的横坐标和纵坐标在并查集中进行合并,这里需要理解,因为横坐标和纵坐标表示的是一个点。
这里有一个非常巧妙的点:并查集中的底层是【一维数组】,石头的位置是【有序数对(二维)】,在合并的时候如何区别横纵坐标。
例如:如果一个石头为(3,3),那么所有横坐标为3和纵坐标为3的石头都在一个连同分量里面,但是我们在连同分量里面需要区分横纵坐标,它们在并查集里面不能相同,根据题目提示,坐标为0-10000,因为我们可以将其中一个坐标映射到另一个与0-10000不重合的区间,可以的做法是把横坐标全部加上10001或者减去10001,或者按位取反的32位整数,最高位变成1后,一定不在0-10000里。
这个方法是一个很巧妙的将二维将为一维的方法,避免(1,0)和(0,1)这种情况,如果不区分的话,这两点会被当成一个连同分量,区分以后就不会被当成同一个连同分量了!
class Solution {
public int removeStones(int[][] stones) {
UnionFind unionFind = new UnionFind();
for(int[] stone : stones){
//因为并查集中不能出现相同的元素值
//这里加上10001是因为坐标的范围在0-10000,加上10001可以保证x的坐标不会和y的坐标值相同
unionFind.union(stone[0]+10001,stone[1]);
}
return stones.length - unionFind.getCount();
}
private class UnionFind{
//key 为 x ,value为x的父亲节点
private Map<Integer,Integer> parent;
//记录连同分量的个数
private int count;
public UnionFind(){
this.parent = new HashMap<>();
this.count = 0;
}
public int getCount(){
return count;
}
//带路径压缩的 查找x属于那个集合
public int find(int x){
if(!parent.containsKey(x)){
//如果并查集中 没有x 那就加入一个连同分量,父节点就是它自己
parent.put(x,x);
count++;
}
//这里是路径压缩,回溯查找x的路径上面 所有的父亲节点设置为根节点
//相当于 并查集中是一位数组的
/*
if(p != id[p]){
id[p] = find(id[p]);
}
return id[p];
*/
//路径压缩
if(x != parent.get(x)){
parent.put(x,find(parent.get(x)));
}
return parent.get(x);
}
public void union(int x,int y){
int rootx = find(x);
int rooty = find(y);
//合并 如果是同一个连同分量的,就不用合并
if(rootx == rooty){
return;
}
//将rootx的父亲节点指向rooty
//相当于 一位数组中的 id[rootx] = rooty;
//这里是将横纵坐标对应的两个集合合并起来,因为x和y表示的是一个点,这里很巧妙,需要理解
parent.put(rootx,rooty);
count--;//连同分量减一
}
}
}