Рассчитайте, какие продукты вместе предоставят запрашиваемую мощность

Скажем, у меня есть три продукта:

Продукт A Будет доставлять 5 ед. Затраты 50.

Продукт B Обеспечивает 9 мощностей. Расходы 80.

Продукт C Поставляет мощность 15. Затраты 140.

Я хочу знать, какую комбинацию продуктов я могу купить, когда мне нужно 7 power. Я мог бы купить два 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 и один раз A. Как мне это сделать?

EDIT. Количество продуктов является переменной. Кроме того, конкретная стоимость и мощность являются переменными. Таким образом, может быть 10 продуктов с более чистыми и более дорогими ценниками.

EDIT 2 Как я уже говорил выше, я хочу рассчитать возможные комбинации (множественное число). Некоторые люди, похоже, пропустили это в моем описании.

Ответы

Ответ 1

Введение

Это была бы проблема Knapsack, но поскольку вы не просто ищете оптимальное решение, вы также хотите найти всю возможную комбинацию

Затем вы можете решить эту проблему Задача суммы подмножества + Изменение монеты:

  • Список всех возможных Комбинация, а не только общая комбинация
  • Получить лучшую комбинацию

    Например,, для 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);
        }
    }
}

Ответ 2

Обновленный ответ

Я согласен с моим первоначальным ответом, но с тех пор получил явное решение. К сожалению, я не разбираюсь в PHP, поэтому реализация, которую я представляю, находится в (плохо написанной) F #.

То, что делает ваш вопрос интересным, заключается в том, что вы не ищете лучшего решения, но для всех возможных решений. Как я указывал в своем первоначальном ответе, это сложно, потому что множество возможных решений бесконечно. В качестве иллюстрации, если вы хотите создать 65 единиц, вы можете использовать 13xA, что дает мощность 5x13 = 65. Но тогда, очевидно, любое решение, которое содержит более 13 единиц A, также будет решением.

Вы не можете вернуть бесконечный набор из функции. Здесь вам понадобится набор всех "граничных" случаев:

  • Если решение содержит столько же единиц, сколько граничный случай для всех продуктов, оно действительно
  • если блок можно удалить из граничного случая, и это все еще возможно, это уже невозможно.

Например, решение S = {A = 13; B = 0; C = 0} является граничным случаем. Удалите одну единицу из любого продукта, и это невозможно - и если комбинация такова, что для каждого продукта она содержит больше единиц, чем S, это допустимое решение, но "доминируемое" по S.

Другими словами, мы не можем вернуть все возможные решения, но мы можем вернуть "предел", который отделяет возможные и неосуществимые решения.

Отметим также, что стоимость Продуктов здесь не имеет значения - как только у вас есть набор граничных случаев, вычисление стоимости решения тривиально.

Учитывая, что вы указываете, что количество продуктов может быть произвольным, это похоже на четкий случай для рекурсии.

Если у вас нет продукта, решение тривиально пустое - решения нет. Если у вас есть 1 продукт, решение - потолок (target/product.Power) Если у вас есть 2 продукта, скажите A: 5 и B: 2, с целевым значением 10, вы могли бы

  • использовать 0 из A → оставшейся цели 10, использовать 5 B (или больше)
  • использовать 1 из A → оставшейся цели - 10 - 5, использовать 3 B (или больше)
  • использовать 2 из A → оставшейся цели - 10 - 10, использовать 0 B (или больше)

A максимизируется, поэтому мы закончили.

