ДокументацияАлгоритмыЖадные алгоритмы и задача о рюкзаке
Средний 12 мин чтения

Жадные алгоритмы и задача о рюкзаке

Что такое жадные алгоритмы, когда они дают оптимальное решение. Разбор задач: размен монет, задача о рюкзаке, интервалы, Huffman coding, Huffman, оптимизация.

жадные алгоритмыgreedyрюкзакинтервалыразмен монеталгоритмыоптимизация

Что такое жадный алгоритм

Жадный алгоритм на каждом шаге делает локально оптимальный выбор — берёт то, что выглядит лучше прямо сейчас. Не оглядывается назад, не заглядывает вперёд.

Иногда это даёт глобально оптимальное решение. Иногда — нет. Разница между «жадный работает» и «жадный не работает» — ключевой момент.

Когда жадный алгоритм оптимален

Жадный подход даёт правильный ответ, если задача обладает двумя свойствами:

  1. Жадный выбор — локально оптимальный выбор приводит к глобально оптимальному решению
  2. Оптимальная подструктура — оптимальное решение содержит оптимальные решения подзадач

Доказать, что жадный алгоритм работает — сложнее, чем написать его. На практике: если не уверены — сравните с DP-решением на тестах.

Задача: размен монет (жадная версия)

Даны номиналы монет. Набрать сумму минимальным количеством монет.

function greedyCoins(coins, amount) {
  const sorted = [...coins].sort((a, b) => b - a)
  let remaining = amount
  const result = {}

  for (const coin of sorted) {
    const count = Math.floor(remaining / coin)
    if (count > 0) {
      result[coin] = count
      remaining -= coin * count
    }
  }

  return remaining === 0 ? result : null
}

greedyCoins([25, 10, 5, 1], 63) // { 25: 2, 10: 1, 1: 3 } → 6 монет

Берём самую большую монету, сколько влезет. Потом следующую. И так до нуля.

Когда жадный подход ломается

greedyCoins([1, 3, 4], 6)
// Жадный: 4 + 1 + 1 = 3 монеты
// Оптимально: 3 + 3 = 2 монеты

Жадный алгоритм не всегда даёт минимум. Для надёжного решения нужна DP (см. статью про динамическое программирование).

Задача: рюкзак (дробный)

Отличие от классического рюкзака: предметы можно брать частично. Разрезать — можно.

function fractionalKnapsack(items, capacity) {
  const sorted = [...items].sort(
    (a, b) => (b.value / b.weight) - (a.value / a.weight)
  )

  let totalValue = 0
  let remaining = capacity

  for (const item of sorted) {
    if (remaining <= 0) break

    const take = Math.min(item.weight, remaining)
    totalValue += (item.value / item.weight) * take
    remaining -= take
  }

  return totalValue
}

const items = [
  { value: 60, weight: 10 },
  { value: 100, weight: 20 },
  { value: 120, weight: 30 },
]

fractionalKnapsack(items, 50) // 240

Сортируем по удельной стоимости (value/weight) и берём начиная с самой выгодной. Для дробного рюкзака жадный подход оптимален. Для 0/1 рюкзака — нет (нужна DP).

Задача: непересекающиеся интервалы

Выбрать максимальное количество непересекающихся интервалов:

function maxIntervals(intervals) {
  const sorted = [...intervals].sort((a, b) => a[1] - b[1])

  let count = 0
  let end = -Infinity

  for (const [start, finish] of sorted) {
    if (start >= end) {
      count++
      end = finish
    }
  }

  return count
}

maxIntervals([[1, 3], [2, 4], [3, 5], [0, 6], [5, 7], [8, 9]])
// 4 интервала: [1,3], [3,5], [5,7], [8,9]

Сортируем по правому краю, берём интервалы которые начинаются после окончания предыдущего. Жадный выбор — брать интервал, который заканчивается раньше.

Задача: минимальное количество платформ

