【解惑】孜孜不倦,用足球赛程详解c#中的yield return用法

在一个知名企业赞助的足球联赛中,有256支球队参赛。为了确保比赛的顺利进行,企业指派了小悦负责熬夜加班制定每一个球队的赛程。尽管她对足球的了解并不多,但是她对待工作的认真态度却让人钦佩。

在小悦的努力下,她顺利完成了第一轮、第二轮和第三轮的比赛安排。然而,在大赛开始前的模拟比赛中,她发现了一个严重的问题:由于参赛球队过多,人为的安排总会导致一些参赛球队被遗漏了比赛。这让她十分焦虑,因为如果不能尽快解决这个问题,联赛的公平性和竞争性将受到严重影响。

为了解决这个问题,小悦开始了她的电话咨询之旅。她先是联系了赛事主办方,了解参赛球队的具体情况。随后,她又联系了计算机专家,希望找到一个解决办法,确保每个参赛球队都能顺利比赛。

在与计算机专家沟通的过程中,小悦了解到这个问题并不简单,因为要考虑到的因素非常多。不过,在专家的帮助下,她逐渐找到了问题的根源,并听取专家的意见采取了一些有效的措施来解决它。最终,经过小悦的不懈努力和计算机专家的协助,每个参赛球队的赛程都被安排得合理而公正。

小悦在熬夜加班制定赛程时,扎着马尾辫,聚精会神地盯着电脑屏幕,手指在键盘上飞快地敲击着。她的眼神中闪烁着智慧的光芒,仿佛在告诉人们:她有能力解决任何问题。当她下意识挽起头发时,这个简单的动作,展现了她的优雅和美丽。

在这个过程中,小悦学到了很多关于足球和赛程安排的知识。她也深刻体会到团队合作的重要性,学会了如何与同事合作解决问题。最终,她的努力得到了上司和公司的肯定,也为她带来了不少宝贵的经验。

小悦面临的问题如下:锦标赛由256支队伍组成。这是一场循环赛(全部比赛),所以有255轮,每支球队每轮比赛一次。每支球队在锦标赛中与其他球队对抗一次(每场比赛在锦标赛中不会重复)。

她的任务是实现一个函数buildMatchesTable,它接收团队的数量(总是正数和偶数)并返回一个矩阵。

矩阵中的每一行代表一轮。矩阵的每一列表示一个匹配。这场比赛是由两支队伍组成的一个阵列。每个团队都用一个数字表示,从1开始直到团队数量。

示例(假设4只球队参赛): 构建匹配表(4)

应该返回一个元组矩阵,如下所示:

{

new[]{(1,2),(3,4)},//第一轮:1对2,3对4

new[]{(1,3),(2,4)},//第二轮:1对3,2对4

new[]{(1,4),(2,3)}//第三轮:1对4,2对3

}


算法实现:

复制代码
 1 public static (int, int)[][] BuildMatchesTable(int n)
 2 {
 3     var matches = new (int, int)[n - 1][];  // 创建一个二维数组,表示比赛表
 4     var teams = FairCycle(n).GetEnumerator();  // 获取一个循环迭代器,用于生成比赛对阵
 5     for (int i = 0; i < n - 1; i++)
 6     {
 7         var round = new (int, int)[n / 2];  // 创建一个一维数组,表示当前轮次的比赛对阵
 8         for (int t = 0; t < n / 2; t++)
 9         {
10             teams.MoveNext();  // 迭代到下一个比赛对阵
11             round[t] = teams.Current;  // 将当前比赛对阵添加到当前轮次的比赛表中
12         }
13         matches[i] = round;  // 将当前轮次的比赛表添加到总的比赛表中
14         // FairCycle函数会在这里负责循环到下一个比赛对阵
15     }
16     return matches;  // 返回生成的比赛表
17 }
18 
19 static IEnumerable<(int,int)> FairCycle(int n)
20 {
21     // 将数字1到n-1按顺时针方式排列
22     // 从12点钟方向开始,1位于12点钟位置
23     int p = 1;
24     while (true)
25     {
26         // 返回n和12点钟位置的数字
27         yield return (p, n);
28         // 然后返回12点钟左右两侧的数字配对
29         // 例如,11和1点钟,10和2点钟,以此类推
30         for (int i = 1; i < n / 2; i++)
31         {
32             int l = p + i;
33             l = l > (n - 1) ? l - (n - 1) : l;  // 对n进行循环,但不包括n本身
34             int r = p + (n - 1) - i;
35             r = r > (n - 1) ? r - (n - 1) : r;
36             yield return (l, r);
37         }
38         // 将时钟旋转n/2,使得p + n/2现在位于12点钟位置
39         p += n / 2;
40         p = p > (n - 1) ? p - (n - 1) : p;
41         // 这将开始下一轮的配对
42     }
43 }

这段代码实现了一个生成比赛表的函数`BuildMatchesTable`和一个辅助函数`FairCycle`。

`BuildMatchesTable`函数接收一个正数n作为参数,返回一个二维数组,表示比赛表。

`FairCycle`函数是一个模拟时钟循环迭代器,用于生成球队比赛对阵表。

