java使用Dijkstra算法实现地图最短路径模型
最近甲方国企因为换领导要裁撤所有外包员工(听来的早的同事说他们每隔一两年换一波外包,全裁后再根据需求招,满满的套路),我TM才干了四个月,回来西安的第一份工作就这么坑,西安的软件环境真是没得说,还是怀念北京老东家的那伙人,好吧,不得已又得开始找工作,各种工具框架什么的看着真是没劲啊,所谓的java基础是复习了一遍又一遍,忘了一边又一遍,之前勤快的时候敲了N多遍没事看个源码,现在懒了最多也是看看面试题。说起复习技术,最不喜欢看的就是工具,容器中间件等,跟着教程走,记着哪里怎么弄,原理是什么,出现问题怎么处理,话说出现问题大部分人又能怎么处理,现学看看原理+百度+灵活思辨处理呗怎么处理,都是遇到问题主要靠摸索攒经验而已,但这些问题平时基本不会遇到,遇到了也都是现学现用,真是么得意思。再就是框架(特别讨厌面试时问框架原理的三流面试官,这纯粹是在考验记忆力,跟编程没有毛线关系),框架有什么可以问的:大到场景应用,小到灵活巧用,无非就这稍有点用,它也就是个标准工具而已,也么意思的很。相对比个人还是更喜欢场景题,提出场景,怎么设计怎么优化怎么解决,没有标准答案,这多有意思,沟通-思考-商议-设计-开发,多有意思,哈哈。
说了好多,不过说起技术,个人更喜欢深究摸索算法类的,实现某个模型功能,优化提升效率等等。这篇文章内容是两年半前在上家公司做的,当时拿到的需求就五百字的word文档(关于应急疏散的最优策略和最短路径的设计,吧啦吧啦),写了些看不懂的公式和描述(最后发现是公司领导在网上抄的,他也看不懂),其他就没了。至于这玩意要做啥,做成什么样,没人说也没人会,老板说:“你自己就看着弄,我们项目是应急项目,应急疏散功能是合同上有的,只要最后有这个功能就行,具体什么样你自己定”。
好吧,万事开头难,既然要做那最起码得差不多点,自此,开启现学现用模式,个人的设计不讲究多复杂,拷贝地图软件最短路径的思路呗,项目有用到GIS地图,那就基于地图做最短路径,然后搜资料文档,摸索实现方案,最后就整了这么一套。
进入正文:该方式基于地图中路口和路线作为建模数据,在地图中找到一片样本区域,基于这片区域标注出所有的路口节点和路线,在百度API提供的接口中获取标注路口节点的经纬度;针对样本区域中的曲线路段或非路口拐点,将曲线颗粒化为多个直线,再依次获取每个直线截取点(未作为路段起始或结束点的节点),如下图中的黄色点,因为地图上是根据经纬度标绘直线的,曲线或拐点的路段需要分段成多个点标绘。我截取的区域如下:

