1. 01背包
问题描述:
有n件物品和一个容量为v的背包。放入第i件物品的占的空间为Ci,得到的价值是Wi,求解将哪些物品装入背包可以
使得总价值最大;
首先要明白这个问题不能用贪心来做,因为当前的选择会影响到以后的选择,也就是说当前的选择的最优解
可能会影响到全局的最优解;
考虑用动态规划来做
1.定义状态:
定义状态dp[i][j]为前i件物品恰放入容量为j的背包时的最大价值;
那么dp[n][v]就是这个问题的最终解;
2.描述不同状态如何转移:
在这个过程中,我们很自然想到的决策就是第i件物品放还是不放;放与不放会直接影响到dp[i][j]的值;
1.放,那么dp[i][j]的状态可由dp[i-1][j-Ci] + Wi转移过来
2.不放,那么dp[i][j]的状态可由dp[i-1][j]转移过来
我们所要做的就是在这两种决策中选取一种最优的;
所以状态转移方程为dp[i][j] = max(dp[i-1][j-Ci] + Wi , dp[i-1][j]);
3.按一个方向求出该问题的解
状态dp[i][j]总是要取决于之前的状态dp[i-1][j-Ci] + Wi,dp[i-1][j],所以我们应按按自底而上的方向求解
当然,除了这种状态,我们可以定义另外一种状态来解决01背包问题
1.定义状态:
定义状态dp[i][j]为前i个物品中价值为j时的耗费的最小空间;
2.描述不同状态如何转移:
在这个过程中,我们能够很自然的想到当前物品的选与不选会影响当前状态;
1.放,那么状态dp[i][j]的状态可由状态dp[i-1][j-W[i]] + C[i]转移过来
2.不放,那么状态dp[i][j]的状态可由状态dp[i-1][j]转移过来
所以状态转移方程为dp[i][j] = min(dp[i-1][j-W[i] + C[i] , dp[i-1][j]);
3.自底而上的求解
注意初始化,dp[0][0]为[0],dp[0][j]为不合法的状态,用一个无穷大的数代替;
在把所有状态求出来之后,我们只需要找出dp[n][j]不大于V的最大j值即可
两种不同状态的代码如下:
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int INF = 2005;
int dp[INF][INF]; //定义状态dp[i][j]为前i件物品放入容量为j的背包时的最大价值;
int W[INF],C[INF];
int main()
{
int n, v;
scanf("%d%d",&n,&v);
for(int i = 1; i <= n; i++)
{
scanf("%d",&W[i]);
}
for(int i = 1; i <= n; i++)
{
scanf("%d",&C[i]);
}
memset(dp,0,sizeof(dp)); //初始化
for(int i = 1; i <= n; i++) //自底而上的求解
{
for(int j = 0; j <= v; j++)
{
if(j >= C[i])
{
dp[i][j] = max(dp[i-1][j],dp[i-1][j - C[i]] + W[i]); //状态转移
}
else
{
dp[i][j] = dp[i-1][j];
}
}
}
printf("%d\n",dp[n][v]);
return 0;
}
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int INF = 2005;
int dp[INF][INF]; //定义状态dp[i][j]为前i个物品中价值为j时的耗费的最小空间;
//这种状态只适用于C[i]的值很大,而W[i]的值较小的情况
int W[INF],C[INF];
int main()
{
int T;
scanf("%d",&T);
int n, v;
scanf("%d%d",&n,&v);
for(int i = 1; i <= n; i++)
{
scanf("%d",&W[i]);
}
for(int i = 1; i <= n; i++)
{
scanf("%d",&C[i]);
}
memset(dp,0,sizeof(dp)); //初始化
for(int i = 1; i < INF; i++)
{
dp[0][i] = INF + 5;
}
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= INF; j++)
{
if(j >= W[i])
{
dp[i][j] = min(dp[i-1][j],dp[i-1][j - W[i]] + C[i]); //状态转移方程
}
else
{
dp[i][j] = dp[i-1][j];
}
}
}
int res = -1;
for(int j = 0; j < INF; j++)if(dp[n][j] <= v) //根据定义的状态可得结果是dp[n][j]不大于V的最大j值即可
{
res = j;
}
printf("%d\n",res);
return 0;
}
01背包代码在时间上不能被优化,但在空间上可以优化,具体如何优化,这里不再讲解,想了解的自行百度
————————————————————————————————————————————————————
2.完全背包
问题描述:
有n种物品和一个容量为v的背包,每种物品都有无限件可用,放入第i种物品的费用是Ci,价值是Wi;问如何装才能使
总价值最大;
仔细看这个问题,会发现这个问题和01背包类似,01背包是有n种物品,每种只有1件;所以我们可以根据01背包
的策略来做;
定义状态dp[i][j]为前i种物品恰放入容量为j的背包时的最大价值;
在01背包中,对于每一种物品来说,策略是取0件或是取1件。在完全背包中,对于每一种物品来说,相关的策略是
取0件,取1件,取2件,取3件......取v/Ci(知道背包装不下为止),类比01背包,我们所要做的就是在这些策略选择一
个最优的;
所以这个状态转方程为:
dp[i][j] = max(dp[i-1][j-k*Ci] + k*Wi) k*Ci <= v;
这种方法的代码为:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define mmset(a,b) memset(a,b,sizeof(a))
using namespace std;
const int INF = 1005;
int dp[INF][INF]; //定义状态dp[i][j]为前i件物品恰放入容量为j的背包时的最大价值;
int main()
{
int n,v;
while(~scanf("%d%d",&n,&v))
{
int W[INF + 5],C[INF + 5];
for(int i = 1; i <= n; i++)
{
scanf("%d %d",&C[i],&W[i]);
}
mmset(dp,0); //初始化
for(int i = 1; i <= n; i++) //自底而上的求解
{
for(int j = 0; j <= v; j++)
{
if(j >= C[i])
{
for(int k = 1; k * C[i] <= j; k++)
{
dp[i][j] = max(dp[i][j],dp[i-1][j-k*C[i]] + k*W[i]); //状态转移方程
}
}
else
{
dp[i][j] = dp[i-1][j];
}
}
}
printf("%d\n",dp[n][v]);
}
return 0;
}
但是这种方法的时间复杂度比较高,所以考虑一下更好的方法;
在上面中,策略是对于第i种物品,有取0件,取1件,取2件,取v/C[i]件。仔细想一下这个过程,我们就会发现
对于每个dp[i][j],我们都要在从取1件开始,直到取到v/C[i]件。为什么要从取1件开始呢,因为我们不确定dp[i][j]
的值从取第几件转移过来是最大的。所以我们要从取第1件开始试,一直试到放不下背包为止;
我们再看一下是如何定义状态的,dp[i][j]是指 前i件物品恰放入容量为j的背包时的最大价值,注意两个词,"恰",
"最大价值",
这说明dp[i][j]是指在空间为j的背包中,不能再装下任意一个前i种物品时,背包所能拥有的最大价值(仔细理解
一下这句话,对接下来推导的状态转移方程有很大帮助)。
所以对于dp[i][j]来说,如果只放1件第i种物品就能使dp[i][j]最大,那么一定是在dp[i][j - C[i]]的基础上放的.
换句话说dp[i][j]是dp[i][j - ci]多放一件i物品转移过来的
那么我们只需要在放不放这件物品进行决策,所以状态转移方程为:
dp[i][j] = max(dp[i][j - C[i]] + W[i], dp[i-1][j]); (有点绕,不懂的可以多想想);
代码如下:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define mmset(a,b) memset(a,b,sizeof(a))
using namespace std;
const int INF = 1005;
int dp[INF][INF]; //定义状态
int main()
{
int n,v;
while(~scanf("%d%d",&n,&v))
{
int W[INF + 5],C[INF + 5];
for(int i = 1; i <= n; i++)
{
scanf("%d %d",&C[i],&W[i]);
}
mmset(dp,0); //初始化
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= v; j++)
{
if(j >= C[i])
{
dp[i][j] = max(dp[i][j-C[i]] + W[i],dp[i-1][j]); //状态转移方程
}
else
{
dp[i][j] = dp[i-1][j];
}
}
}
printf("%d\n",dp[n][v]);
}
return 0;
}
这个代码时间上很难在优化了,空间上可以优化,这里不再讲解,自己思考或者百度。
————————————————————————————————————————————————————
3.多重背包:
问题描述:
有n种物品和一个容量为v的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci,价值是Wi。求解:
如何装才能使背包价值最大。
仍然先把它转化为01背包来做。时间复杂度是O(n*v*(n*Mi)),时间复杂度是比较高的。
考虑是否能优化:
转化为01背包的思路是,第i种物品有Mi个,我们把这第i种物品的Mi个当成01背包来做。我们需要一件一件
的放第i种物品,为什么要一件一件的放呢?(注意,这是优化的关键),因为我们在放第Mi件物品时,必须要
从前Mi-1件转移过来,如果没有前Mi-1件物品,我们就无法得到前Mi件物品的最优解。同理,在求前Mi-1
的最优解,必须从前Mi-2转移过来。干说有些枯燥,我们来举个例子说明一下;
有1种物品,共7件。一个空间为8的背包,每件物品耗费的空间是1,价值是1。问如何装才能使背包价值最大
定义状态为dp[i][j]为前i件物品恰放入空间为j的背包时的最大价值。
我们可以用一个图表来表示求这个问题时,dp[i][j]的变化:
可以看出dp[i]主要是比dp[i-1]多了蓝色部分;看出dp[2][2]是从dp[1][1]转移过来的
dp[3][3]是从dp[2][2]转移过来的;dp[4][4]是从dp[3][3]转移过来的.........
如果没有dp[2],那么dp[3]的状态能求出来吗?(可以自己思考一下)
从dp[2]到dp[3]转移是放了一件物品,既然没有dp[2],我们可以在dp[1]上放2件物品来转移到dp[3];
(注意,这点就相当于理解优化的关键);
注意:可能有人会这样想,直接在dp[1]上加上6件物品,求这样出dp[7]这样会更快。但是这样是错的;
如果放6件物品,dp[7][2]只能从dp[1][-4]转移过来,显然是不合理的。
经过以上分析,我们意识到把一种物品拆分成多个物品时可行的。
我们可以用二进制的思想拆分物品,把个数为Mi个的物品拆分为
2^0, 2^1,2^2.......2^n, Mi - (2^(n+1)-1)个物品;
那么7就可以被分解为个数为1,2,4的物品;
如图
这就是拆分成多个物品的转移过程;
下图是拆分成多个物品后的dp[i][j]的状态
可以看出这个表的1,2,4行分别对应上表的1,3,7行;
可以发现我们把一个物品拆分成若干个二的次幂,可以使得后面的区间不重不漏的从前面转移过来;
代码如下:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define mmset(a,b) memset(a,b,sizeof(a))
using namespace std;
const int INF = 1005;
int dp[INF][INF]; //定义状态
int n,v,count1 = 1;
void ZeroPack(int w,int c);
int main()
{
int W[INF],C[INF],M[INF];
scanf("%d%d",&n,&v);
for(int i = 1; i <= n; i++)
{
scanf("%d %d %d",&W[i],&C[i],&M[i]);
}
mmset(dp,0); //初始化
for(int i = 1; i <= n; i++)
{
int k = 1;
int num = M[i];
while(k < num) //二进制拆分物品
{
ZeroPack(W[i] * k,C[i] * k);
num -= k;
k <<= 1;
}
ZeroPack(W[i] * num,C[i] * num) ;
}
printf("%d\n",dp[count1-1][v]);
return 0;
}
void ZeroPack(int w,int c)
{
for(int j = 0; j <= v; j++)
{
if(j >= c)
{
dp[count1][j] = max(dp[count1-1][j],dp[count1-1][j - c] + w);
}
else
{
dp[count1][j] = dp[count1-1][j] ;
}
}
count1++;
}
这个代码可以在空间上优化,也能在时间上优化;
时间上优化主要是当Ci * Mi >= v,这时候该种物品一定可以装满背包,可以这种物品看成完全背包来做;
空间上优化不再赘述;
————————————————————————————————————————————————————
————————————————————————————————————————————————————
至此,三种基础背包已经讲解完毕,但是还有一些细节上的问题需要注意:
关于背包有两种不同的问法:
第一种是:求背包的最大价值,上面讲的就是这个问题;
第二种是:求背包在装满的时候的最大价值;
关于第二种问法,我们只需要在初始化的时候,把dp[0][0]赋值为0,其他的dp[0][1~v]赋值为-OO(负无穷);
可以这样理解:
初始化的dp数组就是在没有装任何物品的合法状态。如果要求背包恰好装满,那么显然,只有容量为0的背包
是合法状态,因为他能满足当没有一件物品装入背包时“恰好装满”,其他的容量为1~v的背包都不满足,所以
是非法状态,用-OO表示;
如果只是要求背包的最大价值,并没有求装满,那么在初始化的时候dp[0][0~v]可以什么都不装,而且都是
合法状态,所以dp[0][0~v]初始化为0;
————————————————————————————————————————————————————
————————————————————————————————————————————————————
下面是三种背包空间优化过后的代码(主要是利用了滚动数组,这里只给出代码,不再讲解,请自行百度)
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define mmset(a,b) memset(a,b,sizeof(a))
using namespace std;
const int INF = 10005;
int dp[INF];
int n,v; //物品的种类和背包的容量
void ZeroPack(int w,int c) ; //01背包
void CompletePack(int w,int c); //完全背包
void MultiplePack(int w,int c,int m) ; //多重背包
int main()
{
int W[INF],C[INF],M[INF]; //物品的价值,耗费的空间,件数
scanf("%d %d",&n,&v);
for(int i = 1; i <= n; i++)
{
scanf("%d %d %d",&W[i],&C[i],&M[i]);
}
mmset(dp,0); //注意初始化要根据问法不同而改变
for(int i = 1; i <= n; i++)
{
if(M[i] == 1) //属于01背包
{
ZeroPack(W[i],C[i]);
}
else if(C[i] * M[i] >= v) //属于完全背包
{
CompletePack(W[i],C[i]);
}
else //多重背包
{
MultiplePack(W[i],C[i],M[i]) ;
}
}
printf("%d\n",dp[v]);
return 0;
}
void ZeroPack(int w,int c)
{
for(int j = v; j >= c; j--)
{
dp[j] = max(dp[j],dp[j - c] + w);
}
}
void CompletePack(int w,int c)
{
for(int j = c; j <= v; j++)
{
dp[j] = max(dp[j],dp[j - c] + w);
}
}
void MultiplePack(int w,int c,int m)
{
if(c * m >= v)
{
CompletePack(w,c);
}
else
{
int k = 1;
while(k < m)
{
ZeroPack(w*k,c*k);
m -= k;
k <<= 1;
}
ZeroPack(w*m,c*m);
}
}