欧冠十六强模拟抽签及概率统计实验

背景

元旦了,发一篇纪念性质的文章:《欧冠十六强模拟抽签及概率统计实验》,用来统计欧冠十六强每个队和所有可能对手的相遇概率,和每个对阵情况的出现概率。

规则说明

在介绍本次实验之前,先和非球迷或对欧冠十六强抽签规则不明白的朋友明确一下欧冠十六强的抽签规则:

1、欧冠十六强由八个小组的前两名产生,并且根据回避规则抽签为每个球队选出十六强战(即八分之一决赛)的对手;

2、回避规则1:同组回避,即如果球队A和B来自同一小组(也就是说分别是该小组的前两名),则回避,不在十六强战就碰面;

3、回避规则2:同足协回避,每个国家或地区都有一个足协,如苏格兰足协、英格兰足协、德国足协等,如果球队A和B来自同一足协,则回避;

4、回避规则3:同小组排名回避,即如果球队A和B在各自小组内的排名相同,比如均为小组第一或小组第二,则回避。

本次实验的十六强队伍取自21-22赛季的欧冠十六强。

实验设计

我这儿不是写论文,所以设计方面就说得简单一些。本次实验根据回避规则进行3000次独立的十六强抽签,并统计每个球队的相遇情况,和每个对阵图出现的情况,最后不升序输出每支球队的每个对手的相遇概率、相遇频率和每个对阵图出现的概率和频率。

本实验有四个类:Team、Matcher、AgainstPlan和UEFAChampionLeagueDraw。

Team类

Team类是对球队信息和处理的封装,球队信息包括基本信息(球队名称、所在小组、所属足协和组内排名)、本次抽签信息(是否完成匹配和本次抽签选中的对手)和全局抽签信息(所有可能的对手和所有对手相遇次数)。

球队信息处理除了对基本信息和是否完成匹配的get/set之外,还包括对所有可能的对手的增删、对所有对手相遇次数的添加和输出

AgainstPlan类

AgainstPlan类是对阵图类,用于保存和处理每轮抽签产生的对阵图,使用了单例模式。字段就一个:所有对阵图的出现次数,该类负责的就是对所有对阵图的出现次数的添加和输出。由于单条对阵图数据较长,且产生的对阵图种类很多,所以默认只输出出现次数前20的对阵图。

Matcher类

该类没有字段,负责三件事:1、对欧冠十六强的单次抽签,并向Team类中的所有对手相遇次数和AgainstPlan类中的所有对阵图的出现次数字段提交本次抽签结果;2、重置欧冠十六强的抽签情况;3、判断两支球队能否完成匹配

UEFAChampionLeagueDraw类

该类是入口类,负责初始化欧冠十六强的基本信息、启动3000次独立的Matcher抽签、输出统计信息。

代码实现

Team类

首先定义所有的字段,并实现基本的get/set方法:

private String name; // 球队名称

private String country; // 球队所在国家

private String group; // 球队所在小组

private int rankInGroup; // 球队的组内排名

private boolean matched = false; // 球队是否配对

private HashMap oppositeCount = new HashMap<>(); // 记录对手和相遇次数

private String oppositeCache = null; // 对手缓存

private List allPossibleOpposites = new ArrayList<>(); // 所有可能的对手

public String getName() {

return name;

}

public Team setName(String name) {

this.name = name;

return this;

}

public String getCountry() {

return country;

}

public Team setCountry(String country) {

this.country = country;

return this;

}

public String getGroup() {

return group;

}

public Team setGroup(String group) {

this.group = group;

return this;

}

public int getRankInGroup() {

return rankInGroup;

}

public Team setRankInGroup(int rankInGroup) {

this.rankInGroup = rankInGroup;

return this;

}

public boolean isMatched() {

return matched;

}

public Team setMatched(boolean matched) {

this.matched = matched;

return this;

}

然后实现为当前球队添加所有的可能对手:

/**

* 从所有球队@allOpposites中选取所有可能的对手

* @param allOpposites 所有球队

* @return this

*/

public Team addAllPossibleOpposite(Team[] allOpposites) {

for (Team opposite : allOpposites) {

if (opposite.getCountry().equals(country) // 同国回避

|| opposite.getGroup().equals(group) // 同组回避

|| opposite.getRankInGroup() == rankInGroup /* 同排名回避 */ ) {

continue;

}

addPossibleOpposite(opposite);

}

return this;

}

/**

* 从所有球队@allOpposites中选取所有可能的对手

* @param allOpposites 所有球队

* @return this

*/

public Team addAllPossibleOpposite(List allOpposites) {

for (Team opposite : allOpposites) {

if (opposite.getCountry().equals(country) // 同国回避

|| opposite.getGroup().equals(group) // 同组回避

|| opposite.getRankInGroup() == rankInGroup /* 同排名回避 */ ) {

continue;

}

addPossibleOpposite(opposite);

}

return this;

}

/**

* 将对手添加到本队所有对手中

* @param opposite 当前对手

*/

private void addPossibleOpposite(Team opposite) {

if (!allPossibleOpposites.contains(opposite)) {

allPossibleOpposites.add(opposite);

}

}

当其他某个球队完成匹配时,我们要把这支球队从当前球队的可能对手中移除:

/**

* 将对手从本队所有可能的对手中删除,这时可能该对手已经完成配对

* @param opposite 当前对手

* @return this

*/

public Team removePossibleOpposite(Team opposite) {

allPossibleOpposites.remove(opposite);

return this;

}

我们需要判断一下当前球队还有没有可能的对手,以及某个球队是不是当前球队的唯一可能对手,这两点对于判断其他两支球队能否匹配来说至关重要,下面我们会说到这一点:

/**

* 判断@opposite是否是本队唯一可能的对手

* @param opposite 当前对手

* @return true表示当前对手是本队唯一可能的对手,false表示不是

*/

public boolean isOnlyPossibleOpposite(Team opposite) {

return allPossibleOpposites.size() == 1 && allPossibleOpposites.contains(opposite);

}

/*

*

* 本队是否还有可能的对手

* @return true表示还存在可能的对手,false表示不存在

*/

public boolean hasPossibleOpposites() {

return !allPossibleOpposites.isEmpty();

}

当对当前球队进行配对时,我们需要对其本轮抽签的对手缓存进行读写和判断:

/**

* 设置对手缓存

* @param opposite 当前对手

* @return this

*/

public Team setOppositeCache(Team opposite) {

String oppositeName = opposite.getName();

if (!oppositeName.equals(oppositeCache)) {

oppositeCache = oppositeName;

}

return this;

}

public String getOppositeCache() {

return oppositeCache;

}

/**

* 重置对手缓存,此时有两种情况:1、初始化,开始一次新的抽签;2、某次抽签尝试失败,需要重启本次抽签

* @return this

*/

public Team unsetOppositeCache() {

oppositeCache = null;

return this;

}

抽签结束后,我们需要不升序输出当前球队的每个对手相遇情况:

/**

* 输出本队所有对手及对应的相遇频次和频率,按照相遇频率不升序排序

* @return String 待显示的字符串

*/

public String printOppositePossibilities() {

StringBuilder builder = new StringBuilder();

// 将所有出现的对手,按相遇次数降序排序

Integer[] matchCountSorted = new Integer[oppositeCount.size()];

int i = 0;

int totalCount = 0;

for (String opposite : oppositeCount.keySet()) {

matchCountSorted[i] = oppositeCount.get(opposite);

totalCount += matchCountSorted[i];

i++;

}

Arrays.sort(matchCountSorted, Collections.reverseOrder());

// 某个相遇频度是否完成输出的索引列表,1表示该频度对应的所有球队都已经输出,0表示该频度还有没有输出的球队

List appended = new ArrayList();

for (int j = 0; j < oppositeCount.size(); j++) {

appended.add(0);

}

builder.append("{ \n");

for (Integer value : matchCountSorted) {

int index = 0;

for (Map.Entry entry : oppositeCount.entrySet()) {

if (!oppositeCount.get(entry.getKey()).equals(value) || appended.get(index) == 1) {

index++;

continue;

}

builder.append("\t").append(entry.getKey()).append(": ")

.append("遭遇次数:").append(oppositeCount.get(entry.getKey())).append("\t")

.append("遭遇频率:").append(String.format("%.2f", 100 * entry.getValue() / (float) totalCount))

.append("%,\n");

appended.set(index, 1);

index++;

}

}

int lastSplitterIndex = builder.lastIndexOf(",\n");

if (lastSplitterIndex != -1) {

builder.delete(lastSplitterIndex, lastSplitterIndex + ",\n".length() - 1);

}

builder.append("}");

return builder.toString();

}

