12306出票算法随想

引言

每逢假日人流高峰,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下车后,由车厢里没有座位的其他旅客自行占据使用。

经过讨论,我认识到纠结于是否回收实际的座号是个大大的错误。如果将无座号也视为一种虚拟的座位,那么对列车而言只需要区分满载与未满载两种情况:满载时不能再售票,未满载时可售后续区段的票。而整个列车所能承载的人数,或者说是负荷,都将始终是个常量。

在这样的设定下,能否出票的问题就变成了判断列车在特定的区段是否会超载的问题了。至于『有座票』与『无座票』,就自然退化为次要问题了。

至于高铁和动车,我还没有坐过,所以只能揣测由于其车载设备的同步能力远比现有火车强大,因此借由列车员或其他机制实现空闲座号回收与更新,应该不是难题。

总体思路

在我当初大学的《数据结构》课程里,曾经有一个关于银行排队的算法模拟,我的灵感便正得于此。

我的方法建立在这样的一个前提上:『任意时刻列车均不得超载——列车承载的旅客人数不得超过其预定的承载人数。』

以此为准则,我将模拟列车的运行,让列车从始发站出发,在每个站点上下旅客,最后驶到终点结束。期间,我会跟踪列车在每个站点停靠时的载客人数,如果发现有超载的现象,则说明出票发生了问题,卖了不该卖的票。由此,我可以推断出这样一个结论:『如果要出一张票,就必须保证列车不会因此超载。』

模拟列车运行

因为Scala的简洁高效,所以我选择了它。另一方面也是因为我最近正陶醉其中,日后也希望有时间用Scala来模拟12306的客票并发。不过我自知此处的代码粗陋不堪,甚至站点的到达时间和发车时间我都用Float模拟了,因为我还不知道Scala里用什么表示C#里的Timespan,哈哈。

第一步:定义站点Station,区间Interval,线路Line

case class Station(val name: String, val arrival: Float, val depart: Float) {
  require(arrival <= depart)

  def prior(that: Station): Boolean = this.depart < that.arrival
  def after(that: Station): Boolean = this.arrival > that.depart
}

case class Interval(val begin: Station, val end: Station) {
  require(begin prior end)
}

case class Line(val stations: List[Station]) {
  require(stations.length >= 2)
}

第二步:定义列车Train

列车有2个很简单的方法:上客board与下客land。它们都会在超载时抛出异常。

class Train(val capacity: Int) {
  var load: Int = 0;

  def land(amount: Int): Unit = {
    load -= amount
    if (load < 0)
      throw new IllegalArgumentException("Train loading should not be negative.")
  }

  def board(amount: Int): Unit = {
    load += amount
    if (load > capacity)
      throw new IllegalArgumentException("Train is overloading.")
  }
}

第三步:定义订票中心Booking

订票中心是负责出票的地方。它先根据线路的站点列表生成一个包含所有区段的列表intervals,然后将每个区段的已售票sold全部初始为0。然后定义了一个售票方法sell(interval: Interval, amount: Int),它会在区段interval卖出amount张票后,更新该区段已售票的数量。

class Booking(val line: Line, val train: Train) {
  val intervals: IndexedSeq[Interval] = for {
    begin <- line.stations.indices dropRight 1
    end <- line.stations.indices drop begin + 1
  } yield Interval(line.stations(begin), line.stations(end))

  val sold: mutable.Map[Interval, Int] = intervals.map(i => i -> 0)(collection.breakOut)

  override def toString(): String = {
    (for (i <- intervals) yield s"${i.begin.name}${i.end.name}[${sold(i)}]").mkString("  ")
  }

  def sell(interval: Interval, amount: Int) = {
    sold(interval) += amount
  }
}

第四步:定义模拟的驱动器Watcher

Watcher的核心方法是runTrain(),它让列车从始发站点出发,驶往终点。期间到达每个站点时,会模拟『先下后上』的原则,更新列车承载的旅客数量。而每个站点的上下人数,则是根据售出的客票得出:下车的是所有以该站点为终点的客票总和,上车的是所有以该站点为起点的客票总和。

class Watcher(val booking: Booking) {
  def runTrain() = {
    for (
      s <- booking.line.stations
    ) {
      print(s"Train arrived [${s.name}]: ")

      booking.train land land(s)
      booking.train board board(s)
      printf("-[%3d] +[%3d] ", land(s), board(s))

      println(s"= ${booking.train.load}")
    }
  }

