網球循環賽算法剖析

問題描述

設有n個運動員要進行網球循環賽。設計一個知足下列條件的比賽日程表:ios

  • 每一個選手必須與其餘n-1個選手各賽一次
  • 每一個選手一天只能賽一次
  • 當n是偶數時,循環賽進行n-1天
  • 當n是奇數時,循環賽進行n天

程序運行說明

編譯運行源代碼,提示輸入參賽者的數目,以9個參賽者爲例,顯示以下:算法

Please input the number of contestants:
9
File "result.dat" created.
複製代碼

表示程序運行完畢,輸出結果在可執行文件相同目錄下的result.dat文件中數組

此時,result.dat文件中輸出結果以下:bash

The tournament shall be arranged as follow:
選手編號:    1    2    3    4    5    6    7    8    9    
第1  天:     2    1    8    5    4    7    6    3    0    
第2  天:     3    5    1    9    2    8    0    6    4    
第3  天:     4    3    2    1    0    9    8    7    6    
第4  天:     5    7    4    3    1    0    2    9    8    
第5  天:     6    4    5    2    3    1    9    0    7    
第6  天:     7    8    9    0    6    5    1    2    3    
第7  天:     8    9    0    6    7    4    5    1    2    
第8  天:     9    0    6    7    8    3    4    5    1    
第9  天:     0    6    7    8    9    2    3    4    5       
複製代碼

其中,其中一行表明一天,每一列表明一個選手,即第i行j列表示第i天第j個選手的對手編號,行號列號從1開始計(第零行爲選手編號,不算在表格中,從第一天開始爲第一行)架構

若參賽者爲奇數個,則會出現數字0,表明該參賽者當天沒有比賽函數

算法設計說明

寫在前面

本算法核心參考連接在此,但在原做者的算法上作了必定修改post

一些函數和寫法的解釋

在參考連接中做者提供的算法之中有一些本人以前沒見過或者沒用過的寫法和巧妙的運算方法、函數等,在此先加以羅列ui

  1. setw()函數:包含在iomanip頭文件中,爲固定設置字符寬度的函數,默認控制右對齊,若想要進行左對齊須要加入left控制符號,以下:
    outfile << left << setw(17) << "選手編號: ";
    複製代碼
  2. ceil()函數:包含在cmath頭文件中,是一個向上取整的函數,即ceil(n)返回不小於n的最小整數。須要特別注意的是,這個函數在頭文件中定義時返回值類型爲double型,故實際使用時,若涉及到整數的傳參須要把返回類型強制轉換成int型,以下:
    tournament((int)ceil(n / 2.0),table);
    複製代碼
    還須要注意的一點是,ceil的輸入也是double型的,這意味着對於整數n,上面函數中若寫爲ceil(n/2),則括號內部爲整數除法,返回的值是整數除法的結果,天然也是整數,而不是double,此時會引發錯誤。故應寫爲ceil(n/2.0)
  3. 符號"&"的用法:以下面這個實例中:
    if(n & 1){
         for (int i = 1; i <= total_days;i++){
             for (int j = 1; j <= n;j++){
                 if (table[i][j]==n+1)
                     table[i][j] = 0;
             }
         }
     }
    複製代碼
    符號"&"單個使用時表示按位與,在上面的例子(是程序源碼的一部分)中的含義即,將n轉變爲二進制,以後再和1按位與。顯然,當n爲奇數的時候表達式爲1,不然爲0,故這個語句能夠做爲判斷n是不是奇數的判斷語句,很是簡潔
  4. 此外,在寫算法的時候發現,對於二位數組和二重指針的參數傳遞來講,兩者並非等效的,即在下面這段代碼中
    int **table = new int *[n + 2];
     for (int i = 0; i < n + 2;i++)
         table[i] = new int[n + 2];
    
     tournament(n, table);
    複製代碼
    其中tournament函數定義爲:
    void tournament(int n,int **table) 複製代碼
    在這種狀況下必須使用二重指針動態分配內存的方式來初始化table,而不能直接創建一個二維數組table[n+2][n+2]

算法描述

先貼上完整代碼spa

#include<fstream>
#include<iostream>
#include<iomanip>
#include<cmath>
using namespace std;

void Show_Result(int n,int **table)//將結果按行(即按天)輸出到文件中 {
    ofstream outfile;
    outfile.open("result.dat");
    outfile << "The tournament shall be arranged as follow:" << endl;
    int total_days = n & 1 ? n : n - 1;
    outfile << left << setw(17) << "選手編號: ";
    for (int i = 0; i <= total_days;i++){
        if(i)
            outfile << "第" << left << setw(3) << i << left << setw(9) << "天:";
        for (int j = 1; j <= n;j++){
            if (!i)
                table[0][j] = j;
            // cout << table[i][j] << "\t";
            outfile << setw(5) << table[i][j];
        }
        // cout << endl;
        outfile << endl;
    }
        outfile.close();
}

