代码改变世界

个人项目

2019-07-25 16:18  巴拉巴拉嘟嘟  阅读(286)  评论(0编辑  收藏  举报
PSP 2.1Personal Software Process StagesTime
Planning 计划  
· Estimate · 估计这个任务需要多少时间 1h
Development 开发  
· Analysis · 需求分析 (包括学习新技术) 2h
· Design Spec · 生成设计文档 1day
· Design Review · 设计复审 (和同事审核设计文档) -
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) -
· Design · 具体设计 1day
· Coding · 具体编码 5h
· Code Review · 代码复审 -
· Test · 测试(自我测试,修改代码,提交修改)  
Reporting 报告 -
· Test Report · 测试报告 -
· Size Measurement · 计算工作量 -
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 1h
  合计  

1. 地铁网络的数据结构

分析认为,可以使用三个类来代表问题求解所需要的主要数据结构。这三个类包括:

  1. 地铁站节点类 SubwayNode 的一个对象对应着一个地铁站,存储了站名和所属的地铁线名,代表了地铁站与地铁线之间的从属关系。结构伪代码如下:

    // 地铁站节点类
    struct SubwayNode {
     String    name;  // 地铁站的名
     String[*] lines; // 地铁站所处的网络
    }
  2. 地铁路线类 SubwayPath 是一个由 N 个子路线组成的地铁行程路线,其中 N 代表子路线的个数,N == 1 时等效于一个子路线,N == 0 时为无效路线。结构伪代码如下:

    // 地铁路线类
    struct SubwayPath {
     String[N]    lines;    // 每个子路线的所在的地铁线
     String[N][*] stations; // 每个子路线的所途经的地铁线
    }
  3. 地铁站网络类 SubwayNetwork 使用两个字典来存储所有的地铁站和地铁线路,实际代表整个地铁网络结构,结构伪代码如下:

    // 地铁站网络类
    struct SubwayNetwork {
     dict{String : SubwayNode} dictNodes; // { 站名 : 站节点 } 的字典
     dict{String : String[*]}  dictLines; // { 线名 : 沿线站名 } 的字典
    }

与这三个类对应的具体功能方法声明如下:

// 返回站 S 所在的所有线
String[*] = Lines(SubwayNode S) {...}

// 返回线 L 上的所有线
SubwayNode[*] = Stations(String L) {...}

// 生成在线 L 上从 Sa 到 Sb 的路线(子路线),注意:
//   + 若 Sa 或者 Sb 不在 L 上,则返回无效路线
SubwayPath = MakePath(String L, SubwayNode Sa, SubwayNode Sb) {...}

// 判断路线 P 是否为无效路线
Boolean = IsInvalid(SubwayPath P) {...}

// 合并路线 P1, P2, ... 等, 注意:
//   + 若 P1, P2, ... 中任何一个为无效路线,合并结果为无效路线
//   + 若前一路线重点与后一路线起点不一致,合并结果为无效路线
SubwayPath = MergePath(SubwayPath P1, SubwayPath P2, ...) {...}

// 选择 P1, P2, ... 最短的路线,注意:
//   + 无效的路径比任何的路径都要长
SubwayPath = SelectPath(SubwayPath P1, SubwayPath P2, ...) {...}

// 返回站 Sa 和 Sb 所有的共处的线,注意:
//   + 需要基于 SubwayNetwork 存储的结构进行
//   + Sa 和 Sb 共处的线可能有多个
//   + Sa 和 Sb 不在同一条线上则返回空 String[*]
String[*] = SameLines(SubwayNode Sa, SubwayNode Sb) {...}

// 返回线 La 到 Lb 之间的所有换乘站,注意:
//   + 需要基于 SubwayNetwork 存储的结构进行
//   + La 到 Lb 之间的换乘站可能有多个
//   + 无法从 La 换乘到 Lb 则返回空 SubwayNode[*]
SubwayNode[*] = TransStationsBetween(String La, String Lb) {...}

// 返回站 Sa 前后紧邻的中转站,注意:
//   + 需要基于 SubwayNetwork 存储的结构进行
//   + 对于每条线的起始站只有一个
//   + 对于正常非转乘站有两个
//   + 转乘站可能有多个
SubwayNode[*] = NearbyTransStations(SubwayNode Sa) {...}

// 返回站 Sa 到 Sb 的最短路线
//   + 需要基于 SubwayNetwork 存储的结构进行
SubwayPath = BestPath(SubwayPath Sa, SubwayPath Sb) {...}

注意,实际上上述方法中的部分都需要在 SubwayNetwork 所存储网络结构的基础上进行,但为了表述清晰,未在函数参数中添加 SubwayNetwork 对象。可认为相关那些函数是 SubwayNetwork 的类方法。

