You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 12:40:35