让我们以`BuildMatchesTable(4)`为例来详细介绍`yield return`的运行过程和直接使用`return`的运行过程:

首先,我们调用`BuildMatchesTable(4)`函数,它将生成一个4个参与者的比赛表。

执行`var teams = FairCycle(n).GetEnumerator();`时会调用`FairCycle(n)`方法,并开始执行其中的代码。在`FairCycle(n)`方法中,第一次调用`yield return`语句会返回一个`IEnumerator`对象,该对象用于迭代生成比赛对阵。

这意味着在执行`var teams = FairCycle(n).GetEnumerator();`之后,`teams`变量将持有一个可以用于迭代生成比赛对阵的迭代器对象。你可以使用`teams.MoveNext()`方法来逐个获取比赛对阵,每次调用`MoveNext()`方法时,都会执行`FairCycle(n)`方法中的代码,直到遇到下一个`yield return`语句。

使用`yield return`的运行过程如下:

  1. 初始化时钟位置p为1。

  2. 进入无限循环。

  3. 第一次调用`yield return`语句,返回当前时钟位置p和数字n的配对`(p, n)`,即`(1, 4)`。此时,比赛表中的第一场比赛是1号选手对阵4号选手。

  4. 继续循环,进入第二次调用`yield return`语句。

  • 计算左侧数字l,它等于p加上i,即2。

  • 计算右侧数字r,它等于p加上(n-1)-i,即3。

  • 使用`yield return`语句返回左侧数字l和右侧数字r的配对`(l, r)`,即`(2, 3)`。此时,比赛表中的第二场比赛是2号选手对阵3号选手。

  1. 更新时钟位置p,使其加上n/2,即2。此时,时钟位置p指向下一轮的起始位置。

  2. 回到步骤3,开始下一轮的配对。

  • 第三次调用`yield return`语句,返回当前时钟位置p和数字n的配对`(p, n)`,即`(2, 4)`。此时,比赛表中的第三场比赛是2号选手对阵4号选手。

  • 继续循环,进入第四次调用`yield return`语句。

  • 计算左侧数字l,它等于p加上i,即3。

  • 计算右侧数字r,它等于p加上(n-1)-i,即2。

  • 使用`yield return`语句返回左侧数字l和右侧数字r的配对`(l, r)`,即`(3, 2)`。此时,比赛表中的第四场比赛是3号选手对阵2号选手。

  • 更新时钟位置p,使其加上n/2,即4。此时,时钟位置p指向下一轮的起始位置。

  • 回到步骤3,开始下一轮的配对。由于此时时钟位置p超过了(n-1),即4,所以将其减去(n-1),即得到1。此时,时钟位置p重新指向下一轮的起始位置。

  • 第五次调用`yield return`语句,返回当前时钟位置p和数字n的配对`(p, n)`,即`(1, 4)`。此时,比赛表中的第五场比赛是1号选手对阵4号选手。

  • 继续循环,进入第六次调用`yield return`语句。

  • 计算左侧数字l,它等于p加上i,即2。

  • 计算右侧数字r,它等于p加上(n-1)-i,即3。

  • 使用`yield return`语句返回左侧数字l和右侧数字r的配对`(l, r)`,即`(2, 3)`。此时,比赛表中的第六场比赛是2号选手对阵3号选手。

  • 更新时钟位置p,使其加上n/2,即2。此时,时钟位置p指向下一轮的起始位置。

  • 回到步骤3,开始下一轮的配对。由于此时时钟位置p超过了(n-1),即4,所以将其减去(n-1),即得到1。此时,时钟位置p重新指向下一轮的起始位置。

  • ...

这样,`yield return`语句会按需生成比赛对阵,每次调用时返回一个比赛对阵,并在下一次调用时从迭代器Enumerator上一次离开的地方继续执行。

现在,让我们来看看直接使用`return`的运行过程:

  1. 初始化时钟位置p为1。

  2. 直接使用`return`语句返回当前时钟位置p和数字n的配对`(p, n)`,即`(1, 4)`。此时,方法立即终止执行,并将配对`(1, 4)`作为方法的结果返回。这意味着只会生成一个比赛对阵,即1号选手对阵4号选手。

使用`return`语句会立即终止方法的执行,并将指定的值作为方法的结果返回。这意味着方法只会生成一个比赛对阵,并且无法再继续执行其他的代码。


注:FairCycle方法设计为时钟的模式是为了确保每个参赛球队都能在锦标赛中与其他球队公平地对抗一次。该方法的好处包括:

  1. 公平性:通过时钟的模式(所有球队对半匹配一次),每支球队都有机会与其他球队进行比赛,确保了比赛的公平性。没有球队会被遗漏或被偏好,每个球队都有相同的机会竞争。

  2. 竞争性:FairCycle方法确保了每支球队都能面对各种对手,包括实力强大的球队和实力较弱的球队。这种多样性的对手使得比赛更具竞争性,增加了球队之间的激烈程度。

  3. 简洁性:时钟的模式使得比赛安排更加简洁明了。每支球队在每轮比赛中都有一个确定的对手,没有重复或遗漏的情况,减少了混乱和错误的可能性。

  4. 可预测性:由于时钟的模式,每支球队都可以提前知道在每一轮比赛中将要面对的对手。这样,球队可以提前制定战术和策略,更好地准备比赛。

