Algorithms Series: 0/1 BackPack ACM题解 - 经典0/1背包问题

in #programming6 years ago (edited)

image.png

Given n items with size A[i] (i from 0 to n - 1), an integer m denotes the size of a backpack. How full you can fill this backpack?

Put this mathematically, you are trying to:

max(sum(j[i] * A[i]))
subject to: j[i] = {0, 1} and sum(j[i] * A[i]) <= m
where 0 =< i < n

j[i] indicates that whether you put item i in the backpack.

Backtracking

If we start from the first item, we have two choices, put it or do not put it in the bag. So it is a decision binary tree of depth n where each level corresponding to a item. The complexity is O(2^n) .

If the remaining capacity is enough (bigger than the current size of item), otherwise we can choose skipping current item.

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @return: The maximum size
     */
     
    int backPack(int m, vector<int> &A) {
        pack(0, m, A);
        return curMax;
    }

    void pack(int n, int weight, vector<int> &A) {
        int sz = A.size();
        int m = 0;
        for (int i = n; i < sz; i ++) {
            pack(n + 1, weight, A);  // do not put it
            if (weight >= A[i]) {   // we can put it
                m += A[i];
                if (m > curMax) {
                    curMax = m;
                }
                weight -= A[i];
                pack(n + 1, weight, A);  // put it
            } 
        }
    }
    
private:
    int curMax = 0;
};

However, this recursion backtracking is too slow because of the large search space especially if n is large.

Dynamic Programming

If, we use dp[i][j] to represent that if we can use first i items (maximum, could use less) to pack at most j weight. Thus, the following stands:

dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]];

This can be interpreted as:

  1. by achieving dp[i][j] we automatically achieve dp[i + 1][j]
  2. by achieving dp[i][j] we automatically achieve dp[i + 1][j + A[i + 1]]

The DP solution allows you to cache intermediate results so you don't have to calculate them over and over again.

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @return: The maximum size
     */
     
    int backPack(int m, vector<int> &A) {
            int size = A.size();
            vector< vector<bool> > dp(size + 1, vector<bool>(m + 1, false));
            for (int i = 0; i < size + 1; ++ i) {
                dp[i][0] = true;
            }
            for(int i = 1; i < A.size() + 1; i++) {
                for(int j = 1; j < m + 1; j++) {
                    if(j < A[i - 1]) { // insufficient capacity
                        dp[i][j] = dp[i - 1][j];
                    } else {
                        dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]];
                    }
                }
            }
            for (int i = m; i >= 0; i--) { // reverse checking the maximum weight
                if (dp[size][i]) {
                    return i;
                }
            }
            return 0;
    }
};

The O(nm) solution (with O(mn) space) fills the DP array and in the end, we need to check if we can fulfil the backpack from m downards to zero (get the maximum)

Memory Optimisation

We can see that the dp[i] is fully dependent on the dp[i-1], thus, we can optimise the memory consumption from O(mn) to O(m).

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @return: The maximum size
     */
     
    int backPack(int m, vector<int> &A) {
            int size = A.size();
            vector<bool> dp(m + 1, false);
            dp[0] = true;
            for (int i = 1; i < A.size() + 1; i++) {
                for (int j = m; j >= 1; --j) {
                    if(j >= A[i - 1]) {
                        dp[j] = dp[j] || dp[j - A[i - 1]];
                    }
                }
            }
            for (int i = m; i >= 0; i--) {
                if (dp[i]) {
                    return i;
                }
            }
            return 0;
    }
};

The runtime complexity is still O(mn) where you need to reverse the inner loop from m downwards to 1.


Support me and my work as a witness by

  1. voting me here, or
  2. voting me as a proxy.

Some of my contributions: SteemIt Tools, Bots, APIs and Tutorial


0/1背包问题是非常经典的算法问题。要求是:给定 n 个物件,每个物件的重量(或体积)是 A[i] 那么请问一个最多装重(或体积) m 的背包最多可以装下多少这些物件。

如果用数学表示这个问题:

max(sum(j[i] * A[i]))

subject to: j[i] = {0, 1} and sum(j[i] * A[i]) <= m
where 0 =< i < n

j[i] 就是控制是否装下第 i 个 物件 的变量(0或者1)

回溯算法

把整个搜索空间从第一个物件开始当成树根,那么这是一棵二叉树,每层的节点就能对应到每个物品,两个分叉分别代表是否把当前节点(物品)装入背包中。

