15

假设我有三种产品:

产品 A 将提供 5 个功率。费用 50。

产品 B将提供 9 个功率。费用80。

产品 C将提供 15 个功率。费用 140。

我想知道当我需要 7 电源时我可以购买哪些产品组合。我可以买两个A但一个B更便宜。

当我需要 65 电源时。我需要 4 次C和 1 次A(成本 680)。但我也可以购买七种B产品和一种A(成本 610)。

我正在寻找一种方法来计算我需要的给定功率的产品可能组合。

我尝试这样做的方式并没有给我想要的东西:

// $products are sorted DESC on their $power
$power = 65
 while( $power > 0 ) {
    foreach( $products as $productPower ) {
        if( ( $productPower > $power && $power - $productPower > 0 ) || $productPower == end( $products ) ) {
            // Add product to list
            $power -= $productPower;
            break;
        }
    }
 }

此示例代码只会给我 4 次C和 1 次A。我应该怎么做?

编辑产品的数量是可变的。此外,具体成本和功率是可变的。因此,可能有 10 种产品的价格标签更便宜、更昂贵。

编辑 2正如我上面所说,我想计算可能的组合(复数)。有些人似乎在我的描述中忽略了这一点。

4

7 回答 7

9

介绍

这本来是一个背包问题,但是因为您不仅要寻找最佳解决方案,还希望找到所有可能的组合

然后你可以解决这个子集和问题+硬币变化得到:

  • 列出所有可能的组合,而不仅仅是总组合
  • 获得最佳组合

    例如,对于 N = 4,S = {1,2,3},有四种解:{1,1,1,1},{1,1,2},{2,2},{1, 3}。

示例 1

echo "<pre>";
$start = microtime(true);

// Start Finder
$finder = new CombinationFinder(65);

// Add Produts
$finder->addProduct(new Product("A", 5, 50));
$finder->addProduct(new Product("B", 9, 80));
$finder->addProduct(new Product("C", 15, 140));

// Output All Found Combinations
foreach ( $finder as $key => $sales ) {
    echo $sales->getName(), "\t\t\t", $sales->getCombinationCost(), PHP_EOL;
}

// Get Best Combination
echo "Combination: ", $finder->getBestCombination()->getName(), PHP_EOL;
echo "Cost: ", number_format($finder->getBestCombination()->getCombinationCost(), 2), PHP_EOL;

// Total Time
echo PHP_EOL, microtime(true) - $start;

输出

顶级组合

["A",1],["C",4]                 610
["A",1],["B",5],["C",1]         590
["A",4],["C",3]                 620
["A",4],["B",5]                 600
["A",7],["C",2]                 630
["A",10],["C",1]                640
["A",13]                        650

最佳组合

Combination: ["A",1],["B",5],["C",1]
Cost: 590.00

总时间

0.2533269405365

最佳组合

你可以看到最好的组合是 A*1 ,B*5 ,C*1..分解

            A          B           C
Power : 5   *  1 +  9  *  5 +  15  *  1    =   65
Cost  : 50  *  1 +  80 *  5 +  140 *  1    =   590   <---- Better than 610.00   

示例 2

该类可用于 2、3、4 或更多产品组合,但速度非常快

echo "<pre>";
$start = microtime(true);

// Start Finder
$finder = new CombinationFinder(65);

// Add Produts
$finder->addProduct(new Product("A", 5, 50));
$finder->addProduct(new Product("B", 9, 80));
$finder->addProduct(new Product("C", 15, 140));
$finder->addProduct(new Product("D", 20, 120)); // more product class

$finder->run(); // just run

// Get Best Combination
echo "Combination: ", $finder->getBestCombination()->getName(), PHP_EOL;
echo "Cost: ", number_format($finder->getBestCombination()->getCombinationCost(), 2), PHP_EOL;

// Total Time
echo PHP_EOL, microtime(true) - $start;

输出

Combination: ["A",1],["D",3]    //<---------------------- Best Combination
Cost: 410.00

花的时间

1.1627659797668  // less than 2 sec 

使用的类

class Product {
    public $name;
    public $power;
    public $cost;
    public $unit;

    function __construct($name, $power, $cost) {
        $this->name = $name;
        $this->power = $power;
        $this->cost = $cost;
        $this->unit = floor($cost / $power);
    }
}



class Sales {
    /**
     *
     * @var Product
     */
    public $product;
    public $count;
    public $salePower;
    public $saleCost;

    function __construct(Product $product, $count) {
        $this->product = $product;
        $this->count = $count;
        $this->salePower = $product->power * $count;
        $this->saleCost = $product->cost * $count;
    }
}



