Жадные алгоритмы: подробный разбор с примерами
Почему жадные алгоритмы важны?
Жадные алгоритмы — это фундаментальный подход к решению оптимизационных задач, который позволяет:
- 🧠 Получать быстрые решения сложных оптимизационных проблем
- ⚡ Существенно сокращать время работы по сравнению с полным перебором
- 🎯 Находить точные решения для определённого класса задач
- 🔍 Получать приближённые ответы с гарантированной точностью
- 🌟 Разрабатывать интуитивно понятные и легко реализуемые алгоритмы
Принцип работы жадных алгоритмов
Основная идея:
Жадный алгоритм строит решение пошагово, выбирая на каждом шаге локально оптимальное решение (максимально выгодное в текущий момент), не пересматривая сделанных ранее выборов. В отличие от динамического программирования или полного перебора, жадный алгоритм никогда не отменяет принятых решений.
Ключевые компоненты:
- Функция выбора: Определяет, какой элемент выбрать на текущем шаге
- Критерий осуществимости: Проверяет, можно ли добавить элемент к текущему решению
- Функция цели: Определяет оптимальность полученного решения
Когда работают жадные алгоритмы?
Жадные алгоритмы гарантированно находят оптимальное решение, если задача обладает двумя свойствами:
- Свойство жадного выбора: Локально оптимальный выбор приводит к глобально оптимальному решению
- Свойство оптимальной подструктуры: Оптимальное решение задачи содержит оптимальные решения подзадач
Пример 1: Задача о выборе активностей
Формулировка задачи:
Дан набор из n активностей с временем начала и окончания: [start₁, end₁], [start₂, end₂], ..., [startₙ, endₙ]
. Необходимо выбрать максимальное количество не пересекающихся по времени активностей.
Пример входных данных:
Активности:
1: [1, 4] # Начало: 1, Окончание: 4
2: [3, 5] # Начало: 3, Окончание: 5
3: [0, 6] # Начало: 0, Окончание: 6
4: [5, 7] # Начало: 5, Окончание: 7
5: [3, 9] # Начало: 3, Окончание: 9
6: [5, 9] # Начало: 5, Окончание: 9
7: [6, 10] # Начало: 6, Окончание: 10
8: [8, 11] # Начало: 8, Окончание: 11
9: [8, 12] # Начало: 8, Окончание: 12
10: [2, 14] # Начало: 2, Окончание: 14
Жадный подход к решению:
- Отсортировать активности по времени окончания
- Выбрать первую активность
- Последовательно выбирать каждую следующую активность, которая не пересекается с уже выбранными
function selectActivities(activities) { // Сортируем активности по времени окончания activities.sort((a, b) => a[1] - b[1]) const selected = [activities[0]] // Выбираем первую активность let lastEndTime = activities[0][1] // Время окончания последней выбранной активности for (let i = 1; i < activities.length; i++) { const [start, end] = activities[i] // Если текущая активность начинается после окончания последней выбранной if (start >= lastEndTime) { selected.push(activities[i]) lastEndTime = end } } return selected } // Пример использования const activities = [ [1, 4], [3, 5], [0, 6], [5, 7], [3, 9], [5, 9], [6, 10], [8, 11], [8, 12], [2, 14], ] const selectedActivities = selectActivities(activities) console.log(selectedActivities) // Вывод: [[1, 4], [5, 7], [8, 11]]
Почему жадный алгоритм здесь работает?
- Свойство жадного выбора: Выбор активности с самым ранним временем окончания всегда позволяет оставить максимальное время для оставшихся активностей
- Оптимальная подструктура: После выбора первой активности, задача сводится к выбору максимального числа непересекающихся активностей из оставшихся
Временная сложность: O(n log n) из-за сортировки
Пространственная сложность: O(n) для хранения отсортированного массива и результата
Пример 2: Задача о размене монет
Формулировка задачи:
Дан набор номиналов монет и сумма денег. Необходимо разменять данную сумму минимальным количеством монет.
Пример входных данных:
Номиналы монет: [1, 5, 10, 25] (центы)
Сумма: 63 цента
Жадный подход к решению:
- Отсортировать номиналы монет по убыванию
- Последовательно брать максимально возможное количество монет каждого номинала
function coinChange(coins, amount) { // Сортируем номиналы по убыванию coins.sort((a, b) => b - a) let remainingAmount = amount let coinCount = 0 const usedCoins = {} for (const coin of coins) { // Количество монет текущего номинала const count = Math.floor(remainingAmount / coin) if (count > 0) { usedCoins[coin] = count coinCount += count remainingAmount -= coin * count } } // Проверяем, удалось ли разменять всю сумму if (remainingAmount === 0) { return { coinCount, usedCoins } } else { return 'Невозможно разменять сумму данными номиналами' } } // Пример использования const coins = [1, 5, 10, 25] const amount = 63 const result = coinChange(coins, amount) console.log(result) // Вывод: { coinCount: 6, usedCoins: { '25': 2, '10': 1, '1': 3 } }
Особенности этой задачи:
-
Частный случай: Жадный алгоритм дает оптимальное решение для стандартных номиналов монет США [1, 5, 10, 25], но не гарантирует оптимальность для произвольного набора номиналов
-
Контрпример: Для номиналов [1, 3, 4] и суммы 6, жадный алгоритм дает решение из 3 монет (4 + 1 + 1), хотя оптимальное решение — 2 монеты (3 + 3)
Временная сложность: O(n log n + amount/minCoin) в худшем случае
Пространственная сложность: O(n)
Пример 3: Дробная задача о рюкзаке
Формулировка задачи:
Дан набор предметов, каждый с весом и ценностью. Необходимо заполнить рюкзак ограниченной вместимости предметами так, чтобы их суммарная ценность была максимальной. В отличие от 0/1 задачи о рюкзаке, здесь можно брать дробные части предметов.
Пример входных данных:
Предметы: [(10, 60), (20, 100), (30, 120)] (вес, ценность)
Вместимость рюкзака: 50
Жадный подход к решению:
- Вычислить удельную ценность (ценность/вес) для каждого предмета
- Отсортировать предметы по удельной ценности по убыванию
- Последовательно добавлять предметы в рюкзак, начиная с наиболее ценных за единицу веса
function fractionalKnapsack(items, capacity) { // Вычисляем удельную ценность для каждого предмета const valuePerWeight = items.map((item, index) => ({ index, weight: item[0], value: item[1], ratio: item[1] / item[0], })) // Сортируем по удельной ценности по убыванию valuePerWeight.sort((a, b) => b.ratio - a.ratio) let totalValue = 0 let remainingCapacity = capacity const selectedItems = [] for (const item of valuePerWeight) { // Если можем взять весь предмет if (item.weight <= remainingCapacity) { selectedItems.push({ index: item.index, weight: item.weight, value: item.value, fraction: 1, }) totalValue += item.value remainingCapacity -= item.weight } else { // Берем только часть предмета const fraction = remainingCapacity / item.weight selectedItems.push({ index: item.index, weight: item.weight * fraction, value: item.value * fraction, fraction, }) totalValue += item.value * fraction break // Рюкзак заполнен } } return { totalValue, selectedItems } } // Пример использования const items = [ [10, 60], [20, 100], [30, 120], ] const capacity = 50 const result = fractionalKnapsack(items, capacity) console.log(result) // Вывод: { totalValue: 240, selectedItems: [...] }
Почему жадный алгоритм здесь работает?
- Свойство жадного выбора: Взятие предмета с максимальной удельной ценностью на каждом шаге приводит к оптимальному решению
- Возможность дробления: Ключевое отличие от 0/1 задачи о рюкзаке, где жадный алгоритм не оптимален
Временная сложность: O(n log n) из-за сортировки
Пространственная сложность: O(n)
Пример 4: Алгоритм Хаффмана для сжатия данных
Формулировка задачи:
Дан набор символов и их частоты. Необходимо построить префиксный код с минимальной средней длиной.
Жадный подход к решению:
- Создать очередь с приоритетами, содержащую все символы и их частоты
- Последовательно извлекать два узла с наименьшими частотами и объединять их в новый узел
- Добавлять новый узел обратно в очередь
- Повторять, пока в очереди не останется один узел — корень дерева Хаффмана
class Node { constructor(char, freq, left = null, right = null) { this.char = char this.freq = freq this.left = left this.right = right this.code = '' } } function buildHuffmanTree(text) { // Подсчитываем частоты символов const frequencies = {} for (const char of text) { frequencies[char] = (frequencies[char] || 0) + 1 } // Создаем узлы для каждого символа const priorityQueue = Object.entries(frequencies) .map(([char, freq]) => new Node(char, freq)) .sort((a, b) => a.freq - b.freq) // Строим дерево Хаффмана while (priorityQueue.length > 1) { // Извлекаем два узла с наименьшими частотами const left = priorityQueue.shift() const right = priorityQueue.shift() // Создаем новый внутренний узел const newNode = new Node(null, left.freq + right.freq, left, right) // Вставляем новый узел в очередь с сохранением порядка let i = 0 while (i < priorityQueue.length && priorityQueue[i].freq < newNode.freq) { i++ } priorityQueue.splice(i, 0, newNode) } // Корень дерева Хаффмана return priorityQueue[0] } // Генерация кодов для каждого символа function generateCodes(node, prefix = '') { if (node) { // Если это лист (символьный узел) if (node.char) { node.code = prefix return { [node.char]: prefix } } // Рекурсивно обходим дерево const leftCodes = generateCodes(node.left, prefix + '0') const rightCodes = generateCodes(node.right, prefix + '1') return { ...leftCodes, ...rightCodes } } return {} } // Функция кодирования текста function huffmanEncode(text) { const root = buildHuffmanTree(text) const codes = generateCodes(root) let encodedText = '' for (const char of text) { encodedText += codes[char] } return { encodedText, codes, root } } // Пример использования const text = 'this is an example for huffman encoding' const { encodedText, codes } = huffmanEncode(text) console.log('Коды Хаффмана:', codes) console.log('Закодированный текст:', encodedText)
Почему жадный алгоритм здесь работает?
- Свойство жадного выбора: Объединение символов с наименьшими частотами минимизирует суммарную длину кода
- Оптимальная подструктура: Оптимальный код для объединенных символов является частью глобально оптимального решения
Временная сложность: O(n log n) для построения дерева Хаффмана
Пространственная сложность: O(n)
Пример 5: Задача составления расписания для минимизации среднего времени завершения
Формулировка задачи:
Даны n задач с временем выполнения. Необходимо определить порядок их выполнения, минимизирующий среднее время завершения.
Жадный подход к решению:
Выполнять задачи в порядке возрастания времени их выполнения (Кратчайшая работа первой — SJF)
function minimizeCompletionTime(tasks) { // Сортируем задачи по времени выполнения tasks.sort((a, b) => a - b) let currentTime = 0 let totalCompletionTime = 0 // Вычисляем время завершения каждой задачи for (const taskTime of tasks) { currentTime += taskTime totalCompletionTime += currentTime } return { schedule: tasks, averageCompletionTime: totalCompletionTime / tasks.length, } } // Пример использования const taskTimes = [3, 1, 4, 2, 6] const result = minimizeCompletionTime(taskTimes) console.log('Оптимальное расписание:', result.schedule) console.log('Среднее время завершения:', result.averageCompletionTime) // Вывод: Оптимальное расписание: [1, 2, 3, 4, 6] // Среднее время завершения: 7.4
Почему жадный алгоритм здесь работает?
- Математическое доказательство: Можно доказать, что при переупорядочивании двух соседних задач с временами t₁ и t₂, где t₁ > t₂, всегда выгоднее поставить более короткую задачу раньше
- Принцип: Минимизация времени выполнения наибольшего числа задач
Временная сложность: O(n log n) из-за сортировки
Пространственная сложность: O(n)
Когда жадные алгоритмы НЕ работают?
Жадные алгоритмы не всегда находят оптимальное решение. Классические примеры:
1. 0/1 Задача о рюкзаке
Для варианта, где нельзя брать дробные части предметов, жадный выбор не гарантирует оптимальности.
Контрпример: Предметы с весами и ценностями [(10, 60), (100, 120), (20, 100)], вместимость рюкзака 120.
- Жадный алгоритм выберет 1-й и 3-й предметы с суммарной ценностью 160
- Оптимальное решение — 2-й и 3-й предметы с суммарной ценностью 220
2. Поиск кратчайшего пути в графе
При наличии отрицательных весов рёбер жадный подход (как в алгоритме Дейкстры) может не найти кратчайший путь.
3. Задача о построении минимального дерева Штейнера
Жадное соединение ближайших пар вершин не всегда даёт минимальное связующее дерево для заданного набора вершин.
Практические применения жадных алгоритмов
1. Компьютерные сети и маршрутизация
- Алгоритм Дейкстры для поиска кратчайшего пути
- Алгоритм Крускала для построения минимального остовного дерева
2. Сжатие данных
- Алгоритм Хаффмана для энтропийного кодирования
- LZ77 и другие словарные методы сжатия
3. Планирование и оптимизация ресурсов
- Алгоритмы планирования процессов в операционных системах
- Управление памятью и дисковым пространством
4. Финансовые приложения
- Алгоритмы размена монет в банкоматах
- Оптимизация портфеля ценных бумаг
5. Машинное обучение
- Жадные алгоритмы в решающих деревьях
- Алгоритмы градиентного спуска для оптимизации
Рекомендации по разработке жадных алгоритмов
-
Формулировка жадного критерия: Определите, по какому правилу будете выбирать элементы (например, "самый ранний конец", "наибольшая ценность на единицу веса")
-
Доказательство корректности: Убедитесь, что жадный выбор приводит к глобальному оптимуму (часто требует математического доказательства)
-
Проверка на контрпримерах: Попробуйте найти случаи, где жадный алгоритм не даёт оптимального решения
-
Оптимизация реализации: Используйте эффективные структуры данных (например, очередь с приоритетом)
-
Тестирование: Проверьте алгоритм на крайних случаях и больших наборах данных
Заключение
Жадные алгоритмы представляют собой мощный инструмент для решения оптимизационных задач, обеспечивая баланс между эффективностью и простотой реализации. Хотя они применимы не ко всем задачам, понимание принципов жадных алгоритмов и условий их корректности является фундаментальным навыком в арсенале разработчика алгоритмов.
Ключевые выводы:
- 🧩 Жадные алгоритмы принимают локально оптимальные решения на каждом шаге
- 🔎 Для корректности необходимы свойства жадного выбора и оптимальной подструктуры
- ⚖️ Жадные алгоритмы обычно проще и эффективнее динамического программирования, но применимы к меньшему числу задач
- 🧪 Всегда проверяйте применимость жадного подхода, ищите контрпримеры
- 🚀 В подходящих задачах жадные алгоритмы дают идеальный баланс между простотой и эффективностью
Владение жадными алгоритмами и понимание их границ применимости — важнейший навык, позволяющий эффективно решать широкий класс оптимизационных задач в программировании и других областях. 🌟
Для использования AI-помощника необходимо авторизоваться
Примеры вопросов:
Мы создали идеальную платформу для подготовки к собеседованиям. Никакого стресса – только уверенность в своих силах!

Никакого спама – только полезный контент для твоего роста!