awesome-everything EN
↑ Обратно к восхождению

Алгоритмы с нуля

Greedy: чтение кода

Суть Чтение реальных greedy-фрагментов: interval-планировщик с неверным ключом сортировки, greedy, проигрывающий DP, и проход Gas Station. Предскажите поведение и выберите фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Баги greedy тихие: код выполняется, возвращает что-то правдоподобное и ошибается только на тех входах, которые вы не протестировали. Читайте каждый фрагмент как на ревью — найдите выбор, который не является доказуемо безопасным, и выберите фикс, который senior-инженер делает первым.

Цель

Отработайте цикл, который вы запускаете на каждом greedy: прочитать локальный выбор, решить, доказуемо ли он оптимален, и потянуться к правильному ключу сортировки, правильному инварианту или к DP — прежде чем доверять результату.

Фрагмент 1 — interval-планировщик

// Цель: выбрать МАКСИМУМ непересекающихся интервалов.
function maxNonOverlapping(intervals) {
  const sorted = [...intervals].sort((a, b) => a[0] - b[0]); // сортировка по СТАРТУ
  let count = 0, lastEnd = -Infinity;
  for (const [start, end] of sorted) {
    if (start >= lastEnd) { count++; lastEnd = end; }
  }
  return count;
}
// maxNonOverlapping([[1, 10], [2, 3], [4, 5]])  ->  ?
Викторина

Что вернётся на [[1,10], [2,3], [4,5]] и каков фикс в одну строку?

Фрагмент 2 — greedy, проигрывающий DP

// «Минимум монет для amount», всегда беря самую крупную подходящую монету.
function greedyCoins(coins, amount) {
  const sorted = [...coins].sort((a, b) => b - a);
  let left = amount, count = 0;
  for (const c of sorted) {
    while (left >= c) { left -= c; count++; }
  }
  return left === 0 ? count : Infinity;
}
// greedyCoins([1, 5, 6, 9], 11)  ->  ?   (DP-оптимум равен 2: 5 + 6)
Викторина

greedyCoins([1,5,6,9], 11) возвращает 3 (9+1+1), но DP-оптимум равен 2 (5+6). Какое утверждение корректно диагностирует это?

Фрагмент 3 — проход Gas Station

function canCompleteCircuit(gas, cost) {
  let total = 0, tank = 0, start = 0;
  for (let i = 0; i < gas.length; i++) {
    const diff = gas[i] - cost[i];
    total += diff;
    tank += diff;
    if (tank < 0) { start = i + 1; tank = 0; }
  }
  return total >= 0 ? start : -1;
}
Викторина

Ревьюер хочет удалить переменную total и возвращать start напрямую, аргументируя, что сброс и так находит ответ. Почему total должен остаться?

Фрагмент 4 — граница цикла Jump Game II

function jump(nums) {
  let jumps = 0, curEnd = 0, farthest = 0;
  for (let i = 0; i < nums.length; i++) {   // цикл до n-1
    farthest = Math.max(farthest, i + nums[i]);
    if (i === curEnd) { jumps++; curEnd = farthest; }
  }
  return jumps;
}
// jump([2, 3, 1, 1, 4])  ->  ?   (истинный минимум равен 2)
Викторина

Это возвращает 3 на [2,3,1,1,4], но минимум равен 2. В чём ошибка на единицу и каков фикс?

Итог

Каждый greedy читается одинаково: назовите локальный выбор, затем спросите, доказуемо ли он безопасен. Фрагмент 1 использовал неверный ключ сортировки (старт вместо финиша) для максимизации; фрагмент 2 применил greedy на наборе номиналов без greedy choice property, где корректен только DP; фрагмент 3 показал, что глобальный инвариант выполнимости (total >= 0) несущий и отделён от локального сброса; фрагмент 4 был классической ошибкой на единицу в Jump Game II (цикл до n-2, никогда не прыгать из цели). Диагностируйте небезопасный выбор, затем чините ключ, инвариант или откатывайтесь к DP.

Продолжить восхождение ↑Greedy: планировщик, который можно доказать
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.