引言
每逢假日人流高峰,12306便会成为一个热门的话题,2016的春节也毫无例外。大年初二,我和汤雪华(NetFocus)一干人等,在QQ群里围绕12306的购票问题展开了热烈的讨论。最后,由于购票问题远比想象中复杂,所以最终还是公说公有理、婆说婆有理,没有得到一个较为清晰和明确的结果。于是决定动动笔,谈一谈自己的理解,重点是解决面对购票请求时能不能出票的问题。如果我的方法有漏洞,请一定指出,谢谢!
需要说明的是,我的离散数学和组合数学的知识,都早早地还给了我的体育老师,所以下面的这个算法可能会粗糙得让各位无法直视,只希望能完整地表达出我的想法。
出票规则臆测
由于已久未坐过火车,也从未在12306上买过票,所以我根据相关文章假想出以下的出票规则:
假设一列火车K110,从A始发,途经BCD三个站点,最终驶往终点E。那么旅客可以购买的将包括{AB, AC, AD, AE, BC, BD, BE, CD, CE, DE}这10种『区段』的客票。
假设列车承载限额为500人。如果有500名旅客购买了AE全程客票,之后无论哪个区间的客票,都不能再多卖一张了。如果有499名旅客购买了AE全程客票,那么还有的可能,一是再有人买到一张AE全程客票,使列车从始发到终点都满载;二是AE包含的AB/AC/AD/BC/BD/BE/CD/CE/DE每个区段都最多只能售出1张客票,此时就象先下后上原则,区段间彼此不能有交叉,前一个区段有人下车,后面的区段才能再有人上车。比如我们还可以同时出票1张AB,1张BC、1张CD和1张DE的组合,或者1张AC、1张CE的组合。反之,此时若同时有AC、BD、CD各出票1张,则列车到B站点时仍是满载,持BD者将无法上车,而到C站点时持AC者下车,故持CD者可上车。
正因如此,每当我们售出一张某个区段的客票,就会引发其他区段可售票数量产生相应变化。所以,即使是同一编号的列车,在不同日期出行班次内售出的客票总数,都会因为旅客购票区段、购票数量的不同而完全不一样。
在仔细观察后我发现,在列车从始发到终点的整个过程中,唯一不变的是列车最多能容纳的人数——『承载限额』。
承载限额
在此赘述『承载限额』的概念,是为了理清座位与车票之间的关系。
以我数年前坐火车的经历,如果列车还有空余座位,那么我们在购票后将得到一张打印了车厢号、座位号的车票。如果座位已经售罄,那么只要列车承载限额还在其负荷内,那么我们仍能购得一张车票,并凭此进站上车,尽管这张票只载明了车次,而无具体的车厢号与座位号。这便是『有座票』与『无座票』的由来。
由此衍生出的,售出的座号是否由系统回收后再行出售给后续区段旅客,成为干扰我们讨论的问题之一。而我当时假设系统不回收座号。即假设甲买到A到B的有座票,并且是最后一个座位:15车厢102号座。而乙随后买到B到D的票,系统并不因此将这个座位15-102再次出售给乙。而是当甲在B下车后,由车厢里没有座位的其他旅客自行占据使用。
经过讨论,我认识到纠结于是否回收实际的座号是个大大的错误。如果将无座号也视为一种虚拟的座位,那么对列车而言只需要区分满载与未满载两种情况:满载时不能再售票,未满载时可售后续区段的票。而整个列车所能承载的人数,或者说是负荷,都将始终是个常量。
在这样的设定下,能否出票的问题就变成了判断列车在特定的区段是否会超载的问题了。至于『有座票』与『无座票』,就自然退化为次要问题了。
至于高铁和动车,我还没有坐过,所以只能揣测由于其车载设备的同步能力远比现有火车强大,因此借由列车员或其他机制实现空闲座号回收与更新,应该不是难题。
总体思路
在我当初大学的《数据结构》课程里,曾经有一个关于银行排队的算法模拟,我的灵感便正得于此。
第一步
明确整个算法的前提:『任意时刻列车均不得超载——列车承载的旅客人数不得超过其预定的承载限额。』进而,我推导出购票算法的准则:『每出售一张票,都必须保证列车不会因此在实际运行时超载。』
第二步
采取TDD的方法去模拟列车的运行。通过遍历列车经过的每个站点,按照先下后下的原则,计算每个站点上下旅客的数量以及列车的承载人数。当承载人数超过承载限额时,即说明多卖了一些不该卖的票,导致列车超载。这样,我们就可以设定不同区段已售出客票数量的不同组合,再提交给这个模拟的列车运行测试程序,以检验该售票结果是否正确。
第三步
设计出购票算法,模拟客户的购票请求,得到不同的售票结果,再提交给上面的测试程序检验,确保购票算法能产生符合要求的、正确的售票结果。
第四步
对已经正确的算法进行优化,通过重构提高算法的运算效率、降低并发竞争的风险。
第五步
围绕座号分配与回收、解决退票等问题,丰富设计功能,进一步满足业务需求。
综上所述,我的思路实际是模仿迷宫问题的递归,大致可以表述如下:
当一条线路收到一个购票请求时,它会用该请求包含的拟购票数去试探,通过遍历所有站点的已出售票数,模拟列车运行时的旅客上下车情况,检查期间是否会发生超载的情况。如果不会因此产生超载,则说明可以同意该购票请求。否则无论是在哪个站点发生超载,都必须拒绝这个购票请求。
明确了算法准则,下面我就从第二步开始,逐步实现上述预定思路。
注:因为Scala的简洁高效,所以我选择了它。另一方面也是因为我最近正陶醉其中,日后也希望有时间用Scala来模拟12306的客票并发。不过我自知此处的代码粗陋不堪,甚至站点的到达时间和发车时间我都用
Float模拟了,因为我还不知道Scala里用什么表示C#里的Timespan。