题目大意: 有 n n n 个物品,背包大小为 m m m,每个物品有体积 v [ i ] v[i] v[i] 和代价 c [ i ] c[i] c[i] 两个属性,设 f [ i ] [ j ] f[i][j] f[i][j] 表示不选物品 i i i,用剩下的物品凑出体积 j + k m ( k ≥ 0 ) j+km~(k\geq 0) j+km (k≥0) 的最小代价,对于每个 i ∈ [ 1 , n ] i\in [1,n] i∈[1,n],求出 ∑ j = 0 m − 1 f [ i ] [ j ] \sum_{j=0}^{m-1} f[i][j] ∑j=0m−1f[i][j]。
题解
先考虑普通的dp,设
g
[
i
]
[
j
]
g[i][j]
g[i][j] 为前
i
i
i 个物品凑出体积
j
+
k
m
j+km
j+km 的最小代价,那么有:
g
[
i
]
[
j
]
=
min
(
g
[
i
]
[
j
]
,
g
[
i
−
1
]
[
j
−
v
[
i
]
]
+
c
[
i
]
)
g[i][j]=\min(g[i][j],g[i-1][j-v[i]]+c[i])
g[i][j]=min(g[i][j],g[i−1][j−v[i]]+c[i])
其中, j − v [ i ] j-v[i] j−v[i] 在模 m m m 意义下计算。
像这样从前往后dp一次得到 g g g,再从后往前dp一次得到 h h h,那么每次将 g [ i − 1 ] g[i-1] g[i−1] 和 h [ i + 1 ] h[i+1] h[i+1] 合并一下就能得到 f [ i ] f[i] f[i],这样做时间复杂度为 O ( m n 2 ) O(mn^2) O(mn2)。
发现我们并不关心每个物品的加入顺序,于是考虑线段树分治,设 f ( l , r ) [ j ] f(l,r)[j] f(l,r)[j] 表示不选区间 [ l , r ] [l,r] [l,r] 内的物品,凑出体积 j + k m j+km j+km 的最小代价,那么物品 i i i 会对每个管理区间不包含 i i i 的节点造成贡献,而这样的节点数是 O ( log n ) O(\log n) O(logn) 的,也就是说,每个 i i i 只会被加入 log n \log n logn 次,而加入一次的复杂度为 O ( m ) O(m) O(m),一共有 n n n 个物品,那么总时间复杂度为 O ( n m log n ) O(nm\log n) O(nmlogn)。
代码如下:
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define maxn 20010
#define maxm 2010
#define inf 999999999
int n,m,v[maxn],c[maxn];
int f[maxn<<1][maxm],t=0,g[maxm];
int add(int x){return x>=m?x-m:x;}
void trans(int from,int to,int l,int r)
{
for(int i=0;i<m;i++)g[i]=f[to][i]=f[from][i];
for(int i=l;i<=r;i++){
for(int j=0;j<m;j++)f[to][add(j+v[i])]=min(f[to][add(j+v[i])],g[j]+c[i]);
memcpy(g,f[to],m<<2);
}
}
void solve(int l,int r,int id)
{
if(l==r){
long long tot=0;
for(int i=0;i<m;i++)if(f[id][i]>400000000)tot--;
else tot+=f[id][i]; printf("%lld\n",tot);
return;
}
int mid=l+r>>1;
t++;trans(id,t,mid+1,r);solve(l,mid,t);
t++;trans(id,t,l,mid);solve(mid+1,r,t);
}
int main()
{
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d %d",&v[i],&c[i]);
for(int i=1;i<m;i++)f[0][i]=inf;
solve(1,n,0);
}