在求图线任意两点间最短路径时,利用floyd、dijdstra等成熟的算法可以求得,效率还不错。但要求换乘最少、最舒适等路径时,需要求线网图中任意两个点的所有路径,然后根据条件筛选,以上算法无能为力。本人最近做个小项目需要用到这个需求,因此在网上搜索相关资料,找到一个利用栈采用深度优先搜索的算法,利用此算法在下图11条线路190余个站中测试,任意两点间所有路径平均耗时15秒,不能满足需求。
于是,自己琢磨着写了一个算法,现将其记录如下:
第一步:将每条线路视为一个结点,生成所有乘坐线路顺序列表。如上图中楚河汉街——中山公园的路径中,首先采用广度优先搜索生成[[4,2],[4,8,1,2],[4,3,2],......],这样的路径,在生成中剔除明显不合理的路径,如[4,2,7,1,2]这样的路径。
第二步:求第一步生成的线路顺序中相邻两条线的交点(即换乘站),生成以换乘站作为节点的顺序路径。如第一步中[4,3,2]这条线路顺序,4和3的交点为王家湾,3和2的交点为范湖和宏图大道,因此可生成[楚河汉街,王家湾,范湖,中山公园]以及[楚河汉街,王家湾,宏图大道,中山公园]这两起点、终点以及换乘站构成的路径。
第三步:将第二步中每条路径相邻两个站点之间其他站点补齐,即是从起点至终点的完整路径。
因为在第一步剔除了许多不合理路径,因此最后生成所有路径中比以站点深度优先搜索得出来的所有路径少得多,前者为1000余条,后者为15万条。不过没关系,剔除的路径对我们来说没有任何意义。
算法中适用存在环线的线网图,将Line对象中的isCircle设置为true即可。
由于篇幅原因,以下只贴算法中的关键代码,完整的项目以点击下载图中任意两点间所有路径高效算法
首先介绍下算法中存储路径的数据结构:树
Tree:
public class SolutionTree {
private TreeNode root;//权的根节点
public SolutionTree(int Id)
{
root = new TreeNode(Id);
}
public TreeNode getRoot() {
return root;
}
public void setRoot(TreeNode root) {
this.root = root;
}
}
TreeNode:
public class TreeNode implements Serializable {
/**
*
*/
private static final long serialVersionUID = 656317088559367582L;
private int Id; //权节点id
protected TreeNode parentNode;//父节点
protected List<TreeNode> childList;//子节点列表
public TreeNode(int Id) {
this.Id=Id;
initChildList();
}
public TreeNode(int Id,TreeNode parentNode) {
this.Id=Id;
this.parentNode=parentNode;
initChildList();
}
//此处省略get、set以及一些内部方法
}
然后是存储站点信息以及线路信息的Class:
Station.class:
public class Station {
private int id;//车站id
private String name;//车站名
private Station nextSta;//下个站
private Station prevSta;//上个站
private int line;//车站所在的线路
private List<Integer> transferLines = new ArrayList<Integer>();//当前站可换乘线路列表,不是换乘,列表中只有一个0元素
//此外省略了get、set以及一些内部方法
}
Line.class:
public class Line {
private int id; //线路id
private List<Station> stationList = new ArrayList<Station>();//本条线的车站列表
private boolean isCircle ;//是否为环线
//此外省略了get、set以及一些内部方法
}
最后就是算法了,算法中需要三个初始数据,graph:站点列表,将每个站点对象中的所有属性按实际情况赋值;matrix:费用矩阵,可连通,设置为连通的费用值,不可连通,设置为无穷大,算法中统一相邻站点费用为2,换乘站连接费用为5,实际使用中可读取费用数据后赋值;lineTable:线路列表,线路id作为key值。以上三个初始数据在实际使用中可通过第二个构造函数传入赋值。代码如下:
public class PathSearch {
private List<Station> graph; //车站列表,
private static final int INF=Integer.MAX_VALUE;
private int[][] matrix;//费用矩阵
private Map<Integer,Line> lineTable = new HashMap<Integer,Line>();//线路表
/**
* 从数据库或文件读取站点数据,费用表等数据,初始化数据
*/
public PathSearch()
{
try
{
//要求根据库中的车站按所在线中的顺序依次存储
String sql = "Select * From tab_station order by id";
List<Map<String,Object>> result = MyDatabase.search(sql);
graph = new ArrayList<Station>();
List<Station> temp = new ArrayList<Station>();
int line = Integer.parseInt(result.get(0).get("line").toString());
for(int i=0;i<result.size();i++)
{
Map<String,Object> data = result.get(i);
Station sta = new Station();
sta.setId(i);
sta.setLine(Integer.parseInt(data.get("line").toString()));
sta.setName(data.get("name").toString());
if(!(line == sta.getLine()) || i==result.size()-1)
{
Line tl = new Line();
tl.setId(line);
tl.setStationList(temp);
//若线路为环线,在此设置 tl.setCircle(true);
lineTable.put(line, tl);
line = sta.getLine();
if(i==result.size()-1)
{
temp.add(sta);
}
temp = new ArrayList<Station>();
}
temp.add(sta);
List<String> list = Arrays.asList(data.get("transer").toString().replace(" ", "").split(","));
for(String tl : list)
{
sta.addTransferLine(Integer.parseInt(tl));
}
graph.add(sta);
}
//将前后车站相连,graph中的元素也随着改变
for(int key : lineTable.keySet())
{
Line tl = lineTable.get(key);
int lineLength = tl.getStationList().size();
if(tl.isCircle())//如果是环线,首尾相连
{
tl.getStationList().get(0).setPrevSta(tl.getStationList().get(lineLength-1));
tl.getStationList().get(lineLength-1).setNextSta(tl.getStationList().get(0));
}
tl.getStationList().get(0).setNextSta(tl.getStationList().get(1));
tl.getStationList().get(lineLength-1).setPrevSta(tl.getStationList().get(lineLength-2));
for(int i=1;i<lineLength-1;i++)
{
tl.getStationList().get(i).setPrevSta(tl.getStationList().get(i-1));
tl.getStationList().get(i).setNextSta(tl.getStationList().get(i+1));
}
}
matrix = new int[graph.size()][graph.size()];
//权重数组
for(int i = 0; i<graph.size();i++)
{
Station stai = graph.get(i);
for(int j = 0;j<graph.size();j++)
{
Station staj = graph.get(j);
if(i==j)
{
matrix[i][j] = 0;
}
//票价计算,换乘站间设置权重为0,若为时间权限,可在此设置换乘权重
else if(stai.getName().equals(staj.getName()))
{
matrix[i][j] = 5;
}
else
{
matrix[i][j] = INF;
}
//相邻车站权重统一设置为2
if(stai.getNextSta()!=null)
{
if(stai.getNextSta().getId() == staj.getId())
{
matrix[i][j] = 2;
}
}
if(stai.getPrevSta()!=null)
{
if(stai.getPrevSta().getId() == staj.getId())
{
matrix[i][j] = 2;
}
}
if(staj.getNextSta()!=null)
{
if(staj.getNextSta().getId() == stai.getId())
{
matrix[i][j] = 2;
}
}
if(staj.getPrevSta()!=null)
{
if(staj.getPrevSta().getId() == stai.getId())
{
matrix[i][j] = 2;
}
}
}
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
/**
* 从外部将站点列表,费用矩阵及线路表传入,初始化数据
*/
public PathSearch(List<Station> graph,int[][] matrix,Map<Integer,Line> lineTable)
{
this.graph = graph;
this.matrix = matrix;
this.lineTable = lineTable;
}
//记录每对起始id第一次调用getAllPath方法得到数据,若相同的起始id再次再次调用时,可直接调用XXNoMakeAllPath()方法
private List<List<Integer>> allPath;
/**
* 生成起点到终点的所有路径,该方法返回的所有路径比实际的所有路径要少得多,因为在方法中去除了一些明显不合理的路径,
* 例如2号线转3号线,再从3号线转2号线的路径明显不合理
* @param startStaId 起点id
* @param endStaId 终点id
* @return 径路列表
*/
private List<List<Integer>> getAllPath(int startStaId, int endStaId)
{
Station startSta = graph.get(startStaId);
Station endSta = graph.get(endStaId);
//以起始线路id作为根节点初始化一棵树
SolutionTree tree = new SolutionTree(startSta.getLine());
makeTreeNode(tree.getRoot(),endSta.getLine());
//获取树的所有叶子节点
List<TreeNode> ziYeList = new ArrayList<TreeNode>();
tree.getRoot().getZiYe(ziYeList);
Iterator<TreeNode> tempList = ziYeList.iterator();
//删除id不是终点站的叶子结点
while(tempList.hasNext())
{
TreeNode node = tempList.next();
if(node.getId()!=endSta.getLine())
{
tempList.remove();
}
}
//每个叶子节点的父辈节点反转后再加上节点本身,即为从起点到终点经过线路顺序
List<List<Integer>> tempResult = new ArrayList<List<Integer>>();
for(TreeNode node : ziYeList)
{
List<Integer> list = node.getEldersID();
Collections.reverse(list);
list.add(node.getId());
tempResult.add(list);
}
//通过每条线路顺序,获取换乘站的顺序路径
List<List<Integer>> staTempResult = new ArrayList<List<Integer>>();
for(List<Integer> temp : tempResult)
{
//以起点站id作根要点id初始化树
SolutionTree staTree = new SolutionTree(startStaId);
for(int i = 0; i < temp.size()-1; i++)
{
List<TreeNode> fatherList = new ArrayList<TreeNode>();
staTree.getRoot().getZiYe(fatherList);
Line line = lineTable.get(temp.get(i));
Line nextLine = lineTable.get(temp.get(i+1));
//两条线的交点即为换乘站,
List<Integer> jiaoDianList = getJiaoDianZhan(line,nextLine.getId());
//为每个叶子节点增加子节点
for(TreeNode father : fatherList)
{
for(int staId : jiaoDianList)
{
//如果交点站为起始车站,则将换乘站所有下条线路的id作为一个节点添加到父节点中
if(staId == startStaId)
{
Station nextSta = nextLine.getStation(line.getStation(staId).getName());
TreeNode node2 = new TreeNode(nextSta.getId(), father);
father.addChildNode(node2);
}
//否则,则换乘站所有当前线路的id作为一个节点添加到父节点中,同时将换乘站所有下条线路的id作为一个节点添加到刚添加的节点中
else
{
TreeNode node1 = new TreeNode(staId, father);
father.addChildNode(node1);
Station nextSta = nextLine.getStation(line.getStation(staId).getName());
TreeNode node2 = new TreeNode(nextSta.getId(), node1);
node1.addChildNode(node2);
}
}
}
}
List<TreeNode> fatherList = new ArrayList<TreeNode>();
staTree.getRoot().getZiYe(fatherList);
//将所有叶子节点的父辈节点反转并添加该叶子节点id即为第i条线路顺序所经过的换乘站顺序id
for(TreeNode node : fatherList)
{
List<Integer> list = node.getEldersID();
Collections.reverse(list);
list.add(node.getId());
if(!list.contains(endStaId))
{
list.add(endStaId);
}
staTempResult.add(list);
//System.out.println(list);
}
}
List<List<Integer>> result = new ArrayList<List<Integer>>();
for(List<Integer> temp : staTempResult)
{
List<Integer> path = new ArrayList<Integer>();
for(int i=0;i<temp.size()-1;i++)
{
Station sta = graph.get(temp.get(i));
Station nextSta = graph.get(temp.get(i+1));
//两站之间在同一条线,将两站之间的站点补全
if(sta.getLine()==nextSta.getLine())
{
List<List<Integer>> tempPath = lineTable.get(sta.getLine()).getPathInLine(sta.getId(), nextSta.getId());
if(tempPath.size()==1)
{
path.addAll(tempPath.get(0));
}
else
{
int minIndex = -1;
int minDistance = Integer.MAX_VALUE;
for(int index=0;index<tempPath.size();index++)
{
int distance = getDistance(tempPath.get(index));
if(minDistance > distance)
{
minIndex = index;
minDistance = distance;
}
}
if(minIndex!=-1)
{
path.addAll(tempPath.get(minIndex));
}
}
}
else if(i==temp.size()-2)
{
path.add(nextSta.getId());
}
}
result.add(path);
}
allPath = result;
return result;
}
/**
* 获取两条线路的交点
* @param line 当前线路id
* @param nextLineId 下条线路id
* @return 返回两条线路交点集合列表
*/
private List<Integer> getJiaoDianZhan(Line line, int nextLineId) {
// TODO Auto-generated method stub
List<Integer> result = new ArrayList<Integer>();
//为同一条,返回空列表
if(line.getId() == nextLineId)return result;
for(Station sta : line.getStationList())
{
//当前线路的车站的换乘列表中包含下条线路,表明有交集
if(sta.getTransferLines().contains(nextLineId))
{
result.add(sta.getId());
}
}
return result;
}
/**
* 为当前树节点添加子节点
* @param father 当前节点
* @param endLine 终点所在的线路id,用于判断递归结束条件
*/
private void makeTreeNode(TreeNode father,int endLine) {
// TODO Auto-generated method stub
List<Integer> transferList = getXiangGuanXianLu(father,endLine);
for(int line : transferList)
{
TreeNode node = new TreeNode(line,father);
father.addChildNode(node);
//递归直至当前线路等于终点站所在的线路
if(line!=endLine)
{
makeTreeNode(node, endLine);
}
}
}
/**
* 获取与当前线路有交集的线路id
* @param father 当前线路的树节点
* @param endLine 终点所在的线路id
* @return 返回与当前线路有交集的线路id列表
*/
private List<Integer> getXiangGuanXianLu(TreeNode father,int endLine) {
// TODO Auto-generated method stub
List<Integer> result = new ArrayList<Integer>();
Line line = lineTable.get(father.getId());
//获取当前节点的父辈节点id列表
List<Integer> temp = father.getEldersID();
for(Station sta : line.getStationList())
{
for(int tl : sta.getTransferLines())
{
//如果是换乘站,列表中不存在当前相关的线路id,
//除当前相关的线路id为终点点所在的线路id外,当前节点的父辈节点中不能包含此id,同时此id不能与当前节点的id相同
//满足以上条件,才能作为当前节点的子节点
if(tl!=0 && !result.contains(tl) && (tl == endLine ||
(!temp.contains(tl) && tl != father.getId())))
{
result.add(tl);
}
}
}
return result;
}
/**
* 根据费用矩阵,获取路径的费用总和
* @param path 路径列表
* @return 返回路径的费用总和
*/
private int getDistance(List<Integer> path)
{
int sum = 0;
for(int i=0;i<path.size()-1;i++)
{
sum += matrix[path.get(i)][path.get(i+1)];
}
return sum;
}
/**
* 获取费用最小的路径
* @param allPath 所有路径列表
* @return 返回费用最小的路径
*/
private List<List<Integer>> makeShortPath(List<List<Integer>> allPath)
{
if(allPath.size()==0)return allPath;
List<Integer> distance = new ArrayList<Integer>();
for(List<Integer> path : allPath)
{
distance.add(getDistance(path));
}
float minDistance = Collections.min(distance);
List<List<Integer>> result = new ArrayList<List<Integer>>();
for(int i=0;i<allPath.size();i++)
{
if(distance.get(i) == minDistance)
{
result.add(allPath.get(i));
}
}
return result;
}
/**
* 获取换乘最少的路径
* @param allPath 所有路径列表
* @return 返回换乘最少的路径
*/
private List<List<Integer>> makeTransferLessPath(List<List<Integer>> allPath)
{
if(allPath.size()==0)return allPath;
List<Integer> transferNumList = new ArrayList<Integer>();
for(List<Integer> path : allPath)
{
int sum = 0;
for(int i=0;i<path.size()-1;i++)
{
//路径中第i个站与第i+1个站不是同一条线,表明换乘,终点站为换乘站时,不计换乘个数
if(graph.get(path.get(i)).getLine() != graph.get(path.get(i+1)).getLine() && i!=path.size()-2)
{
sum ++;
}
}
transferNumList.add(sum);
}
float minTransferNum = Collections.min(transferNumList);
List<List<Integer>> result = new ArrayList<List<Integer>>();
for(int i=0;i<allPath.size();i++)
{
if(transferNumList.get(i) == minTransferNum)
{
result.add(allPath.get(i));
}
}
return result;
}
/**
* 获取费用最小的路径,并将id转化成站名
* @param startStaId 起始id
* @param endStaId 终点id
* @return 返回费用最小的字符串路径
*/
public List<List<String>> getShortPath(int startStaId, int endStaId) {
List<List<Integer>> allPath = getAllPath(startStaId, endStaId);
List<List<String>> result = new ArrayList<List<String>>();
//获取路径最短的路径
List<List<Integer>> temp = makeShortPath(allPath);
//如果有两条及以上的最短路径,获取换乘最少的路径
if(temp.size()>1)
{
temp = makeTransferLessPath(temp);
}
for(int i=0;i<temp.size();i++)
{
result.add(transformPath(temp.get(i)));
}
return result;
}
/**
* 获取费用最小的路径,并将id转化成站名,不需要生成AllPath
* @param startStaId 起始id
* @param endStaId 终点id
* @return 返回费用最小的字符串路径
*/
public List<List<String>> getShortPathNoMakeAllPath(int startStaId, int endStaId) {
List<List<String>> result = new ArrayList<List<String>>();
if(allPath==null)return result;
//获取路径最短的路径
List<List<Integer>> temp = makeShortPath(allPath);
//如果有两条及以上的最短路径,获取换乘最少的路径
if(temp.size()>1)
{
temp = makeTransferLessPath(temp);
}
for(int i=0;i<temp.size();i++)
{
result.add(transformPath(temp.get(i)));
}
return result;
}
/**
* 获取换乘最少的路径,并将id转化成站名,不需要生成AllPath
* @param startStaId 起始id
* @param endStaId 终点id
* @return 返回换乘最少的字符串路径
*/
public List<List<String>> getTransferLessPathNoMakeAllPath(int startStaId, int endStaId) {
// TODO Auto-generated method stub
//List<List<Integer>> allPath = getAllPath(startStaId, endStaId);
List<List<String>> result = new ArrayList<List<String>>();
if(allPath==null)return result;
//获取换乘个数最少的路径
List<List<Integer>> temp = makeTransferLessPath(allPath);
//如果有多条换乘个数一样的路径,获取其中路径最短的
if(temp.size()>1)
{
temp = makeShortPath(temp);
}
for(int i=0;i<temp.size();i++)
{
result.add(transformPath(temp.get(i)));
}
return result;
}
/**
* 获取换乘最少的路径,并将id转化成站名
* @param startStaId 起始id
* @param endStaId 终点id
* @return 返回换乘最少的字符串路径
*/
public List<List<String>> getTransferLessPath(int startStaId, int endStaId) {
// TODO Auto-generated method stub
List<List<Integer>> allPath = getAllPath(startStaId, endStaId);
List<List<String>> result = new ArrayList<List<String>>();
//获取换乘个数最少的路径
List<List<Integer>> temp = makeTransferLessPath(allPath);
//如果有多条换乘个数一样的路径,获取其中路径最短的
if(temp.size()>1)
{
temp = makeShortPath(temp);
}
for(int i=0;i<temp.size();i++)
{
result.add(transformPath(temp.get(i)));
}
return result;
}
/**
* 将id路径转化成站名的路径
* @param path
* @return
*/
private List<String> transformPath(List<Integer> path)
{
List<String> result = new ArrayList<String>();
//获取起点站线路
String startLine = getStartLine(path);
for(int i=0;i<path.size()-1;i++)
{
Station sta1 = graph.get(path.get(i));
Station sta2 = graph.get(path.get(i+1));
Station temp = graph.get(path.get(path.size()-1));
//换乘站且不是终点站
if(sta1.getName().equals(sta2.getName()) && sta2.getId() != temp.getId())
{
//获取换乘方向
String direction = getTransferDirection(sta2,path.get(i+2));
result.add(sta1.getName()+"(换乘"+sta2.getLine()+"号线,"+direction+")");
i++;
}
//换乘站,且换乘站为终点站
else if(sta1.getName().equals(sta2.getName()))
{
//result.add(sta1.getName());
}
else
{
result.add(sta1.getName());
}
}
result.add(graph.get(path.get(path.size()-1)).getName());
if(path.size()>1)
{
String direction = getTransferDirection(graph.get(path.get(0)),path.get(1));
result.set(0, result.get(0)+"("+startLine+","+direction+")");
}
else
{
result.set(0, result.get(0)+"("+startLine+")");
}
return result;
}
/**
* 获取起点站需要乘坐的路径,起点站为换乘,返回的是换乘站所有的下条线路
* @param path 路径
* @return 返回起点站需要乘坐的路径
*/
private String getStartLine(List<Integer> path) {
// TODO Auto-generated method stub
int n = 0;
if(path.size()==1)
{
return "(已在目的地)";
}
Station sta1 = graph.get(path.get(n));
Station sta2 = graph.get(path.get(n+1));
while(sta1.getName().equals(sta2.getName()))
{
n++;
sta1 = graph.get(path.get(n));
sta2 = graph.get(path.get(n+1));
}
return "乘坐"+sta1.getLine()+"号线";
}
/**
* 获取换乘方向
* @param sta 换乘站
* @param nextId 换乘站的相邻车站id
* @return 换乘方向
*/
private String getTransferDirection(Station sta,Integer nextId) {
// TODO Auto-generated method stub
Line line = lineTable.get(sta.getLine());
if(sta.getPrevSta()!=null && sta.getPrevSta().getId() == nextId)
{
return "往" + line.getStationList().get(0).getName() + "方向";
}
else if(sta.getNextSta()!=null && sta.getNextSta().getId() == nextId)
{
return "往" + line.getStationList().get(line.getStationList().size()-1).getName() + "方向";
}
else
{
return "";
}
}
public List<Station> getGraph() {
return graph;
}
}
测试类:
public static void main(String[] args) {
// TODO 自动生成的方法存根
try
{
long startTime = new Date().getTime();
System.out.println("运行中....");
PathSearch ps = new PathSearch();
int start = 1;
int end = 28;
System.out.println("最短路:"+ps.getShortPath(start,end));
System.out.println("换乘最少:"+ps.getTransferLessPathNoMakeAllPath(start,end));
long endTime = new Date().getTime();
System.out.print("耗时:"+(endTime-startTime)/1000.0+"秒");
}
catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
System.exit(0);
}
}
版权声明:本文作者原创,未经允许不得他用,转载请注明出处。