加载中...

错题本 | LeetCode947. 移除最多的同行或同列石头

题目链接:https://leetcode-cn.com/problems/most-stones-removed-with-same-row-or-column/

题目

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] 不能移除,因为它没有与另一块石头同行/列。

分析

其实不知道这题该算错题还是难题,毕竟它的思路是简单的,但是我并没有想到。

暂且归为错题罢。借由此题捡回了并查集的相关知识,在实现并查集的过程中有一些细节上的错误导致了一次WA,本篇将加以记录。

分析:

由题意可知,在同一行或同一列上的石头属于同一个集合。显然,这样的集合永远可以找到一个删除的顺序,使得集合中只剩下唯一一个石头。

于是题目转化为了以行列为依据的并查集问题。对于一块石头idx,其坐标为(x, y),如果x行上已经有了先来的石头root_x[x],那么将idx加入先前就存在的root_x[x]的并查集中;否则表示idx是这一行最先到达的石头,其后的所有石头都要加入idx的并查集中。对于y列来说同理。

于是实现并查集如下:

int parent[1010];  // 并查集
// memset(parent, -1, sizeof(parent));
// 或者
// for (i = 0; i < 1010; i++) parent[i] = i;
void join(int parent_idx, int son_idx)
{
    int root_p = find(parent_idx);
    int root_s = find(son_idx);
    if (root_p == root_s)
        return;
    else
        parent[root_s] = root_p;
}
int find(int idx)
{
    if (parent[idx] == -1)
        return idx;
    parent[idx] = find(parent[idx]);  // 路径压缩
    return parent[idx];
}

我们遍历所有石头,谁先到达某一行,之后这一行上的石头都要作为它的子节点;谁先到达某一列,之后这一列上的石头都要作为它的子节点。

这一轮遍历之后,所有同行同列的石头都处于同一个并查集中;而全局中并查集的个数就是剩余的石头个数。

class Solution {
public:
    int parent[1010];  // 并查集
    int root_x[10010];
    int root_y[10010];
    void join(int parent_idx, int son_idx)
    {
        int root_p = find(parent_idx);
        int root_s = find(son_idx);
        if (root_p == root_s)
            return;
        else
            parent[root_s] = root_p;
    }
    int find(int idx)
    {
        if (parent[idx] == -1)
            return idx;
        parent[idx] = find(parent[idx]);  // 路径压缩
        return parent[idx];
    }
    int removeStones(vector<vector<int>>& stones) {
        
        int idx, x, y;
        memset(parent, -1, sizeof(parent));
        memset(root_x, -1, sizeof(root_x));
        memset(root_y, -1, sizeof(root_y));

        for (idx = 0; idx < stones.size(); ++idx)
        {
            x = stones[idx][0];
            y = stones[idx][1];
            if (root_x[x] == -1)
            {
                root_x[x] = idx;
            }
            else
            {
                join(root_x[x], idx);
            }
            if (root_y[y] == -1)
            {
                root_y[y] = idx;
            }
            else
            {
                join(root_y[y], idx);
            }
        }

        int cnt = 0;
        for (idx = 0; idx < stones.size(); ++idx)
        {
            if (find(idx) == idx)
            {
                ++cnt;
                cout << idx << " ";
            }
        }
        return stones.size() - cnt;
    }
};

错误记录

并查集中的join函数,原本的写法为:

void join(int parent_idx, int son_idx)
{
    parent[son_idx] = parent_idx;
}

这样的写法是错误的,且非常之离谱。我们知道,两个属于不同并查集的元素的合并意味着两个并查集的合并,显然不能单纯地修改这两个元素本身,而要考虑到这两个并查集的根节点。

就本题来说,这样的写法无法通过这个测试样例:

[[0,1],[1,0],[1,1]]

对于最后一块石头(1, 1),第1行已经有了石头1,第1列已经有了石头0。按照遍历过程的逻辑:

for (idx = 0; idx < stones.size(); ++idx)
{
        x = stones[idx][0];
        y = stones[idx][1];
        if (root_x[x] == -1)
        {
            root_x[x] = idx;
        }
        else
        {
            join(root_x[x], idx);
        }
        if (root_y[y] == -1)
        {
            root_y[y] = idx;
        }
        else
        {
            join(root_y[y], idx);
        }
}

