一、背包
1.01背包
设f(i,j)为只考虑前i个物品的前提下,每个物品只能拿一次,最多用j容量能获得的最大价值。
显然,f(0,0),f(0,1),f(0,2)...f(0,m)的值为0。
假设已知f(i-1,0),f(i-1,1)...f(i-1,V),考虑如何求出f(i,j)(0≤j≤V)。 f(i,j)和f(i-1,j)的区别是f(i,j)还可以考虑第i个物品。 也就是说,f(i,j)只会根据取与不取第i个物品从之前的状态转移过来: ①不拿第i个物品最优:f(i-1,j) ②拿第i个物品下最优:f(i-1,j-w[i])+v[i](需要保证j>=w[i]) 综上所述,f(i,j)=max(f(i-1,j-w[i])+v[i],f(i-1,j))
那么,以f(0,0),f(0,1),f(0,2)...f(0,V)为基础,我们可以依次 求出f(i,j)(1≤i≤N,0≤j≤V)。
f(N,V)就是我们要求的答案。

2.完全背包
状态表示: f[i][j]表示只考虑前i个物品的前提下,每个物品能拿任意次,最多用j容量能获得的最大价值。
状态转移方程: f[i][j]=max( f[i-1][j],f[i][j-w[i]]+v[i] ) (j>=w[i])

优化总结
01背包: 时间复杂度O(N*V),空间复杂度O(V)

完全背包: 时间复杂度O(N*V),空间复杂度O(V)

3.多重背包
有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。
可以用二进制拆分的方法,依次按照1,2,4,8,..来拆分,直到不足时剩下的来当最后一份。
举个例子:
拆分8(1000b):
第一次拆一份大小为20的,剩余7(111b)
第二次拆一份大小为21的,剩余5(101b)
第三次拆一份大小为22的,剩余1(1b)
第四次由于1<23而结束算法,直接将剩下的1个为1份。
根据算法可知,我们最终拆分出来的大小分别为2^0,2^1,..2^k,x。

