再びトーナメント表作成 ⑧ 第一段階完成
先日から、トーナメント表作成の仕組みを焼き直ししている。
infoment.hatenablog.com
今回も、前回の続きから。
前回の結果を踏まえ、最終的にこの形に落ち着いた。
Option Explicit Public PlayerRange As Range Public PlayerDict As Object Public InversFlag As Boolean Public StartNumber As Long Dim Wf As WorksheetFunction Dim NetPalyerNumber As Long Dim TournamentSize As Long Dim PlayerCount As Long ' 名前のセル罫線要否。 Public BordersAroundName As Boolean ' 勝ち残りの名前セル要否。 Public IntermediateName As Boolean Private Sub Class_Initialize() Set Wf = WorksheetFunction End Sub Public Sub init(Optional player_range As Range, _ Optional invers_flag As Boolean = False, _ Optional start_number As Long = 1, _ Optional borders_around_name As Boolean = False, _ Optional intermediate_name As Boolean = False) Dim arr As Variant Dim i As Long If Not player_range Is Nothing Then Set PlayerRange = player_range Else Set PlayerRange = Selection End If InversFlag = invers_flag Set PlayerDict = CreateObject("Scripting.Dictionary") Dim r As Range For Each r In PlayerRange If r.Value <> vbNullString Then NetPalyerNumber = NetPalyerNumber + 1 PlayerDict(NetPalyerNumber) = r.Value End If Next ' 一回戦の人の配置を、前大会の何位からランダムに入れ替えるか。 StartNumber = start_number ' 選手の数から求めるトーナメントサイズ(決勝までの段数)。 TournamentSize = Wf.Ceiling_Math(Wf.Log10(NetPalyerNumber), Wf.Log10(2)) / Wf.Log10(2) ' 一回戦の人数。 PlayerCount = 2 ^ TournamentSize ' 名前周辺の罫線要否。 BordersAroundName = borders_around_name ' 勝ち上がり選手名の箱要否。 IntermediateName = intermediate_name End Sub Private Property Get TournamentOrderArray() As Variant ' 作業用配列。最終的に戻り値となる。 Static arr As Variant If Not IsEmpty(arr) Then TournamentOrderArray = arr Exit Property End If ReDim arr(1 To 1) ' 初期値。選手が一人のとき。 arr(1) = 1 If PlayerCount = 1 Then TournamentOrderArray = arr Exit Property End If Dim i As Long Dim j As Long ' 選手の数に併せて大きくなるトーナメントの仮格納用配列。 Dim TempArray As Variant For i = 1 To TournamentSize ReDim TempArray(1 To UBound(arr) * 2) For j = 1 To UBound(arr) ' 元の配列内の数値を、上から奇数で、下から偶数でセット。 TempArray(j) = arr(j) * 2 - 1 TempArray(UBound(TempArray) + 1 - j) = arr(j) * 2 Next arr = TempArray Next ' 上位半数の相手を改めてセット。 ' 例えば32人のトーナメントでは、1と32が、2と31が、3と30が・・・nと33-nが ' ペアになるようにする。※優先的に前回上位者を一回戦シードとするため。 For i = 1 To PlayerCount If arr(i) > PlayerCount / 2 Then ' 4人一組で評価するとき、2番目は1番目と、3番目は4番目と ' ペアになることから、4で割った余りで処理を変更する。 Select Case i Mod 4 Case 2: arr(i) = PlayerCount - arr(i - 1) + 1 Case 3: arr(i) = PlayerCount - arr(i + 1) + 1 End Select End If Next TempArray = arr ' 配列の最後を1位にしたい場合用。配列の順序をひっくり返す。 If InversFlag Then For i = 1 To UBound(arr) arr(i) = TempArray(UBound(TempArray) - i + 1) Next End If TournamentOrderArray = arr End Property Private Property Get RandomSortDict() As Object Static Dict As Object If Not Dict Is Nothing Then Set RandomSortDict = Dict Exit Property End If ' 作業用配列。 Dim TempArray As Variant ReDim TempArray(1 To NetPalyerNumber) ' 戻り値用配列。 Dim arr As Variant ReDim arr(1 To NetPalyerNumber) Dim i As Long For i = 1 To NetPalyerNumber ' ソートしない範囲。 If i < StartNumber Then TempArray(i) = i ' ソート範囲。iMax以上の桁でソートする。 ' このときtotalの桁数範囲には、元の数字が保存されている。 Else TempArray(i) = CLng(WorksheetFunction.RandBetween(1, 100) * _ (10 ^ Len(CStr(PlayerCount))) + i) End If Next ' 配列ソート。 TempArray = SortArray(TempArray) ' 保存された元の数字を取り出す。 ' SortArray関数を使用すると、配列が0始まりになるので注意。 For i = NetPalyerNumber To 1 Step -1 arr(i) = Right(TempArray(i - 1), CLng(Len(CStr(NetPalyerNumber)))) * 1 Next Set Dict = CreateObject("Scripting.Dictionary") For i = 1 To NetPalyerNumber Dict(i) = arr(i) Next If NetPalyerNumber < PlayerCount Then For i = NetPalyerNumber + 1 To PlayerCount Dict(i) = i Next End If Set RandomSortDict = Dict End Property Public Function SortArray(ByVal source_array As Variant, _ Optional sort_order As Excel.XlSortOrder = xlAscending) As Variant Dim aryList As Object Set aryList = CreateObject("System.Collections.ArrayList") Dim s As Variant For Each s In source_array Call aryList.Add(s) Next Select Case sort_order Case xlAscending ' 昇順でソート。 Call aryList.Sort Case xlDescending ' 昇順でソートののち、降順へ反転。 Call aryList.Sort Call aryList.Reverse End Select SortArray = aryList.ToArray End Function Public Property Get TournamentSortedOrderArray() Dim i As Long Dim arr() As Variant ReDim arr(1 To PlayerCount) For i = 1 To PlayerCount arr(i) = PlayerDict(RandomSortDict(TournamentOrderArray(i))) Next TournamentSortedOrderArray = arr End Property Public Sub CreateTournament() Sheets.Add After:=Sheets(Sheets.Count) Dim i As Long Dim j As Long ' 1回戦~準決勝戦まで。 For j = 1 To TournamentSize ' j回戦の一人一人について。 For i = 1 To (2 ^ TournamentSize) / (2 ^ (j - 1)) With Cells((2 * i - 1) * 2 ^ (j - 1), 3 * j - 2) Select Case j ' 横の罫線だけ、先に描画しておく。 ' ※セル結合により、人名入力セル内は罫線が無くなる。 Case 1 .Resize(, 2).Borders(xlEdgeBottom).Weight = xlThin Case Else .Resize(, 3).Offset(, -1).Borders(xlEdgeBottom).Weight = xlThin End Select .Resize(2).Merge .Resize(2).Borders.Weight = xlThin End With Next Next ' 決勝戦。 With Cells(2 ^ TournamentSize, 3 * j - 2) .Resize(, 2).Offset(, -1).Borders(xlEdgeBottom).Weight = xlThin .Resize(2).Merge .Resize(2).Borders.Weight = xlThin End With ' 縦の線。 For j = 1 To TournamentSize For i = 1 To 2 ^ (TournamentSize - j) Cells((2 ^ (j + 1)) * i - (3 * 2 ^ (j - 1) - 1), 3 * j - 1).Resize(2 ^ j).Borders(xlEdgeRight).Weight = xlThin Next Next ' 名前のセット Dim arr As Variant arr = TournamentSortedOrderArray For i = 1 To UBound(arr) Cells(i * 2 - 1, 1) = arr(i) Next ' シードを確認。 Dim PlayerName(2) As String If TournamentSize >= 2 Then For i = 1 To 2 ^ TournamentSize / 2 ' 一回戦の対戦同士、セル4つを一組で考える。 With Cells(4 * i - 3, 1) ' それぞれのセット順を配列に格納。 PlayerName(0) = .Item(1) & .Item(3) PlayerName(1) = .Item(1) PlayerName(2) = .Item(3) ' 両方ともに空白はあり得ない(その場合、トーナメントの山が一つ小さくなる)。 ' したがって、両者順位の積が0ならば、それは一回戦が不戦勝を意味する(=シード)。 If PlayerName(1) = vbNullString Or PlayerName(2) = vbNullString Then ' セルの結合しなおし。および、罫線の引き直し。 With .Resize(4) .UnMerge .Resize(, 2).Borders.LineStyle = xlNone .Value = vbNullString .Item(2) = PlayerName(0) .Borders.LineStyle = xlNone .Item(2).Resize(2).Merge .Item(2).Resize(2).Borders.Weight = xlThin .Item(2).Resize(2, 2).Borders(xlInsideHorizontal).Weight = xlThin End With End If End With Next End If End Sub Private Sub AdjustFormat() Dim i As Long ' トーナメント表の名前以外の幅を調整。 For i = 0 To TournamentSize Columns("B:C").Offset(, 3 * i).ColumnWidth = 3 Next ' 名前セルの罫線設定。 If Not BordersAroundName Then Columns(2).Borders(xlEdgeLeft).LineStyle = xlNone For i = 0 To TournamentSize Columns(3 * i + 1).Borders.LineStyle = xlNone Next End If ' 勝ち残り名前セルの設定。 If Not IntermediateName Then For i = 0 To TournamentSize - 2 Cells(1, i + 3).Resize(, 2).EntireColumn.Delete Next End If Columns(1).EntireColumn.AutoFit ' 空白セルの高さ設定。 For i = 1 To ActiveSheet.UsedRange.Rows.Count If Cells(i, 1).MergeCells = False Then Cells(i, 1).RowHeight = Cells(i, 1).RowHeight / 4 End If Next End Sub Private Sub Class_Terminate() Call AdjustFormat End Sub
ここまで作りこんでおくと、実際のトーナメント作成はこれだけで済む。
Sub Test() With New VBAProject.Tournament ' 名簿の範囲指定。 .init Sheets("sheet1").ListObjects(1).ListColumns(2).DataBodyRange ' トーナメント作成。 .CreateTournament End With End Sub
前回との大きな違いはやはり、人数の上限が無くなったことか。
5000人で試してみたところ、問題なく作成できた。
やれやれ、やっと形になった。と思ったら、先生から新たなリクエストが。
- 同じ道場の子は、1回戦で当たらないようにしたい。
- 形と組手で、同じ対戦相手と当たらないようにしたい。
トライしましょう。
ということで、今のところ第一弾完成。次回は、1回戦で同門対決にならない
仕掛けに挑戦です。
参考まで。
再びトーナメント表作成 ⑦ 式の一般化
前回からかなり時間が空いてしまったが、トーナメント表作成の仕組みを焼き直ししている。
infoment.hatenablog.com
今回は、ずっと以前に作成したクラスモジュールの一般化について。
なぜ前回から時間が空いてしまったか。理由は幾つかあるが、その一つとして
「ずっと以前に作成したクラスモジュールの一般化にてこずった」というのが
ある。前回作成したトーナメントは、実は32人までにしか対応していない。
そこで今回は、まず↓の部分を一般化することにした。
抜き出してみると、こんな感じだ。
If iMax >= 4 Then Dim SetArrayBoundary() As Variant ReDim SetArrayBoundary(1 To 2 ^ (iMax - 2) - 1) For i = 1 To UBound(SetArrayBoundary) Select Case i Case 1 SetArrayBoundary(i) = (2 ^ iMax) / 2 Case Is <= 3 SetArrayBoundary(i) = (2 ^ iMax) / 4 * (2 * i - 3) Case Is <= 7 SetArrayBoundary(i) = (2 ^ iMax) / 8 * (2 * i - 7) Case Is <= 15 SetArrayBoundary(i) = (2 ^ iMax) / 16 * (2 * i - 15) Case Is <= 31 SetArrayBoundary(i) = (2 ^ iMax) / 32 * (2 * i - 31) End Select Next End If
Select Case で、小刻みに範囲を変化させている。これを一つに統合することを
試みたのだが、集中して考える時間がなかなか取れず、今に至った訳で。まず、
↓ の部分に変数 i が登場しない。
SetArrayBoundary(i) = (2 ^ iMax) / 2
この部分は切り分けるか?と考えては、思考停止の繰り返し。しかしよく見みれば
この部分、正しくは
SetArrayBoundary(i) = (2 ^ iMax) / 2 * (2 * i - 1)
であることに気が付いた。これに気づいた後は、早かった。
If iMax >= 4 Then Dim SetArrayBoundary() As Variant ReDim SetArrayBoundary(1 To 2 ^ (iMax - 2) - 1) For i = 1 To UBound(SetArrayBoundary) j = 2 ^ WorksheetFunction.RoundUp(WorksheetFunction.Log(i + 1, 2), 0) SetArrayBoundary(i) = (2 ^ iMax) / j * (2 * i - j + 1) Cells(i, 1) = i Cells(i, 4) = SetArrayBoundary(i) Next End If
ポイントは、iを直近の2の累乗に切り上げる点にあった。
これで、Select Caseによる場合分けが不要になった。よし、これで先に進める。
そう思ってから、気が付いた。この部分、新しく作っている範囲に含まれてる
から、わざわざ一般化する必要なかった・・・。
というわけで、今日からまた再開です。
参考まで。
再びトーナメント表作成 ⑥ クラスモジュール作成
先日から、トーナメント表作成の仕組みを焼き直ししている。
infoment.hatenablog.com
今回は、これを一旦クラスモジュールに作り替えてみた。
クラスモジュールにした理由は至って単純で、複数回登場する値を使いまわししたかったら。しかし未熟の致すところで、至るところに自ら落とし穴を掘っては嵌りの繰り返しで、前回からこんなに時間が経ってしまった。
クラスモジュール化が判っているなら、最初からそのように作る方がよさそうだ。
という訳で、大晦日にようやく纏まった時間が取れて、形になったのがこちら。
Option Explicit Public PlayerRange As Range Public PlayerDict As Object Public InversFlag As Boolean Public StartNumber As Long Dim Wf As WorksheetFunction Dim NetPalyerNumber As Long Dim TournamentSize As Long Dim PlayerCount As Long Private Sub Class_Initialize() Set Wf = WorksheetFunction End Sub Public Sub init(Optional player_range As Range, _ Optional invers_flag As Boolean = False, _ Optional start_number As Long = 1) Dim arr As Variant Dim i As Long If Not player_range Is Nothing Then Set PlayerRange = player_range Else Set PlayerRange = Selection End If InversFlag = invers_flag Set PlayerDict = CreateObject("Scripting.Dictionary") Dim r As Range For Each r In PlayerRange If r.Value <> vbNullString Then NetPalyerNumber = NetPalyerNumber + 1 PlayerDict(NetPalyerNumber) = r.Value End If Next ' 一回戦の人の配置を、前大会の何位からランダムに入れ替えるか。 StartNumber = start_number ' 選手の数から求めるトーナメントサイズ(決勝までの段数)。 TournamentSize = Wf.Ceiling_Math(Wf.Log10(NetPalyerNumber), Wf.Log10(2)) / Wf.Log10(2) ' 一回戦の人数。 PlayerCount = 2 ^ TournamentSize End Sub Private Property Get TournamentOrderArray() As Variant ' 作業用配列。最終的に戻り値となる。 Static arr As Variant If Not IsEmpty(arr) Then TournamentOrderArray = arr Exit Property End If ReDim arr(1 To 1) ' 初期値。選手が一人のとき。 arr(1) = 1 If PlayerCount = 1 Then TournamentOrderArray = arr Exit Property End If Dim i As Long Dim j As Long ' 選手の数に併せて大きくなるトーナメントの仮格納用配列。 Dim TempArray As Variant For i = 1 To TournamentSize ReDim TempArray(1 To UBound(arr) * 2) For j = 1 To UBound(arr) ' 元の配列内の数値を、上から奇数で、下から偶数でセット。 TempArray(j) = arr(j) * 2 - 1 TempArray(UBound(TempArray) + 1 - j) = arr(j) * 2 Next arr = TempArray Next ' 上位半数の相手を改めてセット。 ' 例えば32人のトーナメントでは、1と32が、2と31が、3と30が・・・nと33-nが ' ペアになるようにする。※優先的に前回上位者を一回戦シードとするため。 For i = 1 To PlayerCount If arr(i) > PlayerCount / 2 Then ' 4人一組で評価するとき、2番目は1番目と、3番目は4番目と ' ペアになることから、4で割った余りで処理を変更する。 Select Case i Mod 4 Case 2: arr(i) = PlayerCount - arr(i - 1) + 1 Case 3: arr(i) = PlayerCount - arr(i + 1) + 1 End Select End If Next TempArray = arr ' 配列の最後を1位にしたい場合用。配列の順序をひっくり返す。 If InversFlag Then For i = 1 To UBound(arr) arr(i) = TempArray(UBound(TempArray) - i + 1) Next End If TournamentOrderArray = arr End Property Private Property Get RandomSortDict() As Object Static Dict As Object If Not Dict Is Nothing Then Set RandomSortDict = Dict Exit Property End If ' 作業用配列。 Dim TempArray As Variant ReDim TempArray(1 To NetPalyerNumber) ' 戻り値用配列。 Dim arr As Variant ReDim arr(1 To NetPalyerNumber) Dim i As Long For i = 1 To NetPalyerNumber ' ソートしない範囲。 If i < StartNumber Then TempArray(i) = i ' ソート範囲。iMax以上の桁でソートする。 ' このときtotalの桁数範囲には、元の数字が保存されている。 Else TempArray(i) = CLng(WorksheetFunction.RandBetween(1, 100) * _ (10 ^ Len(CStr(PlayerCount))) + i) End If Next ' 配列ソート。 TempArray = SortArray(TempArray) ' 保存された元の数字を取り出す。 ' SortArray関数を使用すると、配列が0始まりになるので注意。 For i = NetPalyerNumber To 1 Step -1 arr(i) = Right(TempArray(i - 1), CLng(Len(CStr(NetPalyerNumber)))) * 1 Next Set Dict = CreateObject("Scripting.Dictionary") For i = 1 To NetPalyerNumber Dict(i) = arr(i) Next If NetPalyerNumber < PlayerCount Then For i = NetPalyerNumber + 1 To PlayerCount Dict(i) = i Next End If Set RandomSortDict = Dict End Property Public Function SortArray(ByVal source_array As Variant, _ Optional sort_order As Excel.XlSortOrder = xlAscending) As Variant Dim aryList As Object Set aryList = CreateObject("System.Collections.ArrayList") Dim s As Variant For Each s In source_array Call aryList.Add(s) Next Select Case sort_order Case xlAscending ' 昇順でソート。 Call aryList.Sort Case xlDescending ' 昇順でソートののち、降順へ反転。 Call aryList.Sort Call aryList.Reverse End Select SortArray = aryList.ToArray End Function Public Property Get TournamentSortedOrderArray() Dim i As Long Dim arr() As Variant ReDim arr(1 To PlayerCount) For i = 1 To PlayerCount arr(i) = PlayerDict(RandomSortDict(TournamentOrderArray(i))) Next TournamentSortedOrderArray = arr End Property
テスト用のマクロがこちら。
Sub Hoge() Dim arr As Variant With New VBAProject.Tournament .init Range("B2:B27"), True, 9 arr = .TournamentSortedOrderArray End With Dim i As Long Dim c As Long For i = 1 To UBound(arr) If arr(i) <> vbNullString Then c = c + 1 arr(i) = StrConv(Format(c, "00:"), vbWide) & arr(i) Else arr(i) = "(欠番)" End If Next Range("E2").Resize(UBound(arr)) = WorksheetFunction.Transpose(arr) End Sub
実行した結果がこちらになる。
作った本人も毎回こんがらがるので、改めて解説。
例えば、上から20番目には誰をセットするか。
- 20番目には、昨年度11位の人をセットする。
- しかし昨年度11位の人は、ランダム並び替えで23位の人と入れ替えている。
- 23位の人は、寺井 友以乃さんである。
という三段階のプロセスを踏んでいる。
ようやくここまで辿り着いた。
次回からこれを、トーナメント表に成形します。
参考まで。
再びトーナメント表作成 ⑤ 人を配置してみる
昨日は、トーナメント表作成時に全大会の上位入賞者を良い感じに
分散する関数における「考慮漏れ」の改修について紹介した。
infoment.hatenablog.com
今日は、実際に人の名前をトーナメント表にセットしてみよう。
今日までに作成した仕組みは、以下のとおり。
- 選手数を引数とし、トーナメント表のサイズを計算したうえで、前大会の上位入賞者同士が初戦で当たらないよう組合せを分散する関数。
- 1からnまでの自然数について、任意の数以降をランダムに並べ替える関数。
これを組み合わせて、なんちゃって個人情報から参戦した30名の選手でトーナメント表を作成してみた。
なお、今回の作成に当たっては、上記「1.」の内容を収めた辞書を作成している。
Function GetRandomSortDict(total As Long, Optional start_number As Long = 1) As Object Dim Dict As Object Set Dict = CreateObject("Scripting.Dictionary") Dim arr As Variant arr = GetRandomSortArray(total, start_number) Dim i As Long For i = 1 To total Dict(i) = arr(i) Next Set GetRandomSortDict = Dict End Function
ただしこの辞書は、後ほど消滅する(他の関数に統合される)可能性が高い。
ということで、テストしてみた。今回は30名で人数固定しているが、後ほど
一般化する予定だ。
Sub Test() Dim Dict_Tournament As Scripting.Dictionary Set Dict_Tournament = New Scripting.Dictionary Dim Tb As ListObject Set Tb = ActiveSheet.ListObjects(1) Dim i As Long For i = 1 To 30 With Tb.DataBodyRange Dict_Tournament(i) = Format(.Cells(i, 1), "00:") & _ .Cells(i, 2) & _ "(" & .Cells(i, 3) & ")" End With Next Dim arr As Variant arr = GetTournamentOrderArray(30, True) Dim Dict_Player As Scripting.Dictionary Set Dict_Player = GetRandomSortDict(30, 9) For i = 31 To 32 Dict_Player(i) = i Next For i = 1 To 32 If Dict_Player.Exists(arr(i)) Then arr(i) = Format(arr(i), "00_") & Dict_Tournament(Dict_Player(arr(i))) Else arr(i) = Format(arr(i), "00_") End If Next Range("E1").Resize(32) = WorksheetFunction.Transpose(arr) End Sub
結果は以下のとおり。3回作成した結果を横並びにしてみた。
※各位の名前は、「今回並び順_前大会順位:氏名(出身県)」で表示。
- 何度やっても指定した上位8名は均等に散らばっていて、初戦では戦わない。
- 何度やっても上位から優先的にシードされている。
ということで、ややこしかった人の配置も一段落。ロジックは大体固まったので、ここからさらにブラッシュアップしてみよう。
次回に続きます。
参考まで。
再びトーナメント表作成 ④ 前大会の入賞者を良い感じに分散したい、を修正
先日から、トーナメント表の作成に取り組んでいる。数回前は、
前大会の入賞者を良い感じに分散させて、例えば前回の1位と2位が
一回戦で対戦しないよう工夫してみた。
infoment.hatenablog.com
この中で重大な考慮漏れがあったため、今回はそれを修正する。
例えば31人でトーナメントを作成する場合。今回の考え方では、架空の32人目も
加えてトーナメント表を作成し、32人目と対戦する人を不戦勝にする(=シード)
こととしていた。
しかし前回作成した関数では、1位と32位は対戦しない。そこも含めて、均等に
分散されてしまうからだ。
そこで、良い感じに分散したのちに、改めて1位と32位が、2位と31位が、3位と
30位が、・・・n位と32-n+1位が対戦するよう修正してみた。
Function GetTournamentOrderArray(player_count As Long, _ Optional invers_flag As Boolean = False) As Variant ' 作業用配列。最終的に戻り値となる。 Dim arr() As Variant ReDim arr(1 To 1) ' 初期値。選手が一人のとき。 arr(1) = 1 If player_count = 1 Then GetTournamentOrderArray = arr Exit Function End If ' WorksheetFunctionの表記短縮用。 Dim Wf As WorksheetFunction Set Wf = WorksheetFunction ' 選手の数から求めるトーナメントサイズ。 Dim TournamentSize As Long TournamentSize = 10 ^ (Wf.Ceiling_Math(Wf.Log10(player_count), Wf.Log10(2))) Dim i As Long Dim j As Long ' 選手の数に併せて大きくなるトーナメントの仮格納用配列。 Dim TempArray As Variant ' 底の変換公式を用いて、配列を何回2倍するか求めている。 For i = 1 To Wf.Log(TournamentSize) / Wf.Log(2) ReDim TempArray(1 To UBound(arr) * 2) For j = 1 To UBound(arr) ' 元の配列内の数値を、上から奇数で、下から偶数でセット。 TempArray(j) = arr(j) * 2 - 1 TempArray(UBound(TempArray) + 1 - j) = arr(j) * 2 Next arr = TempArray Next ' 上位半数の相手を改めてセット。 ' 例えば32人のトーナメントでは、1と32が、2と31が、3と30が・・・nと33-nが ' ペアになるようにする。※優先的に前回上位者を一回戦シードとするため。 For i = 1 To TournamentSize If arr(i) > TournamentSize / 2 Then ' 4人一組で評価するとき、2番目は1番目と、3番目は4番目と ' ペアになることから、4で割った余りで処理を変更する。 Select Case i Mod 4 Case 2: arr(i) = TournamentSize - arr(i - 1) + 1 Case 3: arr(i) = TournamentSize - arr(i + 1) + 1 End Select End If Next TempArray = arr ' 配列の最後を1位にしたい場合用。配列の順序をひっくり返す。 If invers_flag Then For i = 1 To UBound(arr) arr(i) = TempArray(UBound(TempArray) - i + 1) Next End If GetTournamentOrderArray = arr End Function
それでは再度、↓で確認してみよう。
Sub Test() Dim arr As Variant Dim i As Long For i = 1 To 18 arr = GetTournamentOrderArray(i, True) ' 貼り付けて確認。 Cells(1, i).Resize(UBound(arr)) = WorksheetFunction.Transpose(arr) Next End Sub
結果がこちら。
今度こそ、前大会の上位者から優先的にシードされるようになった。
危ない、危ない。次回に続きます。
参考まで。
再びトーナメント表作成 ③ 順番をランダムに並べ替えたい-2
昨日は、トーナメントに於いて対戦カードをランダムにするために、
配列の順番をランダムに並べ替えることに挑戦した。
infoment.hatenablog.com
書き始めた時刻が遅かったこともあり、残念ながらタイムアウトとなった。
今日は、昨日の続きから。
理屈の部分は、昨日の内に終えている。
ここで要件として、
n番目以降からランダムソートしたい
を追加してみよう。目的は、例えば
前回入賞者(1~4位)はトーナメント内の位置を固定し、5位以降の選手はランダムに配置したい
などの要求に対応するためだ。
以上を踏まえて作成したのがこちら。なお、配列ソートの関数については、
以前作成したものを流用している。
Function GetRandomSortArray(total As Long, Optional start_number As Long = 1) As Variant ' 作業用配列。 Dim TempArray As Variant ReDim TempArray(1 To total) ' 戻り値用配列。 Dim arr As Variant ReDim arr(1 To total) Dim i As Long For i = 1 To total ' ソートしない範囲。 If i < start_number Then TempArray(i) = i ' ソート範囲。iMax以上の桁でソートする。 ' このときtotalの桁数範囲には、元の数字が保存されている。 Else TempArray(i) = CLng(WorksheetFunction.RandBetween(1, 100) * _ (10 ^ Len(CStr(total))) + i) End If Next ' 配列ソート。 TempArray = SortArray(TempArray) ' 保存された元の数字を取り出す。 ' SortArray関数を使用すると、配列が0始まりになるので注意。 For i = total To 1 Step -1 arr(i) = Right(TempArray(i - 1), CLng(Len(CStr(total)))) * 1 Next GetRandomSortArray = arr End Function
Public Function SortArray(ByVal source_array As Variant, _ Optional sort_order As Excel.XlSortOrder = xlAscending) As Variant Dim aryList As Object Set aryList = CreateObject("System.Collections.ArrayList") Dim s As Variant For Each s In source_array Call aryList.Add(s) Next Select Case sort_order Case xlAscending ' 昇順でソート。 Call aryList.Sort Case xlDescending ' 昇順でソートののち、降順へ反転。 Call aryList.Sort Call aryList.Reverse End Select SortArray = aryList.ToArray End Function
それでは、テストしてみよう。1~32の数字について、5番目以降から
ランダムソートしてみる。
Sub Test() Dim arr As Variant arr = GetRandomSortArray(32, 5) Range("A1:A32") = WorksheetFunction.Transpose(arr) End Sub
結果がこちら。意図したとおり、1~4は順番通りで、5番目からランダムに
並び替えられている。
これはこれで、ちょっとややこしかったかな。
次回に続きます。
参考まで。
再びトーナメント表作成 ③ 順番をランダムに並べ替えたい-1
昨日はトーナメント表作成において、なるべく全大会の入賞者が初戦で
ぶつからないよう、良い感じに分散させることに挑戦した。
infoment.hatenablog.com
しかしそうは言っても、完全ランダムで配置したいこともあるだろう。
ということで今日は、配列をランダムに並べ替えることに挑戦する。
実は今までにも、何度か挑戦したことがあった。そのときの作戦はこうだ。
例)1~32をランダムな順序で配列に格納したい。
- 辞書2を作成する。辞書2のkeyおよびitemには順に1~32をセットしておく。
- 1~32の数字を乱数で作成する。
- 作成した数字が辞書2のキーに存在するか、評価する。存在する場合、辞書1のkeyにセットする。このとき、itemの値は不問。
- 上記でセットしたkeyの値を、辞書2のkeysからRemoveする。
- 繰り返し(2. へ戻る)
これにより、どの数字が使われたか、使われていないかが判るという理屈。
最初の内は乱数の生成結果が重複することはまれだが、後半になるにつれ
何度も乱数生成を要する確率が上がっていくことになる。
↓ 実際試してみると、このケースでは合計で93回も乱数を生成している。
同じケースを1000回繰り返し、乱数生成の合計平均を求めた結果は、
約130回であった。やはり、結構かかってしまう。
そこで、もっと時間が掛からない方法はないものかと考えて、行きついた
結果がこちら。
例)1~32をランダムな順序で配列に格納したい。
- 1~32の各々に、乱数「1~100」×100を乗じて足し算する。
- 足し算した結果を昇順(または降順)ソートする。
- ソートされた順に、下二桁を取り出して配列に格納する。
↓ まず並べ替え用の数を足して、
↓これを並べ替えて、下二桁を抽出。
並べ替えは後から加えたランダムな数が優先される一方で、下二桁は元の数字が
保存されているので、これでランダム並べ替えが成立するという仕組みだ。
と、ここで時間切れ。明日に続きます。
参考まで。