mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-07 21:24:18 +00:00
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:
@@ -1,95 +1,95 @@
|
||||
# 順列問題
|
||||
# 全順列問題
|
||||
|
||||
順列問題は、バックトラッキングアルゴリズムの典型的な応用です。これは、配列や文字列などの与えられた集合から要素のすべての可能な配置(順列)を見つけることを含みます。
|
||||
全順列問題はバックトラッキングアルゴリズムの典型的な応用例です。これは、ある集合(配列や文字列など)が与えられたとき、その要素のあり得るすべての順列を求める問題です。
|
||||
|
||||
以下の表は、入力配列とその対応する順列を含むいくつかの例を示しています。
|
||||
下表に、入力配列とそれに対応するすべての順列から成る例をいくつか示します。
|
||||
|
||||
<p align="center"> 表 <id> 順列の例 </p>
|
||||
<p align="center"> 表 <id> 全順列の例 </p>
|
||||
|
||||
| 入力配列 | 順列 |
|
||||
| :----------- | :----------------------------------------------------------------- |
|
||||
| $[1]$ | $[1]$ |
|
||||
| $[1, 2]$ | $[1, 2], [2, 1]$ |
|
||||
| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ |
|
||||
| 入力配列 | すべての順列 |
|
||||
| :---------- | :----------------------------------------------------------------- |
|
||||
| $[1]$ | $[1]$ |
|
||||
| $[1, 2]$ | $[1, 2], [2, 1]$ |
|
||||
| $[1, 2, 3]$ | $[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]$ |
|
||||
|
||||
## 重複要素がない場合
|
||||
## 等しい要素がない場合
|
||||
|
||||
!!! question
|
||||
|
||||
重複要素のない整数配列が与えられた場合、すべての可能な順列を返してください。
|
||||
重複要素を含まない整数配列を入力として受け取り、あり得るすべての順列を返します。
|
||||
|
||||
バックトラッキングの観点から、**順列を生成するプロセスを一連の選択として見ることができます。** 入力配列が $[1, 2, 3]$ だとします。最初に $1$ を選択し、次に $3$、最後に $2$ を選択すると、順列 $[1, 3, 2]$ が得られます。「バックトラッキング」は前の選択を取り消して、代替オプションを探索することを意味します。
|
||||
バックトラッキングアルゴリズムの観点から見ると、**順列生成の過程は一連の選択の結果として捉えられます**。入力配列が $[1, 2, 3]$ だとすると、最初に $1$ を選び、次に $3$ を選び、最後に $2$ を選べば、順列 $[1, 3, 2]$ が得られます。戻る操作は 1 つの選択を取り消し、その後で別の選択を試し続けることを表します。
|
||||
|
||||
コーディングの観点から、候補集合 `choices` は入力配列のすべての要素で構成され、`state` はこれまでに選択された要素を保持します。各要素は一度だけ選択できるため、**`state` のすべての要素は一意である必要があります**。
|
||||
バックトラッキングコードの観点では、候補集合 `choices` は入力配列中のすべての要素であり、状態 `state` は現時点までに選ばれた要素です。各要素は 1 回しか選べないことに注意してください。**したがって `state` 内の要素はすべて一意でなければなりません**。
|
||||
|
||||
以下の図に示すように、検索プロセスを再帰木に展開できます。各ノードは現在の `state` を表します。ルートノードから開始して、3回の選択の後、葉ノードに到達します—それぞれが順列に対応します。
|
||||
下図のように、探索過程は再帰木として展開できます。木の各ノードは現在の状態 `state` を表します。根ノードから始めて 3 ラウンドの選択を経て葉ノードに到達し、各葉ノードが 1 つの順列に対応します。
|
||||
|
||||

|
||||

