我正在编写运动时间表生成器。给定T个团队(偶数),每回合G个游戏和R个回合,我想生成一个符合条件的时间表:
我有一种算法在大多数情况下都有效,但并非总是如此。在该问题的末尾有详细说明。 我该如何解决(或替换)此算法可对所有合理的输入都有效地起作用?
此问题类似于Sorting pairs of teams with non-repeating | Round-robin tournament和Algorithm: Selecting pairs of teams from a set of games,但有不同的要求。
例如,假设有T = 4个团队。这给了我们6种可能的游戏:
(T0,T1) (T0,T2) (T0,T3) (T1,T2) (T1,T3) (T2,T3)
如果每轮有G = 4场比赛,那么第一轮必须not是这组比赛...
(T0,T1) (T0,T2) (T0,T3) (T1,T2)
…因为T0可以玩3次,但是T3只能玩一次(违反了要求#1)。相反,第一轮可能看起来像这样,每个团队都可以玩两场比赛:
(T0,T1) (T2,T3) (T0,T2) (T1,T3)
如果在第二轮中重复相同的游戏,则将永远不会发生两个游戏(T1,T2)
和(T0,T3)
(违反了要求#2)。因此,我们希望在选择新游戏之前确保将它们包含在第二轮比赛中。 T = 4,G = 4,R = 5的有效时间表是:
(T0,T1) (T2,T3) (T0,T2) (T1,T3)
(T0,T3) (T1,T2) (T0,T1) (T2,T3)
(T0,T2) (T1,T3) (T0,T3) (T1,T2)
(T0,T1) (T2,T3) (T0,T2) (T1,T3)
(T0,T3) (T1,T2) (T0,T1) (T2,T3)
如所见,对于较大的值 R 可以接受的一轮游戏最终可以重复进行。
我拥有的算法如下:
currentPool
。otherPool
。currentPool
中找到游戏。currentPool
移至otherPool
。currentPool
为空,则交换currentPool
和otherPool
。对于T,G和R的许多合理值,此算法有效。但是,有些组合会失败。例如,在T = 6,G = 3,R = 5的情况下,它将生成此计划:
(T0,T1) (T2,T3) (T4,T5)
(T0,T2) (T1,T3) (T0,T4)
(T0,T3) (T1,T2) (T0,T5)
(T1,T4) (T2,T5) (T3,T4)
(T1,T5) (T2,T4) (T3,T5)
第一轮是正确的,但在第二轮中,T0出战两次,T5从未出战。问题很容易发现-在第2轮中选择(T0,T2)
和(T1,T3)
后,唯一可能满足要求#1的游戏将是(T4,T5)
,但是该游戏已经在第一轮中按要求使用了#在全部15个独特的游戏用完之前,不能再使用2个。该算法开始陷入僵局,无法回溯。
最后,为了完整起见,这是所描述算法的JavaScript版本。这是成功运行的示例输出:
let schedule = singleFieldSchedule({
teams : 8,
maxGamesPerRound : 12,
rounds : 8
})
console.log(schedule.map(round => round.map(game => `(T${game[0]},T${game[1]})`).join(' ')).join('\n') )
(T0,T1) (T2,T3) (T4,T5) (T6,T7) (T0,T2) (T1,T3) (T4,T6) (T5,T7) (T0,T3) (T1,T2) (T4,T7) (T5,T6)
(T0,T4) (T1,T5) (T2,T6) (T3,T7) (T0,T5) (T1,T4) (T2,T7) (T3,T6) (T0,T6) (T1,T7) (T2,T4) (T3,T5)
(T0,T7) (T1,T6) (T2,T5) (T3,T4) (T0,T1) (T2,T3) (T4,T5) (T6,T7) (T0,T2) (T1,T3) (T4,T6) (T5,T7)
(T0,T3) (T1,T2) (T4,T7) (T5,T6) (T0,T4) (T1,T5) (T2,T6) (T3,T7) (T0,T5) (T1,T4) (T2,T7) (T3,T6)
(T0,T6) (T1,T7) (T2,T4) (T3,T5) (T0,T7) (T1,T6) (T2,T5) (T3,T4) (T0,T1) (T2,T3) (T4,T5) (T6,T7)
(T0,T2) (T1,T3) (T4,T6) (T5,T7) (T0,T3) (T1,T2) (T4,T7) (T5,T6) (T0,T4) (T1,T5) (T2,T6) (T3,T7)
(T0,T5) (T1,T4) (T2,T7) (T3,T6) (T0,T6) (T1,T7) (T2,T4) (T3,T5) (T0,T7) (T1,T6) (T2,T5) (T3,T4)
(T0,T1) (T2,T3) (T4,T5) (T6,T7) (T0,T2) (T1,T3) (T4,T6) (T5,T7) (T0,T3) (T1,T2) (T4,T7) (T5,T6)
function singleFieldSchedule({teams=8, maxGamesPerRound=12, rounds=8}={}) {
const uniquePairs = a => a.reduce((res,o1,i,a) => res.concat(a.slice(i+1).map(o2 => [o1,o2])), [])
const teamNames = Array.from(Array(teams).keys())
const fullExposure = uniquePairs(teamNames)
const zeroTeamCounts = teamNames.map(n => [n,0])
// Calculate how many games can be played by each team while keeping things fair
const gamesPerTeam = Math.floor(maxGamesPerRound / teams * 2)
const gamesPerRound = gamesPerTeam * teams/2
const schedule = []
const pools = [fullExposure, []]
let poolIndex = 0
for (let r=0; r<rounds; ++r) {
const round = []
schedule.push(round)
const gamesPerTeam = new Map(zeroTeamCounts)
for (let g=0; g<gamesPerRound; ++g) {
let pool = pools[poolIndex]
if (!pool.length) pool = pools[poolIndex=((poolIndex+1)%2)]
// Find the game whose teams have been seen the least
let bestGameSum = Infinity
let bestGameIndex
for (i=0; i<pool.length; ++i) {
const game = pool[i];
// We square the times seen to favor a game where each team has been seen once
// over a game where one team has been seen twice and the other team has never been seen
const gameSum = gamesPerTeam.get(game[0])**2 + gamesPerTeam.get(game[1])**2
if (gameSum < bestGameSum) {
bestGameSum = gameSum
bestGameIndex = i
}
}
let bestGame = pool.splice(bestGameIndex, 1)[0]
round.push(bestGame)
gamesPerTeam.set(bestGame[0], gamesPerTeam.get(bestGame[0])+1);
gamesPerTeam.set(bestGame[1], gamesPerTeam.get(bestGame[1])+1);
// Put this game into the 'other' pool, to be used once this pool of games is used up
pools[(poolIndex+1) % 2].push(bestGame)
}
// Check to see if any team got screwed this round
const shortedTeams = teamNames.filter(t => gamesPerTeam.get(t)<gamesPerTeam)
shortedTeams.forEach( t => {
const ct = gamesPerTeam.get(t)
console.warn(`Team ${t} only got to play ${ct}/${gamesPerTeam} games in round #${r}`)
})
}
return schedule
}
我在理论上没有完美的解决方案,但是我有多项式时间方法应该会更好。
其核心是最大匹配的Blossom Algorithm。在每个回合中,将其与代表尚未玩过的游戏的边缘一起使用。这将更有可能找到可能因当前算法而失败的简单案例的有效解决方案。特别是,您已确保团队不能一轮进行2场比赛,并且尽可能多地使用未使用的游戏。
但是我们可以通过使用a variation to find maximal weight matchings来对此进行改进。如果将每个边的权重设为G^i
,其中G
是打的比赛数,i
是自上一场比赛以来的回合数,则团队不能一轮进行2场比赛,并且我们会玩尽可能多的旧游戏。
此算法可保证您的第一个条件,并真诚地努力到第二个条件都做好。但这并不能保证第二。 (但是,如果您确实有早期重复的话,它们会很好地散开。)
如果您有很多发子弹,则可以根据重量条件进行测试,以确保每轮平均使用正确的次数。