01背包:
题目一:采药
思路就是理解之后简化空间复杂度。
因为原本上就是加上上一轮的只要省略前者就可以。
题目二:开心的金明
就是理解上题之后改变状态转移方程为
总结加反思(推理版):
一句废话: 一开始学习背包算法没有仔细考虑这个状态转移的细节,在这里做一个总结:
先引用 oiwiki 里的一句话解释为什么次主循环要以
的顺序来推:
- 对于当前处理的物品
和当前状态
,在
时,
是会被
所影响的。这就相当于物品
可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法)
- 为了避免这种情况发生,我们可以改变枚举的顺序,从
枚举到
,这样就不会出现上述的错误,因为
总是在
前被更新。
如果没看懂,那就再看下面的解释:
其中的
一句恰就相当于我们的转移方程
,因为现在的
就相当于原来的
。如果将
的循环顺序从上面的逆序改成顺序的话,那么则成了
由
推知,与本题意不符。
如果你还没看懂,建议自己手推一下题目一的样例,如果懒得推下图就是最简版:
还有一点:初始化的细节问题
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
- 如果是第一种问法,要求恰好装满背包,那么在初始化时除了
为 0 其它
均设为
,这样就可以保证最终得到的
是一种恰好装满背包的最优解。
- 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将
全部设为0。
为什么呢?可以这样理解:
初始化的
数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为 0 的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是
了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为 0,所以初始时状态的值也就全部为 0 了。下面代码中也有解释。
模版:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 1e5+10;
const int INF = 0x3f3f3f3f;
int n, m;
int dp[N], w[N], c[N];
auto work=[]()
{
//初始化的区别
memset(dp,0,sizeof(dp));//这是不用塞满的
// dp[0] = 0; 这是塞满的
// L(i, 1, n) dp[i] = -INF;
cin >> n >> m;
L(i, 1, m) cin >> w[i] >> c[i];
L(i, 1, m)
{
L(j, w[i], n)
{
dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
}
}
cout << dp[n];
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
****************************************************************************************************************************************************************************************************************************
完全背包:
题目一:完全背包问题
题目大意:设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。
不难根据上面的推理,已经可以得出完全背包的状态转移方程了:
procedure CompletePack(cost,weight)
for v=cost..V
f[v]=max{f[v],f[v-c[i]]+w[i]}
总结加反思(复杂度版):
一个问题:完全背包跟01背包问题一样有 个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态
的时间是
,总的复杂度是超过
的。
为了将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的方程的确是很重要,可以推及其它类型的背包问题。但我们还是试图改进这个复杂度。
一个简单有效的优化:
完全背包问题有一个很简单有效的优化,是这样的:若两件物品
、
满足
且
,则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得
换成物美价廉的
,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。
这个优化可以简单的
地实现,一般都可以承受。
另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于
的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以
地完成这个优化。
更高效的转化方法是:把第i种物品拆成费用为
、价值为
的若干件物品,其中
满足
。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个
件物品的和。这样把每种物品拆成
件物品,是一个很大的改进。
但我们有更优的
的算法。但我不会告诉你,因为我已经讲过了就在上面。
****************************************************************************************************************************************************************************************************************************
多重背包:
总结加反思:
又一个问题:对于第 种物品有
种策略:取 0 件,取 1 件……取
件。令
表示前
种物品恰放入一个容量为
的背包的最大权值,则有状态转移方程:
但复杂度为
转化为01背包问题
- 另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成
件01背包中的物品,则得到了物品数为
的01背包问题,直接求解,复杂度仍然是
。
- 但是我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。仍然考虑二进制的思想,我们考虑把第
种物品换成若干件物品,使得原问题中第
种物品可取的每种策略——取
件——均能等价于取若干件代换以后的物品。另外,取超过
件的策略必不能出现
- 方法是:将第
种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为
,且k是满足
的最大整数。例如,如果
为13,就将这种物品分成系数分别为
的四件物品。
这样就将第i种物品分成了 种物品,将原问题转化为了复杂度为
的01背包问题,是很大的改进。
下面给出O(log amount)时间处理一件多重背包中物品的过程,其中amount表示物品的数量:
procedure MultiplePack(cost,weight,amount)
if cost*amount>=V
CompletePack(cost,weight)
return
integer k=1
while k<num
ZeroOnePack(k*cost,k*weight)
amount=amount-k
k=k*2
ZeroOnePack(amount*cost,amount*weight)
/*希望你仔细体会这个伪代码,如果不太理解的话,不妨翻译成程序代码以后,单步执行几次,或者头脑加纸笔模拟一下,也许就会慢慢理解了。*/
模版:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 1e5+100;
int n, m, cnt = 0; // n是物品种数,m是总容量
int num[N]; // num对应每种物品的个数
int dp[N], w[N], c[N]; // w是每个物品的重量
auto work=[]() // c是每个物品的价值
{
cin >> n >> m;
L(i, 1, n)
{
int x, y;
cin >> num[i] >> x >> y;
int k = 1;
while(k <= num[i])
{
cnt++;
w[cnt] = k * x;
c[cnt] = k * y;
num[i] -= k;
k <<= 1; // 等价于 k *= 2;
}
if(num[i])
{
cnt++;
w[cnt] = num[i] * x;
c[cnt] = num[i] * y;
}
}
n = cnt;
L(i, 1, n)
R(j, m, w[i])
dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
cout << dp[m];
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
****************************************************************************************************************************************************************************************************************************
混合背包&二维费包:
没错,二维费包就是二维费用背包
放在一块就是因为太相似了 —— —— 都太简单了
其实没有必要太复杂(——竟然不知不觉胡乱bb了4句了。。。。)
回归正题:
混合背包混在哪里?
- 如果将01背包、完全背包、多重背包,三者的杂交版放在一个题目中,那就是混合背包。
- 故如果有三类物品,一类只能取一次,一类只能取
次,另外一类能取无数次,那么只需在对每个物品应用转移方程是,根据物品的类别选用顺序或逆序的循环即可,复杂度是
。
模版1:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 1e5+10;
int n, m, cnt = 0;
int dp[N], w[N], c[N], num[N];
auto work=[]()
{
cin >> m >> n;
L(i, 1, n)
{
int x, y, z; //体积 价值 个数
cin >> x >> y >> z;
if(z != 0 && z != 1)
{
int k = 1;
while(k <= z)
{
cnt++;
w[cnt] = k * x;
c[cnt] = k * y;
num[cnt] = k;
z -= k;
k <<= 1;
}
if(z)
{
cnt++;
w[cnt] = z * x;
c[cnt] = z * y;
num[cnt] = z;
}
}else{
cnt++;
w[cnt] = x;
c[cnt] = y;
num[cnt] = z;
}
}
L(i, 1, cnt)
{
if(!num[i])
{
L(j, w[i], m)
dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
}else{
R(j, m, w[i])
dp[j] = max(dp[j],dp[j-w[i]]+c[i]);
}
}
cout << dp[m];
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
二维背包二在哪里?
- 二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。
设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为
和
。两种代价可付出的最大值(两种背包容量)分别为
和
。物品的价值为
。费用加了一维,只需状态也加一维即可。设
表示前
件物品付出两种代价分别为
和
时可获得的最大价值。状态转移方程就是:
如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量
和
采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品。
模版2:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 505;
const int M = 1e5+10;
int vv, mm, n, cnt = 0; //总体积 总质量 两个限制条件
int dp[N][N], w[M], c[M], v[M];
auto work=[]()
{
cin >> vv >> mm >> n;
L(i, 1, n)
cin >> v[i] >> w[i] >> c[i]; //体积 质量 价值
L(k, 1, n)
R(i, vv, v[k])
R(j, mm, w[k])
dp[i][j] = max(dp[i][j],dp[i-v[k]][j-w[k]]+c[k]);
cout << dp[vv][mm];
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
内个。。。就是因为太简单了就不给离题啦吧。。。
****************************************************************************************************************************************************************************************************************************
分组背包:
题目:通天之分组背包
- 这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设
表示前
组物品花费费用
能取得的最大权值,则有:
物品
属于组
。
使用一维数组的伪代码如下:
for 所有的组k
for v=V..0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}
要千万千万注意这三层循环的意思和顺序不能搞错了
再给出oiwiki上的核心代码做以借鉴:
for (int k = 1; k <= ts; k++) // 循环每一组
for (int i = m; i >= 0; i--) // 循环背包容量
for (int j = 1; j <= cnt[k]; j++) // 循环该组的每一个物品
if (i >= w[t[k][j]]) // 背包容量充足
dp[i] = max(dp[i],
dp[i - w[t[k][j]]] + c[t[k][j]]); // 像0-1背包一样状态转移
模版&题解:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define L(i, j, k) for(int i=j; i <= k; i++)
#define R(i, j, k) for(int i=j; i >= k; i--)
#define fastio ios::sync_with_stdio(false),cin.tie(NULL),cout.tie(NULL)
namespace Dino
{
const int N = 1e5+10;
const int M = 505;
int vv, n, T;
int dp[N], t[M][M], w[N], c[N], tong[N];
auto work=[]()
{
cin >> vv >> n; //cin >> T;
L(i, 1, n)
{
int x, y, z; // 体积 价值 所属组号
cin >> x >> y >> z;
T = max(T, z); // 应题目要求本来这一句在上面就读入了
t[z][++tong[z]] = i;
w[i] = x;
c[i] = y;
}
L(k, 1, T)
R(i, vv, 0)
L(j, 1, tong[k])
if(i >= w[t[k][j]])
dp[i] = max(dp[i],dp[i-w[t[k][j]]]+c[t[k][j]]);
cout << dp[vv];
};
}
signed main()
{
fastio;
return Dino::work(), 0;
}
****************************************************************************************************************************************************************************************************************************
有关于基础背包类型到这里就结束了,我会在下一篇再给出变形背包和其他有意思的优化。
完结撒花
人生不能像做菜,把所有的料都准备好才下锅 —— ——《饮食男女》