目录
01背包问题
题目:有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件。《算法笔记》
样例:
5 8 //n=5,V=8
3 5 1 2 2 //w[i]
4 5 2 1 3 //c[i]
如果暴力的话,复杂度是O(2^n),因此使用动态规划,复杂度为O(nV);
令dp[i][j]表示:前i个物品放入容量为j的背包所能获得的最大价值。
对于dp[i][j]:
- 放入第i件物品:则最大价值为dp[i-1][v-w[i]]+c[i]。
- 不放入第i件物品:则最大极值为dp[i-1][v]。
得到状态转化方程:
dp[i][v] = max{dp[i-1][v],dp[i-1][v-w[i]]+c[i]} (1<=i<=n,w[i]<=v<=V)
for(int i=1;i<=n;i++) //观察到v的范围,注意初始时候dp全赋成0
for(int v=V;v>=w[i];v--)
dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
通过上图以及代码可以发现,dp[i][v]只与dp[i-1][?]有关,因此当v从大到小遍历时,可以把dp[i][v]化简为dp[v]。
得到状态转移方程:
dp[v] = max{dp[v],dp[v-w[i]]+c[i]} (1<=i<=n,w[i]<=v<=V)
for(int i=1;i<=n;i++) 观察到v的范围,注意初始时候dp全赋成0
for(int v=V;v>=w[i];v--)
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
解决引出01背包问题的完整代码如下:
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
int main(){
int n,V;
scanf("%d%d",&n,&V);
vector<int> w(n);
vector<int> c(n);
vector<int> dp(V+1,0);
for(int i=0;i<n;i++){
scanf("%d",&w[i]);
}
for(int i=0;i<n;i++){
scanf("%d",&c[i]);
}
for(int i=0;i<n;i++)
for(int v=V;v>=w[i];v--)
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
printf("%d\n",dp[V]);
return 0;
}
例题:leetcode
思路:
dp[i][j]:从前i个数字选数能否使得其和为j,可以则为1不可以则为0,dp[i][0] = 1;
状态转移方程:
dp[i][v]=dp[i-1][v] | dp[i-1][v-nums[i]]; (1<=i<=n,nums[i]<=v<=整数数组和的一半)
同样可以利用滚动数组!dp[j]=dp[j] | dp[v-nums[i]]; 具体代码略.
完全背包问题
有n种物品,每种物品的单间重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件。
对比01背包问题,完全背包对于每种物品可以不拿也可以拿超过1件。设dp[i][v]表示前i个物品放入容量为j的背包所能获得的最大价值。和之前一样样。
对于dp[i][v]:
- 不拿第i件物品,则dp[i][v]=dp[i-1][v]。
- 拿第i件物品,则dp[i][v] = dp[i][v-w[i]]+c[i]。
因此,可以得到状态转移方程
dp[i][v] = max{dp[i-1][v],dp[i][v-w[i]]+c[i]} (1<=i<=n,w[i]<=v<=V)
边界dp[i][0] = 0(0<=i<=n) 并且dp[0][v] = 0(0<=v<=V)
为了压缩数组,观察原状态转移方程,可以化简如下。
dp[v] = max{dp[v],dp[v-w[i]]+c[i]} (1<=i<=n,w[i]<=v<=V)
边界dp[i][0] = 0(0<=i<=n) 并且dp[0][v] = 0(0<=v<=V) v必须正向枚举
for(int i=1;i<=n;i++)
for(int v=w[i];v<=V;v++)
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
例题leetcode
思路:
dp[i][j]:从前i个硬币中选择能否组成j,可以则为最少所需要的个数,不可以设为一个大数;
状态转移方程:
dp[i][v]=min(dp[i-1][v],dp[i][v-nums[i]]+1); (1<=i<=n,nums[i]<=v<=所要组成的数)
同样可以利用滚动数组!dp[v]=min(dp[v],dp[v-nums[i]]+1);边界:dp[0]=0;
代码如下
class Solution {
public:
int coinChange(vector<int>& coins, int amount) { //完全背包问题
int size = coins.size();
vector<int> dp(amount+1,amount+1);
//不能组成则为amount+1,能组成则为最小硬币个数0,1,2...
dp[0]=0;
for(int i=0;i<size;i++)
for(int j=coins[i];j<=amount;j++)
dp[j] = min(dp[j],dp[j-coins[i]]+1);
if(dp[amount]<=amount)
return dp[amount];
else return -1;
}
};
分组背包
题目:有n件物品,每件物品的重量为w[i],价值为c[i]。这些物品被分为若干组,每组中的物品互相冲突,只能拿一样。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件。《算法笔记》
对于上述问题,定义dp[i][j],表示前i组物品花费费用j能取得的最大价值。可以分为下列情况:
- 第i组不拿。对应的dp[i][j] = dp[i-1][j]
- 第i组拿第1件或者第2件…拿第k件,
对应的dp[i-1][k-c[i][k]]+w[i][k](其中c[i][k]和w[i][k]分别表示第i组第k件的重量和价值)
伪代码
for 所有的组i
for 最高价值j
for 所有属于组i的物品k
f[i][j]=max(f[i-1][j],f[i-1][j-c[k]]+w[k]) //物品k属于组i
可以逆序用滚动数组压缩成一维。
依赖背包
实际上也可以转换成用上述的背包问题解决。
例题:P1064 金明的预算方案
分析
转换成分组背包后,对于组内进行01背包或者直接暴力遍历。
组内暴力遍历AC代码
#include<cstdio>
#include<algorithm>
using namespace std;
/*对于每组的枚举可以使用01恰好背包解决https://www.luogu.com.cn/problemnew/solution/P1064*/
/*用滚动数组能方便很多*/
const int maxn = 32001; //最大钱数
const int maxm = 61; //最大购买物品数量
int v[maxm],p[maxm],q[maxm];
//价格, 重要度,q=0主件,否则表示主件编号
int group[maxm][3]; //第i组的主件和另外两个附件,从1开始
int c[maxm][5];//c[i][j] 第i组第j种方案的价格
int w[maxm][5];//w[i][j] 第i组第j种方案的价格与重要度乘积
int hashmap[maxm]; //原来编号映射到group后的编号
int dp[maxm][maxn];
//首先,把有依附关系的都分到一组,共有K组
//dp[k][j] = 从前k组中选价值不超过j的所能得到的最大收益
//由于题目每组最多只有3件物件,因此有5中情况.1:都不选.2:选择主.3:选择主附1.4选择主附2.5全选
//转移方程 dp[k][j] = max(dp[k-1][j],dp[k-1][j-c[i]]+w[i](c[i],w[i]是k中一种方案对应的价格和价值重要度积)}
int main(){
int n,m; //n总钱数,m希望购买的物品数
scanf("%d %d",&n,&m);
int n_group = 0;
for(int i=1;i<=m;i++){
scanf("%d %d %d",&v[i],&p[i],&q[i]);
if(q[i]==0){
group[++n_group][0]=i; //记录每组的主件,先不记录附件(其主件可能还未输入)
hashmap[i] = n_group;
}
}
for(int i=1;i<=m;i++) //记录附件
if(q[i]!=0)
for(int j=1;j<=2;j++) //插入附件
if(group[hashmap[q[i]]][j]==0){
group[hashmap[q[i]]][j]=i;
break;
}
for(int i=1;i<=n_group;i++){ //计算每组的排列
int a1=group[i][0],a2=group[i][1],a3=group[i][2]; //该组中每个物品的编号
c[i][1] = v[a1];
w[i][1] = v[a1]*p[a1]; //只选主
if(a2!=0){
c[i][2] = v[a1]+v[a2];
w[i][2] = v[a1]*p[a1]+v[a2]*p[a2]; //主,附1
}
if(a3!=0){
c[i][3] = v[a1]+v[a3];
w[i][3] = v[a1]*p[a1]+v[a3]*p[a3]; //主,附2
c[i][4]=v[a1]+v[a2]+v[a3];
w[i][4]=v[a1]*p[a1]+v[a2]*p[a2]+v[a3]*p[a3]; //全选
}
}
for(int i=1;i<=n_group;i++){
for(int j=1;j<=n;j++){
dp[i][j] = dp[i-1][j]; //本组什么都不选
for(int k=1;k<=4&&c[i][k]!=0;k++){ //遍历每组的方案
if(c[i][k]<=j)
dp[i][j] = max(dp[i][j],dp[i-1][j-c[i][k]]+w[i][k]);
}
}
}
printf("%d",dp[n_group][n]);
return 0;
}