如何使用背包算法 [而不仅仅是包的价值] 找到包中的哪些元素?

Posted

技术标签:

【中文标题】如何使用背包算法 [而不仅仅是包的价值] 找到包中的哪些元素?【英文标题】:How to find which elements are in the bag, using Knapsack Algorithm [and not only the bag's value]? 【发布时间】:2011-11-21 07:25:38 【问题描述】:

这里我有使用背包算法计算最优值的代码(装箱 NP 难题):

int Knapsack::knapsack(std::vector<Item>& items, int W)

    size_t n = items.size();
    std::vector<std::vector<int> > dp(W + 1, std::vector<int>(n + 1, 0));
    for (size_t j = 1; j <= n; j++)
    
        for ( int w = 1; w <= W; w++)
        
            if (items[j-1].getWeight() <= w)
            
                dp[w][j] = std::max(dp[w][j-1], dp[w - items[j-1].getWeight()][j-1] + items[j-1].getWeight());
            
            else
            
                dp[w][j] = dp[w][j - 1];
            
        
    
    return dp[W][n];

我还需要显示包中包含的元素。我想创建一个数组来放置所选元素。所以问题是,我可以在哪一步执行这个选择?有没有其他更有效的方法来确定哪些物品已被拿走?

我希望能够知道给我最佳解决方案的项目,而不仅仅是最佳解决方案的价值。

【问题讨论】:

有点难理解你的问题,但我猜你想知道给你最佳解决方案的项目,而不仅仅是最佳解决方案的价值? 【参考方案1】:

可以使用矩阵中的数据来获取您从矩阵中打包的元素,而无需存储任何额外的数据。

伪代码:

line <- W
i <- n
while (i > 0):
  if dp[line][i] - dp[line - weight(i)][i-1] == value(i):
      // the element 'i' is in the knapsack
      i <- i-1 // only in 0-1 knapsack
      line <- line - weight(i)
  else: 
      i <- i-1 

它背后的想法是你迭代矩阵;如果重量差异恰好是元素的大小,则它在背包中。如果不是,则该物品不在背包中,继续没有它。

【讨论】:

这是非常好的伪代码。但是使用它我只能得到添加元素的重量,我也需要他们的名字。我正在考虑做同样的事情,但是将数组 dp 更改为 Item 类型。你的观点是什么? @nightcrime: 使用这个算法,你可以确切地知道包里有哪个元素,你可以在开始这个算法之前创建一个容器[让我们称之为bag,在运行算法时:如果dp[line][i] - dp[line][i-1] == value(i) 然后是 bag.add(items[i-1]),其中 items 是背包函数的项目输入向量。在算法结束时,bag 将包含包中的所有元素,并且只包含它们。 :我知道了。但它仅在我仅添加 1 个元素时才有效。在其他方面,声明 dp[line][i] - dp[line][i-1] == value(i) 永远不会是真的。( @nightcrime:我不确定我是否在关注你,背包算法,我的回答也是如此,不允许你将项目“i”添加到包中两次 [或 3/ 4次]。如果添加元素 i、j、k:此算法将找到所有元素,因为 dp[line][i]-dp[line][i-1] == value(i)dp[line][j]-dp[line][j-1] == value(j)dp[line][k]-dp[line][k-1] == value(k) 你也可以简单地检查dp[line][i] != dp[line][i-1]。如果这是真的,那么第 i 个项目被拿走。【参考方案2】:
line <- W
i <- n
while (i> 0):
  if dp[line][i] - dp[line - weight(i) ][i-1] == value(i):
    the element 'i' is in the knapsack
    cw = cw - weight(i)
    i <- i-1
  else if dp[line][i] > dp[line][i-1]:
    line <- line - 1
  else: 
    i <- i-1

记住当你添加项目 i 时你是如何到达 dp[line][i]

dp[line][i] = dp[line - weight(i) ][i - 1] + value(i);

【讨论】:

【参考方案3】:

用于重建有界 0/1 背包中的物品的算法比该线程中的一些现有代码更简单,可能会让人相信。这个答案旨在稍微揭开这个过程的神秘面纱,并提供一个干净、直接的实现以及一个工作示例。


方法

从对应于表轴的两个索引开始:一个weight 变量初始化为背包容量,一个索引i 沿着项目轴在DP 查找表上向后循环,在索引1 处停止(算法使用i-1 所以停在 1 可以避免越界访问)。

在循环中,如果T[weight][i] != T[weight][i-1],则将项目i-1标记为选中,减去其权重并继续沿项目轴向后退。

重建的时间复杂度为O(length(items))

这是 Python 的伪代码:

def reconstruct_taken_items(T, items, capacity):
    taken = []
    weight = capacity

    for i in range(len(items), 0, -1): # from n downto 1 (inclusive)
        if T[weight][i] != T[weight][i-1]:
            taken.append(items[i-1])
            weight -= items[i-1].weight

   return taken

示例

例如,考虑一个容量为 9 的背包和以下物品:

[item(weight=1, value=2), 
 item(weight=3, value=5), 
 item(weight=4, value=8), 
 item(weight=6, value=4)]

取项目 0、1 和 2 的最佳值为 15。

DP 查找表是

items ---->

0  1  2  3  4
--------------+
0  0  0  0  0 | 0  capacity
0  2  2  2  2 | 1     |
0  2  2  2  2 | 2     |
0  2  5  5  5 | 3     v
0  2  7  8  8 | 4
0  2  7 10 10 | 5
0  2  7 10 10 | 6
0  2  7 13 13 | 7
0  2  7 15 15 | 8
0  2  7 15 15 | 9

对此运行重建算法:

0  0  0  0  0
0  2  2  2  2
0  2  2  2  2
0  2  5  5  5
0  2  7  8  8
0  2  7 10 10
0  2  7 10 10
0  2  7 13 13
0  2  7 15 15
0  2  7 15 15 <-- weight = capacity = 9
        ^   ^
        |   |
      i-1   i = length(items) = 4

在上面的初始状态下,T[weight][i] == T[weight][i-1] (15 == 15) 所以item[i-1] (item(weight=6, value=4)) 没有被占用。递减i 并尝试剩余容量相同的项目。

0  0  0  0  0
0  2  2  2  2
0  2  2  2  2
0  2  5  5  5
0  2  7  8  8
0  2  7 10 10
0  2  7 10 10
0  2  7 13 13
0  2  7 15 15
0  2  7 15 15 <-- weight = 9
        ^
        |
        i = 3

这里,T[weight][i] != T[weight][i-1] (7 != 15) 所以items[i-1],即items[2],或item(weight=4, value=8),一定是被占用了。将剩余重量减少items[i-1].weight9 - 4 = 5,并尝试将item[i-1] 移出图片后剩余重量较小的剩余物品。

0  0  0  0  0
0  2  2  2  2
0  2  2  2  2
0  2  5  5  5
0  2  7  8  8
0  2  7 10 10 <-- weight = 5
0  2  7 10 10
0  2  7 13 13
0  2  7 15 15
0  2  7 15 15
      ^
      |
      i = 2

在这种状态下,我们又得到了T[weight][i] != T[weight][i-1] (2 != 7),所以我们一定是使用了items[i-1],即items[1],或者item(weight=3, value=5)。将剩余权重减少items[i-1].weight5 - 3,然后移至下一项。

0  0  0  0  0
0  2  2  2  2
0  2  2  2  2 <-- weight = 2
0  2  5  5  5
0  2  7  8  8
0  2  7 10 10
0  2  7 10 10
0  2  7 13 13
0  2  7 15 15
0  2  7 15 15
   ^
   |
   i = 1

在这最后一步中,我们又得到了T[weight][i] != T[weight][i-1] (0 != 2),所以我们必须采用items[i-1],即items[0]item(weight=1, value=2)。将剩余权重减少items[i-1].weight,或2 - 1,并退出循环,因为i == 0


C++ 实现

#include <iostream>
#include <vector>

class Knapsack 
public:
    struct Item 
        const int weight;
        const int value;
    ;

private:
    static std::vector<Item> reconstruct_taken_items(
        const std::vector<std::vector<int> > &T,
        const std::vector<Item> &items,
        const int capacity
    ) 
        std::vector<Item> taken;
        int weight = capacity;
    
        for (size_t i = items.size(); i > 0; i--) 
            if (T[weight][i] != T[weight][i-1]) 
                taken.emplace_back(items[i-1]);
                weight -= items[i-1].weight;
            
        
    
        return taken;
    

public:
    static std::vector<Item> solve(
        const std::vector<Item> &items, 
        const int capacity
    ) 
        std::vector<std::vector<int> > T(
            capacity + 1,
            std::vector<int>(items.size() + 1, 0)
        );
        
        for (int i = 1; i <= capacity; i++) 
            for (size_t j = 1; j <= items.size(); j++) 
                const Item &item = items[j-1];

                if (item.weight > i) 
                    T[i][j] = T[i][j-1];
                
                else 
                    T[i][j] = std::max(
                        T[i-item.weight][j-1] + item.value, 
                        T[i][j-1]
                    );
                
            
        
        
        return reconstruct_taken_items(T, items, capacity);
    
;

int main() 
    const int capacity = 9;
    const std::vector<Knapsack::Item> items = 
        1, 2, 3, 5, 4, 8, 6, 4
    ;

    for (const Knapsack::Item &item : Knapsack::solve(items, capacity)) 
        std::cout << "weight: " << item.weight 
                  << ", value: " << item.value << "\n";
    

    return 0;


另见

Printing Items that are in sack in knapsack Knapsack 0-1 path reconstruction (which items to take)

【讨论】:

【参考方案4】:

这是一个 julia 实现:

function knapsack!F<:Real(
    selected::BitVector,    # whether the item is selected
    v::AbstractVectorF,   # vector of item values (bigger is better)
    w::AbstractVectorInt, # vector of item weights (bigger is worse)
    W::Int,                 # knapsack capacity (W ≤ ∑w)
    )

    # Solves the 0-1 Knapsack Problem
    # https://en.wikipedia.org/wiki/Knapsack_problem
    # Returns the assigment vector such that
    #  the max weight ≤ W is obtained

    fill!(selected, false)

    if W ≤ 0
        return selected
    end

    n = length(w)
    @assert(n == length(v))
    @assert(all(w .> 0))

    ###########################################
    # allocate DP memory

    m = Array(F, n+1, W+1)
    for j in 0:W
        m[1, j+1] = 0.0
    end

    ###########################################
    # solve knapsack with DP

    for i in 1:n
        for j in 0:W
            if w[i] ≤ j
                m[i+1, j+1] = max(m[i, j+1], m[i, j-w[i]+1] + v[i])
            else
                m[i+1, j+1] = m[i, j+1]
            end
        end
    end

    ###########################################
    # recover the value

    line = W
    for i in n : -1 : 1
        if line - w[i] + 1 > 0 && m[i+1,line+1] - m[i, line - w[i] + 1] == v[i]
            selected[i] = true
            line -= w[i]
        end
    end

    selected
end

【讨论】:

以上是关于如何使用背包算法 [而不仅仅是包的价值] 找到包中的哪些元素?的主要内容,如果未能解决你的问题,请参考以下文章

算法学习——贪心算法之可拆背包

算法随笔一(背包问题)

01背包--小P寻宝记——粗心的基友

洛谷 1858 多人背包

如何在背包的包中找到最佳元素

两个包的背包算法