不难想像,时间复杂度为 O(2^n)。如果当前背包并不能装下当前物件,那么我们需要继续尝试下一个物品,直到所有物品尝试完并回溯。

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @return: The maximum size
     */
     
    int backPack(int m, vector<int> &A) {
        pack(0, m, A);
        return curMax;
    }

    void pack(int n, int weight, vector<int> &A) {
        int sz = A.size();
        int m = 0;
        for (int i = n; i < sz; i ++) {
            pack(n + 1, weight, A);  // 不放
            if (weight >= A[i]) {   // 可以放
                m += A[i];
                if (m > curMax) {
                    curMax = m;
                }
                weight -= A[i];
                pack(n + 1, weight, A);  // 放
            } 
        }
    }
    
private:
    int curMax = 0;
};

这个算法的问题就是慢。特别是物品数量多的时候,整个搜索空间就变得巨大。

动态规化

如果我们用 dp[i][j] 来表示是否可以用最多 前 i 件物品来装最多重量(或者体积) 为 j 。那么:

dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]];

我们可以反过来理解:

  1. 如果dp[i][j] 那么下一件物品我们不装的话: dp[i + 1][j]
  2. 如果dp[i][j] 那么装下下一件物品: dp[i + 1][j + A[i + 1]]

动态规化的优点就是可以把中间结果给缓存起来,这样就不会像递规回溯一样有些结果(中间节点)会重复计算。

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @return: The maximum size
     */
     
    int backPack(int m, vector<int> &A) {
            int size = A.size();
            vector< vector<bool> > dp(size + 1, vector<bool>(m + 1, false));
            for (int i = 0; i < size + 1; ++ i) {
                dp[i][0] = true;
            }
            for(int i = 1; i < A.size() + 1; i++) {
                for(int j = 1; j < m + 1; j++) {
                    if(j < A[i - 1]) { // 装不下了
                        dp[i][j] = dp[i - 1][j];
                    } else {
                        dp[i][j] = dp[i - 1][j] || dp[i - 1][j - A[i - 1]];
                    }
                }
            }
            for (int i = m; i >= 0; i--) { // 反过来从大到小检查能装下的最大重量
                if (dp[size][i]) {
                    return i;
                }
            }
            return 0;
    }
};

时间复杂度和空间复杂度都是 O(nm)。最后面我们只要反过来检查 dp[A.size()][j] 为 true 的第一个下标 j 就是当前背包能装下的最大重量。

内存优化

上面的DP公式我们不难发现, 在计算 dp[i] 的时候只依赖于 dp[i-1], 这样我们就可以只用一维来存取 dp[i-1] 的情况。这样,空间复杂度从 O(mn) 就降到了 O(m).

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @return: The maximum size
     */
     
    int backPack(int m, vector<int> &A) {
            int size = A.size();
            vector<bool> dp(m + 1, false);
            dp[0] = true;
            for (int i = 1; i < A.size() + 1; i++) {
                for (int j = m; j >= 1; --j) { // 内循环需要从 m 到 1 的检查
                    if(j >= A[i - 1]) {
                        dp[j] = dp[j] || dp[j - A[i - 1]];
                    }
                }
            }
            for (int i = m; i >= 0; i--) {
                if (dp[i]) {
                    return i;
                }
            }
            return 0;
    }
};

支持我的工作 支持我成为 见证人

  1. 请在 这里 投我一票, 或者
  2. 设置我 为代理.

谢谢您!

您可能还会喜欢:SteemIt 工具、API接口、机器人和教程
.

Sort:  

Hi @justyy. I just voted for you as witness. I had no problem doing so because I have used your tools at helloacm.com and find them very handy! Thanks for all you do!

Thank you very much.

Nice, is great program

学习了,先复制粘贴下来,慢慢看。

steem 就这点不好, 以前的文章没有收藏功能。

赞,只要着重讲解动态规划方程就行,程序实现写多了感觉帮助不大,看着方程自己实现就好了。

Hello justyy!

Congratulations! This post has been randomly Resteemed! For a chance to get more of your content resteemed join the Steem Engine Team

Coin Marketplace

STEEM 0.19
TRX 0.15
JST 0.029
BTC 63550.59
ETH 2644.53
USDT 1.00
SBD 2.81