2. 最短路线的查找算法

2.1. 总体思路

本问题实际为一个无权无向图中的最短路径查找,但是如果使用传统图寻径算法进行查找,则忽略的以下问题特性:

  1. 地铁路线中只有换乘站会和其他站有复杂的连接, 对于非换乘站和其他站的连接简单。故具体地铁路线图节点虽多,但图结构简单,没必要使用针对复杂结构图模型的寻径算法。

  2. 地铁路线分线,考虑线之后图结构将更简单。常规寻径算法均只考虑节点间连接关系,而不考虑线的存在,没有充分利用这一结构;

故可以针对地铁路线图的特殊性自行设计算法求解。分析认为,存在若想从起始站 Sa 到终点站 Sb,存在着三种情况:

  1. SaSb 同在一条线上,可通过该线从 Sa 直达 Sb

  2. SaSb 在不同线上,但存在换乘站 St 使得可通过一次换乘从 SaSb

  3. SaSb 在不同线上,且需要多次换乘才能到达;

头两种情况相对简单,可通过查找路线图直接得到,对应的两类路线被称之为 “简单路线”。而第三种情况相对复杂,需要借助递归进行查找,对应的路线被成为 “复杂路线”。

2.2. 简单路线的直接查找

首先考虑查找从 SaSb 的最短直达路线,基本思路如下:

  1. 查找 SaSb 所共在的线 Ls

  2. 如果 Ls 为空说明没法直达,返回一个无效路径;

  3. 如果 Ls 不为空,遍历所有 Ls 上的直达路线,返回最短的;

其程序伪代码如下:

// 得到从 Sa 到 Sb 的最短直达路线,如果不可行返回 INVALID
SubwayPath = BestOneLinesPath(SubwayNode Sa, SubwayNode Sb)
{
 // 找 Sa 和 Sb 所共在的线(可能为多个)
 String[*] Ls = SameLines(Sa, Sb)

 // 如果 Ls 为空,则说明没法直接从 Sa 到 Sb
 if IsEmpty(Ls) return INVALID
 
 // 从所有直达路线中找最短的
 SubwayPath P = INVALID
 for (L[i] in Ls) {
    SubwayPath Pi = MakePath(L[i], Sa, Sb)
    P = SelectPath(P, Pi)
}
 return P
}

再考虑最短的只换乘一次路线的查找,基本思路如下:

  1. 找从 Sa 所在线到 Sb 所在线所有的换乘方案:

    • 使用 Lines(Sa)Lines(Sb) 获取所在的线;

    • 计从 La 经过 StLb 的换乘方案为 T = (La, St, Lb);

    • 遍历所有  SaSb 所在的线,得到所有的换乘方案 Ts;

  2. 如果 Ts 为空说明没法只换乘一次到达,返回一个无效路径;

  3. 如果 Ts 不为空,遍历所有 Ts 中所有换乘方案:

    • 分别得到从 SaSt 以及从 StSb 的直达路线;

    • 合并两条直达路线为最终路线;

    • 选择最短的换乘路线返回;

其程序伪代码如下:

// 得到从 Sa 到 Sb 最短的只换乘一次的路线,如果不可行返回 INVALID
SubwayPath = BestTwoLinesPath(SubwayNode Sa, SubwayNode Sb)
{
 // 找从 Sa 所在线到 Sb 所在线所有的换乘方案
 // 注意 Lines(Sa) 和 Lines(Sb) 均可能为为多个
 TrainPlain = struct { String La, SubwayNode St, String Lb };
 TrainPlain[*] Ts = TransStationsBetween(Lines(Sa), Lines(Sb))

 // 如果 Ts 为空,则说明没法通过换乘一次到达
 if IsEmpty(Ts) return INVALID

 // 遍历所有只换乘一次的路线,选最短的返回
 SubwayPath P = INVALID
 for ({La, St, Lb} in Ts) {
   // 从 Sa 到 St 的直达路线
   SubwayPath Pat = MakePath(La, Sa, St)
   // 从 St 到 Sb 的直达路线
   SubwayPath Ptb = MakePath(Lb, St, Sb)
   // 合并两条路线为最终路线
   SubwayPath Pi  = MergePath(Pat, Ptb)
   // 选 P 和 Pi 中最短的一个
   P = SelectPath(P, Pi)
}
 return P
}

将两者结合即得到寻找最短的 “简单路线”,对应伪代码如下:

// 得到从 Sa 到 Sb 最短的 “简单路线”,如果不可行返回 INVALID
SubwayPath = BestPlainPath(SubwayNode Sa, SubwayNode Sb) {
 return SelectPath(
   BestOneLinePath(Sa, Sb),
   BestTwoLinesPath(Sa, Sb)
)
}

