Excel VBAによる条件付き数珠順列の全列挙アルゴリズムの構築
Excel VBAを用いたプログラミングにおいて、順列や組み合わせの生成は頻出する課題ですが、その中でも「数珠順列(円順列において回転や反転で同一視されるもの)」、さらに「特定の配置条件」が加わった問題は、アルゴリズムの難易度を一段引き上げます。本記事では、ツイッター等で頻出する「特定の条件を満たす要素の配置」を数珠順列として網羅的に抽出する手法を、プロフェッショナルな視点から詳細に解説します。
数珠順列における数学的定義とVBAでのアプローチ
まず、数珠順列の概念を整理します。通常の順列が「一列に並べる」ものであるのに対し、円順列は「円形に並べ、回転して重なるものを同一視する」ものです。さらに数珠順列は、円形に並べたものを「裏返して重なるものも同一視する」という特徴があります。
VBAでこれを実装する際、単純な順列生成(Permutation)をベースに、以下の3つのステップを踏むのが定石です。
1. 基本となる順列の生成(再帰的呼び出しを利用)。
2. 生成された順列に対して「円順列としての重複」を排除する正規化。
3. 「反転」を考慮した判定による数珠順列としての重複排除。
ここに「配置条件(例:AとBは隣り合わない、Cは必ず特定の場所に置く等)」を組み込むことで、問題の解空間を効率的に絞り込むことができます。
再帰処理による順列生成とフィルタリングの最適化
数珠順列を求める際、全順列を生成してから後処理で重複を消すと、計算量が指数関数的に爆発します。要素数がnであればn!の計算量が必要となり、n=10でも360万通りを超えます。実務上は、再帰処理の途中で「条件を満たさない枝」をカットする「枝刈り(Pruning)」が不可欠です。
特にツイッターなどで出題されるパズル問題は、特定の要素の固定や、隣接制約が課されることが多いため、再帰関数の引数に現在の状態を渡し、条件に反した瞬間に探索を打ち切る実装が求められます。
サンプルコード:条件付き数珠順列の生成ロジック
以下に、要素を配列として受け取り、条件を満たす数珠順列をイミディエイトウィンドウに出力するプロフェッショナルな実装例を示します。ここでは簡略化のため、条件として「特定の2要素が隣接しない」という制約を想定しています。
Option Explicit
' メインプロシージャ
Sub GenerateNecklacePermutations()
Dim elements As Variant
elements = Array("A", "B", "C", "D")
Dim used() As Boolean
ReDim used(LBound(elements) To UBound(elements))
Dim result() As String
ReDim result(LBound(elements) To UBound(elements))
' 結果格納用のコレクション(重複排除のため)
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
SolveRecursive elements, used, result, 0, dict
' 出力
Dim key As Variant
For Each key In dict.Keys
Debug.Print key
Next key
End Sub
' 再帰処理
Sub SolveRecursive(elements, used, result, depth, dict)
If depth > UBound(elements) Then
' ここで数珠順列としての正規化を行う
Dim normalized As String
normalized = NormalizeAsNecklace(result)
dict(normalized) = True
Exit Sub
End If
Dim i As Long
For i = LBound(elements) To UBound(elements)
If Not used(i) Then
' 条件チェック:例えばAとBが隣り合わない等の制約をここに記述
If depth > 0 Then
If (result(depth - 1) = "A" And elements(i) = "B") Or _
(result(depth - 1) = "B" And elements(i) = "A") Then
GoTo ContinueLoop
End If
End If
used(i) = True
result(depth) = elements(i)
SolveRecursive elements, used, result, depth + 1, dict
used(i) = False
End If
ContinueLoop:
Next i
End Sub
' 数珠順列としての正規化(回転と反転で同一視)
Function NormalizeAsNecklace(arr) As String
Dim n As Long: n = UBound(arr) + 1
Dim s1 As String, s2 As String
Dim i As Long, j As Long
Dim minS As String
' 回転の最小値を探す
minS = Join(arr, "")
For i = 0 To n - 1
s1 = ""
For j = 0 To n - 1
s1 = s1 & arr((i + j) Mod n)
Next j
If s1 < minS Then minS = s1
Next i
' 反転の最小値を探す
Dim revArr As Variant: revArr = arr
For i = 0 To n - 1
revArr(i) = arr(n - 1 - i)
Next i
For i = 0 To n - 1
s2 = ""
For j = 0 To n - 1
s2 = s2 & revArr((i + j) Mod n)
Next j
If s2 < minS Then minS = s2
Next i
NormalizeAsNecklace = minS
End Function
実務におけるパフォーマンス向上のためのアドバイス
1. **Dictionaryオブジェクトの活用**:
数珠順列の重複排除において、`Scripting.Dictionary`は必須です。キーに正規化後の文字列を格納することで、O(1)の計算量で重複チェックが可能です。
2. **正規化の効率化**:
上記のコードでは反転を計算してから比較していますが、要素数が多くなる場合は「辞書順最小表現」を求めるアルゴリズム(Boothのアルゴリズム等)を応用することで、さらなる高速化が図れます。
3. **メモリ管理**:
再帰呼び出しの深さが深い場合、スタックオーバーフローに注意が必要です。VBAの再帰深度には限界があるため、要素数が15を超えるような大規模な順列計算を行う場合は、再帰を使わず、スタックを明示的に管理する「非再帰的アプローチ」への書き換えを検討してください。
4. **条件分岐の分離**:
実務では条件が複雑化しがちです。条件判定部分を別関数(例:`IsSatisfyCondition(arr)`)として切り出し、メインの探索ロジックから独立させることで、保守性と可読性が飛躍的に向上します。
まとめ:VBAでロジックを組む意義
Excel VBAは単なるデータ集計ツールではありません。今回扱ったような数珠順列の列挙は、組み合わせ爆発をいかに制御するかという、計算機科学の基礎的な知見を問うものです。ツイッターの出題のような「一見単純だが奥が深い問題」をVBAで解くことは、論理的思考力を鍛える最良の訓練となります。
プロフェッショナルなエンジニアとして大切なのは、コードの「正しさ」だけでなく、いかに計算資源を節約し、拡張性のある設計を行うかという点にあります。今回のコードをベースに、皆さんの業務や趣味のパズルに応用し、より洗練されたアルゴリズムを構築してみてください。VBAの可能性は、皆さんのロジック次第で無限に広がります。