  def land(station: Station): Int = {
    var amount: Int = 0
    for {i <- booking.intervals
         if i.end == station
    } amount += booking.sold(i)
    amount
  }

  def board(station: Station): Int = {
    var amount: Int = 0
    for {i <- booking.intervals
         if i.begin == station
    } amount += booking.sold(i)
    amount
  }
}

第五步:让列车跑一会儿

最后,我模拟了ABCDE一共5个站点,一列满载为100人的列车,然后卖了5个区段的票。

object Railway {
  def main(args: Array[String]) = {
    val line = new Line(List(
      Station("A", 1.0f, 1.0f),
      Station("B", 2.0f, 2.1f),
      Station("C", 3.0f, 3.1f),
      Station("D", 4.0f, 4.2f),
      Station("E", 5.0f, 5.0f)
    ))
    val train = new Train(100)
    val booking = new Booking(line, train)
    val watcher = new Watcher(booking)

    print("Line route[interval]: ")
    print((for (s <- line.stations) yield s.name).mkString(" -> "))
    println(s"\t[${booking.intervals.length}]")

    try {
      booking.sell(booking.intervals(0), 3)
      booking.sell(booking.intervals(4), 20)
      booking.sell(booking.intervals(3), 30)
      booking.sell(booking.intervals(6), 35)
      booking.sell(booking.intervals(9), 35)

      println(booking)
      watcher.runTrain()
    } catch {
      case e: IllegalArgumentException => println(e.getMessage)
    }
  }
}

得到如下输出:

  • 第一行是线路图,中括号里是包括的区段总数;
  • 第二行是已售出的客票情况,区段[票数];
  • 后面几行是模拟列车运行时上下旅客的情况
Line route[interval]: A -> B -> C -> D -> E [10]
AB[3]  AC[0]  AD[0]  AE[30]  BC[20]  BD[0]  BE[35]  CD[0]  CE[0]  DE[35]
Train arrived [A]: -[  0] +[ 33] = 33
Train arrived [B]: -[  3] +[ 55] = 85
Train arrived [C]: -[ 20] +[  0] = 65
Train arrived [D]: -[  0] +[ 35] = 100
Train arrived [E]: -[100] +[  0] = 0

如果卖的票有问题,就会象下面这样:

AB[3]  AC[0]  AD[0]  AE[50]  BC[20]  BD[0]  BE[35]  CD[0]  CE[0]  DE[35]
Train arrived [A]: -[  0] +[ 53] = 53
Train arrived [B]: Train is overloading.

我还没写完

这部分,我会根据上面的模拟程序,反其道而行之,得到一个售票的具体算法。先把目前这稿提交了吧,也好请群里的兄弟们指正一二。有空我再补充完整。

写在最后

我的方法从本质上说是一种类似于迷宫问题的递归或者穷举。在迭代过程中若发现会『超载』,则证明此路不通,此票不得出售。

作为对比,老汤在他的文章中用了一个很简单的出票方法,该方法的前提是『原子区间的可用票数,由工作人员在初始化车次时预先设定。』,具体内容参见浅谈个人对12306的核心用户诉求的核心模型设计思路和架构设计

出票时扣减库存的逻辑是:根据订单信息,拿到出发地和目的地,然后获取这段区间里的所有的原子区间。然后尝试将每个原子区间的可用票数减1,如果所有的原子区间都够减,则购票成功;否则购票失败,提示用户该票已经卖完了。

时间: 2024-10-25 21:13:13

12306出票算法随想的相关文章

初探12306售票算法(二)-java代码实践