Поезда приезжают и уезжают. Сколько нужно платформ, чтобы все поместились:

function minPlatforms(arrivals, departures) {
  arrivals.sort((a, b) => a - b)
  departures.sort((a, b) => a - b)

  let platforms = 0
  let maxPlatforms = 0
  let i = 0
  let j = 0

  while (i < arrivals.length) {
    if (arrivals[i] <= departures[j]) {
      platforms++
      maxPlatforms = Math.max(maxPlatforms, platforms)
      i++
    } else {
      platforms--
      j++
    }
  }

  return maxPlatforms
}

minPlatforms([900, 940, 950, 1100], [910, 1200, 1120, 1130]) // 3

Два указателя по прибытиям и отправлениям. Прибыл раньше отправления — нужна ещё платформа.

Задача: прыжки (Jump Game)

Можно ли добраться до конца массива, где каждый элемент — максимальная длина прыжка:

function canJump(nums) {
  let maxReach = 0

  for (let i = 0; i < nums.length; i++) {
    if (i > maxReach) return false
    maxReach = Math.max(maxReach, i + nums[i])
  }

  return true
}

canJump([2, 3, 1, 1, 4]) // true
canJump([3, 2, 1, 0, 4]) // false (застреваем на индексе 3)

Храним максимальную достижимую позицию. Если текущий индекс недостижим — нельзя дойти.

Минимальное количество прыжков

function minJumps(nums) {
  let jumps = 0
  let currentEnd = 0
  let farthest = 0

  for (let i = 0; i < nums.length - 1; i++) {
    farthest = Math.max(farthest, i + nums[i])

    if (i === currentEnd) {
      jumps++
      currentEnd = farthest
    }
  }

  return jumps
}

minJumps([2, 3, 1, 1, 4]) // 2 (прыжок на индекс 1, потом на 4)

Задача: Huffman coding

Сжатие данных: частые символы — короткие коды, редкие — длинные.

class HuffmanNode {
  constructor(char, freq) {
    this.char = char
    this.freq = freq
    this.left = null
    this.right = null
  }
}

function huffmanBuild(freqMap) {
  let nodes = Object.entries(freqMap).map(
    ([char, freq]) => new HuffmanNode(char, freq)
  )

  while (nodes.length > 1) {
    nodes.sort((a, b) => a.freq - b.freq)

    const left = nodes.shift()
    const right = nodes.shift()

    const parent = new HuffmanNode(null, left.freq + right.freq)
    parent.left = left
    parent.right = right

    nodes.push(parent)
  }

  return nodes[0]
}

function huffmanCodes(node, prefix = '', codes = {}) {
  if (node.char !== null) {
    codes[node.char] = prefix
    return codes
  }
  huffmanCodes(node.left, prefix + '0', codes)
  huffmanCodes(node.right, prefix + '1', codes)
  return codes
}

const tree = huffmanBuild({ a: 5, b: 9, c: 12, d: 13, e: 16, f: 45 })
huffmanCodes(tree)
// { f: '0', c: '100', d: '101', a: '1100', b: '1101', e: '111' }

На каждом шаге берём два узла с наименьшей частотой, объединяем. Жадный выбор — всегда брать два самых редких.

Жадный vs DP

КритерийЖадныйDP
СложностьОбычно O(n log n)O(n × W) или O(n²)
ОптимальностьНе всегдаВсегда
ПростотаПрощеСложнее
Когда использоватьКогда можно доказать корректностьКогда жадный не работает

Итог

  • Жадный алгоритм — берём лучшее на каждом шаге, не оглядываясь
  • Работает не всегда: размен монет с 1,3,4 и суммой 6 — пример ошибки
  • Для дробного рюкзака — оптимален. Для 0/1 рюкзака — нужен DP
  • Интервалы: сортировка по правому краю + жадный выбор
  • Если сомневаетесь — проверяйте жадное решение на контрпримерах
  • Jump Game — жадный подход «максимальная достижимая позиция»