实验结束后,我们可以清空一下球队的数据:

/**

* 清空本队所有缓存数据

*/

public void clearAll() {

oppositeCache = null;

oppositeCount.clear();

allPossibleOpposites.clear();

}

球队类的全部代码如下所示:

package com.szc.UEFAChampionLeague;

import java.util.*;

public class Team {

private String name; // 球队名称

private String country; // 球队所在国家

private String group; // 球队所在小组

private int rankInGroup; // 球队的组内排名

private boolean matched = false; // 球队是否配对

private HashMap oppositeCount = new HashMap<>(); // 记录对手和相遇次数

private String oppositeCache = null; // 对手缓存

private List allPossibleOpposites = new ArrayList<>(); // 所有可能的对手

public String getName() {

return name;

}

public Team setName(String name) {

this.name = name;

return this;

}

public String getCountry() {

return country;

}

public Team setCountry(String country) {

this.country = country;

return this;

}

public String getGroup() {

return group;

}

public Team setGroup(String group) {

this.group = group;

return this;

}

public int getRankInGroup() {

return rankInGroup;

}

public Team setRankInGroup(int rankInGroup) {

this.rankInGroup = rankInGroup;

return this;

}

public boolean isMatched() {

return matched;

}

public Team setMatched(boolean matched) {

this.matched = matched;

return this;

}

/**

* 从所有球队@allOpposites中选取所有可能的对手

* @param allOpposites 所有球队

* @return this

*/

public Team addAllPossibleOpposite(Team[] allOpposites) {

for (Team opposite : allOpposites) {

if (opposite.getCountry().equals(country) // 同国回避

|| opposite.getGroup().equals(group) // 同组回避

|| opposite.getRankInGroup() == rankInGroup /* 同排名回避 */ ) {

continue;

}

addPossibleOpposite(opposite);

}

return this;

}

/**

* 从所有球队@allOpposites中选取所有可能的对手

* @param allOpposites 所有球队

* @return this

*/

public Team addAllPossibleOpposite(List allOpposites) {

for (Team opposite : allOpposites) {

if (opposite.getCountry().equals(country) // 同国回避

|| opposite.getGroup().equals(group) // 同组回避

|| opposite.getRankInGroup() == rankInGroup /* 同排名回避 */ ) {

continue;

}

addPossibleOpposite(opposite);

}

return this;

}

/**

* 将对手添加到本队所有对手中

* @param opposite 当前对手

*/

private void addPossibleOpposite(Team opposite) {

if (!allPossibleOpposites.contains(opposite)) {

allPossibleOpposites.add(opposite);

}

}

/**

* 将对手从本队所有可能的对手中删除,这时可能该对手已经完成配对

* @param opposite 当前对手

* @return this

*/

public Team removePossibleOpposite(Team opposite) {

allPossibleOpposites.remove(opposite);

return this;

}

/**

* 判断@opposite是否是本队唯一可能的对手

* @param opposite 当前对手

* @return true表示当前对手是本队唯一可能的对手,false表示不是

*/

public boolean isOnlyPossibleOpposite(Team opposite) {

return allPossibleOpposites.size() == 1 && allPossibleOpposites.contains(opposite);

}

/**

* 本队是否还有可能的对手

* @return true表示还存在可能的对手,false表示不存在

*/

public boolean hasPossibleOpposites() {

return !allPossibleOpposites.isEmpty();

}

/**

* 本次抽签完成,本队将对手缓存@oppositeCache添加到映射oppositeCount中,同时该对手的出现频次+1

*/

public void confirmOppositesCache() {

if (!oppositeCount.containsKey(oppositeCache)) {

oppositeCount.put(oppositeCache, 1);

} else {

oppositeCount.put(oppositeCache, oppositeCount.get(oppositeCache) + 1);

}

}

/**

* 设置对手缓存

* @param opposite 当前对手

* @return this

*/

public Team setOppositeCache(Team opposite) {

String oppositeName = opposite.getName();

if (!oppositeName.equals(oppositeCache)) {

oppositeCache = oppositeName;

}

return this;

}

public String getOppositeCache() {

return oppositeCache;

}

/**

* 重置对手缓存,此时有两种情况:1、初始化,开始一次新的抽签;2、某次抽签尝试失败,需要重启本次抽签

* @return this

*/

public Team unsetOppositeCache() {

oppositeCache = null;

return this;

}

/**

* 清空本队所有缓存数据

*/

public void clearAll() {

oppositeCache = null;

oppositeCount.clear();

allPossibleOpposites.clear();

}

/**

* 输出本队所有对手及对应的相遇频次和频率,按照相遇频率不升序排序

* @return String 待显示的字符串

*/

public String printOppositePossibilities() {

StringBuilder builder = new StringBuilder();

// 将所有出现的对手,按相遇次数降序排序

Integer[] matchCountSorted = new Integer[oppositeCount.size()];

int i = 0;

int totalCount = 0;

for (String opposite : oppositeCount.keySet()) {

matchCountSorted[i] = oppositeCount.get(opposite);

totalCount += matchCountSorted[i];

i++;

}

Arrays.sort(matchCountSorted, Collections.reverseOrder());

// 某个相遇频度是否完成输出的索引列表,1表示该频度对应的所有球队都已经输出,0表示该频度还有没有输出的球队

List appended = new ArrayList();

for (int j = 0; j < oppositeCount.size(); j++) {

appended.add(0);

}

builder.append("{ \n");

for (Integer value : matchCountSorted) {

int index = 0;

for (Map.Entry entry : oppositeCount.entrySet()) {

if (!oppositeCount.get(entry.getKey()).equals(value) || appended.get(index) == 1) {

index++;

continue;

}

builder.append("\t").append(entry.getKey()).append(": ")

.append("遭遇次数:").append(oppositeCount.get(entry.getKey())).append("\t")

.append("遭遇频率:").append(String.format("%.2f", 100 * entry.getValue() / (float) totalCount))

.append("%,\n");

appended.set(index, 1);

index++;

}

}

int lastSplitterIndex = builder.lastIndexOf(",\n");

if (lastSplitterIndex != -1) {

builder.delete(lastSplitterIndex, lastSplitterIndex + ",\n".length() - 1);

}

builder.append("}");

return builder.toString();

}

@Override

public boolean equals(Object o) {

if (this == o) return true;

if (!(o instanceof Team)) return false;

Team team = (Team) o;

return getRankInGroup() == team.getRankInGroup()

&& Objects.equals(getName(), team.getName())

&& Objects.equals(getCountry(), team.getCountry())

&& Objects.equals(getGroup(), team.getGroup());

}

@Override

public int hashCode() {

return Objects.hash(getName(), getCountry(), getGroup(), getRankInGroup());

}

@Override

public String toString() {

return "Team{" +

"name='" + name + '\'' +

", country='" + country + '\'' +

", group='" + group + '\'' +

", rankInGroup=" + rankInGroup +

'}';

}

}

AgainstPlan类

首先定义对阵图类的字段,并实现单例:

private HashMap againstPlan = new HashMap<>(); // 对阵图和出现次数的映射

private static volatile AgainstPlan sInstance;

private AgainstPlan() {}

public static AgainstPlan getsInstance() {

if (sInstance == null) {

synchronized (AgainstPlan.class) {

if (sInstance == null) {

sInstance = new AgainstPlan();

}

}

}

return sInstance;

}

然后,每轮抽签结束后,我们要根据每个球队的抽签结果更新一下映射:

/**

* 提交所有球队单次抽签的结果,形成对阵图,保存到全局缓存映射中

* @param teams 欧冠十六强

*/

