Links

0947. 移除最多的同行或同列石头

题目地址 (947. 移除最多的同行或同列石头)

题目描述

n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。
如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头。
给你一个长度为 n 的数组 stones ,其中 stones[i] = [xi, yi] 表示第 i 块石头的位置,返回 可以移除的石子 的最大数量。
示例 1:
输入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
输出:5
解释:一种移除 5 块石头的方法如下所示:
1. 移除石头 [2,2] ,因为它和 [2,1] 同行。
2. 移除石头 [2,1] ,因为它和 [0,1] 同列。
3. 移除石头 [1,2] ,因为它和 [1,0] 同行。
4. 移除石头 [1,0] ,因为它和 [0,0] 同列。
5. 移除石头 [0,1] ,因为它和 [0,0] 同行。
石头 [0,0] 不能移除,因为它没有与另一块石头同行/列。
示例 2:
输入:stones = [[0,0],[0,2],[1,1],[2,0],[2,2]]
输出:3
解释:一种移除 3 块石头的方法如下所示:
1. 移除石头 [2,2] ,因为它和 [2,0] 同行。
2. 移除石头 [2,0] ,因为它和 [0,0] 同列。
3. 移除石头 [0,2] ,因为它和 [0,0] 同行。
石头 [0,0] 和 [1,1] 不能移除,因为它们没有与另一块石头同行/列。
示例 3:
输入:stones = [[0,0]]
输出:0
解释:[0,0] 是平面上唯一一块石头,所以不可以移除它。
提示:
1 <= stones.length <= 1000
0 <= xi, yi <= 104
不会有两块石头放在同一个坐标点上

前置知识

思路

读完题目之后, 看了下数据范围。我猜测可能和动态规划什么的有关,而且时间复杂度是 $O(n^2)$ 左右,其中 n 为 stones 的长度。继续看了下示例,然后跟着思考了一下,发现很像是某种联通关系。 类似的题目有很多,题目描述记不太清楚了。大概意思是给你一个二维网格,行和列需要一起增加,求最小和什么的。这类题目都是行和列具有某种绑定关系。 于是我想到了使用并查集来做。 后面想了一下,反正就是求联通区域的个数,因此使用 DFS 和 BFS 都可以。如果你想使用 DFS 或者 BFS 来解,可以结合我之前写的 小岛专题 练习一下哦。
继续分析下题目。 题目的意思是任意一个石头可以消除和它同行和同列的其他石子。于是我就想象出了下面这样一幅图,其中红色的方块表示石子,方块的连线表示离得最近的可以消除的石子。实际上,一个石子除了可以消除图中线条直接相连的石子,还可以消除邻居的邻居。这提示我们使用并查集维护这种联通关系,联通的依据自然就是列或者行一样。
上面是一个全联通的图。如下是有两个联通域的图。
有了上面的知识,其实就可以将石子全部建立并查集的联系,并计算联通子图的个数。答案就是 n - 联通子图的个数,其中 n 为 stones 的长度。
核心代码:
n = len(stones)
# 标准并查集模板
uf = UF(n)
# 两个 for 循环作用是将所有石子两两合并
for i in range(n):
for j in range(i + 1, n):
# 如果行或者列相同,将其联通成一个子图
if stones[i][0] == stones[j][0] or stones[i][1] == stones[j][1]: uf.union(i, j)
return n - uf.cnt
有的人想问,这可行么?即我可以将一个联通子图的石子移除只剩下一个么?
答案是肯定的。其实上面我提到了这道题也可使用 DFS 和 BFS 的方式来做。如果你使用 DFS 的方式来做,会发现其实 DFS 路径的取反就是消除的顺序,当然消除的顺序不唯一,因为遍历访问联通子图的序列并不唯一。 如果题目要求我们求移除顺序,那我们可以考虑使用 DFS 来做,同时记录路径信息即可。
使用遍历的方式(BFS 或者 DFS),由于每次访问一个石子都需要使用 visited 来记录访问信息防止环的产生,因此 visited 的逆序也是一个可行的移除顺序。不过这要求你的 visited 的是有序的。实现的方法有很多,有点偏题了,这里就不赘述了。
实际上,上面的并查集代码仍然可以优化。上面的思路是直接将点作为并查集的联通条件。实际上,我们可以将点的横纵坐标分别作为联通条件。即如果横坐标相同的联通到一个子图,纵坐标相同的联通到一个子图。如下图:
为了达到这个模板,我们不能再初始化的时候计算联通域数量了,即不能像上面那样 uf = UF(n)(此时联通域个数为 n)。因为横坐标,纵坐标分别有多少不重复的我们是不知道的,一种思路是先计算出横坐标,纵坐标分别有多少不重复的。这当然可以,还有一种思路是在 find 过程中计算,这样 one pass 即可完成,具体见下方代码区。
由于我们需要区分横纵坐标(上面图也可以看出来),因此可用一种映射区分二者。由于题目限定了横纵坐标取值为 10000 以内(包含 10000),一种思路就是将 x 或者 y 加上 10001。
核心代码:
n = len(stones)
uf = UF(0)
for i in range(n):
uf.union(stones[i][0] + 10001, stones[i][1])
return n - uf.cnt

