目录
一,题目描述
英文描述
中文描述
示例与说明
二,解题思路
图优化+DFS
图优化+并查集
三,AC代码
Java
图优化+DFS
图优化+并查集
四,解题过程
第一博
第二搏
第三搏
第四搏
一,题目描述
英文描述
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.
中文描述
n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。
如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头。
给你一个长度为 n 的数组 stones ,其中 stones[i] = [xi, yi] 表示第 i 块石头的位置,返回 可以移除的石子 的最大数量。
示例与说明
二,解题思路
参考官方题解@力扣官方题解【移除最多的同行或同列石头】
图优化+DFS
直接双重循环遍历两个石子是否为联通关系(同行/同列),显然时间代价是非常高的。
为了更快速的判断两点是否联通,可以借助hash表的力量。
- 采用hash表(key为行/列,value为石子编号)记录行/列中的石子编号。为了区分是行还是列,可以采用10001+列号表示新的列(数据规模是10000);
- 遍历hash表,连接同一行/列中的点,即可获得联通关系;
- DFS获得连通分量数目;
整体来看,步骤1时间复杂度为O(N)。步骤2中,由于每个点最多只能与其他四个点有关联关系,所以看上去是双重循环,时间复杂度其实为O(N)。步骤3,DFS不重复的遍历图中每个点,时间复杂度为O(N)。
图优化+并查集
一个石子的行和列上所有的石子均为联通关系,所以可以将这个行和列看作是联通的。因此如果多个石子属于同一个联通集合时,他们的行和列也可以组成一个集合。不同集合的行和列绝对不会重合。
这样便可以将行/列看作独立的数字进行合并
最后只需要查看石子所涉及到的行/列(即father不为-1)被分为几个集合,即可获得联通分量的个数
三,AC代码
Java
图优化+DFS
class Solution {
public int removeStones(int[][] stones) {
Map<Integer, List<Integer>> record = new HashMap<>();
List<List<Integer>> edge = new ArrayList<>();
for (int i = 0; i < stones.length; i++) {
edge.add(new ArrayList<>());
}
// 存储每行/列中的石头的编号(列用10001+列号表示,这样就可以在一个key为Integer的hash表中存储所有位置信息)
for (int i = 0; i < stones.length; i++) {
if (!record.containsKey(stones[i][0])) {
record.put(stones[i][0], new ArrayList<Integer>());
}
record.get(stones[i][0]).add(i);
if (!record.containsKey(stones[i][1] + 10001)) {
record.put(stones[i][1] + 10001, new ArrayList<Integer>());
}
record.get(stones[i][1] + 10001).add(i);
}
// 同行/列的石头构成联通关系
for (Map.Entry<Integer, List<Integer>> entry : record.entrySet()) {
List<Integer> list = entry.getValue();
for (int i = 1; i < list.size(); i++) {
edge.get(list.get(i - 1)).add(list.get(i));
edge.get(list.get(i)).add(list.get(i - 1));
}
}
boolean[] visited = new boolean[stones.length];
int num = 0;
for (int i = 0; i < stones.length; i++) {
if (visited[i] == false) {
dfs(edge, visited, i);
num++;
}
}
return stones.length - num;
}
void dfs (List<List<Integer>> edge, boolean[] visited, int i) {
visited[i] = true;
List<Integer> list = edge.get(i);
for (int x : list) {
if (visited[x] == false){
dfs(edge, visited, x);
}
}
}
}
图优化+并查集
class Solution {
int findFather(int[] father, int x) {
if (father[x] != x) {
father[x] = findFather(father, father[x]);
}
return father[x];
}
void unionSet(int[] father, int a, int b) {
int fa = findFather(father, a);
int fb = findFather(father, b);
if (fa != fb) {
father[fa] = father[fb];
}
}
public int removeStones(int[][] stones) {
int n = stones.length;
int[] father = new int[20005];// 保证访问不会越界
Arrays.fill(father, -1);
for (int i = 0; i < n; i++) {
// 行/列号存在,则将其父节点设置为行/列号
if (father[stones[i][0]] == -1) {
father[stones[i][0]] = stones[i][0];
}
if (father[stones[i][1] + 10001] == -1) {
father[stones[i][1] + 10001] = stones[i][1] + 10001;
}
unionSet(father, stones[i][0], stones[i][1] + 10001);// 将行和列合并
}
int num = 0;
for (int i = 0; i < father.length; i++) {
if (father[i] == i) num++;
}
return n - num;
}
}
四,解题过程
第一博
一开始没有注意到"最多"这个词的含义,很快就敲出来了,很快啊。
大致思路是每添加一个石子,就在对应的行和列加一。
然后再次遍历数组时,每当石子所在行和列记录不为1就将对应的行和列记录-1。
class Solution {
public:
int removeStones(vector<vector<int>>& stones) {
vector<vector<int>> record(stones.size(), vector<int>(2, 0));
int ans = 0;
for (int i = 0; i < stones.size(); i++) {
record[stones[i][0]][0]++;
record[stones[i][1]][1]++;
}
for (int i = stones.size() - 1; i >= 0; i--) {
if (record[stones[i][0]][0] != 1 || record[stones[i][1]][1] != 1) {
record[stones[i][0]][0]--;
record[stones[i][1]][1]--;
ans++;
}
}
return ans;
}
};
先删除[1, 1],然后[0, 1],[1, 0]就不存在同行或同列的石子了。 而实际上,最多可以删除两个石子(比如先删除[0, 1],再删除[1, 1])
第二搏
第一博中很明显不能满足“最多删除”的条件。
经过观察可以发现处于同行或同列的石子可以看作是一个集合,通过合适的删除顺序,这个集合中最少可只剩一个石子。
因此只要知道这样的集合有多少个,就可以计算出最终答案。
很直观的想到了连通分量这个概念。关于联通分量的求解,可以借助DFS以及并查集来处理。
以往我们在求解联通分量时,题目中一般都会给出任意两点的联通方式(比如边的集合等),但这里只给出了点图,并没有直接给出联通关系。因此就需要自己动手了!
最简单粗暴的形式就是双重循环遍历stones数组,若两个石子间存在关联(行数或列数相同),则将其连接起来,这里采用邻接表的形式来存储(比如石子1与[2, 5, 8]相关联,则edge[1] = [2, 5, 8])
class Solution {
public int removeStones(int[][] stones) {
List<List<Integer>> edge = new ArrayList<>();// 存储点之间的邻接关系
for (int i = 0; i < stones.length; i++) {
edge.add(new ArrayList<>());
}
// 创建图的邻接表形式(一个点和哪些点相关联)
for (int i = 0; i < stones.length; i++) {
for (int j = i + 1; j < stones.length; j++) {
if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1]) {
edge.get(i).add(j);
edge.get(j).add(i);// !!!必须为无向图,否则节点的访问顺序会影响最终计算出的连通分量个数
}
}
}
boolean[] visited = new boolean[stones.length];
Arrays.fill(visited, false);
int num = 0;// 记录联通分量个数
for (int i = 0; i < visited.length; i++) {
if (visited[i] == false) {
dfs(edge, visited, i);
num++;
}
}
return stones.length - num;
}
void dfs (List<List<Integer>> edge, boolean[] visited, int i) {
visited[i] = true;
List<Integer> list = edge.get(i);
for (int x : list) {
if (visited[x] == false){
dfs(edge, visited, x);
}
}
}
}
双重循环,时间O(N^2)。邻接表形式,空间最坏O(N^2)
第三搏
第二搏中暴力双重循环判断两个点是否联通。时间复杂度为O(N^2),效率较低。
有没有更快捷的判断两点是否联通的方法?
- 采用hash表记录行/列中的石子编号。为了区分是行还是列,可以采用10001+列号表示列(数据规模是10000)
- 遍历hash表,连接同一行/列中的点,即可获得联通关系。
- DFS获得连通分量数目;
整体来看,步骤1时间复杂度为O(N)。步骤2中,由于每个点最多只能与其他四个点有关联关系,所以看上去是双重循环,时间复杂度其实为O(N)。步骤3,DFS不重复的遍历图中每个点,时间复杂度为O(N)。
class Solution {
public int removeStones(int[][] stones) {
Map<Integer, List<Integer>> record = new HashMap<>();
List<List<Integer>> edge = new ArrayList<>();
for (int i = 0; i < stones.length; i++) {
edge.add(new ArrayList<>());
}
// 存储每行/列中的石头的编号(列用10001+列号表示,这样就可以在一个key为Integer的hash表中存储所有位置信息)
for (int i = 0; i < stones.length; i++) {
if (!record.containsKey(stones[i][0])) {
record.put(stones[i][0], new ArrayList<Integer>());
}
record.get(stones[i][0]).add(i);
if (!record.containsKey(stones[i][1] + 10001)) {
record.put(stones[i][1] + 10001, new ArrayList<Integer>());
}
record.get(stones[i][1] + 10001).add(i);
}
// 同行/列的石头构成联通关系
for (Map.Entry<Integer, List<Integer>> entry : record.entrySet()) {
List<Integer> list = entry.getValue();
for (int i = 1; i < list.size(); i++) {
edge.get(list.get(i - 1)).add(list.get(i));
edge.get(list.get(i)).add(list.get(i - 1));
}
}
boolean[] visited = new boolean[stones.length];
int num = 0;
for (int i = 0; i < stones.length; i++) {
if (visited[i] == false) {
dfs(edge, visited, i);
num++;
}
}
return stones.length - num;
}
void dfs (List<List<Integer>> edge, boolean[] visited, int i) {
visited[i] = true;
List<Integer> list = edge.get(i);
for (int x : list) {
if (visited[x] == false){
dfs(edge, visited, x);
}
}
}
}
经过多次测试,时间确实优于第二搏中的方法。
第四搏
按照官方给出的题解,转换一种思路。
一个石子的行和列上所有的石子均为联通关系,所以可以将这个行和列看作是联通的。因此如果多个石子属于同一个联通集合时,他们的行和列也可以组成一个集合。不同集合的行和列绝对不会重合。
并查集可以完美解决这个问题