Уравнение ранца с группами предметов
Невозможно называть это проблемой для, но в настоящее время я пытаюсь понять, как интегрировать ограничения в виде групп элементов в рамках проблемы с Knapsack. Мои математические навыки в этой ситуации довольно ограничены, однако я очень заинтересован в том, чтобы сделать эту работу так, как было запланировано, а также выяснить, что делает каждый аспект (в том порядке, поскольку вещи имеют больше смысла, когда они работают).
С учетом сказанного я нашел абсолютно красивую реализацию в Rosetta Code и очистил имена переменных, чтобы помочь мне лучше понять это с очень простой точки зрения.
К сожалению, мне очень трудно понять, как я могу применить эту логику для включения групп элементов. Моя цель состоит в том, чтобы строить команды фэнтези, поставляя свою собственную ценность и вес (очки/зарплату) на одного игрока, но без групп (позиции в моем случае). Я не могу этого сделать.
Может ли кто-нибудь указать мне в правильном направлении? Я просматриваю примеры кода с других языков и дополнительные описания проблемы в целом, однако я хотел бы, чтобы группы были реализованы любым возможным способом.
<?php
function knapSolveFast2($itemWeight, $itemValue, $i, $availWeight, &$memoItems, &$pickedItems)
{
global $numcalls;
$numcalls++;
// Return memo if we have one
if (isset($memoItems[$i][$availWeight]))
{
return array( $memoItems[$i][$availWeight], $memoItems['picked'][$i][$availWeight] );
}
else
{
// At end of decision branch
if ($i == 0)
{
if ($itemWeight[$i] <= $availWeight)
{ // Will this item fit?
$memoItems[$i][$availWeight] = $itemValue[$i]; // Memo this item
$memoItems['picked'][$i][$availWeight] = array($i); // and the picked item
return array($itemValue[$i],array($i)); // Return the value of this item and add it to the picked list
}
else
{
// Won't fit
$memoItems[$i][$availWeight] = 0; // Memo zero
$memoItems['picked'][$i][$availWeight] = array(); // and a blank array entry...
return array(0,array()); // Return nothing
}
}
// Not at end of decision branch..
// Get the result of the next branch (without this one)
list ($without_i,$without_PI) = knapSolveFast2($itemWeight, $itemValue, $i-1, $availWeight,$memoItems,$pickedItems);
if ($itemWeight[$i] > $availWeight)
{ // Does it return too many?
$memoItems[$i][$availWeight] = $without_i; // Memo without including this one
$memoItems['picked'][$i][$availWeight] = array(); // and a blank array entry...
return array($without_i,array()); // and return it
}
else
{
// Get the result of the next branch (WITH this one picked, so available weight is reduced)
list ($with_i,$with_PI) = knapSolveFast2($itemWeight, $itemValue, ($i-1), ($availWeight - $itemWeight[$i]),$memoItems,$pickedItems);
$with_i += $itemValue[$i]; // ..and add the value of this one..
// Get the greater of WITH or WITHOUT
if ($with_i > $without_i)
{
$res = $with_i;
$picked = $with_PI;
array_push($picked,$i);
}
else
{
$res = $without_i;
$picked = $without_PI;
}
$memoItems[$i][$availWeight] = $res; // Store it in the memo
$memoItems['picked'][$i][$availWeight] = $picked; // and store the picked item
return array ($res,$picked); // and then return it
}
}
}
$items = array("map","compass","water","sandwich","glucose","tin","banana","apple","cheese","beer","suntan cream","camera","t-shirt","trousers","umbrella","waterproof trousers","waterproof overclothes","note-case","sunglasses","towel","socks","book");
$weight = array(9,13,153,50,15,68,27,39,23,52,11,32,24,48,73,42,43,22,7,18,4,30);
$value = array(150,35,200,160,60,45,60,40,30,10,70,30,15,10,40,70,75,80,20,12,50,10);
## Initialize
$numcalls = 0;
$memoItems = array();
$selectedItems = array();
## Solve
list ($m4, $selectedItems) = knapSolveFast2($weight, $value, sizeof($value)-1, 400, $memoItems, $selectedItems);
# Display Result
echo "<b>Items:</b><br>" . join(", ", $items) . "<br>";
echo "<b>Max Value Found:</b><br>$m4 (in $numcalls calls)<br>";
echo "<b>Array Indices:</b><br>". join(",", $selectedItems) . "<br>";
echo "<b>Chosen Items:</b><br>";
echo "<table border cellspacing=0>";
echo "<tr><td>Item</td><td>Value</td><td>Weight</td></tr>";
$totalValue = 0;
$totalWeight = 0;
foreach($selectedItems as $key)
{
$totalValue += $value[$key];
$totalWeight += $weight[$key];
echo "<tr><td>" . $items[$key] . "</td><td>" . $value[$key] . "</td><td>".$weight[$key] . "</td></tr>";
}
echo "<tr><td align=right><b>Totals</b></td><td>$totalValue</td><td>$totalWeight</td></tr>";
echo "</table><hr>";
?>
Ответы
Ответ 1
Эта ранцевая программа традиционна, но я думаю, что она затушевывает то, что происходит. Позвольте мне показать вам, как DP можно получить более прямо из решения грубой силы.
В Python (извините, это мой язык сценариев выбора), решение грубой силы может выглядеть так. Во-первых, существует функция для генерации всех подмножеств с широким поиском (это важно).
def all_subsets(S): # brute force
subsets_so_far = [()]
for x in S:
new_subsets = [subset + (x,) for subset in subsets_so_far]
subsets_so_far.extend(new_subsets)
return subsets_so_far
Тогда есть функция, которая возвращает True
, если решение действительно (в пределах бюджета и с правильным разбиением позиции) - вызовите его is_valid_solution
- и функцию, которая при решении возвращает общее значение игрока (total_player_value
). Предполагая, что players
- список доступных игроков, оптимальным решением является это.
max(filter(is_valid_solution, all_subsets(players)), key=total_player_value)
Теперь для DP добавим функцию cull
в all_subsets
.
def best_subsets(S): # DP
subsets_so_far = [()]
for x in S:
new_subsets = [subset + (x,) for subset in subsets_so_far]
subsets_so_far.extend(new_subsets)
subsets_so_far = cull(subsets_so_far) ### This is new.
return subsets_so_far
Что cull
заключается в том, чтобы отбросить частичные решения, которые, очевидно, не будут упущены в нашем поиске оптимального решения. Если частичное решение уже превышает бюджет, или если у него уже слишком много игроков в одной позиции, тогда его можно безопасно отбросить. Пусть is_valid_partial_solution
- функция, проверяющая эти условия (она, вероятно, очень похожа на is_valid_solution
). Пока у нас это есть.
def cull(subsets): # INCOMPLETE!
return filter(is_valid_partial_solution, subsets)
Другим важным тестом является то, что некоторые частичные решения лучше других. Если два частичных решения имеют одинаковое разбиение по положению (например, два вперед и центр) и стоят одинаково, тогда нам нужно только сохранить более ценный. Пусть cost_and_position_breakdown
принимает решение и создает строку, которая кодирует указанные атрибуты.
def cull(subsets):
best_subset = {} # empty dictionary/map
for subset in filter(is_valid_partial_solution, subsets):
key = cost_and_position_breakdown(subset)
if (key not in best_subset or
total_value(subset) > total_value(best_subset[key])):
best_subset[key] = subset
return best_subset.values()
Что это. Там будет много оптимизации (например, отбросьте частичные решения, для которых есть более дешевое и более ценное частичное решение, измените структуры данных, чтобы мы не всегда вычисляли разброс значений и позиций с нуля и уменьшали расходы на хранение), но его можно решать постепенно.
Ответ 2
Одно потенциальное небольшое преимущество в отношении компоновки рекурсивных функций в PHP заключается в том, что переменные передаются по значению (что означает копирование), а не по ссылке, что может спасти шаг или два.
Возможно, вам лучше понять, что вы ищете, включив образец ввода и вывода. Вот пример, который делает комбинации из заданных групп - я не уверен, что это ваше намерение... Я сделал раздел, получающий частичный результат, позволяющий учитывать комбинации с меньшим значением, если их вес ниже - все это можно изменить чтобы обрезать конкретные способы, которые вы хотели бы.
function make_teams($players, $position_limits, $weights, $values, $max_weight){
$player_counts = array_map(function($x){
return count($x);
}, $players);
$positions = array_map(function($x){
$positions[] = [];
},$position_limits);
$num_positions = count($positions);
$combinations = [];
$hash = [];
$stack = [[$positions,0,0,0,0,0]];
while (!empty($stack)){
$params = array_pop($stack);
$positions = $params[0];
$i = $params[1];
$j = $params[2];
$len = $params[3];
$weight = $params[4];
$value = $params[5];
// too heavy
if ($weight > $max_weight){
continue;
// the variable, $positions, is accumulating so you can access the partial result
} else if ($j == 0 && $i > 0){
// remember weight and value after each position is chosen
if (!isset($hash[$i])){
$hash[$i] = [$weight,$value];
// end thread if current value is lower for similar weight
} else if ($weight >= $hash[$i][0] && $value < $hash[$i][1]){
continue;
// remember better weight and value
} else if ($weight <= $hash[$i][0] && $value > $hash[$i][1]){
$hash[$i] = [$weight,$value];
}
}
// all positions have been filled
if ($i == $num_positions){
$positions[] = $weight;
$positions[] = $value;
if (!empty($combinations)){
$last = &$combinations[count($combinations) - 1];
if ($weight < $last[$num_positions] && $value > $last[$num_positions + 1]){
$last = $positions;
} else {
$combinations[] = $positions;
}
} else {
$combinations[] = $positions;
}
// current position is filled
} else if (count($positions[$i]) == $position_limits[$i]){
$stack[] = [$positions,$i + 1,0,$len,$weight,$value];
// otherwise create two new threads: one with player $j added to
// position $i, the other thread skipping player $j
} else {
if ($j < $player_counts[$i] - 1){
$stack[] = [$positions,$i,$j + 1,$len,$weight,$value];
}
if ($j < $player_counts[$i]){
$positions[$i][] = $players[$i][$j];
$stack[] = [$positions,$i,$j + 1,$len + 1
,$weight + $weights[$i][$j],$value + $values[$i][$j]];
}
}
}
return $combinations;
}
Вывод:
$players = [[1,2],[3,4,5],[6,7]];
$position_limits = [1,2,1];
$weights = [[2000000,1000000],[10000000,1000500,12000000],[5000000,1234567]];
$values = [[33,5],[78,23,10],[11,101]];
$max_weight = 20000000;
echo json_encode(make_teams($players, $position_limits, $weights, $values, $max_weight));
/*
[[[1],[3,4],[7],14235067,235],[[2],[3,4],[7],13235067,207]]
*/