public void confirmDrawResult(Team[] teams) {

if (teams == null || teams.length == 0) {

return;

}

StringBuilder builder = new StringBuilder();

for (Team team : teams) {

if (team.getRankInGroup() != 2) {

// 小组第二先主后客,所以只针对小组第二统计对阵图,同时也避免了重复统计

continue;

}

// 对阵图格式:A vs B

builder.append("\t\t").append(team.getName())

.append(" vs ")

.append(team.getOppositeCache())

.append("\n");

}

builder.deleteCharAt(builder.lastIndexOf("\n"));

String currentAgainstPlan = builder.toString();

// 用本次对阵图更新对阵图映射

if (!againstPlan.containsKey(currentAgainstPlan)) {

againstPlan.put(currentAgainstPlan, 1);

} else {

againstPlan.put(currentAgainstPlan, againstPlan.get(currentAgainstPlan) + 1);

}

}

当所有抽签结束后,我们同样要对对阵图情况进行输出,这不过这里进行了截取:

/**

* 输出出现次数前20的对阵情况,如果对阵图缓存量 <= 20,则输出全部对阵情况

* @return 全部对阵情况的字符串

*/

public String printAllAgainstPlan() {

return printAllAgainstPlan(20);

}

/**

* 输出出现次数前topN的对阵情况,如果对阵图缓存量 <= topN,则输出全部对阵情况

* @param topN 截取出现次数的前topN名

* @return 全部对阵情况的字符串

*/

public String printAllAgainstPlan(int topN) {

StringBuilder builder = new StringBuilder();

Integer[] matchCountSorted = new Integer[againstPlan.size()];

int i = 0;

int totalCount = 0;

for (String opposite : againstPlan.keySet()) {

matchCountSorted[i] = againstPlan.get(opposite);

totalCount += matchCountSorted[i];

i++;

}

Arrays.sort(matchCountSorted, Collections.reverseOrder());

// 对阵情况的topN截取

Integer[] matchCountFiltered;

if (matchCountSorted.length > topN) {

matchCountFiltered = Arrays.copyOfRange(matchCountSorted, 0, topN);

} else {

matchCountFiltered = Arrays.copyOf(matchCountSorted, matchCountSorted.length);

}

List appended = new ArrayList();

for (int j = 0; j < againstPlan.size(); j++) {

appended.add(0);

}

builder.append("{ \n");

for (Integer value : matchCountFiltered) {

int index = 0;

for (Map.Entry entry : againstPlan.entrySet()) {

if (!againstPlan.get(entry.getKey()).equals(value) || appended.get(index) == 1) {

index++;

continue;

}

builder.append("\t").append("对阵情况: {\n")

.append(entry.getKey())

.append("\n\n\t\t出现次数:").append(entry.getValue())

.append("\n\t\t出现概率:").append(String.format("%.2f", 100 * entry.getValue() / (float) totalCount))

.append("%\n\t},\n");

appended.set(index, 1);

index++;

}

}

int lastSplitterIndex = builder.lastIndexOf(",\n");

if (lastSplitterIndex != -1) {

builder.delete(lastSplitterIndex, lastSplitterIndex + ",\n".length() - 1);

}

builder.append("}");

return builder.toString();

}

对阵图类的全部代码如下所示:

package com.szc.UEFAChampionLeague;

import java.util.*;

public class AgainstPlan {

private HashMap againstPlan = new HashMap<>(); // 对阵图和出现次数的映射

private static volatile AgainstPlan sInstance;

private AgainstPlan() {}

public static AgainstPlan getsInstance() {

if (sInstance == null) {

synchronized (AgainstPlan.class) {

if (sInstance == null) {

sInstance = new AgainstPlan();

}

}

}

return sInstance;

}

/**

* 提交所有球队单次抽签的结果,形成对阵图,保存到全局缓存映射中

* @param teams 欧冠十六强

*/

public void confirmDrawResult(Team[] teams) {

if (teams == null || teams.length == 0) {

return;

}

StringBuilder builder = new StringBuilder();

for (Team team : teams) {

if (team.getRankInGroup() != 2) {

// 小组第二先主后客,所以只针对小组第二统计对阵图,同时也避免了重复统计

continue;

}

// 对阵图格式:A vs B

builder.append("\t\t").append(team.getName())

.append(" vs ")

.append(team.getOppositeCache())

.append("\n");

}

builder.deleteCharAt(builder.lastIndexOf("\n"));

String currentAgainstPlan = builder.toString();

// 用本次对阵图更新对阵图映射

if (!againstPlan.containsKey(currentAgainstPlan)) {

againstPlan.put(currentAgainstPlan, 1);

} else {

againstPlan.put(currentAgainstPlan, againstPlan.get(currentAgainstPlan) + 1);

}

}

/**

* 输出出现次数前20的对阵情况,如果对阵图缓存量 <= 20,则输出全部对阵情况

* @return 全部对阵情况的字符串

*/

public String printAllAgainstPlan() {

return printAllAgainstPlan(20);

}

/**

* 输出出现次数前topN的对阵情况,如果对阵图缓存量 <= topN,则输出全部对阵情况

* @param topN 截取出现次数的前topN名

* @return 全部对阵情况的字符串

*/

public String printAllAgainstPlan(int topN) {

StringBuilder builder = new StringBuilder();

Integer[] matchCountSorted = new Integer[againstPlan.size()];

int i = 0;

int totalCount = 0;

for (String opposite : againstPlan.keySet()) {

matchCountSorted[i] = againstPlan.get(opposite);

totalCount += matchCountSorted[i];

i++;

}

Arrays.sort(matchCountSorted, Collections.reverseOrder());

// 对阵情况的topN截取

Integer[] matchCountFiltered;

if (matchCountSorted.length > topN) {

matchCountFiltered = Arrays.copyOfRange(matchCountSorted, 0, topN);

} else {

matchCountFiltered = Arrays.copyOf(matchCountSorted, matchCountSorted.length);

}

List appended = new ArrayList();

for (int j = 0; j < againstPlan.size(); j++) {

appended.add(0);

}

builder.append("{ \n");

for (Integer value : matchCountFiltered) {

int index = 0;

for (Map.Entry entry : againstPlan.entrySet()) {

if (!againstPlan.get(entry.getKey()).equals(value) || appended.get(index) == 1) {

index++;

continue;

}

builder.append("\t").append("对阵情况: {\n")

.append(entry.getKey())

.append("\n\n\t\t出现次数:").append(entry.getValue())

.append("\n\t\t出现概率:").append(String.format("%.2f", 100 * entry.getValue() / (float) totalCount))

.append("%\n\t},\n");

appended.set(index, 1);

index++;

}

}

int lastSplitterIndex = builder.lastIndexOf(",\n");

if (lastSplitterIndex != -1) {

builder.delete(lastSplitterIndex, lastSplitterIndex + ",\n".length() - 1);

}

builder.append("}");

return builder.toString();

}

}

Matcher类

首先,我们要对欧冠十六强进行一轮抽签:

/**

* 对所有十六强队伍进行模拟抽签

* @param teams 所有十六强队伍

*/

public static void match(Team[] teams) {

Random random = new Random();

List unmatchedTeams = new ArrayList<>();

unmatchedTeams.addAll(Arrays.asList(teams));

for (Team unmatchedTeam : unmatchedTeams) {

if (!unmatchedTeam.hasPossibleOpposites()) {

return;

}

}

while (!unmatchedTeams.isEmpty()) {

int firstIndex = random.nextInt(unmatchedTeams.size());

// 抽第一支球队

Team firstRank = unmatchedTeams.get(firstIndex);

int secondIndex = random.nextInt(unmatchedTeams.size());

// 抽第二支球队

Team secondRank = unmatchedTeams.get(secondIndex);

if (canMatch(firstRank, secondRank, unmatchedTeams)) {

// 如果这两支球队可以完成配对,而且剩下的队伍也能配对,则完成此次配对

unmatchedTeams.remove(firstRank);

unmatchedTeams.remove(secondRank);

firstRank.setOppositeCache(secondRank).setMatched(true);

secondRank.setOppositeCache(firstRank).setMatched(true);

for (Team team : unmatchedTeams) {

team.removePossibleOpposite(firstRank).removePossibleOpposite(secondRank);

}

} else {

// 剩下未匹配的球队还是否有潜在的对手

boolean restNotMatch = false;

for (Team unmatchedTeam : unmatchedTeams) {

if (!unmatchedTeam.hasPossibleOpposites()) {

restNotMatch = true;

}

}

if (restNotMatch ||

(unmatchedTeams.isEmpty() && !canSimpleMatch(firstRank, secondRank))) {

// 剩下未匹配的球队不能完成配对,或者这两个仅剩的队伍,不能配对。

// 这种情况下,重新开始本次抽签

unmatchedTeams.clear();

unmatchedTeams.addAll(Arrays.asList(teams));

resetMatchFlag(unmatchedTeams);

}

}

}

for (Team team : teams) {

// 本次抽签完成,所有队伍将抽签结果提交

team.confirmOppositesCache();

}

// 提交本次抽签对阵图

AgainstPlan.getsInstance().confirmDrawResult(teams);

}

