在网上一直听别人吹12306的算法多么多么复杂;整的挺玄乎的;周末在高铁站无聊的时候顺便思考这个问题打发时间;
最后发现它这个算法其实挺简单的;没有网上的那么复杂;
思路
简单说一下思路;
它其实就是一个二叉树的分裂算法;
假设我们的票有12个站点; 这里用1-12表示每个站点的名字; 1,2,3,4,5,6,7,8,9,10,11,12;
用户随机购买一段行程; 例如:从2号站上车到8号站下车;当出票成功之后,这张全程票就会裂变成两张新的票;区间分别是1-2;8-12;
如下
再例如:又有一个用户购买了9-11区间;那么就将8-12区间作为一个主节点;8-12节点重新裂变成:8-9;11-12;两个节点‘
现在这张票还剩下 1-2; 8-9; 11-12三个区间的票;这三个区间可以继续卖;但是已经无法再裂变了;
这就是一个超级简单的二叉树;当在某一个节点下购买一张票;那么这个节点就会自动裂变成0-2个子节点;全程票就是我们的根节点;
问题二: 权重问题;
当我们卖9-11的票时;我们可以在8-12里面出票;也可以在全程票1-12中出票;为了不影响购买全程票的用户;我们只能选择在8-12节点下出票;这里我们以区间数作为我们的权重判断标准;(这里不考虑多人候补的情况)
我们以节点经过的区间数量为权重; 例如1-2:算1个区间;1-12算11个区间;区间越小;权重越高;优先在这些区间里面出票;
问题三:树完整性问题
多棵数以座位号区分;
每个座位号都是一颗独立的数;
简单实现
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Data
class Plan implements Comparable<Plan> {
@ApiModelProperty("起始站点")
private Integer startPoint;
@ApiModelProperty("终点站点")
private Integer endPoint;
@ApiModelProperty("经过的站点数量")
private Integer routeSiteNum;
@ApiModelProperty("座位号")
private Integer seatNum;
@Override
public int compareTo(Plan o) {
return this.startPoint.compareTo(o.getStartPoint());
}
public static Plan build(Integer startPoint, Integer endPoint, Integer seatNum) {
if (startPoint.equals(endPoint)) {
return null;
}
Plan plan = new Plan();
plan.setStartPoint(startPoint);
plan.setEndPoint(endPoint);
plan.setRouteSiteNum(endPoint - startPoint);
plan.setSeatNum(seatNum);
return plan;
}
public static Plan build(Integer startPoint, Integer endPoint) {
if (startPoint.equals(endPoint)) {
return null;
}
Plan plan = new Plan();
plan.setStartPoint(startPoint);
plan.setEndPoint(endPoint);
plan.setRouteSiteNum(endPoint - startPoint);
return plan;
}
public final static String[] addressName = new String[]{"北京", "上海", "广州", "深圳", "成都", "杭州", "武汉", "重庆", "西安", "南京", "长沙", "南宁"};
public static boolean play(TreeMap<Integer, List<Plan>> map, Plan plan, TreeMap<Integer, List<Plan>> ticketMap) {
// 最简单的实现
for (Integer key : map.keySet()) {
if (key < plan.getRouteSiteNum()) {
continue;
}
List<Plan> plans = map.get(key);
for (Plan item : plans) {
if (item.startPoint <= plan.getStartPoint() && item.getEndPoint() >= plan.getEndPoint()) {
// 这里表示出票成功
// 如果出票成功,那么就会生成1-2张新的票据
log.info("出票成功:原票起始站: {};终点站:{}", addressName[item.getStartPoint() - 1], addressName[item.getEndPoint() - 1]);
plans.remove(item);
plan.setSeatNum(item.getSeatNum());
List<Plan> tickets = ticketMap.getOrDefault(plan.getSeatNum(), new ArrayList<>());
tickets.add(plan);
// 这里无所谓排序;只是为了debug 的时候可以更好的观察每个座位的出票情况;
Collections.sort(tickets);
ticketMap.put(plan.getSeatNum(), tickets);
addNewPlanToMap(map, build(item.getStartPoint(), plan.getStartPoint(), item.getSeatNum()));
addNewPlanToMap(map, build(plan.getEndPoint(), item.getEndPoint(), item.getSeatNum()));
return true;
}
}
}
return false;
}
public static void addNewPlanToMap(TreeMap<Integer, List<Plan>> map, Plan plan) {
if (plan == null) {
return;
}
List<Plan> planList = map.getOrDefault(plan.getRouteSiteNum(), new ArrayList<Plan>());
planList.add(plan);
map.put(plan.getRouteSiteNum(), planList);
}
}
@Slf4j
public class TrainPlan {
public static void main(String[] args) {
TreeMap<Integer, List<Plan>> map = new TreeMap<>();
// 这里就是初始化1000张全程票
for (int i = 0; i < 1000; i++) {
Plan plan = Plan.build(1, 12, i + 1);
assert plan != null;
List<Plan> planList = map.getOrDefault(plan.getRouteSiteNum(), new ArrayList<>());
planList.add(plan);
map.put(plan.getRouteSiteNum(), planList);
}
int i = 0;
// 准备4000个购票区间,模拟用户买票;
List<Plan> currentPlan = new ArrayList<>();
while (i < 4000) {
int startPoint = (int) (Math.random() * 12) + 1;
int endPoint = (int) (Math.random() * 12) + 1;
if (startPoint < endPoint) {
currentPlan.add(Plan.build(startPoint, endPoint));
i += 1;
}
}
log.info("程序准备完成...");
List<Plan> errorPlan = new ArrayList<>(); //
// 这里记录每个座位的出票情况; key是座位号
TreeMap<Integer, List<Plan>> trainTicketMap = new TreeMap<>();
for (Plan plan : currentPlan) {
boolean play = Plan.play(map, plan, trainTicketMap);
if (play) {
log.info("出票成功: {} ~ {}; 座位号: {}", Plan.addressName[plan.getStartPoint() - 1], Plan.addressName[plan.getEndPoint() - 1], plan.getSeatNum());
} else {
errorPlan.add(plan);
}
}
if (errorPlan.size() > 0) {
for (Plan plan : errorPlan) {
log.error("出票失败: {} ~ {}", Plan.addressName[plan.getStartPoint() - 1], Plan.addressName[plan.getEndPoint() - 1]);
}
}
}
}
执行结果;;;
例如:1号座位的出票结果为
1-3区间 出票成功
3-5区间 出票成功
5-6区间 出票成功
6-9区间 出票成功
9-10区间 出票成功
10-12区间 出票成功
总体说1号座位全程都卖出去了
总座位数1000;
随机购买4000张票;有1536张出票失败;
这里的权重优先级可以继续优化调整,我这里只是用写一个思路,没有细化