01背包,完全背包,多重背包

背包问题是dp的经典内容。首先,先分清楚这三个背包问题。

1.01背包:有n种物品与承重为m的背包。每种物品只有一件,每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大。

2.完全背包:有n种物品与承重为m的背包。每种物品有无限多件,每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大。

3.多重背包:有n种物品与承重为m的背包。每种物品有有限件num[i],每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大。


(一)关于01背包

为什么叫它01背包呢,因为装进去就是1,不装进去就是0。所以针对每个物品就两种状态,装,不装。所以这个背包,只要有足够大的空间,这个物品是有可能被装进去的。
所以有状态转移方程

dp[i][j] = max( dp[i-1][j] , dp[i-1][ j - weight[i] ] + value[i] )

然后就有以下代码:

#include 
using namespace std;
int dp[1005][1005];
int weight[1005];
int value[1005];
int main()
{
    int n, m;
    cin >> m >> n;
    memset(dp, 0, sizeof(dp)); //数组清空,其实同时就把边界给做了清理
    for (int i = 1; i <= n; i++)
        cin >> weight[i] >> value[i];
    //从1开始有讲究的因为涉及到dp[i-1][j],从0开始会越界
    for (int i = 1; i <= n; i++) //判断每个物品能否放进
    {
        for (int j = 0; j <= m; j++) //对每个状态进行判断
        //这边两重for都可以倒着写,只是需要处理最边界的情况
        {
            if (j >= weight[i]) //能放进
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
            else
                dp[i][j] = dp[i - 1][j]; //不能放进
        }
    }
    cout << dp[n][m] << endl;
    return 0;
}

然后啊,我们来仔细分析分析就会发现,这个数组开销还是很大的,因为是二维的,万一哪个数据一大,分分钟内存超限,因此有了下边的解法 :
滚动数组
通过状态转移方程可以发现,我们的dp数组只在i和i-1状态之间转移,所以我们可以只用一个某一维度大小为2的二维数组,用这个数组去滚动存储相邻的两个状态(其他的状态就不需要全部存储了),然后用后一个状态的值去覆盖前面一个状态。所以我们形象的叫它:滚动数组。

但是滚动数组操作还是比较繁琐的,在这里我们可以再进行简化,只用一维数组进行存储,这是一个难点。
先看代码:

for (int i = 1; i <= n; i++) //对每个物品进行判断,可反
{
    for (int j = m; j >= weight[i]; j--) //这里这个循环定死,不能反
    {
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); //其实不断在判断最优解
    }
}

和之前的二维数组相比较,我们可以发现第二个for循环是倒序写的,对比一下状态转移方程,我们在这里仔细分析一下原因:

dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

可以看出唯一的区别就是去掉了 i 这个维度,这就是倒序的原因!我们先想象一下滚动数组的操作情况(和普通二维数组的操作情况没有区别),可以发现 i 层调用 i-1 层的数据时,i 层对应的背包填充量 一定是比 i-1 层的背包填充量 大于等于的(背包填充量即对应着 j 的大小),从公式角度上来看就是:dp[i][j] 调用了 dp[i - 1][j- weight[i]] + value[i] 或者 dp[i - 1][j](可以发现前者的 j 大于等于后两者的 j ),从这里我们就可以看出倒序的原因了:

如果我们使用倒序,我们将优先更新背包填充量较大的数据,在持续更新数据的过程中,不断更新填充量减小的数据,然后调用填充量更小的数据,i 层的数据将不会与 i - 1 层的数据发生交叉冲突(所以实际上我们就可以把它们放在一层),因为我们调用的数据总是还未更新的数据,也就是上一层的数据,也就是说在求状态dp[j]的时候,dp[j - c[i]]还不能被更新过,所以dp[j - c[i]]要放在dp[j]后更新,用递减循环的方式实现这个功能。从滚动数组的角度上来说,就是那个大小为2的维度的数据,总是互不冲突的,也就是说,事实上我们可以把大小为2的维度合并成大小为1的,而作为一个代价,我们在更新数据的时候需要从后往前。


(二)关于完全背包

就像先前讲的,完全背包是每个物品都无限。那么有人肯定会认为这就是个简单的贪心问题了,我只要对着一个性价比最高的物品狂选就是了啊??
是吗?
假如我们认定了选性价比最高的,不一定是完全填满背包的,万一最后一个无法填满背包,而又有别的性价比不高的东西可以恰好填满背包,这就出现了反例了。
什么,特判最后一个状态?
那我再往前推到倒数第二件,第三件怎么办?总不能对每个物品都特判吧。
所以正解就是动态规划。状态转移方程如下:

