Re-translate the Japanese version (#1871)

* Retranslate Japanese docs with GPT-5.4

* Retranslate Japanese code with GPT-5.4
This commit is contained in:
Yudong Jin
2026-03-30 07:30:15 +08:00
committed by GitHub
parent fe6443235b
commit d7b2277d2b
1444 changed files with 83312 additions and 8363 deletions
@@ -1,44 +1,44 @@
# 分割統治索戦略
# 分割統治索戦略
私たちは検索アルゴリズムが主に2つのカテゴリに分類されることを学びました
私たちはすでに学んだように、探索アルゴリズムは大きく二つに分けられる
- **総当たり検索**:データ構造を走査することで実され、時間計算量は $O(n)$ で
- **適応索**独特なデータ組織形式や事前情報を利用し、時間計算量は $O(\log n)$ または $O(1)$ に達することができます
- **力ずく探索**:データ構造を走査することで実され、時間計算量は $O(n)$ である
- **適応的探索**固有のデータ構造や事前情報を利用し、時間計算量は $O(\log n)$ 、さらには $O(1)$ に達しうる
実際、**時間計算量が $O(\log n)$ の索アルゴリズムは通常分割統治戦略に基づいています**。えば二分探索や木などです
実際、**時間計算量が $O(\log n)$ の索アルゴリズムは通常分割統治戦略に基づいて実装される**。たとえば二分探索や木構造である
- 二分探索の各ステップは、問題(配列内でターゲット要素を索する)をより小さな問題(配列の半分でターゲット要素を索する)に分し、配列が空になるかターゲット要素が見つかるまで続けます
- 木は分割統治のアイデアを表現し、二分探索木、AVL木、ヒープなどのデータ構造では、様々な操作の時間計算量は $O(\log n)$ で
- 二分探索の各ステップは、問題(配列内で目標要素を索すること)を小さな問題(配列の半分で目標要素を索すること)に分し、この過程は配列が空になるか目標要素が見つかるまで続
-構造は分割統治の考え方を代表するものであり、二分探索木、AVL 木、ヒープなどのデータ構造では、さまざまな操作の時間計算量はいずれも $O(\log n)$ である
二分探索の分割統治戦略は以下の通りです
二分探索の分割統治戦略は以下のとおりである
- **問題を分割できる**:二分探索は元の問題(配列内での検索)を部分問題(配列の半分での検索)に再帰的に分割し、中間要素とターゲット要素を比較することで実現されます
- **部分問題は独立している**:二分探索では、各ラウンドで一つの部分問題を処理し、の部分問題影響されません
- **部分問題の解をマージする必要ない**:二分探索は特定の要素を見つけることを目的としているため、部分問題の解をマージする必要がありません。部分問題が解決されると、元の問題も解決されます
- **問題は分解できる**:二分探索は元の問題(配列内で探索すること)を部分問題(配列の半分で探索すること)へ再帰的に分解する。これは中央要素と目標要素を比較することで実現され
- **部分問題は独立している**:二分探索では、各ラウンドで一つの部分問題だけを処理し、ほかの部分問題影響を受けない
- **部分問題の解を統合する必要ない**:二分探索は特定の要素を探すことを目的としているため、部分問題の解を統合する必要がない。部分問題が解決されると、元の問題も同時に解決され
分割統治は検索効率を向上させることができます。なぜなら、総当たり検索はラウンドごとに1つの選択肢しか除できませんが、**分割統治は選択肢の半分を除できるからです**
分割統治が探索効率を高められる本質的な理由は、力ずく探索では各ラウンドで一つの候補しか除できないのに対し、**分割統治による探索では各ラウンドで候補の半分を除できる**からである
### 分割統治に基づく二分探索の実装
### 分割統治に基づく二分探索
前の章では、二分探索反復に基づいて実装されました。今度は、分割統治(再帰)に基づいて実装します
前の章では、二分探索を漸化式(反復に基づいて実装した。ここでは分割統治(再帰)に基づいてこれを実装する
!!! question
長さ $n$ の順序付けられた配列 `nums` が与えられ、すべての要素一意である場合、要素 `target`見つけてください
長さ $n$ の順配列 `nums` が与えられ、そのすべての要素一意である要素 `target`探索せよ
分割統治の観点から、索区間 $[i, j]$ に対応する部分問題を $f(i, j)$ と表します。
分割統治の観点から、索区間 $[i, j]$ に対応する部分問題を $f(i, j)$ とす。
元の問題 $f(0, n-1)$ から開始して、以下のステップで二分探索を実行します
元の問題 $f(0, n-1)$ を出発点として、次の手順で二分探索を行う
1. 索区間 $[i, j]$ の中点 $m$ を計算し、それを使用して検索区間の半分を除去します
2. 半分のサイズに縮小された部分問題を再帰的に解決します。これは $f(i, m-1)$ または $f(m+1, j)$ になる可能性があります
3. `target` が見つかるか区間が空になってリターンするまで、ステップ `1.``2.` を繰り返します。
1. 索区間 $[i, j]$ の中点 $m$ を計算し、それに基づいて探索区間の半分を除外する
2. 規模が半分に縮小された部分問題を再帰的に解く。候補は $f(i, m-1)$ または $f(m+1, j)$ である
3. `1.``2.` の手順を繰り返し、`target` が見つかるか区間が空になったら返す。
以下の図は、配列内で要素 $6$ を探す二分探索分割統治過程を示しています
の図は、配列内で要素 $6$ を二分探索する分割統治過程を示してい
![二分探索の分割統治過程](binary_search_recur.assets/binary_search_recur.png)
![二分探索の分割統治過程](binary_search_recur.assets/binary_search_recur.png)
実装コードでは、問題 $f(i, j)$ を解決するために再帰関数 `dfs()` を宣言します:
実装コードでは、再帰関数 `dfs()` を宣言して問題 $f(i, j)$ を解く。
```src
[file]{binary_search_recur}-[class]{}-[func]{binary_search}
@@ -1,70 +1,70 @@
# 二分木構築問題
# 二分木構築問題
!!! question
二分木の前順走査 `preorder` シーケンスと中順走査 `inorder` シーケンスが与えられた場合、二分木を構築しそのルートノードを返してください。二分木に重複するノード値がないと仮定します(以下の図に示すように)。
二分木の前順走査 `preorder` と中順走査 `inorder` が与えられたとき、これらから二分木を構築しそのノードを返してください。二分木には値が重複するノードが存在しないものとします(下図のとおり)。
![二分木構築のサンプルデータ](build_binary_tree_problem.assets/build_tree_example.png)
![二分木構築する例のデータ](build_binary_tree_problem.assets/build_tree_example.png)
### 分割統治問題かどうかの判定
### 分割統治問題かどうかを判断する
`preorder``inorder` シーケンスから二分木を構築する元の問題は、典型的な分割統治問題です。
元の問題は `preorder``inorder` から二分木を構築することであり、典型的な分割統治問題です。
- **問題分解できる**:分割統治の観点から、元の問題を2つの部分問題(左の部分木の構築と右部分木の構築)とルートノード初期化という1つの操作に分割できます。各部分木(部分問題)について、同じアプローチを継続的に適用しより小さな部分木(部分問題)に分割し、最小の部分問題(空部分木)に到達するまで続けます。
- **部分問題は独立している**:左と右部分木は重複しません。左部分木を構築する際、左の部分木に対応する中順走査と前順走査のセグメントのみが必要です。右部分木も同じアプローチが適用されます。
- **部分問題の解を組み合わせることができる**:左と右部分木(部分問題の解)を構築したら、それらをルートノードに接続して元の問題の解を取得できます。
- **問題分解できる**:分割統治の観点から見ると、元の問題は 2 つの部分問題、すなわち左部分木の構築と右部分木の構築に分けられ、さらに根ノード初期化する 1 ステップが加わります。各部分木(部分問題)に対しても、同じ分割方法を再利用しより小さな部分木(部分問題)へと分けていき、最小の部分問題(空部分木)に達した時点で終了します。
- **部分問題は独立している**:左部分木と右部分木は互いに独立しており、両者の間に重なりはありません。左部分木を構築するときは、中順走査と前順走査のうち左部分木に対応する部分だけを見れば十分です。右部分木も同様です。
- **部分問題の解は統合できる**:左部分木と右部分木(部分問題の解)が得られたら、それらをノードに接続することで元の問題の解を得られます。
### 部分木の分割方法
### 部分木をどのように分割するか
の分析に基づいて、この問題は分割統治を使用して解決できます。**しかし、前順走査 `preorder` シーケンスと中順走査 `inorder` シーケンスを使用して左と右部分木をどのように分割すればよいでしょうか**
上の分析より、この問題は分割統治で解けます。**では、前順走査 `preorder` と中順走査 `inorder` を使って左部分木と右部分木をどのように分割すればよいでしょうか**
定義により`preorder``inorder` シーケンスの両方を3つの部分に分割できます
定義に従うと`preorder``inorder` はいずれも 3 つの部分に分けられます
- 前順走査:`[ ルート | 左部分木 | 右部分木 ]`例えば、図では、木は `[ 3 | 9 | 2 1 7 ]` に対応します。
- 中順走査:`[ 左部分木 | ルート |部分木 ]`例えば、図では、木は `[ 9 | 3 | 1 2 7 ]` に対応します。
- 前順走査:`[ 根ノード | 左部分木 | 右部分木 ]` 例えば上図の木は `[ 3 | 9 | 2 1 7 ]` に対応します。
- 中順走査:`[ 左部分木 | 根ノード 右部分木 ]` 例えば上図の木は `[ 9 | 3 | 1 2 7 ]` に対応します。
前の図のデータを使用して、次の図に示すステップに従って分割結果を取得できます
図のデータを例にすると、下図の手順によって分割結果を得られます
1. 前順走査の最初の要素3がルートノードの値です。
2. `inorder` シーケンス内でルートノード3のインデックスを見つけ、このインデックスを使用し`inorder``[ 9 | 3 1 2 7 ]` に分割ます。
3. `inorder` シーケンスの分割に従って、左と右部分木がそれぞれ1個と3個のノードを含むことが簡単に決定できるため、`preorder` シーケンス`[ 3 | 9 | 2 1 7 ]`対応して分割できます。
1. 前順走査の先頭要素 3 が根ノードの値です。
2. 根ノード 3 の `inorder` におけるインデックスを探すと、そのインデックスを用い`inorder``[ 9 | 3 1 2 7 ]` に分割できます。
3. `inorder` の分割結果から、左部分木と右部分木のノード数はそれぞれ 1 と 3 であることがわかり、したがって `preorder``[ 3 | 9 | 2 1 7 ]` に分割できます。
![前順走査と中順走査で部分木分割](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png)
![前順走査と中順走査で部分木分割する](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png)
### 変数に基づく部分木範囲の記述
### 変数を用いて部分木区間を記述する
の分割方法に基づいて、**`preorder``inorder` シーケンスにおけるルート、左の部分木、右の部分木のインデックス範囲を取得しました**。これらのインデックス範囲を記述するために、いくつかのポインタ変数を使用します。
上の分割方法により、**根ノード、左部分木、右部分木が `preorder``inorder` の中で占めるインデックス区間**が得られました。これらのインデックス区間を表すために、いくつかのポインタ変数を導入します。
- 現在の木のルートノード `preorder` シーケンスでのインデックスを $i$ とします。
- 現在の木のルートノード `inorder` シーケンスでのインデックスを $m$ とします。
- 現在の木 `inorder` シーケンスでのインデックス範囲を $[l, r]$ とします。
- 現在の木のノード `preorder` に現れるインデックスを $i$ とします。
- 現在の木のノード `inorder` に現れるインデックスを $m$ とします。
- 現在の木 `inorder` において占めるインデックス区間を $[l, r]$ とします。
以下の表に示すように、これらの変数 `preorder` シーケンスでのルートノードのインデックスと `inorder` シーケンスでの部分木のインデックス範囲を表ます。
次の表のように、これらの変数を用いれば根ノードの `preorder` におけるインデックスと、部分木の `inorder` におけるインデックス区間を表ます。
<p align="center"> 表 <id> &nbsp; 前順走査と中順走査でのルートノードと部分木のインデックス </p>
<p align="center"> 表 <id> &nbsp; 根ノードと部分木の前順走査・中順走査におけるインデックス </p>
| | `preorder` でのルートノードインデックス | `inorder` での部分木インデックス範囲 |
| ------------- | ------------------------------------- | ----------------------------------- |
| 現在の木 | $i$ | $[l, r]$ |
| 左部分木 | $i + 1$ | $[l, m-1]$ |
| 右部分木 | $i + 1 + (m - l)$ | $[m+1, r]$ |
| | 根ノードの `preorder` におけるインデックス | 部分木の `inorder` におけるインデックス区間 |
| ------ | ---------------------------- | ----------------------------- |
| 現在の木 | $i$ | $[l, r]$ |
| 左部分木 | $i + 1$ | $[l, m-1]$ |
| 右部分木 | $i + 1 + (m - l)$ | $[m+1, r]$ |
部分木のルートインデックス $(m-l)$ は「左部分木のノード数」を表すことに注意してください。より明確な理解のために、以下の図を参照することが役立つ場合があります。
右部分木の根ノードのインデックスにある $(m-l)$ は「左部分木のノード数」を意味します。下図と合わせて理解することを勧めます。
![ルートノードと左右部分木のインデックス](build_binary_tree_problem.assets/build_tree_division_pointers.png)
![ノードと左右部分木のインデックス区間の表し方](build_binary_tree_problem.assets/build_tree_division_pointers.png)
### コード実装
### コード実装
$m$ の問い合わせの効率を向上させるために、ハッシュテーブル `hmap`使用して `inorder` シーケンスの要素からそのインデックスへのマッピングを格納します
$m$ の検索効率を高めるために、ハッシュテーブル `hmap`用いて配列 `inorder` の要素からインデックスへの対応を保存します
```src
[file]{build_tree}-[class]{}-[func]{build_tree}
```
以下の図は二分木を構築する再帰過程を示しています。各ノードは再帰の「下降」段階で作成され、各エッジ(参照)は「上昇」段階で形成されます。
図は二分木を構築する再帰過程を示しています。各ノードは下向きに「再帰していく」過程で生成され、各(参照)は上向きに「戻る」過程で張られます。
=== "<1>"
![二分木構築再帰過程](build_binary_tree_problem.assets/built_tree_step1.png)
![二分木構築する再帰過程](build_binary_tree_problem.assets/built_tree_step1.png)
=== "<2>"
![built_tree_step2](build_binary_tree_problem.assets/built_tree_step2.png)
@@ -90,10 +90,10 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー
=== "<9>"
![built_tree_step9](build_binary_tree_problem.assets/built_tree_step9.png)
各再帰関数 `preorder``inorder` シーケンスの分割は以下の図に示されています。
各再帰関数における前順走査 `preorder`中順走査 `inorder` の分割結果を下図に示します。
![各再帰関数での分割](build_binary_tree_problem.assets/built_tree_overall.png)
![各再帰関数での分割結果](build_binary_tree_problem.assets/built_tree_overall.png)
二分木が $n$ 個のノードを持つと仮定すると、各ノードの初期化(再帰関数 `dfs()`呼び出し)には $O(1)$ 時間かかります。**したがって、全体の時間計算量は $O(n)$ です**
木のノード数を $n$ とすると、各ノードの初期化(再帰関数 `dfs()` 1 回の実行)には $O(1)$ 時間かかります。**したがって、全体の時間計算量は $O(n)$** です。
ハッシュテーブルは `inorder` 要素からそのインデックスへのマッピングを格納するため、$O(n)$ スペースが必要です。最悪の場合、二分木が連結リストに退化すると、再帰の深さは $n$ に達し、$O(n)$ のスタックスペースを消費する可能性があります。**したがって、全体の空間計算量は $O(n)$ です**
ハッシュテーブル`inorder` 要素からインデックスへの対応を保存するため、空間計算量は $O(n)$ です。最悪の場合、すなわち二分木が連結リストに退化すると、再帰の深さは $n$ に達し、$O(n)$ のスタックフレーム空間を使用します。**したがって、全体の空間計算量は $O(n)$** です。
@@ -1,40 +1,40 @@
# 分割統治アルゴリズム
# 分割統治
<u>分割統治</u>は重要で人気のあるアルゴリズム戦略です。名前が示すように、アルゴリズムは通常再帰に実装され、「分」と「治」の2つのステップから構成されます。
<u>分割統治法(divide and conquer</u>は、問題を分けて統べるという意味であり、非常に重要で一般的なアルゴリズム戦略です。分割統治法は通常再帰に基づいて実装され、「分」と「治」の 2 つのステップから構成されます。
1. **分(分割段階)**:元の問題を再帰的に2つ以上の小さな部分問題に分解し、最小の部分問題に到達するまで続けます。
2. **治(マージ段階)**:解決方法が既知の最小の部分問題から開始し、部分問題の解をボトムアップ方式でマージして元の問題の解を構築します。
1. **分(分割段階)**:元の問題を 2 つ以上の部分問題へ再帰的に分解し、最小の部分問題に到達した時点で停止します。
2. **治(統合段階)**:解が既知である最小の部分問題から始めて、部分問題の解を下から上へ統合し、元の問題の解を構築します。
以下の図に示すように、「マージソート」は分割統治戦略の典型的な応用の一つです。
1. **分**:元の配列(元の問題)を再帰的に2つの副配列(部分問題)に分割し、副配列が1つの要素のみになるまで(最小の部分問題)続けます。
2. ****順序付けられた副配列(部分問題の解)をボトムアップでマージして、順序付けられた元の配列(元の問題の解)を取得します。
1. **分**:元の配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割し、部分配列に要素が 1 つだけ残るまで続けます。
2. **治**整列済みの部分配列(部分問題の解)を下から上へ統合し、整列済みの元の配列(元の問題の解)をます。
![マージソートの分割統治戦略](divide_and_conquer.assets/divide_and_conquer_merge_sort.png)
## 分割統治問題を特定する方法
## 分割統治法の問題を見極めるには
問題が分割統治解決に適しているかどうかは、通常以下の基準に基づいて決定できます。
ある問題が分割統治法で解くのに適しているかどうかは、通常、次の判断基準を参考にできます。
1. **問題をより小さなものに分解できる**:元の問題より小さく類似した部分問題に分でき、そのような過程を同じ方法で再帰的に実行できます。
2. **部分問題は独立している**:部分問題に重複がなく、独立しており、個別に解決できます。
3. **部分問題の解をマージできる**:元の問題の解は、部分問題の解を組み合わせることで導出されます。
1. **問題分解できる**:元の問題は、より小さく類似した部分問題に分でき、同じ方法で再帰的に分割できます。
2. **部分問題は独立している**:部分問題同士に重複がなく、相互依存もないため、独立して解決できます。
3. **部分問題の解は統合できる**:元の問題の解は、部分問題の解を統合することで得られます。
明らかに、マージソートはこれら3つの基準を満たしています。
明らかに、マージソートは以上の 3 つの判断基準を満たしています。
1. **問題をより小さなものに分解できる**:配列(元の問題)を再帰的に2つの副配列(部分問題)に分割します。
2. **部分問題は独立している**:各配列は独立してソートできます(部分問題は独立して解決できます)。
3. **部分問題の解をマージできる**2つの順序付けられた副配列(部分問題の解)を1つの順序付けられた配列(元の問題の解)にマージできます。
1. **問題分解できる**:配列(元の問題)を 2 つの部分配列(部分問題)へ再帰的に分割します。
2. **部分問題は独立している**:各部分配列は独立ソートできます(部分問題は独立に解けます)。
3. **部分問題の解は統合できる**2 つの整列済み部分配列(部分問題の解)は、1 つの整列済み配列(元の問題の解)に統合できます。
## 分割統治による効率の向上
## 分割統治法で効率を高める
**分割統治戦略はアルゴリズム問題を効果的に解決するだけでなく、しばしば効率を向上させます**。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソートは、分割統治戦略を適用しているため、選択ソート、バブルソート、挿入ソートより高速です。
**分割統治はアルゴリズム問題を効果的に解るだけでなく、多くの場合アルゴリズムの効率も高められます**。ソートアルゴリズムでは、クイックソート、マージソート、ヒープソート選択ソート、バブルソート、挿入ソートより高速ですが、これは分割統治戦略を適用しているためです
私たちの心には疑問があるかもしれません:**なぜ分割統治はアルゴリズム効率を向上させることができ、その根本的な論理は何ですか?** つまり、問題を部分問題に分解し、それらを解決し、それらの解を組み合わせて元の問題に対処することが、元の問題を直接解決するより効率的である理由は何ですか?この質問は2つの側面から分析できます:操作数と並列計算
ここで次の疑問が生じます。**なぜ分割統治はアルゴリズム効率を高められるのでしょうか。その根本的な仕組みは何でしょうか**?言い換えると、大きな問題を複数の部分問題に分解し、部分問題を解き、それらの解を統合して元の問題の解にするという手順は、なぜ元の問題を直接解より効率的なのでしょうか。この問題は、操作回数と並列計算の 2 つの観点から議論できます
### 操作数の最適化
### 操作数の最適化
「バブルソート」を例にると、長さ $n$ の配列を処理するのに $O(n^2)$ 時間が必要です。以下の図に示すように、配列を中点から2つの副配列に分割するとします。そのような分割には $O(n)$ 時間が必要です。各副配列のソートには $O((n / 2)^2)$ 時間が必要です。そして2つの副配列のマージには $O(n)$ 時間が必要です。したがって、全体の時間計算量は:
「バブルソート」を例にると、長さ $n$ の配列を処理するのに $O(n^2)$ 時間がかかります。以下の図ように、配列を中央で 2 つの部分配列に分けると仮定すると、分割には $O(n)$ 時間、各部分配列のソートには $O((n / 2)^2)$ 時間、2 つの部分配列の統合には $O(n)$ 時間が必要で、全体の時間計算量は次のようになります
$$
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
@@ -42,7 +42,7 @@ $$
![配列分割前後のバブルソート](divide_and_conquer.assets/divide_and_conquer_bubble_sort.png)
以下の不等式を計算してみましょう。左側は分割前の総操作数を表し、右側は分割後の操作数をそれぞれ表します:
次に、以下の不等式を計算します。左辺と右辺はそれぞれ、分割前と分割後の操作総数です:
$$
\begin{aligned}
@@ -52,40 +52,40 @@ n(n - 4) & > 0
\end{aligned}
$$
**これは $n > 4$ の場合、分割後の操作数が少なく、より良いパフォーマンスにつながることを意味します**。分割後の時間計算量は依然として二次 $O(n^2)$ ですが、計算量の定数係数が減少していることに注意してください
**これは$n > 4$ のときに分割後の操作回数の方が少なくなり、ソート効率が高くなることを意味します**ただし、分割後の時間計算量は依然として 2 次の $O(n^2)$ であり、計算量の定数項が小さくなっただけです
さらに進むことができます。**副配列をその中点からさらに2つの副配列に分割し続けて、副配列が1つの要素のみになるまで続けたらどうでしょうか?** このアイデアは実際には「マージソート」で、時間計算量は $O(n \log n)$ です。
さらに考えると、**部分配列を中央からさらに 2 つの部分配列へと分割し続け**、部分配列に要素が 1 つだけ残るまで分割を止めないとしたらどうでしょうか。この考え方がまさに「マージソート」であり、時間計算量は $O(n \log n)$ です。
少し違うことを試してみましょう。**2つではなく、より多くの分割に分割したらどうでしょうか?** 例えば、元の配列を $k$ 個の副配列に均等に分割しますか?このアプローチは「バケットソート」と非常に似ており、大量データのソートに非常に適しています。理論的には、時間計算量は $O(n + k)$ に達することができます。
さらに、**分割点をいくつか増やして**、元の配列を平均的に $k$ 個の部分配列に分けるとしたらどうでしょうか。この状況は「バケットソート」と非常によく似ており、大量データのソートに非常に適しています。理論上の時間計算量は $O(n + k)$ に達ます。
### 並列計算による最適化
### 並列計算最適化
分割統治によって生成される部分問題は互いに独立していることが分かっています。**これは、それらを並列で解決できることを意味します** その結果、分割統治はアルゴリズムの時間計算量を減らすだけでなく、**現代のオペレーティングシステムによる並列最適化も促進します。**
分割統治法で生成される部分問題は互いに独立しているため、**通常は並列に解くことができます**。つまり、分割統治はアルゴリズムの時間計算量を下げられるだけでなく、**オペレーティングシステム並列最適化にも有利です**
並列最適化は、複数のコアやプロセッサを持つ環境で特に効果的です。システムが複数の部分問題を同時に処理できるため、計算リソースを完全に活用し、全体的な実行時間大幅に短縮されます。
並列最適化は、マルチコアまたはマルチプロセッサ環境で特に効です。システムが複数の部分問題を同時に処理でき、計算資源をより十分に活用できるため、全体実行時間大幅に短縮できます。
えば、以下の図に示す「バケットソート」では、大量のデータを様々なバケットに均等に分解します。各バケットのソート作業は、利用可能な計算ユニットに割り当てることができます。すべての作業が完了すると、すべてのソートされたバケットがマージされて最終結果が生成されます。
たとえば、以下の図に示す「バケットソート」では、大量のデータをバケットに均等に割り当てることで、すべてのバケットのソート処理を各計算ユニットに分散し、完了後に結果を統合できます。
![バケットソートの並列計算](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png)
## 分割統治の一般的な応用
## 分割統治法の代表的な応用
分割統治は多くの古典的なアルゴリズム問題を解決するために使用できます。
一方では、分割統治は多くの古典的なアルゴリズム問題を解くのに使えます。
- **最近点対の発見**:このアルゴリズムは点の集合を2つの半分に分割することで動作します。そして各半分で再帰的に最近点対を見つけます。最後に、2つの半分にまたがるペアを考慮して、全体の最近点対を見つけます。
- **大整数乗算**一つのアルゴリズムはKaratsubaと呼ばれます。大整数の乗算をいくつかの小さな整数の乗算と加算に分解します。
- **行列乗算**一例はStrassenアルゴリズムです。大きな行列の乗算を複数の小さな行列の乗算と加算に分解します。
- **ハノイの塔問題**:ハノイの塔問題は再帰的に解決でき、分割統治戦略の典型的な応用です。
- **転倒対の解決**:シーケンスで、前の数が後の数より大きい場合、これら2つの数は転対を構成します。転倒対問題の解決は、マージソートの助けを借りて、分割統治のアイデアを利用できます。
- **最近点対探索**:このアルゴリズムは、まず点集合を 2 つに分け、それぞれの部分における最近点対を求め、最後に 2 つの部分をまたぐ最近点対を求めます。
- **大整数乗算**たとえば Karatsuba 法では、大整数の乗算を、より小さな整数どうしのいくつかの乗算と加算に分解します。
- **行列乗算**たとえば Strassen 法では、大きな行列の乗算を複数の小さな行列の乗算と加算に分解します。
- **ハノイの塔問題**:ハノイの塔問題は再帰によって解くことができ、これは典型的な分割統治戦略の応用です。
- **反転対の計算**:ある数列で前の数が後の数より大きい場合、その 2 つの数は転対を構成します。反転対の問題は、分割統治の考え方を利用し、マージソートを用いて解けます。
分割統治はアルゴリズムとデータ構造の設計にも広く応用されています。
他方で、分割統治はアルゴリズムとデータ構造の設計にも非常に広く応用されています。
- **二分探索**:二分探索は、ソート済み配列を中インデックスから2つの半分に分割します。そして、ターゲット値と中要素の比較結果に基づいて、一方の半分が破棄されます。同じプロセスで残りの半分で検索が続行され、ターゲットが見つかるか残りの要素がなくなるまで続きます。
- **マージソート**この節の冒頭ですでに紹介したため、さらなる詳述は不要です
- **クイックソート**:クイックソートはピボット値を選択して配列を2つの副配列に分割し、一方はピボットより小さい要素、もう一方はピボットより大きい要素を持ちます。このプロセスは、これら2つの副配列のそれぞれに対して、1つの要素のみを保持するまで続ます。
- **バケットソート**:バケットソートの基本的なアイデアは、データを複数のバケットに分散させることです。各バケット内の要素をソートした後、バケットから順序よく要素を取得して順序付けられた配列を取得します。
- **木**えば二分探索木、AVL木、赤黒木、B木、B+木など。その操作(検索、挿入削除)はすべて分割統治戦略の応用と見なすことができます。
- **ヒープ**:ヒープは特別なタイプの完全二分木です。その様々な操作(挿入、削除、ヒープ化は、実際に分割統治のアイデアを含意しています。
- **ハッシュテーブル**:ハッシュテーブルは直接分割統治を適用しませんが、一部のハッシュ衝突解決ソリューションは間接的にこの戦略を適用します。えば、チェイン法の長いリストは、クエリ効率を向上させるために赤黒木変換される場合があります。
- **二分探索**:二分探索は、整列済み配列を中央のインデックスで 2 つに分け、目標値と中要素の比較結果に基づいてどちらの半区間を除外するかを決め、残った区間で同じ二分操作を行います。
- **マージソート**節の冒頭で紹介したため、ここでは繰り返しません
- **クイックソート**:クイックソートは基準値を 1 つ選び、配列を、基準値より小さい要素の部分配列と、基準値より大きい要素の部分配列に分け、その後それぞれに対して同じ分割操作を行い、部分配列に要素が 1 つだけ残るまで続ます。
- **バケットソート**:バケットソートの基本的な考え方は、データを複数のバケットに分散し、各バケット内の要素をソートしたうえで、各バケットの要素を順に取り出して整列済み配列を得ることです。
- **木構造**たとえば二分探索木、AVL 木、赤黒木、B 木、B+ 木などでは、探索・挿入削除などの操作をいずれも分割統治戦略の応用とみなせます。
- **ヒープ**:ヒープは特殊な完全二分木であり、挿入、削除、ヒープ化などの各種操作には、実際に分割統治の考え方が含まれています。
- **ハッシュテーブル**:ハッシュテーブル自体は分割統治を直接適用しているわけではありませんが、いくつかのハッシュ衝突解決法では間接的に分割統治戦略が使われています。たとえば、連鎖アドレス法における長い連結リストは、検索効率を高めるために赤黒木変換されます。
**分割統治は巧妙に浸透するアルゴリズムアイデア**であり、様々なアルゴリズムデータ構造に組み込まれていることが分かります。
このように、**分割統治法は「静かに物を潤す」ようなアルゴリズム思想**であり、さまざまなアルゴリズムデータ構造の中に潜んでいます。
@@ -1,37 +1,37 @@
# ハノイの塔問題
# ハノイの塔問題
マージソート二分木構築の両方で、元の問題を2つの部分問題に分解し、それぞれが元の問題のサイズの半分でした。しかし、ハノイの塔では、異なる分解戦略を採用します。
マージソート二分木構築では、いずれも元の問題を元問題の半分の規模をもつ 2 つの部分問題に分解していました。しかし、ハノイの塔の問題では、異なる分解戦略を採用します。
!!! question
3の柱があり、それぞれ `A``B``C`表記されます。最初、柱 `A` $n$ 枚の円盤がり、上から下に向かって昇順のサイズで配置されています。私たちのタスクは、これらの $n$ 枚の円盤を柱 `C` に移し、元の順序を維持することです(以下の図に示すように)。移動中には以下のルールが適用されます
3の柱があり、それぞれ `A``B``C`します。初期状態では、柱 `A` に $n$ 枚の円盤が通されており、上から下へ小さい順に並んでいます。私たちの課題は、この $n$ 枚の円盤を柱 `C` に移し、元の順序を保つことです(以下の図のとおり)。円盤を移動する際には、次のルールに従う必要があります
1. 円盤は 1 本の柱の頂上から取り出し、別の柱の頂上に置くことしかできません。
2. 1 回に移動できる円盤は 1 枚だけです。
3. 小さい円盤は常に大きい円盤の上になければなりません。
1. 円盤は柱の上部からのみ取り除くことができ、別の柱の上部に置く必要があります。
2. 一度に移動できるのは1枚の円盤のみです。
3. 小さい円盤は常に大きい円盤の上にある必要があります。
![ハノイの塔の問題の例](hanota_problem.assets/hanota_example.png)
![ハノイの塔の例](hanota_problem.assets/hanota_example.png)
**サイズ $i$ のハノイの塔問題を $f(i)$ と表記します**。例えば、$f(3)$ は3枚の円盤を柱 `A` から柱 `C` に移動することを表します。
**規模が $i$ のハノイの塔の問題を $f(i)$ と表します** 。たとえば $f(3)$ は、$3$ 枚の円盤を `A` から `C` へ移動するハノイの塔の問題を表します。
### 基本ケースを考える
以下の図に示すように、問題 $f(1)$(円盤が1枚のみ)については、`A` から `C` 直接移動できます。
以下の図に示すように、問題 $f(1)$ 、すなわち円盤が 1 枚だけの場合は、それを `A` から `C` 直接移動すれば済みます。
=== "<1>"
![サイズ1の問題の解](hanota_problem.assets/hanota_f1_step1.png)
![規模 1 の問題の解](hanota_problem.assets/hanota_f1_step1.png)
=== "<2>"
![hanota_f1_step2](hanota_problem.assets/hanota_f1_step2.png)
$f(2)$(円盤が2枚)については、**柱 `B` の助けを借りて小さい円盤大きい円盤の上に保つ**必要があります。以下の図に示すように:
以下の図に示すように、問題 $f(2)$ 、すなわち円盤が 2 枚ある場合は、**小さい円盤が常に大きい円盤の上にある条件を満たすため、`B` を借りて移動を行う必要があります**。
1. まず小さい円盤を `A` から `B` に移動します。
2. 次に大きい円盤を `A` から `C` に移動します。
3. 最後に小さい円盤を `B` から `C` に移動します。
1. まず上の小さい円盤を `A` から `B` へ移します。
2. 次に大きい円盤を `A` から `C` へ移します。
3. 最後に小さい円盤を `B` から `C` へ移します。
=== "<1>"
![サイズ2の問題の解](hanota_problem.assets/hanota_f2_step1.png)
![規模 2 の問題の解](hanota_problem.assets/hanota_f2_step1.png)
=== "<2>"
![hanota_f2_step2](hanota_problem.assets/hanota_f2_step2.png)
@@ -42,20 +42,20 @@ $f(2)$(円盤が2枚)については、**柱 `B` の助けを借りて小さ
=== "<4>"
![hanota_f2_step4](hanota_problem.assets/hanota_f2_step4.png)
$f(2)$ を解決する過程は次のように要約できます:**`B` の助けを借りて2枚の円盤を `A` から `C` に移動する**。ここで`C`ターゲット柱、`B`バッファ柱と呼びます。
問題 $f(2)$ を解く過程は、**2 枚の円盤を `B` を介して `A` から `C` へ移す**と要約できます。このとき`C`目標の柱、`B`補助の柱と呼びます。
### 部分問題の分解
### 部分問題の分解
問題 $f(3)$(つまり、円盤が3枚の場合)については、状況やや複雑になります。
問題 $f(3)$ 、すなわち円盤が 3 枚ある場合になると、状況やや複雑になります。
すでに $f(1)$ と $f(2)$ の解が分かっているので、分割統治の観点を採用し、**`A` の上の2枚の円盤を1つの単位として扱い**、以下の図に示すステップを実行できます。これにより、3枚の円盤を `A` から `C` に正常に移動できます。
$f(1)$ と $f(2)$ の解が既知なので、分割統治の観点から、**`A` の上部にある 2 枚の円盤をひとまとまりとみなして**、次の図の手順を実行できます。こうして 3 枚の円盤を `A` から `C` へ順調に移動できます。
1. `B`ターゲット柱、`C`バッファ柱として、2枚の円盤を `A` から `B` に移動します。
2. 残りの円盤を `A` から直接 `C` 移動します。
3. `C`ターゲット柱、`A`バッファ柱として、2枚の円盤を `B` から `C` に移動します。
1. `B`目標の柱、`C`補助の柱として、2 枚の円盤を `A` から `B` へ移します。
2. `A` に残った 1 枚の円盤を `A` から `C` へ直接移動します。
3. `C`目標の柱、`A`補助の柱として、2 枚の円盤を `B` から `C` へ移します。
=== "<1>"
![サイズ3の問題の解](hanota_problem.assets/hanota_f3_step1.png)
![規模 3 の問題の解](hanota_problem.assets/hanota_f3_step1.png)
=== "<2>"
![hanota_f3_step2](hanota_problem.assets/hanota_f3_step2.png)
@@ -66,32 +66,32 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助
=== "<4>"
![hanota_f3_step4](hanota_problem.assets/hanota_f3_step4.png)
本質的に、**$f(3)$ を2つの $f(2)$ 部分問題と1つの $f(1)$ 部分問題に分解します**。これら3つの部分問題を順次解決することで、元の問題解決され、部分問題が独立しており、それらの解をマージできることを示しています。
本質的に、**問題 $f(3)$ を 2 つの部分問題 $f(2)$ と 1 つの部分問題 $f(1)$ に分けています** 。この 3 つの部分問題を順に解けば、元の問題解決されます。これは、部分問題が独立しており、解を組み合わせられることを示しています。
ここから、以下の図に示すハノイの塔の分割統治戦略を要約できます。元の問題 $f(n)$ を2つの部分問題 $f(n-1)$ と1つの部分問題 $f(1)$ に分割し、以下の順序でこれら3つの部分問題を解決します
ここまでで、次の図に示すハノイの塔の問題を解く分割統治戦略をまとめられます。元の問題 $f(n)$ を 2 つの部分問題 $f(n-1)$ と 1 つの部分問題 $f(1)$ に分け、次の順序でこの 3 つの部分問題を解ます
1. `C` をバッファとして使用し、$n-1$ 枚の円盤を `A` から `B` に移動します。
2. 残りの円盤を `A` から直接 `C` に移動します。
3. `A` をバッファとして使用し、$n-1$ 枚の円盤を `B` から `C` に移動します。
1. $n-1$ 枚の円盤を `C` を介して `A` から `B` へ移します。
2. 残り $1$ 枚の円盤を `A` から `C` へ直接移します。
3. $n-1$ 枚の円盤を `A` を介して `B` から `C` へ移します。
各 $f(n-1)$ 部分問題について、**同じ再帰分割を適用でき**最小の部分問題 $f(1)$ に到達するまで続けます。$f(1)$ は単一の移動のみが必要であることがすでに分かっているため、解決するのは簡単です。
この 2 つの部分問題 $f(n-1)$ は、**同じ方法で再帰的に分割できます**最小の部分問題 $f(1)$ に到達するまでこれを続けます。一方、$f(1)$ の解は既知であり、1 回の移動操作だけで済みます。
![ハノイの塔を解決するための分割統治戦略](hanota_problem.assets/hanota_divide_and_conquer.png)
![ハノイの塔の問題を解く分割統治戦略](hanota_problem.assets/hanota_divide_and_conquer.png)
### コード実装
### コード実装
コードでは、再帰関数 `dfs(i, src, buf, tar)` を定義します。これは`src` から上の $i$ 枚の円盤を`tar` に移動し、柱 `buf` をバッファとして使用します:
コードでは、再帰関数 `dfs(i, src, buf, tar)` を定義します。その役割は、`src`上部にある $i$ 枚の円盤を、補助の柱 `buf` を使って目標の柱 `tar` へ移動することです:
```src
[file]{hanota}-[class]{}-[func]{solve_hanota}
```
以下の図に示すように、ハノイの塔問題は高さ $n$ の再帰木として視覚化できます。各ノードは部分問題を表し、`dfs()` の呼び出しに対応します。**したがって時間計算量は $O(2^n)$、空間計算量は $O(n)$ です。**
以下の図に示すように、ハノイの塔問題は高さ $n$ の再帰木を形成し、各ノードは 1 つの部分問題、すなわち 1 つ起動された `dfs()` 関数に対応します。**したがって時間計算量は $O(2^n)$ 、空間計算量は $O(n)$** です。
![ハノイの塔の再帰木](hanota_problem.assets/hanota_recursive_tree.png)
![ハノイの塔の問題の再帰木](hanota_problem.assets/hanota_recursive_tree.png)
!!! quote
ハノイの塔は古代の伝説に由来します。古代インドの寺院で、僧侶たちは3本の高いダイヤモンドの柱と、異なるサイズの $64$ 枚の金の円盤を持っていました。彼らは、最後の円盤が正しく置かれたとき、世界が終わると信じていました。
ハノイの塔の問題は古い伝説に由来します。古代インドのある寺院で、僧侶たちは 3 本の高いダイヤモンドの柱と、$64$ 枚の大きさの異なる金の円盤を持っていました。僧侶たちは絶えず円盤を動かし、最後の 1 枚が正しく置かれた瞬間に世界が終わると信じていました。
しかし、僧侶たちが1秒に1枚の円盤を移動したとしても、約 $2^{64} \approx 1.84×10^{19}$ —約5850億年—かかり、宇宙の年齢の現在の推定をはるかに超えています。したがって、この伝説が真実であれば、世界の終わりについて心配する必要はおそらくないでしょう
しかし、たとえ僧侶たちが 1 秒に 1 回移動するとしても、合計でおよそ $2^{64} \approx 1.84×10^{19}$ 秒、約 $5850$ 億年が必要で、現在推定されている宇宙の年齢をはるかに上回ります。したがって、この伝説が本当だったとしても、世界の終わり心配する必要はなさそうです
+3 -3
View File
@@ -4,6 +4,6 @@
!!! abstract
困難な問題は層を重ねて分解され、各分解によってより単純になります
分割統治は深い真理を明らかにします:単純さから始めれば、複雑さは解決される
難題は段階的に分解され、そのたびにより単純になっていく
分割統治は一つの重要な事実を示している。単純なことから始めれば、すべてはもはや複雑ではない
+11 -9
View File
@@ -1,11 +1,13 @@
# まとめ
- 分割統治は一般的なアルゴリズム設計戦略で、分割(分割)と統治(マージ)の2つの段階から構成され、一般的に再帰を使用して実装されます。
- 問題が分割統治アプローチに適しているかどうかを判断するために、問題が分解可能かどうか、部分問題が独立しているかどうか、部分問題をマージできるかどうかを確認します。
- マージソートは分割統治戦略の典型的な例です。配列を再帰的に2つの等しい長さの副配列に分割し、1つの要素のみが残るまで続け、次にこれらの副配列を層ごとにマージしてソートを完了します。
- 分割統治戦略の導入は、しばしばアルゴリズムの効率を向上させます。一方では操作数を減らし、他方では分割後のシステムの並列最適化を促進します。
- 分割統治は多数のアルゴリズム問題に適用でき、データ構造とアルゴリズム設計で広く使用され、多くのシナリオに現れます。
- 総当たり検索と比較して、適応検索はより効率的です。時間計算量が $O(\log n)$ の検索アルゴリズムは、通常分割統治戦略に基づいています。
- 二分探索は分割統治戦略のもう一つの古典的な応用です。部分問題の解のマージを含まず、再帰的な分割統治アプローチで実装できます。
- 二分木構築問題では、木の構築(元の問題)を左の部分木と右の部分木の構築(部分問題)に分割できます。これは前順走査と中順走査のインデックス範囲を分割することで実現できます。
- ハノイの塔問題では、サイズ $n$ の問題をサイズ $n-1$ の2つの部分問題とサイズ $1$ の1つの部分問題に分解できます。これら3つの部分問題を順次解決することで、元の問題が解決されます。
### 要点の振り返り
- 分割統治法は一般的なアルゴリズム設計戦略であり、分(分割)と治(統合)の 2 つの段階からなり、通常は再帰に基づいて実装されます。
- それが分割統治法の問題かどうかを判断する基準には、問題を分解できるか、部分問題が独立しているか、部分問題を統合できるかが含まれます。
- マージソートは分割統治法の典型的な応用であり、配列を再帰的に同じ長さの 2 つの部分配列に分割し、要素が 1 つだけになるまで続け、その後で各層を順に統合してソートを完了します。
- 分割統治法を導入すると、多くの場合アルゴリズムの効率を高められます。一方では操作回数が減り、他方では分割後にシステムの並列最適化を行いやすくなります。
- 分割統治法は多くのアルゴリズム問題を解決できるだけでなく、データ構造やアルゴリズム設計にも広く応用されており、至る所でその姿を見ることができます。
- 総当たり探索と比べて、適応的な探索のほうが効率的です。時間計算量が $O(\log n)$ の探索アルゴリズムは、通常は分割統治法に基づいて実装されます。
- 二分探索は分割統治法のもう 1 つの典型的な応用であり、部分問題の解を統合する手順を含みません。再帰的な分割統治によって二分探索を実現できます。
- 二分木を構築する問題では、木の構築(元の問題)を左部分木と右部分木の構築(部分問題)に分けられます。これは、先行順序走査と中間順序走査のインデックス区間を分割することで実現できます。
- ハノイの塔の問題では、規模 $n$ の問題を、規模 $n-1$ の 2 つの部分問題と規模 $1$ の 1 つの部分問題に分けられます。これら 3 つの部分問題を順に解くと、元の問題も解決されます。