动态规划题目
- 动态规划求和问题
- 背包问题
- 带修改动态规划
- 一些合成问题
- 动态规划去后效性
- 决策单调性
- 线性DP的优化
- 箱子堆叠问题
- 数位DP
- 一些离散状态的DP
- DP与拓扑序
- 整数和浮点数DP
- 状压DP和排列
- 区间DP
- 分治+FFT
- 树上卷积式DP
- 参考资料
动态规划求和问题
考虑有这样一个n×m的矩阵M,1≤n,m≤3000。矩阵M代表一个迷宫,Mij为1表示该处不可通行,为0表示该处可通行。玩家只能每次向右或向下移动一格。
迷宫的出口和入口信息现在还不确定,每个为0的位置都有可能为入口和出口。
记f(i,j,u,v)表示入口为(i,j),出口为(u,v)时,有多少可能的从入口到出口的轨迹,现在我们希望求:
∑Mi,j=0∑Mu,v=0f(i,j,u,v)一般的动态规划每趟可以给出O(nm)的时间复杂度,但是由于总共要执行O(nm)次,因此总的时间复杂度将达到O(n2m2)。
我们考虑进行优化,实际上观察正常的动态规划递推公式:
dp(i,j)=dp(i−1,j)+dp(i,j−1)我们可以发现,对于不管入口和出口怎么变换,递推关系都是不会改变的。并且还有最重要的一点,就是递推关系是线性的。
现在我们定义dpi,j为当我们选择(i,j)作为入口的动态规划公式。这时候我们再定义一个新的函数:
g=∑Mi,j=0dpi,j那么我们实际上要求的是:
∑Mi,j=0∑Mu,v=0f(i,j,u,v)=∑Mi,j=0∑Mu,v=0dpi,j(u,v)=∑Mu,v=0∑Mi,j=0dpi,j(u,v)=∑Mu,v=0g(u,v)下面我们证明g也满足递推关系g(i,j)=g(i−1,j)+g(i,j−1)。
g(i,j)=∑Mi,j=0dpi,j(i,j)=∑Mi,j=0[dpi,j(i−1,j)+dpi,j(i,j−1)]=∑Mi,j=0dpi,j(i−1,j)+∑Mi,j=0dpi,j(i,j−1)=g(i−1,j)+g(i,j−1)新的算法的时间复杂度为O(nm)。
所以以后遇到这种多个动态规划,但是每个动态规划除了初始状态不同外,递推关系一致,并且我们最终要对其进行求和的问题,我们都可以用这种方式进行加速。
背包问题
01背包
一般的背包问题定义是这样的。给定n个正整数A=a1,…,an,以及另外一个正整数,判断是否存在一个A的子集S,子集中的所有数之和恰好等于某个给定的整数m。
我们可以定义一个简单的DP,记f(i,j)表示前i个数是否存在一个子集之和等于j。那么我们要求的就是f(n,m)。这个函数的转移非常简单f(i,j)=f(i−1,j)∨f(i−1,j−ai)。因此时间复杂度为O(nm)。
但是这里我们可以利用bitset技巧,将时间复杂度优化到O(132nm)。
完全背包
如果01背包中提到的每个数ai都可以使用无穷次,问是否存在一个A的多重集合S,集合中的所有数之和恰好等于某个给定的整数m。
我们可以定义这样一个简单的DP,记f(i,j)表示前i个数中是否存在一个多重子集之和等于j。我们要求的就是f(n,m),且我们可以推出递推公式:f(i,j)=f(i−1,j)∨f(i,j−ai)。这里我们O(nm)求解这个问题
多重背包
如果01背包中提到的每个数ai可以使用最多bi次,问是否存在一个A的多重集合S,集合中的所有数之和恰好等于某个给定的整数m。
很容易想到一个O(nm2)的做法。但是我们会发现DP公式f(i,j)的值始终是布尔值,这实在有些浪费,我们可以为每个状态都额外携带一些信息(这实际上是非常重要的DP设计的技巧)。我们定义一个新的DP公式g(i,j)表示仅考虑前i个元素,至少需要在前i个元素的多重子集中出现多少次ai,才能得到总和为j。这样我们就发现了一个转移,如果g(i−1,j)≤bi−1,那么g(i,j)=0,否则g(i,j)=g(i,j−bi)+1。这样一来,我们的DP时间复杂度就优化到了O(nm)。
有了上面提到的技术,实际上我们可以优化一类01背包问题。记C=∑iai,我们可以发现A序列中最多只有√C个不同的值,因此我们可以将相同值合并得到一个多重背包问题,这样我们就可以在O(m√C)时间复杂度内求解了。如果C=O(n),那么时间复杂度可以优化为O(m√n)。
其实上面提到的多重背包公式,泛用性并不是很强,因为DP公式的函数值被用来记录一个无用信息,这个信息只是帮助我们实现O(nm)时间复杂度的。比如说我们现在修改一下问题,要求求出所有多重集合中,和正好为m的包含最少元素的多重集合大小,就会发现上面提到的DP公式就失效了。下面介绍一种更加泛用的多重背包解决方案。
记录h(i,j)表示仅考虑前i个A中的数,总和为j至少需要选择多少元素。这样我们在考虑第i个元素的时候,可以根据对于模ai余数不同的j分成不同的组进行计算,这样我们会发现,问题就变成了有两个数组x(对应h(i−1,?))和y(对应h(i,?)),其中yi=i+mini−bi−1≤j≤i(xj−j)。我们实际上可以维护一个单调队列来解决这个问题。
提供一道题目。
超大背包
题目1:有重量为1到8的8类物品,种类为i的物品总共有ai个。要求找到一组系数b1,…,bn,满足0≤bi≤ai,且∑8i=1ibi≤m,且要求∑8i=1ibi最大。其中0≤ai≤1016,1≤m≤1018。
提供一道题目。
这个题目很明显是一个背包,但是背包的容量超大,没法直接DP。
但是我们发现1到8这8个数的最大公倍数仅为840。这意味着对于最优结果,第i个数被选择了ci个,那么ci=840i⋅ai+bi,其中bi<840i。
我们发现取840/i个i的和都是840,我们可以把这些值做一致处理,简单称为单元。考虑不由单元组成的部分∑8i=1ici,其一定不超过840⋅8,也就是说我们可以对这一部分进行DP,dp(i)表示取一些数总和为i的前提下,最多能保留多少单元。显然单元越多能组成的数范围越大。
最后暴力枚举结果中不能由单元提供的额外部分,同时判断最优解即可。
题目2:有n个物品,第i个物品的重量为wi,价值为vi,数量无限。有一个承重为m的背包,要求计算背包最多能装下多少价值的物品。其中1≤wi≤103=W,1≤n≤106,0≤vi≤109,1≤m≤1018,且保证结果范围不超过1018。
提供一道题目。
可以发现实际上最多考虑W个商品,因为相同重量的物品只需要考虑价值最大的商品。
我们可以记录dp(i)表示大小为i的背包最多能携带价值。一般的dp公式是dp(i)=maxnj=1dp(i−wj)+vj,这样做的时间复杂度是O(nm)。
有一种优化的方式,就是我们找一个性价比vkwk最高的物品k,我们知道其余选中物品的某个子集中重量和一旦能整除wk,我们可以将它们全部替换为物品k,得到更高的价值。因此我们知道其余物品中的任意子集的重量和不能整除wk。
但是我们知道x个整数中a1,…,ax,必定有个子集的和能整除x。假设不存在这样的子集,记Si表示前i个整数的和,则S1,…,Sx中每个数的值均落在1到x−1之间。根据鸽巢定理可知,一定有两个数相同,即Si=Sj,其中i<j。因此我们可以发现ai+1,…,ax的和Sj−Si能整除x。
因此我们发现除了物品k外其余物品最多选择wk−1个,这样时间复杂度就达到了O(W3),还是慢了。
这里讲一个天秀的做法。
我们使用第一种做法中提到的dp,但是转移不同。具体的转移如下:
- 背包中什么都不放
- 背包中放一个商品
- 背包中放至少两个商品。这时候我们可以将背包分成两个背包的和,即dp(s)=dp(i)+dp(s−i)。
假如我们背包大小为x,那么我们可以将背包拆分为两个大小差不超过W的子背包。因此我们要计算[s,s],需要依赖状态[⌊s2⌋−W(1−12),⌊s2⌋+W(1−12)]。而这些状态,则依赖于[⌊s22⌋−W(1−122),⌊s22⌋+W(1−122)]。总结就是我们需要计算满足下面条件的状态:
[⌊s2i⌋−W(1−12i),⌊s2i⌋+W(1−12i)]其中0≤i≤⌈log2s⌉。因此可以发现状态的上限为2W(⌈log2s⌉+1)。而每个状态的计算时间复杂度为O(W),因此总的时间复杂度为O(W2log2s)。
由于我们状态比较离散,一种简单的方式就是用哈希表来存状态。但是常数会比较大。
还有一种方式就是用log2s个长度为2W的数组来记录。每次我们深度i和s传入进行查询。但是在小数据的时候会有问题,解决方案就是暴力计算大小不超过W的背包。因此总的时间复杂度为O(W2log2),不变,但是常数变小了。
题目3:给定n个非负整数x1,…,xn,计算总和最大的子集,且子集和不能超过C。其中M=maxni=1xi≤1000,1≤n≤105,1≤C≤108。
正常的做法的时间复杂度为O(nC),或是O(n2M)。但是根据论文《Linear Time Algorithms for Knapsack Problems with Bounded Weights》中提到的算法,可以优化到O(nM)。
带修改动态规划
序列带修改带区间查询动态规划
题目1:给定一个序列a1,a2,…,an。现在要求处理q次请求,每个请求分为两种操作:
- 给定l,r,要求从al,al+1,…,ar中选取若干个不相邻的元素,使得总和最大
- 给定i,y,将ai修改成y。
首先如果没有修改且查询区间总是整个的序列的话,我们而已用动态规划来解决这个问题。但是现在又有修改又有区间查询,怎么搞。
先来考虑分治思想,我们将整个序列一分为二,分为左序列和右序列。之后我们独立计算左序列和右序列,之后我们将左右序列合并起来。合并的方式非常简单,左右子序列都维护一个大小为2×2的矩阵m,我们只需要利用修改后的矩阵乘法将两个矩阵乘起来即可。。
可以看出上面的方式只是解决了l=1,r=n的查询,那么对于多个任意查询如果回答。我们可以将分治过程理解成一棵树,这棵树中满足父顶点所处理的区间不完全落于[l,r]中,但是该顶点落在[l,r]中的顶点数上限是2log2n个,因此我们统计这2log2n个顶点上的处理结果就可以回答请求。这样回答所有请求的时间复杂度
上面提到的技术叫做离线二分。
好了,现在我们知道怎么解决查询了,但是如果处理修改呢,离线二分就解决不了这个问题了。但是细细的想,你会发现离线二分得到的二叉树实际上与线段树非常雷同。实际上,我们可以直接用线段树来替代离线二分得到的树。这样的好处是什么,线段树每次上传下传标记都是在线的,换句话说,我们只需要为线段树设计一套快速的合并算法,利用线段树天然支持修改的特性,我们就可以支持修改操作了。
这里用的合并算法实际上还是维护2×2矩阵,通过修改后的矩阵乘法来合并。
利用线段树我们就可以实现O(log2n)处理每个请求。
提供一道题目。
树上带修改带路径查询动态规划
题目2:给定一个拥有n个顶点的树,第i个顶点上标着一个数字ai。现在要处理q次请求,每个请求分为两种操作:
- 查询树上的最大权独立集
- 给定i,y,将ai修改成y。
这个问题实际上是问题1的树上加强版本。
如果没有修改操作,我们只需要在树上跑一次DP即可。现在多了修改操作,我们来看看怎么解决。
现在的问题是修改了后标记没法上传,我们可以对整棵树进行轻重链剖分,之后我们发现一条重链上的顶点恰好对应线段树的一个区间。我们可以在这个区间上跑真动态规划,这样我们可以立刻得到以重链顶部顶点为根的子树下的结果。这还不够,我们还需要将结果上传到父亲顶点上去,这里是直接暴力上传。可以发现由于沿着重链不断上升,总的上传次数被约束在O(log2n),而每次的时间复杂度为O(log2n),因此一次修改的时间复杂度为O((log2n)2)。
这里提供一道题目。
一些合成问题
题目1:给定n块宝石,第i块宝石的等级为li(li≤n),价格为ci。等级为i的宝石的出售价格为pi。任意两块i级宝石可以合成为一块i+1级宝石。问最大收益是多少,这里的收益是出售宝石的收入减去购买宝石的支出。n≤2000
首先我们按照宝石等级进行排序。之后建立动态规划dp(i,j)表示前i块宝石,手上等级li的石头还剩下j块,此时的最大收益。
动态规划的转移非常简单,这里不细说。总的时间复杂度是O(n2)。
题目2:给定n块宝石,第i块宝石的等级为li(li≤n),价格为ci。等级为i的宝石的出售价格为pi。任意两块i级宝石可以合成为一块i+1级宝石。问最大收益是多少,这里的收益是出售宝石的收入减去购买宝石的支出。特殊要求如果购入的宝石是i1,i2,…,ik,那么要求li1≤…≤lik。n≤2000
现在我们不能对宝石进行排序。建立动态规划dp(t,i,j)表示仅考虑等级不超过t的宝石,第i块宝石是最后收购的宝石,且手头还剩下j块t级别宝石的最大收益。
可以发现总共有O(n3)个状态,但是会发现在处理出dp(t,i,n)后,之后的有效状态随着t的增大1会减少一半。因此真实的时间复杂度还是O(n2)。
动态规划去后效性
动态规划有时候转移需要考虑当前状态的信息,考虑的信息越多,所需要记录的也越多,时间复杂度和空间复杂度也会对应提高。在最严重的时候应该就是需要做状压的时候了。下面介绍一种技术,在一些情况下可以让你摆脱状压带来的时空复杂度飙升问题。
考虑这样一个题目,给定一个n×n的矩形,我们希望在上面放入一些棋子,每个棋子都可以攻击对角线上的其他棋子,现在希望你回答,正好放k个棋子,有多少种方案数。
首先我们可以将这道题旋转45度,之后原本对角线攻击现在就对应变成了沿着垂线或水平线攻击。但是由于得到是一个棱形,因此没法直接用组合数学的方式直接跑。一种简单的方式就是状压DP,之后逐行处理,这里有一个技巧,就是偶数行和奇数行可以独立处理,且它们的有效列集合没有并集。这样做的时间为O(n22n)。
上面的这个算法由于使用了状压,可以看到时间复杂度中出现了一个指数函数2n。当n达到20或更大的时候,上面的算法就过不了了。
下面说一下如果去除状压。我们重新排列行顺序,注意到这不会影响答案的有效性。我们按照行的长度从小到大进行排序。之后我们会发现一个非常有趣的事情,由于第i行始终比前面i−1行都长,因此如果前面i−1行共放置了j个棋子,那么第i行中仅有j个单元格不能被放置,而其余的单元格都可以放置。因此我们可以定义一个函数f(i,j),其表示前i列放置j个棋子的方案数,那么可以直接得出一个简单的转移关系f(i,j)=f(i−1,j)+f(i−1,j−1)⋅(Li−j+1)。这样我们就可以摆脱了可怕的状压,时间复杂度降低为O(n2)。
决策单调性
考虑这样一个DP公式
dp(i,j)=mink≤jdp(i−1,k)+cost(k,j)这是一个非常常见的递推公式,其中0≤i<n,而0≤j<m。这类公式的空间复杂度为O(nm),时间复杂度为O(nm2)。
如果m≥105,那么这个时间复杂度就太大了。可以尝试判断是否满足决策单调性,即j≥k⇒opt(j)≥opt(k),其中opt(j)表示dp(i,j)从dp(i−1,opt(j))转移而来。如果满足,那么我们还可以用分治的方法来完成。设区间[l,r]在函数opt的作用下,值域为[L,R]。那么我们可以选择l与r的中点m,并暴力枚举值域,找到最优点opt(m),之后递归处理[l,m)(它的值域为[L,p(m)])和(m,r](它的值域为[opt(m),R])。可以证明每一层的时间复杂度均为O(mlog2m),因此总的时间复杂度为O(nmlog2m)。
对最小化DP:
- 对于任意k<j<i,都有cost(k,i+1)−cost(k,i)≥cost(j,i+1)−cost(j,i)。则可以使用决策单调性,即opt(i)≤opt(i+1)。
- 对于任意k<j<i,都有cost(k,i+1)−cost(k,i)≤cost(j,i+1)−cost(j,i)。则可以使用反向决策单调性,即opt(i)≥opt(i+1)。
而要对最大化DP应用:
- 对于任意k<j<i,都有cost(k,i+1)−cost(k,i)≤cost(j,i+1)−cost(j,i)。则可以使用决策单调性,即opt(i)≤opt(i+1)。
- 对于任意k<j<i,都有cost(k,i+1)−cost(k,i)≥cost(j,i+1)−cost(j,i)。则可以使用反向决策单调性,即opt(i)≥opt(i+1)。
有时候cost不好直接缓存计算,但是支持O(1)从cost(l,r)变为cost(l±1,r)和cost(l,r±1)。仔细观察决策单调性的过程,会发现这时候在一次递归计算过程中,cost(l,r)的l,r总共移动仅O(mlog2m)次。因此不会影响最终的时间复杂度。
题目1:给定一个序列a1,…,an,其中1≤ai≤n。记子序列al,…,ar的费用为cost(l,r)=∑ri=l∑nj=i+1[ai=aj],即子序列中有多少相同的元素对。现在我们希望将整个序列切分为m个非空子序列,要求所有切分的子序列费用总和最小。输出最小费用。其中n≤105,m≤100
显然这个问题满足决策单调性。
提供一道题目。
题目2:给定一个序列a1,…,an,其中1≤ai≤n。记子序列al,…,ar的费用为cost(l,r)=maxl≤i≤rai−min1≤i≤rai。现在我们希望将整个序列切分为m个非空子序列,要求所有切分的子序列费用总和最小。输出最小费用。其中n≤105,m≤100
这个问题和问题1的做法雷同,但是注意的是这里的单调性是反向的,即j≥k⇒p(j)≤p(k)。
题目3:给定一个序列a1,…,an,其中1≤ai≤n。记子序列al,…,ar的费用为cost(l,r)=(∑ri=lai)k。现在我们希望将整个序列切分为m个非空子序列,要求所有切分的子序列费用总和最小。输出最小费用。其中n≤105,m≤100,其中1≤k≤4,0≤ai≤109。
这种模型具有决策单调性,即k≤j⇒p(k)≤p(j)。
时间复杂度为O(mnlog2n)。
题目4:给定n个位置,第i个位置在xi处,有yi个住户。现在希望建立k座信号站。定义每个住户的惩罚为到最近信号站的距离的平方,现在希望所有住户惩罚和最小,问最小的惩罚和是多少。其中1≤n≤105,1≤k≤100,xi<xi+1。
提供一道题目。
我们定义cost(l,r)表示在位置l到位置r之间建立建立一座信号站,它们的最小总惩罚。可以发现这个值满足决策单调性。稍微证明一下:
考虑cost(k,i+1)的信号站位置为b,而cost(k,i)的信号站位置为a,cost(j,i+1)的信号站位置为d,而cost(j,i)的信号站位置为c。不难得到a≤c且a≤b且c≤d。我们可以可以令d=max(b,c)。这时候我们就有cost(k,i+1)−cost(k,i)≥cost(j,i+1)−cost(j,i)的成立。
区间DP和决策单调性
有时候我们会计算区间DP。其中dp(l,r)=minl<i≤rf(l,i,r)。这样的DP的时间复杂度一般都是O(n3)。但是如果区间DP满足opt(l,r−1)≤opt(l,r)≤opt(l+1,r),那么我们往往可以将区间DP优化到O(n2)。
原理如下,我们可以根据区间的长度从小到大计算DP值,这样的好处是在计算l,r的时候l,r−1和l+1,r已经被计算完成了。而我们可以在opt(l,r−1)和opt(l+1,r)之间暴力枚举opt(l,r)。可以发现对于每个固定的长度,暴力枚举的量实际上是不会超过2n的。因此总的时间复杂度为O(n2)。
提供一些题目:
线性DP的优化
有些DP拥有递推性质,比如,我们用DP求斐波那契数列的第n项。记f(n)表示斐波那契数列的第n项,则有f(n)=f(n−1)+f(n−2)。由于是递推数列,所以我们可以把后面的2项作为向量维护,这样就可以用矩阵来快速计算了。
[1101]n[10]有些DP它的递推关系是线性的,这种情况下也可以用矩阵快速幂。比如考虑这样一个DP:
dp(i,j)=m∑k=1aj,kdp(i−1,k)这样我们可以构建一个大小为m的方阵A,其中第i行第j列的向量为ci,j。同时我们将dp(i,?)看成一个列向量,于是可以发现dp(i,?)=Adp(i−1,?),从而dp(n,?)=Andp(0,?)。
这里顺带一提,一般这样做的时间复杂度为O(m3log2n),但是利用BM算法,可以优化到O(m3+m2log2n)。
还有一种优化,就是我们需要求dp(i1,?),dp(i2,?),…,dp(ik,?)。一般的时间复杂度为O(km3log2M)。其中M=max(i1,…,ik)。这里如果k,m≥100的话,就可能过不了了。有一种比较有用的优化,就是我们预先计算出A0,A1,…,Alog2M,时间复杂度为O(m3log2M)。之后假如我们要求dp(i,?),我们可以用快速幂的方式来求,总共最多O(log2M)次矩阵和向量的乘法运算,考虑到m×m矩阵和m×1的向量的乘积的时间复杂度为O(m2),因此计算一个dp(i,?)的时间复杂度就是O(m2log2M),总的时间复杂度为O((m+k)m2log2M)。
再看另外一个问题,假设有一个环,环上标记了n个点,记为0,1,…,n−1。一个人初始站在0上,之后他会走m步,每一步它都等概率向前和向后走一步,即假设他现在位于i,则有相同的几率会移动到i+1\pmod n和i-1\pmod n。要求计算最后这个人所站在位置的期望值。
我们可以计算概率,之后通过概率求期望。记dp(i,j)表示走了i步后,最后j处的概率。容易发现有dp(i,?)=Adp(i-1,?),其中A是n\times n的矩阵。时间复杂度为O(n^3\log_2m)。利用之前提到的BM优化技术,可以优化到O(n^3+n^2\log_2m)。下面给出更加高效的一个算法。
我们可以用多项式来表示dp(i,?),记f_i(x)=\sum_{j}dp(i,j)x^j。那么我们可以记另外一个多项式,表示转移关系g(x)=x^{-1}+x。这样我们就可以得到f_i(x)=g(x)f_{i-1}(x)。由于x^{n}=x^0,因此我们可以有g(x)=x+x^{n-1}。计算多项式卷积的时间复杂度为O(n^2),比矩阵的乘积O(n^3)要快不少。最终的时间复杂度为O(n^2\log_2m)。
上面这个方式还有一些比较丧心病狂的优化。比如如果我们用FFT计算卷积,则时间复杂度为O(n\log_2n\log_2m),但是由于我们实际上算的是g^m(x),我们可以通过计算e^{m\ln g(x)}得到,算多项式对数和指数的时间复杂度均为n\log_2n,因此最终的时间复杂度为O(n\log_2n)。
箱子堆叠问题
题目1:给定n个箱子,每个箱子都有由长宽高三个属性组成。现在要求我们将箱子堆叠起来,一个箱子可以放到另外一个箱子的上面,当且仅当上面的箱子的长不超过下面箱子的长,而宽也不超过下面箱子的宽。箱子的长宽是可以互换的。问可以堆叠最高的高度为多少。其中n\leq 10^6
我们可以将所有箱子长宽中最大的作为长,长宽中最小的作为宽。可以证明如果两个箱子不能堆叠那么,这两个箱子无论怎么交换长宽都依旧是不能堆叠的。之后可以提前按照箱子的长度进行排序,如果长度相同,就按照宽度进行排序。
接下来可以发现排序放在后面的箱子一定放在排序放在前面的箱子的下面。我们可以发现这个问题类似于LIS问题,记dp(i,j)表示前i个箱子,最底下箱子的宽度为j,最多能堆叠的高度。这个公式利用RMQ可以O(n\log_2n)求解。
题目2:给定n个箱子,每个箱子都有由长宽高三个属性组成。现在要求我们将箱子堆叠起来,一个箱子可以放到另外一个箱子的上面,当且仅当上面的箱子的长不超过下面箱子的长,而宽也不超过下面箱子的宽。箱子的长宽高都是可以互换的。问可以堆叠最高的高度为多少。其中n\leq 15
这道题一种方式就是枚举哪个属性作为高,之后问题就转换成题目1了。时间复杂度为O(3^nn\log_2n)。
还有一种方法就是,我们发现这道题的难度实际上是来自于无法确认箱子的上下顺序,我们可以用二进制来确定哪些箱子已经被用了,哪些没有,来去除上下顺序带来的影响。具体做法就是记dp(s,i,j)为s的箱子集合堆叠在了一起,其中第i个箱子,以第j个属性为高作为顶部的箱子存在下,最大的高度。这样转移只需要枚举其余箱子,以及用哪个属性作为高,转移的时间复杂度为O(3n)。总的时间复杂度为O(3n^22^n)。
数位DP
数位DP是个比较常见的技术,其一般用于计算在[L,R](L\geq 0)中计算每个数在函数f下的总和\sum_{i=L}^Rf(i)。
比如我们希望统计区间[L,R]中有多少数,满足每一位数只能奇数。那么我们可以定义f(i)为1当且仅当i的每一位均为奇数,否则f(i)=0。那么我们就可以用数位DP来计算\sum_{i=L}^Rf(i)。
下面考虑怎么计算。我们当然可以暴力枚举,时间复杂度为O(5^{\log_{10}R})=O(R^{\log_{10}5}),有点大。我们可以定义dp(i,floor,ceil)表示处理完后i+1位数字,floor指示是否受到L的约束,ceil指示是否受到R的约束。可以发现总共有2^2\log_{10}R种状态,每种状态只有10种转移需要计算,因此总的时间复杂度为O(40\log_{10}R),是相当快的。
题目1:给定一个矩形,矩形的第i行第j列的元素为i\oplus j,(行号和列号均从0开始)。要求计算以(x_1,y_1)作为左下角,(x_2,y_2)作为右上角的矩形中不超过k的数的总和。
这个问题也可以通过数位DP解决的。只不过一般数位DP解决的是一维约束条件,但是这里的约束条件是二维的。
我们可以记dp(i,xFloor,xCeil,yFloor,yCeil,kCeil),表示仅考虑后面i+1位数,xFloor表示是否受到x_1的约束,xCeil表示是否受到x_2的约束,yFloor表示是否受到y_1的约束,yCeil表示是否受到y_2的约束,kCeil表示是否受到k的约束,返回有多少数满足条件,且满足条件的数的总和。
很显然总共有2^5\log_2M种状态,每个状态内部只有4个转移,因此时间复杂度为O(2^7\log_2M),还是相当快的。
从这个问题也可以发现,数位DP可以很自然地推广到高维空间中去。
提供一道问题。
一些离散状态的DP
众所周知,大部分情况下我们的DP公式的入参都是一些连续的数值,但是有时候我们发现DP公式的参数是离散的情况。比如这个问题:
问题1:要求将x=1变成n需要的最少费用,你可以支付费用A,令x=2x,也可以支付费用B,令x=3x,也可以支付费用C,令x=5x,也可以支付费用D,令x=x\pm 1。其中n\leq 10^{18}。
这个问题是经典的离散DP的问题,因为我们不可能开这么大的空间来解决这个问题。一般这类问题我们需要用到哈希表(哈希表很适合存储离散的key)和记忆化搜索(只处理有意义的状态)。那么很显然时间复杂度取决于到底拥有多少状态,上面这个问题可以证明状态数不超过(\log_2n)^3,是非常快的。
可以发现离散状态的问题的难点往往是估计状态数,而实际上总是会惊讶的发现真实的状态数非常少。
问题2:给定一个长度为n的01序列A,我们需要计算其所有子集编码后有多少不同的字符串。其中0,1分别可以编码为0,1,如果p,q分别可以编码为P,Q,那么pq可以编码为PQ,且p\ldots p(共k个,k>1)可以编码为(p)\times k。如果X是Y的子集,则意味着X中值为1的下标集合是Y中值为1的下标集合的子集。将结果对某个素数取模。其中1\leq n\leq 100
容易发现DP公式:
f(A)=(A_1+1)f(A_{2,n})+\sum_{i=1}\sum_{j=2}^{ij\leq n}f(A_{1,i}\land \dots \land A_{(j-1)i+1,ji})f(A_{ji+1,n})其中n是A的长度,且A是01序列。可以f的参数也是离散化,很难估计,但是实际上它退化的非常快。理论上其状态数可能达到2^n,但是可以发现许多状态都是不可达的,实际上它的状态总数不超过O(n^3)。因此我们可以直接暴力DP。
对于大部分离散状态的DP,如果其状态数不好统计,我们大可直接暴力DP,之后利用各种剪枝技巧来优化常数,保证通过最大规模的数据。
DP与拓扑序
称一个长度为2n的序列a_1,\ldots,a_{2n}为NRS(非减序列),当且仅当这个序列满足\forall 1\leq i<n(a_i+a_{n-i+1}\leq a_{i+1}+a_{n-i})。给定一个序列b_1,\ldots,b_m,求其中最长的NRS子序列的长度L,以及存在多少长度为L的NRS子序列,这里a_i\leq 10^9,2\leq n\leq 1000。
我们先解决怎么计算L。记dp(l,r)表示以a_l和a_r作为两端的最长的NRS的长度。很显然dp(l,r)=\max_{l<x<y<r\land a_l+a_r\leq a_x+a_y} dp(x,y)。
我们可以对r-l从小到大进行排序处理,并枚举x,y就可以在O(n^4)时间复杂度内解决这个问题。
但是这个时间复杂度还是比较糟糕的,实际上我们可以进一步优化。考虑我们对dp(i,j)按照a_i+a_j作为第一关键字,r-l作为第二关键字进行排序。
将二维数组视作一个二维矩形块,之后每次查询其实都是查的某个矩形区域,而矩形区域的计算我们可以通过泛化的BIT进行处理,查询和插入的时间复杂度都是O((\log_2n)^2),因此总的时间复杂度为O(n^2(\log_2n)^2)。
整数和浮点数DP
很多问题在整数情况下是NP的,但是在浮点数情况下是有多项式解法的。比如整数规划和线性规划。有时候在允许浮点数和仅允许整数的情况下拥有相同的解,比如说最大流、费用流问题等,这时候我们可以用用于浮点数的算法去解决仅允许整数的问题。即使无法完全复用,但是浮点数的做法往往可以给整数做法带来很多好处。
题目1:要求找到n个整数a_1,\ldots,a_n,满足\sum_{i=1}^ni\cdot a_i=m,且费用为\sum_{i=1}^na_i^2尽可能小。输出a_1,\ldots,a_n。其中1\leq n\leq 10^5, 1\leq m\leq 10^5
提供一道题目:SRM738 DriveTheCarHard。
很容易想到一个DP,就是dp(i,j)表示在选定a_1,\ldots,a_i且\sum_{k=1}^ik\cdot a_k=j的前提下,\sum_{k=1}^ia_k^2的最小值。这个DP的转移的时间复杂度是O(m^2\ln n),是过不了的。
首先可以发现如果费用在x的情况下总和的上限为y,那么在费用不超过x的前提下总和可以达到0到y之间的所有值。证明方法非常简单,在我们当前总和为y>1时,我们可以找到第一个下标i,满足a_{i-1}\lt a_i(我们可以认为a_0=0)。这样的下标是一定存在的。我们让a_i减少1,同时让a_{i-1}增加1,可以发现费用是降低的,并且总和恰好减少1。
我们可以分两种情况考虑问题。
一种是n\geq 500。这种情况下,我们可以证明结果不会超过500,因为200\times 800\geq m。因此我们可以重新定义f(i,j)表示仅考虑a_n,a_{n-1},\ldots,a_{n-i+1},总费用不超过j所能达到的最大总和。这时候时间复杂度为O(500^{2.5})。
另外一种方案是n<500的情况。
我们来看在允许a_i是浮点数情况下,答案是多少。可以发现此时a_i带来的收益是i\cdot a_i,其带来的费用为a_i^2,因此平均收益为\frac{i}{a_i}。很显然最终每个未知数的平均收益都是相同的(不然减少平均收益较小的投入,并增大平均收益较大的投入,可以得到更好的结果)。因此a_i=i\cdot a_1。之后同时收益的总和为m,因此\sum_{i=1}^ni^2\cdot a_1=m,可以解出a_1=\frac{m}{\sum_{i=1}^ni^2},从而求出a_i=\frac{i\cdot m}{\sum_{k=1}^nk^2}。
虽然浮点数的情况和整数的情况有所区别,但是二者所作的最优决策却会相当接近,即在已知n,m的前提下,a_n和a'_n非常接近,其中a'_i是浮点数的时候的最优决策。
记opt(i,j),opt(i,j)表示计算dp(i,j)时设置的a_i值。同时记opt'(i,j)在允许浮点数的时候计算dp(i,j)时设置的a_i值。最终可以发现\max_{i,j}|opt(i,j)-opt'(i,j)|不超过某个常数\alpha,在这个问题中\alpha=2。因此我们可以O(\alpha nm)处理出dp(opt(i,j)仅可能为opt'(i,j)\pm 2)。
状压DP和排列
我们可以用状压DP来存储排列信息。比如下面一个问题:
给定一个n\times m的矩阵,要求每一行取正好一个数,每一列最多取一个数。且要求取到的数的总和最大。其中1\leq n\leq 10, 1\leq m\leq 10^4。
这个就是经典的排列转状压DP问题了。如果我们知道m个数的排列信息,我们可以直接O(nm)进行DP即可,但是这里不知道,如果我们暴力枚举所有可能的排列的话,时间复杂度为O(n!m),这会超时。我们可以记dp(i,s)表示仅考虑前i列,且s中的行都被选择过的最大和,这样可以O(2^nmn)时间复杂度内解决这个问题。
同样在有的时候简单的状压可以替换掉子集枚举,考虑下面这个问题。
给定承重为x的背包若干个,以及n件物品,第i件物品的重量为w_i。问最少需要多少个这样的背包可以将所有物品都打包带走,保证1\leq w_i\leq x,且1\leq n\leq 20。
如果我们预先让所有物品按照装入的背包ID排序,则一次O(n)迭代即可算出结果。但是这里我们不知道最优顺序,我们只能暴力枚举所有的排列,时间复杂度O(n!n)。
我们可以直接上状态压缩,记录dp(s)表示装走s集合中的物品所需的最少背包,但是在做状态转移的时候我们需要用到枚举子集的技术,这样时间复杂度会达到O(3^n),过不了。
用状压还有一个技巧,就是保存最后一次决策的信息。我们记f(s)表示装走s集合中的物品所需的最少背包,记g(s)表示在用最少背包装走s集合中的物品的前提下且要让最后使用的背包总重量最小,最后使用背包的重量。下面考虑转移的时候,我们可以枚举最后背包中的任意一件物品,记这件物品为i,则s的状态一定是从s-i转移而来的。因此我们可以枚举s中的元素,时间复杂度降低到O(n2^n),快了很多。
提供一道问题。
区间DP
区间DP一般是将整个区间切分成多个子区间求和的技术。
题目1:给定一个长度为n的序列,你每次可以删除序列中的一个连续回文子串,删除完成后剩余的元素重新拼接。问最少需要多少次操作可以将整个序列完全删除。1\leq n\leq 500
提供一道题目。
套用区间DP的套路,首先我们可以尝试看看区间[l,r]两端的元素是否一起删除。如果不是,则区间必定可以拆分为两个子区间单独决策。否则一起删除,则我们可以继续考虑问题(l,r),如果其至少操作了一次。我们可以将最后一次删除的回文和序列的第l,r元素合并为一个新的回文一起删除。否则就必须额外花费一次删除操作。
上面的过程可以O(n^3)完成。
分治+FFT
题目1:给定一个包含n个顶点m条边的图。第i条边有长度分别为1,\ldots,T的走法,长度为k的走法又有c_{i,k}种走法。接下来要求对于t=1,\ldots,T,计算f(t),即从顶点1出发回到顶点1的路径中,长度为t的不同路径数。结果对素数取模。其中1\leq n,m\leq 10,且1\leq T\leq 5\times 10^4。
很容易想到一个dp,即dp(i,j)表示移动到顶点i,走过路径长度为j的总走法数。这个方法的转移数比较多,达到了O(nT+mT^2)。
接下来考虑如何优化这个DP。可以发现这里的转移非常类似一个卷积,但是我们不能直接卷积,因为我们通过dp(i,j)卷积得到dp(i,j+1)后,dp(i,j+1)依旧会产生额外的贡献。所以这样的话时间复杂度会达到O((n+m)T^2\log_2T)。
一种简单的思路是通过分块,对于块大小B,这样可以做到时间O(mT\sqrt{T\log_2T}),以及一个很大的常数。
还有一种比较靠谱的方式,就是用分治,先计算左边,之后用FFT让左边对右边进行贡献,之后计算右边。可以算出时间复杂度为O(mT(\log_2T)^2)。
树上卷积式DP
树上DP有非常多都很类似限定长度的卷积操作,不妨设有n个顶点,限定的长度为m,满足1\leq m\leq n。这时候由于每条边都需要执行一次卷积操作,总共的时间复杂度会类似于O(nm^2),或者如果适用FFT加速的话可以达到O(nm\log_2m)的时间复杂度。但是实际测试会发现暴力算法跑的非常快,实际上可以证明真实的时间复杂度为O(nm)。
举个例子,在树的每个叶子的权值都是一个特殊的一阶多项式,之后每个非叶子结点的权值为所有子结点的卷积。接下来要求输出根结点权值的前m项,可以证明暴力计算的时间复杂度为O(nm)。
这里给出证明,卷积发生的情况,根据两个多项式的阶数可以分为三种情况:
- 两个多项式的阶数均不小于m。这时候暴力计算的时间复杂度为O(m^2),因为只需要考虑前m项。而这样的多项式最多存在O(n/m)个,每次合并都会减少一个这样的多项式,因此总的时间复杂度为O(nm)。
- 两个多项式的阶数都小于m,这时候可以发现两个点卷积时产生的贡献类似于两个点在lca除相遇,因此任意两个点都只会贡献1,共生成O(n/m)个块,每个块的时间复杂度为O(m^2),这样所有这类操作的时间复杂度之和为O(nm)。
- 一个多项式阶数大于等于m,另外一个小于m。这时候每个较小多项式众的元素最多产生O(m)的贡献,却会减少一个处于多项式阶数小于m多项式众的元素。因此总的时间复杂度约束至O(nm)。
将三种情况之和加起来就得到了总的时间复杂度O(nm)。
提供一些练习题: