分割統治法とは:基本原理と漸近解析(マスター定理)・代表アルゴリズムと並列化/実装の実践ガイド

分割統治法とは — 概要と直感

分割統治法(ぶんかつとうちほう、divide and conquer)は、問題を小さな部分問題に分割し、それぞれを解いてから結果を結合して元の問題の解を得るアルゴリズム設計の基本パターンです。大きな問題を再帰的に扱うことで、単純かつ効率的な解法を得られる場合が多く、ソフトウェア開発やアルゴリズム設計において最も重要な手法の一つです。

基本的な考え方

分割統治法は本質的に次の3ステップで構成されます。

  • 分割(Divide):問題をより小さな同種の部分問題に分割する。
  • 統治(Conquer):各部分問題を再帰的に解く。部分問題が十分小さければ直接解く(基底ケース)。
  • 結合(Combine):部分問題の解を組み合わせて、元の問題の解を構築する。

この単純な枠組みが、効率的なアルゴリズムを生み出しますが、重要なのは「分割の仕方」と「結合のコスト」です。分割で得られる部分問題の数やサイズ、結合にかかる計算量がアルゴリズム全体の性質を決定します。

典型的な例

分割統治法を使った代表的アルゴリズムをいくつか挙げます。

  • バイナリサーチ(Binary Search) — 配列を半分に分割して探索を再帰的に行う。時間計算量は O(log n)。
  • マージソート(Merge Sort) — 配列を半分に分けてそれぞれソートし、マージして一つのソート済み配列にする。時間計算量は Θ(n log n)。
  • クイックソート(Quick Sort) — ピボットで配列を2つの部分に分けて再帰的にソートする。平均 O(n log n)、最悪 O(n^2)。
  • Karatsuba 乗算 — 大きな整数乗算を分割して3つの再帰的乗算で実行し、O(n^{log_2 3}) ≈ O(n^{1.585}) を達成。
  • Strassen 行列乗算 — 8回の乗算を7回に減らし、O(n^{log_2 7}) ≈ O(n^{2.807}) を達成。

漸近解析:漸化式とマスター定理

分割統治法の時間計算量は通常、次の形の漸化式で表されます。

T(n) = a T(n / b) + f(n)

ここで、a は部分問題の個数、n/b は各部分問題のサイズ(対等に分割した場合)、f(n) は分割・結合にかかる追加コストです。これに対してマスター定理(Master Theorem)を用いると、T(n) の漸近評価が可能です。代表的なケースは以下の通りです。

  • もし f(n) = O(n^{c}) で c < log_b a なら、T(n) = Θ(n^{log_b a})。
  • もし f(n) = Θ(n^{log_b a} * log^k n) なら、T(n) = Θ(n^{log_b a} * log^{k+1} n)。
  • もし f(n) = Ω(n^{c}) で c > log_b a かつ正則性条件が満たされるなら、T(n) = Θ(f(n))。

この定理を適用すると、マージソート(a=2, b=2, f(n)=Θ(n))は T(n)=2T(n/2)+Θ(n) より Θ(n log n) であることが分かります。一方、バイナリサーチ(a=1, b=2, f(n)=Θ(1))は Θ(log n) になります。

具体的な擬似コード例

マージソートとバイナリサーチの簡単な擬似コードを示します。

function mergeSort(A):
  if length(A) ≤ 1:
    return A
  mid = length(A) / 2
  L = mergeSort(A[0:mid])
  R = mergeSort(A[mid:end])
  return merge(L, R)
function binarySearch(A, target):
  lo = 0; hi = length(A)-1
  while lo ≤ hi:
    mid = (lo + hi) // 2
    if A[mid] == target: return mid
    if A[mid] < target: lo = mid + 1
    else: hi = mid - 1
  return -1

分割統治法と並列化

分割統治法は自然に並列化に適しています。再帰的に得られる部分問題が独立であれば、それらを並列に処理できます。例えば、マージソートの左右の再帰呼び出しは独立なので、両方を同時に実行できます。

並列アルゴリズムでは「全作業量(work)」と「クリティカルパス長/スパン(span)」という指標が重要です。全作業量はシリアル実行時の総コストに相当し、スパンは依存関係に基づく最短実行時間です。理想的にはスピードアップは work/span に近づきますが、並列オーバーヘッドや負荷分散も考慮する必要があります。

分割統治法と動的計画法(DP)の違い

分割統治法と動的計画法はどちらも再帰的な考え方を使いますが、アプローチが異なります。分割統治法は問題を独立な部分問題に分け、それらを解いて結合します。一方で動的計画法は重複する部分問題を再利用(メモ化やボトムアップ)して計算量を削減します。

したがって、部分問題が独立で重複が少ない場合は分割統治法が適しており、重複が多く且つ部分問題の依存関係が明確な場合はDPが有利です。場合によっては「分割統治法+メモ化」でハイブリッドにすることも有効です。

分割統治法の実用的な注意点と落とし穴

  • 結合コストの過小評価:分割しても結合に要するコストが大きければ全体効率は悪化します。マージソートでは結合に Θ(n) がかかりますが、それでも2分割の利点が勝ります。
  • 再帰オーバーヘッド:関数呼び出しやメモリ割当などの定常オーバーヘッドが無視できない場合、入力サイズが小さいと分割統治法は非効率になります。実装上は基底ケースの閾値で挿入ソート等に切り替えることが多いです。
  • 不均衡分割の問題:クイックソートの最悪ケース(偏った分割)では O(n^2) になるため、ピボット選択戦略(ランダム化、median-of-three 等)が重要です。
  • メモリ消費:配列の分割や一時配列の使用が多いアルゴリズムはメモリ消費が増える。インプレースにする工夫が必要な場合がある。

応用分野 — 幅広い用途

分割統治法は純粋なアルゴリズム設計だけでなく、システム設計やデータ処理にも応用されます。

  • 大規模データ処理:MapReduce は多くの場合、データを分割して並列処理(Map)、結果を結合(Reduce)する分割統治的な構造を持ちます。
  • 計算幾何学:最近点対問題や凸包アルゴリズムなど、多くの幾何アルゴリズムで分割統治法が用いられます。
  • マルチコア/分散処理:独立な部分問題を別々のコアやノードで処理して集約するアーキテクチャと相性が良いです。
  • 数値線形代数:行列分割やハイパフォーマンスな行列乗算(Strassen 等)は分割統治で高速化されます。

具体的な工夫と実装テクニック

  • 基底ケースの調整:小さい n に対しては再帰オーバーヘッドを避けるため、挿入ソートなどの単純アルゴリズムに切り替える。
  • メモリ再利用:再帰ごとに新しい配列を割り当てないように、一時配列を使い回すことでメモリとコピーコストを下げる。
  • ランダム化:クイックソートのピボットをランダムに選ぶことで平均的な性能を保証する。
  • 並列フレームワークの活用:OpenMP、ThreadPool、Fork/Join(Java)等で左右の再帰を並列化する。

まとめ — いつ分割統治法を選ぶべきか

分割統治法は、「問題が自然に独立した小さな同種の部分問題に分割でき、結合が比較的容易である」場合に最も有効です。アルゴリズムの設計段階で部分問題数(a)、分割比(b)、結合コスト(f(n))を意識して漸近解析を行えば、分割統治法が適切かどうかを判断できます。並列実行やメモリ面の工夫を組み合わせることで、実用上の性能をさらに引き上げられます。

参考文献