class SalesCombination {
    private $combinationPower;
    private $combinationCost;
    private $combinationName;
    private $combinationItems;
    private $args;

    function __construct(array $args) {
        list($this->combinationPower, $this->combinationCost, $this->combinationItems) = array_reduce($args, function ($a, $b) {
            $a[0] += $b->salePower;
            $a[1] += $b->saleCost;
            $a[2] = array_merge($a[2], array_fill(0, $b->count, $b->product->name));
            return $a;
        }, array(0,0,array()));
        $this->args = $args;
    }

    function getName() {
        $values = array_count_values($this->combinationItems);
        $final = array();
        foreach ( $values as $name => $amount ) {
            $final[] = array($name,$amount);
        }
        return substr(json_encode($final), 1, -1);
    }

    function getCombinationPower() {
        return $this->combinationPower;
    }

    function getCombinationCost() {
        return $this->combinationCost;
    }
}




class CombinationFinder implements IteratorAggregate, Countable {
    private $sales;
    private $products = array();
    private $power;
    private $found = array();
    private $bestCombination = null;
    private $run = false;

    function __construct($power) {
        $this->power = $power;
    }

    function addProduct(Product $product) {
        $this->products[] = $product;
    }

    function getBestCombination() {
        return $this->bestCombination;
    }

    function getFound() {
        return $this->found ?  : array();
    }

    public function getIterator() {
        if ($this->run === false) {
            $this->run();
        }
        return new ArrayIterator($this->found);
    }

    public function count() {
        return count($this->found);
    }

    function run() {
        $this->run = true;
        $this->buildSales();
        $u = new UniqueCombination($this->sales);
        $u->setCallback(array($this,"find"));
        $u->expand();
    }

    function find() {
        $salesCombination = new SalesCombination(func_get_args());
        if ($salesCombination->getCombinationPower() == $this->power) {
            isset($this->bestCombination) or $this->bestCombination = $salesCombination;
            $salesCombination->getCombinationCost() < $this->bestCombination->getCombinationCost() and $this->bestCombination = $salesCombination;
            $this->found[sha1($salesCombination->getName())] = $salesCombination;
        }
    }

    function buildSales() {
        $total = count($this->products);
        foreach ( $this->products as $product ) {
            $max = floor($this->power / $product->power);
            for($i = 1; $i <= $max; $i ++) {
                $this->sales[$product->name][] = new Sales($product, $i);
            }
        }
    }
}

class UniqueCombination {
    private $items;
    private $result = array();
    private $callback = null;

    function __construct($items) {
        $this->items = array_values($items);
    }

    function getResult() {
        return $this->result;
    }

    function setCallback($callback) {
        $this->callback = $callback;
    }

    function expand($set = array(), $index = 0) {
        if ($index == count($this->items)) {
            if (! empty($set)) {
                $this->result[] = $set;
                if (is_callable($this->callback)) {
                    call_user_func_array($this->callback, $set);
                }
            }
            return;
        }
        $this->expand($set, $index + 1);
        foreach ( $this->items[$index] as $item ) {
            $this->expand(array_merge($set, array($item)), $index + 1);
        }
    }
}
于 2012-11-10T01:50:21.510 回答
5

更新的答案

我支持我最初的答案,但后来得出了一个明确的解决方案。不幸的是,我不精通 PHP,所以我将介绍的实现是在(写得不好)F# 中。

使您的问题变得有趣的一点是,您不是在寻找最佳解决方案,而是在寻找所有可行的解决方案。正如我在原始答案中指出的那样,这很棘手,因为可行解决方案的集合是无限的。举例来说,如果你想生产 65 个单位,你可以使用 13xA,它产生 5x13 = 65 的幂。但是,显然,任何包含超过 13 个单位的 A 的解决方案也是一个解决方案。

您不能从函数返回无限集。您在这里需要的是所有“边界”案例的集合:

  • 如果一个解决方案包含与所有产品的边界情况一样多的单位,则它是有效的
  • 如果可以从边界情况中删除一个单元并且它仍然可行,则它不再可行。

例如,解 S = { A = 13; B = 0; C = 0 } 是边界情况。从任何产品中删除一个单元,这是不可行的 - 如果组合是这样的,对于每个产品,它包含比 S 更多的单元,它是一个有效的解决方案,但由 S“支配”。

换句话说,我们不能返回所有可能的解决方案,但我们可以返回区分可行和不可行解决方案的“极限”。

另请注意,产品的成本在这里是无关紧要的——一旦你有了一组边界情况,计算解决方案的成本就变得微不足道了。

