網球循環賽比賽日程表n爲奇數問題

初衷

在教材上看到這個問題的時候,對於奇數的處理百思不得其解,然而網上的答案要麼就是n=2k的狀況,要麼就是本身根本都沒有理解,給你講了一大堆,各類狀況,很麻煩,甚至有些是錯的誤人子弟。因此寫下這篇思路,分享給各位。其實這個問題的核心就是分治的治該怎麼去構造的問題。ios

問題

設有N個運動員要進行網球循環賽,設計一個知足如下要求的比賽日程表算法

(1)每一個選手必須與其餘n-1個選手各賽一次數組

(2)每一個選手一天只能賽一次bash

(3)當n 是偶數,循環賽進行n-1天,當n是奇數,循環賽進行n天。ui

算法分析

  1. 咱們採用分治法,先算出n/2的狀況,而後進行合併,構造出n的狀況。難點就在於構造過程,設第i個選手第j天比賽的隊員爲A[i][j]。
  2. 若m = n/2爲偶數,這時候咱們知道偶數人數已經算出了前m個隊員的前passed_days天(對於偶數而言是m-1,對於奇數是m)的比賽狀況,咱們怎麼構造呢?
    • 先橫向構造,也就是構造出後m個隊員在前m-1天的比賽狀況,那麼爲了保證不重複咱們採用遞增的構造方式,讓i+m號選手與比A[i][j]大m的選手比賽,也是是說
      • A[i + m][j] = A[i][j] + m; (1≤i≤m,1≤j≤passed_days,i表明隊員,j表明當前比賽的天數)
      • 能夠看出來必定不會重複,由於前m個隊員以前從未與後m個隊員比勝過,而後A[i][j]彼此又是互不相同的,因此A[i+m]彼此也必定不相同
    • 再縱向構造,設n個隊員比賽所需總天數爲days(對於偶數而言是n-1,對於奇數是n),也就是說構造n個隊員在後(days - passed_days)天的比賽狀況,一樣爲了保證不重複咱們也採用增量構造,
      • passed_days +1≤j≤days,
      • rvalue = (count + i-1)%m + m+1;//保證i隊員與後面的隊員(rvalue必然大於m)比賽,這樣就與前面passed_days天的比賽不重複
      • count爲增量初始值爲0,j每加1,count++
      • A[i][j] = rvalue;
      • A[rvalue][j] = i; //由於是兩兩比賽,後面m對用中的與之對應個隊員直接構造出來
    • 須要注意的是縱向構造的時候咱們先構造A[1][j]也就是說先保證第一個隊員在(days - passed_days)比賽的隊員確定與以前(passed_days)是不一樣的,那麼因爲i也是遞增的因此,A[i][j]彼此之間一定也是互不相同的!
  • 舉個栗子:假設n=4.

先計算n/2 = 2,咱們知道A[1][1] = 2,A[2][1] = 1(偶數比賽只有一天)spa

1 2(隊員編號)

2 1(第一天)
複製代碼

接下來咱們構造,n=4,此時days = 3,passed_days = 1,m=2設計

橫向構造:code

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

縱向構造A[1][j]:ip

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

接着縱向構造A[2][j]:ci

1 2 3 4
2 1 4 3
3 4 1 2
4 3 2 1
複製代碼
  1. 若m = n/2爲奇數的話,咱們須要特殊處理下

    • 因爲咱們是按照偶數的邊界構造的,也就是奇數的時候實際上擴充爲了m+1列,那麼咱們須要把與m+1比賽的隊員置0,刪掉多餘的m+1列
    • 其次因爲置0,那麼橫向構造的時候若是A[i][j]=0,說明i隊員在j天沒有比賽,咱們直接讓他與i+m號選手比賽(保證與以前的橫向構造的增量一致)
    • A[i + m][j] = i;
            A[i][j] = i + m;
      複製代碼
    • 可是上面也引發了問題就是,咱們在縱向構造的時候A[1][passed_days]可能會重複(由於在爲0的位置可能填入了m+1的元素,因此咱們須要標記一下,若是是奇數的話縱向構造的其實增量+1),只要保證了A[1][j]不重複,後面由於都是增量構造確定不會重複 r_value = (count + (i - 1) + 1) % m + m + 1;

仍是舉兩個例子:

n=3的構造狀況

1.先算n=2

1 2
2 1
複製代碼

2.m=2,passed_days=1,days = 3橫向構造

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

3.縱向構造A[1][j],j=2,3

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

4.縱向構造A[2][j],j=2,3

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

5.把擴充的置0,同時刪掉多餘的第4列

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