void merge(int n,int **table) {
    int m = (int)ceil(n / 2.0);//m中記錄n/2向上取整的值,注意必定是2.0而不能是2,由於若是是2則括號內部爲整數除法
    int total_days = n & 1 ? n : n - 1;
    int merged_days = m & 1 ? m : m - 1;//已經排好平常表的天數,注意當m爲奇數和偶數時狀況的不一樣

    for (int i = 1; i <= merged_days;i++){//先列後行地橫向構造出n個選手前merged_days天的日程表
        for (int j = m+1; j <= n; j++){//從第m+1列開始構造,直到n列結束
            if (table[i][j-m])    //在第i天時,先檢查某個選手的對應構造選手(即第j-m個選手)當天對手是否爲0號(即當天沒有比賽),若不是,正常賦值
                table[i][j] = table[i][j - m] + m;
            else{//如果,則這一天讓他倆比賽
                table[i][j] = j - m;
                table[i][j - m] = j;
            }
        }
    }

    //繼續構造從第merged_days+1到第total_days的狀況
    for (int i = merged_days + 1; i <= total_days;i++){//先構造第1列,數據按行依次加1
        table[i][1] = table[i - 1][1] + 1;
        table[i][table[i][1]] = 1;//必定要注意構造第一行的時候把後面的對應比賽對手的數據也改了!!!
    }
    for (int i = merged_days + 1; i <= total_days;i++){//緊接着修改從第2列到第merged_days的數據,同時修改對應對手的數據
        for (int j = 2; j <= m;j++){
            //先計算對手,這裏對手的序號的計算公式不太好想
            //應該是以m+1爲基礎,依賴於m、這一行的第一個數、該運動員的編號,三者所共同決定
            int rival = m + 1 + (table[i][1] - (m + 1) + (j - 1)) % m;
            //修改本身的值爲對手,並修改對手的值爲本身
            table[i][j] = rival;
            table[i][rival] = j;
        }
    }

    //若是總人數是奇數,則會出現虛擬的一我的,要把全部和這個虛擬的對手比賽的運動員,對應位置置成0,即沒有比賽
    if(n & 1){//符號"&"是按位與的意思,即將n轉變爲二進制,以後再和1按位與,顯然當n爲奇數的時候表達式爲1,不然爲0
        for (int i = 1; i <= total_days;i++){
            for (int j = 1; j <= n;j++){
                if (table[i][j]==n+1)
                    table[i][j] = 0;
            }
        }
    }
}

void tournament(int n,int **table) {
    if (n==2){//當問題劃分爲2個運動員時,使兩人相互比賽
        table[1][1] = 2;
        table[1][2] = 1;
        // cout << table[1][2];
    }
    else{//若剩餘待排日程表人數超過兩人,則先解決(n/2向上取整)規模的子問題,以後再將子問題合併
        tournament((int)ceil(n / 2.0),table);
        // Show_Result(n, table);
        merge(n, table);
        // Show_Result(n, table);
    }
}


int main() {
    int n;
    cout << "Please input the number of contestants:" << endl;
    cin >> n;
    //創建一個存儲日程表的二維數組table,其中一行表明一天,從第一列開始每一列表明一個選手
    //即第i行j列表示第i天第j個選手的對手編號
    int **table = new int *[n + 2];
    for (int i = 0; i < n + 2;i++)
        table[i] = new int[n + 2];

    tournament(n, table);
    Show_Result(n, table);//生成的循環賽表格輸出到文件中
    cout << "File \"result.dat\" created." << endl;
}
複製代碼

實際上算法的設計思路已經都寫在註釋中了,若是仔細看很快就能看懂算法的架構,在此再簡要敘述一下:設計

  1. 先拆分問題,對於用戶指定的n,考慮規模爲m = ceil(n/2.0)的問題,並解決這個子問題,有兩種狀況
    1. 子問題是偶數,繼續重複上面的動做
    2. 子問題是奇數,補充一個虛擬選手m+1,這樣一共有偶數個選手,因而繼續重複上面的動做
  2. 不斷拆分直到只剩下兩我的,讓他們互相比賽便可
  3. 回過頭來從子問題構造母問題,採起先橫向構造、再縱向構造的方法,有兩種狀況
    1. 子問題是偶數,開開心心地正常構造
    2. 子問題是奇數,因爲以前咱們擴充了一個虛擬選手使得問題變爲了偶數個選手,這時候咱們須要把這個虛擬選手清除,而且把全部與他比賽的人,相應的位置編號由m+1變爲0,表示這天沒有比賽(本來和不存在的人比賽那固然至關於沒有比賽了)
  4. 重複以上的構造方法,直到問題恢復到n的規模,此時問題解決完畢