|
||||
|
||||
### 重複選択の剪定
|
||||
### 重複選択の枝刈り
|
||||
|
||||
各要素が一度だけ選択されることを保証するために、ブール配列 `selected` を導入します。ここで `selected[i]` は `choices[i]` が選択されたかどうかを示します。次に、この配列に基づいて剪定ステップを実行します:
|
||||
各要素が 1 回しか選ばれないようにするため、ブール配列 `selected` の導入を考えます。ここで `selected[i]` は `choices[i]` がすでに選ばれているかどうかを表し、これに基づいて次の枝刈りを行います。
|
||||
|
||||
- `choice[i]` を選択した後、`selected[i]` を $\text{True}$ に設定して選択されたとマークします。
|
||||
- `choices` を反復処理する際、選択されたとマークされたすべての要素をスキップします(つまり、それらの分岐を剪定します)。
|
||||
- 選択 `choice[i]` を行った後、`selected[i]` を $\text{True}$ に設定し、その要素が選択済みであることを表します。
|
||||
- 選択肢リスト `choices` を走査するとき、すでに選ばれたノードはすべてスキップします。これが枝刈りです。
|
||||
|
||||
以下の図に示すように、最初のラウンドで1を選択し、2番目のラウンドで3を選択し、最後のラウンドで2を選択するとします。2番目のラウンドで要素1の分岐と、3番目のラウンドで要素1と3の分岐を剪定する必要があります。
|
||||
下図のように、1 回目に 1、2 回目に 3、3 回目に 2 を選ぶとします。このとき 2 回目では要素 1 の分岐を、3 回目では要素 1 と要素 3 の分岐を刈り取る必要があります。
|
||||
|
||||

|
||||

|
||||
|
||||
図から、この剪定プロセスが検索空間を $O(n^n)$ から $O(n!)$ に削減することがわかります。
|
||||
上図から、この枝刈りにより探索空間の大きさは $O(n^n)$ から $O(n!)$ へ削減されることがわかります。
|
||||
|
||||
### コード実装
|
||||
|
||||
この理解により、フレームワークコードの「空欄を埋める」ことができます。全体のコードを簡潔に保つため、フレームワークの各部分を個別に実装せず、代わりに `backtrack()` 関数ですべてを展開します:
|
||||
以上を整理できれば、フレームワークコードの「穴埋め」を行えます。全体のコードを短くするため、フレームワークコード中の各関数を個別には実装せず、これらを `backtrack()` 関数内に展開します。
|
||||
|
||||
```src
|
||||
[file]{permutations_i}-[class]{}-[func]{permutations_i}
|
||||
```
|
||||
|
||||
## 重複要素を考慮する場合
|
||||
## 等しい要素を考慮する場合
|
||||
|
||||
!!! question
|
||||
|
||||
**重複要素を含む可能性のある**整数配列が与えられた場合、すべての一意の順列を返してください。
|
||||
整数配列を入力として受け取り、**配列には重複要素が含まれる場合があります**。重複しない順列をすべて返します。
|
||||
|
||||
入力配列が $[1, 1, 2]$ だとします。2つの同一要素 $1$ を区別するために、2番目を $\hat{1}$ とラベル付けします。
|
||||
入力配列が $[1, 1, 2]$ だと仮定します。2 つの重複する要素 $1$ を区別しやすくするため、2 つ目の $1$ を $\hat{1}$ と記します。
|
||||
|
||||
以下の図に示すように、この方法で生成される順列の半分は重複です:
|
||||
下図のように、上述の方法で生成される順列の半分は重複しています。
|
||||
|
||||

|
||||

|
||||
|
||||
では、これらの重複順列をどのように除去できるでしょうか?一つの直接的なアプローチは、すべての順列を生成した後にハッシュセットを使用して重複を除去することです。しかし、これはあまり優雅ではありません。**重複を生成する分岐は本来不要であり、事前に剪定されるべきだからです**、これによりアルゴリズムの効率が向上します。
|
||||
では、重複した順列をどのように取り除けばよいのでしょうか。最も直接的なのは、ハッシュ集合を用いて順列結果をそのまま重複排除する方法です。しかしこのやり方は十分に洗練されていません。**なぜなら、重複順列を生成する探索分岐はそもそも不要であり、事前に見つけて枝刈りすべきだからです**。そうすることで、アルゴリズム効率をさらに高められます。
|
||||
|
||||
### 等値要素の剪定
|
||||
### 等しい要素の枝刈り
|
||||
|
||||
以下の図を見ると、最初のラウンドで $1$ または $\hat{1}$ を選択すると同じ順列につながるため、$\hat{1}$ を剪定します。
|
||||
下図を見ると、1 回目のラウンドでは $1$ を選ぶことと $\hat{1}$ を選ぶことは等価であり、これら 2 つの選択の下で生成される順列はすべて重複します。したがって $\hat{1}$ を枝刈りすべきです。
|
||||
|
||||
同様に、最初のラウンドで $2$ を選択した後、2番目のラウンドで $1$ または $\hat{1}$ を選択しても重複分岐につながるため、その時も $\hat{1}$ を剪定します。
|
||||
同様に、1 回目で $2$ を選んだ後では、2 回目のラウンドにおける $1$ と $\hat{1}$ も重複分岐を生むため、2 回目の $\hat{1}$ も枝刈りすべきです。
|
||||
|
||||
本質的に、**私たちの目標は、複数の同一要素が選択の各ラウンドで一度だけ選択されることを保証することです。**
|
||||
本質的には、**各ラウンドの選択において、等しい複数の要素が 1 回しか選ばれないようにすることが目標です**。
|
||||
|
||||

