用动态规划解决多重背包问题(MCKP)?

3

示例数据

对于这个问题,让我们假设以下物品:

  • 物品:苹果、香蕉、胡萝卜、牛排、洋葱
  • 价值:2、2、4、5、3
  • 重量:3、1、3、4、2
  • 最大重量:7

目标:

MCKP是一种带有附加约束条件的背包问题,"[T]该问题将物品划分为k类...每个类别必须选择一个物品"。

我已经编写了使用递归调用和记忆化的动态规划来解决01背包问题的代码。我的问题是是否可以将此约束条件添加到当前的解决方案中?例如,如果我的类别是水果、蔬菜、肉类(从示例中),我需要包含每种类型的1个物品。这些类别也可以是类型1、2、3。

此外,我认为这可以通过线性规划和求解器来解决,但如果可能的话,我想在这里理解答案。

当前代码:

<?php
$value = array(2, 2, 4, 5, 3);
$weight = array(3, 1, 3, 4, 2);

$maxWeight = 7;
$maxItems = 5;

$seen = array(array()); //2D array for memoization
$picked = array();

//Put a dummy zero at the front to make things easier later.
array_unshift($value, 0);
array_unshift($weight, 0);

//Call our Knapsack Solver and return the sum value of optimal set
$KSResult = KSTest($maxItems, $maxWeight, $value, $weight);
$maxValue = $KSResult; //copy the result so we can recreate the table

//Recreate the decision table from our memo array to determine what items were picked
//Here I am building the table backwards because I know the optimal value will be at the end
for($i=$maxItems; $i > 0; $i--) {
        for($j=$maxWeight; $j > 0; $j--) {
            if($seen[$i][$j] != $seen[$i-1][$j]
                && $maxValue == $seen[$i][$j]) {
                array_push($picked, $i);
                $maxValue -= $value[$i];
                break;
            }
        }
}

//Print out picked items and max value
print("<pre>".print_r($picked,true)."</pre>");
echo $KSResult;


//  Recursive formula to solve the KS Problem
//  $n = number of items to check
//  $c = total capacity of bag
function KSTest($n, $c, &$value, &$weight) {
    global $seen;

    if(isset($seen[$n][$c])) {
        //We've seen this subproblem before
        return $seen[$n][$c];
    }
    if($n === 0 || $c === 0){
        //No more items to check or no more capacity
        $result = 0;
    }
    elseif($weight[$n] > $c) {
        //This item is too heavy, check next item without this one
        $result = KSTest($n-1, $c, $value, $weight);
    }
    else {
        //Take the higher result of keeping or not keeping the item
        $tempVal1 = KSTest($n-1, $c, $value, $weight);
        $tempVal2 = $value[$n] + KSTest($n-1, $c-$weight[$n], $value, $weight);

        if($tempVal2 >= $tempVal1) {
            $result = $tempVal2;
            //some conditions could go here? otherwise use max()
        }
        else {
            $result = $tempVal1;
        }
    }
    //memo the results and return
    $seen[$n][$c] = $result;
    return $result;
}
?>

我尝试过的方法:

  1. 我的第一个想法是添加一个类(k)数组,通过类(k)对项目进行排序,并在我们选择选择与下一个项目相同的项目时,检查是保留当前项目还是不包括下一个项目的项目更好。看起来很有希望,但是在检查了几个项目后就崩溃了。大致代码如下: $tempVal3 = $value[$n] + KSTest($n-2, $c-$weight[$n]); max( $tempVal2, $tempVal3);
  2. 另一个想法是,在函数调用时,我可以为每种类别调用一个循环,并仅解决该类型的每次选择1项 +其余值的KS问题。这肯定会做出一些假设,因为集合1的结果可能仍然假定集合2的倍数,例如。

这似乎是方程式(如果你擅长阅读所有这些符号?):)和C ++实现?但我真的看不到类约束发生的地方?

3个回答

1
C++实现看起来不错。

您当前的PHP实现中的一维数组值和权重将变为二维数组。

例如,values[i][j]将是类别 i 中第j个项目的值。在weights[i][j]的情况下也是如此。您将只选取每个类别 i 的一个项目并在最大化条件的同时向前移动。

C++实现还在备忘录中进行了优化。它只保留了两个大小符合max_weight条件的数组,即当前状态和上一个状态。这是因为您只需要这两个状态来计算当前状态。

回答您的疑问:

