• 背包问题 DP


    各种各样的基础背包

    0-1 背包

    非常朴素,复杂度 (O(nV))

    void z_o_pack(int c,int v)
    {
    	 for(int i=V;i>=c;i--)
    	 	 dp[i]=max(dp[i],dp[i-c]+v);
    }
    

    完全背包

    复杂度 (O(nV))

    void comp_pack(int c,int v)
    {
    	 for(int i=c;i<=V;i++)
    	 	 dp[i]=max(dp[i],dp[i-c]+v);
    }
    

    多重背包

    单调队列优化

    (dp[i][j]) 表示前 (i) 个物品放入容量为 (j) 的背包的最大收益 。

    [dp[i][j]=max_{k=0}^{kle k[i]}{{dp[i-1][j-k imes c[i]]+k imes w[i]}} ]

    考虑 (dp) 的转移 。

    [0le p < c[i],0le j le leftlfloor dfrac{V-p}{c[i]} ight floor,0le k le k[i] ]

    [dp[i][p+j imes c]=max{{dp[i-1][p+(j-k) imes c]+k imes w}} ]

    [dp[i][p+j imes c]=max{{dp[i-1][p+(j-k) imes c]-(j-k) imes w+j imes w}} ]

    [dp[i][p+j imes c]=max{{dp[i-1][p+(j-k) imes c]-(j-k) imes w}}+j imes w ]

    这样就可以进行单调队列优化了 。

    复杂度可以达到 (nV)

    int ql,qr;
    struct QUE
    {
    	 int num,val;
    }que[Maxv];
    void many_pack(int c,int w,int m)
    {
    	 if(!c) { add+=m*w; return; }
    	 m=min(m,V/c);
    	 for(int pos=0,s;pos<c;pos++)
    	 {
    	 	 ql=1,qr=0,s=(V-pos)/c;
    	 	 for(int j=0;j<=s;j++)
    	 	 {
    	 	 	 while(ql<=qr && que[qr].val<=(dp[pos+j*c]-j*w)) qr--;
    	 	 	 que[++qr]=(QUE){j,dp[pos+j*c]-j*w};
    	 	 	 while(ql<=qr && (j-que[ql].num)>m) ql++;
    	 	 	 dp[pos+j*c]=max(dp[pos+j*c],que[ql].val+j*w);
    		 }
    	 }
    }
    

    二进制分组

    比起上面的方法理解与实现起来更为简单。

    我们吧多重背包的“多个物品”限制转化为有 (leftlceillog n ight ceil) 个物品的 (0/1) 背包。

    注意我们在二进制拆分时要拆为二进制下末尾连续的若干个 (1) 和一个余数。

    inline void many_pack(int C,int W,int D)
    {
    	 for(int i=1;i<=D;D-=i,i<<=1) for(int j=m;j>=C*i;j--)
    	 	 if(f[j-C*i]!=-inf) f[j]=max(f[j],f[j-C*i]+W*i);
     	 if(D) for(int j=m;j>=C*D;j--) if(f[j-C*D]!=-inf)
     	 	 f[j]=max(f[j],f[j-C*D]+W*D);
    }
    

    多维限制背包

    只是多加几位,没有太大区别。

    混合三种背包的问题

    for(int i=1;i<=n;i++)
    {
    	 if(/*是0-1背包*/) z_o_pack(c[i],w[i]);
    	 if(/*是完全背包*/) comp_pack(c[i],w[i]);
    	 if(/*是多重背包*/) many_pack(c[i],w[i],m[i]);
    }
    

    分组背包

    同一组内只能选一个物品。

    for(int i=1;i<=n;i++)
    	 for(int j=V;j>=0;j--)
    	 	 for(int k=1;k<=cnt[i];k++) if(j>=c[i][k])
    	 	 	 dp[j]=max(dp[j],dp[j-c[i][k]]+w[i][k]);
    

    这样保证每一组内只会选择一个。

    很多背包问题都能都转化为分组背包。

    树形依赖背包

    复杂度:(O(n^2))

    虽然有三层循环,但是内层运算总量与整棵子树内点对个数一致。

    void dfs(int x,int fa)
    {
    	 dp[x][1]=w[i],siz[x]=1;
    	 for(int i=hea[x];i;i=nex[i])
    	 {
    	 	 if(ver[i]==fa) continue;
    	 	 dfs(ver[i],x);
    	 	 siz[x]+=siz[ver[i]];
    	 	 for(int j=min(V+1,siz[x]);j>=2;j--) // 注意要从大到小枚举,防止后效性
    	 	 	 for(int k=1;j-k>=1;k++)
    	 	 	 	 dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[ver[i]][k]);
    	 }
    }
    
    for(int i=1;i<=n;i++) if(!ind[i]) add(0,i); // 给出的图是一个森林
    dfs(0,-1);
    printf("%d
    ",dp[0][V+1]);
    

    注意:想这一类题目时只用考虑如何设置 (dp_{i,j}) ?初始状态是什么?如何转移?

    但是有时候背包容量与子树大小不同阶怎么办?我们需要考虑另一个做法。

    如果选择一个节点,它到根节点的路径上的每一个节点都至少选择了一个物品(默认多重背包 (-) 二进制拆分解决)。

    (f_{i}) 是考虑已经遍历过的节点,花了 (i) 元的最大价值。在遍历到一个节点时,先将已有的 dp 值存储一下,然后在该节点强行选一个物品,对于该节点剩下的物品跑多重背包,然后递归该节点的子节点即可。

    当然,也有可能该节点和其子树都不选,所以在回溯时,将每个 dp 值都与进入该节点是存储的 dp 值取个 (max) 即可。

    例题:P6326 Shopping

    inline void many_pack(int C,int W,int D)
    {
    	 for(int i=1;i<=D;D-=i,i<<=1) for(int j=m;j>=C*i;j--)
    	 	 if(f[j-C*i]!=-inf) f[j]=max(f[j],f[j-C*i]+W*i);
     	 if(D) for(int j=m;j>=C*D;j--) if(f[j-C*D]!=-inf)
     	 	 f[j]=max(f[j],f[j-C*D]+W*D);
    }
    void dfs(int x,int fa,int dep)
    {
    	 if((dep+=c[x])>m) return;
    	 for(int i=0;i<=m;i++) dp[x][i]=f[i];
    	 for(int i=m;i>=dep;i--) f[i]=f[i-c[x]]+w[x];
    	 for(int i=0;i<dep;i++) f[i]=-inf;
    	 many_pack(c[x],w[x],d[x]-1);
    	 for(int i=hea[x];i;i=nex[i]) if(!used[ver[i]] && ver[i]!=fa)
    	 	 dfs(ver[i],x,dep);
    	 for(int i=0;i<=m;i++) dp[x][i]=f[i]=max(dp[x][i],f[i]);
    }
    

    泛化物品

    即一个物品的价值与消耗都会随着加入位置、时间的改变而改变。

    注意:既然物品的价值随着加入位置而改变,应该现将物品排序再加入。


    不同问法的背包问题

    输出方案

    记录从哪一项转移过来即可

    求字典序最小的方案

    咕咕咕

    求方案总数

    (max) 改为 (sum) ,其余不变(此时求出的方案数,包括物品总价不是最大的情况)

    求最优方案总数

    咕咕咕

    求次优解、第K优解

    咕咕咕


    常见例题:

    多种背包混合

    P1941 飞扬的小鸟

    $ exttt{solution}$

    算法:(01) 背包 (+) 完全背包 。

    状态:设 (dp[i][j]) 表示横坐标为 (i) 高度为 (j) 的最少点击次数 。

    1. 上升 :完全背包 转移方式

    2. 下降 :(01) 背包 转移方式

    3. 超过 (m) 变为 (m) : 特判

    代码:

    for(int i=1;i<=n;i++) Low[i]=1,High[i]=m;
    for(int i=1;i<=k;i++) p=rd(),e[p]=1,Low[p]=rd()+1,High[p]=rd()-1;
    memset(g,inf,sizeof(g));
    for(int i=1;i<=m;i++) g[i]=0;
    for(int i=1;i<=n;i++)
    {
    	 memset(f,inf,sizeof(f));
    	 for(int j=x[i];j<=x[i]+m;j++) f[j]=min(f[j-x[i]]+1,g[j-x[i]]+1); // 完全背包
    	 for(int j=m+1;j<=m+x[i];j++) f[m]=min(f[j],f[m]); // 特判超过 m
    	 for(int j=1;j<=m-y[i];j++) f[j]=min(f[j],g[j+y[i]]); // 01 背包
    	 for(int j=1;j<Low[i];j++) f[j]=inf;
    	 for(int j=High[i]+1;j<=m;j++) f[j]=inf; // 不可行方案
    	 if(e[i]) for(int j=Low[i];j<=High[i];j++) if(f[j]<inf) { cnt++; break; } // 统计通过的管道数
    	 memcpy(g,f,sizeof(g));
    }
    for(int i=1;i<=m;i++) ans=min(ans,g[i]);
    if(ans!=inf) printf("1
    %d
    ",ans);
    else printf("0
    %d
    ",cnt);
    

    合并两个限制条件相同的背包

    P4095 [HEOI2013]Eden 的新背包问题

    题意:给定 (n) 种物品,每种有 (m_i) 件,每一件价值为 (v_i) ,重量为 (c_i) 。由于某些原因,在第 (i) 个询问中,第 (i) 种物品不能选择,请对于每个询问求出当前条件下的最大收益。

    其中, (nle 1000,c_ile 100)

    考虑从前往后、从后往前进行背包。

    在第 (i) 个询问中合并 (pre[i-1])(suf[i+1]) 的背包。

    for(int j=0;j<=v;j++) ans=max(ans,dp[0][num-1][j]+dp[1][num+1][v-j]);
    

    当组内重量、价值连续时分组背包的前缀和优化

    CF1559E Mocha and Stars

    题意:

    求有多少长 (n) 的序列 (a) 满足:

    • (l_ile a_ile r_i)
    • (sum_{i=1}^{n}a_ile m)
    • (gcd(a_1,dots,a_n)=1)

    答案对 (998244353) 取模。

    (f(a_1,a_2,a_3,dots,a_n)) 是否是一个满足前两个条件的序列。

    [egin{aligned}ans & = sumlimits_{a_1=l_1}^{r_1}sumlimits_{a_2=l_2}^{r_2}...sumlimits_{a_n=l_n}^{r_n}f(a_1,a_2,...,a_n)[gcd(a_1,a_2,...,a_n)=1] \ & = sumlimits_{a_1=l_1}^{r_1}sumlimits_{a_2=l_2}^{r_2}...sumlimits_{a_n=l_n}^{r_n}f(a_1,a_2,...,a_n)sumlimits_{dmid gcd(a_1,a_2,...,a_n)}mu(d)\ &= sumlimits_{a_1=l_1}^{r_1}sumlimits_{a_2=l_2}^{r_2}...sumlimits_{a_n=l_n}^{r_n}f(a_1,a_2,...,a_n)sumlimits_{dmid a_1 & dmid a_2 & ... & dmid a_n}mu(d) \&= sumlimits_{d=1}^mmu(d) sumlimits_{a_1=leftlceil frac{l_1}{d} ight ceil}^{leftlfloor frac{r_1}{d} ight floor}sumlimits_{a_2leftlceil frac{l_2}{d} ight ceil}^{leftlfloor frac{r_2}{d} ight floor}...sumlimits_{a_n=leftlceil frac{l_n}{d} ight ceil}^{leftlfloor frac{r_n}{d} ight floor}f(a_1,a_2,...,a_n)end{aligned} ]

    之后相当于一个分组背包,有 (n) 件物品,每一件都在 ([l_i,r_i]) 之间,去除不超过 (m) 的重量,求方案数。

    $ exttt{code}$
    #include<bits/stdc++.h>
    using namespace std;
    #define infll 0x7f7f7f7f7f7f7f7fll
    #define inf 0x3f3f3f3f
    #define Maxn 55
    #define Maxm 100005
    #define mod 998244353
    #define int long long
    typedef long long ll;
    inline int rd()
    {
    	 int x=0;
         char ch,t=0;
         while(!isdigit(ch = getchar())) t|=ch=='-';
         while(isdigit(ch)) x=x*10+(ch^48),ch=getchar();
         return x=t?-x:x;
    }
    ll maxll(ll x,ll y){ return x>y?x:y; }
    ll minll(ll x,ll y){ return x<y?x:y; }
    ll absll(ll x){ return x>0ll?x:-x; }
    ll gcd(ll x,ll y){ return (y==0)?x:gcd(y,x%y); }
    int n,m,tot;
    int L[Maxn],R[Maxn],l[Maxn],r[Maxn];
    int dp[Maxm],tmp[Maxm],pri[Maxm],mu[Maxm];
    ll ans;
    bool vis[Maxm];
    signed main()
    {
    	 //ios::sync_with_stdio(false); cin.tie(0);
         //freopen(".in","r",stdin);
         //freopen(".out","w",stdout);
    	 n=rd(),m=rd();
    	 for(int i=1;i<=n;i++) L[i]=rd(),R[i]=rd();
    	 vis[1]=mu[1]=true;
    	 for(int i=2;i<Maxm;i++)
    	 {
    	 	 if(!vis[i]) mu[i]=-1,pri[++tot]=i;
    	 	 for(int j=1;j<=tot && i*pri[j]<Maxm;j++)
    	 	 {
    	 	 	 vis[i*pri[j]]=true;
    	 	 	 if(i%pri[j]==0) break;
    	 	 	 mu[i*pri[j]]=-mu[i];
    		 }
    	 }
    	 for(int d=1,MAX;d<=m;d++) if(mu[d])
    	 {
    	 	 MAX=m/d;
    	 	 for(int i=1;i<=n;i++) l[i]=(L[i]+d-1)/d,r[i]=R[i]/d;
    	 	 for(int i=0;i<=MAX;i++) dp[i]=1;
    	 	 for(int i=1;i<=n;i++)
    	 	 {
    	 	 	 for(int j=0;j<=MAX;j++) tmp[j]=0;
    	 	 	 for(int j=l[i];j<=MAX;j++)
    	 	 	 {
    	 	 	 	 tmp[j]=dp[j-l[i]];
    	 	 	 	 if(j>r[i]) tmp[j]=(tmp[j]-dp[j-r[i]-1]+mod)%mod;
    			 }
    			 dp[0]=0;
    			 for(int j=1;j<=MAX;j++) dp[j]=(dp[j-1]+tmp[j])%mod;
    		 }
    		 ans=(ans+1ll*dp[MAX]*mu[d]%mod+mod)%mod;
    	 }
    	 printf("%d
    ",ans);
         //fclose(stdin);
         //fclose(stdout);
         return 0;
    }
    

    一分为二

    P1651 塔

    题意:有 (n) 个数,从中选出两个集合(可以有剩余),使两个数集中的数字之和相等。

    !!!这不是一道背包问题,因为可以有剩余,所以更具判断 (dp[sum] e -1)(dp[2 imes sum] e -1) 是错的。

    选出的 (2 imes sum)(sum) 不一定是包含关系。

    应该用普通 ( ext{DP}) 解决。

    (当然,当 (n) 非常小的时候,可以直接使用 meet in the middle 解决问题) 如:P3067 [USACO12OPEN]Balanced Cow Subsets G

    两种限制/收益的问题

    将数组下标的一位设为 “任务一” 的值,而将数组内容设为 “任务二” 的最小 (/) 最大值。

    输出答案的时候将下标与值取和、最小、最大……

    P2224 [HNOI2001]产品加工

    ( ightarrow) P2224 solution

    (dp[i][j]) 表示加到 (i) 为止,机器 (1) 使用了 (j) 的时间,而 (dp[i][j]) 值表示机器 (2) 消耗的时间。

    跑背包。

    n=rd();
    memset(dp,inf,sizeof(dp)),dp[0][0]=0;
    for(int i=1,x,y,z,opt,pre;i<=n;i++)
    {
    	 x=rd(),y=rd(),z=rd(),opt=i&1,pre=opt^1;
    	 MAX+=max(x,max(y,z));
    	 for(register int j=0;j<=MAX;j++) dp[opt][j]=inf; // 防止上一次的值影响
    	 for(register int j=0;j<=MAX;j++)
    	 {
    	 	 if(y) dp[opt][j]=min(dp[opt][j],dp[pre][j]+y);
    	 	 if(j>=x && x) dp[opt][j]=min(dp[opt][j],dp[pre][j-x]);
    	 	 if(j>=z && z) dp[opt][j]=min(dp[opt][j],dp[pre][j-z]+z);
    	 }
    }
    for(int i=0;i<=MAX;i++) ans=min(ans,max(dp[n&1][i],i));
    printf("%d
    ",ans);
    

    P2340 [USACO03FALL]Cow Exhibition G

    题意:有 (n) 头奶牛,每一头奶牛有一个情商值和智商值,现在要从中选出若干头奶牛。在奶牛的情商和与智商和都不小于 (0) 的情况下,求出情商与智商之和的最大值。

    (需要滚动数组)设 (dp[j]) 表示在前 (i) 头奶牛中选出情商为 (j) 时的智商最大值为 (dp[j])

    暴力转移。

    最后在 (i>0) 的情况下,计算 (i+dp[i]) 的最大值。

    比较复杂的题目

    P3891 [GDOI2014]采集资源

    先进行一次 ( ext{DP}) 求出 (dp1[i]) 数组,表示花费 (i) 能够获得的最大劳动力。

    之后进行第二次 ( ext{DP}) 求出 (dp2[i][j]) ,表示在第 (i) 时间花费 (j) 能获得最大劳动力(单位时间内的收获最多)。转移方程:

    [dp[i+1][j-k+dp1[k]+dp2[i][j]]=max(dp[i+1][j-k+dp1[k]+dp2[i][j]],dp[k]+dp[i][j]) ]

    在求出 (dp2) 的同时比较此时是否能使收益 ((j-k+dp[k]+dp[i][j])) 大于 (t) ,及时输出。

    注意:初始化使需将数组赋值为不可能取到的值,以防错误转移。

    P2481 [SDOI2010]代码拍卖会

    对于任何一个猪猪举牌的方案,都可以看做 (9) 个以内的若干 “(1) 的后缀” 相加而成!

    我们可以把一个数分割成若干个 (000000dots 11111) 的和。

    不妨记 (cnt(i))(pmod p) 意义下 ([ ext{余数是 i 的“1 的后缀”}]) 的数量。

    之后完成一个背包就好了。

    (dp(i,j,k)) 表示当前考虑到余数为 (i) 的 “(1) 的后缀” ,此前已经放上了 (j) 个 “(1) 的后缀” ,此时构成的数字的 (pmod p) 的余数是 (k) 的方案数。

    初始化:全部填 (1)(n) 位数对 (p) 取模后的状态。(注意这里的处理!!!就是这里卡了 (4~Hours)

    设:

    • (put) 表示放入 (put) 个余数为 (i) 的 “(1) 的后缀” 。
    • (Left) 表示放入前已经有了 (Left) 的余数。
    • (d) 表示在放入前已经有 (s) 个数了。
    • 二项式表示从所有余数为 (i) 的 “(1) 的后缀” 取出 (put) 个的方案数(证明见数论基础)。

    转移方程:

    [dp(i,s+put,(Left+i imes put)\%p)+=dbinom{cnt(i)+put-1}{put} imes sumlimits dp(i-1,s,Left) ]

    $ exttt{code}$
    #define Maxp 505
    #define mod 999911659
    ll n,p,len,add;
    ll cnt[Maxp],Last_pos[Maxp],inv[11];
    ll f[11][Maxp],g[11][Maxp];
    ll C(ll x,ll y)
    {
    	 ll ret=1;
    	 for(ll i=x;i>=x-y+1;i--) ret=ret*i%mod;
    	 for(ll i=y;i>=2;i--) ret=ret*inv[i]%mod;
    	 return ret;
    }
    void pre_cnt()
    {
    	 ll st,addn;
    	 for(ll i=1,tmp=0;i<=p+p;i++)
    	 {
    	 	 tmp=(tmp*10+1)%p;
    	 	 if(Last_pos[tmp]) { st=Last_pos[tmp]-1,len=i-Last_pos[tmp]; break; }
    	 	 addn=tmp,Last_pos[tmp]=i; // 这里!!! 
    	 }
    	 if(n<st) for(int i=1,tmp=0;i<=n;i++) tmp=(tmp*10+1)%p,cnt[tmp]++,addn=tmp;
    	 else
    	 {
    		 ll Times=(n-st)/len,ed=(n-st)%len+st;
    		 if((n-st)%len==0) ed=0; // 这里!!! 
    		 for(int i=1,tmp=0;i<=st+len;i++)
    		 {
    		 	 tmp=(tmp*10+1)%p;
    		 	 if(i==ed) addn=tmp; // 这里!!! 
    		 	 if(i<=st) cnt[tmp]++;
    		 	 else
    			 {
    			 	 cnt[tmp]=(cnt[tmp]+Times)%mod;
    			 	 if(i<=ed) cnt[tmp]=(cnt[tmp]+1)%mod;
    			 }
    		 }
    	 }
    	 f[0][addn]=1;
    }
    ll Dp()
    {
    	 for(int i=0;i<p;i++)
    	 {
    	 	 memcpy(g,f,sizeof(f)),memset(f,0,sizeof(f));
    	 	 for(int s=0;s<9;s++) for(int Put=0;s+Put<9;Put++)
    	 	 {
    	 	 	 ll mul=C(cnt[i]+Put-1,Put);
    	 	 	 for(int Left=0;Left<p;Left++)
    	 	 	 	 f[s+Put][(Left+i*Put)%p]=(f[s+Put][(Left+i*Put)%p]+mul*g[s][Left]%mod)%mod;
    	 	 }
    	 }
    	 ll ret=0;
    	 for(int i=0;i<9;i++) ret=(ret+f[i][0])%mod;
    	 return ret;
    }
    int main()
    {
    	 scanf("%lld%lld",&n,&p);
    	 inv[0]=inv[1]=1; for(ll i=2;i<=10;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod;
    	 pre_cnt();
    	 printf("%lld
    ",Dp());
         return 0;
    }
    
  • 相关阅读:
    正则表达式(一个字符串翻转的例子)(http://www.cnblogs.com/hai98)
    电话号码正则表达式
    用哪种方法判断字符串为空更快速
    SQL内数据类型
    正则表达式基础(http://www.cnblogs.com/hai98)
    随机生成数
    C#源码 备份和恢复数据库
    ajax技术制作得在线歌词搜索功能
    ReadyGo新闻管理系统 1.5版 无任何使用限制
    最新完成的asp.net 2.0网站
  • 原文地址:https://www.cnblogs.com/EricQian/p/15070149.html
Copyright © 2020-2023  润新知