假设有8支球队参加一个锦标赛,并且FairCycle方法被用来安排比赛。FairCycle方法的时钟模式将确保每支球队都能与其他球队公平地对抗一次。

首先,我们将8支球队编号为A、B、C、D、E、F、G、H。根据FairCycle方法,比赛的安排如下:

第一轮: A vs B C vs D E vs F G vs H

第二轮: A vs C B vs D E vs G F vs H

第三轮: A vs D B vs C E vs H F vs G

第四轮: A vs E B vs F C vs G D vs H

第五轮: A vs F B vs E C vs H D vs G

第六轮: A vs G B vs H C vs E D vs F

第七轮: A vs H B vs G C vs F D vs E

通过这个例子,我们可以看到每支球队都与其他球队公平地对抗了一次。FairCycle方法的时钟模式确保了每支球队都有相同的机会竞争,没有球队被遗漏或被偏好。这种安排方式确保了比赛的公平性,并且每支球队都有机会面对各种对手,增加了比赛的竞争性。


测试用例:

复制代码
 1 using NUnit.Framework;
 2 using System;
 3 using System.Linq;
 4 using System.Collections.Generic;
 5 
 6 namespace Solution {
 7   
 8   [TestFixture]
 9   public class SolutionTest
10   {
11     [Test]
12     public void Test2Teams()
13     {
14       var expected = new []{ new []{(1, 2)} };
15 
16       var actual = Tournament.BuildMatchesTable(2);
17       Assert.That(actual, Has.Length.EqualTo(1), "Should have 1 round");
18       Assert.That(actual[0], Has.Length.EqualTo(1), "The round should have 1 match");
19       if(actual[0][0].Item1>actual[0][0].Item2) (actual[0][0].Item1, actual[0][0].Item2) = (actual[0][0].Item2, actual[0][0].Item1);
20       Assert.AreEqual(expected, actual, "The match should be team 1 vs team 2");
21     }
22     
23     [Test]
24     public void Test4Teams() => TestTeams(4);
25     
26     [Test]
27     public void Test20Teams() => TestTeams(20);
28     
29     [Test]
30     public void TestRandom()
31     {
32       Random rand = new Random();
33       
34       TestTeams(2*(3+rand.Next(3)));
35       TestTeams(2*(6+rand.Next(4)));
36     }
37 
38     public void TestTeams(int numberOfTeams)
39     {
40       List<int> teamsExpected = Enumerable.Range(1, numberOfTeams).ToList();
41       HashSet<(int, int)> matchesExpected = new HashSet<(int, int)>();
42       foreach(var round in TournamentSolution.BuildMatchesTable(numberOfTeams))
43       {
44         foreach(var game in round) matchesExpected.Add(game.Item1>game.Item2?(game.Item2,game.Item1):(game.Item1, game.Item2));
45       }
46 
47       var actual = Tournament.BuildMatchesTable(numberOfTeams);
48       Assert.That(actual, Has.Length.EqualTo(numberOfTeams-1), $"Should have {numberOfTeams-1} rounds");
49       foreach(var round in actual)
50       {
51         List<int> teamsByRound = new List<int>();
52         Assert.That(round, Has.Length.EqualTo(numberOfTeams/2), $"Each round should have {numberOfTeams/2} matches");
53         foreach(var game in round)
54         {
55           Assert.That(game, Is.InstanceOf(typeof((int, int))), "Each match is a tupple of 2 teams");
56           teamsByRound.Add(game.Item1);
57           teamsByRound.Add(game.Item2);
58           Assert.True(matchesExpected.Remove(game.Item1>game.Item2?(game.Item2,game.Item1):(game.Item1, game.Item2)), $"{game} is a duplicate or doesn't exist");
59         }
60         teamsByRound.Sort();
61         Assert.AreEqual(teamsExpected, teamsByRound, "Each round should have matches with every team");
62       }
63       Assert.IsEmpty(matchesExpected, "At least one match isn't scheduled");
64     }
65   }
66 }
67 
68 public class TournamentSolution
69 {
70     public static (int, int)[][] BuildMatchesTable(int numberOfTeams)
71     {
72       List<int> teams = Enumerable.Range(1, numberOfTeams).ToList();
73       int roundsNbr = numberOfTeams-1, gamesNbr = numberOfTeams /2, rotatorID = roundsNbr-1, buffer = 0;
74       (int, int)[][] result = new (int, int)[roundsNbr][];
75 
76       for (int i=0; i<roundsNbr; i++)
77       {
78         result[i] = new (int, int)[gamesNbr];
79         
80         for (int j = 0; j < gamesNbr; j++) result[i][j] = (teams[0 + j], teams[roundsNbr - j]);
81 
82         buffer = teams[rotatorID];
83         teams.RemoveAt(rotatorID);
84         teams.Insert(0, buffer);
85       }
86       
87       return result;
88     }
89 }