背包问题是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背包呢,因为装进去就是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;
}
这就是三种基本背包的内容了~~