如何实现100%符合规则的世界杯小组赛抽签算法?
实现100%符合规则的世界杯小组赛抽签算法
我花费大量时间尝试用JavaScript实现UEFA Champions League小组赛抽签功能,先后测试了5-7种算法,但均无法稳定成功(因随机打乱和选取导致成功率不稳定)。受挫放弃数月后,我尝试实现FIFA World Cup小组赛抽签,遇到了同样的问题,却发现原本用于世界杯的某一算法在欧冠场景下可成功运行。
我认为原因在于欧冠规则清晰固定:每组需包含各档球队,且同协会球队不能同组;而世界杯规则更为复杂,除每组包含各档球队、同足联球队不能同组外,还存在UEFA球队的动态规则——每组至少有1支、最多2支UEFA球队,其中5组有2支,3组有1支,现有算法无法处理这类动态规则。
我将算法逻辑举例说明:对每档球队打乱后,为每支球队筛选符合规则的可用分组,同时查看后续球队的可用分组,再统计拥有相同可用分组的球队数量。例如当前球队可用分组为[1,4],另一球队可用分组仅为[4],则分组4需预留,不能分配给当前球队;若有两支球队可用分组均为[1,3],则分组1和3需预留。
可正常运行的欧冠抽签代码
Array.prototype.randomIndex = function () { return Math.floor(Math.random() * this.length); }; Array.prototype.random = function () { return this[this.randomIndex()]; }; Array.prototype.shuffle = function () { return this.sort(() => Math.random() - .5); }; const pots = [[{"name":"Real Madrid","country":"Spain"},{"name":"Eintracht Frankfurt","country":"Germany"},{"name":"Manchester City","country":"England"},{"name":"AC Milan","country":"Italy"},{"name":"Bayern München","country":"Germany"},{"name":"Paris Saint-Germain","country":"France"},{"name":"FC Porto","country":"Portugal"},{"name":"Ajax","country":"Netherlands"}],[{"name":"Liverpool","country":"England"},{"name":"Chelsea","country":"England"},{"name":"FC Barcelona","country":"Spain"},{"name":"Juventus","country":"Italy"},{"name":"Atlético Madrid","country":"Spain"},{"name":"Sevilla","country":"Spain"},{"name":"RB Leipzig","country":"Germany"},{"name":"Tottenham Hotspur","country":"England"}],[{"name":"Borussia Dortmund","country":"Germany"},{"name":"FC Salzburg","country":"Austria"},{"name":"Shakhtar Donetsk","country":"Ukraine"},{"name":"Internazionale","country":"Italy"},{"name":"Napoli","country":"Italy"},{"name":"Benfica","country":"Portugal"},{"name":"Sporting CP Lisbon","country":"Portugal"},{"name":"Bayer Leverkusen","country":"Germany"}],[{"name":"Glasgow Rangers","country":"Scotland"},{"name":"Dinamo Zagreb","country":"Croatia"},{"name":"Olympique Marseille","country":"France"},{"name":"FC København","country":"Denmark"},{"name":"Club Brugge","country":"Belgium"},{"name":"Celtic","country":"Scotland"},{"name":"Viktoria Plzen","country":"Czechia"},{"name":"Maccabi Haifa","country":"Israel"}]]; const groups = new Array(8).fill().map(() => new Array(4)); function getAvailable(team, pot, execlude = null) { let availableGroups = groups.filter(g => g != execlude && g.every(t => !pot.includes(t))); availableGroups = availableGroups.filter(g => g.every(t => t.country != team.country)); return availableGroups; } function getAvailableGroups(teams, pot) { let availableGroups = getAvailable(teams[0], pot); if (availableGroups.length == 1) return availableGroups; let available = []; checking: for (let group of availableGroups) { let leftNextAvailableGroups = teams.slice(1).map(t => getAvailable(t, pot, group)); if (leftNextAvailableGroups.find(ag => !ag.length)) continue checking; let freeGroups = new Set; for (let nextTeamAvailableGroups of leftNextAvailableGroups) { if (nextTeamAvailableGroups.length == 1 && nextTeamAvailableGroups.includes(group)) continue checking; let samePossibilites = leftNextAvailableGroups.filter(ag => ag.length == nextTeamAvailableGroups.length && ag.every((g, i) => g == nextTeamAvailableGroups[i])); if (samePossibilites.length == nextTeamAvailableGroups.length && samePossibilites[0].includes(group)) continue checking; if (availableGroups.some(g => leftNextAvailableGroups.filter(ag => ag.length == 1).includes(g))) continue checking; if (samePossibilites.length > nextTeamAvailableGroups.length) continue checking; nextTeamAvailableGroups.forEach(g => freeGroups.add(g)); } if (freeGroups.size != teams.length - 1) continue checking; available.push(group); } return available; } pots.forEach(pot => pot.shuffle()); let p = 0; for (let pot of pots) { for (let i = 0; i < 8; i++) { if (!p) { groups[i][0] = pot[i]; continue; } let group = getAvailableGroups(pot.slice(i), pot).random(); group[p] = pot[i]; } p++; }
存在问题的世界杯抽签代码
反复刷新后会在控制台抛出“Ran out of possibilities”错误:
Array.prototype.shuffle = function () { return this.sort(() => Math.random() - .5); }; const teams = [{"name":"Qatar","conf":"AFC"},{"name":"Brazil","conf":"CONMEBOL"},{"name":"Belgium","conf":"UEFA"},{"name":"France","conf":"UEFA"},{"name":"Argentina","conf":"CONMEBOL"},{"name":"England","conf":"UEFA"},{"name":"Spain","conf":"UEFA"},{"name":"Portugal","conf":"UEFA"},{"name":"Mexico","conf":"CONCACAF"},{"name":"Netherlands","conf":"UEFA"},{"name":"Denmark","conf":"UEFA"},{"name":"Germany","conf":"UEFA"},{"name":"Uruguay","conf":"CONMEBOL"},{"name":"Switzerland","conf":"UEFA"},{"name":"USA","conf":"CONCACAF"},{"name":"Croatia","conf":"UEFA"},{"name":"Senegal","conf":"CAF"},{"name":"IR Iran","conf":"AFC"},{"name":"Japan","conf":"AFC"},{"name":"Morocco","conf":"CAF"},{"name":"Serbia","conf":"UEFA"},{"name":"Poland","conf":"UEFA"},{"name":"Korea Republic","conf":"AFC"},{"name":"Tunisia","conf":"CAF"},{"name":"Cameroon","conf":"CAF"},{"name":"Canada","conf":"CONCACAF"},{"name":"Ecuador","conf":"CONMEBOL"},{"name":"Saudi Arabia","conf":"AFC"},{"name":"Ghana","conf":"CAF"},{"name":"Wales","conf":"UEFA"},{"name":"Costa Rica","conf":"CONCACAF"},{"name":"Australia","conf":"AFC"}]; const pots = [ teams.slice(0, 8).sort((a, b) => { // Qatar automatically assigned to position A1 if (a.name == "Qatar") return -1; if (b.name == "Qatar") return 1; return Math.random() - .5; }), teams.slice(8, 16).shuffle(), teams.slice(16, 24).shuffle(), teams.slice(24, 32).shuffle() ]; let g = 1; let groups = Array(8).fill().map(() => Object.create(Array.prototype, {number: {value: g++}})); let uefaLength = (a, t) => a + (t?.conf == "UEFA"); function getAvailable(team, pot, UEFACount, execlude = null) { let availableGroups = groups.filter(g => g != execlude && g.every(t => !pot.includes(t))); if (team.conf == "UEFA") { let f = 2 - (groups.filter(g => g.reduce(uefaLength, 0) == 2).length == UEFACount); availableGroups = availableGroups.filter(g => g.reduce(uefaLength, 0) < f); } else { availableGroups = availableGroups.filter(g => g.every(t => t.conf != team.conf)); } return availableGroups; } function firstAvailableGroup(teams, pot, nextPots) { let availableGroups = getAvailable(teams[0], pot, 5); if (availableGroups.length == 1) return availableGroups[0]; checking: for (let group of availableGroups) { let UEFACount = 5 - (teams[0].conf == "UEFA" && group.some(t => t.conf == "UEFA")); let leftNextAvailableGroups = teams.slice(1).map(t => getAvailable(t, pot, UEFACount, group)); if (leftNextAvailableGroups.find(ag => !ag.length)) continue checking; let freeGroups = new Set; for (let nextTeamAvailableGroups of leftNextAvailableGroups) { if (nextTeamAvailableGroups.length == 1 && nextTeamAvailableGroups.includes(group)) continue checking; let samePossibilites = leftNextAvailableGroups.filter(ag => ag.length == nextTeamAvailableGroups.length && ag.every((g, i) => g == nextTeamAvailableGroups[i])); if (samePossibilites.length == nextTeamAvailableGroups.length && samePossibilites[0].includes(group)) continue checking; if (availableGroups.some(g => leftNextAvailableGroups.filter(ag => ag.length == 1).includes(g))) continue checking; if (samePossibilites.length > nextTeamAvailableGroups.length) continue checking; nextTeamAvailableGroups.forEach(g => freeGroups.add(g)); } if (freeGroups.size != teams.length - 1) continue checking; for (let pot of nextPots) { let leftNextAvailableGroups = pot.map(t => getAvailable(t, pot, UEFACount)); if (leftNextAvailableGroups.find(ag => !ag.length)) continue checking; let freeGroups = new Set; for (let nextTeamAvailableGroups of leftNextAvailableGroups) { if (!nextTeamAvailableGroups.length) continue checking; let samePossibilites = leftNextAvailableGroups.filter(ag => ag.length == nextTeamAvailableGroups.length && ag.every((g, i) => g == nextTeamAvailableGroups[i])); if (samePossibilites.length == nextTeamAvailableGroups.length && samePossibilites[0].includes(group)) continue checking; if (samePossibilites.length > nextTeamAvailableGroups.length) continue checking; nextTeamAvailableGroups.forEach(g => freeGroups.add(g)); } if (freeGroups.size != 8) continue checking; } return group; } } let p = 0; draw: for (let pot of pots) { for (let i = 0; i < 8; i++) { if (!p) { groups[i].push(pot[i]); continue; } let group = firstAvailableGroup(pot.slice(i), pot, pots.slice(p + 1)); if (!group) { console.log(pots); console.error("ERROR!", [p, i], pot[i], groups); throw Error("Ran out of possibilities"); } pot[i].group = group; group.push(pot[i]); } p++; }
请问如何实现100%符合规则的世界杯小组赛抽签算法?
[编辑说明:问题已解决]
内容的提问来源于stack exchange,提问作者Mahmoud Khudairi
相关产品推荐
相关产品推荐