鉴于您指定产品的数量可以是任意的,这听起来像是一个明确的递归案例。

如果您没有产品,那么解决方案就是空洞的——没有解决方案。如果您有 1 个产品,则解决方案是上限(目标 / product.Power)如果您有 2 个产品,例如 A:5 和 B:2,目标为 10,您可以

  • 使用 0 个 A -> 剩余目标为 10,使用 5 个 B(或更多)
  • 使用 1 个 A -> 剩余目标是 10 - 5,使用 3 个 B(或更多)
  • 使用 2 of A -> 剩余目标是 10 - 10,使用 0 B(或更多)

A被最大化,所以我们完成了。

请注意,我通过降低 Power 对 A 和 B 进行了排序。未排序的列表也可以,但您会产生“无用”的边界点。例如,我们会得到 [1 B; 2A]和[2B;2 A]。

这个想法可以扩展到完全递归,沿着

Given a list of Products and a remaining Target power to achieve,
If the Product is the last one in the list, use ceiling of Target/product Power,
Else take every possible combination of the head product from 0 to max, and
Search deeper, decreasing Target Power by the units supplied by the Product selected.

下面是一个简单的 F# 实现,可以很容易地对其进行改进,并有望传达这个想法。units 函数返回具有提供目标 Power 所需的 Power 值的产品的最小单位数,递归函数 solve 将组合构建为解决方案列表、具有产品 ID 的元组和要使用的单位数:

type Product = { Id: string; Power: int }
let A = { Id = "A"; Power = 5 }
let B = { Id = "B"; Power = 9 }
let C = { Id = "C"; Power = 15 }

let products = [ A; B; C ] |> List.sortBy(fun e -> - e.Power)

let units (target: int) (value: int) =
    if target < 0
    then 0
    else
        (float)target / (float)value |> ceil |> (int)

let rec solve (products: Product list) 
              (current: (string * int) list) 
              (solutions: (string * int) list list) 
              (target: int) =
    match products with
    | [ ] -> [ ]
    | [ one ] -> ((one.Id, (units target one.Power)) :: current) :: solutions
    | hd :: tl ->
        let max = units target hd.Power
        [ 0 .. max ]
        |> List.fold (fun s u ->
            solve tl ((hd.Id, u) :: current) s (target - u * hd.Power)) solutions

我会这样运行它:

> solve [B;A] [] [] 65;;
Real: 00:00:00.001, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it : (string * int) list list =
  [[("A", 0); ("B", 8)]; [("A", 1); ("B", 7)]; [("A", 3); ("B", 6)];
   [("A", 4); ("B", 5)]; [("A", 6); ("B", 4)]; [("A", 8); ("B", 3)];
   [("A", 10); ("B", 2)]; [("A", 12); ("B", 1)]; [("A", 13); ("B", 0)]]

请注意,解决方案的数量会很快增加。我运行了您的示例,它产生了 28 个解决方案。随着产品数量和目标功率的增加,边界解决方案的数量将扩大很多。

我根本无法用 PHP 编码,但我认为它支持递归 - 也许有人会在 PHP 中展示递归解决方案?无论如何,我希望这会有所帮助。

一个有趣的附带问题是,如果可以以非整数数量购买产品,那么问题会有多不同。在那种情况下,边界实际上是一个表面(我相信是一个多面体);如何充分描述它将是一个有趣的问题!

原始答案

除非我误解了您的问题,否则您所描述的是优化中称为整数线性规划问题的问题,并使用完善的算法来解决这些问题。您的问题听起来像是饮食问题的变体(给定成分,找到最便宜的方法来获得足够的卡路里来生存),这是线性规划的原型之一,具有整数变量约束。

首先,您的问题的解决方案有无数种解决方案;假设 5 x A 是您的问题的解决方案,那么任何超过 5 个单位的 A 组合也将满足您的要求。

编辑:我意识到我可能误解了您的问题 - 我假设您可以购买任意数量的每种产品。如果每个只能购买 1 件,这是一个更简单的问题:它仍然是一个整数规划问题,但更简单的是背包问题。

另请注意,如果您可以使用非整数数量的产品(对您而言似乎并非如此),那么您的问题将更容易解决。

重述问题的最明显方法,使其成为可以相当容易解决的标准优化问题:

找到具有最小总成本的 n 个产品的组合,受限于提供的总能量高于所需阈值。(我假设总成本和交付的总能量都是购买的 A、B、C 数量的线性函数)。

我认为这实际上是您真正想要的 - 解决您问题的最佳方法。如果您真的对枚举所有解决方案感兴趣,那么一种方法是确定定义可行集的边界(即几何边界,这样如果您站在一边,您就知道这不是解决方案,否则就是)。如果您使用的数字不必是整数,这会容易得多。

