|
发表带详细注释的代码:- Sub test()
- Randomize '随机种子初始化
- tms = Timer '计时开始
-
- n = Sheets(2).[a1].End(4).Row - 1 '获取原始数据最大行数
- arr = Sheets(2).[a2].Resize(n, 2) '获取原始数据(选手名字和学校名)
-
- Set ds = CreateObject("Scripting.Dictionary") '定义字典ds(school)key存放每个学校名,item存放选手人数
- Set dn = CreateObject("Scripting.Dictionary") '定义字典dn(name & school)key存放每个选手姓名和学校名,item存放序号
-
- For i = 1 To n
- ds(arr(i, 2)) = ds(arr(i, 2)) + 1 '每个[学校名]对应item存放选手人数+1
- dn(arr(i, 2) & "_" & arr(i, 1)) = i '每个[学校名_选手名]对应item存放序号(初始行号)
- Next
-
-
- For i = n To 4 Step -2 '循环抽取,每次随机取2名选手,抽到i=4时剩余4人,抽完后只剩最后2人一组不用再抽了即可停止。
-
- If t <> "" Then GoTo half_jump '如果已经出现剩余人数最多学校人数=剩余人数的一半,即可进入半数单抽过程。
-
- q = ds.items '每次获取剩余各学校选手人数的数组
- l = WorksheetFunction.Large(q, 1) '获取其中剩余人数最多的学校的人数
-
- If l < i / 2 Then '如果剩余人数最多学校的人数小于待抽取人数的一半,则不用担心下一轮回有死循环。
- '(简单说,如果某个学校剩余人数超过剩余待抽选手人数的一半,则无论如何抽,最后必定会有该学校选手对阵的错误结果。)
-
- '实际随机抽取方法,用了经典的数组倒序洗牌法。
-
- r = Int(Rnd() * i + 1) '从待抽人数中随机抽1个序号r
-
- t1 = arr(r, 1): t2 = arr(r, 2) '抽中的选手信息先存入中间变量t1、t2
-
- arr(r, 1) = arr(i, 1): arr(r, 2) = arr(i, 2) '把当前待抽选手中最后一个(数组i行)的内容,置换放入被抽中的数组序号r行中
- dn(arr(i, 2) & "_" & arr(i, 1)) = r '置换后的该[学校名_选手名]对应的数组序号也要从i行改正为序号r行
-
- arr(i, 1) = t1: arr(i, 2) = t2 '最后存放在中间变量中的本次抽中的选手信息,压栈放入当前最后1行(数组i行)中,完成对调。
-
- '这样就完成了一次随机抽取过程,不仅保证每一次的随机性,并能保证不重复、不遗漏。
-
- ds(t2) = ds(t2) - 1 '[学校/人数]字典ds中,把已经抽中选手的对应学校人数-1
- If ds(t2) = 0 Then ds.Remove (t2) '如果该学校剩余人数=0,则可从[学校/人数]字典ds中删去该学校了。
-
- dn.Remove (t2 & "_" & t1) '[学校选手/序号]字典dn中,删除已经抽中选手的信息
-
- s = dn.keys '获取剩余的所有[学校_选手]信息。(即删除了已抽中选手信息的新的[学校选手/序号]字典dn的keys)
- s = Filter(s, t2, False) '用Filter函数过滤去除和刚刚抽取过选手学校相同的所有选手,以保证接下来的抽取不会抽到相同学校选手。
-
-
-
- '下面是用完全相同的,经典的数组倒序洗牌法,抽取和刚才已经抽中的选手不同学校的选手。
-
- r1 = Int(Rnd() * (UBound(s) + 1)) '按已经过滤的数组s人数进行随机抽取。(因为s数组下标为0,因此相应人数要+1)
- r2 = dn(s(r1)) '先用s数组返回随机抽到r1对应的[学校_选手名],接着用字典dn返回该学校该选手对应的数组更新序号。
-
- t1 = arr(r2, 1): t2 = arr(r2, 2) '抽中的选手信息先存入中间变量t1、t2
-
- arr(r2, 1) = arr(i - 1, 1): arr(r2, 2) = arr(i - 1, 2) '把当前待抽选手中最后一个(数组i-1行)的内容,置换放入被抽中的数组序号r2行中
- dn(arr(i - 1, 2) & "_" & arr(i - 1, 1)) = r2 '置换后的该[学校名_选手名]对应的数组序号也要从i-1行改正为序号r2行
-
- arr(i - 1, 1) = t1: arr(i - 1, 2) = t2 '最后存放在中间变量中的本次抽中的选手信息,压栈放入当前最后1行(数组i-1行)中,完成对调。
-
- ds(t2) = ds(t2) - 1 '[学校/人数]字典ds中,把已经抽中选手的对应学校人数-1
- If ds(t2) = 0 Then ds.Remove (t2) '如果该学校剩余人数=0,则可从[学校/人数]字典ds中删去该学校了。
-
- dn.Remove (t2 & "_" & t1) '[学校选手/序号]字典dn中,删除已经抽中选手的信息
-
- Else
- '如果剩余人数最多学校的人数已经=待抽取人数的一半时,则以后每一次抽取必须先抽这个学校的学生,然后再抽其他学校的。
- '否则,必定会产生该学校选手相互对阵的错误结果,进入死循环。
-
- For j = 0 To UBound(q)
- If q(j) = l Then Exit For '检查剩余人数数组,直至找到那个人数最多的对应序号j
- Next
- p = ds.keys '获取[学校/人数]字典ds的keys数组,即返回剩余学校名称数组p
- t = p(j) '根据这个人数最多学校对应序号j,由[学校/人数]字典ds返回该学校名称
- half_jump:
- s = dn.keys '获取剩余的所有[学校_选手]信息。(即删除了已抽中选手信息的新的[学校选手/序号]字典dn的keys)
- s = Filter(s, t, True) '用Filter函数过滤,保证只在该剩余人数最多学校中抽取1人。
-
- r1 = Int(Rnd() * (UBound(s) + 1)) '按已经过滤的该学校数组人数进行随机抽取。(因为s数组下标为0,因此相应人数要+1)
- r2 = dn(s(r1)) '先用s数组返回随机抽到r1对应的[学校_选手名],接着用字典dn返回该学校该选手对应的数组更新序号。
-
- t1 = arr(r2, 1): t2 = arr(r2, 2) '抽中的选手信息先存入中间变量t1、t2
-
- arr(r2, 1) = arr(i, 1): arr(r2, 2) = arr(i, 2) '把当前待抽选手中最后一个(数组i行)的内容,置换放入被抽中的数组序号r2行中
- dn(arr(i, 2) & "_" & arr(i, 1)) = r2 '置换后的该[学校名_选手名]对应的数组序号也要从i行改正为序号r2行
-
- arr(i, 1) = t1: arr(i, 2) = t2 '最后存放在中间变量中的本次抽中的选手信息,压栈放入当前最后1行(数组i行)中,完成对调。
-
- dn.Remove (t2 & "_" & t1) '[学校选手/序号]字典dn中,删除已经抽中选手的信息
-
-
-
- '下面是用完全相同的,经典的数组倒序洗牌法,抽取和刚才已经抽中的选手不同学校的选手。
-
- s = dn.keys '重新获取剩余的其他所有[学校_选手]信息。(即删除了已抽中选手信息的新的[学校选手/序号]字典dn的keys)
- s = Filter(s, t, False) '用Filter函数过滤,保证只在除去该剩余人数最多学校的其他学校选手中抽取1人。
-
- r1 = Int(Rnd() * (UBound(s) + 1)) '按已经过滤的数组s人数进行随机抽取。(因为s数组下标为0,因此相应人数要+1)
- r2 = dn(s(r1)) '先用s数组返回随机抽到r1对应的[学校_选手名],接着用字典dn返回该学校该选手对应的数组更新序号。
-
- t1 = arr(r2, 1): t2 = arr(r2, 2) '抽中的选手信息先存入中间变量t1、t2
-
- arr(r2, 1) = arr(i - 1, 1): arr(r2, 2) = arr(i - 1, 2) '把当前待抽选手中最后一个(数组i-1行)的内容,置换放入被抽中的数组序号r2行中
- dn(arr(i - 1, 2) & "_" & arr(i - 1, 1)) = r2 '置换后的该[学校名_选手名]对应的数组序号也要从i-1行改正为序号r2行
-
- arr(i - 1, 1) = t1: arr(i - 1, 2) = t2 '最后存放在中间变量中的本次抽中的选手信息,压栈放入当前最后1行(数组i-1行)中,完成对调。
-
- dn.Remove (t2 & "_" & t1) '[学校选手/序号]字典dn中,删除已经抽中选手的信息
-
- End If
- Next
-
- '下面为整理已经全部随机抽取完成的数组arr结果,改为可以直接输出的形式。
- ReDim brr(n / 2 * 3, 1) '定义输出结果数组brr,每一对选手隔开一行。
- For i = 1 To n / 2 '遍历数组arr
- brr(i * 3 - 3, 0) = arr(i * 2 - 1, 2) '当前第1行第1列放入学校名
- brr(i * 3 - 3, 1) = arr(i * 2 - 1, 1) '当前第1行第2列放入选手名
- brr(i * 3 - 2, 0) = arr(i * 2, 2) '当前第2行第1列放入配对选手学校名
- brr(i * 3 - 2, 1) = arr(i * 2, 1) '当前第2行第2列放入配对选手名
- Next
- [a5].Resize(n / 2 * 3, 2) = brr '输出结果
- If t <> "" Then t = t & "(" & l & ")"
- MsgBox Format(Timer - tms, "0.0000s ") & t '输出代码运行时间,以及最后剩余人数超半数信息。
-
- End Sub
复制代码 |
|