1)

我的第一个想法是添加一个类(k)数组,通过类(k)对项目进行排序,当我们选择选择与下一个项目相同的项目时,检查保留当前项目还是不包括下一个项目的项目是否更好。看起来很有前途,但在检查了几个项目后就崩溃了。像这样:$tempVal3 = $value[$n] + KSTest($n-2, $c-$weight[$n]); max( $tempVal2, $tempVal3); 这样做行不通,因为可能会有一些属于类k+1的项目,您需要采取最优值并尊重约束条件,而对于类k,您需要采取次优值。因此,在达到约束条件时,排序并选择最佳选项将无法工作。如果未达到约束条件,则始终可以选择具有最佳重量的最佳值。
另一个想法是,在函数调用时,我可以为每种类别调用一个循环,并仅使用该类型的1个项目和其余值解决KS问题。

是的,你走在正确的道路上。假设你已经解决了前k类问题。现在你将尝试使用第k+1类的值来扩展,同时遵守权重约束。

3)

...但我真的看不出班级限制在哪里发生了?

for (int i = 1; i < weight.size(); ++i) {
    fill(current.begin(), current.end(), -1);
    for (int j = 0; j < weight[i].size(); ++j) {
        for (int k = weight[i][j]; k <= max_weight; ++k) {
            if (last[k - weight[i][j]] > 0)
                current[k] = max(current[k],
                                 last[k - weight[i][j]] + value[i][j]);
        }
    }
    swap(current, last);
}

在上面的C ++片段中,第一个循环迭代类,第二个循环迭代类的值,第三个循环使用前一个状态last和每次只有一个j项的class i 扩展当前状态current。由于您只使用前一个状态last和当前类的1项来扩展和最大化,因此您遵循约束条件。
时间复杂度为O(total_items x max_weight),相当于O(class x max_number_of_items_in_a_class x max_weight)。

0
这是我的PHP解决方案。我尝试以易于理解的方式对代码进行注释。
更新: 我更新了代码,因为旧脚本会给出不可靠的结果。这个更干净,并经过了彻底的测试。关键是我使用了两个备忘录数组,一个在组级别上加速执行,另一个在项目级别上重构结果。我发现任何尝试跟踪正在选择哪些项目的方法都是不可靠的,而且效率要低得多。此外,isset()而不是if($var)对于检查备忘录数组至关重要,因为之前的结果可能是0 ;)
<?php
/**
* Multiple Choice Knapsack Solver
*
* @author Michael Cruz
* @version 1.0 - 03/27/2020
**/
class KS_Solve {
    public $KS_Items;
    public $maxValue;
    public $maxWeight;
    public $maxItems;
    public $finalValue;
    public $finalWeight;
    public $finalItems;
    public $finalGroups;
    public $memo1 = array(); //Group memo
    public $memo2 = array(); //Item memo for results rebuild

    public function __construct() {
        //some default variables as an example.

        //KS_Items = array(Value, Weight, Group, Item #)
        $this->KS_Items = array(
            array(2, 3, 1, 1),
            array(2, 1, 1, 2),
            array(4, 3, 2, 3),
            array(5, 4, 2, 4),
            array(3, 2, 3, 5)
        );

        $this->maxWeight = 7;
        $this->maxItems = 5;
        $this->KS_Wrapper();
    }

    public function KS_Wrapper() {
        $start_time = microtime(true); 

        //Put a dummy zero at the front to make things easier later.
        array_unshift($this->KS_Items, array(0, 0, 0, 0));

        //Call our Knapsack Solver
        $this->maxValue = $this->KS_Solver($this->maxItems, $this->maxWeight);

        //Recreate the decision table from our memo array to determine what items were picked
        //ksort($this->memo2); //for debug
        for($i=$this->maxItems; $i > 0; $i--) {
            //ksort($this->memo2[$i]); //for debug
            for($j=$this->maxWeight; $j > 0; $j--) {
                if($this->maxValue == 0) {
                    break 2;
                }
                if($this->memo2[$i][$j] == $this->maxValue
                    && $j == $this->maxWeight) {
                    $this->maxValue -= $this->KS_Items[$i][0];
                    $this->maxWeight -= $this->KS_Items[$i][1];
                    $this->finalValue += $this->KS_Items[$i][0];
                    $this->finalWeight += $this->KS_Items[$i][1];
                    $this->finalItems .= " " . $this->KS_Items[$i][3];
                    $this->finalGroups .= " " . $this->KS_Items[$i][2];
                    break;
                }
            }
        }

        //Print out the picked items and value. (IMPLEMENT Proper View or Return!)
        echo "<pre>";
        echo "RESULTS: <br>";
        echo "Value: " . $this->finalValue . "<br>";
        echo "Weight: " . $this->finalWeight . "<br>";
        echo "Item's in KS:" . $this->finalItems . "<br>";
        echo "Selected Groups:" . $this->finalGroups . "<br><br>";
        $end_time = microtime(true); 
        $execution_time = ($end_time - $start_time); 
        echo "Results took " . sprintf('%f', $execution_time) . " seconds to execute<br>";

    }

