Dapatkan kombinasi paling efisien dari daftar besar objek berdasarkan bidang

9

Saya mencari untuk memaksimalkan jumlah bintang yang diberikan anggaran tertentu dan batas maksimum pada kombinasi.

Contoh pertanyaan:

Dengan anggaran 500 euro, hanya mengunjungi restoran maksimum yang diizinkan atau kurang, makan dan mengumpulkan bintang sebanyak mungkin.

Saya ingin menulis algoritma yang efisien, yang berpotensi memproses 1 juta contoh restoran hingga 10 restoran maks.

Catatan, ini adalah pos silang dari pertanyaan yang saya ajukan kemarin: Java: Dapatkan kombinasi paling efisien dari daftar besar objek berdasarkan bidang

Solusi di bawah ini akan menetapkan $ 15 per bintang ke r8Restoran, yang berarti bahwa ketika membuat daftar, itu menempatkan itu ke dalam daftar pertama, dan dengan $ 70 yang tersisa itu hanya bisa mendapatkan 2 bintang lagi memberikan total 4 bintang. Namun, jika itu cukup pintar untuk melewati r8restoran (meskipun itu adalah rasio dolar per bintang terbaik) r1restoran sebenarnya akan menjadi pilihan yang lebih baik untuk anggaran, karena biaya $ 100 dan 5 bintang.