|
||||

|
||||
|
||||
### コード実装
|
||||
|
||||
前の問題のコードに基づいて、各ラウンドでハッシュセット `duplicated` を導入します。このセットは、すでに試行した要素を追跡し、重複を剪定できるようにします:
|
||||
前問のコードを土台として、各ラウンドの選択でハッシュ集合 `duplicated` を 1 つ用意し、そのラウンドですでに試した要素を記録して、重複要素を枝刈りすることを考えます。
|
||||
|
||||
```src
|
||||
[file]{permutations_ii}-[class]{}-[func]{permutations_ii}
|
||||
```
|
||||
|
||||
すべての要素が異なると仮定すると、$n$ 個の要素の順列は $n!$ (階乗)個あります。各結果を記録するには長さ $n$ のリストをコピーする必要があり、これには $O(n)$ 時間がかかります。**したがって、総時間計算量は $O(n!n)$ です。**
|
||||
要素どうしがすべて互いに異なると仮定すると、$n$ 個の要素には全部で $n!$ 通りの順列(階乗)があります。結果を記録する際には、長さ $n$ のリストをコピーする必要があり、これに $O(n)$ 時間を要します。**したがって時間計算量は $O(n!n)$** です。
|
||||
|
||||
最大再帰深度は $n$ で、$O(n)$ のスタック空間を使用します。`selected` 配列も $O(n)$ 空間が必要です。一度に最大 $n$ 個の個別の `duplicated` セットが存在する可能性があるため、それらは集合的に $O(n^2)$ 空間を占有します。**したがって、空間計算量は $O(n^2)$ です。**
|
||||
再帰の最大深さは $n$ であり、$O(n)$ のスタックフレーム空間を使います。`selected` は $O(n)$ 空間を使用します。同時刻に存在する `duplicated` は最大で $n$ 個であり、$O(n^2)$ 空間を要します。**したがって空間計算量は $O(n^2)$** です。
|
||||
|
||||
### 2つの剪定方法の比較
|
||||
### 2 種類の枝刈りの比較
|
||||
|
||||
`selected` と `duplicated` はどちらも剪定メカニズムとして機能しますが、異なる問題をターゲットにしています:
|
||||
`selected` と `duplicated` はどちらも枝刈りに用いられますが、目的は異なる点に注意してください。
|
||||
|
||||
- **重複選択の剪定**(`selected` 経由):検索全体に単一の `selected` 配列があり、現在の状態にすでにある要素を示します。これにより、同じ要素が `state` に複数回現れることを防ぎます。
|
||||
- **等値要素の剪定**(`duplicated` 経由):`backtrack` 関数の各呼び出しは独自の `duplicated` セットを使用し、その特定の反復(`for` ループ)ですでに選択された要素を記録します。これにより、等しい要素が選択の各ラウンドで一度だけ選択されることを保証します。
|
||||
- **重複選択の枝刈り**:探索全体を通して `selected` は 1 つだけです。これは現在の状態にどの要素が含まれているかを記録し、ある要素が `state` に重複して現れるのを防ぎます。
|
||||
- **等しい要素の枝刈り**:各ラウンドの選択、すなわち各回の `backtrack` 呼び出しには `duplicated` が含まれます。これはそのラウンドの走査(`for` ループ)でどの要素がすでに選ばれたかを記録し、等しい要素が 1 回しか選ばれないことを保証します。
|
||||
|
||||
以下の図は、これら2つの剪定戦略の範囲を示しています。木の各ノードは選択を表します。ルートから任意の葉への経路は、1つの完全な順列に対応します。
|
||||
下図は、2 つの枝刈り条件が有効になる範囲を示しています。木の各ノードは 1 つの選択を表し、根ノードから葉ノードまでの経路上の各ノードが 1 つの順列を構成することに注意してください。
|
||||
|
||||

|
||||

|
||||
|
||||
Reference in New Issue
Block a user