二、状压DP
状压DP是利用计算机二进制的性质来描述状态的一种DP方式。
举个例子,假如我们现在有3个资源,那么我们可以通过二进制的表示方法表示出这3个资源的状态。对于二进制数011B,可以表示出第1个、第2个资源被占用且第3个资源未被占用的状态(编号一般从低位开始算)。
hdu5418 Victor and World
题意:有n个城市,在n个城市之间有m条双向路,每条路有一个距离,现在问从1号城市去游览其它的2到n号城市最后回到1号城市的最短路径(保证1可以直接或间接到达2到n)。(n<=16)
分析:考虑O(2^n * n^2)的做法
先通过floyed算法O(N^3)求出mat[][]数组,mat[i][j]表示第i个城市到第j个城市的最短距离。
通过二进制数表示每个城市的访问情况,1为已经访问,0表示还没访问,比如011B就表示第1、2城市已经访问,第3个城市还未访问。(即状压,以后提到的状态都是指一个二进制数,意义同上)
dp[i][s]表示,访问状态为s,而且最后一个访问的城市是i号城市的最短距离。
那么转移方程为:dp[ j ][ s|(1<<(j-1)) ]=min(dp[ j ][ s|(1<<(j-1)) ],dp[ i ][ s ]+mat[i][j])
(笔者注:第一维 j 的意思是当前检视的新城市为j,第二维度 s 的意义是之前已经储存的状态s,即到检视j城市为止所保存的以前的访问状态,| 的意思是任意有一个即为访问,所以用来统计检视 j 之前的城市的访问状态和新城市访问状态,(1<<(j-1))即为2^(j-1),即新城市 j 在状态压缩操作中所占的第几位,j-1是因为第一位为2^0)
物理意义:访问完i号城市以后继续去访问j号城市
初始化: memset(dp,0x3f,sizeof(dp)); dp[1][1]=0; //因为起始位置在1号城市
求得dp[][]数组的具体代码
memset(dp,0x3f,sizeof(dp)); //初始化无穷大
dp[1][1]=0;//起始状态赋值为0
for(int s=1;s<=(1<<n)-1;++s){ //从起始状态开始往上枚举
for(int i=1;i<=n;++i){ //枚举当前状态的最后一个城市
if( !(s&(1<<(i-1))) ) continue; //对应的位如果不是1,产生矛盾,continue
for(int j=1;j<=n;++j){ //枚举下一个要去的城市
if( s&(1<<(j-1)) ) continue;//对应的位是1,已经访问过,continue
// ( s|(1<<j-1) )是新的状态
dp[j][s|(1<<(j-1))]=min(dp[j][s|(1<<(j-1))],dp[i][s]+mat[i][j]);
}
}
}
因为最后还需要返回1号城市,所以最后的答案为:
ans=0x3f3f3f3f;
for(int i=1;i<=n;++i) ans=min(ans,dp[i][(1<<n)-1]+mat[i][1]);
(笔者注:枚举以第i个城市为最后检视的终点,这个dp距离+从i到1的mat距离即为答案。(1<<n)-1的意思是所有状态全为1的圆满)
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 20
int cas; int n; int m;
int mat[N][N]; int dp[N][1<<N];
int main(){
scanf("%d",&cas);
while(cas--){
memset(mat,0x3f,sizeof(mat));
scanf("%d %d",&n,&m);
for(int i=1;i<=n;++i) mat[i][i]=0;
for(int i=1;i<=m;++i){
int u; int v; int w;
scanf("%d %d %d",&u,&v,&w);
mat[u][v]=mat[v][u]=min(mat[u][v],w);
}
for(int k=1;k<=n;++k){
for(int i=1;i<=n;++i){
for(int j=1;j<=n;++j){
mat[i][j]=min(mat[i][j],mat[i][k]+mat[k][j]);
}
}
} //floyed 求 mat[][]
memset(dp,0x3f,sizeof(dp)); dp[1][1]=0;
for(int s=1;s<=(1<<n)-1;++s){
for(int i=1;i<=n;++i){
if( !(s&(1<<(i-1))) ) continue;
for(int j=1;j<=n;++j){
if( s&(1<<(j-1)) ) continue;
dp[j][s|(1<<(j-1))]=min(dp[j][s|(1<<(j-1))],dp[i][s]+mat[i][j]);
}
}
}
int ans=0x3f3f3f3f;
for(int i=1;i<=n;++i) ans=min(ans,dp[i][(1<<n)-1]+mat[i][1]);
printf("%d\n",ans);
}
return 0;
}
洛谷P1896 互不侵犯
题意:在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。 ( 1 <=N <=9, 0 <= K <= N * N)
分析:考虑O( N^3 * (1<<N)^2 )的做法
行内限制:一行的状态不能有两个相邻的1,否则会侵犯,举俩个状态为例子 (1)110B (×)因为存在两个相邻的1 (2)101B (√)不存在相邻的1
行间限制:设i行的状态为si,令j=i-1(即j和i是上下两行),设j行的状态为sj,易知有以下限制: (1)si&sj=0 (2)(si>>1)&sj=0 (3)(si<<1)&sj=0 (4)(sj>>1)&si=0 (5)(sj<<1)&si=0 否则会相互侵犯。
由于题目还限制了最终国王的个数,所以我们的dp数组还需要腾出一维来记录当前国王的个数。 所以,开设dp[N][N*N*2][1<<N]数组,dp[i][j][s]的意义为:从上往下放置国王(i越来越大)时,放置完第i行的时候(第i+1行还没开始放),第i行的状态为s,且当前已经放置了j个国王的方案数。 状态转移方程: 在满足全部限制下,dp[ i ][ j+num[s2] ][ s2 ] += dp[ i-1 ][ j ][ s1 ] 其中(1)num[x]为x在二进制表示下1的个数。(2)s2是第i行的状态,s1是第i-1行的状态。 初始化: memset(dp,0,sizeof(dp)); dp[0][0][0]=1;
#include<iostream>
#include<cstdio>
using namespace std;
int n,k,num;
long long cnt[2000],ok[2000];
//cnt[i]:第i种状态的二进制中有几个1
//ok[i]:第i个行内不相矛盾(满足条件2:左右国王不相邻)的状态是多少
long long dp[10][100][2000];
//dp[i][j][s]:我们已经放了i行,用了j个国王,第i行的状态为s的方案数
int main()
{
cin>>n>>k; //n*n的棋盘上放k个国王
for(int s=0;s<(1<<n);s++) //枚举所有可能状态
{
int tot=0,s1=s; //tot:二进制下有多少个1;
while(s1) //一位一位枚举,直至为0(做法类似于快速幂那样)
{
if(s1&1) tot++; //如果最后一位是1,tot++
s1>>=1; //右移看下一位
}
cnt[s]=tot; //预处理这个二进制数有多少个1
if((((s<<1)|(s>>1))&s)==0) ok[++num]=s; //如果这一状态向左向右都没有重复的话,说明左右不相邻,合法
}
dp[0][0][0]=1; //第0行一个也不放的方案数
for(int i=1;i<=n;i++) //枚举我们已经放到了第几行
{
for(int l=1;l<=num;l++) //枚举第i行的状态,这里我们直接枚举所有满足条件2的状态,算是个优化吧
{
int s1=ok[l];
for(int r=1;r<=num;r++) //枚举上一行的状态
{
int s2=ok[r];
if(((s2|(s2<<1)|(s2>>1))&s1)==0) //如果上下,左下右上,左上右下方向都不相邻,合法
{
for(int j=0;j<=k;j++) //枚举国王个数
if(j-cnt[s1]>=0)
dp[i][j][s1]+=dp[i-1][j-cnt[s1]][s2]; //状态转移方程
}
}
}
}
long long ans=0;
for(int i=1;i<=num;i++) ans+=dp[n][k][ok[i]]; //枚举第n行所有可能的情况,统计答案
printf("%lld\n",ans); //输出
return 0;
}
三、单调队列优化DP
当dp方程为dp[ i ]=a[ i ]+b[ j ]时,这个方程是O(n^2)的。 这时可以用单调队列可以将其优化为O(n)。
洛谷P2627 修剪草坪
题意:Farm John有N(1 <= N <= 100,000)只排成一排的奶牛,编号为1...N。每只奶牛的效率是不同的,奶牛i的效率为E_i(0 <= E_i <= 1,000,000,000)。 靠近的奶牛们很熟悉,因此,如果Farm John安排超过K只连续的奶牛,那么,这些奶牛就会罢工去开派对:)。因此,现在Farm John需要你的帮助,计算FJ可以得到的最大效率,并且该方案中没有连续的超过K只奶牛。 A<=B<=10^12。
补充一个知识点: 对于一个数组a[],如果我们要O(1)查询a[l] ,a[l+1],...,a[r-1],a[r]的和,那么我们可以预处理一个前缀和数组sum[]。 对于sum数组,sum[i]=a[1]+a[2]+a[3]+...+a[i-1]+a[i]。 那么对于上述问题,就可以通过sum[r]-sum[l-1]在O(1)的时间内得到。
dp[1][i] 表示在前 i 头奶牛中,选了第 i 头奶牛能获得的最大效率。 dp[0][i] 表示在前 i 头奶牛中,不选第 i 头奶牛能获得的最大效率。 转移方程:
dp[0][i]=max(dp[0][i-1],dp[1][i-1])
dp[1][i]=max(dp[0][j]+ (sum[i]-sum[j]) ) ( j<i&&i-j<=k )<=>( max(i-k,0)<=j<i ) 最后的答案是max(dp[1][i])
(注:dp[1][i]=max(dp[0][j]+ (sum[i]-sum[j]) )为枚举j,从0到j不工作,然后从j+1到i工作;枚举过程中选择最大的)
最后的答案是max(dp[1][i]) (注:答案不是dp[1][n])
按照朴素的方法来写一发,以下是代码。
#include <cstdio>
#include <algorithm>
using namespace std;
#define N 100005
#define ll long long
int n; int k; ll a[N]; ll dp[2][N]; ll sum[N];
int main(){
scanf("%d %d",&n,&k);
for(int i=1;i<=n;++i) scanf("%lld",a+i);
for(int i=1;i<=n;++i) sum[i]=sum[i-1]+a[i];
for(int i=1;i<=n;++i){
dp[0][i]=max(dp[0][i-1],dp[1][i-1]);
for(int j=max(0,i-k);j<i;++j) dp[1][i]=max(dp[1][i],dp[0][j]+sum[i]-sum[j]);
}
ll ans=0;
for(int i=1;i<=n;++i) ans=max(ans,dp[1][i]);
printf("%lld\n",ans);
return 0;
}
不出所料,会tle,要用到队列知识优化
我们看转移方程:
dp[0][i]=max(dp[0][i-1],dp[1][i-1]) (1)
dp[1][i]=max(dp[0][ j ]+ (sum[i]-sum[ j ]) ) max(i-k,0)<=j<i (2)
(1)式的时间复杂度是O(1)的。 (2)式的时间复杂度是O(N)的。 所以我们主要对(2)式进行优化。 对(2)式变化一下: dp[1][i]=sum[i]+max(dp[0][ j ]-sum[ j ]) max(i-k,0)<=j<i (2) 发现在上述表达式中sum[j]是预处理出来的,是已知的。 dp[0][j]由于j<i,所以在求dp[1][i]的时候dp[0][ j ]也已经求出来了,也是已知的。 那么我们能不能考虑,在求出dp[0][ j ]的时候来维护一个最大的dp[0][ j ]-sum[ j ]供后面的dp[1][i]使用? 当然是可以的,我们可以用一个单调递减队列来维护。
可能有人会有疑问,既然要维护一个最大的dp[0][j]-sum[j]供后面的dp[1][i]使用,直接开max变量来维护最大值不就好了吗,为什么特地去用一个单调递减队列维护呢?
我们再看一下转移方程: dp[1][i]=sum[i]+max(dp[0][ j ]-sum[ j ]) max(i-k,0)<=j<i (2)
对于限制max(i-k,0)<=j<i 如果变成 j<i 的话完全是可以只开一个max变量来维护的。
但是这里还有max(i-k,0)<=j这个限制 ,也就意味着随着i变大 使得max(i-k)>j 从而使原本最优的决策 j 无法被使用,也就是说最优的决策 j 随着时间的推移(这里表现为i的增加)不会一直在候选队列中,会因为某些限制被“踢出去”,而原本次优的决策变成了最优的决策。
综上所述,我们只维护一个最优的决策是不行的,第二优、第三优...的决策都可以等到前面比它们优的决策从队首出队以后,自己来成为队列中最优的决策。
同时,如果一个决策后面有一个比它更优的决策,那么这个决策就永远不会成为队列中最优的决策,因为后面会一直存在一个比它更优的决策,所以这个决策继续呆在队列中等待是没有意义的,因为它永远不会成为最优的,所以我们可以把它去掉。
最后,这个队列就变成了一个单调递减队列(在这道题中一个决策j 越优秀表现在dp[0][ j ]+sum[ j ]越大)。
单调递减队列(队列中存的是下标)维护过程
(1)保证队首元素可以使用:while(tail>head&&i-k>q[head]) ++head; 主要就是控制队首元素对应的下标 j 满足max(i-k,0)<=j<i 这个条件
(2)dp[ 1 ][ i ]=sum[i] + dp[ 0 ][ q[head] ] - sum[ q[head] ](因为队首是最优的决策,这样可以保证dp[ 1 ][ i ]是最大的。)
(3) 下标i代表的决策进队的同时维护队列单调递减的性质
while(tail>head&&dp[0][i]-sum[i]>=dp[0][q[tail-1]]-sum[q[tail-1]]) --tail; q[tail++]=i;
注意在我的写法中q[head]是队列首元素,q[tail-1]才是队列尾元素(而不是q[tail])
#include <cstdio>
#include <algorithm>
using namespace std;
#define N 100005
#define ll long long
int n; int k; ll a[N]; ll dp[2][N]; ll sum[N]; ll q[N]; int head; int tail;
int main(){
scanf("%d %d",&n,&k);
for(int i=1;i<=n;++i) scanf("%lld",a+i);
for(int i=1;i<=n;++i) sum[i]=sum[i-1]+a[i];
q[tail++]=0;//先把0放入优先队列
for(int i=1;i<=n;++i){
while(tail>head&&i-k>q[head]) ++head;
dp[0][i]=max(dp[0][i-1],dp[1][i-1]);
dp[1][i]=dp[0][q[head]]+sum[i]-sum[q[head]];
while(tail>head&&dp[0][i]-sum[i]>=dp[0][q[tail-1]]-sum[q[tail-1]]) --tail;
q[tail++]=i;
}
ll ans=0;
for(int i=1;i<=n;++i) ans=max(ans,dp[1][i]);
printf("%lld\n",ans);
return 0;
}
四、斜率优化DP
dp方程为dp[ i ]=a[ i ]*b[ j ]+c[ i ]+d[ j ]时,这个方程是O(n^2)的。 这时可以用斜率优化可以将其优化为O(n)。
洛谷P3195 [HNOI2008]玩具装箱TOY




