标签:等于 完全 意义 假设 lin des ice 基础 image
0/1
背包问题N
件物品和一个容量为 V
的背包。第 i
件物品的体积是 v[i]
,价值是 cost[i]
。求解将哪些物品装入背包可使价值总和最大。f[i][j]
表示前 i
件物品恰放入一个容量为 j
的背包可以获得的最大价值。则其状态转移方程便是:f[i][j]=max{f[i-1][j],f[i-1][j-v[i]]+cost[i]}
i
件物品放入容量为 j
的背包中”这个子问题,若只考虑第 i
件物品的策略(放或不放),那么就可以转化为一个只牵扯前 i-1
件物品的问题。i
件物品,那么问题就转化为“前 i-1
件物品放入容量为 j
的背包中”,价值为 f[i-1][j]
;i
件物品,那么问题就转化为“前 i-1
件物品放入剩下的容量为 j-v[i]
的背包中”,此时能获得的最大价值就是 f[i-1][j-v[i]] + cost[i]
。Description
给定
n
种物品和一个容量为V
的背包,物品i
的体积是 \(v_i\) ,其价值为 \(c_i\)。问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?Input
- 第一行为两个正整数
n
,V
,表示有n
件物品,背包容量为V
\(( 1\le n\le 1000, 1\le V\le 10000)\)- 接下来
n
行,每行两个正整数 \(v_i, c_i\) 表示第i
件物品的体积和价值。Output
- 只有一行,为能放入背包的最大价值。
Sample Input
4 8 2 3 3 4 4 5 5 6
Sample Output
10
分析思路:
定义 f[i][j]
表示前 i
件物品放入体积为 j
的背包中能获得的最大价值
初始化时,i==0 || j==0
时f[i][j]=0
,显然,没有物品,或背包为空时,价值为0
。
我们从 1~n
枚举每一件物品,对当前的第 i
件物品进行分析:
i
件物品的体积大于背包容量 j
,则当前的最优等价于前 i-1
件物品放入 j
的背包中,即f[i][j]=f[i-1][j]
v[i]<=j
,此时对第 i
件物品,我们有两种决策:
i
件物品放入容量为 j
的背包,则前 i-1
件物品能使用的背包容量只有 j-v[i]
,此时:f[i][j]=f[i-1][j-v[i]] + cost[i]
。i
件物品,有可能让背包多放几件前 i-1
件物品,此时:f[i][j]=f[i-1][j]
。f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+cost[i])
如图所示:
代码实现:
#include <cstdio>
#include <cstring>
#include <algorithm>
const int maxn=1000+5,maxv=10000+5;
int v[maxn],c[maxn],f[maxn][maxv];
void Bag(int n,int V){
for(int i=1;i<=n;++i)//依次枚举前i件物品
for(int j=1;j<=V;++j)//从1~V枚举背包容量
if(j<v[i])f[i][j]=f[i-1][j];//如果无法放进第i件物品
else f[i][j]=std::max(f[i-1][j],f[i-1][j-v[i]]+c[i]);
}
void Solve(){
int n,V;scanf("%d%d",&n,&V);
for(int i=1;i<=n;++i) scanf("%d%d",&v[i],&c[i]);
Bag(n,V);
printf("%d\n",f[n][V]);
}
int main(){
Solve();
return 0;
}
时间效率:O(n*V)
,内存:n * V
以上方法的时间和空间复杂度均为 O(N*V)
,其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到 O(V)
。
先考虑上面讲的基本思路如何实现:
i=1..N
,每次算出来二维数组f[i][0..V]
的所有值。f[0..V]
,能不能保证第i
次循环结束后 f[j]
中表示的就是我们定义的状态f[i][j]
呢?f[i][j]
是由f[i-1][j]
和 f[i-1][j-v[i]]
两个子问题递推而来,能否保证在推 f[i][j]
时(也即在第 i
次主循环中推 f[j]
时)能够得到 f[i-1][j]
和 f[i-1][j-v[i]]
的值呢?j=V..0
的顺序推 f[j]
,这样才能保证推 f[j]
时 f[j-v[i]]
保存的是状态f[i-1][j-v[i]]
的值。主要代码如下:
void Bag(int n,int V){
for(int i=1;i<=n;++i)//依次枚举前i件物品
for(int j=V;j>=v[i];--j)//从V~v[i]枚举背包容量
f[j]=std::max(f[j],f[j-v[i]]+c[i]);
}
其中的 f[j]=max{f[j],f[j-v[i]]+cost[i]}
一句恰就相当于我们的转移方程f[i][j]=max{f[i-1][j],f[i-1][j-v[i]]+cost[i]}
,因为现在的 f[j-v[i]]
就相当于原来的f[i-1][j-v[i]]
。
如果将j
的循环顺序从上面的逆序改成顺序的话,那么则成了 f[i][j]
由 f[i][j-v[i]]
推知,与本题意不符,但它却是另一个重要的背包问题最简捷的解决方案,故学习只用一维数组解 01
背包问题是十分必要的。
时间效率:O(n*V)
,内存:V
0/1
背包初始化细节f[0]
为0
其它f[1..V]
均设为-∞
,这样就可以保证最终得到的f[V]
是一种恰好装满背包的最优解。f[0..V]
全部设为0
。f
数组事实上就是在没有任何物品可以放入背包时的合法状态。0
的背包可能被价值为0
的nothing
“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞
了。N
种物品和一个容量为 V
的背包,每种物品都有无限件可用,第 i
件物品的体积是 \(v_i\),价值是 \(c_i\) 。求解将哪些物品装入背包可使价值总和最大。这个问题非常类似于01
背包问题,所不同的是每种物品有无限件。
从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0
件、取1
件、取2
件……等。
按照解01
背包时的思路,令 f[i][j]
表示前 i
种物品恰放入一个容量为 j
的背包的最大权值。
状态转移方程:f[i][j]=max{f[i-1][j-k*v[i]]+k*c[i]}(0<=k*v[i]<=j)
核心代码:
void Bag(int n,int V){//n件物品,背包荣咯昂为V
for(int i=1;i<=n;++i){//枚举物品
for(int k=0;k*v[i]<=V;++k)//取0~V/v[i]件i物品,k=0相当与不去第i件,此时f[i][j]=f[i-1][j]
for(int j=k*v[i];j<=V;++j){//枚举容量
f[i][j]=std::max(f[i][j],f[i-1][j-k*v[i]]+k*c[i]);
}
}
}
时间效率:O(N*V*k)
若两件物品 i,j
满足 v[i]<=v[j]
且 c[i]>=c[j]
,则将物品j
去掉,不用考虑。
j
换成物美价廉的i
,得到至少不会更差的方案。将费用大于V
的物品去掉。
使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)
地完成这个优化。
注意:以上优化并不能从实质上提高时间效率,不过也是在数据比较大的情况下,特别是随机数据有很明显的提升。
二进制拆分优化
分拆方法:
i
种物品拆成费用为 \(v[i]*2^k\) 、价值为 $ c [i]*2^k$ 的若干件物品,其中k
满足 \(v[i]*2^k<=V\) 。i
种物品,总可以表示成若干个\(2^k\) 件物品的和。核心代码实现:
void Bag(int n,int V){//n种物品,背包荣咯昂为V
for(int i=1;i<=n;++i){//枚举物品
for(int k=1;k*v[i]<=V;k<<=1)//枚举第i种物品个数
for(int j=V;j>=k*v[i];--j)//枚举容量
f[i][j]=std::max(f[i-1][j],f[i-1][j-k*v[i]]+k*c[i]);//此表达式有误
//因为此种定义方式使第i种物品只能取2^1,2^2……中的一种,而改为一维即正确
f[j]=std::max(f[j],f[j-k*v[i]]+k*c[i]);//正确,比较下两种写法的区别,自己思考
}
}
}多重背包问题
O(VN)的算法
我们只需把01
背包的一维数组写法的容量枚举的顺序由倒序变为正序即可。
核心代码
void Bag(int n,int V){
for(int i=1;i<=n;++i)//依次枚举前i件物品
for(int j=v[i];j<=V;++j)//从v[i]~V枚举背包容量
f[j]=std::max(f[j],f[j-v[i]]+c[i]);
}
代码只有v
的循环次序不同而已。为什么这样一改就可行呢?
首先想想为什么0/1
背包中要按照j=V..0
的逆序来循环。这是因为要保证第i
次循环中的状态f[i][j]
是由状态f[i-1][j-v[i]]
递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i
件物品”这件策略时,依据的是一个绝无已经选入第 i
件物品的子结果f[i-1][j-v[i]]
。
完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i
种物品”这种策略时,却正需要一个可能已选入第i
种物品的子结果f[i][j-v[i]]
,所以就可以并且必须采用j=0..V
的顺序循环。这就是这个简单的程序为何成立的道理。
这个算法也可以以另外的思路得出。例如,基本思路中的状态转移方程可以等价地变形成这种形式:
f[i][j]=max{f[i-1][j],f[i][j-v[i]]+c[i]}
f[i-1][j]
:表示第i
种物品一件也不取f[i][j-v[i]]
表示前i
种物品,包括第i
种已取若干的基础上再取一件第i
种物品N
种物品和一个容量为 V
的背包,第i
种物品最多有cnt[i]
件可用,第 i
件物品的体积是 \(v_i\),价值是 \(c_i\) 。求解将哪些物品装入背包可使价值总和最大。i
种物品有cnt[i]+1
种策略:取0
件,取1
件……取cnt[i]
件。f[i][j]
表示前i种物品恰放入一个容量为j
的背包的最大权值,则有状态转移方程:
f[i][j]=max{f[i-1][j-k*v[i]]+k*c[i]} (0<=k<=n[i])
将第i种物品分成若干件物品,其中每件物品有一个系数
这些系数分别为\(2^0,2^1,2^2,...,2^{k-1},cnt[i]-2^k+1\),且k
是满足\(cnt[i]\ge 2^k\)的最大整数。
cnt[i]
为13
,就将这种物品分成系数分别为1,2,4,6
的四件物品。1,2,4,6
能组合成1~13
之间的任何一个数。这样就将第i
种物品分成了O(log cnt[i])
种物品,将原问题转化为了复杂度为 \(O(V*\sum_1^n log\ ctn[i])\)的01
背包问题,是很大的改进。
核心代码实现:
void Bag(int n,int V){
for(int i=1;i<=n;++i){//枚举物品
int tot=0;//统计第i种物品已经分解出tot件
for(int k=1;tot+k<=cnt[i] && k*v[i]<=V;k<<=1){//要分解出k件第i物品
tot+=k;
for(int j=V;j>=k*v[i];--j)//对分解出来的k件i物品做01背包
f[j]=std::max(f[j],f[j-k*v[i]]+k*c[i]);
}
int x=cnt[i]-tot;//二进制分解剩下部分,x有可能很大
if(x)//剩下部分不为0,再跑一次01背包
for(int j=V;j>=x*v[i];--j)
f[j]=std::max(f[j],f[j-x*v[i]]+x*c[i]);
}
}
O(VN)
的算法。这个算法基于基本算法的状态转移方程,但应用单调队列的方法使每个状态的值可以以均摊O(1)
的时间求解。DP
目前对大家有一定难度,以后再讲有的物品只可以取一次(01
背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?
显然,枚举每件物品时根据物品的件数,选择相应的背包。
代码实现
#include <cstdio>
#include <cstring>
#include <algorithm>
const int maxn=1000+5,maxv=10000+5,Inf=0x7fffffff;
int f[maxv],v[maxn],c[maxn],cnt[maxn];
void multi_bag(int i,int V){//多重背包
int tot=0;//统计第i种物品已经分解出tot件
for(int k=1;tot+k<=cnt[i] && k*v[i]<=V;k<<=1){//要分解出k件第i物品
tot+=k;
for(int j=V;j>=k*v[i];--j)//对分解出来的k件i物品做01背包
f[j]=std::max(f[j],f[j-k*v[i]]+k*c[i]);
}
int x=cnt[i]-tot;//二进制分解剩下部分
if(x)//剩下部分不为0,再跑一次01背包
for(int j=V;j>=x*v[i];--j)
f[j]=std::max(f[j],f[j-x*v[i]]+x*c[i]);
}
void zero_bag(int i,int V){//01背包
for(int j=V;j>=v[i];--j)
f[j]=std::max(f[j],f[j-v[i]]+c[i]);
}
void complete_bag(int i,int V){//完全背包
for(int j=v[i];j<=V;++j)
f[j]=std::max(f[j],f[j-v[i]]+c[i]);
}
void Solve(){
int n,V;scanf("%d%d",&V,&n);
for(int i=1;i<=n;++i){
scanf("%d%d%d",&cnt[i],&v[i],&c[i]);
if(cnt[i]==1) zero_bag(i,V);
else if(cnt[i]>=V/v[i]) complete_bag(i,V);
else multi_bag(i,V);
}
printf("%d\n",f[V]);
}
int main(){
Solve();
return 0;
}
1
和代价2
,第i
件物品所需的两种代价分别为a[i]
和b[i]
。两种代价可付出的最大值(两种背包容量)分别为V
和U
。物品的价值为c[i]
。费用加了一维,只需状态也加一维即可。
设f[i][v][u]
表示前i件物品付出两种代价分别为 v
和 u
时可获得的最大价值。状态转移方程就是:
f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}
当前状态只跟上一行状态相关,所以我们可以省略第一维:
v
和 u
采用逆序的循环。N
件物品和一个容量为 V
的背包。第 i
件物品的体积v[i]
,价值是c[i]
。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。这个问题变成了每组物品有两种策略:
也就是说设f[k][v]
表示前 k
组物品用容量为 v
的背包装, 能取得的最大权值,则有:
f[k][V]=max{f[k-1][V],f[k-1][V-v[i]]+c[i]}
物品i
属于第k
组
使用一维数组的伪代码如下:
for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-v[i]]+c[i]}
for v=V..0
这一层循环必须在for 所有的i属于组k
之外。这样才能保证每一组内的物品最多只有一个会被添加到背包中。电子科大本部食堂的饭卡有一种很诡异的设计,即在购买之前判断余额。如果购买一个商品之前,卡上的剩余金额大于或等于5元,就一定可以购买成功(即使购买后卡上余额为负),否则无法购买(即使金额足够)。所以大家都希望尽量使卡上的余额最少。
某天,食堂中有 \(n\) 种菜出售,每种菜可购买一次。已知每种菜的价格以及卡上的余额,问最少可使卡上的余额为多少。
10
1 2 3 2 1 1 2 3 2 1
50
32
有 10 种菜,结果自己算吧
1
50
5
-45
只有一种菜,价格为 \(50\),卡上余额 \(5\) 元,此时买这个菜,剩余 \(-45\)。
暂时不想写了
有 \(N(N \le 100)\) 头奶牛,没有头奶牛有两个属性 \(s_i\) 和 \(f_i\),两个范围均为 \([-1000, 1000]\)。
从中挑选若干头牛,\(TS = \sum s[choose], TF = \sum f[choose]\)。
求在保证 \(TS\) 和 \(TF\) 均为非负数的前提下,\(TS+TF\)最大值。
有 5 头牛,下面分别是每头牛的两个属性
5
-5 7
8 -6
6 -3
2 1
-8 -5
选择第 1、3、4 三头牛为最优解
虽然加上 2 号,总和会更大,但是 TF 会变成负数,不合法
心情好的时候再加
有 \(N\) 种不同面值的硬币,分别给出每种硬币的面值 \(v_i\) 和数量 \(c_i\)。同时,售货员每种硬币数量都是无限的,用来找零。
要买价格为 \(T\) 的商品,求在交易中最少使用的硬币的个数(指的是交易中给售货员的硬币个数与找回的硬币个数之和)。
个数最多不能超过 \(20000\),如果不能实现,输出 \(-1\);否则输出此次交易中使用的最少的硬币个数。
有 \(3\) 种硬币,面值分别为 \(5, 25 50\),个数分别为 \(5, 2, 1\),要买 \(70\) 的商品,不存在给小费的情况下,最少的硬币个数为 \(3\)。
自己使用 \(25\) 和 \(50\) 各一个,找回一个面值为 \(5\) 的硬币。
还没顾上写;
标签:等于 完全 意义 假设 lin des ice 基础 image
原文地址:https://www.cnblogs.com/hbhszxyb/p/12232305.html