前言:
学习动态规划,必学背包问题。背包问题可以说是动态规划里的经典、基础问题了。第一次接触时真的懵了很久,现在重新来看明朗了很多,所以想趁这时候写下自己的想法和理解。
感觉动态规划可以拆成:动、态、规划。
态:我们所要解决的问题在当前状态下会有不同的发展方向,或者说下一个状态有多种选择;
动:前一个状态可能会对后面的状态造成一定的影响,状态之间是有联系的,而不是各顾各的,“静态”的。
规划:我们需要规划好在往下一个状态发展时,可以有几种选择(确保考虑了所有可能的情形),并确定好每一种选择对应的不同状态。(我们要做的,就是根据题目的要求,在上述的状态中选取我们要找的状态。)
总而言之,动态规划是一种大的思想,没有什么函数模板可以套用。我们需要学习并且理解这一思想,避免“每次碰到新题自己都想不出来,但一看题解就懂”的情况(紫书上的话哈哈哈)。其中最核心的,也就是状态的选择的转换了,还是得多做题目练习,久了自然会参悟。(大佬忽略我这句话)
进入正题:经典背包问题
背包问题,其实就是物品是否放进背包的问题,对于每个物品,就两种情况:放 和 不放
下面加粗的字是为了区别三种问题的不同。
一、01背包
有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是w[i]。求将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。每种物品数量都为1!
思路:一个物品有两个参数,我们可以根据物品的参数去定义当前的状态,对状态的不同定义会导致出现不同的状态转换方程,难度有异,所以我们要慎选。
对于这类问题,我们可以定义一个二维数组(对应物品的两个参数) d[i][j],表示把前 i个物品装到容量为 j的背包中可以获得的最大总价值。选择好状态的表示方式后,我们开始考虑从当前状态进入到下一个状态时的不同选择。对于背包问题,只有选与不选两种情况,所以有两种不同选择。
①不选:则显然,当前的背包容量和总价值没有变化,但我们物品的选择范围增加了一,所以 d[i][j] = d[i-1][j];
②选:为了达到总价值最大的目的,我们要把选后的总价值跟原来的总价值比较,取最大的。可能有人会说,再选一件物品加入,那总价值肯定大了啊怎么还要比较。不要忘了此时当前背包容量也变化了。我们要明确我们定义的状态是什么,是把前 i个物品装到容量为 j的背包中可以获得的最大总价值,我们对这前 i 件物品的选取方式有多种(组合),我们要在当前容量一定的情况下选择最佳的那种,这样就不是单纯的再选取一个加进去了。回归正题,此时 d[i][j] = max(d[i][j],d[i-1][j-v[i]]+w[i])。
我们来好好剖析一下这个方程中的 d[i-1][j-v[i]]+w[i]。d[i-1][j-v[i]]指把前i-1件物品装到容量为j-v[i]的背包中可以获得的最大总价值,这时候我们再把第i 件物品加入,就变成了把前i 件物品装到容量为 j-v[i]+v[i]==j的背包中可以获得的最大总价值,即d[i][j]。所以d[i][j] = 原来没加入i 时d[i-1][j-v[i]]的总价值,再加第i 件物品的价值w[i]。
for(int i=1; i<=number_of_object; ++i)
{
for(int j=0; j<=capacity_of_bag; ++j)
{
//如果容量不够,那肯定放不下
if(j<v[i])
d[i][j]=d[i-1][j];
//容量够了,选择放或者不放后总价值最大的一个
else
d[i][j] = max(d[i-1][j],d[i-1][j-v[i]]+w[i]);
}
}
//最终答案就是d[number_of_object][capacity_of_bag]了。
我们还可以这样定义状态:d[i]表示背包容量还剩下i 时可以装的最大总价值。变成了一维数组,也占用了更多的空间。
cin>>n>>m;//n件物品,背包容量m
for(int i=1;i<=n;++i)
{
cin>>w>>v;//第i 件物品的价值和体积
//从最大容量遍历,是为了使得同一件物品不重复放进背包
//因为我们每一步都是给f[j]赋值,且是根据我们还未遍历到的j来赋值
//所以f[m-v-1]并没有在f[m]的基础上再加上i 的价值
//若m>2*v,在j=m时我们把可以 i 放进背包,因为此时j<m还未开始遍历
//在j=m-v-1时我们把可以 i放进背包,因为此时j<m-v-1还未遍历
//相当于在原来没有i 的情况下加入了i .
//这时候背包里并没有两个i ,因为i 存在于f[m-v-1]中,所以也必然存在于f[m]中。
//就像是把i 存在的范围缩小了一样。再强调f[m-v-1]并没有在f[m]的基础上再加上i 的价值。
for(int j=m;j>=v;--j)
{
//如果把i 添加后总价值变大,就更新f[j]
//原理同上述的二维数组解法
if(f[j]<f[j-v]+w)
f[j]=f[j-v]+w;
}
}
cout<<f[m]<<endl;
二、完全背包
有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是w[i]。求将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。每种物品都有无限多个
01背包的进阶,有无限多个物品,所以可以重复选择。其实思路跟01背包一样,直接用代码解释。我学习的时候感觉一维数组好理解,所以直接用一维数组。
cin>>n>>m;//n件物品,背包容量m
for(int i=1;i<=n;++i)
{
cin>>v>>w;//第i 件物品的体积和价值
//从 j = v开始循环(从容量小到容量大),可以使得每一个物品被重复选中
//j=v时我们将i 放入,j=2*v时我们也将i 放入,
//此时的d[2*v]是在d[v]的基础上再加上i 的价值
//也就选中了两个i 放入背包,因此实现了重复选中物品
for(int j=v;j<=m;++j)
if(d[j]<d[j-v]+w)
d[j]=d[j-v]+w;
}
cout<<d[m]<<endl;
三、多重背包
有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是w[i]。求将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。第i 种物品有c[i]件。
不同种类物品的个数可以不同。跟完全背包一样,也是建立在01背包的基础上的。如果一种物品有k 件,我们可以看成有k 种参数相同的物品,每种只有一件,这样就完全转为01背包问题去求解了!
cin>>n>>m;//n件物品,背包容量m
for(int i=1;i<=n;++i)
cin>>v[i]>>w[i]>>c[i];//分别表示第I种物品的体积、价值、数量
//前两个for表示了:如果一种物品有k 件,
//我们可以看成有k 种参数相同的物品,每种只有一件
for(int i=1;i<=n;++i)
for(int k=1;k<=c[i];++k)
//从最大容量开始遍历,就是因为思路转换后变成解决01背包问题
for(int j=m;j>=v[i];--j)
if(d[j]<d[j-v[i]]+w[i])
d[j]=d[j-v[i]]+w[i];
cout<<d[m]<<endl;
总结完成!对DP的学习暂时放下了,期末复习要紧。
用一维数组虽然简便但占空间多且不利用输出具体的选择方案。
如何输出具体的选择方案,以后再学。