【力扣时间】【1610】【困难】可见点的最大数目

1、康康题

我是题目

如果今天的题没有图示,是颇有些难理解的。
所以建议大家多读几遍,再结合图示和自己的画图来加强理解。

2、审题

今天的题不愧是困难,和几何问题密切挂钩。
虽然只是初中程度的三角函数知识,但是……
初中的数学知识早就还给老师了啊!

总之,在你读完题后,无论是否理解,都养成习惯性地多审题,从字里行间提炼出重点来。

辣么看看今天的重点:

  1. 观测点不能移动,但可以在原地360度旋转,但是视野范围始终固定。
  2. 同一个坐标可以有多个点,且点之间不会妨碍。
  3. 点可能与观察点上重合,且重合时,无论视野范围和视角,都可以观测到这个点。
  4. 视角可能为0。(这个实际上不影响你做题,但我就是想吐槽看一条线的情况

基于第2条,要是再给点来个高度,且高的点会挡住低的点,辣今天的题就起飞了。

今天的题,可以很容易地想到是个滑动窗口题。
你的视野范围是固定的窗口,控制视角来让它滑动,从而容纳更多的点。
实际上,我就是基于这样的第一印象来实现的,虽然最后有些许不同。

3、思路

其实看完题目,我就有明确的思路了。 但是算上实际实现的时间,是严重超出了给自己定下的时间线。
重点无他,纯粹在于忘光的数学知识如何应用平时用不上的API

辣么,来说下我的思路吧。

读完题后,我就始终从滑动窗口的实现角度来思考。
我们控制视角转动,确实就是在滑动窗口。但是,如果让二维的点映射到一维的角度上呢?

答案也很简单,运用三角函数
在这里插入图片描述
如图,我将一个二维坐标系划分为360°。
接着,我们以点作垂直与x轴的直线,与原点构成三角形,此时斜边的对角即为该点对应的角度
在这里插入图片描述
当然,如果点位于其他象限时,需要处理下角度。
总之,规定 第一象限内的点构成的角度为0° ~ 90°;第二象限内的点构成的角度为90° ~ 180°;第三象限为180° ~ 270°;第四象限为270° ~ 360°。

从而,就把二维的点,映射成了一维的数字(角度)。
辣么窗口,就可以开始滑动了。

说是这么说,鬼知道我查了多少资料。
什么角度和弧度,tan运算,如何用java的Math工具类计算角度等等等等……

4、开工!

class Solution {

    public int visiblePoints(List<List<Integer>> points, int angle, List<Integer> location) {
        int count = 0;
        List<Double> pAngles = new ArrayList<>(points.size());
        for (List<Integer> point : points) {
            //以视角为新坐标系原点
            int[] p = new int[]{point.get(0) - location.get(0), point.get(1) - location.get(1)};

            //与视角重合时,无论角度,直接计为可视
            if (p[0] == 0 && p[1] == 0) {
                count++;
                continue;
            }

            //其余的点,加入角度列表
            pAngles.add(getAngle(p));
        }

        //从小到大排序
        pAngles.sort(Double::compareTo);

        //最大窗口
        int windows = this.moveWindows(pAngles, angle);

        //窗口和重合点
        return count + windows;
    }

    private int moveWindows(List<Double> angles, int ang) {
        if (angles.size() == 0) {
            return 0;
        }

        Double delta;
        int offset = 0, length = 1;

        int maxLength = 0;
        while (offset < angles.size() && length <= angles.size()) {
            //将delta置为第i个点所在的角度
            delta = angles.get(offset);

            //当前游标
            int cur = offset + length - 1;
            if (cur >= angles.size()) {
                cur -= angles.size();
            }

            Double angle = angles.get(cur);
            //绕一周时
            if (cur < offset) {
                angle += 360;
            }

            if (angle <= delta + ang) {
                //计算最大长度
                maxLength = Math.max(maxLength, length);
                //扩大窗口
                ++length;
            } else {
                //移动窗口
                offset += 1;
                length -= 1;
            }
        }

        return maxLength;
    }

    public static double getAngle(int[] point) {
        double x = point[0];
        double y = point[1];
        //4个特殊角度
        if (y == 0) {
            return x > 0 ? 0 : 180;
        }
        if (x == 0) {
            return y > 0 ? 90 : 270;
        }

        double angle = Math.toDegrees(Math.atan2(y, x));

        if (y > 0) {
            return angle;
        } else {
            return 360 + angle;
        }
    }

}

依旧是没有精简过的代码。粗糙但是条例清晰,能够反应我最原始的思路。

5、解读

除了主函数外,我还实现了两个方法来辅助实现逻辑。

其一则是将点变换为角度的方法:

public static double getAngle(int[] point) {
        double x = point[0];
        double y = point[1];
        //4个特殊角度
        if (y == 0) {
            return x > 0 ? 0 : 180;
        }
        if (x == 0) {
            return y > 0 ? 90 : 270;
        }

        double angle = Math.toDegrees(Math.atan2(y, x));

        if (y > 0) {
            return angle;
        } else {
            return 360 + angle;
        }
    }

学习了大牛的思路后,发现这里确实做复杂了,但这的确是我最原始思路的体现。

其中有使用到Math类的两个方法。Math.atan2能根据点的横纵坐标计算出角的弧度Math.toDegrees则是变弧度为角度
如果初中数学忘得差不多了的话,建议提前先补补课本社畜其实也忘得一干二净了
之后的其他逻辑,则是辅助变化角度为我在思路中设计的规则。

其二,是滑动窗口逻辑,也是核心的逻辑:

private int moveWindows(List<Double> angles, int ang) {
        if (angles.size() == 0) {
            return 0;
        }

        Double delta;
        int offset = 0, length = 1;

        int maxLength = 0;
        while (offset < angles.size() && length <= angles.size()) {
            //将delta置为第i个点所在的角度
            delta = angles.get(offset);

            //当前游标
            int cur = offset + length - 1;
            if (cur >= angles.size()) {
                cur -= angles.size();
            }

            Double angle = angles.get(cur);
            //绕一周时
            if (cur < offset) {
                angle += 360;
            }

            if (angle <= delta + ang) {
                //计算最大长度
                maxLength = Math.max(maxLength, length);
                //扩大窗口
                ++length;
            } else {
                //移动窗口
                offset += 1;
                length -= 1;
            }
        }

        return maxLength;
    }

考虑到视线转动过程中,视角会扫到接近360°,从而让视野把第一象限中本已经滑出窗口的点重新扫到。
所以官解是提前就将点的数量翻倍,添加了原始点+2π的弧度进数组中,如下:

 int m = polarDegrees.size();
 for (int i = 0; i < m; ++i) {
 	polarDegrees.add(polarDegrees.get(i) + 2 * Math.PI);
 }

而我没有这样做,取而代之,是让窗口在滑动超过数组后,回到数组的头部。相当于使用了一个环状数组。(其实这里可以用环状链表的,但我不想让代码更复杂了,所以就算了。
如下:

int cur = offset + length - 1;
if (cur >= angles.size()) {
    cur -= angles.size();
}

Double angle = angles.get(cur);
//绕一周时
if (cur < offset) {
    angle += 360;
}

当然,扫过了一周的话,也会让角度加上360。

最后再回到主函数上

public int visiblePoints(List<List<Integer>> points, int angle, List<Integer> location) {
        int count = 0;
        List<Double> pAngles = new ArrayList<>(points.size());
        for (List<Integer> point : points) {
            //以视角为新坐标系原点
            int[] p = new int[]{point.get(0) - location.get(0), point.get(1) - location.get(1)};

            //与视角重合时,无论角度,直接计为可视
            if (p[0] == 0 && p[1] == 0) {
                count++;
                continue;
            }

            //其余的点,加入角度列表
            pAngles.add(getAngle(p));
        }

        //从小到大排序
        pAngles.sort(Double::compareTo);

        //最大窗口
        int windows = this.moveWindows(pAngles, angle);

        //窗口和重合点
        return count + windows;
    }

由于观察点本身不在原点,所以初始化角度数组时,我们是以观察点为原点构筑了新的坐标系。

int[] p = new int[]{point.get(0) - location.get(0), point.get(1) - location.get(1)};

然后,由于与观察点重合的点,无论视角如何转动都会被扫到,所以我用count直接记录了这些点的数目,不用将他们放入窗口数组内。

 //与视角重合时,无论角度,直接计为可视
 if (p[0] == 0 && p[1] == 0) {
       count++;
       continue;
 }

之后,就是调用上述的两个辅助函数,求出最大窗口大小。
最后返回的数量不要忘记加上重合点的数量。

6、提交

在这里插入图片描述
最后,就做了这么一个高不成低不就的解法出来。
不过对于困难题,我也能接受了。

虽然时间上有有很多可以优化的地方,但是想想算了,社畜也没有太多的精力。

7、咀嚼

今天的写法,让我自己都有点嚼不动啊……

首先遍历points[],时间复杂度为O(N),N为points[]的长度。
之后,进行了一次排序,时间复杂度为O(NlogN)
但是滑动窗口里的时间复杂度不太会分析,虽然有些浪费时间,但复杂度应该还是线性的,姑且记为O(2N)
于是整体的时间复杂度应该是O(3N + NlogN) = O(NlogN)
空间复杂度O(N)

虽然我习惯使用空间换时间,但这次没想到如何去换……

8、他人的智慧

庆幸的是,我和官解的思路一样,并且大牛们的思路也是大同小异。
差异仅仅在于对角度转换的处理,和查找时的实现上,并且官解给出了二分查找和滑动窗口查找两种解法。

由于滑动窗口解法更优,且更方便记忆,于是推荐大家还是尽量采用滑动窗口。

这里还是放出优质解法

9、总结

数学真是有趣啊。

今天的题不愧是困难难度。
而其主要难点,可能在于对数学知识的应用上。
三角函数虽只是初中数学的知识,但你早就忘光了吧现实生活中鲜少运用得到,加上工作里也挺难涉及的。所以尽管你可能思路明确,但是在实现上却会浪费过多的时间和精力。

同样的,如果在面试中遇上了思路明确,但是不太好实现或是实现起来过于耗时的题的话,也可以试着向面试官口述思路。
只要讲解正确,能让面试官知道你的真材实料,还是能够得分的。

周四了!挺住啊社畜!明天下班就自由了!

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值