加载中...
返回

错题本 | LeetCode1610. 可见点的最大数目

许久不曾做算法题了,今天的每日一题是道Hard,而我独立地将它搞定了,即便它只有Medium的程度。

实际上是一道没什么意思的题目,本篇主要记录几个思维上的不足点。

题目

给你一个点数组 points 和一个表示角度的整数 angle ,你的位置是 location ,其中 location = [posx, posy]points[i] = [xi, yi] 都表示 X-Y 平面上的整数坐标。

最开始,你面向东方进行观测。你 不能 进行移动改变位置,但可以通过 自转 调整观测角度。换句话说,posxposy 不能改变。你的视野范围的角度用 angle 表示, 这决定了你观测任意方向时可以多宽。设 d 为你逆时针自转旋转的度数,那么你的视野就是角度范围 [d - angle/2, d + angle/2] 所指示的那片区域。

对于每个点,如果由该点、你的位置以及从你的位置直接向东的方向形成的角度 位于你的视野中 ,那么你就可以看到它。

同一个坐标上可以有多个点。你所在的位置也可能存在一些点,但不管你的怎么旋转,总是可以看到这些点。同时,点不会阻碍你看到其他点。

返回你能看到的点的最大数目。

示例1

  • 输入: points = [[2,1],[2,2],[3,3]], angle = 90, location = [1,1]
  • 输出: 3
  • 解释: 阴影区域代表你的视野。在你的视野中,所有的点都清晰可见,尽管 [2,2] 和 [3,3]在同一条直线上,你仍然可以看到 [3,3] 。

示例2

  • 输入: points = [[1,0],[2,1]], angle = 13, location = [1,1]
  • 输出: 1

数据范围

  • 1 <= points.length <= 105
  • points[i].length == 2
  • location.length == 2
  • 0 <= angle < 360
  • 0 <= posx, posy, xi, yi <= 100

分析

根据数据范围,尝试采用 O(nlogn) 的解法即可。

问题转化

在计算可见范围时,我们一定要考虑到的东西不是坐标,而是角度,即 某个点与观测点的夹角

对于给定的任意一个点,我们将它作为观测角的 下边 ,然后以这条边为起点,逆时针转 angle 的角度,即可得到观测范围的 上边 ,这样就能得到一个观测范围。

对于这种完全依赖 夹角角度 而不依赖 坐标 的思路,我们应该能够想到 将直角坐标转化为极坐标

所谓 极坐标 ,是以观测点为原点,将某点表示为 (ρ,θ) 的形式,即表示为 (极径,极角) 的形式。

这里我们不需要确切知道某个点到观测点的距离,只需要知道它的 极角 ,即它和观测点的夹角,约定以X轴正方向为零度,逆时针增加度数。

因此得到了一个将直角坐标转化为极坐标的函数:

#define PI 3.1415926535897932384626
vector<double> pointsInPolar;
int numberOfSuperpositionPoints;

void changePointsIntoPolar(vector<vector<int>>& points, vector<int>& O)
{
    int ox = O[0];
    int oy = O[1];
    numberOfSuperpositionPoints = 0;

    for (auto p : points)
    {
        int px = p[0];
        int py = p[1];
        if (px == ox && py == oy)
        {
            numberOfSuperpositionPoints++;
            continue;
        }
        if (px == ox)
        {
            if (py > oy)
                pointsInPolar.push_back(90.0);
            else
            {
                pointsInPolar.push_back(-90.0);
                pointsInPolar.push_back(270.0);
            }
            continue;
        }
        // 计算夹角
        double pointAngleInPolar = atan2(py - oy, px - ox) * 180 / PI;
        pointsInPolar.push_back(pointAngleInPolar);
        if (pointAngleInPolar < 0)
            pointsInPolar.push_back(pointAngleInPolar + 360.0);
    }
    
    sort(pointsInPolar.begin(), pointsInPolar.end());

    return ;
}

有几个重点要注意:

  • C++函数 atan2 用法参见 atan2 - C++ Reference (cplusplus.com) ,它得到的值是 弧度 ,要转为角度(乘180再除以π);
  • 当给出的点和观测点重合,认为它永远处于视野范围内,用一个变量 numberOfSuperpositionPoints 加以记录。
  • 当两个点的 X 坐标相等,要根据 Y 坐标的大小判断角度。
  • 负数的角度表示从 X 轴正方向 顺时针 旋转,那么这种负数角度应该也有正数表示,即加上 360°
  • 第三点很重要,并影响到第二点

我们可以看到代码中对于负数角度的处理,假如我们有一组数据:

