【分治法】解决循环赛问题(n分为奇数和偶数)


题目

设有N个运动员要进行网球循环赛,设计一个满足以下要求的比赛日程表

  1. 每个选手必须与其他n-1个选手各赛一次;
  2. 每个选手一天只能赛一次;
  3. 当n 是偶数,循环赛进行n-1简单天,当n是奇数,循环赛进行n天。

分析

1. 首先考虑简单问题(n = 2^k)

这个我先上一个图大家应该就可以明白:

第1列为选手,第2-8列为赛程,8个选手赛7天

应该很容易想到分治法,有如下规律:对于任意一个正方形区域(包括4、16……个小方块)左上角和右下角相等,右上角和左下角相等(如果懒得看汉字就直接看上面几种颜色的方块吧

n=8

按分治策略,我们可以将所有的选手分为两半,则n个选手的比赛日程表可以通过n/2个选手的比赛日程表来决定。递归地用这种一分为二的策略对选手进行划分,直到只剩下两个选手时,比赛日程表的制定就变得很简单。这时只要让这两个选手进行比赛就可以了。如上图,所列出的正方形表是8个选手的比赛日程表。其中左上角与左下角的两小块分别为选手1至选手4和选手5至选手8前3天的比赛日程。据此,将左上角小块中的所有数字按其相对位置抄到右下角,又将左下角小块中的所有数字按其相对位置抄到右上角,这样我们就分别安排好了选手1至选手4和选手5至选手8在后4天的比赛日程。

然后这个代码也很容易:

  • 非递归(k是2上面的次数)
    void SetTable(int **a, int k)
    {
        //初始化左上角数
        a[0][0] = 1;
        a[0][1] = 2;
        a[1][0] = 2;
        a[1][1] = 1;
        //然后分治安排
        for(int i = 2; i <= k; i++)
        {
            int len = 1 << i;   //len = 2^i
            int half = len / 2; 
            //左下角子表就是左上角子表加上half
            for(int row = half; row < len; row ++)
            {
                for(int col = 0; col < half; col ++)
                {
                    a[row][col] = a[row - half][col] + half; 
                }
            }
            //右上角子表就是左下角子表
            for(int row = 0; row < half; row ++)
            {
                for(int col = half; col < len; col ++)
                {
                    a[row][col] = a[row + half][col - half];
                }
            }
            //右下角子表就是左上角子表
            for(int row = half; row < len; row ++)
            {
                for(int col = half; col < len; col ++)
                {
                    a[row][col] = a[row - half][col - half];
                }
            }
        }
    }
  • 递归(n是比赛总人数,这里先考虑简单的n = 2^k)
    void tourna(int n, int **a)
    {
        if(n == 1)
        {
            a[0][0] = 1;
            return;
        }
        tourna(n/2, a);
        copy(n, a);
    }
    void copy(int n, int **a)
    {
        int m = n / 2;
        for(int i = 0; i < m; i++)
        {
            for(int j = 0; j < m; j++)
            {
                a[i + m][j + m] = a[i][j];  //右下角
                a[i + m][j] = a[i][j] + m;  //左下角
                a[i][j + m] = a[i + m][j];  //右上角
            }
        }
    }

2. 衍生到一般的偶数(如果n不是2的次方)

举一个例子(n=6),既然是分治法,按照大的思路,我们把tourna(6, a)问题转换成tourna(3, a)【这里的你们就先看成是上面的tourna函数】,那么问题又来了,n=3怎么解决呢?(自然引出了奇数的问题,那我们直接考虑下面的奇数问题,这里大家可以直接跳到第三种情况看,看完之后回看这一点)

我们可以将6分为(1 2 3)和(4 5 6)我们且看两者单独考虑的赛程:
(1 2 3)赛程 (4  5  6)赛程

  • 考虑到(1 2 3)(4 5 6)“1和4”的第四列(第三天),只有一号选手和四号选手没比赛,那就让他们比,同理还有2和5,3和6,推出如下图【左图】
  • 再来分析【右图】,第一行由于(1 5)(1 6)没比赛,后面就填他们,对应的也有(5 1)(6 1),再看第二行可以是(2 4)(2 6),由于6在第五天和1比赛了,所以应该是先填(2 6)然后(2 4),对应的也有(6 2)(4 2);后面同理。

其实我感觉这个就是找规律好吗,和分治法有“桃子”关系

找规律

3. n为奇数

  • 考虑到题目中有n天时间比赛,自然想到n = 4 的情况(看下图)

这个是4个人比赛3天的情况,现在如果只有3个人,那么岂不是正好是3天,我们仅仅需要将其中的4改变为0(看下图),代表选手那天空闲就可

n = 4 n = 3

  • 其实对于一般的奇数,我们都可以转换成相应的n + 1,变成偶数再分治求解,最终递归到尽头就是n = 3(然后我们回到第二问)

最终代码

# include <iostream>
using namespace std;

void output(int **a, int n)
{
    if(n % 2 == 1)  //n为奇数时候的输出
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n + 1; j++)
            {
                if(a[i][j] == n + 1)
                    cout << 0;
                else  cout << a[i][j];
                if (j == n)
                    cout << endl;
                else
                    cout << ' ';
            }
        }
    else    //n为偶数时候的输出
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                cout << a[i][j];
                if (j == n - 1)
                    cout << endl;
                else
                    cout << ' ';
            }
        }
}

void copy(int n, int **a)
{
    int m = n / 2;
    //cout << m << endl;
    for(int i = 0; i < m; i++)
    {
        for(int j = 0; j < m; j++)
        {
            a[i + m][j + m] = a[i][j];  //右下角
            a[i + m][j] = a[i][j] + m;  //左下角
            a[i][j + m] = a[i + m][j];  //右上角
        }
    }
}

void copyodd(int n, int **a)
{
    int m = n / 2;
    int *b = new int[n];
    for(int i = 0; i < m; i++)
    {
        b[i] = m + i + 1;   // 4 5 6
        b[m + i] = b[i];    // 4 5 6
    }
    for(int i = 0; i < m; i ++)
    {
        for(int j = 0; j < m+1; j ++)   //改写左上角和填写右下角
        {   //这里如果你硬要说什么解释的话,我也说不出啥来,我看来就是找规律
            if(a[i][j] > m) 
            {
                a[i][j] = b[i];
                a[m + i][j] = (b[i] + m) % n;
            }
            else a[m +i][j] = a[i][j] + m;
        }
        for(int j = 1; j < m; j++)  //填写右上角和对应的右下角
        {   //这里我当时是把 n = 6作为特例带进去一个一个是试出规律来的
            a[i][m + j] = b[i + j];
            a[b[i + j] - 1][m + j] = i + 1;
        }
    }
}

void makecopy(int n, int **a)
{
    if((n/2 > 1) && ((n/2)%2 == 1)) copyodd(n, a);
    else copy(n, a);
}

void tourna(int n, int **a)
{
    if(n == 1)
    {
        a[0][0] = 1;
        return;
    }
    if(n % 2 == 1) 
    {
        tourna(n+1, a); //奇数+1变成偶数
        return;
    }
    tourna(n/2, a); //分治
    makecopy(n, a);
}

int main()
{
    int n;
    cin >>n;
    int **a = new int *[2*n];
    for(int i = 0; i < 2*n; i++)	//分配空间
        a[i] = new int[2*n];
    tourna(n, a);
    output(a, n);
    for(int i = 0; i < 2*n; i ++)	//delete空间
        delete []a[i];
    delete []a;
    return 0;
}

文章作者: PengShuai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 PengShuai !
评论
  目录