参考:背包九讲2.0
一般解法和空间优化比较熟悉,略过,这里仅记一下初始化时的一个小问题.
对于要求恰好装满背包时,我们可以在初始化时令$F[0] = 0$,$F[1...V]$均为$-\infty$,其中$V$是背包容量,$F$是状态数组.
而在没有要求恰好装满背包时,则可以全部初始化为$0$.
可以这样理解其中的区别:初始化的$F$数组是没有任何物品放入背包时的解,在要求恰好装满的情况下,除了$0$以外的其他容量均没有合法的解,而如果不要求装满,那么没有装入任何物品时,所有容量的背包都有一个合法的零解.
完全背包问题:
给出$N$种物品,每种物品有无限个,给出每种物品的体积$C_i$和价值$W_i$,给出背包的容量$V$,求不超过背包容量的情况下不断装入物品,可以得到的最大价值。
思路:
首先我们考虑最基本的转移方程:
令$F[i][j]$表示考虑前$i$件物品,背包容量为$j$时的最大价值,有:
我们用更清晰的形式写出来:
F[i][j - C_i] = max{\qquad F[i - 1][j - C_i],F[i - 1][j - 2C_i] + W_i,...,F[i - 1][j - kC_i] + (k - 1)W_i} $$ 把两个式子综合一下,得到:
$$ F[i][j] = max{F[i - 1][j],F[i][j - C_i] + W_i} $$ 消去第一维,得到:
$$ F[j] = max{F[j],F[j - C_i] + W_i} $$ 从代码上来看,只需调转$01$背包中第二层循环的枚举顺序:
for(int i = 1; i <= N; i ++ )
{
for(int j = C[i]; j <= V; j ++ )
{
F[j] = max(F[j],F[j - C[i]] + W[i]);
}
}
我们也可以从另一个角度来理解这个方程:当我们考虑$F[i][j]$时,需要的是已经选入第$i$件物品的子结果$F[i - 1][j - C_i]$,因此在一维的情况下,正序枚举容量时,$F[j - C_i]$已经是一个考虑过选入$C_i$的子结果,因此转移方程成立。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define debug(x) cout << #x << " = " << x << endl;
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define pll pair<ll,ll>
#define ld long double
#define ull unsigned long long
#define pb push_back
#define INF 0x3f3f3f3f3f3f3f3f
#define endl '\n'
const ll N = 1000 + 5;
const ll M = 1e4 + 5;
ll n,V;
ll v[N],w[N];
ll f[N];
int main()
{
cin >> n >> V;
for(ll i = 1; i <= n; i ++ )
{
cin >> v[i] >> w[i];
}
for(ll i = 1; i <= n; i ++ )
{
for(ll j = v[i]; j <= V; j ++ )
{
f[j] = max(f[j],f[j - v[i]] + w[i]);
}
}
cout << f[V] << endl;
}
多重背包问题:
多重背包与完全背包的区别是:每种物品有$M_i$件可以取,而不是无穷件.
其基本转移方程和完全背包类似:
$$ F[i][j] = max{F[i - 1][j - kC_i] + kW_i | 0 \leq k \leq M_i} $$ 第一种算法:
如果我们将每种物品分解成$M_i$件物品,那么问题可以转化为$01$背包问题,但是这样复杂度太高了,所以不能直接这样分解。
考虑到,从$2^0,2^1,2^2,...,2^{k - 1}$中选出若干个数相加,可以表示出$0...2^k - 1$间的任意整数.进一步地,我们求出满足$2^0 + 2^1 + 2^2 + ... + 2^p \leq C_i$的最大整数$p$,令$R_i = C_i - 2^0 - 2^1 - ... - 2^p = C_i - 2^{p + 1} + 1$.
综上,$2^0,2^1,...,2^p,R_i$可以且仅可以表示$[0,C_i]$间的任意整数,所以我们可以将$C_i$件物品按其进行分解为$log_2(Ci)$件:$2^0 * C_i,2^1 * C_i,...,2^p * C_i,R_i * C_i$,然后按$01$背包进行求解.
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define debug(x) cout << #x << " = " << x << endl;
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define pll pair<ll,ll>
#define ld long double
#define ull unsigned long long
#define pb push_back
#define INF 0x3f3f3f3f3f3f3f3f
#define endl '\n'
const ll N = 2e4 + 5;
const ll M = 1e4 + 5;
const ll mod = 2147483648;
ll n,V;
ll f[N];
ll v[N],w[N];
int main()
{
cin >> n >> V;
ll cnt = 0;
for(ll i = 1; i <= n; i ++ )
{
ll tv,tw,ts;
cin >> tv >> tw >> ts;
ll k = 1;
while(k <= ts)
{
cnt ++ ;
v[cnt] = k * tv;
w[cnt] = k * tw;
ts -= k;
k *= 2;
}
if(ts > 0)
{
cnt ++ ;
v[cnt] = ts * tv;
w[cnt] = ts * tw;
}
}
for(ll i = 1; i <= cnt; i ++ )
{
for(ll j = V; j >= v[i]; j -- )
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[V] << endl;
}
第二种算法:
当我们只考虑“给出的物品能否填满指定容量的背包”而不关注最大价值,即只关注问题的可行性时,多重背包有一个$O(VN)$的算法.
令$F[i][j]$表示“使用前$i$种物品填满容量为$j$的背包后,最多还剩下多少件物品$i$可用”,$F[i][j] = -1$表示这种状态不可行.
显然,转移时有两种情况:
我们可以采用贪心策略,转移时尽量选第一种,第一种不可行时再选第二种.
代码:
memset(F, -1, sizeof F);
F[0][0] = 0;
for(int i = 1; i <= N; i ++ )
{
for(int j = 0; j <= V; j ++ )
{
if(F[i - 1][j] >= 0)
{
F[i][j] = M[i];
}
else
{
F[i][j] = -1;
}
}
for(int j = 0; j <= V - C[i]; j ++ )
{
if(F[i][j] > 0)
{
F[i][j + C[i]] = max(F[i][j + C[i]], F[i][j] - 1);
}
}
}
以下代码中由于空间限制,将状态数组拆成了两个,$used$数组表示在第$i$个阶段凑满面值$j$的硬币最少要用第$i$种硬币的数量,其余的逻辑和上面是类似的.
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define debug(x) cout << #x << " = " << x << endl;
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define pll pair<ll,ll>
#define ld long double
#define ull unsigned long long
#define pb push_back
#define INF 0x3f3f3f3f3f3f3f3f
#define endl '\n'
const ll N = 2e5 + 5;
const ll M = 1e4 + 5;
const ll mod = 2147483648;
int n,m;
int f[N],used[N];
int a[N],c[N];
void work()
{
for(int i = 1; i <= n; i ++ )
{
cin >> a[i];
}
for(int i = 1; i <= n;i ++ )
{
cin >> c[i];
}
for(int i = 0; i <= m; i ++ )
{
f[i] = 0;
}
f[0] = 1;
for(int i = 1; i <= n; i ++ )
{
for(int j = 0; j <= m; j ++ )
{
used[j] = 0;
}
for(int j = a[i]; j <= m; j ++ )
{
if(!f[j] && f[j - a[i]] && used[j - a[i]] < c[i])
{
f[j] = 1;
used[j] = used[j - a[i]] + 1;
}
}
}
int ans = 0;
for(int i = 1; i <= m; i ++ )
{
ans += f[i];
}
cout << ans << endl;
}
int main()
{
while(cin >> n >> m )
{
if(n == 0 && m == 0)break;
work();
}
}
第三种算法:
我们可以通过单调队列优化问题,可以得到一个$O(NV)$的解法.