首先,将所有十六支球队添加到未匹配球队链表unmatchedTeams中,对每个球队进行简单的对手非空检查后,就开始本轮模拟抽签了。

在当前抽签的每次迭代中,做一下几件事:

1、从未匹配球队中随机抽两支球队(可能是同一支),判断两支球队能否完成匹配,并且匹配后其他球队还是否可以进行匹配(即存在可能的对手);

2、如果能匹配,则将两支球队从未匹配球队中删除、设置各自的第一轮淘汰赛对手为对方、设置两支球队状态为已匹配、将两支球队从其他未匹配球队的可能对手列表中移除;

3、如果不能匹配,存在两种情况:1)、抽取的两支球队不合适,但还有其他可能性;2)、在所有的未匹配球队中,存在没有潜在对手的球队,或者这是两支仅存的球队。如果是情况1,则重新进行1-2步抽签就行;如果是情况2,则认为本轮抽签失败,进行第4步。

4、重启本轮抽签,即用十六支球队重置未匹配球队链表,并重置所有球队的匹配状态。

重复1-4步骤的迭代,直到未匹配球队链表为空,则本轮抽签结束。本轮抽签结束后,向每支队伍提交抽签结果,并提交本次抽签的对阵图。

上述迭代的第一步中“判断两支球队能否完成匹配,并且匹配后其他球队还是否可以进行匹配”,对应的方法为canMatch(),如下所示

/**

* 两支球队能否配对,并且剩余的队伍能否完成配对

* @param firstTeam 第一支球队

* @param secondTeam 第二支球队

* @param allTeams 所有球队

* @return true表示能配对,false表示不能配对

*/

public static boolean canMatch(Team firstTeam, Team secondTeam, List allTeams) {

if (!canSimpleMatch(firstTeam, secondTeam)) {

// 这两支球队不能匹配,直接返回false

return false;

}

allTeams.remove(firstTeam);

allTeams.remove(secondTeam);

if (allTeams.isEmpty()) {

// 没有剩下的球队了,且这两支球队可以匹配,说明匹配完成

return true;

}

// 剩下未匹配的球队中,如果有球队没有了可能的对手,则该次匹配失败

boolean hasNoPossibilities = false;

for (Team otherTeam : allTeams) {

if (!otherTeam.isMatched() && !otherTeam.hasPossibleOpposites()

&& !otherTeam.isOnlyPossibleOpposite(firstTeam)

&& !otherTeam.isOnlyPossibleOpposite(secondTeam)) {

hasNoPossibilities = true;

break;

}

}

return !hasNoPossibilities;

}

canMatch()判断当前状态下,两支球队可以匹配的情况条件为:

1、没有其他球队,且这两支球队可以单独匹配;

2、存在其他球队,且这两支球队可以单独匹配,同时其他球队还有除了当前两支待匹配球队外的潜在对手

负责判断两支球队能否单独匹配的方法为canSimpleMatch():

/**

* 两支球队能否完成匹配

* @param firstTeam 第一支球队

* @param secondTeam 第二支球队

* @return true表示两支球队可以配对,false表示两支球队不能配对

*/

public static boolean canSimpleMatch(Team firstTeam, Team secondTeam) {

if (firstTeam == secondTeam) {

// 同一支队伍,直接回避

return false;

}

if (firstTeam.isMatched() || secondTeam.isMatched()) {

// 某一支球队已经配对,立马回避

return false;

}

if (firstTeam.getRankInGroup() == secondTeam.getRankInGroup()) {

// 只能种子队和非种子队配对

return false;

}

if (firstTeam.getGroup().equals(secondTeam.getGroup())) {

// 同组回避

return false;

}

// 同国回避

return !firstTeam.getCountry().equals(secondTeam.getCountry());

}

如果本轮抽签失败,或者需要启动下一次独立抽签,则需要重置十六强的抽签状态和当前对手:

/**

* 重置所有队伍的匹配情况

* @param teams 所有队伍

*/

public static void resetMatchFlag(Team[] teams) {

for (Team team : teams) {

team.setMatched(false).addAllPossibleOpposite(teams).unsetOppositeCache();

}

}

/**

* 重置所有队伍的匹配情况

* @param teams 所有队伍

*/

public static void resetMatchFlag(List teams) {

for (Team team : teams) {

team.setMatched(false).addAllPossibleOpposite(teams).unsetOppositeCache();

}

}

抽签类的全部代码如下所示:

package com.szc.UEFAChampionLeague;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.List;

import java.util.Random;

public class Matcher {

/**

* 对所有十六强队伍进行模拟抽签

* @param teams 所有十六强队伍

*/

public static void match(Team[] teams) {

Random random = new Random();

List unmatchedTeams = new ArrayList<>();

unmatchedTeams.addAll(Arrays.asList(teams));

for (Team unmatchedTeam : unmatchedTeams) {

if (!unmatchedTeam.hasPossibleOpposites()) {

return;

}

}

while (!unmatchedTeams.isEmpty()) {

int firstIndex = random.nextInt(unmatchedTeams.size());

// 抽第一支球队

Team firstRank = unmatchedTeams.get(firstIndex);

int secondIndex = random.nextInt(unmatchedTeams.size());

// 抽第二支球队

Team secondRank = unmatchedTeams.get(secondIndex);

if (canMatch(firstRank, secondRank, unmatchedTeams)) {

// 如果这两支球队可以完成配对,而且剩下的队伍也能配对,则完成此次配对

unmatchedTeams.remove(firstRank);

unmatchedTeams.remove(secondRank);

firstRank.setOppositeCache(secondRank).setMatched(true);

secondRank.setOppositeCache(firstRank).setMatched(true);

for (Team team : unmatchedTeams) {

team.removePossibleOpposite(firstRank).removePossibleOpposite(secondRank);

}

} else {

// 剩下未匹配的球队还是否有潜在的对手

boolean restNotMatch = false;

for (Team unmatchedTeam : unmatchedTeams) {

if (!unmatchedTeam.hasPossibleOpposites()) {

restNotMatch = true;

}

}

if (restNotMatch ||

(unmatchedTeams.isEmpty() && !canSimpleMatch(firstRank, secondRank))) {

// 剩下未匹配的球队不能完成配对,或者这两个仅剩的队伍,不能配对。

// 这种情况下,重新开始本次抽签

unmatchedTeams.clear();

unmatchedTeams.addAll(Arrays.asList(teams));

resetMatchFlag(unmatchedTeams);

}

}

}

for (Team team : teams) {

// 本次抽签完成,所有队伍将抽签结果提交

team.confirmOppositesCache();

}

// 提交本次抽签对阵图

AgainstPlan.getsInstance().confirmDrawResult(teams);

}

/**

* 重置所有队伍的匹配情况

* @param teams 所有队伍

*/

public static void resetMatchFlag(Team[] teams) {

for (Team team : teams) {

team.setMatched(false).addAllPossibleOpposite(teams).unsetOppositeCache();

}

}

/**

* 重置所有队伍的匹配情况

* @param teams 所有队伍

*/

public static void resetMatchFlag(List teams) {

for (Team team : teams) {

team.setMatched(false).addAllPossibleOpposite(teams).unsetOppositeCache();

}

}

/**

* 两支球队能否完成匹配

* @param firstTeam 第一支球队

* @param secondTeam 第二支球队

* @return true表示两支球队可以配对,false表示两支球队不能配对

*/

public static boolean canSimpleMatch(Team firstTeam, Team secondTeam) {

if (firstTeam == secondTeam) {

// 同一支队伍,直接回避

return false;

}

if (firstTeam.isMatched() || secondTeam.isMatched()) {

// 某一支球队已经配对,立马回避

return false;

}

if (firstTeam.getRankInGroup() == secondTeam.getRankInGroup()) {

// 只能种子队和非种子队配对

return false;

}

if (firstTeam.getGroup().equals(secondTeam.getGroup())) {

// 同组回避

return false;

}

// 同国回避

return !firstTeam.getCountry().equals(secondTeam.getCountry());

}

/**

* 两支球队能否配对,并且剩余的队伍能否完成配对

* @param firstTeam 第一支球队

* @param secondTeam 第二支球队

* @param allTeams 所有球队

* @return true表示能配对,false表示不能配对

*/

public static boolean canMatch(Team firstTeam, Team secondTeam, List allTeams) {

if (!canSimpleMatch(firstTeam, secondTeam)) {

// 这两支球队不能匹配,直接返回false

return false;

}

allTeams.remove(firstTeam);

allTeams.remove(secondTeam);

if (allTeams.isEmpty()) {

// 没有剩下的球队了,且这两支球队可以匹配,说明匹配完成

return true;

}

// 剩下未匹配的球队中,如果有球队没有了可能的对手,则该次匹配失败

boolean hasNoPossibilities = false;

for (Team otherTeam : allTeams) {

if (!otherTeam.isMatched() && !otherTeam.hasPossibleOpposites()

&& !otherTeam.isOnlyPossibleOpposite(firstTeam)

&& !otherTeam.isOnlyPossibleOpposite(secondTeam)) {

hasNoPossibilities = true;

break;

}

}

return !hasNoPossibilities;

}

}

