Получить наиболее эффективную комбинацию большого списка объектов на основе поля

9

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

Пример вопроса:

С бюджетом 500 евро, посещая только максимально разрешенные рестораны или меньше, поужинайте и соберите как можно больше звезд.

Я рассчитываю написать эффективный алгоритм, который потенциально мог бы обработать 1 миллион экземпляров Ресторанов для максимум 10 ресторанов.

Обратите внимание, что это перекрестный пост из вопроса, который я задал вчера: Java: получить наиболее эффективную комбинацию большого списка объектов на основе поля

Приведенное ниже решение назначит r8Ресторану 15 $ за звезду , что означает, что при создании списка он сначала помещает его в список, а с оставшимися 70 $ он может получить только еще 2 звезды, что дает 4 звезды. Однако, если бы он был достаточно умен, чтобы пропустить r8ресторан (даже при том, что это лучшее соотношение доллара к звезде), r1ресторан был бы лучшим выбором для бюджета, так как он стоил бы 100 долларов и 5 звезд.

Кто-нибудь может помочь решить проблему и обойти текущее решение?

import itertools

class Restaurant():
  def __init__(self, cost, stars):
    self.cost = cost
    self.stars = stars
    self.ratio = cost / stars

  def display(self):
    print("Cost: $" + str(self.cost))
    print("Stars: " + str(self.stars))
    print()

r1 = Restaurant(100, 5)
r2 = Restaurant(140, 3)
r3 = Restaurant(90, 4)
r4 = Restaurant(140, 3)
r5 = Restaurant(120, 4)
r6 = Restaurant(60, 1)
r7 = Restaurant(40, 1)
r8 = Restaurant(30, 2)
r9 = Restaurant(70, 2)
r10 = Restaurant(250, 5)

print()
print("***************")
print("** Unsorted: **")
print("***************")
print()

restaurants = [r1, r2, r3, r4, r5, r6, r7, r8, r9, r10]

for restaurant in restaurants:
  print(restaurant.ratio, restaurant.stars)

print()
print("***************")
print("**  Sorted:  **")
print("***************")
print()

sorted_restaurants = sorted(restaurants, key = lambda x: x.ratio, reverse = True)

for restaurant in sorted_restaurants:
  print(restaurant.ratio, restaurant.stars)

print()
print("*********************")
print("** Begin Rucksack: **")
print("*********************")
print()

max = 5
budget = 100

spent = 0
quantity = 0

rucksack = []

for i in itertools.count():

  if len(rucksack) >= max or i == len(sorted_restaurants):
    break

  sorted_restaurants[i].display()

  if sorted_restaurants[i].cost + spent <= budget:
    spent = spent + sorted_restaurants[i].cost
    rucksack.append(sorted_restaurants[i])

print("Total Cost: $" + str(sum([x.cost for x in rucksack])))
print("Total Stars: " + str(sum([x.stars for x in rucksack])))

print()
print("*****************")
print("** Final List: **")
print("*****************")
print()

for restaurant in rucksack:
  restaurant.display()
АК 47
источник
2
Это рюкзак? Прости меня, я снялся.
Кенни Остром
1
Это та же самая концепция рюкзака - budget= максимальный вес рюкзака в кг, max= количество предметов, которое может вместить рюкзак, stars= некоторое значение для предмета и cost= вес предмета в кг
AK47
3
А в чем проблема с кодом, размещенным?
cricket_007
1
@ cricket_007, основываясь на заказе, он назначает r8Ресторану 15 $ за звезду , что означает, что при создании списка он сначала помещает его в список, а с оставшимися 70 $ он может получить только еще 2 звезды. Однако, если бы он был достаточно умен, чтобы пропустить это (даже при том, что это лучшее соотношение доллара к звезде, r1ресторан на самом деле был бы лучшим выбором для бюджета, так как он стоил 100 долларов и 5 звезд
AK47

Ответы:

5

Похоже, ваша проблема почти такая же, как и проблема с рюкзаком: максимизируйте значение с учетом определенных ограничений по весу и объему. В основном значение = общее количество звезд, вес = цена, предел рюкзака = общий бюджет. Теперь есть дополнительное ограничение общего количества «предметов» (посещений ресторана), но это не меняет сути.

Как вы можете знать, а может и не знать, проблема рюкзака сложна с точки зрения NP, что означает отсутствие алгоритма с полиномиальным масштабированием по времени.

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

Подход динамического программирования должен быть довольно хорошим здесь. Он основан на рекурсии: учитывая бюджет B и количество оставшихся посещений V, какой ресторан лучше всего посетить из общего числа ресторанов R?

Смотрите здесь: https://en.wikipedia.org/wiki/Knapsack_problem#0/1_knapsack_problem

По сути, мы определяем массив mдля «максимальных звезд», где m[i, b, v]указано максимальное количество звезд, которое мы можем получить, когда нам разрешено посещать рестораны с номером ресторана (включая), максимально увеличивать iрасходы bи посещать большинство vресторанов (ограничение). ,

Теперь мы заполним этот массив снизу вверх. Например, m[0, b, v] = 0для всех значений bи vпотому, что если мы не можем пойти в какие-либо рестораны, мы не можем получить звезд.

Кроме того, m[i, b, 0] = 0для всех значений iи bпотому что, если мы использовали все наши посещения, мы не можем получить больше звезд.

Следующая строка тоже не сложная:

m[i, b, v] = m[i - 1, b, v] if p[i] > b где p[i]это цена обеда в ресторане i. Что говорит эта строка? Ну, если ресторан iдороже, чем у нас осталось денег ( b), то мы не можем туда пойти. Это означает, что максимальное количество звезд, которое мы можем получить, одинаково, независимо от того, включаем ли мы рестораны до iили только до i - 1.

Следующая строка немного сложнее:

m[i, b, v] = max(m[i-1, b, v]), m[i-1, b - p[i], v-1] + s[i]) if p[i] <= b

