1、康康题
如果今天的题没有图示,是颇有些难理解的。
所以建议大家多读几遍,再结合图示和自己的画图来加强理解。
2、审题
今天的题不愧是困难,和几何问题密切挂钩。
虽然只是初中程度的三角函数知识,但是……
初中的数学知识早就还给老师了啊!
总之,在你读完题后,无论是否理解,都养成习惯性地多审题,从字里行间提炼出重点来。
辣么看看今天的重点:
- 观测点不能移动,但可以在原地360度旋转,但是视野范围始终固定。
- 同一个坐标可以有多个点,且点之间不会妨碍。
- 点可能与观察点上重合,且重合时,无论视野范围和视角,都可以观测到这个点。
- 视角可能为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、总结
数学真是有趣啊。
今天的题不愧是困难难度。
而其主要难点,可能在于对数学知识的应用上。
三角函数虽只是初中数学的知识,但你早就忘光了吧现实生活中鲜少运用得到,加上工作里也挺难涉及的。所以尽管你可能思路明确,但是在实现上却会浪费过多的时间和精力。
同样的,如果在面试中遇上了思路明确,但是不太好实现或是实现起来过于耗时的题的话,也可以试着向面试官口述思路。
只要讲解正确,能让面试官知道你的真材实料,还是能够得分的。
周四了!挺住啊社畜!明天下班就自由了!