n=6的構造狀況

1.首先n/2=3已經算出

2.m=3,passed_days=3,days = 5橫向構造A[i][1],即第一天的

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

3.接着橫向構造完全部passed_days天的

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

4.縱向構造A[1][4],A[1[5],因爲m是奇數因此,構造增量加了1即A[1][4] = (0 + (1 - 1) + 1) % 3 + 3 + 1 = 5;

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

5.縱向構造完(因爲n是偶數,不須要再進行置0操做)

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  
複製代碼

C++代碼

#include <iostream>
#include <iomanip>
#include <cmath>

using namespace std;
const int MAX_NUM = 100;
int A[MAX_NUM+2][MAX_NUM+2];
/* 合併子問題 */
void merge(int n)
{
    /*
     * n 爲偶數時,比賽 n - 1 天
     * n 爲奇數時,比賽 n 天
     */
    int days = n&1 ? n  : n-1;
    /*
     * 中間值,若n爲奇數,則使 m = (n / 2) + 1,
     * 即,前半部分不小於後半部分
     */
    int m = (int)ceil(n / 2.0);
    int passed_days = m& 1? m : m - 1;    /* 已經安排的天數 */

    /*
     *  經過前 n/2 的比賽安排,構造後n/2的比賽安排
     *  若是 n 爲奇數,則會產生一個虛擬選手
     */
    for (int i = 1; i <= m; i++)
    {
        for (int j = 1; j <= passed_days; j++)
        {
            if (A[i][j] != 0)   /* 若是 i 號在第 j 天有對手 */
            {
                /*
                 * 那麼,(i + m) 號在第 j 天的對手爲 i號的
                 * 對手日後數 m 號
                 */
                A[i + m][j] = A[i][j] + m;
            }
            else   /* 若是 i 號在第 j 天沒有對手*/
            {
                /*
                 * 那麼就讓 i 號和 (i + m)號互爲對手
                 */
                A[i + m][j] = i;
                A[i][j] = i + m;
            }
        }
    }

    int add_one = 0;        /*標誌子問題是不是奇數,若是是的話構造增量加1 */
    if (A[1][passed_days] == m + 1)
        add_one = 1;
    for (int i = 1; i <= m; i++)
    {
        for (int j = passed_days + 1, count = 0; j <= days; j++, count++)
        {
            /*
             * 構造i 號在第 j 天的對手
             */
            int r_value = (count + (i - 1)  + add_one) % m + m + 1;
            A[i][j] = r_value;
            A[r_value][j] = i;
        }
    }
    if ( n & 1 )            /* 若是 n 爲奇數,消除虛擬的選手 */
    {
        for (int i = 1; i <= 2 * m; i++)
        {
            for (int j = 1; j <= days; j++)
                if (A[i][j] == n + 1)
                    A[i][j] = 0;        /* A[i][j] = 0 ,表示 i 號選手在第 j 天沒有比賽 */
        }
    }
}

/* 分治求解循環賽問題 */
void tournament(int n)
{
    if (n <= 1)
        return;
    else if (n == 2)        /* 2 個選手 */
    {
        A[1][1] = 2;
        A[2][1] = 1;
    }
    else
    {
        tournament((int)ceil(n / 2.0));
        merge(n);
    }
}

/* 打印循環賽日程表 */
void show_result(int n)
{
    cout << " " << n << "人循環賽" << endl;
    int days = n&1 ?n : n-1;
    cout.flags(ios::left);
    cout << setw(8) << "";
    for (int i = 1; i <= n; i++)
        cout << setw(2)<<i << "號";
    cout << endl;
    cout.flags(ios::left);
    for (int j = 1; j <= days; j++)
    {
        cout << "第"<<setw(2)<<j<< "天 ";
        for (int i = 1; i <= n; i++)
        {
            cout << setw(4) << A[i][j];
        }
        cout << endl;
    }
    cout << endl;
}

int main()
{
    int num;
    while(true){
        cout << "請輸入參賽人數(小於100):(0結束程序)";
        cin >> num;
        if(num == 0) break;
        tournament(num);
        show_result(num);      
    }
    return 1;
}
複製代碼

算法複雜度

設n=2k,第i次循環須要計算(2k/2i)2,2≤i≤k,總共的計算次數粗略的表示爲 12+22 +...+ 22j + ... + 22(k-1) 等比數列求和爲(22k-1)/3,粗略等於22k=n^2。 因此算法時間複雜度爲O(n^2).

因爲只須要一個數組,因此空間代價爲:O(n^2)

相關文章
相關標籤/搜索