【问题标题】:Get the most efficient combination of a large List of objects based on a field获取基于字段的大型对象列表的最有效组合
【发布时间】:2020-03-24 10:08:37
【问题描述】:

在给定一定的预算和组合的最大限制的情况下,我希望最大限度地增加星星的数量。

示例问题:

预算为 500 欧元,仅访问最多允许或更少的餐厅,用餐并收集尽可能多的星星。

我正在寻找一种高效的算法,它可能为最多 10 家餐厅处理 100 万个餐厅实例。

注意,这是我昨天问的一个问题的交叉帖子: Java: Get the most efficient combination of a large List of objects based on a field

下面的解决方案将为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()

【问题讨论】:

  • 这是背包吗?原谅我,我略读了。
  • 与背包的概念相同 -- budget = 背包的最大重量,以公斤为单位,max = 背包可以容纳的物品数量,stars = 物品上的一些价值和@ 987654329@ = 商品重量(公斤)
  • 发布的代码有什么问题?
  • @cricket_007 根据订单,它为r8餐厅分配每颗星15美元,这意味着在生成列表时,它首先将其放入列表中,剩下的70美元它只能再获得2星。但是,如果它足够聪明,可以跳过它(即使它是每颗星的最佳美元比率,r1 餐厅实际上是预算的更好选择,因为它的成本为 100 美元和 5 颗星

标签: python python-3.x combinations knapsack-problem


【解决方案1】:

听起来您的问题与背包问题几乎相同:在给定重量和体积限制的情况下最大化价值。基本上价值=总星数,重量=价格,背包限额=总预算。现在有一个额外的限制,即总“项目”(餐厅访问量),但这并没有改变要点。

您可能知道也可能不知道,背包问题是 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 对于所有 bv 的值,因为如果我们不能去任何餐馆,我们就不能得到任何星星。

另外,m[i, b, 0] = 0 用于所有 ib 的值,因为如果我们用完所有访问,我们将无法获得更多的星星。

下一行也不太难:

m[i, b, v] = m[i - 1, b, v] if p[i] &gt; 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] &lt;= b

呸。 s[i] 是您从餐厅i 获得的星星数量。

这句话说了什么?它是动态编程方法的核心。当考虑我们在查看餐厅时可以获得的最大星级数量时,包括i,然后在最终的解决方案中,我们要么去那里,要么不去那里,我们“只是”必须看看这两条路径中的哪一条导致更多明星:

如果我们不去餐厅i,那么我们保留相同数量的钱和剩余的访问次数。我们可以在这条路径上获得的最大星星数量与我们甚至没有看过餐厅i 相同。这是max 的第一部分。

但是,如果我们真的去餐厅i,那么我们就剩下p[i] 更少的钱、更少的访问量和更多的s[i] 星星。这是max 的第二部分。

现在问题很简单:两者哪个更大。

您可以创建这个数组并用相对简单的 for 循环填充它(从 wiki 获得灵感)。不过,这只是给你星级的数量,而不是实际的餐馆列表。为此,在w 的计算中添加一些额外的簿记。


我希望这些信息足以让你朝着正确的方向前进。

或者,您可以用二进制变量和二次目标函数来编写您的问题,然后在 D-Wave 量子退火器上解决它:-p 如果您想了解更多信息,请给我留言。

【讨论】:

  • 关于多项式时间,最多 10 家餐厅意味着可以通过蛮力解决问题,迭代最多 10 家餐厅的所有组合,并保留最好的一个,在 O(n^10 ) 时间。现在,我也不想运行 n = 10^6 的 O(n^10) 算法,但它是多项式时间。
  • “10 家餐厅”是一个真正固定的数字,还是在上面的示例中只是固定的,对于不同的示例可能更大?
  • 这是一个很好的问题,在分析运行时间时还不清楚问题的哪些参数可以概括出来。当然,没有已知的 k 多项式解,我的意思是,如果我们只对小 k 的问题感兴趣,那么这是一个相当弱的结论。
  • 餐厅的“最大”数量可能会改变。本次迭代可能是 10 次,下一次可能是 5 次。
  • @AK47 不管怎样,我上面勾勒的算法应该很简洁。多维数组的大小由您的预算、餐厅数量和访问次数给出,并且需要 O(1) 来填充数组的一个条目,因此算法运行时间为 O(R BV).
【解决方案2】:

使用与my answer here相同的思路:

在 n 个总和为 S 的正数的集合中,至少有一个小于 S 除以 n (S/n)

您可以从潜在的“最便宜”餐厅开始构建列表

算法的步骤:

  • 查找成本 不同的星级和每颗星级的最低成本。例如 r1, r2, r3, r4, r5
  • 对于上述每个值,找到另外 5 家成本 不同星级的餐厅。再次为每颗星选择最低成本
  • 这样做,直到您达到 10 家餐厅并且您不超过您的预算。
  • 重新运行上述 3 个步骤以限制 1 - 9 家餐厅。
  • 保留产生最多星星的解决方案

当然,您不能重新选择餐厅。

我认为最坏的情况是,您必须计算 5x5x5... = 5^10 + 5^9 + ... + 5^2 + 5(= 大约 1200 万)个解决方案。

在 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您认为可以优化它以处理 100 万条记录吗?
  • 瓶颈是 findCheapestRestaurant() 中的 .filter() 函数,您可以在创建餐厅后按成本对餐厅进行 sort() 并使用 .find() 而不是 filter(),因为只找到第一个将是最便宜的。我在链接中进行了更改。但我认为最好的解决方案是为具有成本索引的餐厅使用数据库(例如 mysql),以便您可以将 .filter() 替换为条件选择。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-02-17
  • 1970-01-01
  • 2015-08-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多