希望这可以帮助!

于 2012-11-03T19:34:55.230 回答
4

对这个特定问题的简单观察可能会帮助人们解决这个问题。此处分配功率和成本的方式。使用产品 B,您可以获得最大的性价比。事实上,您唯一会使用产品 C 的时间是您需要 15 倍或 28-30 倍的功率。

因此,对于 30 以上所需的任何功率,只需使用整数除法即可通过以下方式获得所需的产品 B 的数量:

int num_productB = power_needed/9;

然后通过以下方式了解您需要多少电量:

int leftover = power_needed % 9;

如果剩余大于 5,就多加 1 个 Product B,否则使用 1 个 Product A:

if(leftover > 5)
     num_productB++;
else
     productA = 1;

完整的功能看起来像这样:

function computeBestCombination($power_needed){
$power_results = array();
//index 0 = Product A
//index 1 = Product B
//index 2 = Product C
if($power_needed == 15){
    $power_results[0] = 0;
    $power_results[1] = 0;
    $power_results[2] = 1;
}
else if($power_needed >= 28 && $power_needed <= 30)
    $power_results[0] = 0;
    $power_results[1] = 0;
    $power_results[2] = 2;
else{
    $power_results[1] = $power_needed / 9;
    $left_over = $power_needed % 9;
    if($left_over > 5){
        $power_results[1]++;
    }
    else{
        $power_results[0] = 1;
    }
    $power_results[2] = 0;
}
return $power_results;
}
于 2012-11-03T16:57:46.330 回答
1

检查此代码:

<?php
$products = array(5 => 50, 9 => 80, 15 => 140);

$power = 65;
$output = array();

function calculate_best_relation($products, $power, &$output) {
  $aux = array_keys($products);
  sort($aux);

  $min = $aux[0];
  if ($power <= $min) {
    $output[] = $min;
    return $output;
  }
  else {
    //Calculate best relation
    $relations = array();
    foreach ($products as $p => $c) {
      $relations[$p] = $c / $p;
    }
    asort($relations);

    foreach($relations as $p => $c) {
      if ($power > $c) {
        $output[] = $p;
        $power -= $c;
        calculate_best_relation($products, $power, $output);
        break;
      }
    }
  }
}

calculate_best_relation($products, $power, $output);

print_r($output);
?>

这将打印:

数组 ( [0] => 9 [1] => 9 [2] => 9 [3] => 9 [4] => 9 [5] => 9 [6] => 9 [7] => 5 )

这是正确的解决方案。

PD:当然你可以优化这个功能。

于 2012-11-03T17:33:08.597 回答
1

像纸浆这样的整数编程包将使这变得容易。

这是一个很好的例子,它将指导您完成整个过程。

安装 python 然后easy_install 纸浆,这将工作。

代码也应该易于阅读和遵循。

__author__ = 'Robert'
import pulp

def get_lp_problem(products, required_power):
    prob = pulp.LpProblem("MyProblem", pulp.LpMinimize)
    total_cost = []
    total_power = []
    for product in products:
        var = pulp.LpVariable(product.name,
            lowBound=0,
            upBound=None,
            cat=pulp.LpInteger)
        total_cost.append(var * product.cost)
        total_power.append(var * product.power)

    prob += sum(total_power) >= required_power #ensure we have required power
    prob += sum(total_cost) #minimize total cost!
    return prob

def solve(products, required_power):
    lp_prob = get_lp_problem(products, required_power)
    lp_prob.solve()
    print lp_prob.solutionTime #0.01 seconds
    for var in lp_prob.variables():
        print var.name, var.varValue


from collections import namedtuple
Product = namedtuple("Product", "name, power, cost")
products = [
    Product('A', 5, 50),
    Product('B', 9, 80),
    Product('C', 15, 140)
]
solve(products, 7)
"""
A 0.0
B 1.0
C 0.0

cost = 0*50 + 1*80 + 0*140 = 80
power = 0*5 + 1*9 + 0*15 = 9
"""

solve(products, 65)
"""
A 1.0
B 5.0
C 1.0

cost = 1*50 + 5*80 + 1*140 = 590
power = 1*5 + 5*9 + 1*15 = 65
"""

更多产品:

products = [Product(i, i, i-i/100) for i in range(1000)]
solve(products, 12345)
"""
solution time: 0.0922736688601
1 45.0
100 123.0
power = 123*100 + 45*1 =12345  
"""
于 2012-11-08T11:01:39.830 回答
1