UEFAChampionLeagueDraw类

这个类是入口类,负责三件事:

1、初始化十六强数据;

2、启动3000次独立抽签;

3、输出统计结果。

初始化十六强数据,就是为十六强输入名字等基础信息:

Team[] teams = new Team[16];

teams[0] = setTeam("曼城", "England", "A", 1);

teams[1] = setTeam("巴黎圣日尔曼", "France", "A", 2);

teams[2] = setTeam("利物浦", "England", "B", 1);

teams[3] = setTeam("马德里竞技", "Spain", "B", 2);

teams[4] = setTeam("阿贾克斯", "Netherlands", "C", 1);

teams[5] = setTeam("葡萄牙体育", "Portugal", "C", 2);

teams[6] = setTeam("皇家马德里", "Spain", "D", 1);

teams[7] = setTeam("国际米兰", "Italy", "D", 2);

teams[8] = setTeam("拜仁", "Germany", "E", 1);

teams[9] = setTeam("本菲卡", "Portugal", "E", 2);

teams[10] = setTeam("曼联", "England", "F", 1);

teams[11] = setTeam("比利亚雷亚尔", "Spain", "F", 2);

teams[12] = setTeam("里尔", "France", "G", 1);

teams[13] = setTeam("萨尔茨堡红牛", "Austria", "G", 2);

teams[14] = setTeam("尤文图斯", "Italy", "H", 1);

teams[15] = setTeam("切尔西", "England", "H", 2);

setTeam()是对球队信息初始化的一层封装:

/**

* 初始化一支球队的信息,并将该球队返回

* @param name 球队名称

* @param country 球队所在国家

* @param group 球队所在小组

* @param rankInGroup 球队组内排名

* @return 球队对象

*/

public static Team setTeam(String name, String country, String group, int rankInGroup) {

Team team = new Team();

team.setName(name)

.setCountry(country)

.setGroup(group)

.setRankInGroup(rankInGroup);

return team;

}

然后,进行抽签实验,并输出结果:

/**

* 对欧冠十六强进行抽签实验

* @param teams 欧冠十六强

*/

private static void drawMatches(Team[] teams) {

Matcher.resetMatchFlag(teams);

for (int i = 0; i < 3000; i++) {

// 开始3000次独立抽签

Matcher.match(teams);

Matcher.resetMatchFlag(teams);

}

for (Team team : teams) {

System.out.println(team.getName() + ": " + team.printOppositePossibilities());

team.clearAll();

}

System.out.println(AgainstPlan.getsInstance().printAllAgainstPlan());

}

UEFAChampionLeagueDraw类全部代码如下所示:

package com.szc.UEFAChampionLeague;

public class UEFAChampionLeagueDraw {

public static void main(String[] args) {

Team[] teams = new Team[16];

teams[0] = setTeam("曼城", "England", "A", 1);

teams[1] = setTeam("巴黎圣日尔曼", "France", "A", 2);

teams[2] = setTeam("利物浦", "England", "B", 1);

teams[3] = setTeam("马德里竞技", "Spain", "B", 2);

teams[4] = setTeam("阿贾克斯", "Netherlands", "C", 1);

teams[5] = setTeam("葡萄牙体育", "Portugal", "C", 2);

teams[6] = setTeam("皇家马德里", "Spain", "D", 1);

teams[7] = setTeam("国际米兰", "Italy", "D", 2);

teams[8] = setTeam("拜仁", "Germany", "E", 1);

teams[9] = setTeam("本菲卡", "Portugal", "E", 2);

teams[10] = setTeam("曼联", "England", "F", 1);

teams[11] = setTeam("比利亚雷亚尔", "Spain", "F", 2);

teams[12] = setTeam("里尔", "France", "G", 1);

teams[13] = setTeam("萨尔茨堡红牛", "Austria", "G", 2);

teams[14] = setTeam("尤文图斯", "Italy", "H", 1);

teams[15] = setTeam("切尔西", "England", "H", 2);

drawMatches(teams);

}

/**

* 对欧冠十六强进行抽签实验

* @param teams 欧冠十六强

*/

private static void drawMatches(Team[] teams) {

Matcher.resetMatchFlag(teams);

for (int i = 0; i < 3000; i++) {

// 开始3000次独立抽签

Matcher.match(teams);

Matcher.resetMatchFlag(teams);

}

for (Team team : teams) {

System.out.println(team.getName() + ": " + team.printOppositePossibilities());

team.clearAll();

}

System.out.println(AgainstPlan.getsInstance().printAllAgainstPlan());

}

/**

* 初始化一支球队的信息,并将该球队返回

* @param name 球队名称

* @param country 球队所在国家

* @param group 球队所在小组

* @param rankInGroup 球队组内排名

* @return 球队对象

*/

public static Team setTeam(String name, String country, String group, int rankInGroup) {

Team team = new Team();

team.setName(name)

.setCountry(country)

.setGroup(group)

.setRankInGroup(rankInGroup);

return team;

}

}

运行结果

某次的运行输出结果如下所示:

曼城: {

国际米兰: 遭遇次数:575 遭遇频率:19.17%,

马德里竞技: 遭遇次数:550 遭遇频率:18.33%,

比利亚雷亚尔: 遭遇次数:548 遭遇频率:18.27%,

萨尔茨堡红牛: 遭遇次数:449 遭遇频率:14.97%,

葡萄牙体育: 遭遇次数:440 遭遇频率:14.67%,

本菲卡: 遭遇次数:438 遭遇频率:14.60%

}

巴黎圣日尔曼: {

皇家马德里: 遭遇次数:570 遭遇频率:19.00%,

利物浦: 遭遇次数:541 遭遇频率:18.03%,

尤文图斯: 遭遇次数:534 遭遇频率:17.80%,

曼联: 遭遇次数:525 遭遇频率:17.50%,

阿贾克斯: 遭遇次数:433 遭遇频率:14.43%,

拜仁: 遭遇次数:397 遭遇频率:13.23%

}

利物浦: {

比利亚雷亚尔: 遭遇次数:593 遭遇频率:19.77%,

巴黎圣日尔曼: 遭遇次数:541 遭遇频率:18.03%,

国际米兰: 遭遇次数:531 遭遇频率:17.70%,

本菲卡: 遭遇次数:448 遭遇频率:14.93%,

萨尔茨堡红牛: 遭遇次数:447 遭遇频率:14.90%,

葡萄牙体育: 遭遇次数:440 遭遇频率:14.67%

}

