借助 Dijkstra 算法计算苏州任意两个地铁站的路径

4,111 阅读6分钟
原文链接: www.jianshu.com

苏州四号地铁线在今年四月份终于要开始运营了,正好路过我家。以后每次从上海回来,就可以直接做地铁到家门口了。

既然四号线快要开通了,那么以后我从家门口到园区怎么换乘呢?作为程序员的我,肯定会考虑用什么算法才能找到一条可以换乘的路线。


苏州地铁规划线路.gif

从这个方向上看,这应该是一个有向图的最短路径问题。所以,我选择Dijkstra算法。算法的定义,我简单阐述一下,在大学的数据结构课程上应该都已学过。

算法思想:
设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。


Dijkstra算法步骤.gif

首先,我初始化一些地铁站点的初始数据。

    private static List<Station> line1 = new ArrayList<Station>();//1号线
    private static List<Station> line2 = new ArrayList<Station>();//2号线
    private static List<Station> line4 = new ArrayList<Station>();//4号线
    private static Set<List<Station>> lists = new HashSet<List<Station>>();//所有线集合
    private static int totalStaion = 0;//总的站点数量
    private List<Station> outList = new ArrayList<Station>();//记录已经分析过的站点

    static {
        //1号线
        String line1Str = "木渎站、金枫路站、汾湖路站、玉山路站、苏州乐园站、塔园路站、滨河路站、西环路站、桐泾北路站、广济南路站、养育巷站、乐桥站、临顿路站、相门站、东环路站、中央公园站、星海广场站、东方之门站、文化博览中心站、时代广场站、星湖街站、南施街站、星塘街站、钟南街站";
        String[] line1s = line1Str.split("、");
        for(String s : line1s){
            line1.add(new Station(s));
        }
        for(int i =0;i<line1.size();i++){
            if(i<line1.size()-1){
                line1.get(i).next = line1.get(i+1);
                line1.get(i+1).prev = line1.get(i);
            }
        }

        //2号线
        String line2Str = "高铁苏州北站、大湾站、富元路站、蠡口站、徐图港站、阳澄湖中路站、陆慕站、平泷路东站、平河路站、苏州火车站、山塘街站、石路站、广济南路站、三香广场站、劳动路站、胥江路站、桐泾公园站、友联站、盘蠡路站、新家桥站、石湖东路站、宝带桥南站";
        String[] line2s = line2Str.split("、");
        for(String s : line2s){
            line2.add(new Station(s));
        }
        for(int i =0;i<line2.size();i++){
            if(i<line2.size()-1){
                line2.get(i).next = line2.get(i+1);
                line2.get(i+1).prev = line2.get(i);
            }
        }

        //4号线
        String line4Str =
                "龙道浜站、张庄站、姚祥站、活力岛站、孙武纪念园站、平泷路西站、苏锦站、苏州火车站、北寺塔站、察院场站、乐桥站、三元坊站、南门站、人民桥南站、团结桥站、宝带路站、石湖东路站、红庄站、清树湾站、花港站、江陵西路站、江兴西路站、流虹路站、笠泽路站、顾家荡站、苏州湾东站、松陵大道站、吴江人民广场站、吴江汽车站、庞金路站、同里站";
        String[] line4s = line4Str.split("、");
        for(String s : line4s){
            line4.add(new Station(s));
        }
        for(int i =0;i<line4.size();i++){
            if(i<line4.size()-1){
                line4.get(i).next = line4.get(i+1);
                line4.get(i+1).prev = line4.get(i);
            }
        }

        lists.add(line1);
        lists.add(line2);
        lists.add(line4);
        totalStaion  = line1.size() + line2.size() + line4.size();
    }

