针对欧冠16进8的抽签过程,我主要有以下几个问题:
1,一共有多少种可能的对阵?
2,两次抽签抽出相同的对阵概率有多大(多小)?
3,每种单场对阵(比方说米兰vs巴萨,皇马vs曼联)出现的概率有多少?
4,到底有没有黑幕?
先解决多少种对阵的问题。基本思路是八皇后。不同的是验证位置是否合法有效的方法。八皇后是要避免竖斜方向,抽签要避免同小组,同足协,和竖向(抽了同一支球队)。好比在一个破棋盘上摆“车”。如图就是今年的抽签结果。
画图画的,见谅。车迷不哭么么哒。
代码如下。八皇后+位运算
#define PAIR 8 //16 teams
#define FUAI 0
enum association{ITA,ESP,ENG,GER,FRA,POR,SCO,UKR,TUR}; //football associations
static const enum association as_st_1[PAIR] = {FRA,GER,ESP,GER,ITA,GER,ESP,ENG}; //standing 1, group A to H
static const enum association as_st_2[PAIR] = {POR,ENG,ITA,ESP,UKR,ESP,SCO,TUR}; //standing 2, group A to H
static char versus[PAIR] = {0};
static int count = 0; //# of possible results
int isvalid(int round, char pos){//check if the pos(ition) is valid
if((round!=pos)&&(as_st_2[round]!=as_st_1[pos])){
int i;
char c = 0;
for(i=0;i<round;i++){
c = c|versus[i];
}
return (c!=(c|(1<<(PAIR-1-pos))));
}
return 0;
}
void draw_once(int round){
int pos;
for(pos=0;pos<PAIR;pos++){
if(isvalid(round,pos)){
versus[round] = (0|(1<<(PAIR-1-pos)));
if(round == PAIR-1){
count++;
versus[round]=0;
return;
}
draw_once(round+1);
versus[round]=0;
}
}
}
void draw(){ //封装用的
draw_once(0);
}
void print_available_versus(){
int j;
char k;
for(j=0;j<PAIR;j++){
for(k=0;k<PAIR;k++){
if(isvalid(j,k)!=0){
printf("YES\t");
}else{
printf("NO\t");
}
}
printf("\n");
}
}
void main(){
if(FUAI){
printf("Milan vs Barca, RM vs ManU, qita suibian!\n");
}else{
print_available_versus();
draw();
printf("%d\n",count);
}
}
P.S. 我是不会告诉你们代码里有彩蛋的。不信你把第二行的0换成1试试。
gcc 4.6.1跑结果如下:
第一个8*8矩阵是抽签前所有可能的单场对阵。从上到下第1到8行分别代表A-H组的8个小组第二,从左到右是8个小组第一。蛋疼的同学可以验证,欧足联官网欢迎您。
5463就是所有可能的对阵了。所以你想欧组联能彩排和正式抽出一样的结果,这是什么人品。
再算每一场出现的概率。
每一场出现的概率我们可以通过累加5463种抽签对阵,统计其中单场出现的次数,除以5463得出。在这里,为了增加难度,我准备研究一下全部的抽签过程。
如果对比真实的抽签过程,前面这段代码是按照小组第二从A到H的顺序来的。真实的抽签过程可以参考(屌丝的节目 误)球迷的节日足球之夜节目转播的抽签过程(这儿下的)。
过程是这样的。
老熟人光头男因凡帝诺负责抽小组第二。给丫一个特写。
前利物浦和皇马巨星(以下简称巨星,麦克马纳曼太长了)负责抽小组第一,就是下图左一笑得很开心的那个,右一是(个小喽罗 误)欧组联竞赛总监马切蒂(according to 女主持人)。
具体过程是,光头男先抽出一个小组第二,然后马切蒂找出可能的小组第一,(巨星说快到我碗里来 误)放到巨星面前的碗里,再由巨星来抽出一个。直到全部八场对阵产生为止。
我准备先模拟全部出抽小组第二的过程,再模拟出所有可能对阵。
全部可能的小组第二抽签顺序这种问题让人菊花一紧,没错,就是您的老朋友面试的好基友全排列算法。好了少年你可以去google代码了。下面的是我(google的 误)写的(其实也还是google了一下的)。用的递归+邻位对换。
代码:
#define PAIR 8 //16 teams
enum association{ITA,ESP,ENG,GER,FRA,POR,SCO,UKR,TUR}; //football associations
static const enum association as_st_1[PAIR] = {FRA,GER,ESP,GER,ITA,GER,ESP,ENG}; //standing 1, group A to H
static const enum association as_st_2[PAIR] = {POR,ENG,ITA,ESP,UKR,ESP,SCO,TUR}; //standing 2, group A to H
static char versus[PAIR] = {0};
static int count = 0; //# of possible results
//static int st_2[PAIR] = {0,1,2,3,4,5,6,7};
static int st_2[PAIR] = {7,6,1,4,2,3,5,0}; //this year order
static int acc[PAIR][PAIR] = {0};
void swap(int* a, int* b){
int temp = *a;
*a = *b;
*b = temp;
return;
}
int isvalid(int round, char pos){//check if the pos(ition) is valid
char c = 0;
if((round!=pos)&&(as_st_2[round]!=as_st_1[pos])){
int i;
for(i=0;i<PAIR;i++){
c = c|versus[i];
}
return (c!=(c|(1<<(PAIR-1-pos))));
}
return 0;
}
void acc_result(){
int i,j;
for(i=0;i<PAIR;i++){
for(j=0;j<PAIR;j++){
acc[i][j] += ((versus[i]>>(PAIR-1-j))&1);
}
}
}
void draw_once(int round){
int pos;
int team = st_2[round];
for(pos=0;pos<PAIR;pos++){
if(isvalid(team,pos)){
versus[team] = (0|(1<<(PAIR-1-pos)));
if(round == PAIR-1){
count++;
acc_result();
versus[team]=0;
return;
}
draw_once(round+1);
versus[team]=0;
}
}
}
void draw(){
draw_once(0);
}
void print_perm(){
int i;
for(i=0;i<PAIR;i++){
printf("%d\t",st_2[i]);
}
printf("\n");
}
void perm(int m, int n){
if(m>n){
draw();
return;
}else{
int i;
for(i=m;i<=n;i++){
swap(&st_2[i],&st_2[m]);
perm(m+1, n);
swap(&st_2[i],&st_2[m]);
}
}
}
void perm_draw(){ //封装
perm(0,PAIR-1);
}
void print_available_versus(){
int j;
char k;
for(j=0;j<PAIR;j++){
for(k=0;k<PAIR;k++){
if(isvalid(j,k)!=0){
printf("YES\t");
}else{
printf("NO\t");
}
}
printf("\n");
}
}
void print_acc(){
int i,j;
int sum;
for(i=0;i<PAIR;i++){
sum = 0;
for(j=0;j<PAIR;j++){
sum += acc[i][j];
}
for(j=0;j<PAIR;j++){
printf("%d\t",acc[i][j]);
}
printf("\n");
}
}
void print_acc_per(){
int i,j;
int sum;
for(i=0;i<PAIR;i++){
sum = 0;
for(j=0;j<PAIR;j++){
sum += acc[i][j];
}
for(j=0;j<PAIR;j++){
printf("%2.1f\t",((float)acc[i][j]/sum)*100);
}
printf("\n");
}
}
void main(){
printf("\n");
print_available_versus();
perm_draw();
//draw();
printf("%d\n\n",count);
print_acc();
printf("\n");
print_acc_per();
}
P.S.这里面把彩蛋去掉了。刚才给彩蛋好评的亲请自行DIY。
最后的main里面,可以注释掉perm_draw()那一行,跑draw(),这样就能看到所有对阵里的单场对阵出现的次数和概率。比方说我跑的:
看到华丽的23.2米屌们哭了没?皇马概率最高的是抽尤文,21.8,能抽到21.5的曼联已经是人品了(0.3%的人品也是人品)。
如果少年您真心蛋疼,请注释掉draw(),跑perm_draw()吧。比方说我就蛋疼了一次:
其实概率是一样的,只是遍历了一下2亿多次不同的抽签过程而已。
至于有没有黑幕这种事,就见仁见智了。
好了今天就先到这里了我们下次节目再见。
P.S. 应该还有很多种做法,比方说固定某一场算剩下的可能对阵(ie.能不能把16层递归搞成8层),或者Monte Carlo Method(100万应该够收敛了吧,起码比2亿要快),etc。有什么其他想法欢迎来搞。