#include <cstdio>
#include <algorithm>
using namespace std;
#define N 50005
#define ll long long
#define a(x) (sum[x]+x)
#define b(x) (sum[x]+x+1+l)
#define c(x) (a(x)*a(x))
#define d(x) (dp[x]+b(x)*b(x))
ll n; ll l; ll c[N]; ll sum[N]; int q[N]; int head; int tail;
ll dp[N];
double slope(int j1,int j2){
return (d(j2)-d(j1))/( b(j2)-b(j1)==0?1e-9:b(j2)-b(j1) );
}
int main(){
scanf("%lld %lld",&n,&l);
for(int i=1;i<=n;++i) scanf("%lld",c+i);
for(int i=1;i<=n;++i) sum[i]=sum[i-1]+c[i];
q[tail++]=0;
for(int i=1;i<=n;++i){
while(tail-head>=2&&2*a(i)>=slope(q[head],q[head+1]) ) ++head;
dp[i]=-2*a(i)*b(q[head])+c(i)+d(q[head]);
while(tail-head>=2&&slope(q[tail-1],i)<=slope(q[tail-2],q[tail-1]) ) --tail;
q[tail++]=i;
}
printf("%lld\n",dp[n]);
return 0;
}
总结
(1)单调队列优化适应dp[ i ]=a[ i ]+b[ j ]; 斜率优化适应dp[ i ]=a[ i ]*b[ j ]+c[ i ]+d[ j ]。
(2)单调队列优化主要维护单点递减/递增; 斜率优化主要维护两点间斜率单调递减/递增。
495

被折叠的 条评论
为什么被折叠?



