This commit is contained in:
krahets
2026-04-03 18:46:15 +08:00
parent 377736b1bd
commit 9d21ca86b0
352 changed files with 46563 additions and 11262 deletions
@@ -6,30 +6,30 @@ comments: true
!!! question
$n$ 個のアイテムが与えられ、$i$ 番目のアイテムの重は $wgt[i-1]$値は $val[i-1]$ です。容量が $cap$ のナップサックがあります。各アイテムは1回のみ選択できますが、**アイテムの一部を選択することでき、その値は選択された重量の割合に基づいて計算されます**。限られた容量の下でナップサック内のアイテムの最大値は何ですか?例を下の図に示します。
$n$ 個の品物が与えられ、$i$ 個の品物の重は $wgt[i-1]$、価値は $val[i-1]$ であり、容量が $cap$ のナップサックがある。各品物は 1 回だけ選択できが、**品物の一部を選ことでき、値は選択た重量の割合に応じて計算され**。容量制限の下でナップサック内の品物の最大価値を求めよ。例を以下に示す。
![分数ナップサック問題の例データ](fractional_knapsack_problem.assets/fractional_knapsack_example.png){ class="animation-figure" }
<p align="center"> 図 15-3 &nbsp; 分数ナップサック問題の例データ </p>
分数ナップサック問題は全体的に0-1ナップサック問題と非常に似ており、現在のアイテム $i$ と容量 $c$ を含み、ナップサックの限られた容量内で値を最大化することを目的としています
分数ナップサック問題は 0-1 ナップサック問題と全体として非常によく似ており、状態には現在の品物 $i$ と容量 $c$ が含まれ、目標は容量制限下での最大価値を求めることである
違いは、この問題ではアイテムの一部のみを選択できることです。下の図に示すように、**アイテムを任意に分割し、重量の割合に基づいて対応する値を計算できます**。
異なる点は、本問では品物の一部だけを選べることである。以下に示すように、**品物は任意に分割でき、対応する価値は重量の割合に応じて計算される**。
1. アイテム $i$ について、その単位重量あたりの値は $val[i-1] / wgt[i-1]$ で単位値と呼ばれます
2. 重量 $w$ のアイテム $i$ の一部をナップサックに入れるとすると、ナップサックに追加される値は $w \times val[i-1] / wgt[i-1]$ です
1. 品物 $i$ について、単位重量あたりの値は $val[i-1] / wgt[i-1]$ であり、これを単位値と呼
2. 品物 $i$ の一部を重さ $w$ だけ入れると、ナップサックに増える価値は $w \times val[i-1] / wgt[i-1]$ となる
![アイテムの単位重量あたりの値](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" }
![品物の単位重量あたりの](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" }
<p align="center"> 図 15-4 &nbsp; アイテムの単位重量あたりの値 </p>
<p align="center"> 図 15-4 &nbsp; 品物の単位重量あたりの値 </p>
### 1. &nbsp; 貪欲戦略の決定
ナップサック内のアイテムの総値を最大化することは、**本質的に単位重量あたりの値を最大化することを意味します**。これから、下の図に示す貪欲戦略を導出できます
ナップサック内の品物の総値を最大化することは、**本質的に単位重量あたりの品物価値を最大化すること**である。そこから、以下に示す貪欲戦略を導ける
1. アイテムを単位値の高い順から低い順にソートします。
2. すべてのアイテムを反復し、**各ラウンドで最も高い単位値を持つアイテムを貪欲に選択**します。
3. ナップサックの残り容量が不十分な場合、現在のアイテムの一部を使用してナップサックを満たします。
1. 品物を単位値の高い順にソートす
2. すべての品物を走査し、**各回で単位価値が最も高い品物を貪欲に選択する**
3. 残りのナップサック容量が足りない場合、現在の品物の一部を使てナップサックを満たす。
![分数ナップサック問題の貪欲戦略](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png){ class="animation-figure" }
@@ -37,35 +37,35 @@ comments: true
### 2. &nbsp; コード実装
アイテムを単位値でソートするために `Item` クラスを作成しました。ナップサックが満たされるまでループして貪欲な選択を行い、その後終了して解を返します:
品物を単位値でソートできるように、`Item` クラスを定義する。貪欲選択を繰り返し、ナップサックが満杯になったら終了して解を返す。
=== "Python"
```python title="fractional_knapsack.py"
class Item:
"""アイテム"""
"""品物"""
def __init__(self, w: int, v: int):
self.w = w # アイテムの重
self.v = v # アイテムの価値
self.w = w # 品物の重
self.v = v # 品物の価値
def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:
"""分数ナップサック:貪欲法"""
# アイテムリストを作成、2 つの属性を含む:重量、価値
# 重さと価値の 2 属性を持つ品物リストを作成
items = [Item(w, v) for w, v in zip(wgt, val)]
# 単位価値 item.v / item.w 高い順にソート
# 単位価値 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
```
@@ -73,11 +73,11 @@ comments: true
=== "C++"
```cpp title="fractional_knapsack.cpp"
/* アイテム */
/* 品物 */
class Item {
public:
int w; // アイテムの重
int v; // アイテムの価値
int w; // 品物の重
int v; // 品物の価値
Item(int w, int v) : w(w), v(v) {
}
@@ -85,24 +85,24 @@ comments: true
/* 分数ナップサック:貪欲法 */
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
// アイテムリストを作成、2つの属性を含む:重量、価値
// 重さと価値の 2 属性を持つ品物リストを作成
vector<Item> items;
for (int i = 0; i < wgt.size(); i++) {
items.push_back(Item(wgt[i], val[i]));
}
// 単位価値 item.v / item.w 高い順にソート
// 単位価値 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;
}
}
@@ -113,10 +113,10 @@ comments: true
=== "Java"
```java title="fractional_knapsack.java"
/* アイテム */
/* 品物 */
class Item {
int w; // アイテムの重
int v; // アイテムの価値
int w; // 品物の重
int v; // 品物の価値
public Item(int w, int v) {
this.w = w;
@@ -126,24 +126,24 @@ comments: true
/* 分数ナップサック:貪欲法 */
double fractionalKnapsack(int[] wgt, int[] val, int cap) {
// アイテムリストを作成、2つの属性を含む:重量、価値
// 重さと価値の 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 高い順にソート
// 単位価値 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;
}
}
@@ -154,96 +154,403 @@ comments: true
=== "C#"
```csharp title="fractional_knapsack.cs"
[class]{Item}-[func]{}
/* 品物 */
class Item(int w, int v) {
public int w = w; // 品物の重さ
public int v = v; // 品物の価値
}
[class]{fractional_knapsack}-[func]{FractionalKnapsack}
/* 分数ナップサック:貪欲法 */
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 の高い順にソートする
Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));
// 貪欲選択を繰り返す
double res = 0;
foreach (Item item in items) {
if (item.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (double)item.v / item.w * cap;
// 残り容量がないため、ループを抜ける
break;
}
}
return res;
}
```
=== "Go"
```go title="fractional_knapsack.go"
[class]{Item}-[func]{}
/* 品物 */
type Item struct {
w int // 品物の重さ
v int // 品物の価値
}
[class]{}-[func]{fractionalKnapsack}
/* 分数ナップサック:貪欲法 */
func fractionalKnapsack(wgt []int, val []int, cap int) float64 {
// 重さと価値の 2 属性を持つ品物リストを作成
items := make([]Item, len(wgt))
for i := 0; i < len(wgt); i++ {
items[i] = Item{wgt[i], val[i]}
}
// 単位価値 item.v / item.w の高い順にソートする
sort.Slice(items, func(i, j int) bool {
return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)
})
// 貪欲選択を繰り返す
res := 0.0
for _, item := range items {
if item.w <= cap {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += float64(item.v)
cap -= item.w
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += float64(item.v) / float64(item.w) * float64(cap)
// 残り容量がないため、ループを抜ける
break
}
}
return res
}
```
=== "Swift"
```swift title="fractional_knapsack.swift"
[class]{Item}-[func]{}
/* 品物 */
class Item {
var w: Int // 品物の重さ
var v: Int // 品物の価値
[class]{}-[func]{fractionalKnapsack}
init(w: Int, v: Int) {
self.w = w
self.v = v
}
}
/* 分数ナップサック:貪欲法 */
func fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {
// 重さと価値の 2 属性を持つ品物リストを作成
var items = zip(wgt, val).map { Item(w: $0, v: $1) }
// 単位価値 item.v / item.w の高い順にソートする
items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }
// 貪欲選択を繰り返す
var res = 0.0
var cap = cap
for item in items {
if item.w <= cap {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += Double(item.v)
cap -= item.w
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += Double(item.v) / Double(item.w) * Double(cap)
// 残り容量がないため、ループを抜ける
break
}
}
return res
}
```
=== "JS"
```javascript title="fractional_knapsack.js"
[class]{Item}-[func]{}
/* 品物 */
class Item {
constructor(w, v) {
this.w = w; // 品物の重さ
this.v = v; // 品物の価値
}
}
[class]{}-[func]{fractionalKnapsack}
/* 分数ナップサック:貪欲法 */
function fractionalKnapsack(wgt, val, cap) {
// 重さと価値の 2 属性を持つ品物リストを作成
const items = wgt.map((w, i) => new Item(w, val[i]));
// 単位価値 item.v / item.w の高い順にソートする
items.sort((a, b) => b.v / b.w - a.v / a.w);
// 貪欲選択を繰り返す
let res = 0;
for (const item of items) {
if (item.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (item.v / item.w) * cap;
// 残り容量がないため、ループを抜ける
break;
}
}
return res;
}
```
=== "TS"
```typescript title="fractional_knapsack.ts"
[class]{Item}-[func]{}
/* 品物 */
class Item {
w: number; // 品物の重さ
v: number; // 品物の価値
[class]{}-[func]{fractionalKnapsack}
constructor(w: number, v: number) {
this.w = w;
this.v = v;
}
}
/* 分数ナップサック:貪欲法 */
function fractionalKnapsack(wgt: number[], val: number[], cap: number): number {
// 重さと価値の 2 属性を持つ品物リストを作成
const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));
// 単位価値 item.v / item.w の高い順にソートする
items.sort((a, b) => b.v / b.w - a.v / a.w);
// 貪欲選択を繰り返す
let res = 0;
for (const item of items) {
if (item.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (item.v / item.w) * cap;
// 残り容量がないため、ループを抜ける
break;
}
}
return res;
}
```
=== "Dart"
```dart title="fractional_knapsack.dart"
[class]{Item}-[func]{}
/* 品物 */
class Item {
int w; // 品物の重さ
int v; // 品物の価値
[class]{}-[func]{fractionalKnapsack}
Item(this.w, this.v);
}
/* 分数ナップサック:貪欲法 */
double fractionalKnapsack(List<int> wgt, List<int> val, int cap) {
// 重さと価値の 2 属性を持つ品物リストを作成
List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));
// 単位価値 item.v / item.w の高い順にソートする
items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));
// 貪欲選択を繰り返す
double res = 0;
for (Item item in items) {
if (item.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += item.v / item.w * cap;
// 残り容量がないため、ループを抜ける
break;
}
}
return res;
}
```
=== "Rust"
```rust title="fractional_knapsack.rs"
[class]{Item}-[func]{}
/* 品物 */
struct Item {
w: i32, // 品物の重さ
v: i32, // 品物の価値
}
[class]{}-[func]{fractional_knapsack}
impl Item {
fn new(w: i32, v: i32) -> Self {
Self { w, v }
}
}
/* 分数ナップサック:貪欲法 */
fn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {
// 重さと価値の 2 属性を持つ品物リストを作成
let mut items = wgt
.iter()
.zip(val.iter())
.map(|(&w, &v)| Item::new(w, v))
.collect::<Vec<Item>>();
// 単位価値 item.v / item.w の高い順にソートする
items.sort_by(|a, b| {
(b.v as f64 / b.w as f64)
.partial_cmp(&(a.v as f64 / a.w as f64))
.unwrap()
});
// 貪欲選択を繰り返す
let mut res = 0.0;
for item in &items {
if item.w <= cap {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v as f64;
cap -= item.w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += item.v as f64 / item.w as f64 * cap as f64;
// 残り容量がないため、ループを抜ける
break;
}
}
res
}
```
=== "C"
```c title="fractional_knapsack.c"
[class]{Item}-[func]{}
/* 品物 */
typedef struct {
int w; // 品物の重さ
int v; // 品物の価値
} Item;
[class]{}-[func]{fractionalKnapsack}
/* 分数ナップサック:貪欲法 */
float fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {
// 重さと価値の 2 属性を持つ品物リストを作成
Item *items = malloc(sizeof(Item) * itemCount);
for (int i = 0; i < itemCount; i++) {
items[i] = (Item){.w = wgt[i], .v = val[i]};
}
// 単位価値 item.v / item.w の高い順にソートする
qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);
// 貪欲選択を繰り返す
float res = 0.0;
for (int i = 0; i < itemCount; i++) {
if (items[i].w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += items[i].v;
cap -= items[i].w;
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (float)cap / items[i].w * items[i].v;
cap = 0;
break;
}
}
free(items);
return res;
}
```
=== "Kotlin"
```kotlin title="fractional_knapsack.kt"
[class]{Item}-[func]{}
/* 品物 */
class Item(
val w: Int, // 品物
val v: Int // 品物の価値
)
[class]{}-[func]{fractionalKnapsack}
/* 分数ナップサック:貪欲法 */
fun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {
// 重さと価値の 2 属性を持つ品物リストを作成
var cap = c
val items = arrayOfNulls<Item>(wgt.size)
for (i in wgt.indices) {
items[i] = Item(wgt[i], _val[i])
}
// 単位価値 item.v / item.w の高い順にソートする
items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }
// 貪欲選択を繰り返す
var res = 0.0
for (item in items) {
if (item!!.w <= cap) {
// 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v
cap -= item.w
} else {
// 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += item.v.toDouble() / item.w * cap
// 残り容量がないため、ループを抜ける
break
}
}
return res
}
```
=== "Ruby"
```ruby title="fractional_knapsack.rb"
[class]{Item}-[func]{}
### アイテム ###
class Item
attr_accessor :w # 品物の重さ
attr_accessor :v # 品物の価値
[class]{}-[func]{fractional_knapsack}
def initialize(w, v)
@w = w
@v = v
end
end
### 分数ナップサック:貪欲法 ###
def fractional_knapsack(wgt, val, cap)
# 重さと価値の 2 属性を持つ品物リストを作成する
items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }
# 単位価値 item.v / item.w の高い順にソートする
items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }
# 貪欲選択を繰り返す
res = 0
for item in items
if item.w <= cap
# 残り容量が十分なら、現在の品物を丸ごとナップサックに入れる
res += item.v
cap -= item.w
else
# 残り容量が足りない場合は、現在の品物の一部だけをナップサックに入れる
res += (item.v.to_f / item.w) * cap
# 残り容量がないため、ループを抜ける
break
end
end
res
end
```
ソート以外に、最悪の場合、アイテムのリスト全体を走査する必要があるため、**時間計算量は $O(n)$** です。ここで $n$ はアイテムの数です。
??? pythontutor "コードの可視化"
`Item` オブジェクトリストが初期化されるため、**空間計算量は $O(n)$** です。
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20Item%3A%0A%20%20%20%20%22%22%22%E5%93%81%E7%89%A9%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20w%3A%20int%2C%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%E5%93%81%E7%89%A9%E3%81%AE%E9%87%8D%E3%81%95%0A%20%20%20%20%20%20%20%20self.v%20%3D%20v%20%20%23%20%E5%93%81%E7%89%A9%E3%81%AE%E4%BE%A1%E5%80%A4%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D%2C%20val%3A%20list%5Bint%5D%2C%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B0%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20%E9%87%8D%E3%81%95%E3%81%A8%E4%BE%A1%E5%80%A4%E3%81%AE%202%20%E5%B1%9E%E6%80%A7%E3%82%92%E6%8C%81%E3%81%A4%E5%93%81%E7%89%A9%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%0A%20%20%20%20items%20%3D%20%5BItem%28w%2C%20v%29%20for%20w%2C%20v%20in%20zip%28wgt%2C%20val%29%5D%0A%20%20%20%20%23%20%E5%8D%98%E4%BD%8D%E4%BE%A1%E5%80%A4%20item.v%20%2F%20item.w%20%E3%81%AE%E9%AB%98%E3%81%84%E9%A0%86%E3%81%AB%E3%82%BD%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20%2F%20item.w%2C%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20for%20item%20in%20items%3A%0A%20%20%20%20%20%20%20%20if%20item.w%20%3C%3D%20cap%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E5%8D%81%E5%88%86%E3%81%AA%E3%82%89%E3%80%81%E7%8F%BE%E5%9C%A8%E3%81%AE%E5%93%81%E7%89%A9%E3%82%92%E4%B8%B8%E3%81%94%E3%81%A8%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20item.v%0A%20%20%20%20%20%20%20%20%20%20%20%20cap%20-%3D%20item.w%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E8%B6%B3%E3%82%8A%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E7%8F%BE%E5%9C%A8%E3%81%AE%E5%93%81%E7%89%A9%E3%81%AE%E4%B8%80%E9%83%A8%E3%81%A0%E3%81%91%E3%82%92%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20%28item.v%20%2F%20item.w%29%20%2A%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%80%81%E3%83%AB%E3%83%BC%E3%83%97%E3%82%92%E6%8A%9C%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20return%20res%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10%2C%2020%2C%2030%2C%2040%2C%2050%5D%0A%20%20%20%20val%20%3D%20%5B50%2C%20120%2C%20150%2C%20210%2C%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt%2C%20val%2C%20cap%29%0A%20%20%20%20print%28f%22%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E5%AE%B9%E9%87%8F%E3%82%92%E8%B6%85%E3%81%88%E3%81%AA%E3%81%84%E6%9C%80%E5%A4%A7%E4%BE%A1%E5%80%A4%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=8&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20Item%3A%0A%20%20%20%20%22%22%22%E5%93%81%E7%89%A9%22%22%22%0A%20%20%20%20def%20__init__%28self%2C%20w%3A%20int%2C%20v%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.w%20%3D%20w%20%20%23%20%E5%93%81%E7%89%A9%E3%81%AE%E9%87%8D%E3%81%95%0A%20%20%20%20%20%20%20%20self.v%20%3D%20v%20%20%23%20%E5%93%81%E7%89%A9%E3%81%AE%E4%BE%A1%E5%80%A4%0A%0Adef%20fractional_knapsack%28wgt%3A%20list%5Bint%5D%2C%20val%3A%20list%5Bint%5D%2C%20cap%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E5%88%86%E6%95%B0%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20%E9%87%8D%E3%81%95%E3%81%A8%E4%BE%A1%E5%80%A4%E3%81%AE%202%20%E5%B1%9E%E6%80%A7%E3%82%92%E6%8C%81%E3%81%A4%E5%93%81%E7%89%A9%E3%83%AA%E3%82%B9%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90%0A%20%20%20%20items%20%3D%20%5BItem%28w%2C%20v%29%20for%20w%2C%20v%20in%20zip%28wgt%2C%20val%29%5D%0A%20%20%20%20%23%20%E5%8D%98%E4%BD%8D%E4%BE%A1%E5%80%A4%20item.v%20%2F%20item.w%20%E3%81%AE%E9%AB%98%E3%81%84%E9%A0%86%E3%81%AB%E3%82%BD%E3%83%BC%E3%83%88%E3%81%99%E3%82%8B%0A%20%20%20%20items.sort%28key%3Dlambda%20item%3A%20item.v%20%2F%20item.w%2C%20reverse%3DTrue%29%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20for%20item%20in%20items%3A%0A%20%20%20%20%20%20%20%20if%20item.w%20%3C%3D%20cap%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E5%8D%81%E5%88%86%E3%81%AA%E3%82%89%E3%80%81%E7%8F%BE%E5%9C%A8%E3%81%AE%E5%93%81%E7%89%A9%E3%82%92%E4%B8%B8%E3%81%94%E3%81%A8%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20item.v%0A%20%20%20%20%20%20%20%20%20%20%20%20cap%20-%3D%20item.w%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E8%B6%B3%E3%82%8A%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E7%8F%BE%E5%9C%A8%E3%81%AE%E5%93%81%E7%89%A9%E3%81%AE%E4%B8%80%E9%83%A8%E3%81%A0%E3%81%91%E3%82%92%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E3%81%AB%E5%85%A5%E3%82%8C%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20res%20%2B%3D%20%28item.v%20%2F%20item.w%29%20%2A%20cap%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E3%82%8A%E5%AE%B9%E9%87%8F%E3%81%8C%E3%81%AA%E3%81%84%E3%81%9F%E3%82%81%E3%80%81%E3%83%AB%E3%83%BC%E3%83%97%E3%82%92%E6%8A%9C%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20%20%20%20%20break%0A%20%20%20%20return%20res%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20wgt%20%3D%20%5B10%2C%2020%2C%2030%2C%2040%2C%2050%5D%0A%20%20%20%20val%20%3D%20%5B50%2C%20120%2C%20150%2C%20210%2C%20240%5D%0A%20%20%20%20cap%20%3D%2050%0A%20%20%20%20n%20%3D%20len%28wgt%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20fractional_knapsack%28wgt%2C%20val%2C%20cap%29%0A%20%20%20%20print%28f%22%E3%83%8A%E3%83%83%E3%83%97%E3%82%B5%E3%83%83%E3%82%AF%E5%AE%B9%E9%87%8F%E3%82%92%E8%B6%85%E3%81%88%E3%81%AA%E3%81%84%E6%9C%80%E5%A4%A7%E4%BE%A1%E5%80%A4%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=8&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
組み込みのソートアルゴリズムの時間計算量は通常 $O(\log n)$、空間計算量は通常 $O(\log n)$ または $O(n)$ であり、具体的な値はプログラミング言語の実装に依存する。
ソートを除けば、最悪の場合は品物リスト全体を走査する必要があるため、**時間計算量は $O(n)$** であり、ここで $n$ は品物数である。
`Item` オブジェクトのリストを初期化しているため、**空間計算量は $O(n)$** である。
### 3. &nbsp; 正しさの証明
背理法を使用します。アイテム $x$ が最高の単位値を持ち、あるアルゴリズムが最大値 `res` を生成するが、解にアイテム $x$ が含まれていないと仮定します。
背理法を用いる。品物 $x$ が単位価値最大の品物であり、あるアルゴリズムで得られた最大価値を `res` するが、その解には品物 $x$ が含まれていないと仮定す
今、ナップサックから任意のアイテムの単位重量を取り除き、アイテム $x$ の単位重量で置き換えます。アイテム $x$ の単位値が最であるため、置き換え後の総値は確実に `res` より大きくなります。**これは `res` が最適解であるという仮定と矛盾し、最適解には必ずアイテム $x$ が含まれることを証明します**。
ここでナップサックから単位重量の任意の品物を取り出し、単位重量の品物 $x$ に置き換える。品物 $x$ の単位値が最であるため、置き換え後の総値は必ず `res` より大きくな。**これは `res` が最適解であることに矛盾し、最適解には必ず品物 $x$ が含まれなければならないことを示す**。
この解の他のアイテムについても、上記の矛盾を構できます。全体的に、**単位値がより大きいアイテムは常により良い選択**であり、貪欲戦略が効果的であることを証明します
この解に含まれる他の品物についても、同様の矛盾を構できる。要するに、**単位値がより大きい品物は常により良い選択である**。これは貪欲戦略が効であることを示している
の図に示すように、アイテムの重量と単位値をそれぞれ二次元チャートの横軸と縦軸となすと、分数ナップサック問題は「限られた横軸範囲内で囲まれる最大面積を求める」ことに変換できます。この類は、幾何学的観点から貪欲戦略の効果を理解するのに役立ちます
下に示すように、品物の重さと品物の単位値をそれぞれ二次元グラフの横軸と縦軸となすと、分数ナップサック問題は「有限な横軸区間で囲まれる最大面積を求める問題」に変換でき。この類は、幾何学的観点から貪欲戦略の有効性を理解する助けになる
![分数ナップサック問題の幾何学的表現](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png){ class="animation-figure" }
+263 -79
View File
@@ -2,68 +2,68 @@
comments: true
---
# 15.1 &nbsp; 貪欲アルゴリズム
# 15.1 &nbsp; 貪欲
<u>貪欲アルゴリズム</u>は最適化問題を解決するための一般的なアルゴリズムで、基本的に問題の各意思決定段階で最も良い選択をすること、つまり局所的に最適な決定を貪欲に行い、グローバルに最適解を見つけることを望みます。貪欲アルゴリズムは簡潔で効率的であり、多くの実用的な問題で広く使用されています。
<u>貪欲法(greedy algorithm</u>は最適化問題を解ための一般的なアルゴリズムです。その基本的な考え方は、問題の各意思決定段階において、その時点で最善に見える選択を行い、すなわち貪欲に局所最適な決定を下すことで、大域最適解を得ようとするものです。貪欲法は簡潔で効率的であり、多くの実際の問題で広く用いられています。
貪欲アルゴリズムと動的プログラミングは、どちらも最適化問題を解決するためによく使用されます。両者最適部分構造の性質に依存するなど、いくつかの類似点を共有していますが、動作方法が異なります。
貪欲法と動的計画法は、どちらも最適化問題を解く際によく用いられます。両者には、最適部分構造に依存するなどの共通点がありますが、その動作原理は異なります。
- 動的プログラミングは現在の決定段階ですべての以前の決定を考慮し、過去の部分問題の解を使用して現在の部分問題の解を構築します。
- 貪欲アルゴリズムは過去の決定を考慮せず、代わりに貪欲な選択を続け、問題が解決されるまで問題の範囲を継続的に狭めます。
- 動的計画法は、前の段階までのすべての決定に基づいて現在の決定を考、過去の部分問題の解を用いて現在の部分問題の解を構築します。
- 貪欲は過去の決定を考慮せず、ひたすら前に進みながら貪欲な選択を行い、問題の範囲を縮小し続けて、最終的に問題を解決します。
まず、「完全ナップサック問題」の章で紹介された「コイン交換」の例を通じて貪欲アルゴリズムの動作原理を理解しましょう。すでによく知っていると思います。
まずは例題「コイン両替」を通して、貪欲法の仕組みを理解しましょう。この問題はすでに「完全ナップサック問題」の節で紹介しているので、見覚えがあるはずです。
!!! question
$n$ 種類のコインが与えられ、$i$ 番目の種類のコインの額面は $coins[i - 1]$ 、目標金額は $amt$ です。各種類のコインは無制限に利用可能で、目標金額を構成するのに必要な最小コイン数は何ですか?目標金額を構成できない場合は $-1$ を返してください
$n$ 種類の硬貨が与えられ、$i$ 番目の硬貨の額面は $coins[i - 1]$ 、目標金額は $amt$ です。各硬貨は何度でも選べるとき、目標金額を作るために必要な最小の硬貨枚数を求めてください。目標金額を作れない場合は $-1$ を返します
この問題で採用される貪欲戦略を下の図に示します。目標金額が与えられたとき、**それに最も近く、それを超えないコインを貪欲に選択**し、目標金額が満たされるまでこのステップを繰り返します。
この問題で採用る貪欲戦略は下図のとおりです。目標金額が与えられた、**それを超えず、かつ最も近い硬貨を貪欲に選択**、この手順を目標金額を作り切るまで繰り返します。
![コイン交換の貪欲戦略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" }
![コイン両替の貪欲戦略](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" }
<p align="center"> 図 15-1 &nbsp; コイン交換の貪欲戦略 </p>
<p align="center"> 図 15-1 &nbsp; コイン両替の貪欲戦略 </p>
実装コードは以下の通りです
実装コードは次のとおりです
=== "Python"
```python title="coin_change_greedy.py"
def coin_change_greedy(coins: list[int], amt: int) -> int:
"""硬貨交換:貪欲法"""
# coins リストソートされていると仮定
"""コイン交換:貪欲法"""
# coins リストソート済みと仮定する
i = len(coins) - 1
count = 0
# 残り金額がなくなるまで貪欲選択をループ
# 残額がなくなるまで貪欲選択を繰り返す
while amt > 0:
# 残り金額に最も近く、それより小さい硬貨を見つける
# 残額以下で最も近い硬貨を見つける
while i > 0 and coins[i] > amt:
i -= 1
# coins[i] を選択
# coins[i] を選択する
amt -= coins[i]
count += 1
# 実行可能な解が見つからない場合、-1 を返す
# 実行可能な解が見つからなければ -1 を返す
return count if amt == 0 else -1
```
=== "C++"
```cpp title="coin_change_greedy.cpp"
/* 硬貨両替:貪欲法 */
/* コイン交換:貪欲法 */
int coinChangeGreedy(vector<int> &coins, int amt) {
// 硬貨リストが順序付けされていると仮定
// coins リストはソート済みと仮定する
int i = coins.size() - 1;
int count = 0;
// 残り金額がなくなるまで貪欲選択をループ
// 残額がなくなるまで貪欲選択を繰り返す
while (amt > 0) {
// 残り金額に近く、それ以下の最小硬貨を見つける
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択
// coins[i] を選択する
amt -= coins[i];
count++;
}
// 実行可能な解が見つからない場合、-1 を返す
// 実行可能な解が見つからなければ -1 を返す
return amt == 0 ? count : -1;
}
```
@@ -71,22 +71,22 @@ comments: true
=== "Java"
```java title="coin_change_greedy.java"
/* 硬貨両替:貪欲法 */
/* コイン交換:貪欲法 */
int coinChangeGreedy(int[] coins, int amt) {
// 硬貨リストが順序付けされていると仮定
// coins リストはソート済みと仮定する
int i = coins.length - 1;
int count = 0;
// 残り金額がなくなるまで貪欲選択をループ
// 残額がなくなるまで貪欲選択を繰り返す
while (amt > 0) {
// 残り金額に近く、それ以下の最小硬貨を見つける
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択
// coins[i] を選択する
amt -= coins[i];
count++;
}
// 実行可能な解が見つからない場合、-1 を返す
// 実行可能な解が見つからなければ -1 を返す
return amt == 0 ? count : -1;
}
```
@@ -94,131 +94,315 @@ comments: true
=== "C#"
```csharp title="coin_change_greedy.cs"
[class]{coin_change_greedy}-[func]{CoinChangeGreedy}
/* コイン交換:貪欲法 */
int CoinChangeGreedy(int[] coins, int amt) {
// coins リストはソート済みと仮定する
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;
}
```
=== "Go"
```go title="coin_change_greedy.go"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
func coinChangeGreedy(coins []int, amt int) int {
// coins リストはソート済みと仮定する
i := len(coins) - 1
count := 0
// 残額がなくなるまで貪欲選択を繰り返す
for amt > 0 {
// 残額以下で最も近い硬貨を見つける
for i > 0 && coins[i] > amt {
i--
}
// coins[i] を選択する
amt -= coins[i]
count++
}
// 実行可能な解が見つからなければ -1 を返す
if amt != 0 {
return -1
}
return count
}
```
=== "Swift"
```swift title="coin_change_greedy.swift"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
func coinChangeGreedy(coins: [Int], amt: Int) -> Int {
// coins リストはソート済みと仮定する
var i = coins.count - 1
var count = 0
var amt = amt
// 残額がなくなるまで貪欲選択を繰り返す
while amt > 0 {
// 残額以下で最も近い硬貨を見つける
while i > 0 && coins[i] > amt {
i -= 1
}
// coins[i] を選択する
amt -= coins[i]
count += 1
}
// 実行可能な解が見つからなければ -1 を返す
return amt == 0 ? count : -1
}
```
=== "JS"
```javascript title="coin_change_greedy.js"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
function coinChangeGreedy(coins, amt) {
// coins 配列はソート済みと仮定する
let i = coins.length - 1;
let count = 0;
// 残額がなくなるまで貪欲選択を繰り返す
while (amt > 0) {
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択する
amt -= coins[i];
count++;
}
// 実行可能な解が見つからなければ -1 を返す
return amt === 0 ? count : -1;
}
```
=== "TS"
```typescript title="coin_change_greedy.ts"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
function coinChangeGreedy(coins: number[], amt: number): number {
// coins 配列はソート済みと仮定する
let i = coins.length - 1;
let count = 0;
// 残額がなくなるまで貪欲選択を繰り返す
while (amt > 0) {
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > amt) {
i--;
}
// coins[i] を選択する
amt -= coins[i];
count++;
}
// 実行可能な解が見つからなければ -1 を返す
return amt === 0 ? count : -1;
}
```
=== "Dart"
```dart title="coin_change_greedy.dart"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
int coinChangeGreedy(List<int> coins, int amt) {
// coins リストはソート済みと仮定する
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;
}
```
=== "Rust"
```rust title="coin_change_greedy.rs"
[class]{}-[func]{coin_change_greedy}
/* コイン交換:貪欲法 */
fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {
// coins リストはソート済みと仮定する
let mut i = coins.len() - 1;
let mut count = 0;
// 残額がなくなるまで貪欲選択を繰り返す
while amt > 0 {
// 残額以下で最も近い硬貨を見つける
while i > 0 && coins[i] > amt {
i -= 1;
}
// coins[i] を選択する
amt -= coins[i];
count += 1;
}
// 実行可能な解が見つからなければ -1 を返す
if amt == 0 {
count
} else {
-1
}
}
```
=== "C"
```c title="coin_change_greedy.c"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
int coinChangeGreedy(int *coins, int size, int amt) {
// coins リストはソート済みと仮定する
int i = 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;
}
```
=== "Kotlin"
```kotlin title="coin_change_greedy.kt"
[class]{}-[func]{coinChangeGreedy}
/* コイン交換:貪欲法 */
fun coinChangeGreedy(coins: IntArray, amt: Int): Int {
// coins リストはソート済みと仮定する
var am = amt
var i = coins.size - 1
var count = 0
// 残額がなくなるまで貪欲選択を繰り返す
while (am > 0) {
// 残額以下で最も近い硬貨を見つける
while (i > 0 && coins[i] > am) {
i--
}
// coins[i] を選択する
am -= coins[i]
count++
}
// 実行可能な解が見つからなければ -1 を返す
return if (am == 0) count else -1
}
```
=== "Ruby"
```ruby title="coin_change_greedy.rb"
[class]{}-[func]{coin_change_greedy}
### コイン両替:貪欲法 ###
def coin_change_greedy(coins, amt)
# coins リストはソート済みと仮定する
i = coins.length - 1
count = 0
# 残額がなくなるまで貪欲選択を繰り返す
while amt > 0
# 残額以下で最も近い硬貨を見つける
while i > 0 && coins[i] > amt
i -= 1
end
# coins[i] を選択する
amt -= coins[i]
count += 1
end
# 実行可能な解が見つからなければ `-1` を返す
amt == 0 ? count : -1
end
```
感嘆するかもしれません:なんて簡潔なんだ!貪欲アルゴリズムは約10行のコードでコイン交換問題を解決します。
??? pythontutor "コードの可視化"
## 15.1.1 &nbsp; 貪欲アルゴリズムの利点と制限
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20coin_change_greedy%28coins%3A%20list%5Bint%5D%2C%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E3%82%B3%E3%82%A4%E3%83%B3%E4%BA%A4%E6%8F%9B%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20coins%20%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AF%E3%82%BD%E3%83%BC%E3%83%88%E6%B8%88%E3%81%BF%E3%81%A8%E4%BB%AE%E5%AE%9A%E3%81%99%E3%82%8B%0A%20%20%20%20i%20%3D%20len%28coins%29%20-%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E6%AE%8B%E9%A1%8D%E3%81%8C%E3%81%AA%E3%81%8F%E3%81%AA%E3%82%8B%E3%81%BE%E3%81%A7%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E9%A1%8D%E4%BB%A5%E4%B8%8B%E3%81%A7%E6%9C%80%E3%82%82%E8%BF%91%E3%81%84%E7%A1%AC%E8%B2%A8%E3%82%92%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20while%20i%20%3E%200%20and%20coins%5Bi%5D%20%3E%20amt%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20-%3D%201%0A%20%20%20%20%20%20%20%20%23%20coins%5Bi%5D%20%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20amt%20-%3D%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%23%20%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BD%E3%81%AA%E8%A7%A3%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%EF%BC%9A%E5%A4%A7%E5%9F%9F%E6%9C%80%E9%81%A9%E8%A7%A3%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%A7%E3%81%8D%E3%82%8B%0A%20%20%20%20coins%20%3D%20%5B1%2C%205%2C%2010%2C%2020%2C%2050%2C%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%7Bamt%7D%20%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E3%81%AE%E7%A1%AC%E8%B2%A8%E6%9E%9A%E6%95%B0%E3%81%AF%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%EF%BC%9A%E5%A4%A7%E5%9F%9F%E6%9C%80%E9%81%A9%E8%A7%A3%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%0A%20%20%20%20coins%20%3D%20%5B1%2C%2020%2C%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%7Bamt%7D%20%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E3%81%AE%E7%A1%AC%E8%B2%A8%E6%9E%9A%E6%95%B0%E3%81%AF%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AE%9F%E9%9A%9B%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E6%9E%9A%E6%95%B0%E3%81%AF%203%20%EF%BC%8C%E3%81%A4%E3%81%BE%E3%82%8A%2020%20%2B%2020%20%2B%2020%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20coin_change_greedy%28coins%3A%20list%5Bint%5D%2C%20amt%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E3%82%B3%E3%82%A4%E3%83%B3%E4%BA%A4%E6%8F%9B%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20coins%20%E3%83%AA%E3%82%B9%E3%83%88%E3%81%AF%E3%82%BD%E3%83%BC%E3%83%88%E6%B8%88%E3%81%BF%E3%81%A8%E4%BB%AE%E5%AE%9A%E3%81%99%E3%82%8B%0A%20%20%20%20i%20%3D%20len%28coins%29%20-%201%0A%20%20%20%20count%20%3D%200%0A%20%20%20%20%23%20%E6%AE%8B%E9%A1%8D%E3%81%8C%E3%81%AA%E3%81%8F%E3%81%AA%E3%82%8B%E3%81%BE%E3%81%A7%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%AE%8B%E9%A1%8D%E4%BB%A5%E4%B8%8B%E3%81%A7%E6%9C%80%E3%82%82%E8%BF%91%E3%81%84%E7%A1%AC%E8%B2%A8%E3%82%92%E8%A6%8B%E3%81%A4%E3%81%91%E3%82%8B%0A%20%20%20%20%20%20%20%20while%20i%20%3E%200%20and%20coins%5Bi%5D%20%3E%20amt%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20-%3D%201%0A%20%20%20%20%20%20%20%20%23%20coins%5Bi%5D%20%E3%82%92%E9%81%B8%E6%8A%9E%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20amt%20-%3D%20coins%5Bi%5D%0A%20%20%20%20%20%20%20%20count%20%2B%3D%201%0A%20%20%20%20%23%20%E5%AE%9F%E8%A1%8C%E5%8F%AF%E8%83%BD%E3%81%AA%E8%A7%A3%E3%81%8C%E8%A6%8B%E3%81%A4%E3%81%8B%E3%82%89%E3%81%AA%E3%81%91%E3%82%8C%E3%81%B0%20-1%20%E3%82%92%E8%BF%94%E3%81%99%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%EF%BC%9A%E5%A4%A7%E5%9F%9F%E6%9C%80%E9%81%A9%E8%A7%A3%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%A7%E3%81%8D%E3%82%8B%0A%20%20%20%20coins%20%3D%20%5B1%2C%205%2C%2010%2C%2020%2C%2050%2C%20100%5D%0A%20%20%20%20amt%20%3D%20186%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%7Bamt%7D%20%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E3%81%AE%E7%A1%AC%E8%B2%A8%E6%9E%9A%E6%95%B0%E3%81%AF%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%EF%BC%9A%E5%A4%A7%E5%9F%9F%E6%9C%80%E9%81%A9%E8%A7%A3%E3%82%92%E4%BF%9D%E8%A8%BC%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84%0A%20%20%20%20coins%20%3D%20%5B1%2C%2020%2C%2050%5D%0A%20%20%20%20amt%20%3D%2060%0A%20%20%20%20res%20%3D%20coin_change_greedy%28coins%2C%20amt%29%0A%20%20%20%20print%28f%22%5Cncoins%20%3D%20%7Bcoins%7D%2C%20amt%20%3D%20%7Bamt%7D%22%29%0A%20%20%20%20print%28f%22%7Bamt%7D%20%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%AE%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E3%81%AE%E7%A1%AC%E8%B2%A8%E6%9E%9A%E6%95%B0%E3%81%AF%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%E5%AE%9F%E9%9A%9B%E3%81%AB%E5%BF%85%E8%A6%81%E3%81%AA%E6%9C%80%E5%B0%8F%E6%9E%9A%E6%95%B0%E3%81%AF%203%20%EF%BC%8C%E3%81%A4%E3%81%BE%E3%82%8A%2020%20%2B%2020%20%2B%2020%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
**貪欲アルゴリズムは直接的で実装が簡単であるだけでなく、通常非常に効率的でもあります**。上記のコードで、最小のコイン額面を $\min(coins)$ とすると、貪欲な選択ループは最大 $amt / \min(coins)$ 回実行され、時間計算量は $O(amt / \min(coins))$ になります。これは動的プログラミング解法の時間計算量 $O(n \times amt)$ よりも一桁小さいです。
思わずこう言いたくなるかもしれません。So clean!貪欲法はわずか十行ほどのコードでコイン両替問題を解いてしまいます。
しかし、**一部のコイン額面の組み合わせでは、貪欲アルゴリズムは最適解を見つけることができません**。下の図は2つの例を示しています。
## 15.1.1 &nbsp; 貪欲法の利点と限界
- **正の例 $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枚のコインのみが必要です。
**貪欲法は操作が直接的で実装が簡単なだけでなく、通常は効率も高い**です。上のコードでは、硬貨の最小額面を $\min(coins)$ とすると、貪欲選択のループ回数は高々 $amt / \min(coins)$ 回であり、時間計算量は $O(amt / \min(coins))$ です。これは動的計画法による解法の時間計算量 $O(n \times amt)$ より 1 桁小さいオーダーです。
![貪欲アルゴリズムが最適解を見つけられない例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" }
しかし、**硬貨の額面の組み合わせによっては、貪欲法では最適解を見つけられません**。下図に 2 つの例を示します。
<p align="center"> 図 15-2 &nbsp; 貪欲アルゴリズムが最適解を見つけられない例 </p>
- **正例 $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$ 枚です。
これは、コイン交換問題において、貪欲アルゴリズムがグローバルに最適解を見つけることを保証できず、非常に悪い解を見つける可能性があることを意味します。動的プログラミングの方が適しています。
![貪欲法では最適解を見つけられない例](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" }
一般的に、貪欲アルゴリズムの適用性は2つのカテゴリに分類されます。
<p align="center"> 図 15-2 &nbsp; 貪欲法では最適解を見つけられない例 </p>
1. **最適解を見つけることが保証される**:これらの場合、貪欲アルゴリズムはしばしば最良の選択で、バックトラッキングや動的プログラミングよりも効率的である傾向があります。
2. **準最適解を見つけることができる**:貪欲アルゴリズムはここでも適用可能です。多くの複雑な問題では、グローバル最適解を見つけることは非常に困難であり、高効率の準最適解を見つけることも非常に価値があります。
つまり、コイン両替問題に対して、貪欲法は大域最適解を保証できず、非常に悪い解を見つけてしまうこともあります。この問題は動的計画法で解くほうが適しています。
## 15.1.2 &nbsp; 貪欲アルゴリズムの特徴
一般に、貪欲法が適用できる状況は次の 2 つに分けられます。
それでは、どのような問題が貪欲アルゴリズムで解決するのに適しているのでしょうか?言い換えれば、どのような条件下で貪欲アルゴリズムは最適解を見つけることを保証できるのでしょうか?
1. **最適解を保証できる場合**:この場合、貪欲法はしばしば最良の選択です。多くの場合、バックトラッキングや動的計画法より効率的だからです。
2. **近似最適解を見つけられる場合**:この場合も貪欲法は有効です。多くの複雑な問題では、大域最適解を求めること自体が非常に難しく、より高い効率で準最適解を得られるだけでも十分価値があります。
動的プログラミングと比較して、貪欲アルゴリズムはより厳しい使用条件を持ち、主に問題の2つの性質に焦点を当てています。
## 15.1.2 &nbsp; 貪欲法の特性
- **貪欲選択性**:局所的に最適な選択が常にグローバルに最適な解に導くことができる場合のみ、貪欲アルゴリズムは最適解を得ることを保証できます
- **最適部分構造**:元の問題の最適解はその部分問題の最適解を含みます。
では、どのような問題が貪欲法に適しているのでしょうか。言い換えると、貪欲法はどのような場合に最適解を保証できるのでしょうか
最適部分構造は「動的プログラミング」の章ですでに紹介されているため、ここではこれ以上議論しません。一部の問題には明らかな最適部分構造がありませんが、それでも貪欲アルゴリズムを使用して解決できることに注意することが重要です。
動的計画法と比べると、貪欲法の適用条件はより厳しく、主に次の 2 つの性質に注目します。
主に貪欲選択性を決定する方法を探索します。その記述は単純に見えますが、**実際には、多くの問題の貪欲選択性を証明することは容易ではありません**
- **貪欲選択性**:局所最適な選択が常に大域最適解につながる場合にのみ、貪欲法は最適解を保証できます
- **最適部分構造**:元の問題の最適解が、部分問題の最適解を含むことです。
例えば、コイン交換問題では、貪欲選択性を反証するために反例を簡単に挙げることができますが、それを証明することははるかに困難です。**コインの組み合わせが貪欲アルゴリズムを使用して解決できるためには、どのような条件を満たす必要があるか**と尋ねられた場合、厳密な数学的証明を提供することが困難であるため、しばしば直感や例に頼って曖昧な答えを提供しなければなりません
最適部分構造については「動的計画法」の節ですでに紹介したので、ここでは繰り返しません。なお、問題によっては最適部分構造が明確でなくても、貪欲法で解ける場合があります
ここでは主に、貪欲選択性をどのように判定するかを考えます。説明だけを見ると単純そうですが、**実際には多くの問題で、貪欲選択性を証明するのは容易ではありません**。
たとえばコイン両替問題では、反例を挙げて貪欲選択性が成り立たないことを示すのは簡単ですが、成り立つことを証明するのは難しいです。もし、**どのような条件を満たす硬貨の組み合わせなら貪欲法で解けるのか**と問われると、直感や例示に頼った曖昧な答えしか出せず、厳密な数学的証明を与えるのは困難です。
!!! quote
ある論文では、コインの組み合わせ任意の金額に対して貪欲アルゴリズムを使用して最適解を見つけることができるかどうかを判定するための時間計算量 $O(n^3)$ のアルゴリズムが示されています。
ある論文では、ある硬貨の組み合わせについて、任意の金額に対する最適解を貪欲法で求められるかどうかを判定する時間計算量 $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 &nbsp; 貪欲アルゴリズムによる問題解決のステップ
## 15.1.3 &nbsp; 貪欲法の問題解決手順
貪欲問題の問題解決プロセスは、一般的に以下の3つのステップに分けることができます。
貪欲法による問題解決の流れは、おおむね次の 3 段階に分けられます。
1. **問題分析**問題の特徴を整理し理解する。状態定義、最適化目標、制約などを含みます。このステップはバックトラッキングや動的プログラミングでも関与します。
2. **貪欲戦略の決定**:各ステップで貪欲選択をする方法を決定する。この戦略各ステップで問題規模を縮小し、最終的に問題全体を解決できます。
3. **正確性の証明**:通常問題が貪欲選択性と最適部分構造の両方を持つことを証明する必要があります。このステップには、帰納法や背理法などの数学的証明が必要な場合があります。
1. **問題分析**:状態定義、最適化目標、制約条件などを整理し、問題の性質を理解します。この段階はバックトラッキングや動的計画法でも共通して現れます。
2. **貪欲戦略の決定**:各ステップでどのように貪欲選択を行うかを定めます。この戦略により各ステップで問題規模を縮小し、最終的に問題全体を解決ます。
3. **正しさの証明**:通常は、その問題が貪欲選択性と最適部分構造を持つことを示す必要があります。この段階では、帰納法や背理法などの数学的証明が必要になることがあります。
貪欲戦略の決定は問題解決の核心ステップですが、実装は容易ではない場合があります。主な理由は以下の通りです。
貪欲戦略を定めることは問題解決の核心ですが、実際には簡単ではないことも多く、主な理由は次のとおりです。
- **異なる問題間で貪欲戦略は大きく異なる**。多くの問題では貪欲戦略はかなり直接的で、一般的な思考と試行を通じて思いつくことができます。しかし、一部の複雑な問題では、貪欲戦略非常に見つけにくく、これは個人の問題解決経験アルゴリズム能力の真のテストです。
- **一部の貪欲戦略は非常に誤解を招く**。自信を持って貪欲戦略を設計し、コードを書いてテストに提出したとき、一部のテストケースが通らない可能性が高いです。これは設計された貪欲戦略が「部分的に正しい」だけであるためで、上記のコイン交換の例で説明した通りです。
- **問題ごとに貪欲戦略の差が大きい**。多くの問題では貪欲戦略は比較的わかりやすく、おおまかな考察や試行だけで見つけられます。しかし複雑な問題では、貪欲戦略非常に見えにくいことがあり、その場合は解法経験アルゴリズム力が大きく問われます。
- **一見もっともらしい貪欲戦略もある**。自信を持って貪欲戦略を設計し、コードを書いて提出しても、一部のテストケースを通過できないことがあります。これは、その貪欲戦略が「部分的にしか正しくない」ためであり、先ほどのコイン両替は典型例です。
確性を確保するために、貪欲戦略に対して厳密な数学的証明を提供すべきで、**通常は背理法や数学的帰納法を含みます**。
しさを保証するために、貪欲戦略に対して厳密な数学的証明を行うべきであり、**通常は背理法や数学的帰納法が必要になります**。
しかし、正確性を証明することは容易な作業ではない場合があります。途方に暮れた場合、通常はテストケースに基づいてコードをデバッグし、貪欲戦略を段階的に修正し検証することを選択します。
しかし、正しさの証明もまた簡単とは限りません。手がかりがない場合には、テストケースを使ってコードをデバッグしながら、貪欲戦略を少しずつ修正し検証していくことがよくあります。
## 15.1.4 &nbsp; 貪欲アルゴリズムで解決される典型的な問題
## 15.1.4 &nbsp; 貪欲法の典型問題
貪欲アルゴリズムは、貪欲選択と最適部分構造の性質を満たす最適化問題によく適用されます。以下典型的な貪欲アルゴリズム問題いくつかす。
貪欲は、貪欲選択と最適部分構造を満たす最適化問題によく用いられます。以下典型的な貪欲法の問題いくつか挙げます。
- **コイン交換問題**:一部のコインの組み合わせでは、貪欲アルゴリズムは常に最適解を提供します。
- **区間スケジューリング問題**:いくつかのタスクがあり、それぞれが一定期間にわたって行われるとします。目標はできるだけ多くのタスクを完了することです。常に最も早く終了するタスクを選択すると、貪欲アルゴリズムは最適解を達成できます。
- **分数ナップサック問題**アイテムのセットと運搬容量が与えられ、目標は総重量が運搬容量を超えず、総価値が最大化されるようなアイテムのセットを選択することです。常に最高の価値対重量比(価値/重量)のアイテムを選択すると、貪欲アルゴリズムは一部のケースで最適解を達成できます。
- **株式取引問題**:株価の履歴のセットが与えられ、複数回の取引を行うことができますが、すでに株式を所有している場合は売却後でないと再度購入できません。目標は最大利益を達成することです。
- **ハフマン符号化**:ハフマン符号化は無損失データ圧縮に使用される貪欲アルゴリズムです。ハフマン木を構築することにより、常に最低頻度の2つのノードを統合し、最小重み付きパス長(符号長)のハフマン木を生成します。
- **ダイクストラのアルゴリズム**これは与えられたソース頂点から他のすべての頂点への最短経路問題を解決するための貪欲アルゴリズムです。
- **硬貨のお釣り問題**:ある種の硬貨の組み合わせでは、貪欲法で常に最適解が得られます。
- **区間スケジューリング問題**:いくつかのタスクがあり、それぞれがある時間区間で実行されるとします。できるだけ多くのタスクを完了することが目標で、毎回終了時刻が最も早いタスクを選ぶなら、貪欲法で最適解を得られます。
- **分数ナップサック問題**一群の品物と積載容量が与えられたとき、総重量が容量を超えず、かつ総価値が最大になるように品物を選ぶ問題です。毎回、価値対重量比(価値 / 重量)が最も高い品物を選ぶなら、ある条件下で貪欲法は最適解を得られます。
- **株式売買問題**:株価の履歴が与えられ、複数回の売買が可能ですが、すでに株を保有している場合は売却前に再度購入することはできません。目標は最大利益をることです。
- **ハフマン符号化**:ハフマン符号化は、可逆データ圧縮に用いられる貪欲です。ハフマン木を構築する際、毎回出現頻度が最も低い 2 つのノードを選んで併合すると、最終的に得られるハフマン木の重み付きパス長(符号長)は最小になります。
- **Dijkstra アルゴリズム**:与えられた点から他の頂点への最短経路問題を解く貪欲法です。
+3 -3
View File
@@ -9,13 +9,13 @@ icon: material/head-heart-outline
!!! abstract
ひまわりは太陽の方を向き、常に自分にとって最大の成長を求めます
ヒマワリは太陽に向かって回り、自らが最も大きく成長できる可能性を常に追い求める
貪欲戦略は、一の単純な選択を通じて、段階的に最良の答えへと導きます
貪欲戦略は、一回ごとの単純な選択を通じて、徐々に最適な答えへと導
## 章の内容
- [15.1 &nbsp; 貪欲アルゴリズム](greedy_algorithm.md)
- [15.1 &nbsp; 貪欲](greedy_algorithm.md)
- [15.2 &nbsp; 分数ナップサック問題](fractional_knapsack_problem.md)
- [15.3 &nbsp; 最大容量問題](max_capacity_problem.md)
- [15.4 &nbsp; 最大積分割問題](max_product_cutting_problem.md)
+261 -59
View File
@@ -6,59 +6,59 @@ comments: true
!!! question
配列 $ht$ を入力します。各要素は垂直仕切りの高さを表します。配列内の任意の2つの仕切りと、それらの間のスペースによってコンテナを形成できます。
配列 $ht$ が与えられ、各要素は垂直仕切りの高さを表します。配列内の任意の 2 枚の仕切りと、その間の空間で容器を構成できます。
容器の容量は高さと幅の積(面積)に等しく、高さは短い方の仕切り板で決まり、幅は 2 枚の仕切り板の配列インデックスの差です。
配列から 2 枚の仕切り板を選び、構成される容器の容量が最大となるようにしてください。最大容量を返します。例を以下の図に示します。
コンテナの容量は高さと幅の積(面積)で、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。
![最大容量問題のサンプルデータ](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" }
コンテナの容量を最大化する2つの仕切りを配列から選択し、この最大容量を返してください。例を下の図に示します。
<p align="center"> 図 15-7 &nbsp; 最大容量問題のサンプルデータ </p>
![最大容量問題の例データ](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" }
容器は任意の 2 枚の仕切り板で囲まれるため、**本問の状態は 2 枚の仕切り板のインデックスで表され、$[i, j]$ と記します**。
<p align="center"> 図 15-7 &nbsp; 最大容量問題の例データ </p>
コンテナは任意の2つの仕切りによって形成されるため、**この問題の状態は2つの仕切りのインデックスで表現され、$[i, j]$ と表記されます**。
問題の記述によれば、容量は高さと幅の積に等しく、高さは短い方の仕切りによって決定され、幅は2つの仕切りの配列インデックスの差です。容量 $cap[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)$ になります。
配列の長さを $n$ とすると、2の仕切りの組せ数(状態総数)は $C_n^2 = \frac{n(n - 1)}{2}$ です。最も直接的には、**すべての状態を総当たりできます**。これにより最大容量を求められ、時間計算量は $O(n^2)$ す。
### 1. &nbsp; 貪欲戦略の決定
この問題にはより効率的な解法があります。下の図に示すように、インデックス $i < j$ かつ高さ $ht[i] < ht[j]$ の状態 $[i, j]$ を選択します。つまり、$i$ 短い仕切り、$j$ は高い仕切りです。
この問題にはさらに効率的な解法があります。下の図ように、状態 $[i, j]$ を 1 つ選び、インデックス $i < j$ かつ高さ $ht[i] < ht[j]$ を満たすとします。つまり、$i$ 短い、$j$ が長い板です。
![初期状態](max_capacity_problem.assets/max_capacity_initial_state.png){ class="animation-figure" }
<p align="center"> 図 15-8 &nbsp; 初期状態 </p>
下の図に示すように、**高い仕切り $j$ を短い仕切り $i$ に近づけて移動すると、容量は確実に減少します**。
下の図ように、**このとき長い板 $j$ を短い $i$ に近づけると、容量は必ず小さくなります**。
これは、高い仕切り $j$ を移動すると、幅 $j-i$ が確実に減少するためです。高さは短い仕切りによって決定されるため、高さは同じまま($i$ が短い仕切りのまま)か減少(移動した $j$ が短い仕切りになる)しかありません。
これは、長い板 $j$ を動かした後は幅 $j-i$ が必ず小さくなるためです。また、高さは短い板で決まるので、高さは変わらない( $i$ が依然として短い板)か、小さくなる(移動後の $j$ が短いになる)ことしかありません。
![高い仕切りを内側に移動した後の状態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" }
![長い板を内側へ動かした後の状態](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" }
<p align="center"> 図 15-9 &nbsp; 高い仕切りを内側に移動した後の状態 </p>
<p align="center"> 図 15-9 &nbsp; 長い板を内側へ動かした後の状態 </p>
逆に、**短い仕切り $i$ を内側に移動することによってのみ容量を増加させることが可能です**。幅は確実に減少しますが、**高さが増加する可能性があります**(移動した短い仕切り $i$ が高くなる場合)。例えば、下の図では、短い仕切りを移動した後に面積が増加しています。
逆に考えると、**短い $i$ を内側へ縮めた場合にのみ容量が大きくなる可能性があります**。幅は必ず小さくなりますが、**高さは大きくなる可能性がある**からです(移動後の短い板 $i$ がより長くなる可能性があります)。たとえば次の図では、短い板を動かした後に面積が大きくなっています。
![短い仕切りを内側に移動した後の状態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" }
![短い板を内側へ動かした後の状態](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" }
<p align="center"> 図 15-10 &nbsp; 短い仕切りを内側に移動した後の状態 </p>
<p align="center"> 図 15-10 &nbsp; 短い板を内側へ動かした後の状態 </p>
これにより、この問題の貪欲戦略が導かれます:コンテナの両端に2つのポインタを初期化し、各ラウンドで短い仕切りに対応するポインタを内側に移動し、2つのポインタが出会うまで続けます。
以上から、本問の貪欲戦略を導けます。2 本のポインタを初期化して容器の両端に置き、各ラウンドで短いに対応するポインタを内側へ縮め、2 本のポインタが出会うまで続けます。
下の図は貪欲戦略の実行を示しています。
下の図は貪欲戦略の実行過程を示しています。
1. 最初に、ポインタ $i$ と $j$ 配列の両端に配置されます。
1. 初期状態では、ポインタ $i$ と $j$ 配列の両端にあります。
2. 現在の状態の容量 $cap[i, j]$ を計算し、最大容量を更新します。
3. 仕切り $i$ と $j$ の高さを比較し、短い仕切りを1ステップ内側に移動します。
4. $i$ と $j$ が出会うまでステップ `2.``3.` を繰り返します。
3. $i$ と $j$ の高さを比較し、短い板を内側へ 1 マス移動します。
4. `2.``3.` を繰り返し実行し、$i$ と $j$ が出会ったら終了します。
=== "<1>"
![最大容量問題の貪欲プロセス](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" }
![最大容量問題の貪欲な過程](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" }
=== "<2>"
![max_capacity_greedy_step2](max_capacity_problem.assets/max_capacity_greedy_step2.png){ class="animation-figure" }
@@ -84,29 +84,29 @@ $$
=== "<9>"
![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png){ class="animation-figure" }
<p align="center"> 図 15-11 &nbsp; 最大容量問題の貪欲プロセス </p>
<p align="center"> 図 15-11 &nbsp; 最大容量問題の貪欲な過程 </p>
### 2. &nbsp; 実装
### 2. &nbsp; コード実装
コードは最大 $n$ 回ループするため、**時間計算量は $O(n)$** です。
コードのループ回数は最大でも $n$ 回であるため、**時間計算量は $O(n)$** です。
変数 $i$、$j$、$res$ は一定量の追加スペースを使用するため、**空間計算量は $O(1)$** です。
変数 $i$、$j$、$res$ が使う追加領域は定数サイズなので、**空間計算量は $O(1)$** です。
=== "Python"
```python title="max_capacity.py"
def max_capacity(ht: list[int]) -> int:
"""最大容量:貪欲法"""
# ij を初期化、配列の両端で分割させる
# i, j を初期化し、それぞれ配列の両端に置く
i, j = 0, len(ht) - 1
# 初期最大容量は 0
# 初期最大容量は 0
res = 0
# 2 の板が出会うまで貪欲選択をループ
# 2 の板が出会うまで貪欲選択を繰り返す
while i < j:
# 最大容量を更新
# 最大容量を更新する
cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
# 短いを内側に移動
# 短いを内側へ動かす
if ht[i] < ht[j]:
i += 1
else:
@@ -119,16 +119,16 @@ $$
```cpp title="max_capacity.cpp"
/* 最大容量:貪欲法 */
int maxCapacity(vector<int> &ht) {
// ij を初期化し、配列の両端で分割させる
// i, j を初期化し、それぞれ配列の両端に置く
int i = 0, j = ht.size() - 1;
// 初期最大容量は 0
// 初期最大容量は 0
int res = 0;
// 2の板が出会うまで貪欲選択をループ
// 2の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新
// 最大容量を更新する
int cap = min(ht[i], ht[j]) * (j - i);
res = max(res, cap);
// より短いを内側に移動
// 短いを内側へ動かす
if (ht[i] < ht[j]) {
i++;
} else {
@@ -144,16 +144,16 @@ $$
```java title="max_capacity.java"
/* 最大容量:貪欲法 */
int maxCapacity(int[] ht) {
// ij を初期化し、配列の両端で分割させる
// i, j を初期化し、それぞれ配列の両端に置く
int i = 0, j = ht.length - 1;
// 初期最大容量は 0
// 初期最大容量は 0
int res = 0;
// 2の板が出会うまで貪欲選択をループ
// 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 {
@@ -167,77 +167,279 @@ $$
=== "C#"
```csharp title="max_capacity.cs"
[class]{max_capacity}-[func]{MaxCapacity}
/* 最大容量:貪欲法 */
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;
}
```
=== "Go"
```go title="max_capacity.go"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
func maxCapacity(ht []int) int {
// i, j を初期化し、それぞれ配列の両端に置く
i, j := 0, len(ht)-1
// 初期の最大容量は 0
res := 0
// 2 枚の板が出会うまで貪欲選択を繰り返す
for i < j {
// 最大容量を更新する
capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)
res = int(math.Max(float64(res), float64(capacity)))
// 短い方を内側へ動かす
if ht[i] < ht[j] {
i++
} else {
j--
}
}
return res
}
```
=== "Swift"
```swift title="max_capacity.swift"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
func maxCapacity(ht: [Int]) -> Int {
// i, j を初期化し、それぞれ配列の両端に置く
var i = ht.startIndex, j = ht.endIndex - 1
// 初期の最大容量は 0
var res = 0
// 2 枚の板が出会うまで貪欲選択を繰り返す
while i < j {
// 最大容量を更新する
let cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
// 短い方を内側へ動かす
if ht[i] < ht[j] {
i += 1
} else {
j -= 1
}
}
return res
}
```
=== "JS"
```javascript title="max_capacity.js"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
function maxCapacity(ht) {
// i, j を初期化し、それぞれ配列の両端に置く
let i = 0,
j = ht.length - 1;
// 初期の最大容量は 0
let res = 0;
// 2 枚の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新する
const cap = Math.min(ht[i], ht[j]) * (j - i);
res = Math.max(res, cap);
// 短い方を内側へ動かす
if (ht[i] < ht[j]) {
i += 1;
} else {
j -= 1;
}
}
return res;
}
```
=== "TS"
```typescript title="max_capacity.ts"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
function maxCapacity(ht: number[]): number {
// i, j を初期化し、それぞれ配列の両端に置く
let i = 0,
j = ht.length - 1;
// 初期の最大容量は 0
let res = 0;
// 2 枚の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新する
const cap: number = Math.min(ht[i], ht[j]) * (j - i);
res = Math.max(res, cap);
// 短い方を内側へ動かす
if (ht[i] < ht[j]) {
i += 1;
} else {
j -= 1;
}
}
return res;
}
```
=== "Dart"
```dart title="max_capacity.dart"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
int maxCapacity(List<int> ht) {
// i, j を初期化し、それぞれ配列の両端に置く
int i = 0, j = ht.length - 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;
}
```
=== "Rust"
```rust title="max_capacity.rs"
[class]{}-[func]{max_capacity}
/* 最大容量:貪欲法 */
fn max_capacity(ht: &[i32]) -> i32 {
// i, j を初期化し、それぞれ配列の両端に置く
let mut i = 0;
let mut j = ht.len() - 1;
// 初期の最大容量は 0
let mut res = 0;
// 2 枚の板が出会うまで貪欲選択を繰り返す
while i < j {
// 最大容量を更新する
let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;
res = std::cmp::max(res, cap);
// 短い方を内側へ動かす
if ht[i] < ht[j] {
i += 1;
} else {
j -= 1;
}
}
res
}
```
=== "C"
```c title="max_capacity.c"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
int maxCapacity(int ht[], int htLength) {
// i, j を初期化し、それぞれ配列の両端に置く
int i = 0;
int j = htLength - 1;
// 初期の最大容量は 0
int res = 0;
// 2 枚の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新する
int capacity = myMin(ht[i], ht[j]) * (j - i);
res = myMax(res, capacity);
// 短い方を内側へ動かす
if (ht[i] < ht[j]) {
i++;
} else {
j--;
}
}
return res;
}
```
=== "Kotlin"
```kotlin title="max_capacity.kt"
[class]{}-[func]{maxCapacity}
/* 最大容量:貪欲法 */
fun maxCapacity(ht: IntArray): Int {
// i, j を初期化し、それぞれ配列の両端に置く
var i = 0
var j = ht.size - 1
// 初期の最大容量は 0
var res = 0
// 2 枚の板が出会うまで貪欲選択を繰り返す
while (i < j) {
// 最大容量を更新する
val cap = min(ht[i], ht[j]) * (j - i)
res = max(res, cap)
// 短い方を内側へ動かす
if (ht[i] < ht[j]) {
i++
} else {
j--
}
}
return res
}
```
=== "Ruby"
```ruby title="max_capacity.rb"
[class]{}-[func]{max_capacity}
### 最大容量:貪欲法 ###
def max_capacity(ht)
# i, j を初期化し、それぞれ配列の両端に置く
i, j = 0, ht.length - 1
# 初期の最大容量は 0
res = 0
# 2 枚の板が出会うまで貪欲選択を繰り返す
while i < j
# 最大容量を更新する
cap = [ht[i], ht[j]].min * (j - i)
res = [res, cap].max
# 短い方を内側へ動かす
if ht[i] < ht[j]
i += 1
else
j -= 1
end
end
res
end
```
??? pythontutor "コードの可視化"
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20i%2C%20j%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%97%E3%80%81%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E9%85%8D%E5%88%97%E3%81%AE%E4%B8%A1%E7%AB%AF%E3%81%AB%E7%BD%AE%E3%81%8F%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E6%9C%9F%E3%81%AE%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%81%AF%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%202%20%E6%9E%9A%E3%81%AE%E6%9D%BF%E3%81%8C%E5%87%BA%E4%BC%9A%E3%81%86%E3%81%BE%E3%81%A7%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20cap%20%3D%20min%28ht%5Bi%5D%2C%20ht%5Bj%5D%29%20%2A%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res%2C%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E7%9F%AD%E3%81%84%E6%96%B9%E3%82%92%E5%86%85%E5%81%B4%E3%81%B8%E5%8B%95%E3%81%8B%E3%81%99%0A%20%20%20%20%20%20%20%20if%20ht%5Bi%5D%20%3C%20ht%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20return%20res%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20ht%20%3D%20%5B3%2C%208%2C%205%2C%202%2C%207%2C%207%2C%203%2C%204%5D%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20max_capacity%28ht%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=def%20max_capacity%28ht%3A%20list%5Bint%5D%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20i%2C%20j%20%E3%82%92%E5%88%9D%E6%9C%9F%E5%8C%96%E3%81%97%E3%80%81%E3%81%9D%E3%82%8C%E3%81%9E%E3%82%8C%E9%85%8D%E5%88%97%E3%81%AE%E4%B8%A1%E7%AB%AF%E3%81%AB%E7%BD%AE%E3%81%8F%0A%20%20%20%20i%2C%20j%20%3D%200%2C%20len%28ht%29%20-%201%0A%20%20%20%20%23%20%E5%88%9D%E6%9C%9F%E3%81%AE%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%81%AF%200%0A%20%20%20%20res%20%3D%200%0A%20%20%20%20%23%202%20%E6%9E%9A%E3%81%AE%E6%9D%BF%E3%81%8C%E5%87%BA%E4%BC%9A%E3%81%86%E3%81%BE%E3%81%A7%E8%B2%AA%E6%AC%B2%E9%81%B8%E6%8A%9E%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%99%0A%20%20%20%20while%20i%20%3C%20j%3A%0A%20%20%20%20%20%20%20%20%23%20%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20cap%20%3D%20min%28ht%5Bi%5D%2C%20ht%5Bj%5D%29%20%2A%20%28j%20-%20i%29%0A%20%20%20%20%20%20%20%20res%20%3D%20max%28res%2C%20cap%29%0A%20%20%20%20%20%20%20%20%23%20%E7%9F%AD%E3%81%84%E6%96%B9%E3%82%92%E5%86%85%E5%81%B4%E3%81%B8%E5%8B%95%E3%81%8B%E3%81%99%0A%20%20%20%20%20%20%20%20if%20ht%5Bi%5D%20%3C%20ht%5Bj%5D%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20i%20%2B%3D%201%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20j%20-%3D%201%0A%20%20%20%20return%20res%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20ht%20%3D%20%5B3%2C%208%2C%205%2C%202%2C%207%2C%207%2C%203%2C%204%5D%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20max_capacity%28ht%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%AE%B9%E9%87%8F%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=4&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
### 3. &nbsp; 正しさの証明
貪欲法が列挙よりも高速である理由は、各ラウンドの貪欲選択が一部の状態を「スキップ」するからです。
貪欲法が総当たりより速いのは、各ラウンドの貪欲選択がいくつかの状態を「スキップ」するためです。
例えば、$i$ が短い仕切りで $j$ が高い仕切りである状態 $cap[i, j]$ の下で、短い仕切り $i$ を貪欲に1ステップ内側に移動すると、の図に示す「スキップされた」状態につながります。**これは、れらの状態の容量を後で検証できないことを意味します**。
たとえば状態 $cap[i, j]$ において、$i$ が短い板、$j$ が長い板だとします。貪欲に短い板 $i$ を内側へ 1 マス動かすと、の図に示す状態が「スキップされます。**これは、その後それらの状態の容量を検証できないことを意味します**。
$$
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
$$
![短い仕切りの移動によってスキップされる状態](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" }
![短いの移動によってスキップされる状態](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" }
<p align="center"> 図 15-12 &nbsp; 短い仕切りの移動によってスキップされる状態 </p>
<p align="center"> 図 15-12 &nbsp; 短いの移動によってスキップされる状態 </p>
観察すると、**これらのスキップされた状態は実際には高い仕切り $j$ 内側に移動したすべての状態**です。高い仕切りを内側に移動すると容量が確実に減少することをすでに証明しました。したがって、スキップされた状態は最適解である可能性がなく、**それらをスキップしても最適解を逃すことはありません**。
観察すると、**これらのスキップされた状態は実際には長い板 $j$ 内側へ動かしたすべての状態そのものです**。前述のとおり、長い板を内側へ動かすと容量は必ず小さくなります。つまり、スキップされた状態はいずれも最適解にはなりえず、**それらを飛ばしても最適解を逃すことはありません**。
分析により、短い仕切りを移動する操作は「安全」であり、貪欲戦略が効果的であることが示されます。
以上の分析から、短い板を動かす操作は「安全」であり、貪欲戦略は有効であると分かります。
@@ -2,33 +2,33 @@
comments: true
---
# 15.4 &nbsp; 最大積切断問題
# 15.4 &nbsp; 最大積分割問題
!!! question
整数 $n$ が与えられたとき、それを合計が $n$ になる少なくとも2つの正整数に分割し、これらの整数の最大を求めてください。下図に示すとおりです
正整数 $n$ が与えられたとき、それを少なくとも 2 つの正整数の和に分割し、分割後のすべての整数の積の最大を求め。下図に示す。
![最大積切断問題の定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" }
![最大積分割問題の定義](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" }
<p align="center"> 図 15-13 &nbsp; 最大積切断問題の定義 </p>
<p align="center"> 図 15-13 &nbsp; 最大積分割問題の定義 </p>
$n$ を $m$ 個の整数因子に分割すると仮定し、$i$ 番目の因子を $n_i$ と記すと、
仮に $n$ を $m$ 個の整数因子に分割し、そのうち第 $i$ の因子を $n_i$ と記すと、
$$
n = \sum_{i=1}^{m}n_i
$$
この問題の目は、すべての整数因子の最大積を見つけることです。すなわち
問題の目は、すべての整数因子の積の最大値を求めることであり、すなわち
$$
\max(\prod_{i=1}^{m}n_i)
$$
慮すべき点:分割数 $m$ はどの程度大きくすべきか、各 $n_i$ は何であるべきか?
えるべきことは、分割数 $m$ をいくつにすべきか、各 $n_i$ をいくつにすべきかである。
### 1. &nbsp; 貪欲戦略の決定
経験的に、2つの整数の積は多くの場合その和より大きくなります。$n$ から因子 $2$ を分割すると仮定すると、その積は $2(n-2)$ です。この積を $n$ と比較します:
経験的に、2 つの整数の積はその和より大きくなることが多い。$n$ から因子 $2$ を 1 つ切り出すと、それらの積は $2(n-2)$ となる。この積を $n$ と比較すると、
$$
\begin{aligned}
@@ -38,82 +38,82 @@ n & \geq 4
\end{aligned}
$$
下の図に示すように、$n \geq 4$ のとき、$2$ を分割すると積が増加します。**これは4以上の整数分割すべきであることを示しています**。
のように、$n \geq 4$ のとき、$2$ を 1 つ切り出すと積は大きくなる。**これは、$4$ 以上の整数はすべて分割すべきことを意味する**。
**貪欲戦略1**:分割スキームが $\geq 4$ の因子を含む場合、それはさらに分割されるべきで。最終的な分割因子 $1$、$2$、$3$ のみを含むべきです
**貪欲戦略**:分割方法に $\geq 4$ の因子が含まれるなら、それはさらに分割べきである。最終的な分割方法に現れる因子 $1$、$2$、$3$ の 3 種類だけである
![分割による積の増加](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" }
![分割により積が大きくな](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" }
<p align="center"> 図 15-14 &nbsp; 分割による積の増加 </p>
<p align="center"> 図 15-14 &nbsp; 分割により積が大きくなる </p>
次に、どの因子が最適かを考慮します。因子 $1$、$2$、$3$ の中で、明らかに $1$ が最悪です。$1 \times (n-1) < n$ 常に成り立つため、$1$ を分割すると実際に積が減少します
次に、どの因子が最適かを考える。$1$、$2$、$3$ の 3 つの因子のうち、明らかに $1$ が最も悪い。なぜなら $1 \times (n-1) < n$ 常に成り立、$1$ を切り出すとかえって積が小さくなるからである
下の図に示すように、$n = 6$ のとき、$3 \times 3 > 2 \times 2 \times 2$ です。**これは $3$ を分割する方が $2$ を分割するよりも良いことを意味します**。
のように、$n = 6$ のとき、$3 \times 3 > 2 \times 2 \times 2$ が成り立つ。**これは、$2$ を切り出すより $3$ を切り出すほうが有利であることを意味す**。
**貪欲戦略2**:分割スキームには最大で2つの $2$ があるべきです。3つの $2$ は常に2つの $3$ に置き換えてより高い積を得ることができるからで
**貪欲戦略**:分割方法の中に存在してよい $2$ は高々 2 つである。なぜなら、3 つの $2$ は常に 2 つの $3$ に置き換えられ、より大きな積を得られるからである
![最適な分割因子](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png){ class="animation-figure" }
<p align="center"> 図 15-15 &nbsp; 最適な分割因子 </p>
上記から、以下の貪欲戦略を導出できます
以上より、次の貪欲戦略が導かれる
1. 入力整数 $n$ について、余りが $0$、$1$、または $2$ になるまで因子 $3$ を継続的に分割します。
2. 余りが $0$ の場合、$n$ $3$ の倍数であることを意味するため、それ以上の行動は取りません
3. 余りが $2$ の場合、さらに分割を続けず、そのまま保持します。
4. 余りが $1$ の場合、$2 \times 2 > 1 \times 3$ であるため、最後の $3$ を $2$ に置き換えるべきで
1. 整数 $n$ を入力し、余りが $0$、$1$、$2$ になるまで、そこから因子 $3$ を繰り返し切り出す。
2. 余りが $0$ のとき、$n$ $3$ の倍数であることを表すため、何も処理しない
3. 余りが $2$ のときは、それ以上分割せず、そのまます。
4. 余りが $1$ のとき、$2 \times 2 > 1 \times 3$ であるため、最後の $3$ を $2$ に置き換えるべきである
### 2. &nbsp; コード実装
の図に示すように、整数を分割するためにループを使用する必要はなく、床除算演算を使用して $3$ の数 $a$ を取得し、剰余演算を使用して余り $b$ を取得できます。したがって:
図のように、ループで整数を分割する必要はなく、切り捨て除算によって $3$ の数 $a$ を、剰余演算によって余り $b$ を得られる。このとき、
$$
n = 3a + b
n = 3 a + b
$$
$n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積は $1 \times (n - 1)$ であることに注意してください
なお、$n \leq 3$ の境界ケースでは、必ず $1$ を 1 つ分割する必要があり、積は $1 \times (n - 1)$ となる
=== "Python"
```python title="max_product_cutting.py"
def max_product_cutting(n: int) -> int:
"""切断の最大積:貪欲法"""
# n <= 3 の場合、1 を切り出す必要がある
"""最大切断積:貪欲法"""
# n <= 3 のときは、必ず 1 を切り出す
if n <= 3:
return 1 * (n - 1)
# 貪欲に 3 を切り出、a 3 の個数、b 余り
# 貪欲に 3 を切り出、a 3 の個数、b 余りとする
a, b = n // 3, n % 3
if b == 1:
# 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変
# 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return int(math.pow(3, a - 1)) * 2 * 2
if b == 2:
# 余りが 2 の場合、何もしない
# 余りが 2 のときは、そのままにする
return int(math.pow(3, a)) * 2
# 余りが 0 の場合、何もしない
# 余りが 0 のときは、そのままにする
return int(math.pow(3, a))
```
=== "C++"
```cpp title="max_product_cutting.cpp"
/* 最大切断:貪欲法 */
/* 最大切断:貪欲法 */
int maxProductCutting(int n) {
// n <= 3 の場合、1 を切り出す必要がある
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出す。a 3 の個数、b 余り
// 貪欲に 3 を切り出し、a 3 の個数、b 余りとする
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return (int)pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 余りが 2 の場合、何もしない
// 余りが 2 のときは、そのままにする
return (int)pow(3, a) * 2;
}
// 余りが 0 の場合、何もしない
// 余りが 0 のときは、そのままにする
return (int)pow(3, a);
}
```
@@ -121,24 +121,24 @@ $n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積
=== "Java"
```java title="max_product_cutting.java"
/* 最大切断:貪欲法 */
/* 最大切断:貪欲法 */
int maxProductCutting(int n) {
// n <= 3 の場合、1 を切り出す必要がある
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出す。a 3 の個数、b 余り
// 貪欲に 3 を切り出し、a 3 の個数、b 余りとする
int a = n / 3;
int b = n % 3;
if (b == 1) {
// 余りが 1 の場合、1 * 3 のペアを 2 * 2 に変
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return (int) Math.pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 余りが 2 の場合、何もしない
// 余りが 2 のときは、そのままにする
return (int) Math.pow(3, a) * 2;
}
// 余りが 0 の場合、何もしない
// 余りが 0 のときは、そのままにする
return (int) Math.pow(3, a);
}
```
@@ -146,78 +146,266 @@ $n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積
=== "C#"
```csharp title="max_product_cutting.cs"
[class]{max_product_cutting}-[func]{MaxProductCutting}
/* 最大切断積:貪欲法 */
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);
}
```
=== "Go"
```go title="max_product_cutting.go"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
func maxProductCutting(n int) int {
// n <= 3 のときは、必ず 1 を切り出す
if n <= 3 {
return 1 * (n - 1)
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
a := n / 3
b := n % 3
if b == 1 {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return int(math.Pow(3, float64(a-1))) * 2 * 2
}
if b == 2 {
// 余りが 2 のときは、そのままにする
return int(math.Pow(3, float64(a))) * 2
}
// 余りが 0 のときは、そのままにする
return int(math.Pow(3, float64(a)))
}
```
=== "Swift"
```swift title="max_product_cutting.swift"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
func maxProductCutting(n: Int) -> Int {
// n <= 3 のときは、必ず 1 を切り出す
if n <= 3 {
return 1 * (n - 1)
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
let a = n / 3
let b = n % 3
if b == 1 {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return pow(3, a - 1) * 2 * 2
}
if b == 2 {
// 余りが 2 のときは、そのままにする
return pow(3, a) * 2
}
// 余りが 0 のときは、そのままにする
return pow(3, a)
}
```
=== "JS"
```javascript title="max_product_cutting.js"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
function maxProductCutting(n) {
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
let a = Math.floor(n / 3);
let b = n % 3;
if (b === 1) {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return Math.pow(3, a - 1) * 2 * 2;
}
if (b === 2) {
// 余りが 2 のときは、そのままにする
return Math.pow(3, a) * 2;
}
// 余りが 0 のときは、そのままにする
return Math.pow(3, a);
}
```
=== "TS"
```typescript title="max_product_cutting.ts"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
function maxProductCutting(n: number): number {
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
let a: number = Math.floor(n / 3);
let b: number = n % 3;
if (b === 1) {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return Math.pow(3, a - 1) * 2 * 2;
}
if (b === 2) {
// 余りが 2 のときは、そのままにする
return Math.pow(3, a) * 2;
}
// 余りが 0 のときは、そのままにする
return Math.pow(3, a);
}
```
=== "Dart"
```dart title="max_product_cutting.dart"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
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 (pow(3, a - 1) * 2 * 2).toInt();
}
if (b == 2) {
// 余りが 2 のときは、そのままにする
return (pow(3, a) * 2).toInt();
}
// 余りが 0 のときは、そのままにする
return pow(3, a).toInt();
}
```
=== "Rust"
```rust title="max_product_cutting.rs"
[class]{}-[func]{max_product_cutting}
/* 最大切断積:貪欲法 */
fn max_product_cutting(n: i32) -> i32 {
// n <= 3 のときは、必ず 1 を切り出す
if n <= 3 {
return 1 * (n - 1);
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
let a = n / 3;
let b = n % 3;
if b == 1 {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
3_i32.pow(a as u32 - 1) * 2 * 2
} else if b == 2 {
// 余りが 2 のときは、そのままにする
3_i32.pow(a as u32) * 2
} else {
// 余りが 0 のときは、そのままにする
3_i32.pow(a as u32)
}
}
```
=== "C"
```c title="max_product_cutting.c"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
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 pow(3, a - 1) * 2 * 2;
}
if (b == 2) {
// 余りが 2 のときは、そのままにする
return pow(3, a) * 2;
}
// 余りが 0 のときは、そのままにする
return pow(3, a);
}
```
=== "Kotlin"
```kotlin title="max_product_cutting.kt"
[class]{}-[func]{maxProductCutting}
/* 最大切断積:貪欲法 */
fun maxProductCutting(n: Int): Int {
// n <= 3 のときは、必ず 1 を切り出す
if (n <= 3) {
return 1 * (n - 1)
}
// 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
val a = n / 3
val b = n % 3
if (b == 1) {
// 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return 3.0.pow((a - 1)).toInt() * 2 * 2
}
if (b == 2) {
// 余りが 2 のときは、そのままにする
return 3.0.pow(a).toInt() * 2 * 2
}
// 余りが 0 のときは、そのままにする
return 3.0.pow(a).toInt()
}
```
=== "Ruby"
```ruby title="max_product_cutting.rb"
[class]{}-[func]{max_product_cutting}
### 最大分割積:貪欲法 ###
def max_product_cutting(n)
# n <= 3 のときは、必ず 1 を切り出す
return 1 * (n - 1) if n <= 3
# 貪欲に 3 を切り出し、a を 3 の個数、b を余りとする
a, b = n / 3, n % 3
# 余りが 1 のときは、1 * 3 を 2 * 2 に変える
return (3.pow(a - 1) * 2 * 2).to_i if b == 1
# 余りが 2 のときは、そのままにする
return (3.pow(a) * 2).to_i if b == 2
# 余りが 0 のときは、そのままにする
3.pow(a).to_i
end
```
![切断後の最大積の計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" }
??? pythontutor "コードの可視化"
<p align="center"> 図 15-16 &nbsp; 切断後の最大積の計算方法 </p>
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=import%20math%0A%0Adef%20max_product_cutting%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%88%87%E6%96%AD%E7%A9%8D%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20n%20%3C%3D%203%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E5%BF%85%E3%81%9A%201%20%E3%82%92%E5%88%87%E3%82%8A%E5%87%BA%E3%81%99%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20%2A%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E3%81%AB%203%20%E3%82%92%E5%88%87%E3%82%8A%E5%87%BA%E3%81%97%E3%80%81a%20%E3%82%92%203%20%E3%81%AE%E5%80%8B%E6%95%B0%E3%80%81b%20%E3%82%92%E4%BD%99%E3%82%8A%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20a%2C%20b%20%3D%20n%20%2F%2F%203%2C%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%201%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%811%20%2A%203%20%E3%82%92%202%20%2A%202%20%E3%81%AB%E5%A4%89%E3%81%88%E3%82%8B%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%20-%201%29%29%20%2A%202%20%2A%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%202%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%20%2A%202%0A%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%200%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2058%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20max_product_cutting%28n%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%88%86%E5%89%B2%E7%A9%8D%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=import%20math%0A%0Adef%20max_product_cutting%28n%3A%20int%29%20-%3E%20int%3A%0A%20%20%20%20%22%22%22%E6%9C%80%E5%A4%A7%E5%88%87%E6%96%AD%E7%A9%8D%EF%BC%9A%E8%B2%AA%E6%AC%B2%E6%B3%95%22%22%22%0A%20%20%20%20%23%20n%20%3C%3D%203%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E5%BF%85%E3%81%9A%201%20%E3%82%92%E5%88%87%E3%82%8A%E5%87%BA%E3%81%99%0A%20%20%20%20if%20n%20%3C%3D%203%3A%0A%20%20%20%20%20%20%20%20return%201%20%2A%20%28n%20-%201%29%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E3%81%AB%203%20%E3%82%92%E5%88%87%E3%82%8A%E5%87%BA%E3%81%97%E3%80%81a%20%E3%82%92%203%20%E3%81%AE%E5%80%8B%E6%95%B0%E3%80%81b%20%E3%82%92%E4%BD%99%E3%82%8A%E3%81%A8%E3%81%99%E3%82%8B%0A%20%20%20%20a%2C%20b%20%3D%20n%20%2F%2F%203%2C%20n%20%25%203%0A%20%20%20%20if%20b%20%3D%3D%201%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%201%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%811%20%2A%203%20%E3%82%92%202%20%2A%202%20%E3%81%AB%E5%A4%89%E3%81%88%E3%82%8B%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%20-%201%29%29%20%2A%202%20%2A%202%0A%20%20%20%20if%20b%20%3D%3D%202%3A%0A%20%20%20%20%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%202%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%20%2A%202%0A%20%20%20%20%23%20%E4%BD%99%E3%82%8A%E3%81%8C%200%20%E3%81%AE%E3%81%A8%E3%81%8D%E3%81%AF%E3%80%81%E3%81%9D%E3%81%AE%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%99%E3%82%8B%0A%20%20%20%20return%20int%28math.pow%283%2C%20a%29%29%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20n%20%3D%2058%0A%0A%20%20%20%20%23%20%E8%B2%AA%E6%AC%B2%E6%B3%95%0A%20%20%20%20res%20%3D%20max_product_cutting%28n%29%0A%20%20%20%20print%28f%22%E6%9C%80%E5%A4%A7%E5%88%86%E5%89%B2%E7%A9%8D%E3%81%AF%20%7Bres%7D%22%29&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=5&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">全画面で見る ></a></div>
**時間計算量はプログラミング言語のべき乗演算の実装に依存します**。Pythonでは、よく使用されるべき乗計算関数は3種類あります:
![最大積分割の計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" }
- 演算子 `**` と関数 `pow()` の両方の時間計算量は $O(\log⁡ a)$ です。
- `math.pow()` 関数は内部でC言語ライブラリの `pow()` 関数を呼び出し、浮動小数点べき乗を実行し、時間計算量は $O(1)$ です。
<p align="center"> 図 15-16 &nbsp; 最大積分割の計算方法 </p>
変数 $a$ と $b$ は一定サイズの追加スペースを使用するため、**空間計算量は $O(1)$** です
**時間計算量は、プログラミング言語におけるべき乗演算の実装方法に依存する**。Python を例に取ると、よく使われるべき乗計算関数は 3 種類ある
- 演算子 `**` と関数 `pow()` の時間計算量はいずれも $O(\log⁡ a)$ である。
- 関数 `math.pow()` は内部で C 言語ライブラリの `pow()` 関数を呼び出し、浮動小数点のべき乗を実行するため、時間計算量は $O(1)$ である。
変数 $a$ と $b$ が使う追加領域は定数サイズであり、**したがって空間計算量は $O(1)$ である**。
### 3. &nbsp; 正しさの証明
背理法を使用し、$n \geq 3$ のケースのみを分析します
背理法を用い、$n \geq 4$ の場合のみを考える
1. **すべての因子 $\leq 3$**:最適分割スキームが因子 $x \geq 4$ を含むと仮定すると、それを確実に $2(x-2)$ にさらに分割でき、より大きな積を得られます。これは仮定矛盾します。
2. **分割スキームに $1$ 含まれない**:最適分割スキームが因子 $1$ を含むと仮定すると、それを確実に別の因子と結合してより大き積を得られます。これは仮定矛盾します。
3. **分割スキームには最大で2つの $2$ が含まれる**:最適分割スキームが3つの $2$ を含むと仮定すると、それらを確実に2つの $3$ に置き換えて、より高い積を達成できます。これは仮定矛盾します。
1. **すべての因子 $\leq 3$** :最適分割方法に $\geq 4$ の因子 $x$ が存在すると仮定すると、それは必ずさらに $2(x-2)$ に分割でき、より大きい(または等しい)積が得られ。これは仮定矛盾す
2. **分割方法に $1$ 含まれない** :最適分割方法に因子 $1$ が 1 つ存在すると仮定すると、それは必ず別の因子に併合でき、より大き積を得られ。これは仮定矛盾す
3. **分割方法に含まれる $2$ は高々 2 つ** :最適分割方法に 3 つの $2$ が含まれると仮定すると、それは必ず 2 つの $3$ に置き換えられ、積はより大きくなる。これは仮定矛盾す
+12 -10
View File
@@ -4,13 +4,15 @@ comments: true
# 15.5 &nbsp; まとめ
- 貪欲アルゴリズムは最適化問題を解決するためによく使用され、原理は各決定段階で局所的に最適な決定を行い、グローバルに最適な解を達成することです。
- 貪欲アルゴリズムは貪欲な選択を次々と反復的に行い、各ラウンドで問題をより小さな部分問題に変換し、問題が解決されるまで続けます。
- 貪欲アルゴリズムは実装が簡単なだけでなく、問題解決効率も高いです。動的プログラミングと比較して、貪欲アルゴリズムは一般的により低い時間計算量を持ちます
- コイン交換問題において、貪欲アルゴリズムは特定のコインの組み合わせに対して最適解を保証できますが、他の組み合わせでは貪欲アルゴリズムが非常に悪い解を見つける可能性があります
- 貪欲アルゴリズム解法に適した問題は2つの主要な性質を持ちます:貪欲選択性と最適部分構造。貪欲選択性は貪欲戦略の効果を表します
- 一部の複雑な問題では、貪欲選択性を証明することは簡単ではありません。逆に、無効性を証明することはしばしばより容易で、コイン交換問題などがその例です
- 貪欲問題の解決は主に3つのステップから構成されます:問題分析、貪欲戦略の決定、正しさの証明。このうち、貪欲戦略の決定が重要なステップであり、正しさの証明がしばしば挑戦となります
- 分数ナップサック問題は0-1ナップサック問題に基づいてアイテムの一部の選択を可能にし、したがって貪欲アルゴリズムを使用して解決できます。貪欲戦略の正しさは背理法によって証明できます
- 最大容量問題は全探索法で解決でき、時間計算量は $O(n^2)$ です。貪欲戦略を設計することで、各ラウンドで短い板を内側に移動し、時間計算量を $O(n)$ に最適化します
- 切断後の最大積問題において、2つの貪欲戦略を導出します:$\geq 4$ の整数は継続的に切断されるべきで、最適な切断因子は $3$ です。コードにはべき乗演算が含まれ、時間計算量はべき乗演算の実装方法に依存し、一般的に $O(1)$ または $O(\log n)$ です
### 1. &nbsp; 重要な振り返り
- 貪欲法は通常、最適化問題を解くために用いられ、その原理は各意思決定段階で局所最適な決定を行い、全体最適解を得ることを目指すというものである
- 貪欲法は反復的に次々と貪欲な選択を行い、各ラウンドで問題をより小さな部分問題へと変換し、最終的に問題を解決する
- 貪欲法は実装が簡単であるだけでなく、問題を解く効率も高い。動的計画法と比べると、貪欲法の時間計算量は通常より低い
- 硬貨両替問題では、ある種の硬貨の組み合わせに対しては貪欲法で最適解を保証できるが、別の組み合わせではそうではなく、非常に悪い解を見つけてしまう可能性がある
- 貪欲法による解法に適した問題は、貪欲選択性と最適部分構造という 2 つの性質を備えている。貪欲選択性は、貪欲戦略の有効性を表している
- 一部の複雑な問題では、貪欲選択性を証明するのは容易ではない。相対的には、反例による否定のほうが簡単であり、硬貨両替問題がその一例である
- 貪欲法の問題を解く流れは主に 3 段階に分かれる。すなわち、問題分析、貪欲戦略の決定、正しさの証明である。このうち、貪欲戦略の決定が中核であり、正しさの証明はしばしば難所となる
- 分数ナップサック問題は 0-1 ナップサックを基に、品物の一部を選ぶことを許しているため、貪欲法で解くことができる。貪欲戦略の正しさは背理法で証明できる
- 最大容量問題は全探索で解くことができ、時間計算量は $O(n^2)$ である。貪欲戦略を設計し、各ラウンドで短い板を内側へ動かすことで、時間計算量を $O(n)$ に最適化できる。
- 最大分割積問題では、2 つの貪欲戦略を順に導いた。すなわち、$\geq 4$ の整数はすべてさらに分割すべきであり、最適な分割因子は $3$ である。コードにはべき乗演算が含まれており、時間計算量はその実装方法に依存し、通常は $O(1)$ または $O(\log n)$ である。