背包问题可以视为组合dp,而最长上升子序列问题视为线性dp(区间dp),它们的区别在于当前位置的决策跟前面具体某个的值有没有关系,或者说,当前位置的选与不选与已经确定的序列的最后一个位置有没有关系,如果有关系,那么就是线性dp,需要以i作为结尾,如果没关系就是组合dp,前i个笼统考虑即可。
核心地方就在于每个物品只能选一次,所以每个物品只有选与不选两种情况。然后要梳理出什么是费用(即我们对什么量进行了限制),什么是价值。不过什么是价值什么是费用这个不要死扣,最关键的地方在于每个物品只有1个,当前物品的选与不选与前面具体某个的值关系不大。
思路:题意很明显了,就是从n个物品中选若干个,体积不超过V,求最大价值。
我们先来考虑状态表示:定义dp[i][j]来表示从前i个物品中选,体积不超过j的情况下的价值,值表示最大价值。
状态计算:这里看最后一个元素,就是第i个元素选与不选的区别。
如果不选,那么就是从在前i-1个元素,然后体积不超过j的情况转化而来
dp[i][j]=dp[i-1][j]
如果选,那么转化也是从在前i-1个元素中选的情况转化,这时候体积还为j实际上就是不合适的,因为a[i]有体积,要将a[i]放进去的话,可能就会超出j的限制,所以我们将v[i]预留出来
dp[i][j]=dp[i-1][j-v[i]]+w[i]
那么状态及状态转移就分析出来了,我们按照这个思路来实现一下:
#include
using namespace std;
int dp[1010][1010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=0;j<=m;j++)
/*不能直接从v开始循环,因为前面的部分在不放i的情况下是合法的,
所有合法方案都要更新,因为它们即使不是答案,也可以用来更新别的值
*/
{
dp[i][j]=dp[i-1][j];
if(v<=j) dp[i][j]=max(dp[i][j],dp[i-1][j-v]+w);
}
}
printf("%d",dp[n][m]);
}
但是我们注意到,这里二维的状态实际上还是有点麻烦,还能进一步优化。我们可以将二维的状态优化成一维:
我们注意到用来更新第i层时,实际上只用到了第i-1层的数据,那么我们i-1一层肯定是先被更新的,如果它在第i层还每被更新就被调用,那么它的值就仍为第i-1层的,实际上可以拿来直接用。
那么就来考虑一下第i层的更新顺序,很显然,dp[i][j]更新时用到的要么是第i层j位置的值,要么用的是小于j的值,所以我们只要倒序更新,就可以保证用到的是没更新过的上一层的值。那么就可以用一维数组来实现。
#include
using namespace std;
int dp[1010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=m;j>=v;j--)
{
dp[j]=max(dp[j],dp[j-v]+w);
}
}
printf("%d",dp[m]);
}
思路:每种草药只有一株,那么只有选与不选两种情况,那么就是01背包问题,然后来看费用是什么,显然这里对时间有限制所以我们的第二维就是时间,然后要求的是最大价值。和上面的模型大差不差。
#include
using namespace std;
int dp[1010];
int main()
{
int n,m;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=m;j>=v;j--)
dp[j]=max(dp[j],dp[j-v]+w);
}
printf("%d",dp[m]);
}
思路:这个题在01背包的基础上变化,实际上还有点意思,会发现我们的费用和价值都是体积,乍一看和01背包好像不大一样,模型中01背包的费用和价值不同,但实际上没什么区别,我们就将费用和价值都视为体积来算。要求小剩余空间就是求可以装的最大体积数,最后减一下即可。
#include
using namespace std;
int dp[20010];
int main()
{
int n,m;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)
{
int v;
scanf("%d",&v);
for(int j=m;j>=v;j--)
dp[j]=max(dp[j],dp[j-v]+v);
}
printf("%d",m-dp[m]);
}
思路:这题要求的并非最大值,而是方案的个数,实际放在后面那个求方案数的模块比较好,但是它本质上是01背包问题,我们干脆就放在这儿一块儿来思考即可。
我们前面定义的都是费用不超过j时的情况,但是这里要求恰好为m,所以我们的定义实际也得改一下,改成和恰好为j的情况。
那么状态定义就是,dp[i][j]从前i个中选择,和恰好为j的方案数。
状态转移:也是看最后一个,即a[i]选与不选。
不选a[i]:dp[i][j]=dp[i-1][j]
选a[i],那么就要预留出a[i]的位置:dp[i][j]=dp[i-1][j-a[i]]
所以dp[i][j]=dp[i-1][j]+dp[i-1][j-a[i]]。
这个题最关键的地方在于边界值, 因为我们可以发现如果边界值置零的话,按照上式计算,那么整个数组都是0,毫无意义,实际上有两种处理方式,将所有的dp[i][0]全部赋值成1,因为前i个中选结果为0,那么就是一个都不选,实际上也是一种方案。还有一种处理方式:
dp[i][j]=dp[i-1][j];
if(a==j) dp[i][j] += 1;
if(a<=j) dp[i][j] += dp[i-1][j-a];
在恰好为a的时候加上一种方案,这里只选a[i]本身是一种不含在另外两种方案中(如果没有单独处理0的话),实际上这个操作也等价dp[i][j]+=dp[i][0],只是因为没有预处理,所以这里特判一下,实际上都大差不差。
#include
using namespace std;
int dp[10010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
dp[0]=1;
for(int i=1;i<=n;i++)
{
int a;
cin>>a;
for(int j=m;j>=a;j--)
{
dp[j] += dp[j-a];
}
}
cout<
思路:这题的难点实际在阅读理解(哈哈哈),我们提炼一下就是,总共有n元钱,m个物品,每个物品有两个值价格和重要度,它们的乘积是价值,且每个物品只有一个,我们要在预算范围内得到最大价值。那么就是常规的01背包问题 。
#include
using namespace std;
int dp[30010];
int main()
{
int m,n;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
w *= v;
for(int j=m;j>=v;j--)
{
dp[j]=max(dp[j],dp[j-v]+w);
}
}
cout<
思路:这题每个元素的属性随着时间在不断变化,所以我们不仅要考虑每个点的选与不选,更要考虑它的选择顺序,因为它的属性会随选择顺序变化,之前见过的背包问题,每个物品的属性是固定的,所以这道题最先要解决的就是选择顺序问题,那么实质就是贪心
如何贪心呢?
我们讨论两个元素i,j
如果先吃i,再吃j,那么能量为:
vi+vj-lj*si
如果反着来,那么能量为:
vj+vi-li*sj
我们比较两者大小
会发现取决于
-lj*si与-li*sj的大小,移个项:
-si/li -sj/lj
所以应该是si/li越小,那么就应该先吃,于是顺序就找到了
(ps:很多贪心问题都可以通过考虑相邻两个可以产生的不同顺序的效益来思考。)
那么规定好顺序之后,就是01背包了,需要考虑每个元素的选与不选问题
定义数组dp[i][j]表示从前i个元素中选,时间恰好为j的时候,产生的效益,值表示最大效益。(因为我们需要通过时间算出每个元素的损失,所以必须要定义确切的时间)
状态计算就是:
不选dp[i][j]=dp[i-1][j]
选:dp[i][j]=dp[i-1][j-s]+e-(j-s)*l
那么就是两者的最大值
体积上限是每块石头能量都耗尽的值
最后循环遍历就能找到答案
#include
using namespace std;
int dp[10010];
struct node
{
int s,e,l;
}a[120];
bool cmp(node a,node b)
{
return a.s*b.l>s>>e>>l;
a[i]={s,e,l};
sum += s;
}
sort(a+1,a+n+1,cmp);
memset(dp,0,sizeof dp);
for(int i=1;i<=n;i++)
{
int s=a[i].s,e=a[i].e,l=a[i].l;
for(int j=sum;j>=s;j--)
{
dp[j]=max(dp[j],dp[j-s]+e-l*(j-s));
}
}
int mx=0;
for(int j=0;j<=sum;j++) mx=max(mx,dp[j]);//因为不是不超过是恰好,所以需要循环找
cout<<"Case #"<
完全背包问题的核心就在于每种物品可以选无限个。
思路:我们就此来分析一下完全背包的模型。这里物品的种类数是n,背包容积是v,每种物品可以选无限个,那么物品的选择就从选与不选转化成了选多少个。
状态表示,因为每个物品与相邻物品之间并没有关系要求,所以我们定义dp[i][j]表示从前i个物品中选,体积不超过j的选法的价值,属性为最大价值。
状态划分,我们以当前物品选几个来划分。
选0个:dp[i][j]=dp[i-1][j]
选1个:dp[i][j]=dp[i-1][j-v]+w
...
选k个:dp[i][j]=dp[i-1][j-kv]+kw
显然我们只要再加一层循环表示选多少个,那么就可以实现了:
#include
using namespace std;
int dp[1010][1010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=0;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
for(int k=1;k*v<=j;k++)
{
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v]+k*w);
}
}
}
cout<
但是这里有三重循环,时间复杂度但凡高一点就超时了,那么我们来考虑优化。这里很容易联想到01背包的优化,我们通过优化掉第一维来优化,第一维能优化掉吗?当然可以,循环的层数会减少吗?当然不会。所以真正核心的问题没有解决。我们要想办法优化掉一层循环,来找下规律吧。
dp[i,j]=max(dp[i-1,j],dp[i-1,j-v]+v,dp[i-1,j-2v]+2v,dp[i-1,j-3v]+3w,...,dp[i,j-sv]+sw)(最多s个)
dp[i,j-v]=max( dp[i-1,j-v], dp[i-1,j-2v]+w, dp[i-1,j-3v]+2w,...dp[i,j-sv]+(s-1)w)
有没有发现,dp[i,j]取max值的后面一段和dp[i,j-v]有很多地方重合。
实际上我们可以进行替换:
dp[i,j]=max(dp[i-1,j],dp[i,j-v]+w);
或者还能这么理解,我们要求的dp[i,j]实际上是从dp[i,j-sv]开始每次放一个,求前缀中的最大值,而根据这里dp[i][j-v]的定义,我们实际上求的就是前缀中的最大值。
那么这样就直接将第三维优化掉了。
#include
using namespace std;
int dp[1010][1010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=0;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
if(v<=j) dp[i][j]=max(dp[i][j],dp[i][j-v]+w);
}
}
cout<
另外我们注意到循环实际上到1e9还能卡着过(看情况,尽量不要到这么大,这就是在超时的边缘徘徊),但是int类型的数组开不到这么大,所以我们看看,能不能像前面一样直接优化掉一维。
一个int占4Byte,比如一道题有64MB空间限制,那么理论上可以开64MB / 4B = 64 * 1024 * 1024 / 4 约等于1.6*10^7的长度
每次用到的值,一个是上一层的j位置的值,一个是本层前面已经更新过的j-v位置的值。那么只要第二层正着循环,就可以直接优化掉一维。
#include
using namespace std;
int dp[1010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=v;j<=m;j++)
{
dp[j]=dp[j];
dp[j]=max(dp[j],dp[j-v]+w);
}
}
cout<
还有一点,注意到这里j是从v开始的,但是二维的时候,j是从0开始的,是因为在二维的情况下,尽管这一层的这个位置仍然是上一层的值,但是下一层的更新是需要用到这个值的,如果不更新那么就是0,下一层的结果就会出问题,但是如果是一维,它不更新就保持的是上一次更新的值,是有意义的。
那么完全背包的模型就讨论完了。
思路:这题虽然是求方案数,但是每种书有无限本,所以本质上还是完全背包问题,我们放在这里讨论就好。
首先,这里有一点值得注意的,n元钱全部用来买书,这里很关键,决定了我们定义的集合的意义。虽然没明说,但是这里默认每本书有无限个,就是完全背包问题。
状态表示,定义dp[i][j]表示从前i个中选,花费恰好是j的方案数。
状态计算,也就是这种书选多少本,不过这里不是求最大值,而是求和。
dp[i,j]=dp[i-1,j]+dp[i-1,j-v]+dp[i-1,j-2v]+...+dp[i-1,j-sv]
dp[i,j-v]= dp[i-1,j-v]+dp[i-1,j-2v]+...+dp[i-1,j-sv]
所以dp[i,j]=dp[i-1,j]+dp[i,j-v]
看第二维使用的是什么,显然是上一层层j位置的值和本层j-v位置的值,那么可以直接优化掉第一维,然后正向访问即可。
另外要注意到边界值的处理问题,这取决于dp[][]边界的意义,如果是求最大值,那么dp[i][0]为0,因为一个都不选的话,价值就是0,但是对于求方案,dp[i][0]需要为1,因为不选也是一种方案。所以优化成一维后,就要给dp[0]一个初值1。另外dp[0][j]的初值取决与我们定义的是恰好还是不超过,恰好的话只有dp[0][0]才有意义,剩下的dp[0][j]因为一个都不选结果只能是0,所以它们都是无效值。不过最重要的还是根据实际情况来确定边界值的处理。
#include
using namespace std;
int dp[1010];
int main()
{
int n;
int a[]={0,10,20,50,100};
scanf("%d",&n);
dp[0]=1;
for(int i=1;i<=4;i++)
{
for(int j=a[i];j<=n;j++)
{
dp[j]+=dp[j-a[i]];
}
}
cout<
思路:这个题实际上跟买书题差不多,都是有若干物品,每种物品可以选任意多个(完全背包问题),最后组成m。要求组成m的话,那么状态定义就是恰好为m
状态表示:定义dp[i][j]表示从前i个物品中选,体积恰好为j的方案数。
状态计算:
不选第i个物品:dp[i][j]=dp[i-1][j]
选第i个物品可以选若干个,那么就是用前面所有的可能情况来更新:
dp[i][j]+=dp[i-1][j-kv](k=1,2,3,...)它所用到的这些实际上在dp[i][j-v]中都已经累计过了,那么我们可以直接用dp[i][j-v]来更新
所以状态转移就是dp[i][j]=dp[i-1][j]+dp[i][j-v]
可以优化成一维:dp[j]+=dp[j-v]
因为用到的dp[j-v]是本层的,所以要从体积小的往体积大的算
另外因为这里定义的是恰好,所以我们要考虑一个物品都不选时的边界状态,显然一个物品都不选的时候,只用dp[0]有一种方案,剩下的方案数是0,因为我们定义的是全局变量,而且只有一组样例,所以不用初始化。
#include
#define int long long
using namespace std;
int dp[3010];
signed main()
{
int n,m;
scanf("%lld%lld",&n,&m);
dp[0]=1;
for(int i=1;i<=n;i++)
{
int v;
scanf("%lld",&v);
for(int j=v;j<=m;j++)
{
dp[j]+=dp[j-v];
}
}
cout<
ps:这里的数据范围看着不大,实际上是有爆int的可能的。
思路:这个题讲人话就是,我们进行一种规定,如果两个货币系统能表示出来的数都相同,那么这两个货币系统就是等价的,现有一个大小为n的货币系统,我们要求出与它等价的货币系统的最小为多大。
这里涉及到最大独立集,那么我们想想怎么求解。a能表示的b都能表示的话,那么每个a都能被b表示出来,同时每个b也能被a表示出来,还有就是b之间不能相互表示,否则可以被表示的那个数就可以被删掉,因为它能表示的数,也可以用其他的b来表示,然后我们猜测一个性质,b一定是从a中选出来的,证明如下:
如果bi不属于a[],但是由于b[]与a[]的性质,那么bi可以被一系列a表示出来,那么就可以被表示这些a的b表示出来,就相当于bi可以被其他的b表示,那么就可以删除。所以证明b一定来源于a。
进而得到,因为bj可以被一系列a表示,而这一系列a又能被一系列b表示,那么如何保证这个bj不被其他的b表示呢,那么等号左右要消去bj,就可以保证它不被其他的b表示。
现在b的范围从任意缩小到a了,那么我们继续想,对于a可以被表示的数,它肯定只能被小于它的数表示,而且表示方法是固定的,或者换句话说它被凑出来用到的ai种类如果确定的话,那么每一种的个数也是确定的。我们依次遍历,对于遍历过的数,它们能表示的数其实可以算出来,因为我们完全背包中有一种定义方式:dp[i][j]表示从前i个中选,体积恰好为j的情况,我们可以将属性定义为方案数,那么如果dp[i-1][a[i]]有方案的话,就说明a[i]可以被从1-(i-1)中的数表示出来。
#include
using namespace std;
int v[120],dp[25010];
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&v[i]);
sort(v+1,v+1+n);
memset(dp,0,sizeof dp);
int m=v[n];
int cnt=0;
dp[0]=1;
for(int i=1;i<=n;i++)
{
if(dp[v[i]]) continue;
else cnt++;
for(int j=v[i];j<=m;j++)
{
dp[j]=dp[j]+dp[j-v[i]];
}
}
cout<
ps:我们由此延伸出,背包模型不仅能求方案数、最大值、最小值,还能根据方案数判断某个值能否被表示。这个就有很多应用场景。最裸的就是给定区间和询问,判断询问的数能不能被区间中的数表示出来,如果可以输出表示它的方案数。看似麻烦,实际是01背包问题。最核心的就是需要意识到背包结束后我们是会将所有情况都覆盖的,如果定义的是恰好,那么就可以反映出能表示这个数的方案。至于循环上限,可以把每个数都加上,那就是上限,中间的都可以通过动态规划求出来。
多重背包实际上是完全背包问题的延伸,区别就在于对于每种物品的数量做出限制。
思路:这题的数据范围比较小,所以我们就可以用完全背包问题那边的暴力思想来解决,即:
状态表示:定义dp[i][j]表示从前i个物品中选,总体积不超过j的选法的价值,值这些选法价值中的最大值。
状态计算:根据第i个物品选几个来划分:
dp[i,j]=max(dp[i-1,j],dp[i-1,j-v]+w,...dp[i-1,j-sv]+sw);
#include
using namespace std;
int dp[120][120];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
for(int j=0;j<=m;j++)
{
for(int k=0;k*v<=j&&k<=s;k++)
{
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v]+k*w);
}
}
}
cout<
这个数据范围变大了,再用暴力就写不了了,这里我们引入一种特别的优化方式,二进制压缩。
可知每个数都可以转化成二进制数
这么来说,我们挑的数一定是在s的范围内,我们以6为例来讨论一下:
6=2^0+2^1+3=1+2+3 (2^2拆不出来了)
5=2^1+3=2+3
4=2^0+3=1+3
3=2^0+2^1=3
2=2^1=2
1=2^0
0即不选,不用考虑所以我们可以发现对s中2^k进行打包成小块。可以在保证每个小块都只用一次的情况下表示出1-s的所有数。那么每一小块都只用一次不就是01背包问题,就相当于把问题转化成01背包问题。
#include
using namespace std;
int v[30010],w[30010],dp[2010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
int k=0;
for(int i=1;i<=n;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
for(int j=0;j<=30;j++)
{
if( (1< c ) break;
v[++k] = (1<=v[i];j--)
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
cout<
ps:这里最核心的就是要意识到,我们最后的最优方案,每种物品选的个数是一定的。然后我们就要想办法把s和最后选出来的个数联系起来。我们可以发现每个数都可以用二进制数表示,那么我们将s拆解成二进制的一小份一小份,那么这些小份一定可以将最后的个数凑出来,而且每一小份只选一次,所以就是01背包问题。
思路:这道题的数据范围更大了,上面二进制的优化都会超时,那么就只能再想别的方法。我们套用完全背包的优化方式,看一下暴力时每组数据的规律:
dp[i,j]=max(dp[i-1,j],dp[i-1,j-v]+w,dp[i-1,j-2v]+2w,..., dp[i-1,j-sv]+sw )
dp[i,j-v]=max( dp[i-1,j-v], dp[i-1,j-2v]+v,..., dp[i-1,j-sv]+(s-1)w, dp[i-1,j-(s+1)v]+sw )
其实跟完全背包还有区别,因为后面还多了一组,所以不能用总体的最大值来表示前面的最大值。
那么后半部分的最大值该怎么求呢,我们多列几个出来看:
前面的看不出规律,那么我们一直写到最后,这里r=j%v,因为每次少v,最后只能剩下r。倒着看,最后一个可以更新倒数第二个,倒数第二个可以用来更新倒数第三个,...那么什么时候不能更新呢?我们看f[i,r+sv]还能用后面的来更新,
那么f[i,r+(s+1)v]最后用到的数是多少呢,显然是f[i-1,r+v],
而f[i,r+sv]最后用到的是f[i-1,r],那么就多了一部分出来,所以就不能用f[i,r+sv]来更新f[i,r+(s+1)v],
两维坐标看着还是有些麻烦,我们不妨转化成一维来看,为了区分,我们将当前层的用f表示,上一层的用g表示
f[r]=g[r];
f[r+v]=max(g[r+v],g[r]+w,)
f[r+2v]=max(g[r+2v],g[r+v]+w, g[r]+2w)
f[r+3v]=max(g[r+3v],g[r+2v]+w,g[r+v]+2w,g[r]+3w)
...
f[r+(s-2)v]=max(g[r+(s-2)v], g[r+(s-3)v]+w ,...,g[r]+(s-2)w)
f[r+(s-1)v]=max(g[r+(s-1)v], g[r+(s-2)v]+w ,..., g[r]+(s-1)w)
f[r+sv]= max( g[r+sv] ,..., g[r]+sw)//s个g
f[r+(s+1)v] = max(g[r+(s+1)v],g[r+sv]+w, ,..., g[r+v]+sw)
f[r+(s+2)v]=max(g[r+(s+2)v] ,..., g[r+2v]+sw)
再省略掉后面那个常数,然后再按从小到大的顺序排一下:
f[r]= g[r];
f[r+v]=max( g[r],g[r+v])
f[r+2v]=max(g[r],g[r+v],g[r+2v])
f[r+3v]=max(g[r],g[r+v],g[r+2v],g[r+3v])
....
f[r+(s-2)v]=max( g[r],g[r+v],g[r+(s-3)v],g[r+(s-2)v])
f[r+(s-1)v]=max( g[r],g[r+v],g[r+2v],g g[r+(s-2)v],g[r+(s-1)v])
f[r+sv]= max( g[r],g[r+v],g[r+2v] ,..., g[r+(s-2)v],g[r+(s-1)v],g[r+sv])
f[r+(s+1)v] = max(g[r+v],g[r+2v],g[r+3v],g[r+4v]...,g[r+(s-1)v],g[r+sv],g[r+(s+1)v])
f[r+(s+2)v]= max(g[r+2v],g[r+3v],..., g[r+sv],g[r+(s+1)v],g[r+(s+2)v])
这样就明显了,在sv之前,我们是每次加入新的,一旦到sv之后,我们所选的数的个数就定下来了,后面再新增就要把开头的数弹出去。那么这不就明显了,就是滑动窗口求最大值问题。
关于滑动窗口求最大值,思路就是,如果一旦放进一个很大的数,那么前面的都不会再出现在结果中了。如果当前要放入的数小于前面出现的数,那么就很显然当前这个数不能把前面的数否决掉,但是,窗口长度有限,前面的数最终会被弹出去,当前放入的数可能在后面的数中作为最大值出现,所以要记录下来。滑动窗口维护的相当于是一个单减的序列。每次的最大值就是队头,每次放新元素从队尾放入。放入新元素前需要判一下队头有没有超限用不用弹出,再判下队尾用不用弹出冗余 。
滑动窗口有两种实现方式,deque和数组模拟,deque可能会超时,所以我们用数组模拟来是实现。
然后其实还有一个问题,就是偏移量w的问题,我们可以找到规律,某个位置与j之间有多少个v,那么就是要乘多少个w,所以我们判断一下j-q[t-1])/v*w < g[j]是否成立,成立再弹
#include
using namespace std;
int f[20010],g[20010];
int q[20010],h,t;
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
for(int r=0;rrs) h++;//与当前位置的距离/v后超过s,即在窗口之外
while(h
至此多重背包的三个模型就分析完了。
思路:这题实际上很裸,就是简单的多重背包问题,主要看用哪个模型。
暴力的时间复杂度是O(n*m*s)
二进制的时间复杂度是O(30*n*m),30*n是最多能拆出来的份数,m是背包体积
单调队列优化时间复杂的是O(n*m)//r层是v的最大值,j层是m/v,乘起来就是m
本题n*m*s=30000000,小于1e9,直接暴力就能解决。
#include
using namespace std;
int dp[600][6010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
for(int j=0;j<=m;j++)
{
for(int k=0;k<=s&&k*v<=j;k++)
{
dp[i][j]=max(dp[i][j],dp[i-1][j-k*v]+k*w);
}
}
}
cout<
分组背包的核心,就在于有若干组,每组之中最多只能选一个。
我们来看,这里可以沿用完全背包和多重背包的暴力做法,去讨论选0个或者选第几个,那么循环就能解出来。
但是这里的循环顺序很关键:
//这里则会导致有些物品更新时用的是已经更新过的值,因为大家的大小不一 for(int x=1;x<=s;x++)//这层有多个物品,大家体积的是不一样的 for(int j=m;j>=v[x];j--) dp[j]=max(dp[j],dp[j-v[x]]+w[x]); //这个相当于在限制体积的情况下将这一组所有的物品都考虑一遍 for(int j=m;j>=0;j--) for(int x=1;x<=s;x++)//这层有多个物品,大家体积的是不一样的 if(v[x]<=j) dp[j]=max(dp[j],dp[j-v[x]]+w[x]);
所以应该在每一个体积下将这层的物品更新一遍,这样才能保证,用到的都是上一层的值。
#include
using namespace std;
int dp[200],v[200],w[200];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=0;i=0;j--)
for(int x=1;x<=s;x++)
if(v[x]<=j) dp[j]=max(dp[j],dp[j-v[x]]+w[x]);
}
cout<
思路:这里很容易想到多重背包问题,但是我们j台是视为一个整体有一个价值的,不像多重背包中那样每个物品的属性相同。 所以还是分组背包问题,在选择给每个公司分配几台。不过这题还有一点不一样,它不仅要求最值,还要求具体方案。
求具体方案的都是用从最后一步开始往前转移而来。
这里任意的话,那么一旦dp[i][j]==dp[i-1][j-v]+w就输出,一定写清楚,不要在细节上出错,不然真的很可惜。
#include
using namespace std;
int dp[200][100],v[200][200],w[200][200];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&w[i][j]);
for(int i=1;i<=n;i++)
{ for(int j=m;j>=0;j--)
{
dp[i][j]=dp[i-1][j];
for(int x=1;x<=m;x++)
{
if(x<=j) dp[i][j]=max(dp[i][j],dp[i-1][j-x]+w[i][x]);
}
}
}
int j=m;
cout<=1;i--)
{
for(int k=0;k<=j;k++)
{
if(dp[i][j]==dp[i-1][j-k]+w[i][k])
{
cout<
思路:我们来考虑一下,每个物品与它相邻的物品的选择与否实际没有关系,所以它们之间是相互独立的关系,那么只要按照物品的种类来判断选它的策略即可。对于01背包就按01背包的策略,对于完全背包就按完全背包的策略,对于多重背包就按多重背包的策略。为了简化代码也为了降低多重背包的时间复杂度,实际上可以将01背包视为多重背包,对多重背包采用二进制优化来写。
#include
using namespace std;
int dp[1010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int v,w,s;
scanf("%d%d%d",&v,&w,&s);
if(s==0)
{
for(int j=v;j<=m;j++)
dp[j]=max(dp[j],dp[j-v]+w);
}
else
{
if(s==-1)s=1;
for(int k=1;k<=s;k*=2)//二进制分解
{
for(int j=m;j>=k*v;j--)//分解出的每一组都是01背包问题,所以是从大到小遍历
{
dp[j]=max(dp[j],dp[j-k*v]+k*w);
}
s -= k;
}
if(s)//如果分解结束还有剩余
{
for(int j=m;j>=s*v;j--)
{
dp[j]=max(dp[j],dp[j-s*v]+s*w);
}
}
}
}
cout<
二维费用背包问题实际上就是多了一个限制,本质上还是上面4种基础背包问题模型的延伸。
思路:这题就是一道很裸的二位费用背包问题,我们对背包的体积和重量都做出了限制。但是每个物品只有一个就是选与不选的问题,所以实际是01背包的延伸。这里既然多了一种状态,那么我们就给数组多加一维即可。
状态表示,定义dp[i][j][k]表示从前i个物品中选,体积不超过j,重量不超过k的价值的集合,它的值表示最大值
状态计算,划分的依据就是第i个物品的选与不选,
选:dp[i][j][k]=dp[i-1][j][k]
不选:dp[i][j][k]=dp[i-1][j-v][k-m]+w
那么即 dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-v][k-m]+w)
我们可以发现只用到了i-1层的值,可以和01背包的优化一样来进行优化。
dp[j][k]=max(dp[j][k],dp[j-v][k-m]+w)
因为用到的是较小的值,所以我们从大到小遍历。
#include
using namespace std;
int dp[120][120];
int main()
{
int n,v,m;
scanf("%d%d%d",&n,&v,&m);
for(int i=1;i<=n;i++)
{
int sv,sm,sw;
scanf("%d%d%d",&sv,&sm,&sw);
for(int j=v;j>=sv;j--)
{
for(int k=m;k>=sm;k--)
{
dp[j][k]=max(dp[j][k],dp[j-sv][k-sm]+sw);
}
}
}
cout<
思路:我们可以看到这个题也是有两重限制,精灵球的个数和皮卡丘的体力值,所以是二维费用问题,然后每种精灵只有一个,区别在于选与不选,那么就是01背包。
状态表示:定义dp[i][j][k]表示从前i个中选,精灵球消耗不超过j,皮卡丘体力消耗不超过k的个数的集合,它的值表示最大值。
状态计算:
不选:dp[i][j][k]=dp[i-1][j][k]
选:dp[i][j][k]=dp[i-1][j-v][k-a]+1
可以优化掉第一维:dp[j][k]=max(dp[j][k],dp[j-v][k-a]+1);
从大到小访问,另外注意到皮卡丘消耗的体力值不能恰好为它的初始体力值。
那么就可以求出最大精灵个数了,我们还有求出收服这么多小精灵,皮卡丘消耗的最小体力值,那么就可以遍历找出来。由于我们要找最大个数,那么第一维肯定是拉满,不同在于第二维。因为状态是不大于。
#include
using namespace std;
int dp[1010][510];
int main()
{
int n,m,k;
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=k;i++)
{
int q,s;
scanf("%d%d",&q,&s);
for(int j=n;j>=q;j--)
{
for(int k=m-1;k>=s;k--)
{
dp[j][k]=max(dp[j][k],dp[j-q][k-s]+1);
}
}
}
int res=dp[n][m-1];
cout<=0;i--)
if(dp[n][i]==res) x=min(x,i);
else break;
printf("%d",m-x);
}
ps:这种细节上的处理要注意,就像这题不能取到m一样。
思路:我们有氧气和氮气的最低限制,所以是二维背包问题,然后每个物品只有选与不选两种情况,那么就是01背包。这里给定了需要的氮氧的量, 我们携带的只能多不能少。所以我们要求的不是之前的不超过,也不能定义为恰好,恰好的话数组范围要开的太大了,如果每一瓶一种气体拉满另一种气体只有1,是合法的情况,但是当少的那种气体达到要求时,另一种气体就太多了,
所以这里我们定义dp[i][j]作为氧气体积不小于i,氮气体积不小于j的选法中罐子的总重量,值表示最小值。
那么来考虑状态计算,
当前物品有选与不选两种情况:
不选:dp[i][j]=dp[i][j]//上一层的这个值
选:dp[i][j]=dp[i-a][j-b]+w
不小于与其他情况的区别就是i,j的下限,显然在不超过和恰好等于的两种情况中,i>=a,j>=b,再小就出现负值了,在这两种情况中负值是无意义的。但是在不小于这种情况下,负值是有意义的,不小于0也是不小于-1,所以是有意义的,那么该如何更新呢,数组下标显然不能为负值。我们不小于一个负数,实际上等价于不小于0,用0时的值来更新即可。
对于这个问题我们再深入一点来讨论,比如对于dp[a-3][b-2],现在要放入a,b,那么假设此时已有的氧气和氮气就是a-3和b+m,那么放入后就是2*a-3,2*b-2,按照数组的定义,至少为2*a-3和2*b-2也是至少为a和b,实际上相当于把大于的情况全部转化到这里来。
另外很重要的一点就是这个题的初值处理,除了dp[0][0]以外,其他的值的初值都是无疑义的,因为不可能在一个都没选的情况下就不少于某个值。所以我们在循环更新中判断显然太麻烦,所以我们直接把它们赋成一个不可能被用的值,这里求最小值,我们就把它初始化成正无穷。后面更新的时候根本用不了,只有它们被确切有意义的情况更新的时候才有意义,那个时候也会同时被赋一个更小的值,解除正无穷的状态。
#include
using namespace std;
int dp[30][80];
int main()
{
int n,m,q;
scanf("%d%d%d",&n,&m,&q);
memset(dp,0x3f,sizeof dp);
dp[0][0]=0;
while(q--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
for(int i=n;i>=0;i--)
{
for(int j=m;j>=0;j--)
{
dp[i][j]=min(dp[i][j],dp[max(0,i-a)][max(0,j-b)]+c);
//i<=a,j<=b的这一部分都会用f[0][0]来更新,而这一部分的值不为正无穷了又可以用来更新别的状态
}
printf("\n");
}
}
cout<
ps:截至目前为止,我们遇到的有四种定义集合的方法:
dp[i]以i结尾(一般用于相邻元素之间有关系的情况)
dp[i]不超过i(一般用于给定上限的情况)(这种其实还好,因为一个都不选的话,一定不超过任何一个,所以它们都可以被用来更新别的值)
dp[i]恰好为i(一般用于和为一个确定的值)(这种的边界状态也一定要想清楚,因为无效状态不能调用)
dp[i]不小于i(一般用于给定下限的情况)(这种情况的边界值一定要想清楚,以及它的i可以为负值,只是我们把它转接到0上了而已)
这个实际上就是更改一下几种基本背包问题模型定义的集合的属性,一般都是求最大值,这里改成求数量即可。
就是状态更新需要注意,对于dp[i][j],
选:dp[i][j]=dp[i-1][j-v]
不选:dp[i][j]=dp[i-1][j]
所以这个边界的处理,不太一样,一个都不选可以视为一种方案,对于不同集合定义,初值赋得不同,如定义恰好,则dp[0]=1,其余都为0,如定义不大于那么0,一个都不选的情况下,实际上都为1,但一般求方案数都是求恰好为某个值的方案数。
思路:这题虽然是求方案数,但是每种书有无限本,所以本质上还是完全背包问题
首先,这里有一点值得注意的,n元钱全部用来买书,这里很关键,决定了我们定义的集合的意义。虽然没明说,但是这里默认每本书有无限个,就是完全背包问题。
状态表示,定义dp[i][j]表示从前i个中选,花费恰好是j的方案数。
状态计算,也就是这种书选多少本,不过这里不是求最大值,而是求和。
dp[i,j]=dp[i-1,j]+dp[i-1,j-v]+dp[i-1,j-2v]+...+dp[i-1,j-sv]
dp[i,j-v]= dp[i-1,j-v]+dp[i-1,j-2v]+...+dp[i-1,j-sv]
所以dp[i,j]=dp[i-1,j]+dp[i,j-v]
看第二维使用的是什么,显然是上一层层j位置的值和本层j-v位置的值,那么可以直接优化掉第一维,然后正向访问即可。
另外要注意到边界值的处理问题,这取决于dp[][]边界的意义,如果是求最大值,那么dp[i][0]为0,因为一个都不选的话,价值就是0,但是对于求方案,dp[i][0]需要为1,因为不选也是一种方案。所以优化成一维后,就要给dp[0]一个初值1。另外dp[0][j]的初值取决与我们定义的是恰好还是不超过,恰好的话只有dp[0][0]才有意义,剩下的dp[0][j]因为一个都不选结果只能是0,所以它们都是无效值。不过最重要的还是根据实际情况来确定边界值的处理。
#include
using namespace std;
int dp[1010];
int main()
{
int n;
int a[]={0,10,20,50,100};
scanf("%d",&n);
dp[0]=1;
for(int i=1;i<=4;i++)
{
for(int j=a[i];j<=n;j++)
{
dp[j]+=dp[j-a[i]];
}
}
cout<
思路:我们前面定义的都是费用不超过j时的情况,但是这里要求恰好为m,所以我们的定义实际也得改一下,改成和恰好为j的情况。
那么状态定义就是,dp[i][j]从前i个中选择,和恰好为j的方案数。
状态转移:也是看最后一个,即a[i]选与不选。
不选a[i]:dp[i][j]=dp[i-1][j]
选a[i],那么就要预留出a[i]的位置:dp[i][j]=dp[i-1][j-a[i]]
所以dp[i][j]=dp[i-1][j]+dp[i-1][j-a[i]]。
这个题最关键的地方在于边界值, 因为我们可以发现如果边界值置零的话,按照上式计算,那么整个数组都是0,毫无意义,实际上有两种处理方式,将所有的dp[i][0]全部赋值成1,因为前i个中选结果为0,那么就是一个都不选,实际上也是一种方案。还有一种处理方式:
dp[i][j]=dp[i-1][j];
if(a==j) dp[i][j] += 1;
if(a<=j) dp[i][j] += dp[i-1][j-a];
在恰好为a的时候加上一种方案,这里只选a[i]本身是一种不含在另外两种方案中(如果没有单独处理0的话),实际上这个操作也等价dp[i][j]+=dp[i][0],只是因为没有预处理,所以这里特判一下,实际上都大差不差。
#include
using namespace std;
int dp[10010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
dp[0]=1;
for(int i=1;i<=n;i++)
{
int a;
cin>>a;
for(int j=m;j>=a;j--)
{
dp[j] += dp[j-a];
}
}
cout<
思路:这题比较有趣因为我们可以发现求得不超过某个体积限制下价值最大的方案数。不超过某个体积限制下的最大价值应该定义的是dp[i][j]从前i个中选,体积不超过j的最大价值。但是我们如果定义g[i][j]为从前i个中选,体积不超过j的方案数,那么这里体积从0到j都是不超过j的合法方案,显然不是我们要求的,那么如果定义从前i个中选,体积恰好是j的方案数,那么最大价值的时候对应的确切体积我们也不知道,因为前面求最大价值求得是不超过j的最大价值,所以前面我们需要把定义改成,dp[i][j]从前i个中选,体积恰好是j的最大价值,然后遍历找出最大值,然后再遍历每一个f[],如果它是最大值,那么它的方案就要加进结果中去。这样看似可以,但是要意识到,体积恰好是j有很多方案,并不是每一个方案的结果都是最大价值,所以相当于两个j不是同步的。
那么现在就得换个思路:我们在求体积恰好是j的时候来更新g[]数组,就是判断第i位选不选
我们定义mx=max(f[i-1][j-v]+w,f[i-1][j]);//因为f[i][j]的值只有这两个来源。
如果选就说明f[i-1][j-v]+w==mx,那么方案就是g[i][j]+=g[i-1][j-v]
如果不选就说明f[i-1][j]==mx 那么g[i][j]+=g[i-1][j]
如果与两个都相等就是选不选都可,两个值都要加上,否则就是只能加一个,一定要判断清楚。这里和求具体方案的处理办法有点想,通过判断第i位选与不选的情况来实现。因为本题要求的是实际能产生某个价值的方案数,所以它的限制实际上有两重,一个是体积,一个是价值,如果分开满足,那么体积为某个值的时候价值不一定最大,所以一定要在决定恰为某个体积的最大价值的时候,就记录合法方案数。通过当前位的选与不选来动态更新。
#include
using namespace std;
int f[1010],g[1010];
const int mod=1e9+7;
int main()
{
int n,m;
scanf("%d%d",&n,&m);
memset(f,-0x3f,sizeof f);
f[0]=0;
g[0]=1;
for(int i=1;i<=n;i++)
{
int v,w;
scanf("%d%d",&v,&w);
for(int j=m;j>=v;j--)
{
int mx=max(f[j],f[j-v]+w);
int s=0;
if(mx==f[j]) s=(s+g[j])%mod;
if(mx==f[j-v]+w) s=(s+g[j-v])%mod;
f[j]=mx,g[j]=s;
}
}
int mx=-0x3f3f3f3f,sum=0;
for(int i=0;i<=m;i++) mx=max(mx,f[i]);
for(int i=0;i<=m;i++)
if(mx==f[i])
sum=(sum+g[i])%mod;
cout<
这种题的核心思想就是逆推,逆推最后一个状态的物品选与不选,因为最后一个物品状态的来源是有限的,确定来源的过程就可以确定选与不选。然后往前推即可。
思路:很显然是01背包问题求最大值的解法,关键在于求方案数
方案数求解就是从最后一个状态开始逆推。
最后一个状态的更新有两个来源dp[i-1][j]和dp[i-1][j-v]+w
那么对应的就有三种情况:
最后一个位置必选,必不选,选与不选都可。
选,则dp[i][j]= dp[i-1][j-v]+w
不选则dp[i][j]=dp[i-1][j]
选不选都可,则dp[i][j]与两者都相等。
这里要求字典序最小的方案数,所以我们从前往后确定比较好,前面的如果可选可不选的话,选上的方案字典序更小,刚好可以满足题目要求。
那么背包计算的时候,从后往前即可。
另外我们不能通过记录res来往前推,因为对于选不选都可的位置,res最后为0,j为0,那么很多个状态都符合要求,所以就会出现bug,那么就是直接判断当前状态怎么转化来的。
#include
using namespace std;
int v[1010],w[1010],dp[1010][1010];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&w[i]);
for(int i=n;i>=1;i--)
{
for(int j=0;j<=m;j++)
{
dp[i][j]=dp[i+1][j];
if(v[i]<=j)dp[i][j]=max(dp[i][j],dp[i+1][j-v[i]]+w[i]);
}
}
int j=m;
for(int i=1;i<=n;i++)
{
if(j>=v[i]&&dp[i][j]==dp[i+1][j-v[i]]+w[i])
{
cout<
最简单的就是一点被选的话,另外一个点一定会被选,然后此时求最大价值、方案数什么的,就是有依赖的背包。这个依赖背包问题很容易联想到拓扑排序。而实际上这里比较复杂的情况也是用树形dp来算的。
不过比较简单的就可以转化成分组背包问题,将每一个不依赖于任何点的点及依赖它的点视为一组,然后通过二进制表示状态的方法来表示子元素的选与不选,如果选就将它算处理啊并讨论。一组里面只选一个,就是分组背包问题。
思路:
目前要考虑的问题就是如何分别记录这些值,而且还要区别开每一类。
我们定义一个pair
数组来记录父节点的值 定义vector
>来记录每一个父节点对应的那些子节点
分组背包的讨论显然直接套模板即可,所以问题就是如何得到每一组中的不同情况:
因为每个点都有选与不选两种状态,这里引入二进制,如果那一位是1,那么就表示选,否则就不选,同一组中所有的情况就可以都表示出来了。
比如当前二进制数是b,要判断第k位是否为1,那么b>>k&1即可实现。
#include
using namespace std;
pair r[80];
vector> s[60];
int dp[60][32010];
int main()
{
int n,m;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)
{
int v,p,q;
scanf("%d%d%d",&v,&p,&q);
p*=v;
if(!q) r[i]={v,p};
else s[q].push_back({v,p});
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
for(int k=0;k < 1<>x & 1 )//
{
v += s[i][x].first;
p += s[i][x].second;
}
}
if(v<=j) dp[i][j]=max(dp[i][j],dp[i-1][j-v]+p);
}
}
}
cout<
但是像下面这道题就不能用二进制来压缩,因为状态太多了。
思路:我们对于每一个节点及其子树视为一组来看的话,会发现组内的情况太多了,都讨论2^k太大了,所以我们需要换种方式来划分集合,集合划分的依据是不重不漏:
我们定义dp[u][j]表示以u为根节点,体积不超过j的情况下的价值集合,它的属性是最大价值。
那么我们既然不按每一种情况来划分,就要换种方式划分,这里我们可以按照体积来划分,一个体积可能对应多种方案,我们将同一体积对应的方案视为一类,那么就转化成求以u为节点,体积不超过j的情况下,每一类体积对应的最大价值中的最大值。这里所说的每一类体积是分给子树的体积,所以它的上限是m-v[u](u是根节点,或是说父节点)
结合深搜就能解出来。
以某个点为根,那么这个根节点是必选的,我们先把它的位置预留出来计算可以分给它的子树的每一种不同体积对应的最大价值,最后再把它放进去找最大价值,那么就更新出dp[u][j]的值了。
类比上面,其实也是讨论子树的情况,不过上面是按照子节点选不选分,这里是按给每个子节点分多少位置来划分,实际上也是保证不重不漏的,那么就符合集合划分的要求。
#include
using namespace std;
int h[120],e[120],ne[120],idx;
int v[120],w[120];
int dp[120][120];
int n,m,r;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int s=e[i];
dfs(s);
for(int j=m-v[u];j>=0;j--)//循环体积
{
for(int k=0;k<=j;k++)//分类
{
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[s][k]);
}
}
}
for(int i=m;i>=0;i--)
{
if(i
结语:背包的核心实际就是集合的划分,用集合将状态表示出来,然后进行划分,划分到能算为止。所以只要是能表示能划分的状态,其实都可以算出来,所以它的应用很广泛。
ps:没有什么一定不会的,没有任何人能定义你,包括你自己,永远不要给自己设置上限,别人做不到,不代表你做不到,今天做不到,不代表明天做不到,不要害怕,也不要给自己设置上限。不是鸡血,只是觉得人不是用来被定义的,所有的定义都是用来被打破的。你确实不用向任何人证明自己,但是你要清楚自己想要的到底是什么。