    /**
    *  Recursive function to solve the MCKS Problem
    *  $n = number of items to check
    *  $c = total capacity of KS   
    **/
    public function KS_Solver($n, $c) {
        $group = $this->KS_Items[$n][2];
        $groupItems = array();
        $count = 0;
        $result = 0;
        $bestVal = 0;

        if(isset($this->memo1[$group][$c])) {
            $result = $this->memo1[$group][$c];
        }
        else {
            //Sort out the items for this group
            foreach($this->KS_Items as $item) {
                if($item[2] == $group) {
                    $groupItems[] = $item;
                    $count++;
                }
            }
            //$k adjusts the index for item memoization
            $k = $count - 1;

            //Find the results of each item + items of other groups
            foreach($groupItems as $item) {
                if($item[1] > $c) {
                    //too heavy
                    $result = 0;
                }
                elseif($item[1] >= $c && $group != 1) {
                    //too heavy for next group
                    $result = 0;
                }
                elseif($group == 1) {
                    //Just take the highest value
                    $result = $item[0];
                }
                else {
                    //check this item with following groups
                    $result = $item[0] + $this->KS_Solver($n - $count, $c - $item[1]);
                }

                if($result == $item[0] && $group != 1) {
                    //No solution with the following sets, so don't use this item.
                    $result = 0;
                }

                if($result > $bestVal) {
                    //Best item so far
                    $bestVal = $result;
                }
                //memo the results
                $this->memo2[$n-$k][$c] = $result;
                $k--;
            }
            $result = $bestVal;
        }

        //memo and return
        $this->memo1[$group][$c] = $result;
        return $result;
    }
}
new KS_Solve();
?>

0

我不是PHP程序员,但我会尝试用良好的解释写出伪代码。

在原始问题中,每个单元格的含义是:“使用1到项填充背包,直到达到容量的价值”,您提供的链接中的解决方案将每个单元格定义为“使用来自桶1到项的物品填充背包,直到达到容量的价值”。请注意,在这种变化中,不存在从类别中不取元素的情况。

因此,在每个步骤(每次调用KSTest$n$c),我们需要找到要从第n个类中选择哪个元素,使得该元素的重量小于c 并且它的价值+KSTest(n-1,c-w)最大。

因此,我认为您应该仅更改else ifelse语句,使其类似于:

else {
    $result = 0
    for($i=0; $i < $number_of_items_in_nth_class; $i++) {
        if ($weight[$n][$i] > $c) {
            //This item is too heavy, check next item
            continue;
        }
        $result = max($result, KSTest($n-1, $c - $weight[$n][$i], $value, $weight));
    }
}

现在有两个免责声明:

  1. 我不会编写 PHP 代码,所以这段代码无法运行 :)

  2. 这不是你提供的链接中给出的实现,说实话,我不明白为什么他们算法的时间复杂度如此之小(还有什么是 C),但是这个实现应该可以工作,因为它遵循了给定递归公式的定义。

这个的时间复杂度应该是 O(max_weight * number_of_classes * size_of_largerst_class)


感谢您抽出时间帮助解决这个问题。我对实现还有点困惑。您是在建议for循环基本上会在我测试$tempVal2的地方进行吗?$weight是否成为一个二维数组,还是您只是说每个类别中的每个项目都是$weight[$i]?如果有帮助的话,这是我正在遵循的伪代码:https://youtu.be/xOlhR_2QCXY - MikeCruz13
如果我表述不清楚,对不起。我建议的代码是替代else ifelse语句的。你说得对,现在权重是一个二维数组,但可以按照你想要的方式实现它,以便能够访问特定类别的所有权重。 - Yonlif

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接