假设用户在乐鱼体育里已经生成了6支虚拟篮球队:雷霆、海港、钢铁、飞鹰、山城、北风,接着输入一句:“给它们排一个主客场双循环的联赛赛程。”最省事的实现方式是列出所有对阵,随机洗牌,然后每3场切成一轮。代码不超过十行,结果看上去也像一张赛程表。问题在于,只要逐轮检查一遍,几乎一定会发现它排不下去。
一张“看起来没问题”的随机赛程
先把数学算清楚。6支队双循环,每两队主客场各交手一次:单循环是6×5÷2=15场,双循环翻倍为30场。偶数支队伍时每轮每队恰好打一场,所以每轮3场,一共10轮,每队主场5场、客场5场。
下面是一次随机洗牌后切出来的赛程片段(模拟示例,“主队-客队”):
- 第3轮:雷霆-钢铁、钢铁-山城、海港-北风。钢铁在同一轮出现两次,飞鹰这一轮没有比赛。
- 第5至第8轮:雷霆连续四轮都是客队,而第9、10轮它又连续两个主场。
- 第1轮海港-雷霆,第2轮雷霆-海港:两支队的主客场两个回合被排在相邻两轮,第二回合几乎没有悬念上的铺垫。
- 第10轮三场比赛都在积分榜中游球队之间进行,而排名前两位的雷霆和海港早在第2轮就打完了全部交手。
第一条问题是致命的,后面三条则是“能打,但很难看”。这正好对应赛程编排里两类性质不同的约束。
随机排出合法赛程的概率有多低
有人会说,多洗几次总能洗出一张合法的。可以直接算一下。以6队单循环为例,15场比赛有15!种排列;而能切出“每轮3场、每队每轮恰好一场”的排列,等于合法的轮次划分方式(6队单循环共有6种本质不同的划分)乘以5轮的先后顺序、再乘以每轮内部3场的顺序,即6×5!×(3!)^5。两者相除,概率约为百万分之四点三,也就是大约二十三万次洗牌里才有一次合法。我们用脚本把随机洗牌实际跑了两百万次,命中的比例与这个数量级一致。
换成8队单循环,28场、7轮、每轮4场,合法划分共有6240种,概率会降到约两万亿分之一。这还只是“每队每轮最多一场”这一条约束,没算主客场、场馆和休息。靠随机碰运气是行不通的,赛程必须被“构造”出来。
硬约束:违反一条,整张赛程作废
硬约束是那些一旦违反,比赛在物理上或规则上就无法进行的条件。在一个虚拟联赛里,常见的硬约束包括:
- 每支队每一轮最多打一场;对偶数队单循环来说,是恰好一场。
- 每对球队的交手次数正确:单循环一次,双循环两次且主客场各一次。
- 一座场馆同一时段只能容纳一场比赛。
- 共用场馆的球队不能在同一时段都坐主场。比如设定里钢铁和山城都以城西体育馆为主场,那么同一轮如果两队都是主队,就必须错开时段,或者其中一队改客场。
- 两场比赛之间的最短休息时间。模拟设定中要求同一支队两场比赛之间至少间隔48小时,如果第6轮在周五20:00,这支队的第7轮就不能早于周日20:00开打。
- 淘汰阶段的对阵必须等上一轮结果出来才能确定,不能提前写死。
这些约束还会叠在一起。以乐鱼体育里一个常见的模拟设定为例:6支队只有5座主场馆,其中钢铁和山城共用城西体育馆,每轮3场比赛安排在同一天的14:00和19:30两个时段,两轮之间隔两天。这时每一轮的主客场方向一旦确定,场馆和时段也就被连带锁住了:如果钢铁和山城同一轮都做主队,就必须一场放14:00、一场放19:30;而在19:30出场的那支队,下一轮如果被排在两天后的14:00,间隔只有约42.5小时,达不到48小时,只能换到晚场,这又可能和另一支队的场馆需求撞在一起。单看每一条都很简单,同时满足才是真正的工作量。
赛程生成器对硬约束的态度只能是“零容忍”:宁可告诉用户“按你给的场馆和天数排不下”,也不能交出一张违规的赛程。
软约束:不违规,但会让联赛变难看
软约束允许违反,但每违反一次都要付出代价。常见的有这几类:
- 主客场交替。连续两个主场或连续两个客场,在赛程编排文献里称为一次break。理论上,偶数N队单循环至少会产生N−2次break,所以“完全没有连续主客场”是做不到的,目标是把break控制在最少,并且分散到不同球队身上。
- 避免长串连续客场。连续两个客场可以接受,连续四个客场对任何一支队都不公平,尤其在模拟里如果客场会带来体能或氛围上的劣势。
- 背靠背。篮球赛事里,前一天晚上打完第二天又要比赛,模拟的体能消耗会明显放大。允许出现,但应该均匀分配,不能让某一支队背靠背4次、另一支队一次都没有。
- 同一对手的两个回合之间要有间隔。双循环里常见的做法是前半程打完所有对手一次,后半程再打一次,这样同一对手的两回合至少隔了几轮。
- 悬念的位置。强强对话如果全部集中在前两轮,后半程就没有看点;更合理的是把一部分放在赛季中段,把至少一场放在最后一两轮。
- 最后一轮同时开赛。同一轮里与排名直接相关的比赛最好在同一时间开始,避免后开赛的球队根据已知结果调整打法。
软约束之间还会互相打架。想让强强对话出现在最后一轮,就可能要牺牲某支队的主客场交替;想减少背靠背,就需要更多比赛日,而用户给的天数可能不够。所以软约束通常会带权重,由用户决定哪一条更重要。
圆桌法:先保证每一轮都是合法配对
处理硬约束最经典的工具是圆桌法(circle method),按这种方式预先排好的对阵表也常被称为Berger表。做法是固定一支球队不动,其余球队围成一圈,每轮顺时针转一格,然后按位置两两配对。以6支队编号1到6为例,一个可行的结果是:
- 第1轮:1-6、2-5、3-4
- 第2轮:1-5、6-4、2-3
- 第3轮:1-4、5-3、6-2
- 第4轮:1-3、4-2、5-6
- 第5轮:1-2、3-6、4-5
5轮15场,每两队恰好相遇一次,每轮每队恰好一场,“同一轮打两场”这种问题从构造上就被排除了。双循环时,把这5轮原样再排一遍并交换主客场,就得到第6到第10轮,每队主客场各5场,同一对手的两回合也自然隔开了5轮。奇数支队伍时,加一个虚拟的“轮空”位置即可,和它配对的球队那一轮休息,所以5队单循环需要5轮、每轮2场,共10场。
不过圆桌法只解决了“谁和谁在哪一轮打”,并不管主客场是否交替、哪一轮放强强对话、场馆够不够用。直接照抄会出现另一种不自然:固定不动的1号队在两个半程的衔接处可能连续主场或连续客场,前后半程的主客场翻转也会在衔接处制造额外的break。
约束求解:从“排出来”到“排得好”
在乐鱼体育的赛程生成流程里,圆桌法给出的是一张合法的起点,接下来交给约束求解去调整。常见的调整空间有三层:把球队编号重新映射到圆桌位置上,决定谁在哪个位置,从而影响强强对话落在第几轮;调换轮次的先后顺序;在每场比赛里选择主客场方向。每一层调整都会用硬约束检查一遍,再对软约束计算一个总代价,例如每次break记1分、每次背靠背记2分、连续三个客场记5分、最后一轮没有任何一场前四名之间的对决记10分。
求解方法可以是带回溯的搜索:按轮次逐场分配时段和场馆,一旦发现某支队休息不足或场馆冲突就退回上一步换一种分配;规模再大时,可以交给约束规划或整数规划求解器。随机在这里仍然有用,但它的位置变了:它只在“满足所有硬约束、软约束代价相同”的几个方案之间做选择,让同样的输入每次能生成风格略有不同的赛程,并且用种子记录下来,保证用户能复现同一个版本。
回到那6支球队,经过这样一轮调整后的模拟结果可以是:每队主客场各5场,全联赛break总数控制在较低水平且没有任何一支队连续三个客场;雷霆和海港的两个回合分别落在第3轮和第9轮;第10轮的三场比赛在同一时间开始,其中一场是积分榜前两名的直接对话。
淘汰阶段的赛程还有另一套规矩
如果联赛后面还接季后赛,约束又会变化。单败淘汰每场淘汰一队,N支队需要N−1场才能产生唯一冠军;种子排位要让排名最高的两支队只可能在决赛相遇;赛程上要给同一半区的两场比赛留出相同的休息时间,否则先打完的一方会多出一天恢复。队伍数量不是2的幂时怎么安排轮空,赛制又该如何根据队数和天数自动选择,可以参考淘汰赛、循环赛和小组赛的自动选择。
用户改一条条件,赛程要能重新求解
一句话生成的赛程只是第一个版本。用户接着可能会说:“北风的主场第7轮被占用了”“联赛要压缩到两周内打完”“把雷霆和海港的第二回合放到最后一轮”。第一条是新增硬约束,第二条会让背靠背无法避免,第三条是提高某条软约束的权重。好的赛程生成器不会从头随机再排一张,而是在当前版本上做局部重排,并把代价变化告诉用户,比如“满足该要求后,飞鹰将新增一次连续客场”。赛程和球队、场馆、规则之间的完整依赖,在8支球队篮球赛事的生成物清单里有更具体的展开。
所以,AI生成赛程不能只是随机排列球队,原因并不复杂:随机排列连“每队每轮一场”都几乎保证不了,更谈不上公平和悬念。赛程本身就是一个约束问题,先用确定的方法构造合法结构,再用求解去平衡各种软约束,最后才轮到随机在合格的方案之间挑选。