Обратите внимание, что я отсортировал A и B, уменьшив Power. Также будет работать несортированный список, но вы создадите "бесполезные" граничные точки. Например, мы получили бы [1 B; 2 A] и [2 B; 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 #, которая может быть легко улучшена и, надеюсь, передаст эту идею. Функция единиц возвращает минимальное количество единиц продукта с величиной мощности, необходимой для подачи целевой мощности, а рекурсивная функция решает собрать комбинации в список решений, кортежи с идентификатором продукта и количеством единиц, которые будут использоваться:

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? В любом случае, надеюсь, это поможет.

Интересным вопросом будет вопрос о том, насколько различна проблема, если продукты могут быть приобретены в нецелых количествах. В этом случае граница действительно будет поверхностью (как я полагаю, многогранник); как правильно описать это было бы интересной проблемой!

Оригинальный ответ

Если я не понимаю ваш вопрос, то, что вы описываете, это то, что известно в оптимизации, как проблема Integer Linear Programming с хорошо установленными алгоритмами для разрешить их. Ваша проблема звучит как вариация проблемы Диеты (учитывая ингредиенты, найти самый дешевый способ получить достаточное количество калорий, чтобы выжить), один из архетипов линейного программирования, с целыми переменными ограничениями.

Во-первых, решение вашей проблемы, как указано, имеет бесконечное количество решений; предположим, что 5 x A является решением вашей проблемы, тогда любая комбинация с более чем 5 единицами A также удовлетворит ваши требования.

Изменить: я понимаю, что, возможно, неправильно понял вашу проблему - я предположил, что вы можете купить любое количество каждого продукта. Если вы можете купить всего по 1 единицу, это более простая проблема: она по-прежнему является проблемой целочисленного программирования, но более простой - проблемой Knapsack.

Обратите также внимание, что если вы можете не целыми количествами продуктов (похоже, не для вас), ваша проблема значительно проще решить.

Самый очевидный способ переформулировать вашу проблему, что делает ее стандартной проблемой оптимизации, которая может быть решена довольно легко:

Найдите комбинацию n продуктов, которая имеет минимальную общую стоимость, при условии, что общая подаваемая энергия выше желаемого порога. (Я предполагаю, что как общая стоимость, так и общая сумма энергии - это линейные функции от количества приобретённых А, В, С...).

Я предполагаю, что это на самом деле то, что вы действительно хотите - наилучшее решение вашей проблемы. Если вы действительно заинтересованы в перечислении всего решения, один из способов сделать это - определить границы, которые определяют допустимое множество (т.е. Геометрическую границу, такую, что если вы находитесь на одной стороне, вы знаете, что это не решение, иначе это). Это намного проще, если вы работаете с числами, которые не должны быть целыми.

Надеюсь, это поможет!

Ответ 3

Простое наблюдение по этой конкретной проблеме может помочь людям в решении этого вопроса. Способ распределения электроэнергии и затрат здесь. Вы получаете наибольшую ценность за свои деньги с Продуктом B. Фактически, единственный раз, когда вы когда-либо использовали Продукт C, - это когда вам нужно ровно 15 мощностей или 28-30 мощности.

Итак, для любой мощности, необходимой выше 30, просто используйте целочисленное деление, чтобы получить номер продукта B, который вам нужен:

int num_productB = power_needed/9;

Затем узнайте, сколько энергии вам нужно:

int leftover = power_needed % 9;

Если оставшееся число больше 5, просто добавьте еще один продукт B, Else use 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;
}

Ответ 4

Проверьте этот код:

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

Какое правильное решение.

P.D: Конечно, вы можете оптимизировать функцию.

Ответ 5

Целый программный пакет, такой как целлюлоза, сделает это простым, как пирог.

Вот красивый пример, который поможет вам в этом процессе.

Установите python, а затем easy_install pulp, и это будет работать.

Код должен быть легко читаемым и следовать также.

__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  
"""

Ответ 6

Это довольно хорошо разрешено с помощью динамического программирования . Хитрость заключается в поиске математической взаимосвязи между все более большими значениями и предыдущими меньшими значениями.

Итак, пусть 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 power. Затем будет работать следующий псевдокод:

// 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]

Анализ оставлен как упражнение для читателя: -)

Ответ 7

Вы хотите оптимизировать следующую функцию

$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;
    }
}

Здесь описывается способ оптимизации функции: Оптимизация функций