闲言:
唠点闲话,蓝桥杯要10.17举办,我也开学了,放在这上面的时间会相对减少了,因为要照顾绩点嘛,话说这几天还挺累的,因为下周要考试了,终于把高数从头到尾复习了一遍,接下来就还有英语,大物还得复习,哎,还是要坚持住啊,吃得苦中苦方为人上人嘛,加油喽,文化课学累了,就来学一下算法吧,背包问题也学了挺久了,今天简单整理一下,不过可能整理不完,会慢慢补全的。
(01)背包
题目概述:
(01)背包就是每件物品只可以使用一次,然后求总体积不超过背包容量的总价值最大。
数据范围
(0) (<) (N),(V) (leq) (1000)
(0) (<) (V_i,W_i) (leq) (1000)
思路:
状态表示:(f(i,j)) 其中的(i)表示只从前(i)个物品选,(j)表示选出来的物品总体积 (leq) j,因此(f(i,j))就表示从前(i)个物品中选出总体积不超过(j)的最大价值。
状态计算:集合的划分
我们划分为不含(i)个物品与包含(i)个物品两个集合。
不含第(i)个物品的话,用状态方程表示就是:(f(i, j) = f(i-1,j))。
包含第(i)个物品,则我们可以考虑先把第(i)个物品去掉,因为我们在去掉已经确定了价值的第(i)个物品的情况下并不会影响剩下(i-1)个最大值,最后再将(w[i])加上即可,则状态转移方程:
因此我们可以得到这道题的状态转移方程:
因此可以写出代码:
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int j = 1; j <= n; j++) f[j][0] = 0; //表示在体积为0的情况下 价值为0
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i-1][j]; //不考虑第i件物品
if(j >= v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i])
}
}
cout << f[n][m] << endl;
return 0;
}
然后我们可以考虑优化成一维:
- 先都去掉一维,则变成了
for(int i = 1; i <= n; i++)
{
for(int j = 0; j <= m; j++)
{
f[j] = [j]; //不考虑第i件物品
if(j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i])
}
}
想想这样合适吗,当然不合适,j是从小到大枚举的,因此(j - v[i])是已经被更新了,即是在当前第(i)层被更新,那么现在再把它变成二维就等价于(f[i][j] = max(f[i][j], f[i][j-v[i]] + w[i])),而原答案就是(f[i][j] = max(f[i][j], f[i - 1][j-v[i]] + w[i]))应该是从(i - 1)层更新过来的,因此需要改一下第二层(for)循环,让(j)从大到小枚举,(j - v[i] < j),因此这里的(j - v[i])用的还是是(i - 1)层的,所以符合原来二维状态方程,同时,调节了第二层循环之后,(j >= v[i])变成恒等式,直接去掉(if)就好了,对于, (f[i][j] = f[i-1][j]) -> (f[j] = f[j]),等价吗,当然等价,因为算(f[j])的时候,左边(f[j])是第(i)层的,待更新,右边的值是(i - 1)层的(j),我们既然要用(i - 1)层的更新i层的,那么新的(f[j])就完全等价于旧的(f[j]), 那么f[j] = f[j]就完全可以直接省略就好。
因此代码变成了这样
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = m; j >= v[i]; j--)
{
f[j] = max(f[j], f[j-v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
完全背包
题目概述:
每种物品可以有无限件可用,求最大价值
数据范围:
(0) (<) (N),(V) (leq) (1000)
(0) (<) (V_i,W_i) (leq) (1000)
思路:
状态表示:(f(i,j)) 其中的(i)表示只从前(i)种物品选,(j)表示选出来的物品总体积 (leq) j,因此(f(i,j))就表示从前(i)种物品中选出总体积不超过(j)的所有选法。
状态计算:集合划分
第(i)种物品选(0),(1),(2),(3),(……),(k-1),(k)个。
如果第(i)种物品,一个也不选,则说明我们只考虑前(i-1)种物品,即:(f(i-1, j))
如果选第(i)种物品选(k)个,则我们可以采取与(01)背包类似的思路,去掉已经确定了价值的(k)个第(i)种物品,因为这并不会影响前(i-1)种物品我们选法的最大价值,因此得到方程:
这里注意,第(i)种物品如果我们不选的话,就等价于(f(i-1, j)),因此不选的情况我们也可以并到上式子中,因此我们得到最终式子:
可以得到朴素版代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
for(int k = 0; k*v[i] <= j; k++)
{
f[i][j] = max(f[i][j], f[i-1][j - k * v[i]] + k * w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
但是看本题的数据范围就知道这个(O(N^3))的代码是一定会(TLE)的,然后进行优化:
将式子展开:
而:
观察上下两式,我们看出式子(1)后 (f(i-1, j-v) + w, f(i-1, j-2*v) + 2*w, f(i-1, j-3*v) + 3*w, ...)可以由式子(2):(f(i, j-v)) (+) (w)得到,于是我们就可以进行合并,我们可以得到这样的式子:
因此这样就可以不用再枚举(k)了,只需要枚举两个状态即可。
(f(i, j) = max(f(i-1, j), f(i, j-v) + w))与(01)背包的状态转移方程并不同,(01)背包需要从(i-1)转移而来,而完全背包是从(i)转移而来。
优化后的代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
f[i][j] = f[i-1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
还可以优化成一维:参考上面的01背包二维优化成一维,因为我们这里的状态f[i][j]是第i件物品选几个,我们用第i层的更新第i层的,这里我们从小到大枚举,因此得到代码。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
{
for(int j = v[i]; j <= m; j++)
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
多重背包(I)
题目概述:
每件物品只能选有限个,然后求总体积不超过背包容量的总价值最大。
数据范围
(0) (<) (N),(V) (leq) (100)
(0) (<) (V_i,W_i) (leq) (100)
思路:
题目只有(0-100)的数据范围,因此(O(n^3))是完全可以的,那么这道题就跟完全背包的朴素做法思路基本一样了,只是多了一个限定条件,所选物品有限所以直接写就好。
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int f[N][N];
int v[N], w[N], s[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i] >> s[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= s[i] && k * v[i] <= j; k++) {
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
多重背包 (II)
题目概述:
每件物品只能选有限个,然后求总体积不超过背包容量的总价值最大。
数据范围
(0) (<) (N) (leq) (1000)
(0) (<) (V) (leq) (2000)
(0) (<) (v_i,w_i,s_i) (leq) (2000)
思路:
二进制优化
这个如果再用朴素做法就不行了,(O(NVS)) 大约要(4*10^9),肯定不行,而若我们对(S)进行二进制拆开然后将整个问题当成(01)背包来做,那么复杂度就被有优化为了(O(NVlogS)) 大约为(2*10^7),优化了两个数量级。
那为什么我们不用优化完全背包的方法来优化多重背包呢?先从状态转移方程出发
而:
可见(2)式比(1)式多了一项,因此不能这样优化。
进入二进制优化正题:
假设有件物品的数量为(200),那么我们可以分成(1, 2, 4, 8, 16, 32, 64)这么几组,还剩下一个(73)不再拆分,分成的每一组我们最多只能拿一次那么就可以按照(01)背包做了。
那会出现什么问题吗,例如我们想拿3件但是并没有啊,这问题并不会出现,(1 + 2 = 3)啊, 对于例子,(1,2)可以凑出(1-3)的任何一个数字,同理(1, 2)加一个4就可以凑出(1-7)任何一个数字,再同理,假设我们可以把(n)分出一组(1, 2, ..., 2^k, C),(C)不足以(2^{k+1}),否则我们就是(1, 2, ..., 2^k, 2^{k+1}),我们可以用(1, 2, ..., 2^k)凑出(2^{k+1} - 1)(根据等差数列求和)中的任何一个数字,那么再加一个(C)就可以凑出(1-n)中任何一个数。那么这样再做一遍(01)背包即可,复杂度就由(O(NVS))优化到了(O(NVlogS))。
代码:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12000;
int v[N], w[N];
int n, m, f[N];
int main() {
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; i++) {
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while (k <= s) {
cnt++;
v[cnt] = a * k, w[cnt] = b * k; //每个物品a 价值为b
s -= k;
k *= 2;
}
if (s > 0) {
cnt++;
v[cnt] = a * s, w[cnt] = b * s;
}
}
n = cnt;
for (int i = 1; i <= n; i++) {
for (int j = m; j >= v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}
分组背包
有 (N) 组物品和一个容量是 (V) 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 (v_{ij}),价值是 (w_{ij}),其中 (i) 是组号,(j) 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 (N,V),用空格隔开,分别表示物品组数和背包容量。
接下来有 (N) 组数据:
每组数据第一行有一个整数 (S_i),表示第 (i) 个物品组的物品数量;
每组数据接下来有 (S_i) 行,每行有两个整数 (v_{ij}),(w_{ij}),用空格隔开,分别表示第 (i) 个物品组的第 (j) 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
(0<N,V≤100)
(0<S_i≤100)
(0<v_{ij},w_{ij}≤100)
思路:
还是大同小异的代码与思路,因为这里是每层只能选一种我们从(i-1)层更新,所以写成一维从大到小枚举
思路
#include <iostream>
using namespace std;
const int N = 110;
int s[N], w[N][N], v[N][N];
int n, m, f[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> s[i];
for (int j = 0; j < s[i]; j++) {
cin >> v[i][j] >> w[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 0; k < s[i]; k++) { //枚举有多少个
if (v[i][k] <= j) {
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
}
cout << f[m] << endl;
return 0;
}