DAY 2 上午

背包


 

背包dp

一般是给出一些“物品”,每个物品具有一些价值参数和花费参数,要求在满足花费限制下最大化价值或者方案数。

最简单几种类型以及模型

0/1背包

完全背包

多重背包

 

0/1背包问题

给出n个物品,每个物品有Vi的价值和Wi的费用,我们总共有m块钱,求最多能得到多少价值的物品。

 

设dp[i][j]表示前i个物品花了j元钱的最大价值

分情况讨论:第i个物品选了或者没选

则dp[i][j]=max{dp[i-1][j] , dp[i-1][j-w[i]]+v[i]}

一般写法:

memset(dp,-0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++)
{
    for(int j=0;j<w[i];j++) dp[i][j]=dp[i-1][j];
    for(int j=w[i];j<=m;j++) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}

更节省空间

memset(dp,-0x3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=n;i++)
{
    for(int j=m;j>=w[i];i--) f[j]=max(f[j],f[j-w[i]]+v[i]);
}

输出方案:在取max的操作中做标记

方案数:如果每个物品没有价值,只有费用,就只需要把取max换成加法

如果有价值,设一个f[i][j]表示方案数,如果是从第一个转移过来的就把他赋值为第一个的方案数,如果是 从第二个转移过来的就把他赋值为第二个的方案数,如果相同就把他们两个加起来

 

 

完全背包

每一个物品可以选无限个。

◦dp[i][j]=max{dp[i][j-w[i]],dp[i-1][j]}

在这里提一下一个贪心的预处理

对于所有 Vi≥Vj,Ci≤Cj 的物品 i,都可以完全扔掉

对于体积相同的物品只需要留下价值最大的物品

对于随机数据这个优化的力度非常大。

memset(dp,-0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++)
{
    for(int j=0;j<w[i];j++) dp[i][j]=dp[i-1][j];
    for(int j=w[i];j<=m;j++) dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
}
memset(dp,-0x3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=n;i++)
{
    for(int j=w[i];j<=m;i--) f[j]=max(f[j],f[j-w[i]]+v[i]);
}

多重背包

对每一个物品,最多能用t[i]次。

最暴力的方法就是转移的时候枚举这个物品选几个即可。

dp[i][j] = max{ dp[i-1][j-w[i]*k] + v[i]*k | k<=t[i]}

复杂度O( N*M*t[i] ) 

memset(dp,-0x3f,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++)
{
    for(int j=0;j<=m;j++)
    {
        dp[i][j]=dp[i-1][j];
        for(int k=1;k<=c[i]&&k*w[i]<=j;k++)
        dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
    }
}
memset(dp,-0x3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=n;i++)
{
    for(int j=m;j>=0;j--)
    {
        for(int k=1;k<=c[i]&&k*w[i]<=j;k++)
        f[j]=max(f[j],f[j-k*w[i]]+k*v[i]);
    }
}

优化一:可以把t[i]拆成1+2+4+8…2^k+x, 这样k+1组,x<2^(k+1) ,然后我们会发现这些组能拼成0…t[i]每一种情况,然后这样我们就成了n*log(t[i])个物品的0/1背包问题。

对于[0,2^(k+1)-1]都能拼成,所以[x,2^(k+1)-1+x]也都能拼成,x<=2^(k+1)-1, 则[0,2^(k+1)-1+x]全能拼成。

复杂度O(n*log(t[i])*m)

优化二:

dp[i][j]=max(dp[i-1][j-w[i]*k]+v[i]*k|k<=t[i]}

我们发现对于第二维,我们的j和能转移过来的j-w[i]*k在模w[i]意义下是同余的,也就是说我们可以对于第二维按照模w[i]进行分类,不同类之间不会互相影响。

设fr[i][j]=dp[i-1][j*w[i]+r]。r是我们枚举模w[i]的一个类。

 

fr[i][j]=dp[i][j*w[i]+r]

