«1. Введение

Задача о рюкзаке — это задача комбинаторной оптимизации, имеющая множество приложений. В этом уроке мы решим эту проблему на Java.

2. Задача о рюкзаке

В задаче о рюкзаке у нас есть набор предметов. У каждого предмета есть вес и ценность:

Мы хотим положить эти предметы в рюкзак. Однако у него есть ограничение по весу:

Поэтому нам нужно выбрать предметы, общий вес которых не превышает ограничения по весу, а их общая стоимость как можно выше. Например, лучшее решение для приведенного выше примера — выбрать предмет весом 5 кг и предмет весом 6 кг, что дает максимальную стоимость в 40 долларов США в пределах ограничения веса.

Задача о рюкзаке имеет несколько вариантов. В этом уроке мы сосредоточимся на задаче о рюкзаке 0-1. В задаче о рюкзаке 0-1 каждый предмет нужно либо выбрать, либо оставить. Мы не можем взять часть суммы товара. Кроме того, мы не можем брать предмет несколько раз.

3. Математическое определение

Давайте теперь формализуем задачу о рюкзаке 0-1 в математической записи. Учитывая набор из n элементов и ограничение веса W, мы можем определить задачу оптимизации следующим образом:

Эта задача является NP-трудной. Следовательно, в настоящее время не существует алгоритма с полиномиальным временем для ее решения. Однако для этой задачи существует алгоритм псевдополиномиального времени, использующий динамическое программирование.

4. Рекурсивное решение

Для решения этой задачи можно использовать формулу рекурсии:

В этой формуле M(n,w) является оптимальным решением для n элементов с предельным весом w. Это максимальное из следующих двух значений:

    Оптимальное решение из (n-1) элементов с ограничением веса w (исключая n-й элемент) Значение n-го элемента плюс оптимальное решение из (n -1) предметы и w минус вес n-го предмета (включая n-й предмет)

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

Мы можем реализовать эту формулу рекурсии в Java:

int knapsackRec(int[] w, int[] v, int n, int W) {
    if (n <= 0) { 
        return 0; 
    } else if (w[n - 1] > W) {
        return knapsackRec(w, v, n - 1, W);
    } else {
        return Math.max(knapsackRec(w, v, n - 1, W), v[n - 1] 
          + knapsackRec(w, v, n - 1, W - w[n - 1]));
    }
}

На каждом шаге рекурсии нам нужно оценить два субоптимальных решения. Следовательно, время работы этого рекурсивного решения равно O(2n).

5. Решение для динамического программирования

Динамическое программирование — это стратегия линеаризации экспоненциально сложных задач программирования. Идея состоит в том, чтобы хранить результаты подзадач, чтобы нам не пришлось пересчитывать их позже.

Мы также можем решить задачу о рюкзаке 0-1 с помощью динамического программирования. Чтобы использовать динамическое программирование, мы сначала создаем двумерную таблицу с размерностями от 0 до n и от 0 до W. Затем мы используем восходящий подход для вычисления оптимального решения с этой таблицей:

int knapsackDP(int[] w, int[] v, int n, int W) {
    if (n <= 0 || W <= 0) {
        return 0;
    }

    int[][] m = new int[n + 1][W + 1];
    for (int j = 0; j <= W; j++) {
        m[0][j] = 0;
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= W; j++) { 
            if (w[i - 1] > j) {
                m[i][j] = m[i - 1][j];
            } else {
                m[i][j] = Math.max(
                  m[i - 1][j], 
                  m[i - 1][j - w[i - 1]] + v[i - 1]);
            }
        }
    }
    return m[n][W];
}

В этом решение, у нас есть вложенный цикл по номеру элемента n и пределу веса W. Следовательно, время его выполнения составляет O (nW).

6. Заключение

В этом уроке мы показали математическое определение задачи о рюкзаке 0-1. Затем мы предоставили рекурсивное решение этой проблемы с реализацией Java. Наконец, мы использовали динамическое программирование для решения этой проблемы.

Как всегда, исходный код статьи доступен на GitHub.