Ottieni la combinazione più efficiente di un ampio elenco di oggetti basato su un campo


9

Sto cercando di massimizzare il numero di stelle dato un certo budget e limite massimo sulla combinazione.

Domanda di esempio:

Con un budget di 500 euro, visitando solo i ristoranti consentiti o meno, cenare e raccogliere il maggior numero di stelle possibile.

Sto cercando di scrivere un algoritmo efficiente, che potrebbe potenzialmente elaborare 1 milione di istanze di ristorante per un massimo di 10 ristoranti.

Nota, questo è un post trasversale di una domanda che ho posto ieri: Java: ottieni la combinazione più efficiente di un ampio elenco di oggetti basato su un campo

La soluzione seguente assegnerà 15 $ per stella al r8ristorante, il che significa che quando genera l'elenco, lo inserisce prima nell'elenco e con i restanti 70 $ può ottenere solo altre 2 stelle per un totale di 4 stelle. Tuttavia, se fosse abbastanza intelligente da saltare il r8ristorante (anche se è il miglior rapporto in dollari per stella), il r1ristorante sarebbe in realtà una scelta migliore per il budget, in quanto costa 100 $ e 5 stelle.

Qualcuno può aiutare a tentare il problema e battere la soluzione attuale?

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
Questo zaino è? Perdonami, ho scremato.
Kenny Ostrom,

1
È lo stesso concetto di zaino - budget= peso massimo dello zaino in kg, max= numero di oggetti che lo zaino può contenere, stars= un valore sull'articolo e cost= peso dell'articolo in kg
AK47

3
E qual è il problema con il codice pubblicato?
cricket_007,

1
@ cricket_007 in base all'ordine, assegna 15 $ per stella al r8ristorante, il che significa che quando genera l'elenco, lo inserisce prima nell'elenco e con i restanti 70 $ può ottenere solo altre 2 stelle. Tuttavia, se fosse abbastanza intelligente da saltare quello (anche se è il miglior rapporto in dollari per stella, il r1ristorante sarebbe in realtà una scelta migliore per il budget, in quanto costa 100 $ e 5 stelle
AK47

Risposte:


5

Sembra che il tuo problema sia più o meno lo stesso del problema Zaino: Massimizza il valore dati determinati vincoli di peso e volume. Fondamentalmente valore = totale stelle, peso = prezzo, limite zaino = budget totale. Ora c'è un ulteriore vincolo sul totale degli "oggetti" (visite al ristorante) ma ciò non cambia l'essenza.

Come forse saprai, il problema dello zaino è NP difficile, il che significa che non è noto alcun algoritmo con ridimensionamento del tempo polinomiale.

Tuttavia, potrebbero esserci algoritmi pseudopolinomiali efficienti che utilizzano la programmazione dinamica e, naturalmente, ci sono euristiche efficienti, come l'euristica "avida" che sembra aver scoperto. Questa euristica prevede di iniziare a riempire prima gli oggetti con la "densità" più alta (la maggior parte delle stelle per dollaro). Come hai visto, questa euristica non riesce a trovare il vero ottimale in alcuni casi.

L'approccio di programmazione dinamica dovrebbe essere abbastanza buono qui. Si basa su una ricorsione: dato un budget B e un numero di visite rimanenti V, qual è la migliore serie di ristoranti da visitare su una serie totale di ristoranti R?

Vedi qui: https://en.wikipedia.org/wiki/Knapsack_problem#0/1_knapsack_problem

Fondamentalmente definiamo un array mper "numero massimo di stelle", dove m[i, b, v]è il numero massimo di stelle che possiamo ottenere quando ci è consentito visitare ristoranti fino al (incluso) numero di ristorante i, spendere al massimo be visitare la maggior parte dei vristoranti (il limite) .

Ora, riempiamo questo array dal basso verso l'alto. Ad esempio, m[0, b, v] = 0per tutti i valori di be vperché se non possiamo andare in nessun ristorante, non possiamo ottenere stelle.

Inoltre, m[i, b, 0] = 0per tutti i valori di ie bperché se avessimo esaurito tutte le nostre visite, non potremmo ottenere più stelle.

La riga successiva non è troppo difficile:

m[i, b, v] = m[i - 1, b, v] if p[i] > b dov'è p[i]il prezzo di cenare al ristorante i. Cosa dice questa linea? Bene, se il ristorante iè più costoso di quanto ci restino i soldi ( b), allora non possiamo andarci. Ciò significa che la quantità massima di stelle che possiamo ottenere è la stessa se includiamo ristoranti fino a io solo fino a i - 1.

La riga successiva è un po 'complicata:

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

Uff. s[i]è la quantità di stelle che ottieni dal ristorante itra l'altro.

