文章目录
零、前言
多重背包问题,和01背包、完全背包问题相比,实就是每个物品可以取的数目是限定的。它是01背包和完全背包的一般性问题。
一、多重背包初步
1.1问题描述
有n种物品和一个容量为m的背包。第i种物品最多有s件,每件体积是ci,价值是wi。求解选择哪些物品放入背包,使物品体积总和不超过背包容量,且价值总和最大。只输出最大价值。
1.2朴素算法
01背包:第 i 种物品取0件、1件。
多重背包:第 i 种物品取0、1、……、si件
如果我们把第i种物品拆成si件物品,然后当作01背包问题去求解,我们发现是可以得到正确解的。
在01背包的板子上稍加修改即可
// c[i],w[i],s[i];体积、价值、数目
for (int i = 1; i <= n; i++)
for (int j = m; j >= c[i]; j--)
for (int k = 0; k <= s[i] && k * c[i] <= j; k++)
f[j] = max(f[j], f[j - k * c[i]] + w[i] * k);
时间复杂度:O(mΣsi)
当我们有n,m,si <= 1000时,时间复杂度就能达到1e9量级,显然会超时,那么如何优化呢?
二、朴素算法的优化策略
2.1二进制优化
2.1.1算法思想
二进制拆分的思想我们在很多算法或者数据结构中都有涉及,如倍增算法、树状数组等。
朴素算法是把一件物品拆成了si件单独的物品,这样做太浪费了,二进制优化也是拆分物品,不过是这样拆的:
将第 i 件物品拆成20、21、22、……、2(k - 1)、si - 2^k + 1件物品,其价值和体积为单件价值/体积乘数量,然后也按照01背包做法去求解。
但是,朴素算法把一件拆成si件然后按照01背包去求解是正确的是因为其能够计算出每件物品去0~si件的情况,那么我们二进制拆分是否也能如此呢?
2.2二进制优化的正确性证明
对于20、21、22、……、2(k - 1)、si - 2^k + 1
对于前k项,可以组合出[0, 2^k - 1]内的数字,然后加上第k + 1项也就是si - 2^k + 1后,又能表示出[si - 2^k + 1, si]内的数字
故二进制优化可以考虑到原问题的所有情况。
2.3代码实现
时间复杂度:O(mΣlogsi),当有n,m,si <= 1000时,算法在1e7量级,是可以过的
//int f[N], n, m, c[N], w[N], tot = 0;
for (int i = 1, C, W, S; i <= n; i++)
{
cin >> C >> W >> S;//第i件物品的体积,价值,数目
for (int j = 1; j <= S; j <<= 1)
++tot, c[tot] = j * C, w[tot] = j * C, S -= j;
if (S)
++tot, c[tot] = S, w[tot] = s * W;
}
for (int i = 1; i <= tot; i++)//01背包板子
for (int j = m; j >= c[i]; j--)
f[j] = max(f[j], f[j - c[i]] + w[i]);
2.2单调队列优化
2.2.1算法思想
回看朴素算法代码
// c[i],w[i],s[i];体积、价值、数目
for (int i = 1; i <= n; i++)
for (int j = m; j >= c[i]; j--)
for (int k = 0; k <= s[i] && k * c[i] <= j; k++)
f[j] = max(f[j], f[j - k * c[i]] + w[i] * k);
f[j] = max(f[j], f[j - c[i]] + w[i], f[j - 2*c[i]] + 2*w[i], …, f[j - k * c[i]] + w[i] * k)
我们发现对于第 i 件物品而言,f(i, j)的可转移前驱状态f(i - 1, k)都满足k 和 j对c[i]的余数相同
那么我们可以将原本的f[0 … m]简化为f[0, 1, …, c[i] - 1],即按对c[i]的余数将状态分类:
f[0]、f[c[i]]……f[kc[i]]
f[1]、f[1 + c[i]]……f[1 + kc[i]]
……
f[c[i] - 1]、f[2*c[i] - 1]……f[(k + 1)c[i] - 1]
f[j]是由前面不超过数量s的同类值递推得到的。这就相当于从前面宽度为s的窗口挑选最大值来更新当前值。所以,我们用单调队列来维护窗口最大值,这样**每次获取窗口最大值是均摊O(1)**的。
这就需要顺序更新f值,怎么办呢?只需要增加一个备份数组g即可。
2.2.2代码实现
时间复杂度:O(n C m/C) = O(nm)**
for (int i = 1, C, W, S; i <= n; i++)
{
memcpy(g, f, sizeof f), cin >> C >> W >> S;//体积、价值、数目
for (int j = 0; j < C; j++)
{
h = t = 0;
for (int k = j; k <= m; k += C)
{
if (t - h && q[h] < k - S * C)//队头不在区间[k - S*C, k - C]内
h++;
if (t - h)
f[k] = max(f[k], g[q[h]] + (k - q[h]) / C * W);//状态转移
while (t - h && g[k] >= g[q[t - 1]] + (k - q[t - 1]) / C * W)//维护单调队列队头到队尾单调递减
t--;
q[t++] = k;
}
}
}
2.3、总结
两种优化方法都应用了拆分思想
二进制优化拆分的是物品数量si,si件拆分成logsi件
单调队列优化拆分的是背包容量m,根据v的余数,把f[0…m]拆分成c个类,使f[0…m] 在0(m)内完成更新。
三、OJ练习
3.1P1776 宝物筛选
3.1.1原题链接
P1776 宝物筛选 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
3.1.2思路分析
板子题,直接跑板子
3.1.3AC代码
3.1.3.1二进制优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
const int N = 1010, M = 1e5 + 10;
int f[M], q[M], c[M], w[M], n, m, tot;
signed main()
{
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, C, W, S; i <= n; i++)
{
cin >> W >> C >> S;
for (int j = 1; j <= S; j <<= 1)
++tot, c[tot] = j * C, w[tot] = j * W, S -= j;
if (S)
++tot, c[tot] = S * C, w[tot] = S * W;
}
for (int i = 1; i <= tot; i++)
for (int j = m; j >= c[i]; j--)
f[j] = max(f[j], f[j - c[i]] + w[i]);
cout << f[m];
return 0;
}
3.1.3.1单调队列优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
const int N = 1010, M = 1e5 + 10;
int f[M], g[M], q[M], n, m, h, t;
signed main()
{
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1, c, w, s; i <= n; i++)
{
memcpy(g, f, sizeof f), cin >> w >> c >> s;
for (int j = 0; j < c; j++)
{
h = t = 0;
for (int k = j; k <= m; k += c)
{
if (t - h && q[h] < k - c * s)
h++;
if (t - h)
f[k] = max(g[k], g[q[h]] + (k - q[h]) / c * w);
while (t - h && g[k] >= g[q[t - 1]] + (k - q[t - 1]) / c * w)
t--;
q[t++] = k;
}
}
}
cout << f[m];
return 0;
}
3.2P1782 旅行商的背包
3.2.1原题链接
P1782 旅行商的背包 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
3.2.2思路分析
对于前n个物品我们当作多重背包去解
然后后m个奇货,枚举体积当01背包做就行,由于m最大也就5,时间复杂度完全可以
3.2.3AC代码
3.2.3.1二进制优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
int read()
{
int s = 0, w = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
w *= -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
s = (s << 3) + (s << 1) + (ch ^ 48);
ch = getchar();
}
return s * w;
}
void write(int x)
{
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar((x % 10) ^ 48);
}
const int N = 1e4 + 10, M = 1e4 + 10;
int f[M], c[N * 10], w[N * 10], n, m, V, tot;
signed main()
{
//freopen("in.txt", "r", stdin);
n = read(), m = read(), V = read();
for (int i = 1, C, W, S; i <= n; i++)
{
C = read(), W = read(), S = read();
for (int j = 1; j <= S; j <<= 1)
c[++tot] = C * j, w[tot] = W * j, S -= j;
if (S)
c[++tot] = C * S, w[tot] = W * S;
}
for (int i = 1; i <= tot; i++)
for (int j = V; j >= c[i]; j--)
f[j] = max(f[j], f[j - c[i]] + w[i]);
for (int i = 1, x, y, z; i <= m; i++)
{
x = read(), y = read(), z = read();
for (int j = V; j; j--)
for (int k = 0; k <= j; k++)
f[j] = max(f[j], f[j - k] + (x * k + y) * k + z);
}
write(f[V]);
return 0;
}
3.2.3.1单调队列优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
int read()
{
int s = 0, w = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
w *= -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
s = (s << 3) + (s << 1) + (ch ^ 48);
ch = getchar();
}
return s * w;
}
void write(int x)
{
if (x < 0)
putchar('-'), x = -x;
if (x > 9)
write(x / 10);
putchar((x % 10) ^ 48);
}
const int N = 1e4 + 10, M = 1e4 + 10;
int f[M], g[M], q[N * 10], n, m, V, h, t;
signed main()
{
//freopen("in.txt", "r", stdin);
n = read(), m = read(), V = read();
for (int i = 1, C, W, S; i <= n; i++)
{
C = read(), W = read(), S = read(), memcpy(g, f, sizeof f);
for (int j = 0; j < C; j++)
{
h = t = 0;
for (int k = j; k <= V; k += C)
{
while (t - h && q[h] < k - S * C)
h++;
if (t - h)
f[k] = max(g[k], g[q[h]] + (k - q[h]) / C * W);
if (t - h && g[k] >= g[q[t - 1]] + (k - q[t - 1]) / C * W)
t--;
q[t++] = k;
}
}
}
for (int i = 1, x, y, z; i <= m; i++)
{
x = read(), y = read(), z = read();
for (int j = V; j; j--)
for (int k = 0; k <= j; k++)
f[j] = max(f[j], f[j - k] + (x * k + y) * k + z);
}
write(f[V]);
return 0;
}
3.3Dividing
3.3.1原题链接
3.3.2思路分析
背包求解可行性问题
挺无脑的,我们把价值当作体积,价值当作价值(我在说什么=-=
这俩哥们给大理石附加的value我们当作价值也当作体积,然后背包体积当作价值的二分之一,看这个包能不能装满就行
然后就是跑板子,没啥思维难度
3.3.3AC代码
3.3.3.1二进制优化
#include <iostream>
#include <cstring>
#include <algorithm>
#include <numeric>
using namespace std;
#define int long long
int f[6 * 20000 + 10], c[20000 * 500], w[20000 * 500], tot;
int s[7], m, _ = 0;
signed main()
{
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
while (cin >> s[1] >> s[2] >> s[3] >> s[4] >> s[5] >> s[6], s[1] || s[2] || s[3] || s[4] || s[5] || s[6])
{
tot = 0, m = s[1] + s[2] * 2 + s[3] * 3 + s[4] * 4 + s[5] * 5 + s[6] * 6;
if (m & 1)
{
cout << "Collection #" << ++_ << ":\n"
<< "Can't be divided.\n";
continue;
}
m >>= 1, memset(f, 0, sizeof f);
for (int i = 1; i <= 6; i++)
{
for (int j = 1; j <= s[i]; j <<= 1)
c[++tot] = i * j, w[tot] = i * j, s[i] -= j;
if (s[i])
c[++tot] = i * s[i], w[tot] = i * s[i];
}
for (int i = 1; i <= tot; i++)
for (int j = m; j >= c[i]; j--)
f[j] = max(f[j], f[j - c[i]] + w[i]);
if (f[m] == m)
cout << "Collection #" << ++_ << ":\n"
<< "Can be divided.\n";
else
cout << "Collection #" << ++_ << ":\n"
<< "Can't be divided.\n";
}
return 0;
}
3.3.3.2单调队列优化
#include <iostream>
#include <cstring>
#include <algorithm>
#include <numeric>
using namespace std;
#define int long long
int f[6 * 20000 + 10], g[6 * 20000 + 10], q[6 * 20000 + 10], h, t;
int s[7], m, _ = 0;
signed main()
{
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
while (cin >> s[1] >> s[2] >> s[3] >> s[4] >> s[5] >> s[6], s[1] || s[2] || s[3] || s[4] || s[5] || s[6])
{
m = s[1] + s[2] * 2 + s[3] * 3 + s[4] * 4 + s[5] * 5 + s[6] * 6;
if (m & 1)
{
cout << "Collection #" << ++_ << ":\n"
<< "Can't be divided.\n\n";
continue;
}
m >>= 1, memset(f, 0, sizeof f);
for (int i = 1; i <= 6; i++)
{
memcpy(g, f, sizeof f);
for (int j = 0; j < i; j++)
{
h = t = 0;
for (int k = j; k <= m; k += i)
{
if (t - h && q[h] < k - s[i] * i)
h++;
if (t - h)
f[k] = max(g[k], g[q[h]] + (k - q[h]) / i * i);
while (t - h && g[k] >= g[q[t - 1]] + (k - q[t - 1]) / i * i)
t--;
q[t++] = k;
}
}
}
if (f[m] == m)
cout << "Collection #" << ++_ << ":\n"
<< "Can be divided.\n\n";
else
cout << "Collection #" << ++_ << ":\n"
<< "Can't be divided.\n\n";
}
return 0;
}
3.4Coins
3.4.1原题链接
3.4.2思路分析
同样是背包解决可行性问题
在本题中背包体积就是m,物体体积就是物体的价值,跑完背包板子从1遍历到m看看可行的状态有多少就行
3.4.3AC代码
3.4.3.1二进制优化
#include <iostream>
#include <cstring>
#include <algorithm>
#include <numeric>
using namespace std;
#define int long long
const int N = 110, M = 100000 + 10;
int n, m, c[N * 10], A[N], tot, ret;
bool f[M];
signed main()
{
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
while (cin >> n >> m, n)
{
memset(f, 0, sizeof f), tot = ret = 0;
for (int i = 1; i <= n; i++)
cin >> A[i];
for (int i = 1, S; i <= n; i++)
{
cin >> S;
for (int j = 1; j <= S; j <<= 1)
c[++tot] = A[i] * j, S -= j;
if (S)
c[++tot] = A[i] * S;
}
f[0] = 1;
for (int i = 1; i <= tot; i++)
for (int j = m; j >= c[i]; j--)
f[j] = f[j] || f[j - c[i]];
for (int i = 1; i <= m; i++)
ret += f[i];
cout << ret << '\n';
}
return 0;
}
3.4.3.2单调队列优化
#include <iostream>
#include <cstring>
#include <algorithm>
#include <numeric>
using namespace std;
#define int long long
const int N = 110, M = 100000 + 10;
int n, m, q[N * 10], h, t, A[N], ret;
bool f[M], g[M];
signed main()
{
//freopen("in.txt", "r", stdin);
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
while (cin >> n >> m, n)
{
memset(f, 0, sizeof f), ret = 0;
for (int i = 1; i <= n; i++)
cin >> A[i];
f[0] = 1;
for (int i = 1, s; i <= n; i++)
{
cin >> s, memcpy(g, f, sizeof f);
for (int j = 0; j < A[i]; j++)
{
h = t = 0;
for (int k = j; k <= m; k += A[i])
{
while (t - h && q[h] < k - s * A[i])
h++;
if (t - h)
f[k] = g[k] || g[q[h]];
while (t - h && g[k] >= g[q[t - 1]])
t--;
q[t++] = k;
}
}
}
for (int i = 1; i <= m; i++)
ret += f[i];
cout << ret << '\n';
}
return 0;
}
3.5Cash Machine
3.5.1原题链接
1276 – Cash Machine (poj.org)(男人八题!)
3.5.2思路分析
也是可行性问题
背包体积即cash,然后物体的体积就是钱的面额,然后就是跑板子就行了
3.5.3AC代码
3.5.3.1二进制优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
#define sc scanf
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
w *= -1;
ch = getchar();
}
while (ch < '0' || ch > '9')
{
s = (s << 1) + (s << 3) + (ch ^ 48);
ch = getchar();
}
return s * w;
}
inline void write(int x)
{
if (x < 0)
x = -x, putchar('-');
if (x > 9)
write(x / 10);
putchar((x % 10) ^ 48);
}
const int N = 1e5 + 10;
bool f[N];
int c[N * 10], w[N * 10], tot, n, m;
signed main()
{
//freopen("in.txt", "r", stdin);
while (~sc("%lld%lld", &m, &n))
{
tot = 0, memset(f, 0, sizeof f);
for (int i = 1, S, W; i <= n; i++)
{
sc("%lld%lld", &S, &W);
// S = read(), W = read();
for (int j = 1; j <= S; j <<= 1)
++tot, c[tot] = w[tot] = j * W, S -= j;
if (S)
++tot, c[tot] = w[tot] = S * W;
}
f[0] = 1;
for (int i = 1; i <= tot; i++)
for (int j = m; j >= c[i]; j--)
f[j] = f[j] || f[j - c[i]];
int ma = m;
for (; !f[ma]; ma--)
;
write(ma), putchar('\n');
}
return 0;
}
3.5.3.2单调队列优化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
#define sc scanf
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while (ch < '0' || ch > '9')
{
if (ch == '-')
w *= -1;
ch = getchar();
}
while (ch < '0' || ch > '9')
{
s = (s << 1) + (s << 3) + (ch ^ 48);
ch = getchar();
}
return s * w;
}
inline void write(int x)
{
if (x < 0)
x = -x, putchar('-');
if (x > 9)
write(x / 10);
putchar((x % 10) ^ 48);
}
const int N = 1e5 + 10;
bool f[N], g[N];
int q[N], h, t, n, m;
signed main()
{
//freopen("in.txt", "r", stdin);
while (~sc("%lld%lld", &m, &n))
{
memset(f, 0, sizeof f), f[0] = 1;
for (int i = 1, s, w; i <= n; i++)
{
sc("%lld%lld", &s, &w), memcpy(g, f, sizeof f);
for (int j = 0; j < w; j++)
{
h = t = 0;
for (int k = j; k <= m; k += w)
{
if (t - h && q[h] < k - s * w)
h++;
if (t - h)
f[k] = g[k] || g[q[h]];
while (t - h && g[k] >= g[q[t - 1]])
t--;
q[t++] = k;
}
}
}
int ma = m;
for (; !f[ma]; ma--)
;
write(ma), putchar('\n');
}
return 0;
}
三、总结
在实际刷题过程中,个人感觉二进制优化板子写着最无脑,出错率最低,单调队列有时候还会敲错个变量啥的,而且实测似乎很多题不比单调队列慢,单调队列稍微写错一点就TLE了,当然可能是自己菜(,总体而言,多重背包问题还是比较简单的一类问题,当作变形的01背包就行了。
标签:10,ch,背包,二进制,int,详解,3.1,include,优化 From: https://blog.csdn.net/EQUINOX1/article/details/136679818