马德里竞技: {

曼城: 遭遇次数:550 遭遇频率:18.33%,

曼联: 遭遇次数:549 遭遇频率:18.30%,

里尔: 遭遇次数:527 遭遇频率:17.57%,

尤文图斯: 遭遇次数:526 遭遇频率:17.53%,

阿贾克斯: 遭遇次数:445 遭遇频率:14.83%,

拜仁: 遭遇次数:403 遭遇频率:13.43%

}

阿贾克斯: {

切尔西: 遭遇次数:655 遭遇频率:21.83%,

马德里竞技: 遭遇次数:445 遭遇频率:14.83%,

巴黎圣日尔曼: 遭遇次数:433 遭遇频率:14.43%,

国际米兰: 遭遇次数:393 遭遇频率:13.10%,

比利亚雷亚尔: 遭遇次数:378 遭遇频率:12.60%,

本菲卡: 遭遇次数:357 遭遇频率:11.90%,

萨尔茨堡红牛: 遭遇次数:339 遭遇频率:11.30%

}

葡萄牙体育: {

尤文图斯: 遭遇次数:462 遭遇频率:15.40%,

曼联: 遭遇次数:456 遭遇频率:15.20%,

皇家马德里: 遭遇次数:456 遭遇频率:15.20%,

利物浦: 遭遇次数:440 遭遇频率:14.67%,

曼城: 遭遇次数:440 遭遇频率:14.67%,

里尔: 遭遇次数:389 遭遇频率:12.97%,

拜仁: 遭遇次数:357 遭遇频率:11.90%

}

皇家马德里: {

切尔西: 遭遇次数:1023 遭遇频率:34.10%,

巴黎圣日尔曼: 遭遇次数:570 遭遇频率:19.00%,

萨尔茨堡红牛: 遭遇次数:489 遭遇频率:16.30%,

本菲卡: 遭遇次数:462 遭遇频率:15.40%,

葡萄牙体育: 遭遇次数:456 遭遇频率:15.20%

}

国际米兰: {

曼联: 遭遇次数:576 遭遇频率:19.20%,

曼城: 遭遇次数:575 遭遇频率:19.17%,

利物浦: 遭遇次数:531 遭遇频率:17.70%,

里尔: 遭遇次数:472 遭遇频率:15.73%,

拜仁: 遭遇次数:453 遭遇频率:15.10%,

阿贾克斯: 遭遇次数:393 遭遇频率:13.10%

}

拜仁: {

切尔西: 遭遇次数:606 遭遇频率:20.20%,

国际米兰: 遭遇次数:453 遭遇频率:15.10%,

比利亚雷亚尔: 遭遇次数:414 遭遇频率:13.80%,

马德里竞技: 遭遇次数:403 遭遇频率:13.43%,

巴黎圣日尔曼: 遭遇次数:397 遭遇频率:13.23%,

萨尔茨堡红牛: 遭遇次数:370 遭遇频率:12.33%,

葡萄牙体育: 遭遇次数:357 遭遇频率:11.90%

}

本菲卡: {

皇家马德里: 遭遇次数:462 遭遇频率:15.40%,

曼联: 遭遇次数:449 遭遇频率:14.97%,

利物浦: 遭遇次数:448 遭遇频率:14.93%,

尤文图斯: 遭遇次数:443 遭遇频率:14.77%,

曼城: 遭遇次数:438 遭遇频率:14.60%,

里尔: 遭遇次数:403 遭遇频率:13.43%,

阿贾克斯: 遭遇次数:357 遭遇频率:11.90%

}

曼联: {

国际米兰: 遭遇次数:576 遭遇频率:19.20%,

马德里竞技: 遭遇次数:549 遭遇频率:18.30%,

巴黎圣日尔曼: 遭遇次数:525 遭遇频率:17.50%,

葡萄牙体育: 遭遇次数:456 遭遇频率:15.20%,

本菲卡: 遭遇次数:449 遭遇频率:14.97%,

萨尔茨堡红牛: 遭遇次数:445 遭遇频率:14.83%

}

比利亚雷亚尔: {

利物浦: 遭遇次数:593 遭遇频率:19.77%,

尤文图斯: 遭遇次数:574 遭遇频率:19.13%,

曼城: 遭遇次数:548 遭遇频率:18.27%,

里尔: 遭遇次数:493 遭遇频率:16.43%,

拜仁: 遭遇次数:414 遭遇频率:13.80%,

阿贾克斯: 遭遇次数:378 遭遇频率:12.60%

}

里尔: {

切尔西: 遭遇次数:716 遭遇频率:23.87%,

马德里竞技: 遭遇次数:527 遭遇频率:17.57%,

比利亚雷亚尔: 遭遇次数:493 遭遇频率:16.43%,

国际米兰: 遭遇次数:472 遭遇频率:15.73%,

本菲卡: 遭遇次数:403 遭遇频率:13.43%,

葡萄牙体育: 遭遇次数:389 遭遇频率:12.97%

}

萨尔茨堡红牛: {

皇家马德里: 遭遇次数:489 遭遇频率:16.30%,

尤文图斯: 遭遇次数:461 遭遇频率:15.37%,

曼城: 遭遇次数:449 遭遇频率:14.97%,

利物浦: 遭遇次数:447 遭遇频率:14.90%,

曼联: 遭遇次数:445 遭遇频率:14.83%,

拜仁: 遭遇次数:370 遭遇频率:12.33%,

阿贾克斯: 遭遇次数:339 遭遇频率:11.30%

}

尤文图斯: {

比利亚雷亚尔: 遭遇次数:574 遭遇频率:19.13%,

巴黎圣日尔曼: 遭遇次数:534 遭遇频率:17.80%,

马德里竞技: 遭遇次数:526 遭遇频率:17.53%,

葡萄牙体育: 遭遇次数:462 遭遇频率:15.40%,

萨尔茨堡红牛: 遭遇次数:461 遭遇频率:15.37%,

本菲卡: 遭遇次数:443 遭遇频率:14.77%

}

切尔西: {

皇家马德里: 遭遇次数:1023 遭遇频率:34.10%,

里尔: 遭遇次数:716 遭遇频率:23.87%,

阿贾克斯: 遭遇次数:655 遭遇频率:21.83%,

拜仁: 遭遇次数:606 遭遇频率:20.20%

}

