加载中...
返回

错题本 | 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。

有朋自远方来,不亦说乎?
Built with Hugo
Theme Stack designed by Jimmy