mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-25 04:26:07 +00:00
build
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.2 分数ナップサック問題
|
||||
|
||||
!!! question
|
||||
|
||||
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重量は $wgt[i-1]$ で値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できますが、**アイテムの一部を選択することができ、その値は選択された重量の割合に基づいて計算されます**。限られた容量の下でナップサック内のアイテムの最大値は何ですか?例を下の図に示します。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-3 分数ナップサック問題の例データ </p>
|
||||
|
||||
分数ナップサック問題は全体的に0-1ナップサック問題と非常に似ており、現在のアイテム $i$ と容量 $c$ を含み、ナップサックの限られた容量内で値を最大化することを目的としています。
|
||||
|
||||
違いは、この問題ではアイテムの一部のみを選択できることです。下の図に示すように、**アイテムを任意に分割し、重量の割合に基づいて対応する値を計算できます**。
|
||||
|
||||
1. アイテム $i$ について、その単位重量あたりの値は $val[i-1] / wgt[i-1]$ で、単位値と呼ばれます。
|
||||
2. 重量 $w$ のアイテム $i$ の一部をナップサックに入れるとすると、ナップサックに追加される値は $w \times val[i-1] / wgt[i-1]$ です。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-4 アイテムの単位重量あたりの値 </p>
|
||||
|
||||
### 1. 貪欲戦略の決定
|
||||
|
||||
ナップサック内のアイテムの総値を最大化することは、**本質的に単位重量あたりの値を最大化することを意味します**。これから、下の図に示す貪欲戦略を導出できます。
|
||||
|
||||
1. アイテムを単位値の高い順から低い順にソートします。
|
||||
2. すべてのアイテムを反復し、**各ラウンドで最も高い単位値を持つアイテムを貪欲に選択**します。
|
||||
3. ナップサックの残り容量が不十分な場合、現在のアイテムの一部を使用してナップサックを満たします。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-5 分数ナップサック問題の貪欲戦略 </p>
|
||||
|
||||
### 2. コード実装
|
||||
|
||||
アイテムを単位値でソートするために `Item` クラスを作成しました。ナップサックが満たされるまでループして貪欲な選択を行い、その後終了して解を返します:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="fractional_knapsack.py"
|
||||
class Item:
|
||||
"""アイテム"""
|
||||
|
||||
def __init__(self, w: int, v: int):
|
||||
self.w = w # アイテムの重量
|
||||
self.v = v # アイテムの価値
|
||||
|
||||
def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:
|
||||
"""分数ナップサック:貪欲法"""
|
||||
# アイテムリストを作成、2 つの属性を含む:重量、価値
|
||||
items = [Item(w, v) for w, v in zip(wgt, val)]
|
||||
# 単位価値 item.v / item.w で高い順にソート
|
||||
items.sort(key=lambda item: item.v / item.w, reverse=True)
|
||||
# 貪欲選択をループ
|
||||
res = 0
|
||||
for item in items:
|
||||
if item.w <= cap:
|
||||
# 残り容量が十分な場合、アイテム全体をナップサックに入れる
|
||||
res += item.v
|
||||
cap -= item.w
|
||||
else:
|
||||
# 残り容量が不十分な場合、アイテムの一部をナップサックに入れる
|
||||
res += (item.v / item.w) * cap
|
||||
# 残り容量がなくなったため、ループを中断
|
||||
break
|
||||
return res
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="fractional_knapsack.cpp"
|
||||
/* アイテム */
|
||||
class Item {
|
||||
public:
|
||||
int w; // アイテムの重量
|
||||
int v; // アイテムの価値
|
||||
|
||||
Item(int w, int v) : w(w), v(v) {
|
||||
}
|
||||
};
|
||||
|
||||
/* 分数ナップサック:貪欲法 */
|
||||
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
|
||||
// アイテムリストを作成、2つの属性を含む:重量、価値
|
||||
vector<Item> items;
|
||||
for (int i = 0; i < wgt.size(); i++) {
|
||||
items.push_back(Item(wgt[i], val[i]));
|
||||
}
|
||||
// 単位価値 item.v / item.w で高い順にソート
|
||||
sort(items.begin(), items.end(), [](Item &a, Item &b) { return (double)a.v / a.w > (double)b.v / b.w; });
|
||||
// 貪欲選択をループ
|
||||
double res = 0;
|
||||
for (auto &item : items) {
|
||||
if (item.w <= cap) {
|
||||
// 残り容量が十分な場合、アイテム全体をナップサックに入れる
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 残り容量が不十分な場合、アイテムの一部をナップサックに入れる
|
||||
res += (double)item.v / item.w * cap;
|
||||
// 残り容量がなくなったため、ループを中断
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="fractional_knapsack.java"
|
||||
/* アイテム */
|
||||
class Item {
|
||||
int w; // アイテムの重量
|
||||
int v; // アイテムの価値
|
||||
|
||||
public Item(int w, int v) {
|
||||
this.w = w;
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
|
||||
/* 分数ナップサック:貪欲法 */
|
||||
double fractionalKnapsack(int[] wgt, int[] val, int cap) {
|
||||
// アイテムリストを作成、2つの属性を含む:重量、価値
|
||||
Item[] items = new Item[wgt.length];
|
||||
for (int i = 0; i < wgt.length; i++) {
|
||||
items[i] = new Item(wgt[i], val[i]);
|
||||
}
|
||||
// 単位価値 item.v / item.w で高い順にソート
|
||||
Arrays.sort(items, Comparator.comparingDouble(item -> -((double) item.v / item.w)));
|
||||
// 貪欲選択をループ
|
||||
double res = 0;
|
||||
for (Item item : items) {
|
||||
if (item.w <= cap) {
|
||||
// 残り容量が十分な場合、アイテム全体をナップサックに入れる
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// 残り容量が不十分な場合、アイテムの一部をナップサックに入れる
|
||||
res += (double) item.v / item.w * cap;
|
||||
// 残り容量がなくなったため、ループを中断
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="fractional_knapsack.cs"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{fractional_knapsack}-[func]{FractionalKnapsack}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="fractional_knapsack.go"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="fractional_knapsack.swift"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="fractional_knapsack.js"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="fractional_knapsack.ts"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="fractional_knapsack.dart"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="fractional_knapsack.rs"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractional_knapsack}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="fractional_knapsack.c"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="fractional_knapsack.kt"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="fractional_knapsack.rb"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractional_knapsack}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="fractional_knapsack.zig"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
ソート以外に、最悪の場合、アイテムのリスト全体を走査する必要があるため、**時間計算量は $O(n)$** です。ここで $n$ はアイテムの数です。
|
||||
|
||||
`Item` オブジェクトリストが初期化されるため、**空間計算量は $O(n)$** です。
|
||||
|
||||
### 3. 正しさの証明
|
||||
|
||||
背理法を使用します。アイテム $x$ が最高の単位値を持ち、あるアルゴリズムが最大値 `res` を生成するが、解にアイテム $x$ が含まれていないと仮定します。
|
||||
|
||||
今、ナップサックから任意のアイテムの単位重量を取り除き、アイテム $x$ の単位重量で置き換えます。アイテム $x$ の単位値が最高であるため、置き換え後の総値は確実に `res` より大きくなります。**これは `res` が最適解であるという仮定と矛盾し、最適解には必ずアイテム $x$ が含まれることを証明します**。
|
||||
|
||||
この解の他のアイテムについても、上記の矛盾を構築できます。全体的に、**単位値がより大きいアイテムは常により良い選択**であり、貪欲戦略が効果的であることを証明します。
|
||||
|
||||
下の図に示すように、アイテムの重量と単位値をそれぞれ二次元チャートの横軸と縦軸と見なすと、分数ナップサック問題は「限られた横軸範囲内で囲まれる最大面積を求める」ことに変換できます。この類推は、幾何学的観点から貪欲戦略の効果を理解するのに役立ちます。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-6 分数ナップサック問題の幾何学的表現 </p>
|
||||
@@ -0,0 +1,230 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.1 貪欲アルゴリズム
|
||||
|
||||
<u>貪欲アルゴリズム</u>は最適化問題を解決するための一般的なアルゴリズムで、基本的に問題の各意思決定段階で最も良い選択をすること、つまり局所的に最適な決定を貪欲に行い、グローバルに最適な解を見つけることを望みます。貪欲アルゴリズムは簡潔で効率的であり、多くの実用的な問題で広く使用されています。
|
||||
|
||||
貪欲アルゴリズムと動的プログラミングは、どちらも最適化問題を解決するためによく使用されます。両者は最適部分構造の性質に依存するなど、いくつかの類似点を共有していますが、動作方法が異なります。
|
||||
|
||||
- 動的プログラミングは現在の決定段階ですべての以前の決定を考慮し、過去の部分問題の解を使用して現在の部分問題の解を構築します。
|
||||
- 貪欲アルゴリズムは過去の決定を考慮せず、代わりに貪欲な選択を続け、問題が解決されるまで問題の範囲を継続的に狭めます。
|
||||
|
||||
まず、「完全ナップサック問題」の章で紹介された「コイン交換」の例を通じて貪欲アルゴリズムの動作原理を理解しましょう。すでによく知っていると思います。
|
||||
|
||||
!!! question
|
||||
|
||||
$n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ で、目標金額は $amt$ です。各種類のコインは無制限に利用可能で、目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は $-1$ を返してください。
|
||||
|
||||
この問題で採用される貪欲戦略を下の図に示します。目標金額が与えられたとき、**それに最も近く、それを超えないコインを貪欲に選択**し、目標金額が満たされるまでこのステップを繰り返します。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-1 コイン交換の貪欲戦略 </p>
|
||||
|
||||
実装コードは以下の通りです:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="coin_change_greedy.py"
|
||||
def coin_change_greedy(coins: list[int], amt: int) -> int:
|
||||
"""硬貨交換:貪欲法"""
|
||||
# coins リストがソートされていると仮定
|
||||
i = len(coins) - 1
|
||||
count = 0
|
||||
# 残り金額がなくなるまで貪欲選択をループ
|
||||
while amt > 0:
|
||||
# 残り金額に最も近く、それより小さい硬貨を見つける
|
||||
while i > 0 and coins[i] > amt:
|
||||
i -= 1
|
||||
# coins[i] を選択
|
||||
amt -= coins[i]
|
||||
count += 1
|
||||
# 実行可能な解が見つからない場合、-1 を返す
|
||||
return count if amt == 0 else -1
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="coin_change_greedy.cpp"
|
||||
/* 硬貨両替:貪欲法 */
|
||||
int coinChangeGreedy(vector<int> &coins, int amt) {
|
||||
// 硬貨リストが順序付けされていると仮定
|
||||
int i = coins.size() - 1;
|
||||
int count = 0;
|
||||
// 残り金額がなくなるまで貪欲選択をループ
|
||||
while (amt > 0) {
|
||||
// 残り金額に近く、それ以下の最小硬貨を見つける
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// coins[i] を選択
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 実行可能な解が見つからない場合、-1 を返す
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="coin_change_greedy.java"
|
||||
/* 硬貨両替:貪欲法 */
|
||||
int coinChangeGreedy(int[] coins, int amt) {
|
||||
// 硬貨リストが順序付けされていると仮定
|
||||
int i = coins.length - 1;
|
||||
int count = 0;
|
||||
// 残り金額がなくなるまで貪欲選択をループ
|
||||
while (amt > 0) {
|
||||
// 残り金額に近く、それ以下の最小硬貨を見つける
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// coins[i] を選択
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// 実行可能な解が見つからない場合、-1 を返す
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="coin_change_greedy.cs"
|
||||
[class]{coin_change_greedy}-[func]{CoinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="coin_change_greedy.go"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="coin_change_greedy.swift"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="coin_change_greedy.js"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="coin_change_greedy.ts"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="coin_change_greedy.dart"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="coin_change_greedy.rs"
|
||||
[class]{}-[func]{coin_change_greedy}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="coin_change_greedy.c"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="coin_change_greedy.kt"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="coin_change_greedy.rb"
|
||||
[class]{}-[func]{coin_change_greedy}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="coin_change_greedy.zig"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
```
|
||||
|
||||
感嘆するかもしれません:なんて簡潔なんだ!貪欲アルゴリズムは約10行のコードでコイン交換問題を解決します。
|
||||
|
||||
## 15.1.1 貪欲アルゴリズムの利点と制限
|
||||
|
||||
**貪欲アルゴリズムは直接的で実装が簡単であるだけでなく、通常非常に効率的でもあります**。上記のコードで、最小のコイン額面を $\min(coins)$ とすると、貪欲な選択ループは最大 $amt / \min(coins)$ 回実行され、時間計算量は $O(amt / \min(coins))$ になります。これは動的プログラミング解法の時間計算量 $O(n \times amt)$ よりも一桁小さいです。
|
||||
|
||||
しかし、**一部のコイン額面の組み合わせでは、貪欲アルゴリズムは最適解を見つけることができません**。下の図は2つの例を示しています。
|
||||
|
||||
- **正の例 $coins = [1, 5, 10, 20, 50, 100]$**:このコインの組み合わせでは、任意の $amt$ に対して、貪欲アルゴリズムは最適解を見つけることができます。
|
||||
- **負の例 $coins = [1, 20, 50]$**:$amt = 60$ とすると、貪欲アルゴリズムは組み合わせ $50 + 1 \times 10$ しか見つけられず、合計11枚のコインですが、動的プログラミングは最適解 $20 + 20 + 20$ を見つけることができ、3枚のコインのみが必要です。
|
||||
- **負の例 $coins = [1, 49, 50]$**:$amt = 98$ とすると、貪欲アルゴリズムは組み合わせ $50 + 1 \times 48$ しか見つけられず、合計49枚のコインですが、動的プログラミングは最適解 $49 + 49$ を見つけることができ、2枚のコインのみが必要です。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-2 貪欲アルゴリズムが最適解を見つけられない例 </p>
|
||||
|
||||
これは、コイン交換問題において、貪欲アルゴリズムがグローバルに最適な解を見つけることを保証できず、非常に悪い解を見つける可能性があることを意味します。動的プログラミングの方が適しています。
|
||||
|
||||
一般的に、貪欲アルゴリズムの適用性は2つのカテゴリに分類されます。
|
||||
|
||||
1. **最適解を見つけることが保証される**:これらの場合、貪欲アルゴリズムはしばしば最良の選択で、バックトラッキングや動的プログラミングよりも効率的である傾向があります。
|
||||
2. **準最適解を見つけることができる**:貪欲アルゴリズムはここでも適用可能です。多くの複雑な問題では、グローバル最適解を見つけることは非常に困難であり、高効率の準最適解を見つけることも非常に価値があります。
|
||||
|
||||
## 15.1.2 貪欲アルゴリズムの特徴
|
||||
|
||||
それでは、どのような問題が貪欲アルゴリズムで解決するのに適しているのでしょうか?言い換えれば、どのような条件下で貪欲アルゴリズムは最適解を見つけることを保証できるのでしょうか?
|
||||
|
||||
動的プログラミングと比較して、貪欲アルゴリズムはより厳しい使用条件を持ち、主に問題の2つの性質に焦点を当てています。
|
||||
|
||||
- **貪欲選択性**:局所的に最適な選択が常にグローバルに最適な解に導くことができる場合のみ、貪欲アルゴリズムは最適解を得ることを保証できます。
|
||||
- **最適部分構造**:元の問題の最適解はその部分問題の最適解を含みます。
|
||||
|
||||
最適部分構造は「動的プログラミング」の章ですでに紹介されているため、ここではこれ以上議論しません。一部の問題には明らかな最適部分構造がありませんが、それでも貪欲アルゴリズムを使用して解決できることに注意することが重要です。
|
||||
|
||||
主に貪欲選択性を決定する方法を探索します。その記述は単純に見えますが、**実際には、多くの問題の貪欲選択性を証明することは容易ではありません**。
|
||||
|
||||
例えば、コイン交換問題では、貪欲選択性を反証するために反例を簡単に挙げることができますが、それを証明することははるかに困難です。**コインの組み合わせが貪欲アルゴリズムを使用して解決できるためには、どのような条件を満たす必要があるか**と尋ねられた場合、厳密な数学的証明を提供することが困難であるため、しばしば直感や例に頼って曖昧な答えを提供しなければなりません。
|
||||
|
||||
!!! quote
|
||||
|
||||
ある論文では、コインの組み合わせが任意の金額に対して貪欲アルゴリズムを使用して最適解を見つけることができるかどうかを判定するための時間計算量 $O(n^3)$ のアルゴリズムが提示されています。
|
||||
|
||||
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
|
||||
|
||||
## 15.1.3 貪欲アルゴリズムによる問題解決のステップ
|
||||
|
||||
貪欲問題の問題解決プロセスは、一般的に以下の3つのステップに分けることができます。
|
||||
|
||||
1. **問題分析**:問題の特徴を整理し理解する。状態定義、最適化目標、制約などを含みます。このステップはバックトラッキングや動的プログラミングでも関与します。
|
||||
2. **貪欲戦略の決定**:各ステップで貪欲な選択をする方法を決定する。この戦略は各ステップで問題の規模を縮小し、最終的に問題全体を解決できます。
|
||||
3. **正確性の証明**:通常、問題が貪欲選択性と最適部分構造の両方を持つことを証明する必要があります。このステップには、帰納法や背理法などの数学的証明が必要な場合があります。
|
||||
|
||||
貪欲戦略の決定は問題解決の核心ステップですが、実装は容易ではない場合があります。主な理由は以下の通りです。
|
||||
|
||||
- **異なる問題間で貪欲戦略は大きく異なる**。多くの問題では、貪欲戦略はかなり直接的で、一般的な思考と試行を通じて思いつくことができます。しかし、一部の複雑な問題では、貪欲戦略は非常に見つけにくく、これは個人の問題解決経験とアルゴリズム能力の真のテストです。
|
||||
- **一部の貪欲戦略は非常に誤解を招く**。自信を持って貪欲戦略を設計し、コードを書いてテストに提出したとき、一部のテストケースが通らない可能性が高いです。これは設計された貪欲戦略が「部分的に正しい」だけであるためで、上記のコイン交換の例で説明した通りです。
|
||||
|
||||
正確性を確保するために、貪欲戦略に対して厳密な数学的証明を提供すべきで、**通常は背理法や数学的帰納法を含みます**。
|
||||
|
||||
しかし、正確性を証明することは容易な作業ではない場合があります。途方に暮れた場合、通常はテストケースに基づいてコードをデバッグし、貪欲戦略を段階的に修正し検証することを選択します。
|
||||
|
||||
## 15.1.4 貪欲アルゴリズムで解決される典型的な問題
|
||||
|
||||
貪欲アルゴリズムは、貪欲選択と最適部分構造の性質を満たす最適化問題によく適用されます。以下は典型的な貪欲アルゴリズム問題のいくつかです。
|
||||
|
||||
- **コイン交換問題**:一部のコインの組み合わせでは、貪欲アルゴリズムは常に最適解を提供します。
|
||||
- **区間スケジューリング問題**:いくつかのタスクがあり、それぞれが一定期間にわたって行われるとします。目標はできるだけ多くのタスクを完了することです。常に最も早く終了するタスクを選択すると、貪欲アルゴリズムは最適解を達成できます。
|
||||
- **分数ナップサック問題**:アイテムのセットと運搬容量が与えられ、目標は総重量が運搬容量を超えず、総価値が最大化されるようなアイテムのセットを選択することです。常に最高の価値対重量比(価値/重量)のアイテムを選択すると、貪欲アルゴリズムは一部のケースで最適解を達成できます。
|
||||
- **株式取引問題**:株価の履歴のセットが与えられ、複数回の取引を行うことができますが、すでに株式を所有している場合は売却後でないと再度購入できません。目標は最大利益を達成することです。
|
||||
- **ハフマン符号化**:ハフマン符号化は無損失データ圧縮に使用される貪欲アルゴリズムです。ハフマン木を構築することにより、常に最低頻度の2つのノードを統合し、最小重み付きパス長(符号化長)のハフマン木を生成します。
|
||||
- **ダイクストラのアルゴリズム**:これは与えられたソース頂点から他のすべての頂点への最短経路問題を解決するための貪欲アルゴリズムです。
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
comments: true
|
||||
icon: material/head-heart-outline
|
||||
---
|
||||
|
||||
# 第 15 章 貪欲法
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
ひまわりは太陽の方を向き、常に自分にとって最大の成長を求めます。
|
||||
|
||||
貪欲な戦略は、一連の単純な選択を通じて、段階的に最良の答えへと導きます。
|
||||
|
||||
## 章の内容
|
||||
|
||||
- [15.1 貪欲アルゴリズム](greedy_algorithm.md)
|
||||
- [15.2 分数ナップサック問題](fractional_knapsack_problem.md)
|
||||
- [15.3 最大容量問題](max_capacity_problem.md)
|
||||
- [15.4 最大積切断問題](max_product_cutting_problem.md)
|
||||
- [15.5 まとめ](summary.md)
|
||||
@@ -0,0 +1,249 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.3 最大容量問題
|
||||
|
||||
!!! question
|
||||
|
||||
配列 $ht$ を入力します。各要素は垂直仕切りの高さを表します。配列内の任意の2つの仕切りと、それらの間のスペースによってコンテナを形成できます。
|
||||
|
||||
コンテナの容量は高さと幅の積(面積)で、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。
|
||||
|
||||
コンテナの容量を最大化する2つの仕切りを配列から選択し、この最大容量を返してください。例を下の図に示します。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-7 最大容量問題の例データ </p>
|
||||
|
||||
コンテナは任意の2つの仕切りによって形成されるため、**この問題の状態は2つの仕切りのインデックスで表現され、$[i, j]$ と表記されます**。
|
||||
|
||||
問題の記述によれば、容量は高さと幅の積に等しく、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。容量 $cap[i, j]$ の式は:
|
||||
|
||||
$$
|
||||
cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
|
||||
$$
|
||||
|
||||
配列の長さを $n$ と仮定すると、2つの仕切りの組み合わせ数(状態の総数)は $C_n^2 = \frac{n(n - 1)}{2}$ です。最も直接的なアプローチは**すべての可能な状態を列挙する**ことで、時間計算量は $O(n^2)$ になります。
|
||||
|
||||
### 1. 貪欲戦略の決定
|
||||
|
||||
この問題にはより効率的な解法があります。下の図に示すように、インデックス $i < j$ かつ高さ $ht[i] < ht[j]$ の状態 $[i, j]$ を選択します。つまり、$i$ は短い仕切り、$j$ は高い仕切りです。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-8 初期状態 </p>
|
||||
|
||||
下の図に示すように、**高い仕切り $j$ を短い仕切り $i$ に近づけて移動すると、容量は確実に減少します**。
|
||||
|
||||
これは、高い仕切り $j$ を移動すると、幅 $j-i$ が確実に減少するためです。高さは短い仕切りによって決定されるため、高さは同じまま($i$ が短い仕切りのまま)か減少(移動した $j$ が短い仕切りになる)しかありません。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-9 高い仕切りを内側に移動した後の状態 </p>
|
||||
|
||||
逆に、**短い仕切り $i$ を内側に移動することによってのみ容量を増加させることが可能です**。幅は確実に減少しますが、**高さが増加する可能性があります**(移動した短い仕切り $i$ が高くなる場合)。例えば、下の図では、短い仕切りを移動した後に面積が増加しています。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-10 短い仕切りを内側に移動した後の状態 </p>
|
||||
|
||||
これにより、この問題の貪欲戦略が導かれます:コンテナの両端に2つのポインタを初期化し、各ラウンドで短い仕切りに対応するポインタを内側に移動し、2つのポインタが出会うまで続けます。
|
||||
|
||||
下の図は貪欲戦略の実行を示しています。
|
||||
|
||||
1. 最初に、ポインタ $i$ と $j$ が配列の両端に配置されます。
|
||||
2. 現在の状態の容量 $cap[i, j]$ を計算し、最大容量を更新します。
|
||||
3. 仕切り $i$ と $j$ の高さを比較し、短い仕切りを1ステップ内側に移動します。
|
||||
4. $i$ と $j$ が出会うまでステップ `2.` と `3.` を繰り返します。
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<4>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<5>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<6>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-11 最大容量問題の貪欲プロセス </p>
|
||||
|
||||
### 2. 実装
|
||||
|
||||
コードは最大 $n$ 回ループするため、**時間計算量は $O(n)$** です。
|
||||
|
||||
変数 $i$、$j$、$res$ は一定量の追加スペースを使用するため、**空間計算量は $O(1)$** です。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="max_capacity.py"
|
||||
def max_capacity(ht: list[int]) -> int:
|
||||
"""最大容量:貪欲法"""
|
||||
# i、j を初期化、配列の両端で分割させる
|
||||
i, j = 0, len(ht) - 1
|
||||
# 初期最大容量は 0
|
||||
res = 0
|
||||
# 2 つの板が出会うまで貪欲選択をループ
|
||||
while i < j:
|
||||
# 最大容量を更新
|
||||
cap = min(ht[i], ht[j]) * (j - i)
|
||||
res = max(res, cap)
|
||||
# 短い板を内側に移動
|
||||
if ht[i] < ht[j]:
|
||||
i += 1
|
||||
else:
|
||||
j -= 1
|
||||
return res
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="max_capacity.cpp"
|
||||
/* 最大容量:貪欲法 */
|
||||
int maxCapacity(vector<int> &ht) {
|
||||
// i、j を初期化し、配列の両端で分割させる
|
||||
int i = 0, j = ht.size() - 1;
|
||||
// 初期最大容量は 0
|
||||
int res = 0;
|
||||
// 2つの板が出会うまで貪欲選択をループ
|
||||
while (i < j) {
|
||||
// 最大容量を更新
|
||||
int cap = min(ht[i], ht[j]) * (j - i);
|
||||
res = max(res, cap);
|
||||
// より短い板を内側に移動
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="max_capacity.java"
|
||||
/* 最大容量:貪欲法 */
|
||||
int maxCapacity(int[] ht) {
|
||||
// i、j を初期化し、配列の両端で分割させる
|
||||
int i = 0, j = ht.length - 1;
|
||||
// 初期最大容量は 0
|
||||
int res = 0;
|
||||
// 2つの板が出会うまで貪欲選択をループ
|
||||
while (i < j) {
|
||||
// 最大容量を更新
|
||||
int cap = Math.min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.max(res, cap);
|
||||
// より短い板を内側に移動
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="max_capacity.cs"
|
||||
[class]{max_capacity}-[func]{MaxCapacity}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="max_capacity.go"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="max_capacity.swift"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="max_capacity.js"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="max_capacity.ts"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="max_capacity.dart"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="max_capacity.rs"
|
||||
[class]{}-[func]{max_capacity}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="max_capacity.c"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="max_capacity.kt"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="max_capacity.rb"
|
||||
[class]{}-[func]{max_capacity}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="max_capacity.zig"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
|
||||
### 3. 正しさの証明
|
||||
|
||||
貪欲法が列挙よりも高速である理由は、各ラウンドの貪欲選択が一部の状態を「スキップ」するからです。
|
||||
|
||||
例えば、$i$ が短い仕切りで $j$ が高い仕切りである状態 $cap[i, j]$ の下で、短い仕切り $i$ を貪欲に1ステップ内側に移動すると、下の図に示す「スキップされた」状態につながります。**これは、これらの状態の容量を後で検証できないことを意味します**。
|
||||
|
||||
$$
|
||||
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-12 短い仕切りの移動によってスキップされる状態 </p>
|
||||
|
||||
観察すると、**これらのスキップされた状態は実際には高い仕切り $j$ が内側に移動したすべての状態**です。高い仕切りを内側に移動すると容量が確実に減少することをすでに証明しました。したがって、スキップされた状態は最適解である可能性がなく、**それらをスキップしても最適解を見逃すことはありません**。
|
||||
|
||||
分析により、短い仕切りを移動する操作は「安全」であり、貪欲戦略が効果的であることが示されます。
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.4 最大積切断問題
|
||||
|
||||
!!! question
|
||||
|
||||
正の整数 $n$ が与えられたとき、それを合計が $n$ になる少なくとも2つの正の整数に分割し、これらの整数の最大積を求めてください。下の図に示すとおりです。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-13 最大積切断問題の定義 </p>
|
||||
|
||||
$n$ を $m$ 個の整数因子に分割すると仮定し、$i$ 番目の因子を $n_i$ と表記すると、
|
||||
|
||||
$$
|
||||
n = \sum_{i=1}^{m}n_i
|
||||
$$
|
||||
|
||||
この問題の目標は、すべての整数因子の最大積を見つけることです。すなわち、
|
||||
|
||||
$$
|
||||
\max(\prod_{i=1}^{m}n_i)
|
||||
$$
|
||||
|
||||
考慮すべき点:分割数 $m$ はどの程度大きくすべきか、各 $n_i$ は何であるべきか?
|
||||
|
||||
### 1. 貪欲戦略の決定
|
||||
|
||||
経験的に、2つの整数の積は多くの場合その和よりも大きくなります。$n$ から因子 $2$ を分割すると仮定すると、その積は $2(n-2)$ です。この積を $n$ と比較します:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
2(n-2) & \geq n \newline
|
||||
2n - n - 4 & \geq 0 \newline
|
||||
n & \geq 4
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
下の図に示すように、$n \geq 4$ のとき、$2$ を分割すると積が増加します。**これは4以上の整数を分割すべきであることを示しています**。
|
||||
|
||||
**貪欲戦略1**:分割スキームが $\geq 4$ の因子を含む場合、それらはさらに分割されるべきです。最終的な分割は因子 $1$、$2$、$3$ のみを含むべきです。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-14 分割による積の増加 </p>
|
||||
|
||||
次に、どの因子が最適かを考慮します。因子 $1$、$2$、$3$ の中で、明らかに $1$ が最悪です。$1 \times (n-1) < n$ が常に成り立つため、$1$ を分割すると実際に積が減少します。
|
||||
|
||||
下の図に示すように、$n = 6$ のとき、$3 \times 3 > 2 \times 2 \times 2$ です。**これは $3$ を分割する方が $2$ を分割するよりも良いことを意味します**。
|
||||
|
||||
**貪欲戦略2**:分割スキームには最大で2つの $2$ があるべきです。3つの $2$ は常に2つの $3$ に置き換えてより高い積を得ることができるからです。
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-15 最適な分割因子 </p>
|
||||
|
||||
上記から、以下の貪欲戦略を導出できます。
|
||||
|
||||
1. 入力整数 $n$ について、余りが $0$、$1$、または $2$ になるまで因子 $3$ を継続的に分割します。
|
||||
2. 余りが $0$ の場合、$n$ が $3$ の倍数であることを意味するため、それ以上の行動は取りません。
|
||||
3. 余りが $2$ の場合、さらに分割を続けず、そのまま保持します。
|
||||
4. 余りが $1$ の場合、$2 \times 2 > 1 \times 3$ であるため、最後の $3$ を $2$ に置き換えるべきです。
|
||||
|
||||
### 2. コード実装
|
||||
|
||||
下の図に示すように、整数を分割するためにループを使用する必要はなく、床除算演算を使用して $3$ の数 $a$ を取得し、剰余演算を使用して余り $b$ を取得できます。したがって:
|
||||
|
||||
$$
|
||||
n = 3a + b
|
||||
$$
|
||||
|
||||
$n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積は $1 \times (n - 1)$ であることに注意してください。
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="max_product_cutting.py"
|
||||
def max_product_cutting(n: int) -> int:
|
||||
"""切断の最大積:貪欲法"""
|
||||
# n <= 3 の場合、1 を切り出す必要がある
|
||||
if n <= 3:
|
||||
return 1 * (n - 1)
|
||||
# 貪欲的に 3 を切り出す、a は 3 の個数、b は余り
|
||||
a, b = n // 3, n % 3
|
||||
if b == 1:
|
||||
# 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換
|
||||
return int(math.pow(3, a - 1)) * 2 * 2
|
||||
if b == 2:
|
||||
# 余りが 2 の場合、何もしない
|
||||
return int(math.pow(3, a)) * 2
|
||||
# 余りが 0 の場合、何もしない
|
||||
return int(math.pow(3, a))
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="max_product_cutting.cpp"
|
||||
/* 最大積切断:貪欲法 */
|
||||
int maxProductCutting(int n) {
|
||||
// n <= 3 の場合、1 を切り出す必要がある
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪欲に 3 を切り出す。a は 3 の個数、b は余り
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換
|
||||
return (int)pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
// 余りが 2 の場合、何もしない
|
||||
return (int)pow(3, a) * 2;
|
||||
}
|
||||
// 余りが 0 の場合、何もしない
|
||||
return (int)pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title="max_product_cutting.java"
|
||||
/* 最大積切断:貪欲法 */
|
||||
int maxProductCutting(int n) {
|
||||
// n <= 3 の場合、1 を切り出す必要がある
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// 貪欲に 3 を切り出す。a は 3 の個数、b は余り
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変換
|
||||
return (int) Math.pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
// 余りが 2 の場合、何もしない
|
||||
return (int) Math.pow(3, a) * 2;
|
||||
}
|
||||
// 余りが 0 の場合、何もしない
|
||||
return (int) Math.pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title="max_product_cutting.cs"
|
||||
[class]{max_product_cutting}-[func]{MaxProductCutting}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="max_product_cutting.go"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="max_product_cutting.swift"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="max_product_cutting.js"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="max_product_cutting.ts"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="max_product_cutting.dart"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="max_product_cutting.rs"
|
||||
[class]{}-[func]{max_product_cutting}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="max_product_cutting.c"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="max_product_cutting.kt"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="max_product_cutting.rb"
|
||||
[class]{}-[func]{max_product_cutting}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="max_product_cutting.zig"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> 図 15-16 切断後の最大積の計算方法 </p>
|
||||
|
||||
**時間計算量はプログラミング言語のべき乗演算の実装に依存します**。Pythonでは、よく使用されるべき乗計算関数は3種類あります:
|
||||
|
||||
- 演算子 `**` と関数 `pow()` の両方の時間計算量は $O(\log a)$ です。
|
||||
- `math.pow()` 関数は内部でC言語ライブラリの `pow()` 関数を呼び出し、浮動小数点べき乗を実行し、時間計算量は $O(1)$ です。
|
||||
|
||||
変数 $a$ と $b$ は一定サイズの追加スペースを使用するため、**空間計算量は $O(1)$** です。
|
||||
|
||||
### 3. 正しさの証明
|
||||
|
||||
背理法を使用し、$n \geq 3$ のケースのみを分析します。
|
||||
|
||||
1. **すべての因子 $\leq 3$**:最適分割スキームが因子 $x \geq 4$ を含むと仮定すると、それを確実に $2(x-2)$ にさらに分割でき、より大きな積を得られます。これは仮定と矛盾します。
|
||||
2. **分割スキームに $1$ が含まれない**:最適分割スキームが因子 $1$ を含むと仮定すると、それを確実に別の因子と結合してより大きな積を得られます。これは仮定と矛盾します。
|
||||
3. **分割スキームには最大で2つの $2$ が含まれる**:最適分割スキームが3つの $2$ を含むと仮定すると、それらを確実に2つの $3$ に置き換えて、より高い積を達成できます。これは仮定と矛盾します。
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.5 まとめ
|
||||
|
||||
- 貪欲アルゴリズムは最適化問題を解決するためによく使用され、原理は各決定段階で局所的に最適な決定を行い、グローバルに最適な解を達成することです。
|
||||
- 貪欲アルゴリズムは貪欲な選択を次々と反復的に行い、各ラウンドで問題をより小さな部分問題に変換し、問題が解決されるまで続けます。
|
||||
- 貪欲アルゴリズムは実装が簡単なだけでなく、問題解決効率も高いです。動的プログラミングと比較して、貪欲アルゴリズムは一般的により低い時間計算量を持ちます。
|
||||
- コイン交換問題において、貪欲アルゴリズムは特定のコインの組み合わせに対して最適解を保証できますが、他の組み合わせでは貪欲アルゴリズムが非常に悪い解を見つける可能性があります。
|
||||
- 貪欲アルゴリズム解法に適した問題は2つの主要な性質を持ちます:貪欲選択性と最適部分構造。貪欲選択性は貪欲戦略の効果を表します。
|
||||
- 一部の複雑な問題では、貪欲選択性を証明することは簡単ではありません。逆に、無効性を証明することはしばしばより容易で、コイン交換問題などがその例です。
|
||||
- 貪欲問題の解決は主に3つのステップから構成されます:問題分析、貪欲戦略の決定、正しさの証明。このうち、貪欲戦略の決定が重要なステップであり、正しさの証明がしばしば挑戦となります。
|
||||
- 分数ナップサック問題は0-1ナップサック問題に基づいてアイテムの一部の選択を可能にし、したがって貪欲アルゴリズムを使用して解決できます。貪欲戦略の正しさは背理法によって証明できます。
|
||||
- 最大容量問題は全探索法で解決でき、時間計算量は $O(n^2)$ です。貪欲戦略を設計することで、各ラウンドで短い板を内側に移動し、時間計算量を $O(n)$ に最適化します。
|
||||
- 切断後の最大積問題において、2つの貪欲戦略を導出します:$\geq 4$ の整数は継続的に切断されるべきで、最適な切断因子は $3$ です。コードにはべき乗演算が含まれ、時間計算量はべき乗演算の実装方法に依存し、一般的に $O(1)$ または $O(\log n)$ です。
|
||||
Reference in New Issue
Block a user