Уф. s[i]это количество звезд, которые вы получаете от ресторана, iкстати.

Что говорит эта строка? Это сердце подхода динамического программирования. При рассмотрении максимального количества звезд, которое мы можем получить при просмотре ресторанов до включительно i, то в полученном решении мы либо идем туда, либо нет, и нам «просто» нужно увидеть, какой из этих двух путей приведет к большему звезды:

Если мы не пойдем в ресторан i, то у нас останется столько же денег и оставшихся посещений. Максимальное количество звезд, которые мы можем получить на этом пути, такое же, как если бы мы даже не смотрели в ресторан i. Это первая часть в max.

Но если мы пойдем в ресторан i, у нас останется p[i]меньше денег, меньше посещений и s[i]больше звезд. Это вторая часть в max.

Теперь вопрос прост: какой из двух больше.

Вы можете создать этот массив и заполнить его относительно простым циклом for (черпайте вдохновение из вики). Это просто дает вам количество звезд, а не фактический список ресторанов для посещения. Для этого добавьте немного дополнительной бухгалтерии к расчету w.


Я надеюсь, что этой информации достаточно, чтобы направить вас в правильном направлении.

В качестве альтернативы, вы можете написать свою задачу в терминах бинарных переменных и квадратичной целевой функции и решить ее на квантовом аннелере D-Wave :-p Напишите мне, если вы хотите узнать больше об этом.

Lagerbaer
источник
Что касается полиномиального времени, то максимальное из 10 ресторанов означает, что проблема может быть решена с помощью грубой силы, итерации по всем комбинациям до 10 ресторанов и сохранения наилучшего за O (n ^ 10) времени. Теперь я не хочу запускать алгоритм O (n ^ 10) с n = 10 ^ 6, но это полиномиальное время.
kaya3
Является ли «10 ресторанов» действительно фиксированным числом или только фиксированным в приведенном выше примере, и может быть больше для другого примера?
Lagerbaer
Это хороший вопрос, и неясно, какие параметры задачи следует обобщать при анализе времени выполнения. Конечно, нет известного решения, которое является полиномиальным по k, я просто имею в виду, что это довольно слабый вывод, если нас интересует только проблема для малых k.
kaya3
Максимальное количество ресторанов может измениться. Эта итерация может быть 10, а следующая может быть 5.
AK47
@ AK47 Несмотря на это, алгоритм, который я набросал выше, должен быть довольно аккуратным. Размер многомерного массива определяется вашим бюджетом, количеством ресторанов и количеством посещений, и для заполнения одной записи массива требуется O (1), поэтому алгоритм работает во времени O (R) Б В).
Лагербаер
2

Используя ту же идею, что и мой ответ здесь :

В наборе из n положительных чисел, суммирующих до S, хотя бы одно из них будет меньше S, деленного на n (S / n)

Вы можете составить список, начиная с потенциальных «самых дешевых» ресторанов .

Шаги алгоритма:

  • Найдите 5 ресторанов стоимостью <500/10, каждый с разными звездами и с наименьшей стоимостью для каждой звезды . например, r1, r2, r3, r4, r5
  • Для каждого из приведенных выше значений найдите еще 5 ресторанов со стоимостью <(500 - стоимость (x)) / 9 и разными звездами . Снова выберите самую низкую стоимость для каждой звезды
  • делайте это, пока не дойдете до 10 ресторанов и не превысите свой бюджет.
  • Повторите 3 шага выше для ограничения 1 - 9 ресторанов.
  • Держите решение, которое производит больше звезд