为什么要这么设计呢,主要还是根据Dijkstra算法的数据需要,Dijkstra算法的详细讲解可以百度了解。根据Dijkstra算法的特点我们得先有符合算法的结构数据:每个点可到达的其他点的集合,集合中可到达的点信息包含点ID和距离,如下:
// 点2: {点3:110 ,点8:100 }
// 点3:{点2:110}
// 点8:{点2:100 ,点9:120}
// 点9:{点8:120 , 点10:90 , 点13:89}
// 点10:{点9:90 ,...}
// 点13:{点9:89 ,... }
准备:
一、表的设计如下,总共两张表:
1.节点表(id,节点名称,经度,维度,是否为路口节点,是否为事故点,是否为安置点,所在路段ID);
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "CeNodeConfig对象", description = "路口节点信息表")
public class CeNodeConfig implements Serializable {private static final long serialVersionUID = 1L;@ApiModelProperty(value = "主键")@TableId(value = "ID", type = IdType.AUTO)private Integer id;@ApiModelProperty(value = "路口名称")private String name;@ApiModelProperty(value = "经度(负值表示西经,正值为东经)")private String longitude;@ApiModelProperty(value = "纬度(负值为南纬,正值为北纬)")private String latitude;@ApiModelProperty(value = "事故爆发点最近节点 0:不是,1:是")private Integer sceneOfAction;@ApiModelProperty(value = "安置点最近节点 0:不是,1:是")private Integer temporaryShelter;@ApiModelProperty(value = "路口节点(0:不是,1:是)")private Integer crossingNode;@ApiModelProperty(value = "所在路线ID(非路口节点)")private Integer sectionId;
}
2.路段表(id,路段名称,路段物理距离,路段阻抗值,路段起始节点ID,路段结束节点ID,是否为双向路段);
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "CeSectionConfig对象", description = "路段信息表")
public class CeSectionConfig implements Serializable {private static final long serialVersionUID = 1L;@ApiModelProperty(value = "主键")@TableId(value = "ID", type = IdType.AUTO)private Integer id;@ApiModelProperty(value = "路段名")private String name;@ApiModelProperty(value = "路段物理距离")private Double physicalDistance;@ApiModelProperty(value = "风险阻抗值")private Double riskImpedanceValue;@ApiModelProperty(value = "路段起始节点ID")private Integer startNodeId;@ApiModelProperty(value = "路段结束节点ID")private Integer endNodeId;@ApiModelProperty(value = "是否为双向路段 0:不是,1:是")private Integer bothway;
}
二、数据准备:
使用百度坐标系获取上图中这些节点的经纬度,依次给这些节点定义名称;节点与节点之间的蓝色线为路段,定义路段名称,根据百度API或经纬度计算方法计算每个路段的长度;
将节点和路段的相关数据插入到节点表和路段表中(之前公司的项目,现在只有代码连不到数据库,建表语句离职退了电脑没了,数据就不贴了);
三、数据准备工作完成,开始实现
Dijkstra算法需要的是当前点到其他点以及距离的集合数据,那么先需要数据封装,将我们的节点数据和路段数据封装成算法需要的:
1.先查询节点网数据,根据路段表递归查询并关联获取所有路段数据数据,查询sql如下:
/*** 查询指定点可关联到的节点网集合:* 1.指定节点作为结束节点递归查出符合双向路段条件的根节点,如果没有根节点则指定节点为根节点;* 2.根据根节点查出所有子节点关联的路段信息;* 3.根据路段关联查询对应的起始和结束节点数据* @Param id 指定点ID* @return 关联的节点网集合*/@Select("select d.ID cId, d.NAME cName, d.LONGITUDE cLongi, d.LATITUDE cLati, " +" c.ID pId, c.NAME pName, c.LONGITUDE pLongi, c.LATITUDE pLati, " +" a.ID sId, a.NAME sName, a.PHYSICAL_DISTANCE sDistance, " +" a.RISK_IMPEDANCE_VALUE sRisk, a.BOTHWAY sBothway " +" from CE_SECTION_CONFIG a " +" right join ( " +" SELECT distinct(ID) id " +" FROM CE_SECTION_CONFIG START WITH START_NODE_ID = ( " +" select nvl(max(id), #{id}) as id " +" from ( " +" SELECT distinct(START_NODE_ID) id " +" FROM CE_SECTION_CONFIG where BOTHWAY = 1 START WITH END_NODE_ID = #{id} " +" CONNECT BY END_NODE_ID = PRIOR START_NODE_ID order by ID asc " +" ) a where rownum =1 ) " +" CONNECT BY START_NODE_ID = PRIOR END_NODE_ID order by ID asc " +" ) b " +" on a.id = b.id " +" inner join CE_NODE_CONFIG c on a.START_NODE_ID = c.ID " +" inner join CE_NODE_CONFIG d on a.END_NODE_ID = d.ID " +" order by c.ID asc ")List selectNextCeNodeConfig(Integer id);
上面的sql可以这么理解:路段关联的起始节点当作父节点,关联的结束节点当作子节点,给定一个节点作为子节点然后向上递归查询根节点,再根据根节点查询所有子节点作为起始节点的路段,这样就可以查询出指定节点关联的所有路线网。这里的指定节点可以理解为事故点,当发生事故需要疏散时,将事故附近最近的节点作为指定节点,这样递归查询后的结果则是查询出事故点可以到达的所有路线网。
我这里只是用的小样本区域数据并不大,如果是大面积的地图区域数据,那关联出来的路线网络数据量会很大,这种情况下就不适合全量递归加载,因为条条大路通罗马,所有的路线都会串起来,最终的结果是只要不是封闭区域的其他区域路线都会被查询加载,那数据量就大的离谱了,这种情况下可以加一些限制条件,比如计算路线的起始或结束节点和指定节点的直线距离,是否大于某个距离就不查询,至于距离的计算可以使用触发器自写函数等方式,或者使用权重过滤等方式,这里没涉及就先不考虑了。
查询返回的节点网数据结构如下:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "CeNextNodeConfig对象", description = "路段起始节点到结束节点的数据信息")
public class CeNextNodeConfig implements Serializable {private static final long serialVersionUID = 1L;@ApiModelProperty(value = "路段结束节点ID")@TableId(value = "cId", type = IdType.AUTO)private Integer cId;@ApiModelProperty(value = "路段结束节点路口名称")private String cName;@ApiModelProperty(value = "路段结束节点经度(负值表示西经,正值为东经)")private String cLongi;@ApiModelProperty(value = "路段结束节点纬度(负值为南纬,正值为北纬)")private String cLati;@ApiModelProperty(value = "路段起始节点ID")private Integer pId;@ApiModelProperty(value = "路段起始节点路口名称")private String pName;@ApiModelProperty(value = "路段起始节点经度(负值表示西经,正值为东经)")private String pLongi;@ApiModelProperty(value = "路段起始节点纬度(负值为南纬,正值为北纬)")private String pLati;@ApiModelProperty(value = "路段ID")private Integer sId;@ApiModelProperty(value = "路段名称")private String sName;@ApiModelProperty(value = "路段物理距离")private Double sDistance;@ApiModelProperty(value = "路段风险阻抗值")private Double sRisk;@ApiModelProperty(value = "路段是否为双向路段(0:不是,1:是)")private Integer sBothway;}
这里查询出后的结果以每条路段为一条数据,数据中包含路段信息,起始和结束节点信息;
2、接下来对路段集合信息做封装处理,
我们现有的数据如下:
//路段S1信息..., 起始节点N2信息...,结束节点N3信息...; 单向路段,无3-2
//路段S2信息..., 起始节点N2信息...,结束节点N8信息...; 双向路段,有8-2
//路段S3信息..., 起始节点N3信息...,结束节点N8信息...;
//路段S4信息..., 起始节点N3信息...,结束节点N4信息...;
//路段S5信息..., 起始节点N8信息...,结束节点N9信息...;
//路段S6信息..., 起始节点N8信息...,结束节点N2信息...; 双向路段,有2-8
......
我们想要的结果如下:
// 点2: {点3:110 ,点8:100 }
// 点3:{点2:110}
// 点8:{点2:100 ,点9:120}
// 点9:{点8:120 ,点10:90 , 点13:89}
// 点10:{点9:90 ,...}
// 点13:{点9:89 ,... }
封装方法如下:
//节点数据分类public static void nodeClassify(HashMap> graph,List startLD){Map> resultMap = new HashMap>();for(CeNextNodeConfig ce : startLD){if(resultMap.containsKey(ce.getPName())){//父节点已经存在resultMap.get(ce.getPName()).add(ce);}else{List theCes = new ArrayList();theCes.add(ce);resultMap.put(ce.getPName(),theCes);}}// 点2:{2-3,2-8} 点8:{8-9} 点9:{9-10,9-13}//调用数据封装方法for(Map.Entry> entry : resultMap.entrySet()){nodePackage(graph,entry.getValue());}}//节点数据封装public static void nodePackage(HashMap> graph,List startLD) {HashMap> graph1 = Dijkstra.clone(graph);String pName = startLD.get(0).getPName();int i = 0;for(Map.Entry> maps : graph1.entrySet()){if (pName.equals(maps.getKey())) { // 父节点在结果集中已经存在for(CeNextNodeConfig cs : startLD) {graph.get(maps.getKey()).put(cs.getCName(), cs.getSDistance()); //存储父节点到子节点if(cs.getSBothway() == 1){//双向路段int j = 0;for(Map.Entry> mapsz : graph1.entrySet()){if(cs.getCName().equals(mapsz.getKey())){ // 子节点在结果集中已经存在graph.get(mapsz.getKey()).put(pName,cs.getSDistance()); // 存储子节点到父节点j = 1;}}if(j == 0){ // 子节点在结果集中不存在HashMap map = new HashMap();map.put(pName,cs.getSDistance());graph.put(cs.getCName(),map); // 新增子节点到结果集中}}}i = 1;}}// 点2: {点3:110 ,点8:100 }// 点3:{点2:110}// 点8:{点2:100 ,点9:120}// 点9:{点8:120 , 点10:90 , 点13:89}// 点10:{点9:90 ,...}// 点13:{点9:89 ,... }// ...HashMap map = new HashMap();if(i == 0){ //父节点在结果集中不存在HashMap map1 = new HashMap();graph.put(pName,map1); //点2: {点3:110 ,点8:100 } 双向: 点3:{点2:110} 点8:{点2:100}for(CeNextNodeConfig cs : startLD) {map1.put(cs.getCName(),cs.getSDistance()); //存储父节点到子节点if(cs.getSBothway() == 1){//双向路段,存储子节点到父节点map = new HashMap();map.put(pName,cs.getSDistance());graph.put(cs.getCName(),map);}}}}
在上述封装过程中,进行了双向路段判断,这个是我当时根据现实生活中存在单向行驶路段的情况加的,双向路段的封装处理在上述代码中也考虑到了
调用时如下:
HashMap> graph = new HashMap>();
nodeClassify(graph,nextNodes);//封装节点网数据,nextNodes为查询出的路段网数据
3、至此,节点网数据已经封装完成,接下来就开始使用Dijkstra算法进行最短路径计算,实现如下:
//计算最短路径长度和路线public static Map> dijkstra(String start, String end, HashMap> graph){Map> result = new HashMap>();List pathL = new ArrayList<>();Map costMap = graph.get(start); //{点3:110 ,点8:100 ,点2:220}Map parentMap = new HashMap<>();HashSet processed = new HashSet<>();parentMap.put(start,null); //{点2:点3}processed.add(start); //点2,点3,点8while (!costMap.isEmpty()){String lowest = getLowestCostNode(costMap,processed); //点8if(lowest.equals(end)){String Node = end;Stack stack = new Stack<>();stack.push(Node);while(parentMap.get(Node) != null){stack.push(parentMap.get(Node));Node = parentMap.get(Node);}pathL.add(start);//System.out.print(start);while(!stack.isEmpty()){pathL.add(stack.pop());//System.out.print("->" + stack.pop() ); //使用栈中当前节点且弹出}//System.out.println();result.put(costMap.get(lowest), pathL);return result;}processed.add(lowest); // 点2,点3,点8HashMap map = graph.get(lowest); //{点2:100 ,点9:120}if(map != null && !map.isEmpty()){for(Map.Entry node : map.entrySet()){double cost = costMap.get(lowest) + node.getValue(); // 100 + 100if(!costMap.containsKey(node.getKey()) || (costMap.containsKey(node.getKey()) && cost < costMap.get(node.getKey()))){costMap.put(node.getKey(),cost); //{点2:220}parentMap.put(node.getKey(),lowest); //{点2:点3}}}}}return result;}public static String getLowestCostNode(Map costMap,HashSet processed ){double min = Double.MAX_VALUE;String res = null;for(Map.Entry entry : costMap.entrySet()){if(!processed.contains(entry.getKey()) && entry.getValue() <= min){min = entry.getValue();res = entry.getKey();}}return res;}
调用方式如下:
//start:起始节点名称
//end:结束节点名称
//封装后的节点网数据
Map> pathResult = dijkstra(start,end,grapha);
四、测试:这里我自己模拟了一小部分地图封装处理后的数据进行测试:

测试方式如下:
public static void main(String[] args){HashMap> graph = new HashMap<>();HashMap jd0 = new HashMap<>();jd0.put("点2",30D);jd0.put("点12",220D);graph.put("点1",jd0);HashMap jd1 = new HashMap<>();jd1.put("点1",30D);jd1.put("点3",110D);jd1.put("点8",210D);graph.put("点2",jd1);HashMap jd2 = new HashMap<>();jd2.put("点2",110D);jd2.put("点8",100D);jd2.put("点4",180D);graph.put("点3",jd2);HashMap jd3 = new HashMap<>();jd3.put("点2",210D);jd3.put("点3",100D);jd3.put("点9",30D);graph.put("点8",jd3);HashMap jd4 = new HashMap<>();jd4.put("点8",30D);jd4.put("点10",180D);jd4.put("点13",90D);graph.put("点9",jd4);HashMap jd5 = new HashMap<>();jd5.put("点4",130D);jd5.put("点9",180D);jd5.put("点15",100D);jd5.put("点11",170D);graph.put("点10",jd5);HashMap jd6 = new HashMap<>();jd6.put("点3",180D);jd6.put("点5",170D);jd6.put("点10",130D);graph.put("点4",jd6);HashMap jd7 = new HashMap<>();jd7.put("点1",220D);jd7.put("点13",140D);jd7.put("点18",150D);graph.put("点12",jd7);HashMap jd8 = new HashMap<>();jd8.put("点9",90D);jd8.put("点12",140D);jd8.put("点14",40D);graph.put("点13",jd8);HashMap jd9 = new HashMap<>();jd9.put("点13",400D);jd9.put("点15",160D);jd9.put("点20",100D);graph.put("点14",jd9);HashMap jd10 = new HashMap<>();jd10.put("点10",100D);jd10.put("点14",160D);jd10.put("点21",100D);graph.put("点15",jd10);HashMap jd11 = new HashMap<>();jd11.put("点12",150D);jd11.put("点20",160D);graph.put("点18",jd11);HashMap jd12 = new HashMap<>();jd12.put("点14",100D);jd12.put("点18",160D);jd12.put("点21",160D);graph.put("点20",jd12);HashMap jd13 = new HashMap<>();jd13.put("点15",100D);jd13.put("点20",160D);graph.put("点21",jd13);HashMap jd14 = new HashMap<>();jd14.put("点4",170D);jd14.put("点11",130D);graph.put("点5",jd14);HashMap jd15 = new HashMap<>();jd15.put("点5",130D);jd15.put("点10",170D);jd15.put("点22",200D);graph.put("点11",jd15);HashMap jd16 = new HashMap<>();jd16.put("点11",200D);jd16.put("点21",170D);graph.put("点22",jd16);Map> jg = dijkstra("点2","点9",graph);print(jg);}//打印最短路径计算结果public static void print(Map> pathResult){Set set=pathResult.keySet();List ggList = new ArrayList();for(double aa : set){ggList = pathResult.get(aa);System.out.println("==== 路径长度: "+aa+" ; 路径节点数: "+ggList.size());for(int i = 0;i "+ggList.get(i));}else{System.out.print(" -> "+ggList.get(i));}}}}
测试结果如下:
==== 路径长度: 240.0 ; 路径节点数: 3
==== 最短路径为: 点2 -> 点8 -> 点9
由结果可见:点2到点9的最短路径长度为240,经过的路口节点为:点2 -> 点8 -> 点9
五、处理结果:现在得到了计算后的最短路径节点和最短路径距离,这样的数据给前端无法使用,前端需要的是包含经纬度的点,这样才能在地图上标绘使用,需要封装处理结果返回给前端:
处理结果封装实体类:
@Data
@ApiModel(value="查询路线信息", description = "查询路线信息")
public class ShortesSectionMessageRspVo {@ApiModelProperty("路线ID")private Integer sectionID;@ApiModelProperty("路线名称")private String sectionName;@ApiModelProperty("起点信息")private Map originNode;@ApiModelProperty("终点信息")private Map terminusNode;@ApiModelProperty("其他节点信息")private List
封装处理结果:
//路段信息封装public List sectionPackage(Map> result, List nextNodesBF){List sectionVOList = new ArrayList();ShortesSectionMessageRspVo sectionM = new ShortesSectionMessageRspVo();for(Map.Entry> entry : result.entrySet()){Map originNode=new HashMap();Map terminusNode=new HashMap();List resNode =entry.getValue();//封装路径节点结果集for(int i=0; i();originNode.put("id",String.valueOf(ceNext.getCId()));originNode.put("name",ceNext.getCName());originNode.put("jd",ceNext.getCLongi());originNode.put("wd",ceNext.getCLati());sectionM.setOriginNode(originNode);//封装终点信息terminusNode=new HashMap();terminusNode.put("id",String.valueOf(ceNext.getPId()));terminusNode.put("name",ceNext.getPName());terminusNode.put("jd",ceNext.getPLongi());terminusNode.put("wd",ceNext.getPLati());sectionM.setTerminusNode(terminusNode);break;}else if(cName.equals(zdName) && pName.equals(qdName)){sectionM.setSectionID(ceNext.getSId());sectionM.setSectionName(ceNext.getSName());sectionM.setRiskValue(ceNext.getSRisk());sectionM.setSectionDistance(ceNext.getSDistance());//封装起点信息originNode = new HashMap();originNode.put("id",String.valueOf(ceNext.getPId()));originNode.put("name",ceNext.getPName());originNode.put("jd",ceNext.getPLongi());originNode.put("wd",ceNext.getPLati());sectionM.setOriginNode(originNode);//封装终点信息terminusNode = new HashMap();terminusNode.put("id",String.valueOf(ceNext.getCId()));terminusNode.put("name",ceNext.getCName());terminusNode.put("jd",ceNext.getCLongi());terminusNode.put("wd",ceNext.getCLati());sectionM.setTerminusNode(terminusNode);break;}}//封装其他节点信息int sectionID = sectionM.getSectionID();if(sectionID == 0){//没有路段信息continue;}List cesectionNodes = iCeNodeConfigService.list(new QueryWrapper().eq("SECTION_ID",sectionID).eq("CROSSING_NODE",0).orderByAsc()); // 根据路段ID查询该路段上的其它非路口节点List
上面封装计算结果时有个特别注意的地方:将非路口节点(曲线颗粒化节点或拐点节点,图中黄色点)封装进路线中,不然前端拿到的节点在地图上标绘时弯曲的路会直接标绘成直线,处理方式如上代码,根据路段关联查询每个路段的非路口节点并封装进路线结果集中。
剩下的就是将结果数据返回给前端上图,前端在地图上标绘出事故点、安置点、最短路线、距离等数据,还有一些封装事故点处理、起始点处理、路线阻抗值(某条路段拥堵情况、危险系数处理等)计算、路线等级计算等,跟最短路径算法无关我这里没写,如果要在真实场景使用,整个设计以及实现需要牵扯很多变量因素,这里的实现也只能算是个差很多的初始模板。这个功能最后客户没有用,只是在演示的时候使用我录入的区域数据在地图上进行演示,对于非专业地图公司实用性基本没有,最主要的原因是数据,非专业公司哪里来那么多路口节点和路段信息数据,总不可能人工去地图软件上手工扒取,之前想过用程序脚本爬取,可是看了各家地图软件以及和gis同事讨论后结果是爬取不可能,因为爬取到的数据和我要的数据不是一个路数,除非我的数据进行重新设计,所以实用性基本没有,被老板拿来给客户充数演示而已,因此后续的很多功能我最后也没再做。
整个设计实现测试等都是自己完成,大概10-15个工作日左右吧,主要是前期的设计和实现思路花了不少时间,只要想好了剩下开发的事都好说,拿到需求后我第一时间想到的是结合生活中地图软件的方式,老板的意思也是结合地图要一个疏散方案的实现,当时因为别的业务开发我和前端GIS开发同事交互了很多,也知道前端只需要数据点经纬度,基于这个前提,我开始上网搜索最短路径的实现方案,网上介绍最多也是最广泛被使用的算法就是Dijkstra算法,很多其他公司的最短路径也是基于Dijkstra算法的理念设计的,针对这个算法看了一些网上的代码案例,怎么说呢,算法这东西,你不清楚它的原理和模型思路,看代码再多都没用,最后无奈一心搜索Dijkstra算法的很多思路模型案例,大致明白以后才结合我要做的场景开始设计数据结构,算法要的是点以及点到点的距离作为计算条件,结合这个思路开始设计表结构和数据以及数据封装。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