[[0,0],[0,2]]

90

[1,1]

如果放任负数角度不管,我们得到的转化为极坐标的点应该是 [-135.0, 135.0] ,这样,不管以哪个点为视野下边,加上了 90° 的观察角,都无法看到另一个点了。

而按照我们的代码中的处理办法,对于这个负角度的点,我们额外将它的等价正角度点放入了集合中,即应该得到 [-135.0, 135.0, 225.0] 的集合,那么只要以 135.0° 为视野下边,然后加上 90° 的观察角,就能够看到 225° 的点,实际上就是刚才被忽略的负角度的点。

这样做实际上有不小的坏处,那就是 引入额外的空间和时间复杂度 !本来只有一个负角度点,现在要额外存储并计算它的等价正角度点,还是很花费空间和时间的。

但是我今天只是作一个错题的记录,就不考虑它的优化方案了。

求解

现在我们已知视野下边和观察角,相当于也已知视野上边了。要快速从所有点中找到可见点的数目,很简单,做一个二分查找即可。

使用二分查找的办法快速找到 第一个视野上边之上 的点。

int calcVvisiablePointsNumber(int startPointIndex, int visiableAngle)
{
    double finalVisiablePoint = pointsInPolar[startPointIndex] + visiableAngle;
    int l = 0;
    int r = pointsInPolar.size() - 1;

    if (pointsInPolar[r] <= finalVisiablePoint)
        return r - startPointIndex + 1;
    while (l <= r)
    {
        int mid = (l + r) >> 1;
        if (pointsInPolar[mid] <= finalVisiablePoint)
            l = mid + 1;
        else
            r = mid - 1;
    }

    return l - startPointIndex;
}

我们遍历所有点,分别以它们作为视野下边,用二分求出可见点数,时间复杂度 O(nlogn)

AC 代码

class Solution {
private:
    #define PI 3.1415926535897932384626
    vector<double> pointsInPolar;
    int numberOfSuperpositionPoints;
public:
    void changePointsIntoPolar(vector<vector<int>>& points, vector<int>& O)
    {
        int ox = O[0];
        int oy = O[1];
        numberOfSuperpositionPoints = 0;

        for (auto p : points)
        {
            int px = p[0];
            int py = p[1];
            if (px == ox && py == oy)
            {
                numberOfSuperpositionPoints++;
                continue;
            }
            if (px == ox)
            {
                if (py > oy)
                    pointsInPolar.push_back(90.0);
                else
                {
                    pointsInPolar.push_back(-90.0);
                    pointsInPolar.push_back(270.0);
                }
                continue;
            }
            // 计算夹角
            double pointAngleInPolar = atan2(py - oy, px - ox) * 180 / PI;
            pointsInPolar.push_back(pointAngleInPolar);
            if (pointAngleInPolar < 0)
                pointsInPolar.push_back(pointAngleInPolar + 360.0);
        }

        sort(pointsInPolar.begin(), pointsInPolar.end());

        return ;
    }

    int calcVvisiablePointsNumber(int startPointIndex, int visiableAngle)
    {
        double finalVisiablePoint = pointsInPolar[startPointIndex] + visiableAngle;
        int l = 0;
        int r = pointsInPolar.size() - 1;

        if (pointsInPolar[r] <= finalVisiablePoint)
            return r - startPointIndex + 1;
        while (l <= r)
        {
            int mid = (l + r) >> 1;
            if (pointsInPolar[mid] <= finalVisiablePoint)
                l = mid + 1;
            else
                r = mid - 1;
        }

        return l - startPointIndex;
    }

    void debugOut()
    {
        for (auto agl : pointsInPolar)
            cout << agl << " ";
        cout << endl;
    }

    int visiblePoints(vector<vector<int>>& points, int angle, vector<int>& location) {
        changePointsIntoPolar(points, location);
        
        int res = numberOfSuperpositionPoints;

        // debugOut();

        for (int i = 0; i < pointsInPolar.size(); i++)
        {
            int nowVisiablePointNumber = calcVvisiablePointsNumber(i, angle);
            res = max(res, nowVisiablePointNumber + numberOfSuperpositionPoints);
        }

        return res;
    }
};

错误记录

In fact,我的代码表现蛮差劲的,但也作为我花费一定时间和脑力独立解决的一道困难题加以记录,仅供自娱自乐罢了。

可以看到我WA了多少次,在此记录一下解题过程中的若干盲区:

  • 负角度点没有转化等价为正角度;
  • 忽视了“重合点永远可见”这个道理;
  • 二分边界。

Over。

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

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

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