{

对阵情况: {

巴黎圣日尔曼 vs 拜仁

马德里竞技 vs 里尔

葡萄牙体育 vs 曼联

国际米兰 vs 阿贾克斯

本菲卡 vs 曼城

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 尤文图斯

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

},

对阵情况: {

巴黎圣日尔曼 vs 拜仁

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 尤文图斯

国际米兰 vs 里尔

本菲卡 vs 曼联

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 曼城

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 曼联

葡萄牙体育 vs 曼城

国际米兰 vs 利物浦

本菲卡 vs 里尔

比利亚雷亚尔 vs 阿贾克斯

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 拜仁

葡萄牙体育 vs 曼联

国际米兰 vs 阿贾克斯

本菲卡 vs 利物浦

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 皇家马德里

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 拜仁

葡萄牙体育 vs 利物浦

国际米兰 vs 曼城

本菲卡 vs 皇家马德里

比利亚雷亚尔 vs 阿贾克斯

萨尔茨堡红牛 vs 曼联

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 皇家马德里

马德里竞技 vs 拜仁

葡萄牙体育 vs 利物浦

国际米兰 vs 阿贾克斯

本菲卡 vs 曼城

比利亚雷亚尔 vs 尤文图斯

萨尔茨堡红牛 vs 曼联

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 里尔

葡萄牙体育 vs 利物浦

国际米兰 vs 拜仁

本菲卡 vs 皇家马德里

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 曼联

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 曼联

马德里竞技 vs 尤文图斯

葡萄牙体育 vs 利物浦

国际米兰 vs 阿贾克斯

本菲卡 vs 曼城

比利亚雷亚尔 vs 里尔

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 曼联

葡萄牙体育 vs 曼城

国际米兰 vs 阿贾克斯

本菲卡 vs 里尔

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 曼联

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 利物浦

国际米兰 vs 里尔

本菲卡 vs 尤文图斯

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 皇家马德里

切尔西 vs 拜仁

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 皇家马德里

马德里竞技 vs 曼城

葡萄牙体育 vs 尤文图斯

国际米兰 vs 曼联

本菲卡 vs 里尔

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 阿贾克斯

切尔西 vs 拜仁

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 皇家马德里

马德里竞技 vs 拜仁

葡萄牙体育 vs 里尔

国际米兰 vs 曼联

本菲卡 vs 曼城

比利亚雷亚尔 vs 尤文图斯

萨尔茨堡红牛 vs 利物浦

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 里尔

国际米兰 vs 拜仁

本菲卡 vs 曼城

比利亚雷亚尔 vs 尤文图斯

萨尔茨堡红牛 vs 曼联

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 里尔

葡萄牙体育 vs 尤文图斯

国际米兰 vs 拜仁

本菲卡 vs 皇家马德里

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 曼联

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 尤文图斯

葡萄牙体育 vs 皇家马德里

国际米兰 vs 曼城

本菲卡 vs 曼联

比利亚雷亚尔 vs 拜仁

萨尔茨堡红牛 vs 阿贾克斯

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 皇家马德里

马德里竞技 vs 拜仁

葡萄牙体育 vs 曼联

国际米兰 vs 利物浦

本菲卡 vs 阿贾克斯

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 尤文图斯

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 拜仁

葡萄牙体育 vs 皇家马德里

国际米兰 vs 曼联

本菲卡 vs 里尔

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 尤文图斯

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 拜仁

马德里竞技 vs 曼联

葡萄牙体育 vs 利物浦

国际米兰 vs 里尔

本菲卡 vs 尤文图斯

比利亚雷亚尔 vs 阿贾克斯

萨尔茨堡红牛 vs 曼城

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 尤文图斯

葡萄牙体育 vs 拜仁

国际米兰 vs 曼联

本菲卡 vs 皇家马德里

比利亚雷亚尔 vs 里尔

萨尔茨堡红牛 vs 曼城

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 曼城

国际米兰 vs 曼联

本菲卡 vs 利物浦

比利亚雷亚尔 vs 里尔

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 曼联

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 里尔

国际米兰 vs 曼城

本菲卡 vs 利物浦

比利亚雷亚尔 vs 尤文图斯

萨尔茨堡红牛 vs 皇家马德里

切尔西 vs 拜仁

出现次数:4

出现概率:0.13%

}

}

其中每支球队的所有可能对手的相遇情况统计为:

曼城: {

国际米兰: 遭遇次数:575 遭遇频率:19.17%,

马德里竞技: 遭遇次数:550 遭遇频率:18.33%,

比利亚雷亚尔: 遭遇次数:548 遭遇频率:18.27%,

萨尔茨堡红牛: 遭遇次数:449 遭遇频率:14.97%,

葡萄牙体育: 遭遇次数:440 遭遇频率:14.67%,

本菲卡: 遭遇次数:438 遭遇频率:14.60%

}

巴黎圣日尔曼: {

皇家马德里: 遭遇次数:570 遭遇频率:19.00%,

利物浦: 遭遇次数:541 遭遇频率:18.03%,

尤文图斯: 遭遇次数:534 遭遇频率:17.80%,

曼联: 遭遇次数:525 遭遇频率:17.50%,

阿贾克斯: 遭遇次数:433 遭遇频率:14.43%,

拜仁: 遭遇次数:397 遭遇频率:13.23%

}

利物浦: {

比利亚雷亚尔: 遭遇次数:593 遭遇频率:19.77%,

巴黎圣日尔曼: 遭遇次数:541 遭遇频率:18.03%,

国际米兰: 遭遇次数:531 遭遇频率:17.70%,

本菲卡: 遭遇次数:448 遭遇频率:14.93%,

萨尔茨堡红牛: 遭遇次数:447 遭遇频率:14.90%,

葡萄牙体育: 遭遇次数:440 遭遇频率:14.67%

}

马德里竞技: {

曼城: 遭遇次数:550 遭遇频率:18.33%,

曼联: 遭遇次数:549 遭遇频率:18.30%,

里尔: 遭遇次数:527 遭遇频率:17.57%,

尤文图斯: 遭遇次数:526 遭遇频率:17.53%,

阿贾克斯: 遭遇次数:445 遭遇频率:14.83%,

拜仁: 遭遇次数:403 遭遇频率:13.43%

}

阿贾克斯: {

切尔西: 遭遇次数:655 遭遇频率:21.83%,

马德里竞技: 遭遇次数:445 遭遇频率:14.83%,

巴黎圣日尔曼: 遭遇次数:433 遭遇频率:14.43%,

国际米兰: 遭遇次数:393 遭遇频率:13.10%,

比利亚雷亚尔: 遭遇次数:378 遭遇频率:12.60%,

本菲卡: 遭遇次数:357 遭遇频率:11.90%,

萨尔茨堡红牛: 遭遇次数:339 遭遇频率:11.30%

}

葡萄牙体育: {

尤文图斯: 遭遇次数:462 遭遇频率:15.40%,

曼联: 遭遇次数:456 遭遇频率:15.20%,

皇家马德里: 遭遇次数:456 遭遇频率:15.20%,

利物浦: 遭遇次数:440 遭遇频率:14.67%,

曼城: 遭遇次数:440 遭遇频率:14.67%,

里尔: 遭遇次数:389 遭遇频率:12.97%,

拜仁: 遭遇次数:357 遭遇频率:11.90%

}

皇家马德里: {

切尔西: 遭遇次数:1023 遭遇频率:34.10%,

巴黎圣日尔曼: 遭遇次数:570 遭遇频率:19.00%,

萨尔茨堡红牛: 遭遇次数:489 遭遇频率:16.30%,

本菲卡: 遭遇次数:462 遭遇频率:15.40%,

葡萄牙体育: 遭遇次数:456 遭遇频率:15.20%

}

国际米兰: {

曼联: 遭遇次数:576 遭遇频率:19.20%,

曼城: 遭遇次数:575 遭遇频率:19.17%,

利物浦: 遭遇次数:531 遭遇频率:17.70%,

里尔: 遭遇次数:472 遭遇频率:15.73%,

拜仁: 遭遇次数:453 遭遇频率:15.10%,

阿贾克斯: 遭遇次数:393 遭遇频率:13.10%

}

拜仁: {

切尔西: 遭遇次数:606 遭遇频率:20.20%,

国际米兰: 遭遇次数:453 遭遇频率:15.10%,

比利亚雷亚尔: 遭遇次数:414 遭遇频率:13.80%,

马德里竞技: 遭遇次数:403 遭遇频率:13.43%,

巴黎圣日尔曼: 遭遇次数:397 遭遇频率:13.23%,

萨尔茨堡红牛: 遭遇次数:370 遭遇频率:12.33%,

葡萄牙体育: 遭遇次数:357 遭遇频率:11.90%

}

本菲卡: {

皇家马德里: 遭遇次数:462 遭遇频率:15.40%,

曼联: 遭遇次数:449 遭遇频率:14.97%,

利物浦: 遭遇次数:448 遭遇频率:14.93%,

尤文图斯: 遭遇次数:443 遭遇频率:14.77%,

曼城: 遭遇次数:438 遭遇频率:14.60%,

里尔: 遭遇次数:403 遭遇频率:13.43%,

阿贾克斯: 遭遇次数:357 遭遇频率:11.90%

}

曼联: {

国际米兰: 遭遇次数:576 遭遇频率:19.20%,

马德里竞技: 遭遇次数:549 遭遇频率:18.30%,

巴黎圣日尔曼: 遭遇次数:525 遭遇频率:17.50%,

葡萄牙体育: 遭遇次数:456 遭遇频率:15.20%,

本菲卡: 遭遇次数:449 遭遇频率:14.97%,

萨尔茨堡红牛: 遭遇次数:445 遭遇频率:14.83%

}

比利亚雷亚尔: {

利物浦: 遭遇次数:593 遭遇频率:19.77%,

尤文图斯: 遭遇次数:574 遭遇频率:19.13%,

曼城: 遭遇次数:548 遭遇频率:18.27%,

里尔: 遭遇次数:493 遭遇频率:16.43%,

拜仁: 遭遇次数:414 遭遇频率:13.80%,

阿贾克斯: 遭遇次数:378 遭遇频率:12.60%

}