使用动态编程可以很好地解决这个问题。诀窍在于找到越来越大的值与以前的较小值之间的数学关系。

因此,设C(p)p功率的成本。然后我们从您的基本案例中知道以下内容:

假设我有三种产品:

产品 A 将提供 5 个功率。费用 50。

产品 B 将提供 9 个功率。费用80。

产品 C 将提供 15 个功率。费用 140。

C(5) = 50
C(9) = 80
C(15) = 140

您可以根据需要定义基本情况。大概C(0) = 0,但没有给出。

然后诀窍是找到解决这个问题的递归。使用给定的值,我们得到

C(p) = Min(C(p-5) + 50, C(p-9) + 80, C(p-15) + 140)

更一般地说,您必须遍历每个基本案例并查看哪种方式更便宜。

所以现在你有两种方法来构建你的解决方案:递归或使用动态编程。考虑到递归函数,前者更容易,但显然效率很低。那么,另一种方法是从底部开始并迭代构建您的解决方案。

假设您想找到 p 功率的成本。然后以下伪代码将起作用:

// Create an array big enough to hold elements 0 through p inclusive.
var solution = new Array(p+1);

// Initialize the array with the base cases.
for each base case b:
  solution[power(b)] = cost(b);

// Now we build the array moving forward
for i from 0 to p:
  // Start with a really big number
  solution[i] = +Infinity;

  // Iterate over base case to see what the cheapest way to get i power is.
  for each base case b:
    solution[i] = min(solution[i], solution[i - power(b)] + cost(b);

// The final answer is the last element in the array, but you get everything
// else for free. You can even work backwards and figure out the cheapest
// combination!
return solution[p]

分析留给读者作为练习:-)

于 2012-11-12T01:27:23.180 回答
0

您要优化以下功能

$cost = $amountOfProductA * $costOfProductA + $amountOfProductB * $costOfProductB + $amountOfProductC * $costOfProductC

有以下限制

$powerDeliveredByA * $amountOfProductA + $powerDeliveredByB * $amountOfProductB + $powerDeliveredByC * $amountOfProductC = 65

因此,这些行找到产生 65(或接近 65,使用您必须设置的可接受阈值)的解决方案,然后按成本对解决方案数组进行排序,并获取解决方案数组的第一个元素:

$requiredPower = 65;
$productA = array('amount' => 0, 'cost' => 50, 'powerDelivered' => 5);
$productB = array('amount' => 0, 'cost' => 80, 'powerDelivered' => 9);
$productC = array('amount' => 0, 'cost' => 140, 'powerDelivered' => 15);
$increment = 0.01;
$threshold = 0.01;
$solutions = array();
while($productA['amount'] * $productA['powerDelivered'] < $requiredPower)
{
    $productC['amount'] = 0;
    while($productB['amount'] * $productB['powerDelivered'] < $requiredPower)
    {
        $productC['amount'] = 0;
        while($productC['amount'] * $productC['powerDelivered'] < $requiredPower)
        {
            if($productA['amount'] * $productA['powerDelivered'] + $productB['amount'] * $productB['powerDelivered'] + $productC['amount'] * $productC['powerDelivered'] > $requiredPower + $threshold)
            {
                break;
            }
            if(isWithinThreshold($productA['powerDelivered'] * $productA['amount'] + $productB['powerDelivered'] * $productB['amount'] + $productC['powerDelivered'] * $productC['amount'], $requiredPower, $threshold))
            {
                //var_dump($productA['powerDelivered'] * $productA['amount'] + $productB['powerDelivered'] * $productB['amount'] + $productC['powerDelivered'] * $productC['amount']);
                $cost = $productA['amount'] * $productA['cost'] + $productB['amount'] * $productB['cost'] + $productC['amount'] * $productC['cost'];
                $solutions[number_format($cost,10,'.','')] = array('cost' => $cost, 'qA' => $productA['amount'], 'qB' => $productB['amount'], 'qC' => $productC['amount']);
            }
            $productC['amount'] = $productC['amount'] + $increment;
        }
        $productB['amount'] = $productB['amount'] + $increment;
    }
    $productA['amount'] = $productA['amount'] + $increment;
}
ksort($solutions, SORT_NUMERIC);
$minimumCost = array_shift($solutions);
var_dump($minimumCost);

//checks if $value1 is within $value2 +- $threshold
function isWithinThreshold($value1, $value2, $threshold)
{
    if($value1 >= $value2 - $threshold && $value1 <= $value2 + $threshold)
    {
        return true;
    }
}

这里描述了优化函数的方法:函数优化

于 2012-11-03T16:45:55.430 回答