fr[i][j]=max{fr[i-1][p]+v[i]*(j-p)|p>=j-t[i}

p=j-k

fr[i][j]=max{fr[i-1][p]-v[i]*p|p>=j-t[i]}+v[i]*j

实际上就是一个滑动窗口取最值的问题,直接单调队列优化即可。

 

 

分组背包

一共有n组,每组有size[i]个物品,第i组第j个物品的费用为w[i][j],价值v[i][j],每个组里的物品是互斥的,意味着你在一组物品中只能选择一个 物品,求花费小于等于m能得到的最大价值。

Size之和小于等于1000m<=1000 

memset(dp,-0x3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=n;i++)
{
    for(int j=m;j>=0;j--)
    {
        for(int k=1;k<=size[i];k++)
        f[j]=max(f[j],f[j-w[i][k]]+v[i][k]);
    }
}

经典例题2

给出n个化学反应,每个反应消耗a[i]升氧气和b[i]升氢气,可以得到w[i]的价值,现在总共有X升氧气和Y升氢气,求我们最多可以得到多少价值。

n,a[i],b[i],X,Y<=100

二维背包问题

此外还有二维背包问题,实质上和本身和普通背包毫无思维上的区别。

因为限制条件多了一个,所以我们需要给最初最基本的dp多加一维状态。

设dp[i][x][y]表示前i个物品,消耗了x的氧气和y的氢气所能得到的最大收益是多少。

然后考虑一个物品选还是不选即可。

 

 

做背包问题最关键的就是找清楚并反问自己?

这题里面什么是容量? 什么是物品? 什么是物品的费用? 什么是物品的价值? 

容量,就是这题当中我们怎样表示状态的数组。

费用,就是用来f[i]---->f[i+v[k]],状态转移的跨度。

价值,就是你这个dp的数组,所维护的东西。维护的数值!

背包dp一定要理解好这三点,因为很多时候题目中的费用并非背包dp中的费用

 

vijos1240

1.给你一些房间,告诉你这些房间的容纳人数和价格。

2.安排一定数量的人住到旅馆里,满足:

a.不同性别的人如果不是夫妻那么不能住一个房间。

b.一对夫妻如果住在一起,那么房间不能安排其他的人进去哪怕房间 没盛满人。

你来写一个程序帮助佳佳找到安排这些来参加旅行的人住进旅馆所需要 的最小花费。

M:参加旅行的男性人数、 f:参加旅行的女性人数、 r:旅馆的房间数、c:这些男女中有多少对夫妻、Bi:每个房子容纳人数和、Pi:每个房子价格。注意每一个人不是单身就是和他/她唯一的妻子/丈夫一起参加旅行。

◦ 0<=m,f,r<=300,0<=c<=Min(m,f)0<=Pi<=102<=Bi<=300

 

假如没有夫妻住在同一间房dp[i][x][y][0]表示前i个房间x个男的y个女的并且没有夫妇住在一起的最少花费,转移考虑下一间房全是男或者全是女

假如有夫妻住在同一间房,那么最多只有一对夫妻单独一间房。因为如果有两对,就可以把男男一间女女一间调换,结果不会更坏,而且还有可能更好。f[i][j][k][1]表示前i个房间住j名男性k名女性并且有一对夫妇住在一起的最小

花费

f[i][j][k][0]=min(f[i-1][j][k][0],f[i-1][j-v[i]][k][0]+p[i]][f[i-1,j][k-v[i]][0]+p[i])

f[i][j][k][1]= min(f[i-1][j][k][1],f[i-1][j-v[i]][k][1]+p[i],f[i-1][j][k-v[i]][1]+p[i],f[i-1][j-1][k-1][0]+p[i])

 

Bzoj 1190

给你N颗宝石,每颗宝石都有重量和价值V。要你从这些宝石中选取一些宝石,保证总重量不超过W,且总价值最大为,并输出最大的总价值,每颗宝石的重量符合a*2^b

V<=1e9 ◦ a<=10; b<=30

 

f[i][j]表示从高到低考虑到第i位,剩下j*2^i的体积可用的最大价值是多少

f[j]+v->f[j-a]

a*2^b

(j-a)*2^b

j’=j*2+1

 

Bzoj3163 heoi2013 新背包问题

N个物品,第i个物品有c[i]个,购买第i个物品需要a[i]元,可获利b[i]的价值。有m个询问,每次询问:如果第x个物品禁止购买,你有y元的话,能获得的最大价值是多少?询问之间互相独立。

◦N<=1000,m<=3*10^5

 

divide(l,r)表示除去[l,r]之间的都被加进去了

初始调用dd(1,n)

dd(l,mid)时,把[mid+1,r]内的物品加入dp数组。

我们这里定义的加入这个物品u,就是多考虑上这个物品之后构成的dp数组。

若是0/1背包的加入也就是做以下这个操作。

For (int i=n;i>=w[u];i--) dp[i]=max(dp[i],dp[i-w[u]]+v[u]);

当l=r时,将对应所有的询问在dp数组查询即可。

单调队列优化的话,复杂度O(n*m*log(n)),每个物品被加进去log次,每次O(m) 

void work(int l,int r,int d)
{
    if(l==r)
    {
        for(int i=head[i];i;i=edg[i].nxt) ans[id[i]]=dp[d-1][S[i]];
        return;
    }
    int mid=(l+r)>>1;
    for(int i=0;i<=m;i++) dp[d][i]=dp[d-1][i];
    for(int i=mid+1;i<=r;i++) insert(dp[d],i);
    work(l,mid,d+1);
    for(int i=0;i<=m;i++) dp[d][i]=dp[d-1][i];
    for(int i=l;i<=mid;i++) insert(dp[d],i);
    work(mid+1,r,d+1);
}

Insert(dp,i):是在dp数组当中加入i号物品。

 

 

bzoj2287

ftiasch 有 N 个物品, 体积分别是 W1, W2, ..., WN。 由于她的疏忽, 第 i 个物品丢失了。 “要使用剩下的 N - 1 物品装满容积为 x 的背包,有几种方法呢?” -- 这是经典的问题了。

她把答案记为 Count(i, x) ,想要得到所有1 <= i <= N, 1 <= x <= M的 Count(i,x) 表格。

◦N,M<=3000

 

 

f[i]表示加进去这个物品装满i体积(但是有可能不选)的方案数

f[i]=f[i]+f[i-w]

g[i]表示没加进去时候的方案数

f[i]=g[i]+g[i-w]-->g[i]=f[i]-g[i-w]

从小到大枚举一遍

当i<w时,f[i]=g[i]

 

经典题

给出n个物品,每个物品有体积v[i],要求把物品分成两堆,然后使得两堆物品体积的绝对值最小。

n,v[i]<=100

 

一类整数划分问题

1:求把n划分成k个正整数的方案数?

2:求把n划分成互不相同k个正整数的方案数?

3:求把n划分成k个不大于m的互不相同正整数的方案数?

4:求把n划分成k个奇数的方案数?

………………………………………………

如果n,k和m是同阶的,我们看看最优能做到多少的复杂度。

 

求把n划分成k个正整数的方案数。

考虑数形结合一波

我们把这个问题转化成把n个格子分成k行 ,这样每一种不同的涂色方法就对应了不同的方案

讨论有没有一就可以了。

复杂度n^2

 

求把n划分成互不相同的k个正整数的方案数。

复杂度n√n

 

求把n划分成k个不大于m的互不相同正整数的方案数。 

BZOJ 3612

◦给定一个杠杆,等距均匀分布一共2n+1个等重质点,支点在n+1处,求拿走k个质点后使杠杆仍然保持平衡的方案数 mod p的值。

◦ 1 <= n <= 10000,1 <= k <= 10,2 <= p <= 10000,且 k <= 2n+1

 

枚举和和左边选的个数

 

由此类问题对状态转移的一点感触

dp问题中,转移就是分情况讨论,每种情况对应一个方案数或最优值,而这个方案数或最优值可以表示为之前已经求出来的dp值的组合。

只不过分情况讨论可能方法很多,以一种方式讨论能转化为已知的dp值的叠加,另一种方式也可以。我们需要保证的是:讨论不漏掉任何情况像计数问题也不能出现方案重叠(求max和min其实是可以重叠的),同

时选择分类项数尽量少的方案,以便得到更优的复杂度。

上面几道题的转移方式可能没有原先一些问题的分类方式直观,但是也的确满足了不重不漏尽量简洁的条件。当然这可能也并非是唯一的转移方式,只要保证能划归到之前已经求出的dp值就行。 

 

数位DP

经典的数位Dp是要求统计符合限制的数字的个数。

◦一般的形式是:求区间[n,m]满足限制f(1)、f(2)、f(3)等等的数字的数量是多少。条件 f(i)一般与数的大小无关,而与数的组成有关。

善用不同进制来处理,一般问题都是10进制和二进制的数位dp。

数位dp的部分一般都是很套路的,但是有些题目在数位dp外面套了一个华丽的外衣,有时我们难以看出来。

 

HDU3652

统计区间 [1,n] 中含有 '13' 且模 13 为 0 的数字有多少个。

N<=10^9

 

暴力的去枚举每一个数然后去计算必然太慢。

我们先来考虑一个更简单的形式

 

统计区间 [1,n] 中含有 '3' 的数字有多少个。

 

从高位到低位填数

P10P9....P0(数位)

N10N9....N0(上界)

枚举从哪里开始没有上界了,然后情况之和就是总的方案数

f[i][1]表示i之前出现过3的方案数,f[i][0]表示i之前有没有3无所谓的方案数

f[i][0]=Σf[i-1][0] (j=0~9)=10*f[i-1][0]

f[i][1]=Σ(j!=3)f[i-1][1] (j==3)f[i-1][0] (j=0~9)

 

那回归到原题呢?

枚举哪一位不同没什么变化吧,跟原先一样枚举就好了。

就是f数组要变,因为约束条件更多了,所以状态的维数要增加。

设f[k][前面是否已经出现13][上一位是否是1][前面的那些数mod13等于多少],转移的话同样还是枚举这一位是填什么即可

其实,我们刚刚那个dp的过程自然是思路很清晰,但是一般实现我们不那么写。因为毕竟还是好多个for循环,预处理一套循环,算答案一套循环,记忆化搜索的话则是要什么算什么,会好写很多。

实际上数位dp往往都是用记忆化搜索的方式来实现,就是求什么调用什么,调用完了,记下来,下次就不用重新算了。

我们来看一下上一题的代码

关于数位dp的经验

1:注意很多时候带进去是n==0要特殊处理。

2:还有一般问[m,n],我们求[1,n]-[1,m-1]但是有的时候m0就炸了。

3:求所有包含49的数,其实就是(总数-所有不包含49的数)。前者的化需要有两维限制,一个是上一位是什么,一个是之前有没有49。但是后 者只需要记一个上一位是什么。就能好写一些。

4:一般问题的数位dp部分,都是套路,但是这并不代表它外面华丽的外衣和与其他算法结合的的部分也是无脑的。要看出它是考数位dp,要看出问题怎么变化一下就是数位dp了。

5:dp初始化memset要置为-1。不能置为0!!!!!!因为有很多时候dp值就应该是0,然后我们如果误以为是因为之前没有计算,重新计算的话,就会tle。

 

这里不能写成0。

6:既然是记忆化搜索,那就可以剪枝!!!!可行性剪枝!!

7:注意windy数的情况,有时前导0也需要记的!!! 

原文地址:https://www.cnblogs.com/lcezych/p/11314746.html