Cosa dice questa linea? È il cuore dell'approccio alla programmazione dinamica. Quando si considera la quantità massima di stelle che possiamo ottenere guardando ristoranti fino a quelli inclusi i, quindi nella soluzione risultante o ci andiamo o no, e "solo" dobbiamo vedere quale di questi due percorsi porta a più stelle:

Se non andiamo al ristorante i, conserviamo la stessa quantità di denaro e le visite rimanenti. La quantità massima di stelle che possiamo ottenere in questo percorso è la stessa di se non guardassimo nemmeno al ristorante i. Questa è la prima parte del max.

Ma se andiamo al ristorante i, allora restiamo con p[i]meno soldi, una visita in meno e s[i]più stelle. Questa è la seconda parte del max.

Ora la domanda è semplice: quale delle due è più grande.

Puoi creare questo array e riempirlo con un ciclo relativamente semplice (prendi ispirazione dal wiki). Questo ti dà solo la quantità di stelle, non l'elenco effettivo dei ristoranti da visitare. Per questo, aggiungi un po 'di contabilità extra al calcolo di w.


Spero che le informazioni siano sufficienti per portarti nella giusta direzione.

In alternativa, puoi scrivere il tuo problema in termini di variabili binarie e una funzione quadratica dell'obiettivo e risolverlo sull'annelaer quantico D-Wave :-p Inviami un messaggio se vuoi saperne di più.


Per quanto riguarda il tempo polinomiale, il massimo di 10 ristoranti significa che il problema può essere risolto con la forza bruta, ripetendo tutte le combinazioni di un massimo di 10 ristoranti e mantenendo il migliore, in tempo O (n ^ 10). Ora, non voglio nemmeno eseguire un algoritmo O (n ^ 10) con n = 10 ^ 6, ma è tempo polinomiale.
kaya3,

I "10 ristoranti" sono comunque un numero veramente fisso, o semplicemente risolti nell'esempio sopra, e potrebbero essere più grandi per un esempio diverso?
Lagerbaer,

Questa è una buona domanda e non è chiaro quali parametri del problema debbano essere generalizzati durante l'analisi del tempo di esecuzione. Naturalmente, non esiste una soluzione nota che sia polinomiale in k, intendo solo che è una conclusione piuttosto debole se siamo interessati solo al problema per il piccolo k.
kaya3,

Il numero "massimo" di ristoranti potrebbe cambiare. Questa iterazione potrebbe essere 10, e successivamente potrebbe essere 5.
AK47

@ AK47 Indipendentemente da ciò, l'algoritmo che ho disegnato sopra dovrebbe essere abbastanza pulito. La dimensione dell'array multidimensionale è data dal budget, dal numero di ristoranti e dal numero di visite, e ci vuole O (1) per riempire una voce dell'array, quindi l'algo corre nel tempo O (R B V).
Lagerbaer,

2

Utilizzando la stessa idea della mia risposta qui :

In una raccolta di n numeri positivi che si sommano a S, almeno uno di essi sarà inferiore a S diviso per n (S / n)

potresti costruire la lista partendo dai potenziali ristoranti "più economici" .

I passaggi dell'algoritmo:

  • Trova i 5 ristoranti con un costo <500/10, ognuno con stelle diverse e il costo più basso per ogni stella . ad es. r1, r2, r3, r4, r5
  • Per ciascuno dei valori sopra riportati, trova altri 5 ristoranti con costo <(500 - costo (x)) / 9 e stelle diverse . Seleziona di nuovo il costo più basso per ogni stella
  • fallo finché non raggiungi 10 ristoranti e non superi il budget.
  • Eseguire nuovamente i 3 passaggi precedenti per il limite di 1 - 9 ristoranti.
  • Mantieni la soluzione che produce il maggior numero di stelle

Naturalmente, non è possibile selezionare nuovamente un ristorante.

Penso che nel caso peggiore, dovrai calcolare 5x5x5 ... = 5 ^ 10 + 5 ^ 9 + ... + 5 ^ 2 + 5 (= circa 12 milioni) soluzioni.

In 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]);


Ehi @Jannes Botis, ci vogliono 27 secondi per 100000 ristoranti: repl.it/repls/StripedMoralOptimization Pensi che sia possibile ottimizzarlo per funzionare con 1 milione di record?
AK47

Il collo di bottiglia è la funzione .filter () all'interno di findCheapestRestaurant (), è possibile ordinare () i ristoranti a costo dopo che sono stati creati e utilizzare .find () invece di filter () poiché solo il primo trovato sarà il più economico. Ho apportato la modifica al collegamento. Ma penso che la soluzione migliore sarebbe quella di utilizzare un database (ad esempio mysql) per ristoranti con un indice sui costi, in modo da poter sostituire .filter () con una selezione condizionale.
Jannes Botis,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.