2021-01-15 移除最多的同行或同列石头

题目:

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
  • 不会有两块石头放在同一个坐标点上

题解

方法一:深度优先搜索

思路及解法

我们将这个二维平面抽象成图,把石子看作「点」,石子间的同行或同列关系看作「边」。如果两个石子同属某一行或某一列,我们就认为这两个石子之间有一条边。由题意可知,对于任意一个点,只要有点和它相连,我们就可以将其删除。

显然,对于任意一个连通图,我们总可以通过调整节点的删除顺序,把这个连通图中删到只剩下一个节点。本题中我们不需要关注如何安排删除顺序,只需要了解这个性质即可。

拓展:

对于希望进一步拓展的同学,这里给出一个方法:从连通块中处理出任意一个生成树,该生成树的以任意一点为根节点的后序遍历均为可行解。

这样我们只需要统计整张图中有多少个极大连通子图(也叫做连通块或连通分量)即可。最终能够留下来的点的数量,即为连通块的数量。我们用总点数减去连通块的数量,即可知道我们可以删去的点的最大数量。

在实际代码实现中,我们首先枚举计算任意两点间的连通性,然后使用深度优先搜索的方式计算连通块的数量即可。

代码
var removeStones = function(stones) {
    const n = stones.length;
    const edge = {};
    for (const [i, [x1, y1]] of stones.entries()) {
        for (const [j, [x2, y2]] of stones.entries()) {
            if (x1 === x2 || y1 === y2) {
                edge[i] ? edge[i].push(j) : edge[i] = [j];
            }
        }
    }

    vis = new Set();
    let num = 0;
    for (let i = 0; i < n; i++) {
        if (!vis.has(i)) {
            num++;
            dfs(i, vis, edge);
        }
    }
    return n - num;
};

const dfs = (x, vis, edge) => {
    vis.add(x);
    for (let y of edge[x]) {
        if (!vis.has(y)) {
            dfs(y, vis, edge);
        }
    }
}
复杂度分析

时间复杂度:O(n^2),其中 n 为石子的数量。我们需要枚举计算任意两个石子是否在同行或同列,建图时间复杂度 O(n^2),同时我们需要通过深度优先搜索计算连通块数量,每一个点和每一条边都被枚举一次,时间复杂度 O(n+m)。其中 m 是边数,可以保证 m < n^2 。因此总时间复杂度为 O(n^2)。

空间复杂度:O(n^2))。最坏情况下任意两点都相连,用来保存连通属性的边集数组将会达到 O(n^2) 的大小。

方法二:优化建图 + 深度优先搜索

思路及解法

注意到方法一中,建图的效率太过低下,我们考虑对其优化。

注意到任意两点间之间直接相连与间接相连并无影响,即我们只关注两点间的连通性,而不关注具体如何联通。因此考虑对于拥有 k 个石子的任意一行或一列,我们都恰使用 k-1 条边进行连接。这样我们就可以将边数从 O(n^2) 的数量级降低到 O(n)。

这样,我们首先利用哈希表存储每一行或每一列所拥有的石子,然后分别处理每一行或每一列的连通属性即可。

注意到每一个石子的横坐标与纵坐标的范围均在 [1,10^4],因此在实际代码中,我们可以使用同一张哈希表,只需要令纵坐标加 10^4,以区别横坐标与纵坐标即可。

代码
var removeStones = function(stones) {
    const n = stones.length;
    const edge = {};
    const rec = {};
    for (const [i, [x, y]] of stones.entries()) {
        rec[x] ? rec[x].push(i) : rec[x] = [i];
        rec[y + 10001] ? rec[y + 10001].push(i) : rec[y + 10001] = [i];
    }

    for (const vec of Object.values(rec)) {
        const k = vec.length;
        for (let i = 1; i < k; i++) {
            edge[vec[i - 1]] ? edge[vec[i - 1]].push(vec[i]) : edge[vec[i - 1]] = [vec[i]];
            edge[vec[i]] ? edge[vec[i]].push(vec[i - 1]) : edge[vec[i]] = [vec[i - 1]];
        }
    }

    const vis = new Set();
    let num = 0;
    for (let i = 0; i < n; i++) {
        if (!vis.has(i)) {
            num++;
            dfs(i, vis, edge);
        }
    }
    return n - num;
};

const dfs = (x, vis, edge) => {
    vis.add(x);
    if (edge[x]){
        for (const y of edge[x]) {
            if (!vis.has(y)) {
                dfs(y, vis, edge);
            }
        }
    }
    
}

复杂度分析

时间复杂度:O(n),其中 n 为石子的数量。任意一个石子至多只有四条边与其相连,且至多被遍历一次。

空间复杂度:O(n)。任意一个石子至多只有四条边与其相连,用来保存连通属性的边集数组至多只会达到 O(n) 的大小

方法三:优化建图 + 并查集

思路及解法

我们也可以变换思路,在方法一与方法二中,我们维护的是石子,实际上我们也可以直接维护石子所在的行与列。

实际操作时,我们直接将每一个石子的行与列进行合并即可,可以理解为,每一个点不是与其他所有点进行连接,而是连接到自己所在的行与列上,由行与列进行合并。

同时,既然我们只关注连通性本身,我们就可以利用并查集维护连通性。在实际代码中,我们以哈希表为底层数据结构实现父亲数组 ff,最后哈希表中所有的键均为出现过的行与列,我们计算有多少行与列的父亲恰为自己,即可知道连通块的数量。

代码
var removeStones = function(stones) {
    const dsu = new DisjointSetUnion();
    for (const [x, y] of stones) {
        dsu.unionSet(x, y + 10001);
    }
    return stones.length - dsu.numberOfConnectedComponent();
};

class DisjointSetUnion {
    constructor() {
        this.f = new Map();
        this.rank = new Map();
    }

    find (x) {
        if (!this.f.has(x)) {
            this.f.set(x, x);
            this.rank.set(x, 1);
            return x;
        }
        if (this.f.get(x) === x) {
            return x;
        }
        this.f.set(x, this.find(this.f.get(x)));
        return this.f.get(x);
    }

    unionSet (x, y) {
        let fx = this.find(x), fy = this.find(y);
        if (fx  && fy) {

        }
        if (fx === fy) {
            return;
        }
        if (this.rank.get(fx) < this.rank.get(fy)) {
            [fx, fy] = [fy, fx];
        }
        this.rank.set(fx, this.rank.get(fy) + this.rank.get(fx));
        this.f.set(fy, fx);
    }

    numberOfConnectedComponent () {
        let sum = 0;
        for (const [x, fa] of this.f.entries()) {
            if (x === fa) {
                sum++;
            }
        }
        return sum;
    }
}
复杂度分析

时间复杂度:O(nα(n)),其中 nn 为石子的数量。α是反 Ackerman 函数。

空间复杂度:O(n)O(n)。空间为并查集和哈希表的开销

第一次接触并查集
f指的是父每个元素父节点的集合
rank应该指的是每个节点有多少个子节点(包括自身)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值