其中,「正常構造」是如何操做的還須要詳細說明一下: 以6個選手爲問題規模舉例說明:

選手編號:    1    2    3    4    5    6    
第1  天:     2    1    6    5    4    3    
第2  天:     3    5    1    6    2    4    
第3  天:     4    3    2    1    6    5    
第4  天:     5    6    4    3    1    2    
第5  天:     6    4    5    2    3    1    
複製代碼

在這個問題中,實際上算法裏的數組長成這個樣子:

2    1    6    5    4    3    
3    5    1    6    2    4    
4    3    2    1    6    5    
5    6    4    3    1    2    
6    4    5    2    3    1    
複製代碼

其中,最上面一行爲算法table二維數組的第1行,同理最左邊一列爲table中的第一列 在計算這個問題的時候,先完成規模爲3的問題的計算,爲了完成規模爲3的問題,須要先完成規模爲2的問題,此時讓兩我的相互比賽,即:

2    1
複製代碼

當試圖解決規模爲3的問題的時候,咱們發現它是奇數,故擴充一個虛擬對手,變爲構造一個四個選手的日程表問題,因此接下來咱們看一下如何從2個選手構造4個選手: 咱們先橫向構造,將多的兩我的第一天的賽程和前兩人對稱,比賽對手的編號加2,即:

2    1    4    3  
複製代碼

以後再縱向構造,首先,將第一列即第一號選手的對手按照天天增長1構造出來,同時修改對手的信息,即:

2    1    4    3    
3    X    1    X    
4    X    X    1
複製代碼

以後,構造前2列(對於規模爲n的問題應該是前m列,m定義見上),從還未安排好平常表的那行開始,按照先行再列依次填寫前兩列中上表中標註爲"X"的位置,填寫的規則爲: 最小的數是m+1,最大的數是n,從這一行第一列的數開始依次循環填寫,同時將對應對手的值做以修改 在本例中,即最小的數爲3,最大數爲選手個數4,第二行第一列爲3,則第二行第二列填4,同時第二行第四列填2,表示次日2號和4號相互對戰;以後填寫第三行,第三行第一列爲4,則填寫第三行第二列爲3,同時修改第三行第3列爲2,以下:

2    1    4    3    
3    4    1    2    
4    3    2    1
複製代碼

因爲實際上咱們須要解決的是3個選手的問題,故咱們把多餘的去掉,相應位置置0:

2    1    0        
3    0    1       
0    3    2   
複製代碼

繼續從3個選手構造6個選手 先橫向構造,此時咱們能夠看到和以前的不一樣在於有數字0的出現,這時先無論0,其他按照以前法則正常橫向構造:

2    1    0    5    4    0    
3    0    1    6    0    4    
0    3    2    0    6    5    
複製代碼

其實其中的0表明當天這個選手沒有比賽,故咱們直接讓這一天都沒有比賽的兩我的互相比賽便可

2    1    6    5    4    3    
3    5    1    6    2    4    
4    3    2    1    6    5 
複製代碼

接着按照上面的法則,先構造完第一列

2    1    6    5    4    3    
3    5    1    6    2    4    
4    3    2    1    X    X    
5    X    X    X    1    X    
6    X    X    X    X    1  
複製代碼

按照以前說的法則依次構造每一行

2    1    6    5    4    3    
3    5    1    6    2    4    
4    3    2    1    6    5    
5    6    4    3    1    2    
6    X    X    X    X    1
複製代碼
2    1    6    5    4    3    
3    5    1    6    2    4    
4    3    2    1    6    5    
5    6    4    3    1    2    
6    4    5    2    3    1  
複製代碼

最終完成了整個日程表的構造 其他規模的日程表構造同理

寫在最後

  1. 網球賽日程表的算法從瞭解算法,查找資料,寫算法,到寫說明文檔,前先後後花費了十個小時左右的時間,在這個過程當中收穫了不少,好比說判斷奇偶的小技巧、調整行寬的函數,等等,固然更重要的是瞭解了分治問題的架構方法、總體構造思路
  2. 循環賽日程表的創建實際上應該還有別的算法,往後有時間但願能繼續瞭解
  3. 此算法必定還有不少不足之處,如有任何意見或建議歡迎聯繫: Ethan_Lee@bupt.edu.cn
相關文章
相關標籤/搜索