Рубрики
Без рубрики

Соберите это 💰 жадные алгоритмы в JavaScript

Обзор Одна менее понятная идея среди инженеров JavaScript (если вы не оказались … Tagged с JavaScript, Node, WebDev, обсудите.

Обзор

Одной из менее понятной идеи среди инженеров JavaScript (если только вы не учитесь на собеседованиях) является использование жадных алгоритмов. A жадный алгоритм Делает любой выбор, кажется лучшим в данный момент, и решает подзадачи, которые возникают позже. Чтобы использовать визуальную метафору, мы поместили результат каждой подзадачи в «сумке», а затем повторяем с последовательно меньшими подзадаченными. Когда подзадача пуста (нечего делать), мы возвращаем содержимое сумки.

Оказывается, эта стратегия может привести к некоторым очень элегантным решениям практических проблем. В остальной части этой статьи мы рассмотрим четыре, казалось бы, разные проблемы, которые имеют почти идентичные решения (подсказка: все они используют жадный алгоритм). В заключение мы рассмотрим более внимательную структуру, общую для всех четырех проблем. Давайте погрузимся!

Пример: Проблема смены монеты

You are given coins of different denominations and a total amount of 
money. Write a function that returns the smallest set of coins that 
sums to that amount.

Найдите минутку, чтобы подумать о том, как вы сделаете это, прежде чем продолжить … (ответ прямо ниже)

function makeChange(amount, coins, bag = []) {
  if (amount === 0) return bag
  let largestCoin = getLargestCoin(amount, coins)
  return makeChange(amount - largestCoin, coins, bag.concat([largestCoin]))
}

function getLargestCoin(amount, coins) {
  let sortedCoins = coins.sort((a, b) =\> a - b)
  for (let i = sortedCoins.length - 1; i \>= 0; i--) {
    if (sortedCoins[i] \<= amount) return sortedCoins[i]
  }
  throw new Error('no coin that divides amount')
}

console.log(
  makeChange(42, [1, 5, 10, 25])
)
// [25, 10, 5, 1, 1]

Мы держим «сумку» монет и рекурсивно добавляем монеты в сумку, которая соответствует нашим критериям отбора (Выберите самую большую деноминацию монеты <сумма) . Если самая большая монета имеет ценность В , мы добавляем C в сумку и позвонить MakeChange с Сумма - C Анкет Это продолжается до сумма равно 0, а мешок монет возвращается.

Быстрая заметка о выражении {... сумка, ... {[fn (array [0])]: matches}} Так как там много всего происходит. Прежде всего, что делает {... a, ... b} иметь в виду? Это называется Распространение объекта Анкет Думайте об этом как о Smoshing вместе объектов A и B, чтобы создать новый объект. Так {... Сумка, ... что -то еще} объединит объект сумка с объектом что -то Анкет В этом случае что -то объект {[fn (array [0])]: matches} которая является новой группой, которую мы вставляем в сумку.

Я также объясню разницу между {[Key]: значение} и {ключ: значение} Анкет Эти квадратные скобки означают Вычисленные свойства Анкет Вы можете прикрепить любое выражение между квадратными скобками, и значение этого выражения станет значением ключа. Так например {[1 + 1]: 2} будет таким же, как {2: 2} Анкет

Пример: Groupby

Implement the "groupBy" function which takes an array A and a function F,
and returns an object composed of keys generated from the results of 
running each element of A through F. The corresponding value of each key 
is an array of elements responsible for generating the key.

Найдите минутку, чтобы подумать о том, как вы сделаете это, прежде чем продолжить … (ответ прямо ниже)

/*
  input: [6.1, 4.2, 6.3]
  function: Math.floor
  output: { '4': [4.2], '6': [6.1, 6.3] }
*/

function groupBy(array, fn, bag = {}) {
  if (array.length === 0) return bag
  let matches = array.filter(x =\> fn(x) === fn(array[0]))
  let rest = array.filter(x =\> fn(x) !== fn(array[0]))
  return (
    groupBy(
    rest,
    fn,
    { ...bag, ...{ [fn(array[0])]: matches } }
    )
  )
}

console.log(
  groupBy([6.1, 4.2, 6.3], Math.floor)
)
// { '4': [4.2], '6': [6.1, 6.3] }

Держите «мешок» групп и рекурсивно добавьте группы в сумку, которые соответствуют нашим критериям отбора fn (x) (массив [0]) . Тогда позвоните GroupBy На оставшихся элементах с обновленной сумкой. Это продолжается до тех пор, пока оригинальный массив не станет пустым, а сумка не будет возвращена.

Пример: проблема выбора активности

Еще одна классическая проблема – Проблема выбора деятельности Анкет

Imagine you are trying to schedule a room for multiple competing events, 
each having its own time requirements (start and end time). How do you 
schedule the room such that you can host the maximum number of events 
with no scheduling conflicts?

Найдите минутку, чтобы подумать о том, как вы сделаете это, прежде чем продолжить … (ответ прямо ниже)

class Appointment {
  constructor(name, from, to) {
    this.name = name
    this.from = from
    this.to = to
  }
}

// push new appointments onto bag one-by-one until no more appointments are left
function getMaxAppointments(appointments, bag = []) {
  if (appointments.length === 0) return bag
  let selectedAppointment = appointments.sort((a, b) =\> a.to - b.to)[0] // sort from earliest end to latest end
  let futureCandidates = appointments.filter(a =\> a.from \> selectedAppointment.to)
  return getMaxAppointments(
    futureCandidates,
    bag.concat([selectedAppointment])
  )
}

let a1 = new Appointment('brush teeth', 0, 2)
let a2 = new Appointment('wash face', 1, 3)
let a3 = new Appointment('make coffee', 3, 5)
let a4 = new Appointment('blowdry hair', 3, 4)
let a5 = new Appointment('take shower', 4.5, 6)
let a6 = new Appointment('eat cereal', 7, 10)

console.log(
  getMaxAppointments([a1, a2, a3, a4, a5, a6]).map(a =\> a.name)
) 
// ['brush teeth', 'blowdry hair', 'take shower', 'eat cereal']

Пример: собрать анаграммы

Для нашего последнего примера мы рассмотрим проблему Группирование анаграмм Анкет

Given an array of strings, group anagrams together.

For example:
Input: ["eat", "tea", "tan", "ate", "nat", "bat"],
Output:
[
  ["ate","eat","tea"],
  ["nat","tan"],
  ["bat"]
]

Найдите минутку, чтобы подумать о том, как вы сделаете это, прежде чем продолжить … (ответ прямо ниже)

function collectAnagrams(words, bag = []) {
  if (words.length === 0) return bag
  let matches = words.filter(w =\> isAnagram(w, words[0]))
  let rest = words.filter(w =\> !isAnagram(w, words[0]))
  return collectAnagrams(
    rest,
    bag.concat([matches])
  )
}

function stringSorter(a, b) { return a.localeCompare(b) }

function isAnagram(a, b) {
  let aSorted = a.toLowerCase().split('').sort(stringSorter).join('')
  let bSorted = b.toLowerCase().split('').sort(stringSorter).join('')
  return aSorted === bSorted
}

let x = ['bag', 'gab', 'foo', 'abg', 'oof', 'bum']
console.log(collectAnagrams(x))
// [['bag', 'gab', 'abg'], ['foo', 'oof'], ['bum']]

Общая структура

Так что же общего у всех этих проблем? Для каждой итерации через цикл мы выбираем подмножество элементов с ввода и добавляем его в сумку. Оставшиеся элементы проводятся до следующей итерации петли в качестве следующего входа. Когда вход пуст, мы возвращаем сумку.

Следующая диаграмма может помочь прояснить вещи, используя наш пример GroupBy:

Если вам более удобно с псевдокодом, вот шаблон, который мы использовали во всех предыдущих примерах:

function bagItUp(things, bag = []) {
  if (things is empty) return bag
  let thingsToPutInBag = ...
  let restOfThings = ...
  return bagItUp(
    restOfThings,
    bag + thingsToPutInBag
  ) 
}

Соединять

Что вы думаете? Вы решили аналогичные проблемы на работе или личные проекты, используя жадные алгоритмы? Дайте мне знать в комментариях ниже или на Twitter !

Оригинал: “https://dev.to/albertywu/bag-it-up–greedy-algorithms-in-javascript-3gac”