2.3. 复杂路线的递归查找

当没法直接找到从 SaSb 的简单路线,则需要考虑通过递归查找多次换乘的复杂路线。基本的思路是查找起点站 Sa 和终点站 Sb 相邻的各换乘站,递归查找从换乘站到 SaSb 的路线。共有三种递归查找方案:

  1. Sa 开始,查找从其相邻的换乘站 StSb 的路线,递归进行直至找到。对应伪代码如下:

    // 从起点站开始递归查找 Sa 到 Sb 的最佳路线
    SubwayPath = BestRecursivePath1(SubwayNode Sa, SubwayNode Sb)
    {
     // 查找简单路线,如果找到了就返回,终止迭代
     SubwayPath P = BestPlainPath(Sa, Sb)
     if (! IsInvalid(P)) return P

     // 查找 Sa 相邻的换乘站
     SubwayNode[*] Ts = NearbyTransStations(Sa)
     
     // 挨个换乘站找看能够找到最佳路线
     for (St in Ts) {
       // 从 Sa 到 St 的直达路线
       SubwayPath Pat = BestOneLinePath(Sa, St)
       // 递归查找从 St 到 Sb 的路线
       SubwayPath Ptb = BestRecursivePath1(St, Sb)
       // 合并两条路线为最终路线,选 P 和 Pi 中最短的一个
       SubwayPath Pi  = MergePath(Pat, Ptb)
       P = SelectPath(P, Pi)
    }
     return P
    }
  2. Sb 开始,查找从 Sa 到其相邻的换乘站 St 的路线,递归进行直至找到。对应伪代码如下:

    // 从终点站开始递归查找 Sa 到 Sb 的最佳路线
    SubwayPath = BestRecursivePath2(SubwayNode Sa, SubwayNode Sb)
    {
     // 查找简单路线,如果找到了就返回,终止迭代
     Subway P = BestPlainPath(Sa, Sb)
     if (! IsInvalid(P)) return P

     // 查找 Sb 相邻的换乘站
     SubwayNode[*] Ts = NearbyTransStations(Sb)
     
     // 挨个换乘站找看能够找到最佳路线
     for (St in Ts) {
       // 递归查找从 Sa 到 St 的路线
       SubwayPath Pat = BestRecursivePath2(Sa, St)
       // 从 St 到 Sb 的直达路线
       SubwayPath Ptb = BestOneLinePath(St, Sb)
       // 合并两条路线为最终路线,选 P 和 Pi 中最短的一个
       SubwayPath Pi = MergePath(Pat, Ptb)
       P = SelectPath(P, Pi)
    }
     return P
    }
  3. SaSb 两端开始,查找从 StAStB 的路线,递归进行直至找到。对应伪代码如下:

    // 从起点站和终点站开始,双向递归查找 Sa 到 Sb 的最佳路线
    SubwayPath = BestRecursivePath3(SubwayNode Sa, SubwayNode Sb)
    {
     // 查找简单路线,如果找到了就返回,终止迭代
     SubwayPath P = BestPlainPath(Sa, Sb)
     if (! IsInvalid(P)) return P

     // 查找 Sa 和 Sb 相邻的换乘站
     SubwayNode[*] TsA = NearbyTransStations(Sa)
     SubwayNode[*] TsB = NearbyTransStations(Sb)

     // 挨个换乘站找看能够找到最佳路线
     for (StA in TsA) for (StB in TsB) {
       // 从 Sa 到 StA 的直达路线
       SubwayPath Pat = BestOneLinePath(Sa, StA)
       // 递归查找从 StA 到 StB 的路线
       SubwayPath Ptt = BestRecursivePath3(StA, StB)
       // 从 StB 到 StA 的直达路线
       SubwayPath Ptb = BestOneLinePath(St, Sb)
       // 合并三条路线为最终路线,选 P 和 Pi 中最短的一个
       SubwayPath Pi = MergePath(Pat, Ptt, Ptb)
       P = SelectPath(P, Pi)
    }
     return P
    }

2.4. 最终路线的查找

最终路线的查找方法即分别尝试三种递归 “复杂路线” 查找(递归查找中同样涵盖了 “简单路线” 的查找),从所找出的三条路线中找到最短的那个作为最终路径。对应伪代码如下:

// 最终最佳路线的查找
SubwayPath = BestPath(SubwayNode Sa, SubwayNode Sb) {
 return SelectPath(
   BestRecursivePath1(Sa, Sb)
   BestRecursivePath2(Sa, Sb)
   BestRecursivePath3(Sa, Sb)
)
}