Отримайте найефективнішу комбінацію великого Списку об’єктів на основі поля


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()

2
Це ранець? Пробачте, я скуйовдився.
Кенні Остром

1
Це те саме поняття, як рюкзак - budget= максимальна вага рюкзака в кг, max= кількість предметів, які вміщає рюкзак, stars= деяке значення на предметі і cost= вага предмета в кг
AK47

3
І в чому проблема з розміщеним кодом?
крикет_007

1
@ cricket_007 на підставі замовлення, він призначає r8Ресторану 15 доларів за зірку , а це означає, що при створенні списку він перший додає його до списку, а з рештою 70 $ він може отримати лише ще 2 зірки. Однак, якби це було досить розумно, щоб пропустити це (хоча це найкраще співвідношення долара за зірку, r1ресторан насправді був би кращим вибором для бюджету, оскільки це коштує 100 доларів і 5 зірок
AK47

Відповіді:


5

Здається, що ваша проблема майже однакова як проблема Knapsack: Максимізуйте значення, враховуючи певні обмеження ваги та гучності. В основному значення = загальна кількість зірок, вага = ціна, ліміт рюкзака = загальний бюджет. Тепер є додаткове обмеження загальної кількості предметів (відвідування ресторану), але це не змінює суть.

Як ви можете, чи не можете знати, проблема з рюкзаком є ​​важкою 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

Phew. s[i]- кількість зірок, які ви отримуєте від ресторану ibtw.

Що говорить цей рядок? Це серце підходу динамічного програмування. Розглядаючи максимальну кількість зірок, яку ми можемо отримати, переглядаючи ресторани до і в тому числіi , тоді в отриманому рішенні ми або їдемо туди, або ми не робимо, і ми "просто" повинні бачити, яка з цих двох стежок веде до більше зірок:

Якщо ми не ходимо в ресторан i, то зберігаємо таку ж суму грошей і решта відвідувань. Максимальна кількість зірок, яку ми можемо отримати на цьому шляху, така сама, як якщо б ми навіть не заглядали в ресторанi . Ось перша частина в max.

Але якщо ми все-таки підемо в ресторан i, то нам залишиться p[i]менше грошей, менше відвідування таs[i] більше зірок. Це друга частина в max.

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

Ви можете створити цей масив і заповнити його відносно простим циклом (візьміть натхнення у вікі). Це просто дає вам кількість зірок, але не фактичний список ресторанів, які слід відвідати. Для цього додайте додаткову бухгалтерію до розрахунку w.


Я сподіваюсь, що інформації вистачить, щоб рушити вас у правильному напрямку.

Крім того, ви можете написати свою проблему у вигляді двійкових змінних та квадратичної цільової функції та вирішити її на квантовому аннелаері D-Wave :-p Повідомте мене, якщо ви хочете дізнатися більше про це.


Що стосується часу полинома, то максимум 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 Б V).
Lagerbaer

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 Як ви думаєте, чи можна оптимізувати його для роботи з мільйонами записів?
AK47

Функція вузького місця є .filter () всередині findCheapestRestaurant (), ви можете сортувати () ресторани за вартістю після їх створення та використовувати .find () замість фільтру (), оскільки лише перший знайдений буде найдешевшим. Я змінив посилання. Але я думаю, що найкращим рішенням було б використовувати базу даних (наприклад, mysql) для ресторанів з індексом вартості, щоб ви могли замінити .filter () на умовний вибір.
Янній Botis
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.