В своем проекте по обогащению у старших я решил ответить на распространенный, но сложный вопрос собеседования: как решить задачу о рюкзаке 0–1:

Предположим, у нас есть набор из n элементов, каждый с заданным неотрицательным значением wi и неотрицательным значением vi, где i = 1,…, n. Нам дается рюкзак грузоподъемностью Вт. Найдите максимальное значение для подмножества элементов, общий вес которых не превышает W.

Решение грубой силы

Чтобы найти метод грубой силы для решения этой проблемы, я подумал о том, какой будет наиболее прямой подход. Я рассудил, что было бы проверить все возможные подмножества элементов, общий вес которых ≤ W, и получить набор с максимальным значением.

Каждый элемент в наборе данных элементов имеет две возможности: он либо входит в оптимальное подмножество, либо нет. В начале задачи в рюкзаке нет предметов, и поэтому любой предмет, вес которого ≤ W, может быть помещен в рюкзак. Это означает, что любой предмет, добавленный впоследствии в рюкзак, должен быть ≤ W -Sum (w1 +… + wm), где m = количество предметы в рюкзаке. Когда больше нет предметов для исследования, возвращается сумма значений предметов в рюкзаке. Таким образом, решение методом перебора решается с использованием рекурсии:

function maxKnapsack(values, weights, W, i) {
    if (W === 0 || i === 0) {
         return 0;
    if (weights[i] > W)
         return maxKnapsack(values, weights, W, i-1);
    else
         let included = values[i] + maxKnapsack(values, weights, W-weights[i], i-1);
         let excluded = maxKnapsack(values, weights, W, i-1);
         return Math.max(included, excluded);
}

В дополнение к значениям и весам элементов, а также допустимому весу, функция должна иметь возможность принимать значение индекса в качестве параметра (эта функция предполагает, что начальное значение начинается с items.length-1, но это может быть переписан так, чтобы он начинался с 0 и заканчивался на items.length). Поскольку не гарантируется, что элемент будет включен в оптимальное решение, если он ≤ текущей грузоподъемности, возвращаемое значение является максимальным между результатом, когда элемент включен в решение, и результатом, когда элемент не включен. включены в раствор. По сути, эта функция разбивает каждую подзадачу на две более мелкие подзадачи и оценивает их. Это означает, что в наихудшем сценарии количество вызовов функции равно 2 ^ n, что означает, что решение методом перебора имеет временную сложность O (2 ^ n). Хотя это не проблема, когда n мало, когда n становится большим, время, необходимое для поиска оптимального значения, становится непомерно высоким.

Оптимизированное решение

Причина низкой эффективности времени выполнения для решения методом грубой силы заключалась в том, что ему необходимо было проверить каждое возможное подмножество элементов, общий вес которых находился в пределах ограничений. Это часто означало решение подзадач более одного раза, поскольку они перекрывались, поскольку мы не хранили ответы, и у нас не было никаких средств их переноса. Таким образом, нам нужен какой-то кеш для мемоизации.

Важно помнить, что сумма весов элементов, используемых в оптимальном решении, не обязательно должна быть в точности равной W; сумма весов также может быть ‹W. Таким образом, другим способом анализа подмножества является создание двумерной таблицы оптимальных значений, где ось Y представляет максимальное количество предметов, разрешенных в рюкзаке и указателе, а ось X представляет значение W. Чтобы помочь в визуализации, предположим, что нам даны следующие данные:

  • items = [{значение: 6, вес: 1}, {значение: 10, вес: 2}, {значение: 12, вес: 3}]
  • W = 5

Таблица ниже будет выглядеть примерно так:

   0  1  2  3  4  5
0  0  0  0  0  0  0
1  0  6  6  6  6  6
2  0  6  10 16 16 16
3  0  6  10 16 18 22

Элемент в нижней и самой правой ячейке - это значение оптимального подмножества предметов для рюкзака. Оптимальное решение - создать таблицу, в которой каждое значение в столбце каждой строки равно max (значение соответствующего значения элемента индекса + наибольшее значение в таблице, чей соответствующий вес элемента + вес текущего элемента ≤ W, значение ячейки в предыдущей строке и том же столбце). Когда он будет полностью заполнен, верните самое нижнее правое значение.

function maxKnapsack(items, W) {
     let cache = [];
     for (g = 0; g < items.length+1; g++){
          cache[g] = [];
          for (h = 0; h < W+1; h++) {
               cache[g][h] = 0;
          }
     }
     let weights = items.map(item => item.weight);
     let values = items.map(item => item.value);
     for (let i = 0; i < items.length+1; i++) {
          for (let j = 0; j < W+1; j++) {
               if (i ==== 0 || j === 0)
                    cache[i][j] = 0;
               else if (weights[i-1] <= j) {
                    let included = values[i-1] + cache[i-1][j-weights[i-1]];
                    let excluded = cache[i-1][j];
                    cache[i][j] = Math.max(included, excluded);
               }
               else
                    cache[i][j] = cache[i-1][j]
          }
     }
     return cache[items.length][W];
}

Поскольку сформированная таблица имеет размер nW, это решение имеет эффективность использования времени и пространства, равную O (nW).

Прочие примечания

Основная трудность задачи о рюкзаке 0–1 как вопроса на собеседовании заключается в том, что, хотя необходимость динамического программирования очевидна, количество подзадач, на которые оно может быть разбито, значительно меньше. Оптимизация также сложна, так как в зависимости от приведенных примеров время выполнения решения методом грубой силы может показаться более эффективным по времени, что может запутать ситуацию. Например, если W = 70, а набор элементов имеет вид [{значение: 70, вес: 10}, {значение: 80, вес: 15}, {значение: 100, вес : 20}, {значение: 130, вес: 25}, {значение: 140, вес: 35}, {значение: 160, вес: 40}], затем худшее время для решения методом перебора = 2⁶, или 64 , а время оптимального решения = 6 * 70 = 420.

Решение проблемы с рюкзаком 0–1 также более неудобно в Javascript, чем в других языках, потому что Javascript не позволяет легко объявлять размерный массив. Хотя это второстепенный момент, его стоит иметь в виду.

Мой LinkedIn: https://www.linkedin.com/in/alexmorson/