再做一个项目的时候,想到了这样一个问题:
涉及到使用百度地图,在地图中随机的选取任意多个区域,如何求出这些区域的一个中心点坐标;就是在若干个区域中,找出最中心的一个区域,用于展示到用户屏幕的最中心处。
上面的问题,简化成数学问题就是,在一个平面区域中,散落n的点 P 1 − P n P_1-P_n P1−Pn(n>0),求在这个平面区域(二维坐标系)上这些散落点组成的最少区域的中心点C,找到中心点后,在求出这若干点离中心点最近的点 P l P_l Pl.
对于上面问题,先不用证明问题是否可解,因为这就是编程问题,不用在数学上严格的去证明;
- 假设问题是有解的
- 用计算机编程如何实现
- 如何更高效,更简洁的实现,当散落的点越来越多,构成的区域多边形越来越复杂,如何提供更加高效算法
- 考虑区域
点密度加权
1,算出更加优化的区域“中心点”
问题解
下面进行问题解分析,(分析过程几乎可以证明问题是否可解了)
我只提供一种分析思路,不对的话请见谅
- 首先,定义一个点 Gps(lon,lat),然后将若干个点用集合List包装起来 List<Gps>;
- 甭管用什么方法,总是可以从list中取出一个点来(可以取第一个,也可以取最后一个,或者中间位置的一个或者随机一个),把它设置为中心点;
- 然后用剩下的点和中心点做距离比较,总能找到一个最大距离 D 1 D_1 D1,将这个距离保存下来;
- 然后重复从list取另外一个点作为中心点(比如,第一次取第一个,第二次取第二个),再跟其余点做距离比较,那么也会得到一个最大距离 D 2 D_2 D2,保存;
- 以此类推,重复步骤2,3,会得到一系列的最大距离 D 3 , D 4 , . . . , D n D_3,D_4,...,D_n D3,D4,...,Dn,并保存;
- 从这n个距离集合中,取最少距离的那个即为中心点,完毕。
上面过程中保存距离,应该使用Map这种数据结构,key为被选中的点,value为最大距离;然后从map中选出value最少的那个key。
上面的解决方案,基本上可以满足:用计算机编程如何实现这一条了,编程的话应该很easy;
现在考虑的是,这种过程是否高效简洁?
不好说,因为没有对比的其他解决方法,现在分析一下上面算法特点:
- 只涉及到加减法,这点应该是优势,不需要太多的数学理论
- 不断的循环,当n较少时,会很有利,当n不断增大后,可能循环多了就没意义了,甚至无法计算
- 这个算法解决不了,上面提到的“密度加权”问题
现在再说一下,为什么在最长距离集合中距离最短的,就是最中心点了?
这个最好有数学系的同学给证明一下,我也只是通过观察发现,可能最短就是中心点,用这种方式找出来的点是可能有多个中心点的,下面给大家分析一下
首先从最简单的开始,只有一个点,那么这个点就是中心点,这是事实;
两个点,那么任意两个都可作为中心点,也是事实;
三个点,假设三个点的距离都相等,那么这三个点任意一个可以作为中心点,如果三条边不相等,如图
对于ABC三个点,分别做连线,得到AB,AC,BA,BC,CA,CB,需要保证找到每个点的最大距离,那么会得到 A: AC,B:BC,C:CA;另外还要保证最大距离的半径下的圆形区域内将所有点包含,显然AC=CA>BC,那么就能选出B作为中心点;
可以再取四个点的区域,用上面的方法分析,首先正方形的四个点,会发现对角线是为最大距离,并且每个点的最大距离都相等,那么这是个点任意一个可以作为中心点;其他的任意四个点,也是总能找到某个点的最大距离,并且以这个距离为半径做圆形可以将四个点都包含在内,那么这个点就是中心点;
编程实现
前面已经进行了算法描述,下面接着编程如何实现,并测试效果
首先需要引入一个坐标类,用于标识某个点,并提供一个计算两点间距离的方法
类Gps
package com.iscas.tools;
/**
* 地理坐标 Gps <br>
*
* @author 王俊伟 wjw.happy.love@163.com
*
* @version 1.0.0
*/
public class Gps {
/** 纬度 */
public double lat;
/** 经度 */
public double lon;
public Gps(double lon, double lat) {
this.lat = lat;
this.lon = lon;
}
/**
* 求当前点跟目标点的距离
*
* @param gps
* 目标点
* @return 距离
*/
public double distance(Gps gps) {
if (this.equals(gps)) {
return 0d; // 同一个点距离为0
}
return EarthUtil.getDistance(this.lon, this.lat, gps.lon, gps.lat);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
Gps gps = (Gps) obj;
if (gps.lon == this.lon && gps.lat == this.lat) {
return true;
}
return false;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "(" + this.lon + "," + this.lat + ")";
}
public void print() {
System.out.println(this);
}
算法实现
然后看一下具体的算法实现部分,非常简单
/**
* 根据一组坐标选取中心点坐标
*
* @param gpses
* 坐标集合
* @return 中心点坐标
*/
public static Gps pickCenter(List<Gps> gpses) {
Gps g = null;// 目标中心点
Map<Gps, Double> maxDistances = new HashMap<>();
List<Double> distances = new ArrayList<>();
for (Gps center : gpses) {// 遍历每个中心点
for (Gps gps : gpses) {
distances.add(center.distance(gps));
}
Collections.sort(distances, Collections.reverseOrder());//逆序,需要取最大的距离
maxDistances.put(center, distances.get(0));
distances.clear();
}
// 对maxDistances,根据value排序,取距离最小的
g = maxDistances.entrySet().stream().sorted(Comparator.comparingDouble(e -> e.getValue())).map(m -> m.getKey())
.collect(Collectors.toList()).get(0);
return g;
}
上面算法比较简洁,原理就是,以某个点为中心,画同心圆,最后能在这些点中找到一个(或若干个)最短半径构成的圆形区域能恰好包含这些所有的点,这个圆形的中心点就是我们所需要的中心点。
测试
下面是我找了5组测试数据,分别测试上次,记录耗时,另外说明一下点的生成为随机生成 lon的范围[0,180),lat范围[0,90)
点数(个) | 耗时(ms) | 点数(个) | 耗时(ms) | 点数(个) | 耗时(ms) |
---|---|---|---|---|---|
100 | 75 | 100 | 124 | 100 | 109 |
1000 | 708 | 1000 | 768 | 1000 | 822 |
10000 | 61,671 | 10000 | 60,185 | 10000 | 60,661 |
100000 | 至少半个小时还没结果,kill了 | 100000 | 6546799 | 100000 | 实在不敢跑了 |
1000000 | 上面的还没跑完呢,double kill | 1000000 | – | 1000000 | – |
原本对设计时候对10万个点还是有信心的,没想到结果,真是一言难尽啊!
说一下我电脑配置吧:内存12G(这个肯定够了),cpu Intel i5-4460 3.20GHz 4核,结果在一万个点就掉链子了,这算法真是不测不知道,一测吓一跳,虽然前面已经分析过,随着点数增多,会有性能问题…
实际上在跑到1万个点测试的时候,就已经是令人不可接受了,一万个点大概需要1分钟才能找到中心点,在实际应用中我得项目最大承载量是需要在1万5千个点中找到中心点。
仔细想想,结合算法,对于1万个点,仅仅求任意两点的距离就需要计算1亿次,这里面第一次排序还需要执行1万次,监测可以发现1个点内排序一次大概1-2ms,1万次;对10万个点,内排序一次在20ms左右,也需要1万次。
这样再计算一下,对于10万个点,内排序就需要 ((20*100000)/1000 )/ 60 = 33min!
外排序较快172毫秒完成。
一百万个点的时候,一次内排序耗时360毫秒左右,这个时间真的耗费不起。
这个算法应该说对于项目来说是不符合预期的,还需要优化!
结语
上面已经提供了一套解决此问题的算法,但是算法并不完美,对于加权处理还不够;希望有大神能够提供更加完善的证明获取其他算法。
另外,上面全是基于二维平面,实际还可以扩展到三维空间,用上面的方式一样能解决问题。
点密度加权:平面区域内,散落的点呈现某种趋势,如一些区域稠密,一些区域稀疏等,对于稠密区权重要高于稀疏区,考虑到此条件下,找到的中心点应该偏向与稠密区域 ↩︎