⓷妙!探索12306车内换座的最优分配法
⓵悟!从12306改签困惑到数据库设计—高铁随记
⓶惊!12306业务系统升级也能被普通人推动?
1
回顾
话说L老师坐高铁去北京参加DTCC大会,不经意间推动了12306车内换座业务上线后,收到了许多朋友的留言。有人告诉我,他们切身体验到了该业务带来的便利,如下。
2
最少换座次数的需求分析
CREATE TABLE TRAIN_SEATS (
SEAT_ID NUMBER PRIMARY KEY,
TRAIN_ID VARCHAR2(10),
CAR_NO NUMBER,
SEAT_NO VARCHAR2(3),
SEGMENT NUMBER,
STATUS VARCHAR2(10)
);INSERT INTO TRAIN_SEATS VALUES (1, 'G322', 1, '1A', 1, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (2, 'G322', 1, '1A', 2, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (3, 'G322', 1, '1A', 3, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (4, 'G322', 1, '1A', 4, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (5, 'G322', 1, '1A', 5, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (6, 'G322', 2, '2B', 1, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (7, 'G322', 2, '2B', 2, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (8, 'G322', 2, '2B', 3, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (9, 'G322', 2, '2B', 4, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (10, 'G322', 2, '2B', 5, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (11, 'G322', 3, '3C', 1, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (12, 'G322', 3, '3C', 2, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (13, 'G322', 3, '3C', 3, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (14, 'G322', 3, '3C', 4, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (15, 'G322', 3, '3C', 5, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (16, 'G322', 4, '4D', 1, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (17, 'G322', 4, '4D', 2, 'OCCUPIED');
INSERT INTO TRAIN_SEATS VALUES (18, 'G322', 4, '4D', 3, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (19, 'G322', 4, '4D', 4, 'AVAILABLE');
INSERT INTO TRAIN_SEATS VALUES (20, 'G322', 4, '4D', 5, 'AVAILABLE');
构造的基本数据用表格可用直观的展示如下:
根据座位可用情况列出车内换乘的方案,有方案ABCD...多种组合,如下。
表格这么一列,结论出来了:方案A仅需1次换座,方案B和C需2次,方案D需3次。方案A为最佳换座方案。
看到这可能会有人会说,这很简单,一眼就看明白了。其实,你只是看懂了需求而已,实现起来可不容易。接下来要做的,就是告诉计算机一个它能识别的算法。
3
最少换座次数的关键算法
首先,找出所有连续可用的座位段。比如,1车1A可以连续坐1-2段,2车2B可以连续坐2-3段,3车3C可以连续坐3-4段,4车4D可以连续做3-5段。
对于每个起始段,只保留能坐得最远的那个座位。为什么?因为如果有两个座位都能从某一段开始,那么选择能坐得更远的那个显然更好。
从第一段起,不断选择能接上的且坐得最远的下一个座位,直到覆盖全程。
这个方法之所以有效,是因为不存在这样的情况:选择坐得更短的座位反而能得到更好的结果。即使是连续路段的座位,你也可以在中间换到其他座位。比如,如果座位A可以坐1,2,3,4段,座位B可以坐3,4,5,6段,那么在3或4换座位都不会影响最终的换座次数。
4
最少换座次数的算法的SQL实现
WITH available AS (
SELECT *
FROM train_seats
WHERE train_id = 'G322'
AND status = 'AVAILABLE'
-- 如果对旅程路段有要求就写在这里
-- AND segment BETWEEN 开始 AND 结束
),
s AS (
-- 这个子查询找出从每个段出发能够坐得最远的那些座位, 假设路段segment都是连续的正整数
SELECT *
FROM (
SELECT t.*
, ROW_NUMBER() OVER (PARTITION BY first_segment ORDER BY cnt DESC) rn
-- 每个段只需留下能坐得最长的一个座位
FROM available
MATCH_RECOGNIZE (
-- 识别出每张票最多能连续坐几段
PARTITION BY car_no, seat_no
ORDER BY segment
MEASURES
MIN(segment) AS first_segment,
MAX(segment) AS last_segment,
COUNT(*) AS cnt
ONE ROW PER MATCH
PATTERN (strt a*)
DEFINE a AS segment = PREV(segment) + 1
) t
)
WHERE rn = 1
-- 这个筛选过后数据已经很少了,每个路段最多留下一行数据,递归的工作量很少
),
t(last_segment, path, rn) AS (
-- 执行递归SQL,利用上述结果拼出完整的行程
SELECT last_segment,
'段'|| first_segment || ':' || car_no || '车:' || seat_no AS path,
1 AS rn
FROM s
WHERE first_segment = 1
-- 假设旅程开始段从1开始,也可以修改
UNION ALL
SELECT s.last_segment,
t.path || '->'|| '段'|| first_segment || ':'|| s.car_no || '车:'|| s.seat_no,
ROW_NUMBER() OVER (ORDER BY s.last_segment DESC) rn
-- 那些拼上去的座位按终点路段排序找出最远的
FROM t, s
WHERE t.rn = 1
-- 只需留下能够走得最远的那一个座位
AND s.first_segment <= t.last_segment + 1
AND s.last_segment > t.last_segment
)
SELECT path
FROM t
WHERE rn = 1
AND last_segment = 5
-- 终点路段为5,可以根据需求修改
;WITH available AS (
SELECT train_seats.*,
segment - ROW_NUMBER() OVER (PARTITION BY car_no, seat_no ORDER BY segment) AS grp_id
-- 连续序号的差值作为分组依据
FROM train_seats
WHERE train_id = 'G322'
AND status = 'AVAILABLE'
-- 如果对旅程路段有要求就写在这里
-- AND segment BETWEEN 开始 AND 结束
),
s AS (
-- 这个子查询找出从每个段出发能够坐得最远的那些座位, 假设路段segment都是连续的正整数
SELECT * FROM (
SELECT car_no, seat_no,
MIN(segment) AS first_segment,
MAX(segment) AS last_segment,
COUNT(*) AS cnt,
ROW_NUMBER() OVER ( PARTITION BY MIN(segment) ORDER BY COUNT(*) DESC) rn
-- 每个段只需留下能坐得最长的一个座位
FROM available
GROUP BY car_no, seat_no, grp_id
)
WHERE rn = 1
-- 这个筛选过后数据已经很少了,每个路段最多留下一行数据,递归的工作量很少
)
SELECT* FROM (
SELECT SYS_CONNECT_BY_PATH ('段'|| first_segment || ':'|| car_no || '车:'|| seat_no, '->') AS path,
LEVEL AS change_count
FROM s
WHERE last_segment = 5
-- 终点路段为5,可以根据需求修改
START WITH first_segment = 1
CONNECT BY first_segment <= PRIOR last_segment + 1 AND last_segment > PRIOR last_segment
ORDER BY LEVEL
)
WHERE ROWNUM = 1;如何识别连续的数据段:写法1使用了MATCH_RECOGNIZE,这是一个强大的工具,可以用来发现数据中的模式。当然了,也可用ROW_NUMBER()的差额来求连续段,其原理是构造一个从1开始的连续序号,用这个序号和目标列进行相减,如果目标列是连续的话,这个差值就是一个恒定的数;假如目标列有断点,那么这个差值就会变成另外一个数字。用这个差值来进行分组就可以得到连续段。
如何构建最优路径:使用递归查询来构建最优路径,这种方法可以处理任意长度的旅程。
如何提高效率:首先找出了每个起始段能坐到的最远位置,这大大减少了后续处理的工作量。
如何使解决方案具有灵活性:这个方案可以轻松适应不同的旅程长度和座位情况,只需要修改几个参数就可以了。
5
结语
最后也给ITPUB数据库论坛打一个小小的广告,虽说这是一个新媒体的时代,我认为论坛还是讨论技术问题最好的地方,请大家积极参与,这里有以newkid领衔的国内外顶尖数据库专家,是很好的学习成长的机会。
明天就是国庆节,如果您在出行时体验到了车内换座,请记住我们这段故事。祝大家国庆快乐!
小朋友都能懂的人工智能⓷ -惊世骇俗的狗故事 小朋友都能懂的人工智能⓸ -狗大师的修仙之路
小朋友都能懂的人工智能⓺ -注意,句中高能!
小朋友都能懂的人工智能⑪一滴墨汁成就一代画师
小朋友都能懂的人工智能⑫从画师到视频大师
小朋友都能懂的人工智能⑬AI时代,未来就业走势
数据库二十年目睹之怪现状⓵ 太!多!了!
数据库二十年目睹之怪现状⓶ 测评现形记
数据库二十年目睹之怪现状⓷ 隐蔽的套壳
数据库二十年目睹之怪现状⓸ 小黑入狱记
从DTCC专场变换窥探数据库风云
从围棋收官到秦楚大战的数据库SQL实现(上)
从围棋收官到秦楚大战的数据库SQL实现(中)
从围棋收官到秦楚大战的数据库SQL实现(下
⓵悟!从12306改签困惑到数据库设计—高铁随记
⓶惊!12306业务系统升级也能被普通人推动?
预告:《超融合数据库》即将出版,关注梁老师公众号,敬请期待。