常见优化
单调队列
形式
dp[i]=min{f(k)}
dp[i]=max{f(k)}
要求
f(k)是关于k的函数
k的范围和i有关
转移方法
维护一个单调递增(减)的队列,可以在两头弹出元素,一头压入元素。
队列中维护的是两个值。一个是位置,这和k的范围有关系,另外一个是f(k)的值,这个用来维护单调性,当然如果f(k)的值可以利用dp值在O(1)的时间内计算出来的话队列中可以只维护一个表示位置的变量。
枚举到一个i的时候,首先判断队首元素的位置是否已经不满足k的范围了,如果不满足就将队首元素弹出队列。
将新的f(k)的值入队的时候要注意维护队列中元素f(k)值的单调性,这时候判断新入队的元素时候破坏单调性,如果破坏了,就弹出队尾的元素,直到不会破坏单调性为止。
注意要点
主要要注意的是队列中维护单调性的标准是针对f(k)的值,而不是dp值,所以在新的元素入队的时候要判断f(k)值的大小而不是dp值的大小。
此外要注意每次在优化的那一维之前清空队列,而不是在全局情况下清空队列。
单调队列的O(nlogn)优化
形式
dp[i]=max{dp[k]+cost[k+1,i]}
要求
cost[k+,i]不具有可以在O(1)时间内计算出来的性质,比如区间最大值等情况。
决策点集k对应的某个值a[k]是具有单调性的。
转移方法
由于a[k]具有单调性,那么我们维护单调队列的依据就是a[k]的值。
那么剩下的去全部决策点最值的工作就可以利用一个平衡树来维护。
注意要点
有的时候队列cost的值和队列中的两个元素有关系,那么我们要首先先加入一个元素到队列中,注意这时候我们不将其加入平衡树,弹出的时候也是一样的,如果队列仅剩一个元素了,那么我们就不对平衡树做操作。
四边形不等式
形式
dp[i][j]=min{dp[i-1][k]+cost[k+1,i]}
dp[i][j]=max{dp[i-1][k]+cost[k+1,i]}
要求
cost函数满足四边形不等式,也就是两边之和小于等于中间的之和
cost[i][j]+cost[i'][j']<=cost[i'][j]+cost[i][j']
转移方法
定义s[i][j]表示得到dp[i][j]的时候对应的最优策略,那么三重for循环,第一重枚举i,第二重枚举j的时候从大到小,第三重枚举k的时候,k是有一个上下界的,下界是s[i-1][j],上界就是s[i][j+1],注意如果j=n的时候,上界是第二维的大小。
注意要点
只有cost函数满足四边形不等式的时候才能作此优化,否则不能得到正确的结果。
斜率优化
形式
dp[i]=min{dp[k]+cost[k,i]}
dp[i]=max{dp[k]+cost[k,i]}
要求
cost[k,i]是决策单调的,定义如下:
假设k是dp[i]的决策,那么定义w[k,i]表示k对应的决策值,也就是w[k,i]=dp[k]+cost[k,i]。
我们需要证明的是对于k1<k2<i如果有w[k1,i]>w[k2,i](如果dp值是取max,那么就相反),就说明k2比k1更优,并且对于任何的i'>i都有w[k1,i']>w[k2,i']。这说明了k1这个决策对于i之后的元素没有任何贡献就可以删去。
这时候我们做一个变形,将w[k1,i]和w[k2,i]中关于i的放到一边,剩下的放到另外一边,那么可以整理出像x[i]<g[k1,k2]/h[k1,k2]这种形式,由于这个式子是等价于w[k1,i]<w[k2,i]的,说明这时候k1比k2更优。我们
我们设s[k1,k2]=g[k1,k2]/h[k1,k2],那么有当x[i]<s[k1,k2]的时候k1比k2更优,如果x[i]>=s[k1,k2]的时候k2更优,可以舍弃k1了。
转移方法
我们维护一个决策点的队列,k0<k1<k2….,并且维护其单调性,满足s[k0,k1]<….<s[kn-1,kn]。
左边元素出队的条件,如果有s[k0,k1]<x[i],那么说明了k1比k0来的优,那么我们就舍弃k0,直到队列中全部的点对满足x[i]<s[k0,k1]<s[k1,k2]<…<s[kn-1,kn]。那么我们的dp[i]就可以从k0转移过来。
右边元素入队条件,要保证s的单调性。
以上是取min的例子,取max的类似,也可以得到一个决策单调的队列。
注意要点
决策单调性是用来维护队列使用的方法,而不是恒等式,不需要做证明,我们需要做的仅仅是对其变形,变形成g/h的形式。
我们一般使用k1<k2证明w[k1,i]<w[k2,i](取max的话相反)的方式来得到队列中需要满足的单调性是递增的还是递减的。
解题报告
POJ1163
最基础的dp,直接记录状态
dp[i][j]表示以第i行第j列开头的三角形的最大值
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+a[i][j];
从后向前递推
POJ1579
直接记忆化搜索或者迭代都可以
POJ2081
直接模拟,用bool数组判重
POJ1953
直接dp
记录dp[i][j]表示以j为结尾长度为i的串的方法数
dp[i][0]=dp[i-1][0]+dp[i-1][1];
dp[i][1]=dp[i-1][0];
POJ1458
经典的LCS问题
直接记录状态dp[i][j]表示第一个串前i个和第二个串前j个的LCS长度
如果a[i]=b[j]那么dp[i][j]=dp[i-1][j-1]+1
否则dp[i][j]=max(dp[i-1][j],dp[i][j-1])
POJ2250
和上题类似,加了记录路径的过程,用算法导论中的经典记录方式即可,附图参考
a |
b |
c |
f |
b |
a |
|||
0 |
1 |
2 |
3 |
4 |
5 |
6 |
||
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
|
a |
1 |
0 |
1 |
1 |
1 |
1 |
1 |
1 |
b |
2 |
0 |
1 |
2 |
2 |
2 |
2 |
2 |
f |
3 |
0 |
1 |
2 |
2 |
3 |
3 |
3 |
c |
4 |
0 |
1 |
2 |
3 |
3 |
3 |
3 |
a |
5 |
0 |
1 |
2 |
3 |
3 |
3 |
4 |
POJ1159
对于一个串s1,先求其反串s2,求出s1和s2的LCS就是答案。
POJ1080
利用一个矩阵来帮助dp转移
可以直接用dp[i][j]表示第一个串前i个和第二个串前j个的最大值,转移利用矩阵可以比较轻松,注意边界条件。
POJ2192
开一个bool型的dp数组,dp[i][j]表示A的前i个元素和B的前i个元素能否构成s[i+j]这个串,状态定义下来后转移就很简单了。
如果dp[i][j-1]&&b[j]==s[i+j]那么dp[i][j]=true
如果dp[i-1][j]&&a[i]==s[i+j]那么dp[i][j]=true
否则dp[i][j]=false
POJ3356
类似LCS,但是不要想太复杂,否则反而可能做不出来。
定义dp[i][j]表示A的前i个和B的前j个配对最少要多少步骤。
如果a[i]=b[j],那么显而易见
dp[i][j]=min(dp[i-1][j-1],dp[i-1][j]+1,dp[i][j-1]+1)
否则
dp[i][j]=min(dp[i-1][j-1]+1,dp[i][j-1]+1,dp[i-1][j]+1)
POJ1887
从这题可以联想到经典的LIS优化,通过单调性,可以用二分在O(nlogn)的时间内求出一个序列的LIS。
思路如下:
我们定义d[k]=min{a[i]}, dp[i]=k
这样我们可以保证d数组时单调递增的,每次二分查找小于a[i]的LIS长度最大的那个位置,那么dp[i]就是等于这个位置pos了。
另外写完了程序,你会发现实际上dp数组是没有用处的,你只要记录一个ret表示最大LIS长度就行了,最后ret就是答案。
POJ2533
裸的LIS,可以用O(n^2)的方法过,记录dp[i]表示以a[i]结尾的LIS长度,转移方程如下:
dp[i]=max{dp[k]}+1, a[k]<a[i]
POJ1631
LIS的O(nlogn)算法实践。
POJ1157
很简单的dp
直接记录dp[i][j]表示考虑到第i行选择第j个元素时候的元素和最大值,那么有如下转移
如果j<i那么明显,这个格子不能被选择到,这样dp[i][j]=-oo
否则dp[i][j]=max{dp[i-1][k]}+a[i][j],k<j
注意当i=0的时候dp[i][j]=a[i][j]
POJ1088
记忆化搜索+dp
直接对每个格子进行dfs,转移的话向附近四个格子中的比当前格子低的格子转移。
POJ1050
基础题,首先对一维的进行解答,对于一个一维序列a,我们要求他的最大连续子序列和,那么有如下的dp转移方程:
dp[i]表示以i结尾的最大连续子序列和,那么就有dp[i]=max(dp[i-1],0)+a[i]
这样取dp数组中最大的一个即可,复杂度是O(n)的。
拓展到二维的情况,在x方向用一维的方法在O(m)的时间内求得解,在y方向在O(nm)的时间内枚举全部的行,这样复杂度是O(nm^2)的。
注意一个技巧,也就是用sum[i][j]表示第j列的前i行的序列和,这样才能保证枚举是O(nm)的。
POJ1014
经典背包问题,有六样物品,背包容量是他们权值之和的一半,然后直接记录一个bool型的dp数组,dp[i]为true表示这个容量是可达的,最后判断背包容量是否可达即可。
POJ1160
HDU1227
经典dp问题,记录w[i][j]表示在i到j范围内建立一个邮局管理i到j这个区间的全部村庄,这些村庄到邮局的距离之和,那么再记录dp[i][j]表示已经有i个村庄,管理范围是1到j这个范围的全部村庄的结果。
那么有如下的转移方程:
dp[i][j]=min{dp[i-1][k]}+w[k+1][j],其中i-1<=k<j
特别地,对i=1有dp[i][j]=w[i][j]。
POJ1125
这道题考察Floyd,虽然是图论,但是也算是经典dp了,题意是很多人传播谣言,问你谁的谣言传播到了全部人而且速度最快。
建图就直接n个点连有向边,跑Floyd,如果一个人的谣言可以传播到全部人,也就是到其他点的距离都不是无穷大,那么就记到达的最大距离为这个点的值,然后在全部的人中取最小的一个即可。
POJ1179
经典的表达式加括号问题
首先O(n)枚举切断的边,然后拉成一个表达式,这样可以得到一个长度为n的序列
设dp[i][j]表示第i到第j这一段序列的最大值,那么有
当i=j时dp[i][j]=a[i]
否则dp[i][j]=max(a[i] op dp[i+1][j], dp[i][j-1] op a[j])
O(n^2)的状态,O(1)的转移
总的复杂度O(n^3)
POJ1036
滚动数组的运用,先抛开滚动数组,直接定义
dp[i][j]表示时间为i并且门的状态为j的时候可以得到的最大价值,转移方程:
dp[i][j]=max(dp[i-1][j-1],dp[i-1][j],dp[i-1][j+1])+Σp
p表示i时刻来并且s值等于j的人的p值。总的复杂度O(T*K)
注意:初始化的时候dp[0][0]=0其他dp[0][k]=-1,-1表示状态不可达,转移的时候也要注意那三个状态是否可达。
POJ1276
背包问题+二进制优化
直接将每个物品的数量按照二进制规则拆分,然后进行0-1背包,然后找到最大金额就可以了。
注意直接定义dp[i]是否可达就可以,不用记录最大值。
POJ1837
可以算是背包问题了,定义
dp[i][j]表示放上前i个砝码的时候力矩和为j的方案数目,注意j有可能是负的,所以要增加一个delta。估计一下范围25*15*20=7500,所以全部增加7500。
转移方程,从前推后好写:dp[i][j+w[i]*pos[k]]+=dp[i-1][j]
当然,这题可以做一个小的优化,记录一个left和right表示可达的力矩范围,然后枚举这个范围,会快很多。
USACO3.2.3
Stringsobits,按位dp。这题如果是考察长度为N有L个1的串的个数的话,就是很简单的一道题目了,现在要你做的事情,就是把第k大的数打出来而已。
记录dp[i][j]表示长度为i并且有j个1的串的个数,那么我们就可以很简单的得到转移方程。
dp[i][j]=dp[i-1][j]+dp[i-1][j-1]分别表示第i个数是0或者1的情况,注意当i或者j等于0的时候,dp[i][j]=1。
这样我们得到了dp值之后,就是一个统计的过程了,首先将k自减1,统计前k个数,i从n枚举到1,如果长度为i-1包含L个1的串的个数小等于k,那么就打印一个1出来,并且k减去dp[i-1][L],L自减1,否则打印一个0。
通过这样数位统计的方式可以快速地得到解。注意dp数组的两重for循环赋值,第一维从0到n-1,因为长度为n的不会被统计,第二维从0到L,注意不能是从0到i,否则会出错,dp[1][1]需要用到dp[0][1],这里j>i的。
数位统计的关键代码如下:
while(n--){
if(k&&dp[n][l]<=k){
putchar('1');
k-=dp[n][l];
l--;
}else{
putchar('0');
}
}
puts("");
POJ1946
两次dp,注意题目的意思,我们可以这样理解,每头牛都是一样的地位,那么我们就规定每次交换领队都是从前向后的,也就是说,1号带队先跑一段距离,然后2号带队,以此类推。
这样我们的dp状态就很定义了,定义dp[i][j]表示第i头牛带队的时候跑了j分钟可以达到的最大圈数,这样我们转移的时候直接枚举第二维即可。
dp[i][j]=max{dp[i-1][k]}+dis[j-k][e-dp[i-1][k]]其中i-1<=k<j
对此转移方程的理解:第i头牛带队跑了j-k的时间,花费的能量是dp[i-1][k],注意这里除了带队的花费是距离的平方以外其他的都是距离值。而这里dis[i][j]表示前i分钟剩余j能量时领队可以跑出的最大距离。
dis数组也可以通过dp来计算。
dis[i][j]=max{dis[i-1][j-k*k]+k}注意这里k*k<=j
理解:dis[i][j]表示前i分钟可以跑过的距离,那么一分钟跑k圈,花费能量为k*k,那么能量就降低k*k,距离增加k。
FZU1894
单调队列入门。
在一个双端队列中维护一个单调递减的序列(因为要求最大值),然后有如下操作:
入队:判断队尾元素是否是小于入队元素,如果是,那么要将其出队,这样才能保持单调递减的序列,如果不是,就不管,然后将元素加入到队尾,注意记录id,说明这个元素是第几个元素。
出队:判断队首id是否和我们实际上需要出队的元素的id相等,如果不等就不管,如果相等,就弹出。
查询最大值:直接输出队首元素即可。
注意这题题面中说如果队列元素为空,就输出-1,所以我们记录另外一个值n表示现在真实的队列中有多少个元素。
POJ2823
典型的单调队列问题,和上题一样,维护两个队列,一个记录最大值,它是单调递减的,一个记录最小值,它是单调递增的。然后直接O(n)的跑就可以。
POJ的数据有点问题,加外挂可以进1000ms,否则G++会TLE,C++跑差不多5800ms左右。
HDU3415
枚举sum[i]表示前i个元素的和,注意这里要将n扩展到2*n为了循环。
然后枚举每一个sum[i],找到i之前k个元素[i-k,j]区间内的sum最小值sum[k],sum[i]-sum[k]就是最优解,然后取全局最优解即可。
注意一点在队列中在处理队列之前就要先将i-k-1号元素删除,我们要在队列维护最多k+1个元素,然后去最值,最后才将sum[i]入队。
复杂度O(n)。
HDU3449
背包问题,分n个盒子,每个盒子都有一个价值,那么我们就分n类dp。
设dp[0][j]表示容量为j的时候得到的最大价值,dp[1][j]枚举到的这个背包容量为j的时候的最大价值,注意初始化为负无穷。转移:
dp[1][j]可以从dp[0][j-w[i][j]-p[i]]+v[i][j]转移,也可以通过dp[1][j-w[i][j]]+v[i][j]转移,前者表示没买盒子,后者表示已经买过盒子,注意就是转移的时候要先写后者,防止出现p[i]=0的情况。
注意这题用max函数可能会TLE,判断大小用if语句再赋值会快很多。
最后的答案是max{dp[0][k]}。
HDU3474
单调队列,为了要使得元素中C大于J,那么我们令C为1,J为-1,说明每一个长度为N的区间,都要有区间前i个元素和的值大于等于0。所以我们要从左向右枚举N个区间,求出它前面N个元素和的最小值是否比第它前面第N个元素来的小即可。
换个说法,也就是对于任意一个切口,之后的i个元素sum[i]>=0,所以min{sum[i]}>=0。可以通过一个宽度为N的轮滑得到这个最小的和值,同HDU3415的做法。
需要对序列前后各扫描一次,得到的位置做上标记,最后O(n)的检查每个位置是否可行。注意统计的时候为了防止错位,下标从1开始会更好地编码。还有就是在第一个元素进入队列之前要先让0进入队列。
POJ1742
典型的单调队列优化dp。
对于n类物品,某类物品的数量为k,价值为v,容量为w。
那么考虑到这个种类的物品的时候有如下dp方程:
设dp[i]表示容量为i的时候得到的最大价值,那么我们就有:dp[i]=max{dp[i-k*w]+k*v}
换一种写法,dp[mod+k*w]=max{dp[mod+j*w]+(k-j)*v}
这里为了方便,我们用s[j]表示dp[mod+j*w],所以就有dp[mod+k*w]=max{s[j]+(k-j)*v}=max{s[j]-j*v}+k*v这里k已经是定值,也就是说dp[mod+k*w]只和一定范围内的s[j]-j*v的最大值有关,可以用单调队列优化。
总的复杂度是O(N*V)的。
这道题比较简单,由于价值都是1的,我们就直接开一个bool型数组存储是否存在解就可以了。然后枚举到的k如果dp[k]是可达的,那么我们就将其id入队,否则就不管,另外由于价值一样,所以不要记录价值。
此类背包问题还可以进一步地优化,如果k等于1的时候直接写0-1背包,如果物品可以取得的总容量大于背包总容量,那么就直接写完全背包,因为这两种写法和单调队列相比常数小了非常多。
最后考虑到很大的输入输出量,选择C++编译器。
HDU3535
有条件限制的背包问题,主要分三类。定义dp[i][j]表示到第i组物品容量为j的时候的最大价值,那么有如下的状态转移方程:
第一类,至少要买一个,那么有:
dp[i][j]=max(dp[i][j],dp[i][j-c]+v,dp[i-1][j-c]+v)
注意枚举物品的时候看枚举的状态是否是可达的。之前将全部初始化为负无穷。
注意这里要写成if语句,那么要先判断能否从dp[1][k-c]转移,再判断能否从dp[0][k-c]转移,如果相反了,会出现如果c等于0,那么就多算了一个v。
第二类,至多买一个,那么有:
dp[i][j]=max(dp[i][j],dp[i-1][j-c]+v)
这里比较好理解
第三类,没有限制,这里直接跑0-1背包就可以了
dp[i][j]=max(dp[i][j],dp[i][j-c]+v),这里初始化dp[i][k]=dp[i-1][k]。
POJ3250
有很多的方法过这题,以下介绍三种方法,主要思路是将序列反转(其实不反转也没什么),然后去找第i个元素左边靠最右的比这个元素大的位置j,那么i-j+1就是第i个元素对应的解。
第一种方法:利用线段树动态更新,完成上部分的查询,复杂度O(nlogn)。
第二种方法:利用一个单调栈,栈内维护一个递减的区间,记录在此元素之前比其大的元素序列的下标。新的元素入队,如果栈顶元素大于这个入队元素,那么结果就加上这个元素的下标减去栈顶元素下标再减去1,否则栈顶元素出栈,以此类推。算法复杂度是O(n)的。
第三种方法:利用并查集的思路直接做。记录r[i]表示第i个元素可以到达的最右边的下标,那么每次利用下面的方式压缩路径:
r[i]=i;
while(h[i]>h[r[i]+1])r[i]=r[r[i]+1];
算法复杂度是O(n*α(n))的。
HDU2571
在地图上的动态规划,算是很简单的一道题目了,注意这题的要求,已经给定了终点和起点,那么就一定要注意状态的定义和转移,定义dp[i][j]表示到达(i,j)这个位置的时候得到的最大幸运值,那么就有三种转移方式dp[i][j]=max(dp[i-1][j],dp[i][j-1],dp[i][k]),其中如果i-1为0就没有dp[i-1][j]这个转移,这里一定要记得不能通过对dp[i][0]和dp[0][j]赋值为0然后直接写这个方程的方式转移,这样不能保证会过起点,所以要分情况讨论。对于第三项,k是j的因子,也就是说j%k等于0,这里枚举k只要枚举到k*2<=j即可,会省下比较多的时间。
HDU2159
二维多重背包,定义dp[i][j]表示用去的忍耐度为i杀怪数目为j的时候获得的最大经验值,两重for循环都是从小到大枚举的,之后从小到大枚举i,得到最小的i使得do[i][j]>=n成立,这时候m-i就是解,否则输出-1。
HDU2577
定义dp[i][j]表示到第j个字母,大小写状态为i(0表示小写1表示大写)剩下要多少步数,针对第j个字母的大小写状态转移,初始化dp[0][n]=0,dp[1][n]=1用来保证最后大小写开关是关着的。
HDU2845
分别对横竖都求一次不连续的子字段和就可以。
设dp[i]表示以第i个元素结尾的不连续子字段之和,则有dp[i]=max{dp[k]}+a[i],其中0<=k<i-1,利用一个变量ma来动态维护(0,i-2)区间内的最大值即可,注意ma初始化要为0,最后返回dp数组的最大值,如果最大值小于0就返回0。
本题要注意n*m<=200000,所以我们只要开一个200000的数组来记录每一行的值,读取一行计算一次,放入另外一个ans数组中,最后对ans(相当于行)再求一次不连续的子字段和即可。时间复杂度O(nm)。
HDU1978
记忆化搜索加动态规划,对到达的某个点(i,j),我们枚举他可以到达的点(ii,jj),那么就有dp[i][j]+=d[[ii][jj],注意要保证两个点的Manhattan距离要小等于mp[i][j]。时间复杂度O(n^2*max(d))。
HDU1160
对结构体求LIS即可,注意先按照重量排序,在转移的时候判断只有重量严格大于并且速度严格小于才能进行转移。时间复杂度O(n^2)。
HDU1025
典型的LIS的O(nlogn)优化,先将两个变量任意一个进行排序,另外一个求LIS即可。
注意输出单复数。
POJ2373
单调队列优化的dp。首先我们先进行预处理,将可以合并的区间合并到一起,这个可以在O(nlogn)的时间内完成。方法是按照x排序,然后找相邻的两个区间(a,b)和(c,d)是否满足a<d并且b>c,注意这里必须严格大于才行,因为这里的区间都是开区间,如果存在b==c这样的情况,那么b这个点就可以分割。
然后进行动态规划转移,令dp[i]为前i个区间可以划分的最小区间数目,那么就有:
dp[i]=min{dp[i-2*k]}+1, A<=k<=B,如果不存在这样的k值,或者i是在某个区间内,那么dp[i]就为oo,注意初始化dp[0]=1, dp[1]=oo。
运用类似多重背包的方法将i划分成2的一个剩余类,也就是说我们可以对上式进行变形,变成如下的形式:
dp[mod+2*j]=min{dp[mod+2*k]}+1, A<=j-k<=B,这里dp[mod+2*j]是个仅关于j的函数,可以用单调队列维护一定区间的最值,最后问题得到解决。
注意这题的一些细节,首先当L为奇数的时候dp[L]与dp[1]属于同一个剩余类,那么dp[L]就为oo,所以直接可以输出-1。其次,对于判断i是否在区间内,可以用一个指针p动态向后移动的方式来判断,如果第p个区间的右边界小等于i,那么p就右移,知道p等于n或者右边界大于i为止,这时候判断第p个区间的左边界是否比i小,如果左边界小于i,那么dp[i]就是oo。最后,如果dp[L]等于oo记得输出-1。
这里还有一个很好的技巧,在队列中加一个哨兵oo,它的id也为oo,这样可以处理如果i<A的情况,当然也可以直接预处理出对所有的i<A都有dp[i]=oo,注意这里枚举和0的同剩余类的元素要从2开始循环!
HDU1024
记dp[i][j]表示有i组,以第j个元素结尾的最大子字段和,那么显然当i=1的时候就按照一维的来算:dp[1][1]=s[1],其他情况dp[1][j]=max(0,dp[1][j-1])+s[j]。
当i>1的时候考虑到有两种选择,一种是从第i-1组的dp[i-1][i-1]到dp[i-1][j]这个范围内的任意一个转移,另外一种就是从dp[i][j-1]转移,所以有dp[i][j]=max{dp[i][k],dp[i][j-1]}+s[j],其中i-1<=k<j。
最后的答案必须是max{dp[m][k]}注意k的范围是m<=k<=n。
这样裸的复杂度是O(mn^2)的明显超时,在转移过程中进行优化,记录ma表示dp[i-1][i-1]到dp[i-1][j-1]的最大值,直接O(1)的得到max{dp[i-1][k]}的值。另外要使用滚动数组来防止空间复杂度过高,这样空间复杂度从O(nm)降至O(n)。优化后的时间复杂度是O(nm)的。
HDU1074
状态压缩dp加记忆化搜索。记录dp[st]表示到达st这个状态的时候的最少扣分。
dp[st]=min{dp[k]}其中k状态可以到达st状态,记录一个tp数组,tp[st]表示到达st这个状态时需要花费的时间,这个数组在O(2^n*n)的时间内预处理出来。
另外还要有两个辅助数组pre和id,pre[st]表示状态st的前一个状态,id[st]表示完成st状态的最后一个科目的id,注意由于要输出最小字典序解,所以字符串从大到小排列(为了dfs的时候反过来输出是最小字典序)。
HDU1078
很囧的题目没看懂,实际上就是给你一个迷宫,每次只能向权值较高的地方走,走的限制是每次只能朝四个方向的其中一个走至多k步,问你可以得到的最大价值。
明显的dp加记忆化搜索。直接定义dp[i][j]表示以(i,j)这个格子为起点能得到的最大价值,注意由于每次只能走向权值较大的点,相当于是一个DAG,肯定不存在回路的,所以直接转移即可。
HDU1158
定义dp[i][j]表示第i个月有j个人的时候的最小费用,这样dp[i][j]就可以通过dp[i-1][k]中的有效状态转移过来,dp[i][j]=min{dp[i][k]+cost}+j*salary。关于初始化,对全部i>=1,将dp[0][i]设为oo表示不可达的状态,这样我们就能保证第一个月是通过dp[0][0]转移来的。最后结果取dp[n][k](k>=c[n])的最小值。
这里为了防止无上界现象,我们去max{c[i]}为第二维的上界,可以很容易证明对于k>max{c[i]}的dp[i][k]肯定不会是最优解,也不会是最优子问题。
HDU1165
这题不能用记忆化搜索或者迭代,因为你并不知道n可以到多大,题目只说了dp[i][j]的值会在某个范围内,所以我们进行公式推导。
m=0时dp[m][n]=n+1
m=1时,由于dp[m][0]=dp[m-1][1]=2,并且dp[m][n]=dp[m-1][dp[m][n-1]]=dp[m][n-1]+1,所以有dp[m][n]=n+2
m=2时,dp[m][0]=dp[m-1][1]=3,并且dp[m][n]=dp[m-1][dp[m][n-1]]=dp[m][n-1]+2,所以有dp[m][n]=2*n+3
m=3时,dp[m][0]=dp[m-1][1]=5,并且dp[m][n]=dp[m-1][dp[m][n-1]]=2*dp[m][n-1]+3,所以有dp[m][n]=8*2^n-3
CDOJ1303
POJ1160的加强版,首先我们要做的事情是对在(i,j)区间建立邮局的最小距离和进行优化,其实这个计算可以在O(n^2)内完成。利用下列的推导即可:
let k = (i+j)/2
w[i][j]=(p[k]-p[i])+(p[k]-p[i+1])+...+(p[k]-p[k-1])+(p[k+1]-p[k])+...+(p[j]-p[k])
=p[k]*(k-i)-p[k]*(j-k)+(p[k+1]+p[k+2]+...+p[j])-(p[i]+p[i+1]+...+p[k-1])
=p[k]*(2*k-i-j)+(sum[j]-sum[k])-(sum[k-1]-sum[i-1])
这样我们就得到了关于sum的一个式子,这里的sum[i]表示从1到i的地点的坐标和。
然后我们观察状态转移的式子
dp[i][j]=min{dp[i-1][k]+w[k+1][j]}
这里我们可以证明对任意的i'和j’,如果i<i'<j’<j的话,就有w[i'][j’]+w[i][j]<=w[i'][j]+w[i][j’],符合四边形不等式的关系式,所以我们可以优化转移方程,在O(N*P)的时间内完成这个dp过程,记录s[i][j]表示取得最优解的dp[i][j]的k值,那么对于状态转移,我们只要枚举所有符合s[i-1][j]<=k<=s[i][j+1]的k即可。注意这里用到s[i][j+1],所以第二重for循环从后向前循环,当j=n时,枚举所有符合s[i-1][j]<=k<=n的k值。当dp[i][j]被dp[i-1][k]更新时,令s[i][j]=k即可。
详细可以参考代码。
HDU1428
图论加dp,首先用spfa预处理出最短路,然后记忆化搜索dp。只要下一个结点到目的地的距离小于该点到目的地的距离那么就转移。
CDOJ1367
类似树状数组的方法,记录dp[i][j]表示从1,1到i,j的元素的和,那么有:
dp[i][j]+dp[i-1][j-1]-dp[i-1][j]-dp[i][j-1]=f[i][j],其中f[i][j]表示i,j这点是否有东西。
移项得:dp[i][j]=f[i][j]+dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1],然后递推即可。
对于每个查询,我们枚举全部的左上角,看一个区域A,B内元素和是否为0即可,元素和依然用树状数组的类似方法计算。
HDU1505 POJ1964
类似HDU1506的题目,首先先预处理出每个格子向上可以达到的最大高度,这个可以用dp实现,如果这个格子是R那么dp[i][j]=0,否则dp[i][j]=dp[i-1]j]。
然后枚举每一行,把这一行当做最下方的一行,对这个模型运用类似HDU1506的压缩路径的方式在O(n)的时间内处理。总的复杂度是O(n^2)的。
HDU1422
单调队列优化的dp。首先先定义dp[i]表示以i结束的旅程的最大长度,如果第i个城市的生活费加上之前剩下的生活费之和大于在第i个城市的花费时,显然有dp[i]=dp[i-1]+1
现在利用单调队列,维护一个宽度为n的窗口,记录花费前k项和在一段区间的最小值,再记录一个f指针表示我们的起点,当到达i这个城市的时候钱不够时,也就是说这个区间的最小值与前f项和的差值小于0的时候,我们把f自增。这样可以在O(n)的时间内完成dp的转移。
CDOJ1214
二分加dp,首先二分答案,然后每次用dp判断是否可行。
设dp[i]表示前i个元素符合每一堆的元素和不超过mid的条件下,需要多少堆。容易想到转移dp[i]=max{dp[k]}+1,其中k<i并且sum[i]-sum[k]<=mid。这样这题就可以在O(n^2logn)的时间内解决了。本题难点就在于如何定义状态。
POJ1015
这题dp主要是定义状态,如果定义错了的话很可能不满足无后效性。
错误的定义方式:定义p[i][j]表示考虑到前i个元素,已经选择j个的,达到最优解时的p值之和。d[i][j]也如此定义。用pre[i][j]记录路径。以上的方法的错误的。这样定义状态不满足无后效性,完全有可能后面的解是从前面的非最优解转移过来。
正确的方法是记录dp[i][j]表示已经选择了i个元素,p和d的差值为j的时候p与d之和的最大值,另外加两个辅助数组,pre[i][j]表示取得dp[i][j]的时候最后取得的元素id,s[i]表示第i个元素的p值与d值的差。
状态转移方程(从前向后推):dp[i+1][j+s[k]]=max(dp[i+1][j+s[k]],dp[i][j]+p[k]+q[k]),如果可以转移,那么就更新pre[i+1][j+s[k]]=k。
注意转移的之前要判断路径上是否已经有id为k这个元素,如果有,就不能转移,所以要写一个check函数判断。
最后根据pre数组得到路径,假设最优解是st,那么路径上最后一个元素就是pre[m][st],之前一个元素就是pre[m-1][st-s[pre[m][st]]],以此类推。在记录路径的同时对各个元素的p值和d值求和。
一般遇到差值最小、差值的绝对值最小这类问题,我们可以直接把差值当做一维状态来dp,注意要设置offset参数,防止数组越界。
此外还要注意这道题我们dp的顺序,只有把m放在for循环的第一维,我们才能保证我们的路径不会被覆盖。
POJ1141
经典dp,括号匹配问题,直接定义dp[i][j]表示从i到j这个范围最少需要添加多少括号。转移分四类:
第一类是s[i]和s[j]能配对上,也就是说s[ij]=()或者[]的时候,dp[i][j]=min(dp[i][j],dp[i+1][j-1])
第二类是s[i]是左括号的时候,我们可以在最右边加括号匹配,dp[i][j]=min(dp[i][j],dp[i+1][j]+1)
第三类和第二类对称,s[j]是右括号的时候,dp[i][j]=min(dp[i][j],dp[i][j-1]+1)
最后考虑将序列分成两个部分,枚举得到dp[i][j]=min{dp[i][k]+dp[k+1][j]}
利用一个数组ans来记录这个最优解的来源,负数表示前面三种情况,整数表示最后一种情况的分界点。
这题利用记忆化搜索会很方便。时间复杂度是O(n^3)的。
HDU1423 CDOJ1300
LCIS,经典问题,记录dp[i][j]表示前i个A的元素和前j个B的元素构成的LCIS长度,限制以Ai结尾,而B不做限制。
转移方程dp[i][j]=max{dp[k][j-1]}+1,k<i且a[k]=b[j]。这样定义状态时间复杂度是O(n^3)的,可以做优化,对第二维先进行循环,再对第一维循环,记录一指针p表示对所有的a[k]<b[j],dp[p][j-1]的值最大。这样边更新dp值,p指针边后移,这样转移就是dp[i][j]=dp[p][j-1]+1。
HDU1300
先理清题意,本题的意思是低等级的珍珠可以用高等级的代替,但是必须全部代替,这样我们就可以直接寻求最优策略。
设dp[i]表示前i个物品需要的花费,那么显然有dp[i]=min{dp[k]+cost[k+1][i]},注意这里的cost[k+1][i]的计算方法即可。
HDU1502
定义dp[i][j][k]表示A有i个,B有j个,C有k个的时候的方案数,那么对于i<j或者j<k或者i<k的情况,dp[i][j][k]=0。
否则dp[i][j][k]=dp[i-1][j][k]+dp[i][j-1][k]+dp[i][j][k-1]。
由于会超long long所以用Java写大数。
HDU3530
维护两个单调队列,一个维护最小值,一个维护最大值,每次只要判断最大值减去最小值的值是否大于k,如果大于k那么就移动f指针,否则不移动,注意记录一个t变量表示起点,初始化t为-1。在处理完队列之后我们查看最大值减去最小值是否大于m,如果大于m就统计长度,否则不更新。
HDU1244
记录dp[i][j]表示以第i个元素结尾,有j组的时候的最大和,那么有dp[i][j]=max{dp[k][j-1]}+sum[i-l[j]+1][i],其中k+l[j]<=i,那么这样的转移是O(n)的,为了优化,记录s[i][j]表示s[0][j]到s[i][j]中的最大dp值,那么可以在O(1)的时间内做到转移,最后可以得到一个转移方程:
dp[i][j]=s[i][j-1]+sum[i-l[j]+][i]。sum数组可以用前n项和得到。
HDU1243
LCS的变形,直接记录数组sc表示用某个字母击中对手得到的积分,那么当a[i]=b[j]的时候,dp[i][j]=dp[i-1][j-1]+sc[a[i]-‘a’]。其余转移和LCS一样。
HDU2870
最大完全子矩阵,枚举a、b、c然后运用类似HDU1506的方法来求出最优解。
CF47-D
首先二分半径,看至少有k个被摧毁的概率是否大于1-e,如果大于,就减小半径,否则增加半径,至少k个建筑被摧毁的概率计算方法如下:
记录dp[i][j]表示前i个有j个被摧毁的概率,那么有dp[i][j]=dp[i-1][j-1]*p[i]+dp[i-1][j]*(1-p[i])。
POJ1037
组合dp,考虑如下的问题:
在一个n个数的全排列中找到第k个排列,我们的做法是从前向后扫,比如我们要找到4个数的第10个排列,首先我们尝试第一个数,如果第一个数是1,那么剩下的3个数的排列有3!=6种,然而6<10,这说明了第一个数比1大,我们尝试2,剩下的三个数的排列依然是6个,这时候我们想,2134这个排列应该是排在第1+6=7位,这说明,我们如果以2开头,我们要找的是剩下的第10-6=4位排列,而三个数的全排列是6,比4来的大,所以第一位就是2,我们要做的就是找到由1、3、4三个组成的第4个排列,这样我们就可以用一个dfs来递推执行。
这道题目难度加大了一些,考虑的是“波浪形“的序列,那么我们利用dp记录dp[i][j][0]和dp[i][j][1]表示长度为i的以j开头的序列,并且这个数比上一个数来的大(或者小)的时候,可能的方法数,这时候我们可以写出转移方程:
dp[i][j][0]=Σdp[i-1][k][1]其中1<=k<j
dp[i][j][1]=Σdp[i-1][k][0]其中j<=k <i
这样我们考虑到一个数的时候我们就直接通过累计的方式计算出这一位是是剩余的数字中的第几大。
注意一下就是统计第一位的时候有可能有两种情况,一种是第一个数大于第二个数,另外一种是第一个数小于第二个数,注意一下前面一种情况的排列一定是小于后面一种情况,所以求和的时候先加上dp[n][i][0],如果不成立则加上dp[n][i][1],以此类推。
POJ3624
裸的0-1背包,直接做。
POJ3628
利用bool型标记每个高度是否可达,最后枚举即可。
POJ2184
记录dp[i]表示s的值是i的时候最大的f值,最后直接统计就可以了,这题注意offset的利用,还有就是不能直接使用这个数组,上界是2000n,offset是1000n,如果使用整个数组,会TLE。注意当s的值是正的和负的时候for循环的方向不一样。
POJ2264
LCS构造解,开一个数组s[i][j]表示取dp[i][j]的时候的方向,然后构造出LCS,之后按照a串优先的方式打印出最后的结果。
POJ2955
典型的O(n^3)的复杂度dp。定义dp[i][j]表示区间[i,j]中的最大长度,那么如果s[i]=s[j]那么dp[i][j]可以由dp[i+1][j-1]+2转移,如果s[i]是左括号那么dp[i][j]可以由dp[i+1][j]转移,如果s[j]是右括号,那么dp[i][j]可以由dp[i][j-1]转移。最后dp[i][j]可以由dp[i][k]+dp[k+1][j]转移。
POJ1745
dp,定义dp[i][j]表示前i个数是否可以组成模k等于j的结果,直接转移就可以。
最后判断dp[n-1][0]是否是true即可。
POJ3494
最大子矩阵,直接做即可,和city game一样的做法。
POJ2033
定义dp[i]表示以第i个数结尾的结果数目,直接转移就可以了,注意如果相邻的两个数都是0,那么答案是0,如果开头的数是0,答案也是0。
转移的时候按照s[i]是不是0分开考虑,注意数组大小,开1200就WA了,开120000过的。
POJ2704
记忆化搜索,直接定义dp[i][j]表示到达格子(i,j)的方法数,通过同一行靠左边或者同一列靠上边的格子转移。
POJ2696
直接记忆化搜索即可,注意一下dp[i]的值是非负的,如果小于0,就要加一个模数。
POJ2342
经典树形dp,直接定义dp[i][0]表示第i个结点为根,并且第i个结点不是参加人员的时候获得的最大rating,定义dp[i][1]表示第i个结点为根,并且第i个结点是参加人员的时候获得的最大rating,那么显而易见dp[i][0]=Σmax(dp[k][0],dp[k][1]),dp[i][1]=Σdp[k][0],其中k是i的孩子结点。
使用记忆化搜索,注意dp数组的初始化。
NOI2005瑰丽华尔兹
单调队列优化的dp,记录dp[k][i][j]表示在第k这阶段在(i,j)这个位置的时候滑过的最长距离,那么以向下为例,那么有dp[k][i][j]=max{dp[k][i'][j]+i-i'},这里的枚举一维可以用单调队列优化。
这题有几点比较重要的地方,首先是最大值函数,不是dp值,而是dp值与一个关于i或者j的变量的和,我们维护队列的时候要维护的也是这个值,而不是dp值。此外,在每次枚举一维之前要初始化队列,注意是在枚举的那一维之前,而不是全局枚举之前。
NOI练习 生产产品
单调队列dp,记录dp[i][j]表示第i个生产线生产到第j个阶段的时候花费的最小时间,那么j从1开始i计数,定义dp[i][0]=-K。转移方程dp[i][j]=min{dp[k][j']+t[i][j]-t[i][j']+K},其中j-l<=j'<=j-1,k<>i。注意这里由于是j'<=j-1,所以新的dp值不能优先入队。
注意t[i][j]表示第i个生产线生产前j个阶段花费的时间,这里可以用单调队列优化,不过优化方法非常的特殊,我们先枚举j,再枚举i和k,对每一对i和k都开一个单调队列,然后优化即可,注意f和b指针的维护。
HDU3401
单调队列优化dp,定义dp[i][j]表示第i天手上有j份股票的时候的赚到的钱的数量,那么可以到达dp[i][j]的路径有dp[i-1][j]和dp[k][j'],其中0<=k<i-w,j'是可达的全部状态,也就是如果j'<j,那么j-j'<=as[i],如果j'>j,那么j'-j<=bs[i]。
那么以买入为例,dp[i][j]=max{dp[k][j']-(j-j')*ap[i]}。
注意这里,由于dp[i][j]可以由dp[i-1][j]转移过来,所以显而易见有dp[i][j]>=dp[i-1][j],所以对于第一维,我们可以优化成k=i-w-1。对于第二维,利用单调队列就可以达到效果。
HDU2993
斜率优化入门题,注意一下这题数据可能过多,加了外挂才过的。
方法是维护一个下凸的图形,首先构造函数
f(i,j)=(s[i]-s[j-1])/(i-(j-1))
然后根据函数构造一系列的点P(i,s[i]),然后维护斜率队列,注意一点,判断凸性用叉积判断,队列的b指针维护凸性,f指针维护一个最优值。
用一个图可以很清楚的看出来,如果f+1所在的点对应的斜率比f对应的大的话,f就对以后的全部点都没有作用了,所以我们可以进行f++操作。这样最多一进一出,可以在O(n)的时间内得到结果。
这里为了方便,我使用的是点队列。
POJ3017
经典dp,利用单调队列加平衡树。
首先定义状态,定义dp[i]表示以i结尾的最小值,那么有转移方程dp[i]=min{dp[k]+max{a[k+1,…,i]}},其中sum[k+1,…,i]<=m。
注意k的范围,k的上界是i-1,下界可以利用一个指针lim来向后移动,注意每个元素都是正数,那么下界一定是单调递增的。
接下来我们要处理k的值,注意满足条件的k一定是a[k]>=max{a[k+1,…,i]}的,这样我们维护一个单调递减的队列,维护的标准是对应的a值单调递减,队列中还要维护一个pos和一个val,val是函数值,我们令val=dp[q[x].pos]+a[i],注意这里是a[i]是因为max{a[k+1,…,i]}一定是a[i],这里正是利用了单调队列的性质来维护的。
注意队列中a值单调的,但是val值并非是单调的,那么我们要维护一个平衡树来维护val的值,然后每次取一个最小值作为dp值的解。
平衡树中比较好写的就是sbt了,利用sbt可以比较快的完成这个任务。
最后要注意一下,我们先将pos=1的元素先入队一次,这样的话如果队列只有一个元素的时候,由于没有dp[q[x].pos]+a[q[x+1].pos]这种写法,那么这时候我们就不管这个,因为这时候sbt中一定是没有元素的。
NOI练习 锯木厂选址
斜率优化dp,首先我们先做如下的定义:
sumd[i]表示第i颗树到第1颗树的距离
sumw[i]表示前i颗数的总重量
c[i]表示在第i个位置建立第一个锯木厂,将前i颗树送到这个锯木厂花费的钱
w[i,j]表示在第i个位置建立第二个锯木厂,将j+1颗树到第j颗树都送到这个锯木厂花费的钱
dp[i]表示在第i个位置建立第二个锯木厂,花费的最低总价钱
明显有
sumd[i]=Σd[k],1<=k<=i-1
sumw[i]=Σw[k],1<=k<=i
c[i]=c[i-1]+sumw[i-1]*d[i-1]
w[i,j]=c[i]-c[j]-sumw[j]*(sumd[i]-sumd[j])
dp[i]=min{c[k]+w[i,k]+w[n+1,i]}
注意我们定义n+1位置有一颗树,d[n+1]=0并且w[n+1]=0。
我们定义t[k,i]=c[k]+w[i,k]+w[n+1,i]
根据单调性定义我们对k1<k2<i,计算t[k1,i]-t[k2,i]的值,计算出对应的g和h,维护单调性即可。
NOI练习 玩具装箱
斜率优化dp,我们定义dp[i]表示前i个玩具打包需要的最少价值,定义s[i]=Σc[k]+i 1<=k<=i,其中s[0]=0,那么明显有递推式s[i]=s[i-1]+c[i]+1。
接下来我们可以写出dp的转移方程dp[i]=min{dp[k]+(s[i]-(s[k]+1)-L)^2},利用单调性直接找到对应的斜率就可以了。
注意dp方程不要写错了,减去的是s[k]+1而不是s[k]。
HDU3507
斜率优化dp,定义s[i]表示前i个元素的c[i]之和, dp[i]表示前i个元素打印需要的花费,那么有dp[i]=min{dp[k]+(s[i]-s[k])^2}+M。
以上的式子满足决策单调,直接利用斜率优化来处理,可以在O(n)的时间内解决。
HDU2829
斜率优化dp,定义dp[i][j]表示使用了j个炸弹,前i个元素的最小值,cost[i][j]表示从i到j这些元素的乘积和,那么有
cost[1][i]=cost[1][k]+cost[k+1][i]+Σc[j]*(s[i]-s[k])=cost[1][k]+cost[k+1][i]+s[k]*(s[i]-s[k])
所以cost[k+1][i]=-cost[1][k]+cost[1][i]+sqr(s[k])-s[k]*s[i]
dp转移方程dp[i][j]=min{dp[k][j-1]+cost[k+1][i]}其中cost的公式已经给出。
最后我们只要维护一个c[i]表示cost[1][i]即可,这时候我们换一个公式c[i]=c[i-1]+s[i-1]*val[i]。
剩下的问题就利用m个单调队列就可以搞定。
这题也可以用用四边形不等式在O(n^2)的时间内转移。四边形不等式就定义dp[i][j]表示有i个炸弹前j个元素的最小值,直接转移即可。
HDU1561
树形dp,定义dp[i][j]表示选择以i为根的结点,有j个课程被选择到的时候得到的最大学分,那么有dp[i][j]=dp[k1][k]+dp[k2][j-k-1]+val[i]。
注意k1和k2表示二叉树的两个孩子结点,val表示第i个课程的学分。
最后的答案是dp[0][m+1],注意由于选择了第0个课程,所以要多加一个1。
这题运用到了多叉树转化二叉树的方法,记录二叉树的左孩子是多叉树的孩子结点,右孩子是多叉树结点的兄弟,记录last变量表示该结点的最后一个孩子。那么每次连接边(u,v)的时候我们查看last值是否是-1,如果是,那么son和last都为v,否则设置last的bro为v,u的last为v。
ZOJ3463
定义dp[i][j][k]表示到第i个音符,左手在j,右手在k的时候花费的最小能量,注意双手不能重叠并且有有一只手可以碰到第i个音符。否则dp[i][j][k]=-1。
这题的意思是大拇指到小拇指方向的9个按键都可以按到,理解了好久,o(╯□╰)o。。
之后取dp[n-1]的最小值就可以了。