核心的算法在这里,每次计算完一个最近的站点,然后再分析下一个最近的站点,直到结束。所以说Dijkstra又是使用贪心算法的策略。

    /**
     * 计算从start站到end站的最短经过路径
     * @param start
     * @param end
     */
    public void search(Station start,Station end){
        if(outList.size() == totalStaion){
            printRoute(start,end);
            return;
        }

        if(!outList.contains(start)){
            outList.add(start);
        }

        //如果起点站的OrderSetMap为空,则第一次用起点站的前后站点初始化之
        if(start.getOrderSetMap().isEmpty()){
            List<Station> linkedstations = getAllLinkedStations(start);
            for(Station s : linkedstations){
                start.getAllPassedStations(s).add(s);
            }
        }

        Station shortest = getShortestPath(start);//获取距离起点站start最近的一个站(有多个的话,随意取一个)
        if(shortest == end){
            printRoute(start,end);
            return;
        }

        for(Station station : getAllLinkedStations(shortest)){
            if(outList.contains(station)){ // 已经分析过的station,不再做任何分析
                continue;
            }

            int shortestPath = (start.getAllPassedStations(shortest).size()-1) + 1;//前面这个1表示计算路径需要去除自身站点,后面这个1表示增加了1站距离
            if(start.getAllPassedStations(station).contains(station)){
                //如果start已经计算过到此station的距离,那么比较出最小的距离
                if((start.getAllPassedStations(station).size()-1) > shortestPath){
                    //重置start到周围各站的最小路径
                    start.getAllPassedStations(station).clear();
                    start.getAllPassedStations(station).addAll(start.getAllPassedStations(shortest));
                    start.getAllPassedStations(station).add(station);
                }
            } else {
                //如果start还没有计算过到此station的距离
                start.getAllPassedStations(station).addAll(start.getAllPassedStations(shortest));
                start.getAllPassedStations(station).add(station);
            }
        }
        outList.add(shortest);
        search(start,end);//重复计算,往外面站点扩展
    }

    /**
     * station站到各个站的最短距离,相隔1站,距离为1,依次类推
     * @param station
     * @return
     */
    private Station getShortestPath(Station station){
        int minPatn = Integer.MAX_VALUE;
        Station rets = null;
        for(Station s :station.getOrderSetMap().keySet()){
            if(outList.contains(s)){ // 已经分析过的station,不再做任何分析
                continue;
            }

            LinkedHashSet<Station> set  = station.getAllPassedStations(s);//参数station到s站所经过的所有站点的集合
            if(set.size() < minPatn){
                minPatn = set.size();
                rets = s;
            }
        }
        return rets;
    }

    /**
     * 获取参数station直接相连的所有站,包括交叉线上面的站
     * @param station
     * @return
     */
    private List<Station> getAllLinkedStations(Station station){
        List<Station> result = new ArrayList<Station>();
        for(List<Station> line : lists){
            if(line.contains(station)){
                Station s = line.get(line.indexOf(station));
                if(s.prev != null){
                    result.add(s.prev);
                }
                if(s.next != null){
                    result.add(s.next);
                }
            }
        }
        return result;
    }

    /**
     * 打印实际的线路图
     * @param start
     * @param end
     */
    private void printRoute(Station start,Station end) {
        StringBuilder sb = new StringBuilder();
        sb.append("找到:"+end.getName()+",共经过"+(start.getAllPassedStations(end).size()-1)+"站").append("\r\n");
        for(Station station : start.getAllPassedStations(end)){
            sb.append(station.getName()+"->");
        }

        sb.delete(sb.length()-2,sb.length());
        System.out.println(sb.toString());
    }

我们来看看如何使用这个程序,从南门站到星湖街站应该如何换乘。

subway.search(new Station("南门站"), new Station("星湖街站"));

得到如下的结果:


运行结果.jpeg

总结

Dijkstra只是一种最短路径的算法,除此之外还有Floyd算法、Bellman-Ford算法、SPFA算法等等。而且真实的地铁换乘算法,还要考虑到高峰时地铁人流、换乘时需要走的路(特别是上海的地铁某些站点换乘时需要走很多的路)、每一站所花费的时间、总共花费的时间和金钱...

因此,本文只是模拟了地铁换乘,只是一种实现思路而已。