Adakah yang bisa membantu mencoba masalah dan mengalahkan solusi saat ini?

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()
AK 47
sumber
2
Apakah ransel ini? Maafkan saya, saya membaca skim.
Kenny Ostrom
1
Ini adalah konsep yang sama dengan ransel - budget= berat ransel maksimal dalam kg, max= jumlah barang yang dapat disimpan ransel, stars= beberapa nilai pada barang tersebut dan cost= berat barang dalam kg
AK47
3
Dan apa masalah dengan kode yang diposting?
cricket_007
1
@ cricket_007 berdasarkan pesanan, ia memberikan $ 15 per bintang ke r8Restoran, yang berarti bahwa ketika membuat daftar, ia menempatkan itu ke dalam daftar pertama, dan dengan sisa $ 70 itu hanya bisa mendapatkan 2 bintang lagi. Namun, jika itu cukup pintar untuk melompati itu (meskipun itu adalah rasio dolar per bintang terbaik, r1restoran sebenarnya akan menjadi pilihan yang lebih baik untuk anggaran, karena itu adalah $ 100 biaya dan 5 bintang
AK47

Jawaban:

5

Kedengarannya seperti masalah Anda hampir sama dengan masalah Knapsack: Maksimalkan nilai mengingat batasan berat dan volume tertentu. Nilai dasarnya = total bintang, berat = harga, batas ransel = total anggaran. Sekarang ada kendala tambahan total "item" (kunjungan restoran) tetapi itu tidak mengubah intinya.

Seperti Anda mungkin atau mungkin tidak tahu, masalah ransel adalah NP keras, yang berarti tidak ada algoritma dengan penskalaan waktu polinomial diketahui.

Namun, mungkin ada algoritma pseudopolinomial yang efisien menggunakan pemrograman dinamis, dan tentu saja ada heuristik yang efisien, seperti heuristik "serakah" yang tampaknya telah Anda temukan. Heuristik ini melibatkan mulai mengisi dengan item "kepadatan" tertinggi (sebagian besar bintang per dolar) terlebih dahulu. Seperti yang telah Anda lihat, heuristik ini gagal menemukan optimal yang sebenarnya dalam beberapa kasus.

Pendekatan pemrograman dinamis harus cukup bagus di sini. Ini didasarkan pada rekursi: Dengan anggaran B dan sejumlah kunjungan yang tersisa V, restoran apa yang paling baik untuk dikunjungi dari sekumpulan restoran R?

Lihat di sini: https://en.wikipedia.org/wiki/Knapsack_problem#0/1_knapsack_problem

Pada dasarnya kami mendefinisikan larik muntuk "bintang maksimum", di mana m[i, b, v]jumlah bintang maksimum yang bisa kami dapatkan ketika kami diizinkan mengunjungi restoran hingga (dan termasuk) jumlah restoran i, paling banyak belanja b, dan mengunjungi di sebagian besar vrestoran (batas) .

Sekarang, kami dari bawah ke atas mengisi array ini. Misalnya, m[0, b, v] = 0untuk semua nilai bdan vkarena jika kita tidak dapat pergi ke restoran mana pun, kita tidak dapat memperoleh bintang.

Juga, m[i, b, 0] = 0untuk semua nilai idan bkarena jika kami menggunakan semua kunjungan kami, kami tidak dapat memperoleh bintang lagi.

Baris berikutnya juga tidak terlalu sulit:

m[i, b, v] = m[i - 1, b, v] if p[i] > b dimana p[i]harga makan di restoran i. Apa kata garis ini? Nah, jika restoran ilebih mahal daripada uang yang tersisa ( b) maka kita tidak bisa pergi ke sana. Yang berarti jumlah maksimum bintang yang dapat kita peroleh adalah sama apakah kita termasuk restoran hingga iatau hanya sampai i - 1.

Baris berikutnya agak rumit:

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

Fiuh. s[i]adalah jumlah bintang yang kamu dapatkan dari restoran ibtw.

Apa kata garis ini? Ini adalah jantung dari pendekatan pemrograman dinamis. Ketika mempertimbangkan jumlah maksimum bintang yang bisa kita dapatkan ketika melihat restoran hingga dan termasuk i, maka dalam solusi yang dihasilkan kita pergi ke sana atau tidak, dan kita "hanya" harus melihat yang mana dari dua jalur ini yang mengarah ke lebih banyak bintang:

Jika kita tidak pergi ke restoran i, maka kita menyimpan jumlah uang yang sama dan sisa kunjungan. Jumlah maksimal bintang yang bisa kita dapatkan di jalur ini sama dengan jika kita bahkan tidak melihat restoran i. Itu bagian pertama dalam max.

Tetapi jika kita pergi ke restoran i, maka kita hanya memiliki p[i]sedikit uang, satu kunjungan lebih sedikit, dan s[i]lebih banyak bintang. Itu bagian kedua dalam max.

Sekarang pertanyaannya sederhana: mana dari keduanya yang lebih besar.

Anda dapat membuat array ini dan mengisinya dengan loop yang relatif sederhana (ambil inspirasi dari wiki). Ini hanya memberi Anda jumlah bintang, bukan daftar restoran yang sebenarnya untuk dikunjungi. Untuk itu, tambahkan beberapa pembukuan tambahan ke dalam perhitungan w.


Saya harap informasi itu cukup untuk mengarahkan Anda ke arah yang benar.

Atau, Anda dapat menulis masalah Anda dalam hal variabel biner dan fungsi tujuan kuadratik dan menyelesaikannya pada annelaer kuantum D-Wave :-p Pesan saya jika Anda ingin tahu lebih banyak tentang itu.

Lagerbaer
sumber
Mengenai waktu polinomial, maksimal 10 restoran berarti bahwa masalahnya dapat diselesaikan dengan kekerasan, iterasi melalui semua kombinasi hingga 10 restoran, dan menjaga yang terbaik, dalam waktu O (n ^ 10) waktu. Sekarang, saya tidak ingin menjalankan algoritma O (n ^ 10) dengan n = 10 ^ 6, tetapi ini adalah waktu polinomial.
kaya3
Apakah "10 restoran" merupakan angka yang benar-benar tetap, atau hanya diperbaiki pada contoh di atas, dan bisa lebih besar untuk contoh yang berbeda?
Lagerbaer
Itu pertanyaan yang bagus, dan tidak jelas parameter masalah mana yang akan digeneralisasi saat menganalisis waktu berjalan. Tentu saja, tidak ada solusi diketahui yang jumlahnya banyak dalam k, maksud saya itu kesimpulan yang cukup lemah jika kita hanya tertarik pada masalah untuk k kecil.
kaya3
Jumlah "maks" restoran dapat berubah. Iterasi ini mungkin 10, dan berikutnya mungkin 5.
AK47
@ AK47 Terlepas dari itu, algoritme yang saya buat di atas seharusnya cukup rapi. Ukuran array multi-dimensi diberikan oleh anggaran Anda, jumlah restoran, dan jumlah kunjungan, dan dibutuhkan O (1) untuk mengisi satu entri array, sehingga algo berjalan dalam waktu O (R B V).
Lagerbaer
2

Menggunakan ide yang sama dengan jawaban saya di sini :

Dalam koleksi n angka positif yang berjumlah S, setidaknya salah satunya akan kurang dari S dibagi dengan n (S / n)

Anda dapat membuat daftar mulai dari restoran "termurah" yang potensial .

Langkah-langkah algoritma:

  • Temukan 5 restoran dengan biaya <500/10, masing-masing dengan bintang berbeda dan biaya terendah untuk setiap bintang . misalnya r1, r2, r3, r4, r5
  • Untuk masing-masing nilai di atas, cari 5 restoran lain dengan biaya <(500 - biaya (x)) / 9 dan bintang yang berbeda . Sekali lagi pilih biaya terendah untuk setiap bintang
  • lakukan ini sampai Anda mencapai 10 restoran dan Anda tidak melebihi anggaran Anda.
  • Jalankan kembali 3 langkah di atas untuk 1 - 9 restoran batas.
  • Jaga solusi yang menghasilkan bintang paling banyak

Tentu saja, Anda tidak dapat memilih kembali restoran.

Saya pikir kasus terburuk, Anda harus menghitung 5x5x5 ... = 5 ^ 10 + 5 ^ 9 + ... + 5 ^ 2 + 5 (= sekitar 12 juta) solusi.

Dalam 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
sumber
Hey @Jannes Botis, butuh 27 detik untuk 100000 Restoran: repl.it/repls/StripedMoralOptimization Apakah Anda pikir mungkin untuk mengoptimalkannya agar berfungsi dengan 1 juta catatan?
AK47
Hambatannya adalah fungsi .filter () di dalam findCheapestRestaurant (), Anda dapat mengurutkan () restoran berdasarkan biaya setelah dibuat dan menggunakan .find () alih-alih filter () karena hanya yang pertama ditemukan akan menjadi yang termurah. Saya membuat perubahan di tautan. Tapi saya pikir solusi terbaik adalah dengan menggunakan database (misalnya mysql) untuk restoran dengan indeks biaya, sehingga Anda dapat mengganti .filter () dengan pilih bersyarat.
Jannes Botis