多重背包问题的两种O(M*N)解法
多重背包的题目很多,最著名的是poj1742楼教主的男人八题之一。
poj1742:coins
有几种面值的钱币和每种的数量,问能够组成m以内的多少种钱数
这个题大家都归为多重背包问题,不过跟实际意义上的背包还是有所差别的
因为如果把钱币看作背包中的物品,那么这个物品的价值和重量是相等的。
也就是没有“性价比"的。。
一种比较快速简单的做法是:
在判断能否放满某个体积时,如果能放满,尽量少用当前物品,贪心一下,对当前物品最优即可。
也可以用dp的思路想,就是dp[i][j]保存 j 体积最少用多少个物品 i
状态转移很明显:如果 dp[i-1][j] 合法 那么显然 dp[i][j]=0,否则在判断dp[i][j]能否由dp[i][j-w[i]]+1得到
复杂度O(N*M),而且常数也很小,可以通过楼教主的题目
两种代码如下:
#include <iostream> #include <stdio.h> #include<string.h> #include<algorithm> #include<string> #include<ctype.h> using namespace std; int num[100010]; bool dp[100010]; int a[110]; int c[1010]; int n,m; int main() { freopen("in.txt","r",stdin); //int T,cas=0; //scanf("%d",&T); //while(T--) while(scanf("%d%d",&n,&m),n+m) { //cas++; //scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",a+i); for(int i=1;i<=n;i++) scanf("%d",c+i); memset(dp,0,sizeof(dp)); dp[0]=1; for(int i=1;i<=n;i++) { memset(num,0,sizeof(num)); for(int j=a[i];j<=m;j++) { if(num[j-a[i]]>=c[i]) continue; if(dp[j]||(!dp[j-a[i]])) continue; num[j]=num[j-a[i]]+1; dp[j]=1; } } int ans=0; for(int i=1;i<=m;i++) { ans+=dp[i]; } //printf("Case %d: %d\n",cas,ans); printf("%d\n",ans); } return 0; }
#include <iostream> #include <stdio.h> #include<string.h> #include<algorithm> #include<string> #include<ctype.h> using namespace std; int dp[100010]; int a[110]; int c[1010]; int n,m; int main() { //freopen("in.txt","r",stdin); while(scanf("%d%d",&n,&m),n+m) { for(int i=1;i<=n;i++) scanf("%d",a+i); for(int i=1;i<=n;i++) scanf("%d",c+i); memset(dp,-1,sizeof(dp)); dp[0]=0; int ans=0; for(int i=1;i<=n;i++) { for(int j=0;j<=m;j++) { if(dp[j]!=-1) { dp[j]=0; } else if(j>=a[i]&&dp[j-a[i]]!=-1&&dp[j-a[i]]<c[i]) { dp[j]=dp[j-a[i]]+1; } } } for(int i=1;i<=m;i++) { ans+=(dp[i]!=-1); } printf("%d\n",ans); } return 0; }
但是物品如果重量和价值不同,那么以上方法就不行了。。
以上方法的策略是对某一个价值,尽量少选当前物品,但是我们又知道背包问题中,我们要尽量使达到某一个价值时所用的重量最小
这两个策略显然是会出现矛盾的。
下面我们来看一下这种情况的解决方法:
先按照传统背包问题的写法,写出朴素dp方程:
dp[i][j]=max(dp[i-1][j-k*w[i]]+k*v[i]) 其中0<=k<=c[i]
显然这个方程的复杂度是 n*v*c 的。数据稍大就会超时,那么怎么优化呢
求最值的dp优化,首先想到是单调队列优化,但是这个方程貌似更单调队列无关
尝试把它变形
我们令 r=j%w[i] , p=j/w[i]
则以上方程变为
dp[i][ p*w[i]+r ]=dp[i-1][ (p-k)*w[i] + r]+k*v[i]。
为了观察方便,再令 k=p-k,并移项,得到
dp[i][ p*w[i]+ r]-p*v[i] =dp[i-1][k*w[i]+r]-k*v[i] 其中对于特定的p,k属于 [p-c[i] , p]。
这个式子就跟简单的单调队列优化dp的式子很像了。。
把k当作单调队列下标,对于[ 0,w[i]-1 ]的每一个r,枚举k,建立一个单调队列,就可以完成状态转移了
由于单调队列复杂度是O(n)
总复杂度即为 O(n*r*p)=O(n*m)
不过这个通用的做法由于常数过大。。我的程序没过楼教主的那道题,感人的TLE了一晚上
也不知道到底慢了多少
后来我百度了很多这道题单调队列的题解发现是用单调队列的写法去填bool数组。我感觉本质上和第一种方法没什么差别。所以就不纠结了= =
贴上第二种做法的核心代码:
for(i=1; i<=n; ++i) { int p=m/w[i]; for(j=w[i]-1; j+1; --j) { s=e=0; for(int k=p; k+1; --k) { if(k*w[i]+j>m) continue node tmp; tmp.num=k; int tt=tmp.val=dp[k*w[i]+j]-k*v[i]; int x; if(tt>=q[s].val) { s=e=0; } else { for(x=e-1; x>=s&&tt>=q[x].val; --x); e=x+1; } q[e++]=tmp; //入队 } //由于压缩到一维背包需要从大到小循环,故先将能转移到 k=p的所有状态入队 for(int k=p; k+1; --k) { if(k*w[i]+j>m) continue; node tmp; int x; if(k>=c[i]) { tmp.num=k-c[i]; int tt=tmp.val=dp[(k-c[i])*w[i]+j]-(k-c[i])*v[i]; if(tt>=q[s].val) { s=e=0; } else { for(x=e-1; x-s+1&&tt>=q[x].val; --x); e=x+1; } q[e++]=tmp; } //入队 for(x=s; e-x&&q[x].num>k; ++x); s=x; //出队 dp[k*w[i]+j]=q[s].val+k*v[i]; } } }