mirror of
https://github.com/krahets/hello-algo.git
synced 2026-06-30 09:34:25 +00:00
409 lines
37 KiB
Markdown
409 lines
37 KiB
Markdown
---
|
|
comments: true
|
|
---
|
|
|
|
# 15.1 Жадный алгоритм
|
|
|
|
<u>Жадный алгоритм (greedy algorithm)</u> - это распространенный метод решения задач оптимизации. Его основная идея состоит в том, чтобы на каждом этапе принятия решения выбирать вариант, который выглядит наилучшим прямо сейчас, то есть жадно принимать локально оптимальные решения в надежде получить глобально оптимальный результат. Жадные алгоритмы просты и эффективны, поэтому широко применяются во многих практических задачах.
|
|
|
|
Жадные алгоритмы и динамическое программирование часто используются для решения задач оптимизации. У них есть некоторое сходство, например оба метода опираются на свойство оптимальной подструктуры, но принципы работы различаются.
|
|
|
|
- Динамическое программирование при получении текущего решения учитывает все предыдущие решения и использует ответы для прошлых подзадач, чтобы построить ответ для текущей подзадачи.
|
|
- Жадный алгоритм не учитывает предыдущие решения, а просто движется вперед, каждый раз делая жадный выбор и постепенно сужая область задачи, пока она не будет решена.
|
|
|
|
Чтобы лучше понять принцип работы жадного алгоритма, разберем его на примере задачи «размен монет». Эта задача уже встречалась в разделе «задача о полном рюкзаке», поэтому она наверняка вам знакома.
|
|
|
|
!!! question
|
|
|
|
Дано $n$ видов монет. Номинал монеты $i$ равен $coins[i - 1]$, целевая сумма равна $amt$, причем каждую монету можно брать неограниченное число раз. Требуется найти минимальное число монет, которыми можно набрать целевую сумму. Если набрать сумму невозможно, верните $-1$.
|
|
|
|
Жадная стратегия для этой задачи показана на рисунке 15-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) {
|
|
// Предположить, что список coins упорядочен
|
|
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) {
|
|
// Предположить, что список 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;
|
|
}
|
|
```
|
|
|
|
=== "C#"
|
|
|
|
```csharp title="coin_change_greedy.cs"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
/* Размен монет: жадный алгоритм */
|
|
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"
|
|
### Размен монет: жадный алгоритм ###
|
|
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
|
|
```
|
|
|
|
??? pythontutor "Визуализация кода"
|
|
|
|
<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%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D0%BD%20%D0%BC%D0%BE%D0%BD%D0%B5%D1%82%3A%20%D0%B6%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%22%22%22%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%B5%D0%B4%D0%BF%D0%BE%D0%BB%D0%BE%D0%B6%D0%B8%D1%82%D1%8C%2C%20%D1%87%D1%82%D0%BE%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20coins%20%D1%83%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BE%D1%87%D0%B5%D0%BD%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%D0%A6%D0%B8%D0%BA%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%20%D0%B2%D1%8B%D0%BF%D0%BE%D0%BB%D0%BD%D1%8F%D1%82%D1%8C%20%D0%B6%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%2C%20%D0%BF%D0%BE%D0%BA%D0%B0%20%D0%BD%D0%B5%20%D0%BE%D1%81%D1%82%D0%B0%D0%BD%D0%B5%D1%82%D1%81%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D1%8B%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%9D%D0%B0%D0%B9%D1%82%D0%B8%20%D0%BC%D0%BE%D0%BD%D0%B5%D1%82%D1%83%2C%20%D0%BA%D0%BE%D1%82%D0%BE%D1%80%D0%B0%D1%8F%20%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B5%20%D0%BE%D1%81%D1%82%D0%B0%D1%82%D0%BA%D0%B0%20%D1%81%D1%83%D0%BC%D0%BC%D1%8B%20%D0%B8%20%D0%BD%D0%B0%D0%B8%D0%B1%D0%BE%D0%BB%D0%B5%D0%B5%20%D0%BA%20%D0%BD%D0%B5%D0%BC%D1%83%20%D0%B1%D0%BB%D0%B8%D0%B7%D0%BA%D0%B0%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%20%D0%92%D1%8B%D0%B1%D1%80%D0%B0%D1%82%D1%8C%20coins%5Bi%5D%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%D0%95%D1%81%D0%BB%D0%B8%20%D0%B4%D0%BE%D0%BF%D1%83%D1%81%D1%82%D0%B8%D0%BC%D0%BE%D0%B5%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BD%D0%B5%20%D0%BD%D0%B0%D0%B9%D0%B4%D0%B5%D0%BD%D0%BE%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20-1%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%96%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%BE%D0%B4%D1%85%D0%BE%D0%B4%3A%20%D0%B3%D0%B0%D1%80%D0%B0%D0%BD%D1%82%D0%B8%D1%80%D1%83%D0%B5%D1%82%20%D0%BD%D0%B0%D1%85%D0%BE%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B3%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%20%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%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%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5%20%D1%87%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BC%D0%BE%D0%BD%D0%B5%D1%82%20%D0%B4%D0%BB%D1%8F%20%D0%BD%D0%B0%D0%B1%D0%BE%D1%80%D0%B0%20%D1%81%D1%83%D0%BC%D0%BC%D1%8B%20%7Bamt%7D%20%3D%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%D0%96%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%BE%D0%B4%D1%85%D0%BE%D0%B4%3A%20%D0%BD%D0%B5%20%D0%B3%D0%B0%D1%80%D0%B0%D0%BD%D1%82%D0%B8%D1%80%D1%83%D0%B5%D1%82%20%D0%BD%D0%B0%D1%85%D0%BE%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B3%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%20%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%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%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5%20%D1%87%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BC%D0%BE%D0%BD%D0%B5%D1%82%20%D0%B4%D0%BB%D1%8F%20%D0%BD%D0%B0%D0%B1%D0%BE%D1%80%D0%B0%20%D1%81%D1%83%D0%BC%D0%BC%D1%8B%20%7Bamt%7D%20%3D%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%D0%9D%D0%B0%20%D1%81%D0%B0%D0%BC%D0%BE%D0%BC%20%D0%B4%D0%B5%D0%BB%D0%B5%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D1%83%D0%BC%20%D1%80%D0%B0%D0%B2%D0%B5%D0%BD%203%3A%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%D0%A0%D0%B0%D0%B7%D0%BC%D0%B5%D0%BD%20%D0%BC%D0%BE%D0%BD%D0%B5%D1%82%3A%20%D0%B6%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%22%22%22%0A%20%20%20%20%23%20%D0%9F%D1%80%D0%B5%D0%B4%D0%BF%D0%BE%D0%BB%D0%BE%D0%B6%D0%B8%D1%82%D1%8C%2C%20%D1%87%D1%82%D0%BE%20%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA%20coins%20%D1%83%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BE%D1%87%D0%B5%D0%BD%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%D0%A6%D0%B8%D0%BA%D0%BB%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%20%D0%B2%D1%8B%D0%BF%D0%BE%D0%BB%D0%BD%D1%8F%D1%82%D1%8C%20%D0%B6%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%B2%D1%8B%D0%B1%D0%BE%D1%80%2C%20%D0%BF%D0%BE%D0%BA%D0%B0%20%D0%BD%D0%B5%20%D0%BE%D1%81%D1%82%D0%B0%D0%BD%D0%B5%D1%82%D1%81%D1%8F%20%D1%81%D1%83%D0%BC%D0%BC%D1%8B%0A%20%20%20%20while%20amt%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%23%20%D0%9D%D0%B0%D0%B9%D1%82%D0%B8%20%D0%BC%D0%BE%D0%BD%D0%B5%D1%82%D1%83%2C%20%D0%BA%D0%BE%D1%82%D0%BE%D1%80%D0%B0%D1%8F%20%D0%BC%D0%B5%D0%BD%D1%8C%D1%88%D0%B5%20%D0%BE%D1%81%D1%82%D0%B0%D1%82%D0%BA%D0%B0%20%D1%81%D1%83%D0%BC%D0%BC%D1%8B%20%D0%B8%20%D0%BD%D0%B0%D0%B8%D0%B1%D0%BE%D0%BB%D0%B5%D0%B5%20%D0%BA%20%D0%BD%D0%B5%D0%BC%D1%83%20%D0%B1%D0%BB%D0%B8%D0%B7%D0%BA%D0%B0%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%20%D0%92%D1%8B%D0%B1%D1%80%D0%B0%D1%82%D1%8C%20coins%5Bi%5D%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%D0%95%D1%81%D0%BB%D0%B8%20%D0%B4%D0%BE%D0%BF%D1%83%D1%81%D1%82%D0%B8%D0%BC%D0%BE%D0%B5%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%BD%D0%B5%20%D0%BD%D0%B0%D0%B9%D0%B4%D0%B5%D0%BD%D0%BE%2C%20%D0%B2%D0%B5%D1%80%D0%BD%D1%83%D1%82%D1%8C%20-1%0A%20%20%20%20return%20count%20if%20amt%20%3D%3D%200%20else%20-1%0A%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%D0%96%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%BE%D0%B4%D1%85%D0%BE%D0%B4%3A%20%D0%B3%D0%B0%D1%80%D0%B0%D0%BD%D1%82%D0%B8%D1%80%D1%83%D0%B5%D1%82%20%D0%BD%D0%B0%D1%85%D0%BE%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B3%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%20%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%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%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5%20%D1%87%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BC%D0%BE%D0%BD%D0%B5%D1%82%20%D0%B4%D0%BB%D1%8F%20%D0%BD%D0%B0%D0%B1%D0%BE%D1%80%D0%B0%20%D1%81%D1%83%D0%BC%D0%BC%D1%8B%20%7Bamt%7D%20%3D%20%7Bres%7D%22%29%0A%0A%20%20%20%20%23%20%D0%96%D0%B0%D0%B4%D0%BD%D1%8B%D0%B9%20%D0%BF%D0%BE%D0%B4%D1%85%D0%BE%D0%B4%3A%20%D0%BD%D0%B5%20%D0%B3%D0%B0%D1%80%D0%B0%D0%BD%D1%82%D0%B8%D1%80%D1%83%D0%B5%D1%82%20%D0%BD%D0%B0%D1%85%D0%BE%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5%20%D0%B3%D0%BB%D0%BE%D0%B1%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%20%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE%20%D1%80%D0%B5%D1%88%D0%B5%D0%BD%D0%B8%D1%8F%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%D0%9C%D0%B8%D0%BD%D0%B8%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D0%BE%D0%B5%20%D1%87%D0%B8%D1%81%D0%BB%D0%BE%20%D0%BC%D0%BE%D0%BD%D0%B5%D1%82%20%D0%B4%D0%BB%D1%8F%20%D0%BD%D0%B0%D0%B1%D0%BE%D1%80%D0%B0%20%D1%81%D1%83%D0%BC%D0%BC%D1%8B%20%7Bamt%7D%20%3D%20%7Bres%7D%22%29%0A%20%20%20%20print%28f%22%D0%9D%D0%B0%20%D1%81%D0%B0%D0%BC%D0%BE%D0%BC%20%D0%B4%D0%B5%D0%BB%D0%B5%20%D0%BC%D0%B8%D0%BD%D0%B8%D0%BC%D1%83%D0%BC%20%D1%80%D0%B0%D0%B2%D0%B5%D0%BD%203%3A%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>
|
|
|
|
У вас может невольно вырваться: «Эврика!» Жадный алгоритм решает задачу размена монет всего примерно десятью строками кода.
|
|
|
|
## 15.1.1 Преимущества и ограничения жадного алгоритма
|
|
|
|
**Жадный алгоритм не только прост в реализации, но и обычно очень эффективен**. В приведенном выше коде обозначим минимальный номинал монеты через $\min(coins)$, тогда жадный выбор выполняется не более чем $amt / \min(coins)$ раз, а временная сложность равна $O(amt / \min(coins))$. Это на порядок меньше, чем временная сложность решения через динамическое программирование $O(n \times amt)$.
|
|
|
|
Однако **для некоторых наборов номиналов монет жадный алгоритм не может найти оптимальный ответ**. На рисунке 15-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>
|
|
|
|
Иными словами, в задаче о размене монет жадный алгоритм не гарантирует нахождение глобально оптимального решения и иногда может приводить к очень плохому ответу. Для этой задачи больше подходит динамическое программирование.
|
|
|
|
В общем случае жадный алгоритм применим в двух следующих ситуациях.
|
|
|
|
1. **Можно гарантировать нахождение оптимального решения**: в таком случае жадный алгоритм часто является лучшим выбором, поскольку обычно он эффективнее, чем поиск с возвратом и динамическое программирование.
|
|
2. **Можно найти приближенно оптимальное решение**: в таком случае жадный алгоритм тоже полезен. Для многих сложных задач поиск глобального оптимума очень труден, и возможность быстро найти субоптимальный ответ уже весьма ценна.
|
|
|
|
## 15.1.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 Этапы решения задач жадным алгоритмом
|
|
|
|
Процесс решения жадной задачи в общем виде можно разбить на три шага.
|
|
|
|
1. **Анализ задачи**: разобраться в свойствах задачи, включая определение состояний, целевой функции и ограничений. Этот этап присутствует и в поиске с возвратом, и в динамическом программировании.
|
|
2. **Определение жадной стратегии**: определить, какой жадный выбор следует делать на каждом шаге. Эта стратегия должна уменьшать размер задачи на каждом этапе и в итоге привести к решению всей задачи.
|
|
3. **Доказательство корректности**: обычно требуется доказать, что задача обладает свойством жадного выбора и оптимальной подструктурой. На этом этапе может понадобиться математическое доказательство, например индукция или доказательство от противного.
|
|
|
|
Определение жадной стратегии - это ключевой этап решения, но на практике он часто оказывается непростым по следующим причинам.
|
|
|
|
- **Жадные стратегии для разных задач сильно различаются**. Для многих задач стратегия довольно очевидна, и до нее можно дойти за счет общих рассуждений и нескольких проб. Но в более сложных задачах жадная стратегия может быть очень скрытой, и тут уже многое зависит от опыта решения задач и алгоритмической подготовки.
|
|
- **Некоторые жадные стратегии выглядят убедительно, но оказываются обманчивыми**. Бывает, что мы с уверенностью придумали жадную стратегию, написали код и отправили его на проверку, а часть тестов не проходит. Причина в том, что спроектированная стратегия лишь «частично верна», и описанная выше задача о размене монет - типичный пример.
|
|
|
|
Чтобы гарантировать корректность, нужно дать строгое математическое доказательство жадной стратегии, **обычно с использованием доказательства от противного или математической индукции**.
|
|
|
|
Однако и доказательство корректности может оказаться непростой задачей. Если идей нет, мы обычно начинаем отлаживать код на тестовых примерах, постепенно меняя и проверяя жадную стратегию.
|
|
|
|
## 15.1.4 Типичные задачи для жадного алгоритма
|
|
|
|
Жадные алгоритмы часто применяются в задачах оптимизации, обладающих свойством жадного выбора и оптимальной подструктурой. Ниже приведены некоторые типичные задачи, решаемые жадным подходом.
|
|
|
|
- **Задача о размене монет**: при некоторых системах монет жадный алгоритм всегда дает оптимальный ответ.
|
|
- **Задача о расписании интервалов**: пусть есть несколько задач, каждая выполняется в некотором временном интервале, и требуется завершить как можно больше задач. Если каждый раз выбирать задачу с самым ранним временем окончания, то жадный алгоритм дает оптимальный ответ.
|
|
- **Задача о дробном рюкзаке**: дана группа предметов и грузоподъемность. Требуется выбрать предметы так, чтобы их общий вес не превышал ограничение, а общая ценность была максимальной. Если каждый раз выбирать предмет с наилучшим отношением стоимости к весу, то в некоторых случаях жадный алгоритм дает оптимальный ответ.
|
|
- **Задача о покупке и продаже акций**: дана история цен акции. Можно совершать несколько сделок, но если акция уже куплена, то до продажи покупать снова нельзя. Цель - получить максимальную прибыль.
|
|
- **Код Хаффмана**: это жадный алгоритм для сжатия данных без потерь. Построив дерево Хаффмана и каждый раз объединяя два узла с наименьшей частотой, мы получаем дерево с минимальной взвешенной длиной пути, то есть минимальной длиной кодирования.
|
|
- **Алгоритм Дейкстры**: это жадный алгоритм решения задачи о кратчайших путях от заданной исходной вершины до всех остальных вершин.
|