dp[i][j] = max(dp[i - 1][j - k * weight[i]] + k*value[i]) 0<=k*weight[i]<=m

这样看还要多一重for去算k(放入这个物品的个数)
那么这里二维数组显然就不如一维的了。
下面是一维数组的写法:

#include 
using namespace std;
int dp[1005];
int weight[1005];
int value[1005];
int main()
{
    int n, m;
    cin >> m >> n;
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++)
        cin >> weight[i] >> value[i];
    for (int i = 1; i <= n; i++)
        for (int j = weight[i]; j <= m; j++)
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    cout << dp[m] << endl;
    return 0;
}

这里我们在对比一下状态转移方程会发现01背包的与完全背包的完全相同,唯一区别就是数据更新的顺序了,注意到完全背包是正序更新的数据,仔细分析一下原因:
01背包我们进行倒序更新的原因,就是因为我们要不断更新后面的数据,而在这个过程中我们需要调用前面的数据,为了保证前面的数据是i-1层的,所以要倒序进行更新。与之进行对比,完全背包的物品数目是无限的,也就是说一个物品只要能装的下,我们就可以一直装,所以我们先要计算出前面的数据,在前面的数据已经充分不会改变之后,后面的数据再调用前面的数据才是正确的。所以完全背包是正序更新数据的。


(三)关于多重背包

多重背包是指每个物品都有限定的数目。
理解了前面两种背包,那么第三种背包理解起来就毫不费力了。
首先这种可以把物品拆开,把相同的num[i]件物品 看成 价值跟重量相同的num[i]件不同的物品,那么!!是不是就转化成了一个规模稍微大一点的01背包了。
代码如下:

#include 
using namespace std;
int dp[1005];
int weight[1005], value[1005], num[1005];
int main()
{
    int n, m;
    cin >> n >> m;
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++)
        cin >> weight[i] >> value[i] >> num[i];
    for (int i = 1; i <= n; i++) //每种物品
        for (int k = 0; k < num[i]; k++) //其实就是把这类物品展开,调用num[i]次01背包代码
            for (int j = m; j >= weight[i]; j--) //正常的01背包代码
                dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    cout << dp[m] << endl;
    return 0;
}

但是事实上多重背包还是可以继续优化的,下面是最优的一种写法,代码如下:

#include 
using namespace std;
const int N = 1005;

int dp[N];
int c[N], w[N], num[N];
int n, m;

void ZeroOne_Pack(int cost, int weight, int n) //把01背包封装成函数
{
    for (int i = n; i >= cost; i--)
        dp[i] = max(dp[i], dp[i - cost] + weight);
}

void Complete_Pack(int cost, int weight, int n) //把完全背包封装成函数
{
    for (int i = cost; i <= n; i++)
        dp[i] = max(dp[i], dp[i - cost] + weight);
}

int Multi_Pack(int c[], int w[], int num[], int n, int m) //多重背包
{
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++) //遍历每种物品
    {
        if (num[i] * c[i] > m)
            Complete_Pack(c[i], w[i], m);
        //如果全装进去已经超了重量,相当于这个物品就是无限的
        //因为是取不光的。那么就用完全背包去套
        else
        {
            int k = 1;
            //这里用到是二进制思想,降低了复杂度
            //因为他取的1 ,2, 4, 8...与余数个该物品,打包成一个大型的该物品
            //这样完全可以凑出了从0 - k个该物品取法
            //把复杂度从k变成了log k
            //如k=11,则有1, 2, 4, 4,足够凑出0-11个该物品的取法
            while (k < num[i])
            {
                ZeroOne_Pack(k * c[i], k * w[i], m);
                num[i] -= k;
                k <<= 1;
            }
            ZeroOne_Pack(num[i] * c[i], num[i] * w[i], m);
        }
    }
    return dp[m];
}

int main()
{
    int t;
    cin >> t;
    while (t--)
    {
        cin >> m >> n;
        for (int i = 1; i <= n; i++)
            cin >> c[i] >> w[i] >> num[i];
        cout << Multi_Pack(c, w, num, n, m) << endl;
    }
    return 0;
}

这就是三种基本背包的内容了~~

你可能感兴趣的:(dp)