代码

并查集

代码支持: Python3
其中 class UF 部分是标准的无权并查集模板,我一行代码都没变。关于模板可以去 并查集 查看。
class UF:
def __init__(self, M):
self.parent = {}
self.cnt = 0
# 初始化 parent,size 和 cnt
for i in range(M):
self.parent[i] = i
self.cnt += 1
def find(self, x):
if x != self.parent[x]:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
return x
def union(self, p, q):
if self.connected(p, q): return
leader_p = self.find(p)
leader_q = self.find(q)
self.parent[leader_p] = leader_q
self.cnt -= 1
def connected(self, p, q):
return self.find(p) == self.find(q)
class Solution:
def removeStones(self, stones: List[List[int]]) -> int:
n = len(stones)
uf = UF(n)
for i in range(n):
for j in range(i + 1, n):
if stones[i][0] == stones[j][0] or stones[i][1] == stones[j][1]: uf.union(i, j)
return n - uf.cnt
复杂度分析
令 n 为数组 stones 的长度。
  • 时间复杂度:$O(n^2logn)$
  • 空间复杂度:$O(n)$

优化的并查集

代码支持: Python3
class UF:
def __init__(self, M):
self.parent = {}
self.cnt = 0
def find(self, x):
if x not in self.parent:
self.cnt += 1
self.parent[x] = x
if x != self.parent[x]:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
return x
def union(self, p, q):
if self.connected(p, q): return
leader_p = self.find(p)
leader_q = self.find(q)
self.parent[leader_p] = leader_q
self.cnt -= 1
def connected(self, p, q):
return self.find(p) == self.find(q)
class Solution:
def removeStones(self, stones: List[List[int]]) -> int:
n = len(stones)
uf = UF(0)
for i in range(n):
uf.union(stones[i][0] + 10001, stones[i][1])
return n - uf.cnt
复杂度分析
令 n 为数组 stones 的长度。
  • 时间复杂度:$O(nlogn)$
  • 空间复杂度:$O(n)$

DFS

代码支持: Java
by 一位不愿透露姓名的热心网友。
public int removeStones(int[][] stones) {
Set visit = new HashSet();
int count = 0;
int offset = 10000;
HashMap <Integer,List<int []>>map = new HashMap();
// 构造图 每一项是一个节点
for (int i = 0; i < stones.length; i++) {
int [] node = stones[i];
List<int []> list = map.getOrDefault(node[0]-offset,new ArrayList<>());
list.add(node);
map.put(node[0]-offset,list);
List<int []> list1 = map.getOrDefault(node[1],new ArrayList<>());
list1.add(node);
map.put(node[1],list1);
}
// 寻找联通分量
for (int i = 0; i < stones.length; i++) {
int [] node = stones[i];
if (!visit.contains((node))){
visit.add((node));
dfs(node,visit,map);
count++;
}
}
return stones.length-count;
}
// 遍历节点
public void dfs(int[]node, Set set,HashMap <Integer,List<int []>>map){
int offset = 10000;
List<int []> list = map.getOrDefault(node[0]-offset,new ArrayList<>());
for (int i = 0; i < list.size(); i++) {
int[] item = list.get(i);
if (!set.contains((item))){
set.add((item));
dfs(item,set,map);
}
}
List<int []> list2 = map.getOrDefault(node[1],new ArrayList<>());
for (int i = 0; i < list2.size(); i++) {
int[] item = list2.get(i);
if (!set.contains((item))){
set.add((item));
dfs(item,set,map);
}
}
}
复杂度分析
令 n 为数组 stones 的长度。
  • 时间复杂度:建图和遍历图的时间均为 $O(n)$
  • 空间复杂度:$O(n)$
力扣的小伙伴的点下我头像的关注按钮,这样就会第一时间收到我的动态啦~
以上就是本文的全部内容了。大家对此有何看法,欢迎给我留言,我有时间都会一一查看回答。更多算法套路可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 38K star 啦。