单调队列优化多重背包
单调队列优化多重背包
 背包问题:
01,完全比较简单,套个板子就好,需要注意的是:
const int maxn = 2e4+11;//由于maxn是由max(n,m)决定的,当背包的体积过大时,我们就不能再使用体积来定义状态了
    来一道例题:https://ac.nowcoder.com/acm/problem/15948  【  题目的V 巨大,我们不能再定义dp[ i ] 为体积为i的状态了,观察到n很小,限制一下v的范围,然后记忆化dfs即可】
多重背包优化:
① 二进制拆分,容易运用
② 单调队列,十分快,时间是o(nv) 少了logk,但是很难理解(
int _ML() { int pre = 0,now = 1,ans = 0,k,V,VAL; for(int i = 1;i <= n;i++) { k = 1; while(c[i]>0) { if(c[i]<k)k = c[i]; V = v[i]*k,VAL = val[i]*k; for(int j = 0;j <= m;ans = max(dp[now][j],ans),j++) if(j<V)dp[now][j] = dp[pre][j];//继承状态不能少 else dp[now][j] = max(dp[pre][j-V]+VAL,dp[pre][j]); swap(pre,now);//每计算完一次转换状态 c[i] -= k;//减去系数 k *= 2; } } return ans; }
#include <iostream> #include <algorithm> #include <cstdio> #include <cstring> #include <vector> #include <queue> #include <cmath> #include <set> #include <map> #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define ll long long #define ls (ro<<1) #define rs (ro<<1|1) #define me(a,b) memset(a,(b),sizeof a) using namespace std; const int maxn = 1e5+11; //maxn 取决于 max(n,V); int t,n,m,V,val,v,c,dp[maxn],k,ans; struct PACK{int id,val;}; PACK q[maxn]; int head,tail; int main() { while(~scanf("%d%d",&n,&V)) { me(dp,0);ans = 0; for(int i = 1;i <= n;i++) { scanf("%d%d%d",&val,&v,&c); //int d; 余数 //int k; 倍数 //int c; min(c[i],m/v[i]) 能使用的个数 c = min(c,V/v); for(int d = 0;d < v;d++) { k = (V-d)/v; head = 0,tail = 0;//clear队列 for(int j = 0;j <= k;j++) { //删除队列尾部小于这个数的数,然后再插进去维护单调性(基操) while(head<tail&&dp[d+j*v]-j*val >= q[tail-1].val)//这里的 >= 控制严格单调性 tail--; q[tail].id = j;//记录index q[tail++].val = dp[d+j*v] - j*val; //如果head的数超出了搜索的范围,pop it out while(head<tail&&q[head].id < j - c) head++; //每一个dp[d+j*v]来自于前面的最大值/0 + 队列最大值 + j * val dp[d+j*v] = max(dp[d+j*v],q[head].val+j*val); } } } for(int i = 1;i <= V;i++)ans = max(dp[i],ans); printf("%d\n",ans); } }
关于单调队列,我目前只发现运用于最大最小值的状态转移...
我的理解:(图片转载)

为甚么要取d余数,并且遍历余数呢?
讲解中说得十分清楚,因为只有d相同的数,才会有联系(后面的继承前面的),
所以我们按 d 分组,并且一个个遍历。
为什么要剪掉 j * val 之后再放入队列呢?
首先看:pre状态由{ 1,1 ,1} { 1,2,1 } { 1,3,1}{ 1,4,1} { 价值,体积,个数}
这几件物品转移而来,其实看一下也可以看出来
那么我们现在加入{ 2,3,2}的物品
| pre: | 1 | 2 | 3 | 4 | 4 | 4 | 4 | 
| now: | 1 | 2 | 3 | 4 | 5 | 6 | 6 | 
①我们d = 0,j = 0 -> k 时,是蓝色部分,同时最多可用的物品数量是 c = min( c , V/v) = 2,
② 考虑到:放入队列的数是 1 , (4-2),(4-2*2)即{ 1 , 2, 0 }//注意是从pre转到now
③最后一个点显然从第4个点加上一个物品就好,但是我们计算的时候是 :
dp[ d + j*v] = max(dp[d + j * v],q[head].val + j * val); 也就是最大值2 +2*val
④可以看出:减去 j * val 是因为容斥定理 是不是恍然大悟呢
懂了的话,来一道题目练练手:取最小值的多重背包 原题-->【POJ2005】
我们维护的单调队列是区间最小值,需要注意的是,每一个DP状态设置为INF,dp[0] = 0;
注意每一个变量代表的含义,不要混乱了
#include<cstdio> #include<iostream> #include<algorithm> #include<cstring> #define me(a,b) memset(a,(b),sizeof a) using namespace std; const int maxn = 5e5+11,inf = 0x3f3f3f3f; int dp[maxn],val[maxn],c[maxn],n,m,V; struct PACK{int id,val;}; PACK q[maxn]; int head,tail; int main() { while(~scanf("%d",&n)) { me(val,0),me(c,0),me(dp,inf); for(int i = 1;i <= n;i++)scanf("%d",&val[i]); for(int i = 1;i <= n;i++)scanf("%d",&c[i]); scanf("%d",&V); dp[0]=0; for(int i = 1;i <= n;i++) { int d = val[i],tot = min(c[i],V/val[i]); for(int j = 0;j < d;j++)//余数 { head = tail = 0; q[head].val = -inf;//设置为最小值,防止出现负值 int k = (V-j)/val[i]; //k倍数 for(int t = 0;t <= k;t++) { while(head<tail&&dp[j+t*val[i]]-t<=q[tail-1].val)tail--; q[tail].id = t; q[tail++].val = dp[j+t*val[i]]-t; while(head<tail&&q[head].id < t - tot)head++; dp[j+t*val[i]] = min(dp[j+t*val[i]],q[head].val+t); //调试代码 //for(int i = 0;i <= V;i++) //cout<<dp[i]<<" "; //cout<<endl; } } } if(dp[V]!=inf)printf("%d\n",dp[V]); else puts("-1"); } return 0; }

                
            
        
浙公网安备 33010602011771号