周五闲来无事,基于上一篇关于初探12306售票算法(一)-理论,进行了java编码实践供各位读者参考(以下为相关代码的简单描述) 1.订票工具类 1.1初始化一列车厢的票据信息 /** * 生成Ticket信息 * * @param train * @return */ public static List<Ticket> initTicketList(Train train) { List<Ticket> result = new ArrayList<Ticket>(

初探12306售票算法

1.以G71列车为例,首先对车次站台进行占位编码(从1开始到最后一站递加) 对以上占位简单描述以下:G71总共18个站点那么我们的单个座位的座位标识可以用十八位长度的二进制字符串表示10000000000000000每一位代表一个站点,每天放票前初始化到下面的订票表中,数据如下 订票表中的始发受限站点和终到受限站点可以灵活搭配(这个就可以实现限制站点发售) 2.查询余票 如果我们要查询日期为2016-06-11,始发站保定东站(3)到韶关站(15)的G71二等座F座位余票情况只需要执行如下sql

12306 外包给阿里巴巴、IBM 等大企业做是否可行?

知乎上看到的,转载过来,雅俗共赏 12306首秀被骂的狗血喷头后铁道部找来IBM.阿里巴巴等大企业要解决方案,给出的条件是资金管够但是问题得解决.几大企业最后都拒绝了(其中阿里巴巴最后负责了排队系统的建设).12306开始自己尝试解决问题.他们发现市面上可以买到的成套解决方案都不足以应付春运购票负载,所以只能自己改进已有的数据库(注:其实是改用VMware SQLFire/GemFire,这里我之前理解错误).以前12306用的是小型机,发现性能严重不足,遂改用x86系统+linux平台(原平台

《算法之道》精华 算法设计部分

<算法之道>精华 算法设计部分 本书作者邹恒明,作者另有一本书<数据结构之弦>,以及<操作系统之哲学原理>都是非常好的书 这本书能够算得上是深入浅出.文笔非常好,作者加入了非常多自己的思考 本文仅包含算法设计部分,算法分析略去,并没有严格依照章节顺序来记录 附录 算法随想 有人喜欢遍历,希望踏遍千山万水,人生丰富多彩:有人一生贪婪,眼界不宽,及时行乐:有人注定穷搜,辛辛苦苦,收获有限:有人善用时空均衡,用最少的时间办最多的事情.十分精明:有人会分治,再难的问题也能解决.

摘录02

摘自知乎. 12306首秀被骂的狗血喷头后铁道部找来IBM.阿里巴巴等大企业要解决方案,给出的条件是资金管够但是问题得解决.几大企业最后都拒绝了(其中阿里巴 巴最后负责了排队系统的建设).12306开始自己尝试解决问题.他们发现市面上可以买到的成套解决方案都不足以应付春运购票负载,所以只能自己改进已有 的数据库(注:其实是改用VMware SQLFire/GemFire,这里我之前理解错误).以前12306用的是小型机,发现性能严重不足,遂改用x86系统+linux平台(原平台为 HP Supe

浅谈12306设计思路和算法

前言 春节期间,在汤哥的ENode QQ群(185916873)里,大家对12306的模型设计的讨论已经炸开了锅,很多大神都参与了讨论.由于我的DDD知识比较弱,和汤哥讨论了3-4个晚上,最后我跪了,在模型设计方面汤哥是相当专业的,汤哥的模型设计是相当正确的.在订票的算法上,和汤哥有点不同,我这篇文章旨在介绍我的设计思路和算法.在模型设计和架构设计上,汤哥的文章已经讲的很详细了.阅读本文之前,先要拜读汤哥的这篇文章. 我是技术性宅男,平时不写文章,我认为有写文章的时间,代码都能实现出来了.汤哥鼓

技术揭秘12306改造(一):尖峰日PV值297亿下可每秒出票1032张-CSDN.NET

技术揭秘12306改造(一):尖峰日PV值297亿下可每秒出票1032张-CSDN.NET 再谈Mysql MHA - 斩月 - 51CTO技术博客

Linux协议栈查找算法优化随想

Linux的网络协议栈实现可谓精确却不失精巧,不必说Netfilter,单单说TC就够了,但是有几处硬伤,本文做一个不完备的记录,就当是随笔,不必当真. 0.查找的种类 Linux协议栈作为一个纯软件实现,保留了硬件接口,但是本文不涉及硬件.       在Linux的协议栈实现中,由于没有硬件电路的固化,查找算法是难免的,比如路由查找,邻居查找,conntrack查找,socket查找,不一而足.事实上,协议栈作为一个公共组织,为所有的数据包服务,如果一个数据包到达协议栈,处理逻辑必须帮它找到

身为码农,为12306说两句公道话

C语言入门 模式的秘密---责任链模式 Grunt-beginner前端自动化工具 洪大师带你解读Symfony2框架 原文出处: 西西河 - 代码狗   欢迎分享原创到伯乐头条 我曾在淘宝写过一段时间代码,2012年在一家百强民企做电商副总,当时在极为艰苦的条件下带队开发了一个B2C网站,走支付宝和银联支付通道,年营业额千万级(当然实在太少了,我只是说这个网站投入了实际的运营). 也就在那个时候,我对12306嗤之以鼻,觉得他们做得太烂了,认为自己能带队花几百万半年时间做个好的出来.于是我狂妄