里尔: {

切尔西: 遭遇次数:716 遭遇频率:23.87%,

马德里竞技: 遭遇次数:527 遭遇频率:17.57%,

比利亚雷亚尔: 遭遇次数:493 遭遇频率:16.43%,

国际米兰: 遭遇次数:472 遭遇频率:15.73%,

本菲卡: 遭遇次数:403 遭遇频率:13.43%,

葡萄牙体育: 遭遇次数:389 遭遇频率:12.97%

}

萨尔茨堡红牛: {

皇家马德里: 遭遇次数:489 遭遇频率:16.30%,

尤文图斯: 遭遇次数:461 遭遇频率:15.37%,

曼城: 遭遇次数:449 遭遇频率:14.97%,

利物浦: 遭遇次数:447 遭遇频率:14.90%,

曼联: 遭遇次数:445 遭遇频率:14.83%,

拜仁: 遭遇次数:370 遭遇频率:12.33%,

阿贾克斯: 遭遇次数:339 遭遇频率:11.30%

}

尤文图斯: {

比利亚雷亚尔: 遭遇次数:574 遭遇频率:19.13%,

巴黎圣日尔曼: 遭遇次数:534 遭遇频率:17.80%,

马德里竞技: 遭遇次数:526 遭遇频率:17.53%,

葡萄牙体育: 遭遇次数:462 遭遇频率:15.40%,

萨尔茨堡红牛: 遭遇次数:461 遭遇频率:15.37%,

本菲卡: 遭遇次数:443 遭遇频率:14.77%

}

切尔西: {

皇家马德里: 遭遇次数:1023 遭遇频率:34.10%,

里尔: 遭遇次数:716 遭遇频率:23.87%,

阿贾克斯: 遭遇次数:655 遭遇频率:21.83%,

拜仁: 遭遇次数:606 遭遇频率:20.20%

}

看来,单从单独的球队来看,电脑很希望切尔西和皇马再次相遇啊,上赛季蓝军就是淘汰的皇马闯进的决赛。从拜仁的角度上看,拜仁遇到老对手切尔西的概率是所有对手中最高的,但在切尔西的四支潜在对手中,遭遇南大王的概率却是最低的。

我们看一下整体的对阵图情况:

{

对阵情况: {

巴黎圣日尔曼 vs 拜仁

马德里竞技 vs 里尔

葡萄牙体育 vs 曼联

国际米兰 vs 阿贾克斯

本菲卡 vs 曼城

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 尤文图斯

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

},

对阵情况: {

巴黎圣日尔曼 vs 拜仁

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 尤文图斯

国际米兰 vs 里尔

本菲卡 vs 曼联

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 曼城

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 曼联

葡萄牙体育 vs 曼城

国际米兰 vs 利物浦

本菲卡 vs 里尔

比利亚雷亚尔 vs 阿贾克斯

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 拜仁

葡萄牙体育 vs 曼联

国际米兰 vs 阿贾克斯

本菲卡 vs 利物浦

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 皇家马德里

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 拜仁

葡萄牙体育 vs 利物浦

国际米兰 vs 曼城

本菲卡 vs 皇家马德里

比利亚雷亚尔 vs 阿贾克斯

萨尔茨堡红牛 vs 曼联

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 皇家马德里

马德里竞技 vs 拜仁

葡萄牙体育 vs 利物浦

国际米兰 vs 阿贾克斯

本菲卡 vs 曼城

比利亚雷亚尔 vs 尤文图斯

萨尔茨堡红牛 vs 曼联

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 里尔

葡萄牙体育 vs 利物浦

国际米兰 vs 拜仁

本菲卡 vs 皇家马德里

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 曼联

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 曼联

马德里竞技 vs 尤文图斯

葡萄牙体育 vs 利物浦

国际米兰 vs 阿贾克斯

本菲卡 vs 曼城

比利亚雷亚尔 vs 里尔

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 曼联

葡萄牙体育 vs 曼城

国际米兰 vs 阿贾克斯

本菲卡 vs 里尔

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 曼联

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 利物浦

国际米兰 vs 里尔

本菲卡 vs 尤文图斯

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 皇家马德里

切尔西 vs 拜仁

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 皇家马德里

马德里竞技 vs 曼城

葡萄牙体育 vs 尤文图斯

国际米兰 vs 曼联

本菲卡 vs 里尔

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 阿贾克斯

切尔西 vs 拜仁

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 皇家马德里

马德里竞技 vs 拜仁

葡萄牙体育 vs 里尔

国际米兰 vs 曼联

本菲卡 vs 曼城

比利亚雷亚尔 vs 尤文图斯

萨尔茨堡红牛 vs 利物浦

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 里尔

国际米兰 vs 拜仁

本菲卡 vs 曼城

比利亚雷亚尔 vs 尤文图斯

萨尔茨堡红牛 vs 曼联

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 里尔

葡萄牙体育 vs 尤文图斯

国际米兰 vs 拜仁

本菲卡 vs 皇家马德里

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 曼联

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 尤文图斯

葡萄牙体育 vs 皇家马德里

国际米兰 vs 曼城

本菲卡 vs 曼联

比利亚雷亚尔 vs 拜仁

萨尔茨堡红牛 vs 阿贾克斯

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 皇家马德里

马德里竞技 vs 拜仁

葡萄牙体育 vs 曼联

国际米兰 vs 利物浦

本菲卡 vs 阿贾克斯

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 尤文图斯

切尔西 vs 里尔

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 拜仁

葡萄牙体育 vs 皇家马德里

国际米兰 vs 曼联

本菲卡 vs 里尔

比利亚雷亚尔 vs 曼城

萨尔茨堡红牛 vs 尤文图斯

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 拜仁

马德里竞技 vs 曼联

葡萄牙体育 vs 利物浦

国际米兰 vs 里尔

本菲卡 vs 尤文图斯

比利亚雷亚尔 vs 阿贾克斯

萨尔茨堡红牛 vs 曼城

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 利物浦

马德里竞技 vs 尤文图斯

葡萄牙体育 vs 拜仁

国际米兰 vs 曼联

本菲卡 vs 皇家马德里

比利亚雷亚尔 vs 里尔

萨尔茨堡红牛 vs 曼城

切尔西 vs 阿贾克斯

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 曼城

国际米兰 vs 曼联

本菲卡 vs 利物浦

比利亚雷亚尔 vs 里尔

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:4

出现概率:0.13%

},

对阵情况: {

巴黎圣日尔曼 vs 曼联

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 里尔

国际米兰 vs 曼城

本菲卡 vs 利物浦

比利亚雷亚尔 vs 尤文图斯

萨尔茨堡红牛 vs 皇家马德里

切尔西 vs 拜仁

出现次数:4

出现概率:0.13%

}

}

三个出现次数为5的概率最高对阵图如下所示:

对阵情况: {

巴黎圣日尔曼 vs 拜仁

马德里竞技 vs 里尔

葡萄牙体育 vs 曼联

国际米兰 vs 阿贾克斯

本菲卡 vs 曼城

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 尤文图斯

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

},

对阵情况: {

巴黎圣日尔曼 vs 拜仁

马德里竞技 vs 阿贾克斯

葡萄牙体育 vs 尤文图斯

国际米兰 vs 里尔

本菲卡 vs 曼联

比利亚雷亚尔 vs 利物浦

萨尔茨堡红牛 vs 曼城

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

},

对阵情况: {

巴黎圣日尔曼 vs 尤文图斯

马德里竞技 vs 曼联

葡萄牙体育 vs 曼城

国际米兰 vs 利物浦

本菲卡 vs 里尔

比利亚雷亚尔 vs 阿贾克斯

萨尔茨堡红牛 vs 拜仁

切尔西 vs 皇家马德里

出现次数:5

出现概率:0.17%

}

居然三种对阵中切尔西的对手都是皇马,而梅西所在的巴黎签运也很差,两次拜仁一次尤文,虽然尤文上半赛季在联赛中踢得实在一般。

结语

本项目纯属娱乐,实际情况中,我仁得运气还不错,抽到了实力相对较弱得萨尔茨堡,皇马遇到了梅西、拉莫斯所在的大巴黎,马竞遇到了C罗所在的曼联,红军国米相遇,也是剧情效果不错。