最后一块石头最终将加入0号并查集。且此时剩余独立的并查集有01

然而,最后一块石头实际上是01的交汇点。按照正确的流程,它根据x坐标首先加入1号并查集后,再加入0号并查集,这时就会将1号石头也带入0号并查集,这也是本题使用并查集思路的正确性所在。

正确的办法是在合并时考虑根节点的修改,详见前文代码。

此外,本题使用的并查集还可以进行优化,包括针对本题的封装(参考[1]),和针对并查集这一结构本身的按秩合并和路径压缩(参考[2])。

本题还有基于图论的解法,不在本篇讨论范围内。

参考资料

[1] 小宇.简单思路+优化(超100%)[EB/OL].2021-01-15

https://leetcode-cn.com/problems/most-stones-removed-with-same-row-or-column/solution/jian-dan-jie-fa-chao-100-by-mantoufan-k3ne/

[2] yex.【详解】并查集[EB/OL].2021-01-15

https://leetcode-cn.com/problems/most-stones-removed-with-same-row-or-column/solution/tu-jie-bing-cha-ji-by-yexiso-nbcz/

10 comments
Anonymous
Markdown is supported
@mpv945
mpv945commentedover 2 years ago

添加图片,如果使用外部图床的http链接 。图片无法点击放大,你那边怎么解决的?

@SGS4ever
SGS4evercommentedover 2 years ago

@mpv945
添加图片,如果使用外部图床的http链接 。图片无法点击放大,你那边怎么解决的?

我的博客没有使用图床,所以没办法帮到你~

@Celetherin
Celetherincommentedover 2 years ago

您好,我也是使用的stack主题,我在照着您的方法添加返回顶部按钮时,遇到了按钮虽然出现、也能够点击,但无法实现实际上的返回顶部功能的问题,我没有任何的代码知识,不知道您有没有解决方法?
另外,也是想提醒一下其他需要这篇教程的朋友,最新版的stack主题,添加返回按钮的组件应该在layouts/partials/sidebar/right.html, 在layouts/_default/single.html中添加代码会导致出现两个右边栏。

@jsjcjsjc
jsjcjsjccommentedover 2 years ago

请教一下博主,如何优雅的给stack主题添加广告哈?
我只想在左或者右侧边栏底部,或者每篇文章底部添加一个小小的广告,但是默认似乎的满屏广告哈~~
感谢

@SGS4ever
SGS4evercommentedover 2 years ago
@ClimbingMouse
ClimbingMousecommentedalmost 2 years ago

你好,按照你的方法设置页面载入动画,这个动画不会停止咋办啊

@46fafa
46fafacommentedalmost 2 years ago

博主你好,请问一下主页布局修改哪里的代码如何作用于整个网页,我发现修改后的布局只存在主页和前两篇文章,其他部分还是没修改的样子

@4kohakunushi
4kohakunushicommentedover 1 year ago

你好,关于左侧栏图标高亮我这里存在一些问题想请教你。我取消了原本主页直接抓取post的内容在中间显示的版块,这个部分改成了其他东西,与此同时新增了一个抓取post信息的与links、search等目录并列的一个目录,现在的问题是这些部分虽然都能正常显示,但是对应的抓取post的那个目录无法选中以后高亮,应该修改增加什么才能让它也可以选中后高亮呢?

@SGS4ever
SGS4evercommentedover 1 year ago

首先我只能基于本文使用的Stack版本来尝试解答,因为没看过当前的Stack主题的代码~
我重新翻了下此前写的关于高亮的内容,理论上只要你的post页面的标题在menu配置中即可高亮。如果post页面是你站点的根路径,那应该可以参考我的文章里写的方法,修改下active的触发逻辑~

@4kohakunushi
你好,关于左侧栏图标高亮我这里存在一些问题想请教你。我取消了原本主页直接抓取post的内容在中间显示的版块,这个部分改成了其他东西,与此同时新增了一个抓取post信息的与links、search等目录并列的一个目录,现在的问题是这些部分虽然都能正常显示,但是对应的抓取post的那个目录无法选中以后高亮,应该修改增加什么才能让它也可以选中后高亮呢?

@sansan-cc
sansan-cccommentedabout 1 month ago

感谢博主的建站帖子,有很大的帮助。

有朋自远方来,不亦说乎?