【问题描述】
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。” 如果你是辰辰,你能完成这个任务吗?
【输入文件】
输入文件medic.in的第一行包含两个正整数N,M。M表示总共能够用来采药的时间,N代表山洞里的草药的数目。接下来的N行每行包括两个的整数,分别表示采摘某株草药的时间Ti和这株草药的价值Vi。
【输出文件】
输出文件为medic.out,仅包含一个整数表示规定时间内可以采到的草药的最大总价值。
【输入样例】
3 9
10 10
8 1
1 2
【输出样例】
3
【数据规模和约定】
50%的数据中 N,M ≤ 1000;
100%的数据中 N,M ≤ 100000,Ti,Vi ≤10。
若有n种物品,背包容量为m,物品体积、价值、最大使用次数为v,w,c,则朴素的动规方程为:f[i]=max{f[i-v*k]+w*k} (1<=k<=c)。我们把所有可能达到的体积按照除以当前物品体积v的余数划分为0~v-1,则当余数为k(k∈[0,v-1])时又可以划分为k,k+v,k+2*v…k+j*v…(1<=j<=(m-k)div v)这几种具体的体积,由于对于余数为k时的转移只会发生在以上列举出的几个体积上,所以可以建立关于以上几个体积的单调队列,以便于快速地找到最优决策。但是这里要注意一点,由于这几个决策的体积和价值都不相同,直接没有可比性,所以我们把这些决策的体积统一到为k时比较,统一方法只需要利用体积为k+j*v这一特点,把需要插入队中的f[k+j*v]的价值减去j*w,就是当体积为k时的一个可以用于比较的“参考”价值。可以很容易想到:由于转移时,使用当前物品贡献的那一部分是二者之差,所以这与减掉j*w之前是等效的。
这样,每次求f[k+j*v]时,只需要在队列中找到一个最优的决策f[k+j’*v],使得j-j’<=c即可,剩下的工作就只有维护单调队列了。
核心代码和注释:
procedure insert(x,y:longint);//插入到一个价值单调递减,使用次数单调递增的队列中
begin
while (l<=r)and(b[r]<=y) do dec(r);
inc(r);a[r]:=x;b[r]:=y;
end;
begin
readln(n,m); //n为物品个数、m为背包容量
for i:=1 to n do
begin
read(v,w,c); //读入当前物品:v为物品体积、w为物品价值、c为物品可用次数
if m div v<c then c:=m div v; //最多可使用次数
for k:=0 to v-1 do //把所有的体积按除以v的余数划分为0~v-1
begin
l:=1;r:=0; //清空队列
for j:=0 to (m-k) div v do //余数为k的又分为k,v+k,2*v+k…j*v+k…
begin
insert(j,f[j*v+k]-j*w); //等效于把体积统一到k,价值减去j*w,这样比较优劣才有意义
while a[l]<j-c do inc(l); //删除次数超过c的
f[j*v+k]:=b[l]+j*w; //用队列头的值更新f[j*v+k]
end;
end;
end;
writeln(f[m]);
end.
/*
多重背包问题:第一种为二进制优化 时间复杂度 O(NMlog2S)
第二种使用单调队列 时间复杂度 O(NM)
*/
//方法一
#include <cstdio>
#define max(a,b) (a)>(b)?(a):(b)
int dp[100001];
int w[11][11];
int main()
{
freopen("medic.in","r",stdin);
freopen("medic.out","w",stdout);
int n,m;
scanf("%d%d",&n,&m);
int a,b;
for(int i = 0; i < n; ++i)
{
scanf("%d%d",&a,&b);
w[a][b]++;
}
for(int i = 1; i < 11; ++i)//多重背包
{
for(int j = 1; j < 11; ++j)
{
if(!w[i][j]) continue;
if(w[i][j]*i < m)//01背包
{
int k = 1;
int count = w[i][j];
while(k < count)//2进制思想,每次处理一堆
{
for(int v = m; v >= k*i; --v)
dp[v] = max(dp[v],dp[v-k*i]+k*j);
count -= k;
k *= 2;
}
for(int v = m; v >= count*i; --v)
dp[v] = max(dp[v],dp[v-count*i]+count*j);
}
else//完全背包
{
for(int v = 1; v <= m; ++v)
if(v-i>=0) dp[v] = max(dp[v],dp[v-i]+j);
}
}
}
int ans = 0;
for(int i = 0; i <= m; ++i)
if( ans < dp[i]) ans = dp[i];
printf("%d\n",ans);
return 0;
}
/* 方法2
#include <cstdio>
#define max(a,b) (a)>(b)?(a):(b)
int f[100001];
int ww[11][11];
int l,r;
int a[100001];
int b[100001];
inline void insert(int x,int y)
{
while(l<=r && b[r]<=y) r--;
++r;
a[r] = x;
b[r] = y;
}
int main()
{
freopen("medic.in","r",stdin);
freopen("medic.out","w",stdout);
int N,m;
scanf("%d%d",&N,&m);//m为背包容量
int cc,dd;
int sum = 0;
for(int i = 0; i < N; ++i)
{
scanf("%d%d",&cc,&dd);
ww[cc][dd]++;
}
for(int i = 1; i < 11; ++i)
for(int jj = 1; jj < 11; ++jj)
{
if(!ww[i][jj]) continue;//处理每件物品
int c = i;//物品的耗费
int w = jj; //物品价值
int n = ww[i][jj];//物品个数
if(m/c < n) n = m/c; //最多可使用次数
for(int k = 0; k <= c-1; ++k)//把所有的体积按除以v的余数划分为0~v-1
{
l = 1;r = 0; //清空队列
for(int j = 0; j <= (m-k)/c; ++j)//余数为k的又分为k,v+k,2*v+k…j*v+k…
{
//insert(j,f[j*c+k]-j*w);//等效于把体积统一到k,价值减去j*w,这样比较优劣才有意义
int x = j;
int y = f[j*c+k]-j*w;
while(l<=r && b[r]<=y) r--;
++r;
a[r] = x;
b[r] = y;
while(a[l]<(j-n)) ++l; //删除次数超过n的
f[j*c+k] = b[l]+j*w; //用队列头的值更新f[j*c+k]
}
}
}
printf("%d\n",f[m]);
return 0;
}
*/