动态规划-背包问题

Posted sesiria

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划-背包问题相关的知识,希望对你有一定的参考价值。

背包问题是一种组合优化的NP完全问题。有N个物品和容量为W的背包,每个物品都有自己的体积w和价值v,求拿哪些物品可以使得背包所装下的物品的总价值最大。如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题;如果不限定物品的数量,则问题称为“无界背包或完全背包”问题。

一、0-1背包问题

可以用动态规划来解决背包问题。以0-1背包为例。我们可以定义一个二维数组dp存储最大值,其中dp[i][j]表示前i件物品体积不超过j的情况下能达到的最大价值;如果我们将物品i放入背包,假设第i件物品体积为w,价值为v,那么我么能得到dp[i][j] = dp[i - 1][j - w] + v, 只需要在便利过程中对着两种情况取最大值即可,总时间和空间复杂度都为O(NW)。

int knapsack(vector<int> weights, vector<int> values, int N, int W) 
    vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
    for (int i = 1; i <= N; i++) 
        int w = weights[i - 1], v = values[i - 1];
        for (int j = 1; j <= W; j++) 
            if (j >= w) 
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w] + v);
            
            else 
                dp[i][j] = dp[i - 1][j];
            
        
    
    return dp[N][W];

0-1背包问题-状态转移矩阵样例

 

可以进一步对0-1背包进行空间优化,将复杂度降低为O(W)。如图所示,假设我们目前考虑物品i=2, 且其体积为w=2, 价值为v=3;对于背包容量j,我们可以得到dp[2][j] = max(dp[1][j], dp[1][j - 2] + 3)。 可以发现我们永远只依赖于上一排i=1的信息,之前算过的其他物品都不需要再使用。因此我们可以去掉dp矩阵的第一个维度,在考虑物品i时变成dp[j] = max(dp[j], dp[j-w] + v)。 这里注意需要逆向便利,这样才能够调用上一行物品i-1时 dp[j-w]的值;若按照从左往右的顺序进行正向遍历,则dp[j-w]的值在遍历到j之前就已经背更新成物品i的值了。

int knapsack(vector<int> weights, vector<int> values, int N, int W) 
    vector<int> dp(W + 1, 0);
    for (int i = 1; i <= N; ++i) 
        int w = weights[i - 1], v = values[i - 1];
        for (int j = W; j >= w; j--) 
            dp[j] = max(dp[j], dp[j - w] + v);
        
    
    return dp[W];

 

二、完全背包问题

完全背包问题中,一个物品可以拿多次。如下图所示:

假设我们便利到物品i=2, 且其体积为w=2, 价值为v=3; 对于背包容量j = 5, 组多只能装下2个该物品。那么我们的状态转移方程就变成了dp[2][5] = max(dp[1][5], dp[1][3] + 3, dp[1][1] + 6)。 如果采用这种方法,假设背包容量无穷大而物体的体积无穷小,我们这里的比较次数也会趋近于无穷大,远超O(NW)的时间复杂度。

怎么解决这个问题呢?我们发现在dp[2][3]的似乎和其实已经考虑过了dp[1][3]和dp[2][1]的情况,而在dp[2][1]的时候也已经考虑过dp[1][1]的情况。因此如下图所示:

对于拿多个物品的情况,我们只需要考虑dp[2][3]即可,即dp[2][5] = max(dp[1][5], dp[2][3]+3). 这样就得到了完全背包问题的状态转移方程:

dp[i][j] = max(dp[i - 1][j], dp[i][j-w] +v), 其与0-1背包问题的差别仅仅是把状态转移方程中的第二个i-1变成了i。

int knapsack(vector<int> weights, vector<int> values, int N, int W) 
    vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
    for (int i = 1; i <= N; i++) 
        int w = weights[i - 1], v = values[i - 1];
        for (int j = 1; j <= W; j++) 
            if (j >= w) 
                dp[i][j] = max(dp[i - 1][j], dp[i][j - w] + v);
            
            else 
                dp[i][j] = dp[i - 1][j];
            
        
    
    return dp[N][W];

同样的,我们也可以利用空间压缩将时间复杂度降低为O(W)。这里注意哦我们在便利每一行的时候必须正向便利,因为我们要利用当前物品在第j-w列的信息。

int knapsack(vector<int> weights, vector<int> values, int N, int W) 
    vector<int> dp(W + 1, 0);
    for (int i = 1; i <= N; i++) 
        int w = weights[i - 1], v = values[i - 1];
        for (int j = w; j <= W; j++) 
            dp[j] = max(dp[j], dp[j - w] + v);
        
    
    return dp[W];

注意:压缩空间到底需要正向还是逆向遍历呢?物品和体积哪个放在外曾,哪个放在内层呢?这取决与状态转移方程的依赖关系。在思考空间压缩之前,不妨将转移矩阵画出来,方便思考如何进行空间压缩。

如果实在不想仔细思考,有个简单的口诀:0-1背包对物品的迭代放外层,里层的体积或值逆向遍历;完全背包对物品的迭代放里层,外层的体积或价值正向遍历。

以上是关于动态规划-背包问题的主要内容,如果未能解决你的问题,请参考以下文章

动态规划 之背包问题(九讲)

动态规划-01背包问题

背包型动态规划

动态规划 01背包问题

动态规划-背包问题

动态规划-背包问题