一、01背包
01背包特点:每种物品仅有一件,可以选择放或不放。
用F[i,v]表示前i件物品恰好放入一个容量为v的背包可以获得的最大价值。
则状态转移方程:F[i,V]=max{F[i-1,v],F[i-1,v-cost[i]]+W[i]};
方程理解:
“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品放或是不放,那么可以装换为“前i-1件物品放入容量为v的背包中”,价值为F[i-1,v];如果放第i件物品,装换成“前i-1件物品放入 v-c[i] 的背包中”,此时可以获得最大价值就是F[i-,1,v-c[i] ]+w[i]。
简单案例:
序号 费用 价值
1 2 12
2 1 10
3 3 20
4 2 15
求容量(总花费)为5的最大价值:
使用二维数组时间复杂度和空间复杂度均为O(VN)
//使用二维数组
int cost[4]={2,1,3,2}; //花费容量
int W[4]={12,10,20,15};//价值
int dp[5][11]={0};
for(int i=1;i<5;i++) //1-5表示有4个物品
{
for(int v=cost[i-1];v<=5;v++) //表示容量的变化(i-1是因为cost下标是从0开始的)
{
if(v-cost[i-1]>=0) //判断是否能装下.
dp[i][v]=max(dp[i-1][v],dp[i-1][v-cost[i-1]]+W[i-1]);
else
dp[i][v]=dp[i-1][v];
}
}
//打印dp数组,最终结果为dp[4][5]
for(int i=0;i<5;i++)
{
for(int j=0;j<=5;j++){
cout<<dp[i][j]<<" ";
}
cout<<endl;
}
//结果:
/*
0 0 0 0 0 0
0 0 12 12 12 12
0 10 12 22 22 22
0 10 12 22 30 32
0 10 15 25 30 37
*/
一维数组空间优化,时间复杂度为O(VN),空间复杂度O(V)
事实上,这要求在每次主循环中我们以 v ← V … 0 的递减顺序计算 F [v],这样才能保证计算 F[v] 时 F [v - Ci ] 保存的是状态 F[i - 1, v - Ci ] 的值,如果是v ← 0… V 的递增顺序计算,一维数组会将 F[i - 1, v - Ci ]覆盖。
//一维数组空间优化
int cost[4]={2,1,3,2};
int W[4]={12,10,20,15};
int T[6]={0};
for(int i=0;i<4;i++)
{
for(int v=5;v>=cost[i];v--)
{
T[v]=max(T[v],T[v-cost[i]]+W[i]);
}
}
for(int i=0;i<=5;i++)
{
cout<<T[i]<<" ";
}
cout<<endl;
//结果:0 10 15 25 30 37
关于数组初始化
有的题目要求“恰好装满背包”时的最优解、有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了 F [0] 为 0,其它 F [1…V ] 均设为 - ∞ ,这样就可以保证最终得到的 F [V ] 是一种恰好装满背包的最优解。 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 F [0…V ] 全部设为 0。
二、完全背包
完全背包特点:有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。
问题如下:
放入第 i 种物品 的费用是 Ci ,价值是 Wi。
求解:将哪些物品装入背包,可使这些物品的耗费的费用总 和不超过背包容量,且价值总和最大。
可以按照每种物品不同的策略写出状态转移方程:
F [i, v] = max{ F [i - 1,v - kC i ] + kW i | 0 ≤kC i ≤v}
//一维数组完全背包
for(int i=0;i<4;i++)
{
for(int v=cost[i];v<=5;v++) //升序,01背包一维是降序
{
T[v]=max(T[v],T[v-cost[i]]+W[i]);
}
}
for(int i=0;i<=5;i++)
{
cout<<T[i]<<" ";
}
cout<<endl;
01 背包中要按照 v 递减的次序来循环。让 v 递减是为了保证第 i 次循环中的状态 F [i, v] 是由状态 F [i - 1, v - Ci ] 递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第 i 件物品”这件策略时,依据的是一个绝无已经选入第 i 件物品的子结果 F [i - 1, v - Ci ]。
而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第 i 种物品”这种策略时,却正需要一个可能已选入第 i 种物品的子结果 F [i, v - Ci ],所以就可以并且必须采用 v递增的顺序循环。这就是这个简单的程序为何成立的道理。
值得一提的是,上面的伪代码中两层 for 循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。这个算法也可以由另外的思路得出。例如,将基本思路中求解 F [i, v - Ci ] 的状态转移方程显式地写出来,代入原方程中,会发现该方程可以等价地变形成这种形式:F [i, v] = max (F [i - 1, v], F [i, v - Ci ] + Wi )
三、多重背包
题目
有 N 种物品和一个容量为 V 的背包。第 i 种物品最多有 Mi 件可用,每件耗费的空间是 Ci,价值是 Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
考虑二进制的思想,我们考虑把第 i 种物品换成若干件物品,使得原问题中第
i 种物品可取的每种策略——取 0 … Mi 件——均能等价于取若干件代换以后的物品。
另外,取超过 M i 件的策略必不能出现。
方法是:将第 i 种物品分成若干件 01 背包中的物品,其中每件物品有一个系数。这件物品的费用和价值均是原来的费用和价值乘以这个系数。令这些系数分别为
1, 2, 2^2… 2^(k - 1), Mi - 2^k+ 1,且 k 是满足 M i - 2k+ 1 > 0 的最大整数。例如,如果 M i为 13,则相应的 k = 3,这种最多取 13 件的物品应被分成系数分别为 1, 2, 4, 6 的四件物品。
分成的这几件物品的系数和为 M i,表明不可能取多于 M i 件的第 i 种物品。另外这种方法也能保证对于 0… M i 间的每一个整数,均可以用若干个系数的和表示。
int bag,number[2020],value[2020],dp[2020];
void ZeroOnePack(int weight,int value) //01背包
{
for(int v=bag;v>=weight;v--)
{
dp[v]=max(dp[v],dp[v-weight]+value);
}
}
void CompletePack(int weight,int value) //完全背包
{
for(int v=weight;v<=bag;v++)
{
dp[v]=max(dp[v],dp[v-weight]+value);
}
}
void MultiplePack(int weight,int value,int number) //多重背包
{
if(bag<=number*weight) // 如果该物品总容量比背包总容量要小,那么这个物品可以直接取完
{
CompletePack(weight,value);//完全背包
return;
}
else//否则装换成01背包
{
int k =1;
while(k<=number)
{
ZeroOnePack(k*weight,k*value);
number=number-k;
k=2*k; //二进制思想处理
}
ZeroOnePack(number*weight,number*value);
}
}