Конечно, вы не можете выбрать ресторан.

Я думаю, что в худшем случае вам придется рассчитать 5x5x5 ... = 5 ^ 10 + 5 ^ 9 + ... + 5 ^ 2 + 5 (= около 12 миллионов) решений.

В JavaScript

function Restaurant(name, cost, stars) {
    this.name = name;
    this.cost = cost;
    this.stars = stars;
}

function RestaurantCollection() {
    var restaurants = [];
    var cost = 0;
    this.stars = 0;

    this.addRestaurant = function(restaurant) {
        restaurants.push(restaurant);
        cost += restaurant.cost;
        this.stars += restaurant.stars;
    };

    this.setRestaurants = function(clonedRestaurants, nCost, nStars) {
        restaurants = clonedRestaurants;
        cost = nCost;
        this.stars += nStars;
    };
    this.getAll = function() {
        return restaurants;
    };

    this.getCost = function() {
        return cost;
    };
    this.setCost = function(clonedCost) {
        cost = clonedCost;
    };

    this.findNext5Restaurants = function(restaurants, budget, totalGoal) {
        var existingRestaurants = this.getAll();
        var maxCost = (budget - cost) / (totalGoal - existingRestaurants.length);
        var cheapestRestaurantPerStarRating = [];
        for(var stars = 5; stars > 0; stars--) {
            var found = findCheapestRestaurant(restaurants, stars, maxCost, existingRestaurants);
            if(found) {
                cheapestRestaurantPerStarRating.push(found);
            }
        }
        return cheapestRestaurantPerStarRating;
    };

    this.clone = function() {
        var restaurantCollection = new RestaurantCollection();
        restaurantCollection.setRestaurants([...restaurants], this.getCost(), this.stars);
        return restaurantCollection;
    };
}

function findCheapestRestaurant(restaurants, stars, maxCost, excludeRestaurants) {
     var excludeRestaurantNames = excludeRestaurants.map(restaurant => restaurant.name);
     var found = restaurants.find(restaurant => restaurant.stars == stars && restaurant.cost <= maxCost && !excludeRestaurantNames.includes(restaurant.name));
     return found;
}

function calculateNextCollections(restaurants, collections, budget, totalGoal) {
    var newCollections = [];
    collections.forEach(collection => {
        var nextRestaurants = collection.findNext5Restaurants(restaurants, budget, totalGoal);
        nextRestaurants.forEach(restaurant => {
            var newCollection = collection.clone();
            newCollection.addRestaurant(restaurant);
            if(newCollection.getCost() <= budget) {
                 newCollections.push(newCollection);
            }
        });
    });
    return newCollections;
};

var restaurants = [];
restaurants.push(new Restaurant('r1', 100, 5));
restaurants.push(new Restaurant('r2',140, 3));
restaurants.push(new Restaurant('r3',90, 4));
restaurants.push(new Restaurant('r4',140, 3));
restaurants.push(new Restaurant('r5',120, 4));
restaurants.push(new Restaurant('r6',60, 1));
restaurants.push(new Restaurant('r7',40, 1));
restaurants.push(new Restaurant('r8',30, 2));
restaurants.push(new Restaurant('r9',70, 2));
restaurants.push(new Restaurant('r10',250, 5));

restaurants.sort((a, b) => a.cost - b.cost);
var max = 5;
var budget = 100;

var total = max;
var totalCollections = [];

for(var totalGoal = total; totalGoal > 0; totalGoal--) {
    var collections = [new RestaurantCollection()];

    for(var i = totalGoal; i > 0; i--) {
        collections = calculateNextCollections(restaurants, collections, budget, totalGoal);
    }
    totalCollections = totalCollections.concat(collections);
}

var totalCollections = totalCollections.map(collection => { 
      return {
          name: collection.getAll().map(restaurant => restaurant.name),
          stars: collection.stars,
          cost: collection.getCost()
      }
});

console.log("Solutions found:\n");
console.log(totalCollections);

totalCollections.sort((a, b) => b.stars - a.stars);
console.log("Best solution:\n");
console.log(totalCollections[0]);

Яннес Ботис
источник
Привет @Jannes Botis, на 100000 ресторанов уходит 27 секунд: repl.it/repls/StripedMoralOptimization Как вы думаете, можно ли оптимизировать его для работы с 1 миллионом записей?
AK47
Узким местом является функция .filter () внутри findCheapestRestaurant (), вы можете отсортировать () рестораны по стоимости после их создания и использовать .find () вместо filter (), поскольку только 1-й найденный будет самым дешевым. Я сделал изменение в ссылке. Но я думаю, что лучшим решением было бы использование базы данных (например, mysql) для ресторанов с индексом стоимости, чтобы вы могли заменить .filter () условным выбором.
Яннес Ботис