目录
题目完成进度
21/32(数据更新ing)
线性DP大概就是个入门的感jio(虽然也有难题),所以在这里就不过多赘述,只是大致讲一下。
线性DP有三类经典例题,最长上升子序列(LIS),最长公共子序列(LCS),数塔问题。这里主要是要理解DP的“阶段”“状态”“决策”三要素,然后记住应用DP的三个条件:“子问题重叠性”“最优子结构”“无后效性”。
同时这一节还讲到了DP的初步优化:离散化、贪心、变量维护决策集合(啥玩意???)、前缀和预处理等,根据题目的具体情况灵活使用。
接下来搬运几道例题:
Mr Youngs Picture Permutations
Greatest Common Increasing Subsequence
1.0/1背包
0/1背包的问题模型如下:
给n个物品,其中第i个物品的体积为$V_i$,价值为$W_i$。有一容积为m的背包,求放入背包的物品的最大总价值。
0/1背包是最基础的啦,这里就不多讲,主要注意一下循环的顺序就好。
直接上例题——
2.完全背包
完全背包的问题模型如下:
给n种物品,其中第i种物品的体积为$V_i$,价值为$W_i$,并且有无数个。有一容积为m的背包,求放入背包的物品的最大总价值。
参考0/1背包的做法,不过要把0/1背包的倒序循环改为正序循环,这就对应着每种物品可以使用无数次。
int f[M]; memset(f,0xcf,sizeof(f)); f[0]=0; for(int i=1;i<=n;i++) for(int j=v[i];j<=m;j++) f[j]=max(f[j],f[j-v[i]]+w[i]); int ans=0; for(int i=0;i<=m;i++) ans=max(ans,f[i]);
上两道例题——
3.多重背包
多重背包的问题模型如下:
给n种物品,其中第i种物品的体积为$V_i$,价值为$W_i$,并且有$C_i$个,放进体积为m的背包里,求最大价值。
这里介绍解决多重背包问题的三种方法:直接拆分法,二进制拆分法,单调队列法。
•直接拆分法
这是解决多重背包问题最直接也最简单的方法,很显然可以想到的做法,即把第i种物品看做独立的$C_i$个物品,然后按照0/1背包的做法解决问题。
int f[M];//f[i]表示占用背包体积为i时的最大价值 memset(f,0xcf,sizeof(f));//赋初始值为无穷小 f[0]=0; for(int i=1;i<=n;i++) for(int j=1;j<=c[i];j++) for(int k=m;k>=v[i];k--) f[k]=max(f[k],f[k-v[i]]+w[i]); int ans=0; for(int i=0;i<=m;i++) ans=max(ans,f[i]);
•二进制拆分法
求最大整数p满足$2^0+2^1+…+2^ple C_i$,然后设$R_i=C_i-2^0-2^1…-2^p$,so…
1.根据p的最大性,故有$2^0+2^1+…+2^{p+1}>C_i$,则可以推出$2^{p+1}>R_i$,因此从$2^0,2^1,…,2^p$中选出若干个相加可以表示出$0~R_i$之间的任何整数。
2.从$2^0,2^1,…,2^p$以及$R_i$中选出若干个相加,可以表示出$R_i~R_i+2^{p+1}-1$之间的任何整数,而根据$R_i$的定义可得:$$R_i+2^{p+1}-1=C_i-frac{1*(1-2^{p+1})}{1-2}+2^{p+1}-1=C_i$$因此上面的结论就可以修改为:从$2^0,2^1,…,2^p$以及$R_i$中选出若干个相加,可以表示出$R_i~C_i$之间的任何整数。
综上所述,我们可以把数量为$C_i$的物品拆成p+2个物品,他们的体积分别为:$2^0*V_i,2^1*V_i,…,2^p*V_i,R_i*V_i$,这p+2个物品可以凑成$0~C_i*V_i$之间所有能被$V_i$整除的数,并且不能凑成大于$C_i*V_i$的数。这等价于原问题中体积为$V_i$的物品可以使用$0~C_i$次。
•单调队列法
→走链接←
来道例题吧QAQ
4.分组背包
分组背包的问题模型如下:
给n组物品,其中第i组有$C_i$件物品,第i组的第j个物品的体积为$V_{i,j}$,价值为$W_{i,j}$。有一容积为m的背包,每组至多选择一个物品放入背包,求最大总价值。
背包类问题的解法都是大同小异,所以这里就直接上代码了(我才不是懒得讲了)
int f[M]; memset(f,0xcf,sizeof(f)); for(int i=1;i<=n;i++) for(int j=m;j>=0;j--) for(int k=1;k<=c[i];k++) if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]); int ans=0; for(int i=0;i<=m;i++) ans=max(ans,f[i]);
区间DP属于线性DP的一种,它以“区间长度”作为DP的“阶段”,使用区间的左右端点描述两个维度。在区间DP中,一个状态由若干个比它更小且包含与它的区间所代表的状态转移而来,因此区间DP的决策往往就是划分区间的方法。
直接上例题讲解吧QWQ
给定一棵有n个结点的树,我们任选一个结点为根结点,从而定义出每个结点的深度和每棵子树的根。在树上设计DP算法时,一般以结点从深到浅(子树从小到大)的顺序作为DP的“阶段”。DP算法的状态表示中,第一维通常是结点编号(代表以该节点为根的子树)。大多数时候,我们采用递归的方式实现树形DP,对于每个结点x,先递归在它的每个子结点上进行DP,在回溯时,从子结点向结点x进行状态转移。
先上一道例题——
除了自上而下的递归,我们也可以用自下而上的拓扑排序来执行树形DP。
在有些题目中,树是以一张n个点、n-1条边的无向图形式给出的,在这种情况下,我们就要用邻接表存下这n-1条边,任选一个点出发执行DFS,并注意标记结点是否已经被访问过,以避免在遍历中沿着反向边返回父节点。
•背包类树形DP
这类题目被称为背包类树形DP,又称有树形依赖的背包问题。它实际上是背包与树形DP的结合。除了以“结点编号”为树形DP的阶段,通常我们也会把当前背包的体积作为第二维状态,状态转移时,我们要处理的实际上是一个分组背包问题。
•二次扫描与换根法
•环形结构上的DP问题
在许多环形结构问题中,我们都能够通过枚举法,选择一个位置把环断开,变成线性结构进行计算,最后根据每次枚举的结果求出答案。这样的环形问题称为“可拆解的环形问题”,我们可以采取适当的策略避免枚举,使时间复杂度降到最低。
这里主要介绍两种策略:
1.执行两次DP,第一次在任意位置把环断开成链,按照线性问题求解;第二次通过适当的条件和赋值,保证计算出的状态等价于把断开的位置强制相连。
2.断环为链
上两道例题,分别是使用了这两种策略求解——
•有后效性的DP转移方程
有时候我们会遇到形似DP的题目,但是不满足“无后效性”的条件。这时候我们可以把DP的各个状态看做未知量,状态的转移看做方程,然后用高斯消元求出状态转移方程的解。
在更多的题目中,DP的状态转移“分阶段带环”——我们需要把DP和高斯消元相结合,在整体层面采用DP框架,而在局部使用高斯消元解出互相影响的状态。
小二上例题——
Broken Robot
DP的过程是随着“阶段”的增长,在每个状态维度上不断扩展的。在任意时刻,已经求出最优解的状态与尚未求出最优解的状态在各维度上的分界点组成了DP扩展的“轮廓”。对于某些问题,我们需要在DP的“状态”中记录一个集合,保存这个“轮廓”的详细信息,以便进行状态转移。若集合大小不超过$N$,集合中每个元素都是小于$K$的自然数(状态以0/1表示较常见),则我们可以把这个集合看作一个$N$位$K$进制数,以一个$[0,K^N-1]$之间的十进制整数的形式作为DP状态的一维。这种把集合转化为整数记录在DP状态中的一类算法,就被称为状态压缩的DP算法。
下面以例题详细讲解——
炮兵阵地
倍增的思想还是很简单的,参照LCA的做法即可,直接上例题讲解。
Count The Repetitions
状态压缩DP和倍增DP都是从状态表示入手对DP进行的优化,这里从转移入手,探讨DP程序实现中的优化手段。
对于某些题目,随着DP阶段的增长,决策的取值范围的下界不变,上界递增。更一般地,只要这个决策的候选集合只扩大、不缩小,我们就可以仅用一个变量维护最值,不断与新加入候选集合的元素比较,即可直接得到最优决策,$O(1)$地执行转移。
而在更复杂的情况下,我们就要用更加高级的数据结构维护DP决策的候选集合,以便快速执行插入元素、删除元素、查询最值等基本操作,把朴素的在取值范围中枚举决策的时间优化为维护数据结构的时间。
这里用几道例题概括一下几种常见的情况——
Cleaning Shifts
The Battle of Chibi
在第一道例题中,取值范围除上界不断递增外,下界变化没有规律,因此采用更加灵活的支持区间最值维护的数据结构。
在第二道例题中,取值范围有两个限制条件,一个是关于“数组下标”的位置,一个是关于“数列A的数值”的位置,它们实际上是两种“坐标”。DP循环保证了第一个条件的满足,对于第二个条件,我们在“数列A”这个“坐标轴”上建立了以$f$数组中的状态为值的数据结构。
总而言之,无论DP决策的限制条件是多是少,我们都要尽量对其进行分离。多维DP状态在执行内层循环时,把外层循环变量看作定值。状态转移取最优决策时,简单的限制条件用循环顺序处理,复杂的限制条件用数据结构维护。
单调队列非常适合优化决策取值范围的上、下界均单调变化,每个决策在候选集合中插入或删除至多一次的问题,例如求最大子序和。
例题——
Fence
Cut the Sequence
单调队列优化多重背包
前面介绍了多重背包的朴素解法和二进制拆分解法,若使用单调队列,可以使复杂度降到$O(NM)$。
在进行DP的过程中,当外层循环进行到$i$时,$f[j]$表示从前$i$种物品中选出若干个放入背包,体积之和为$j$时,最大的价值之和。倒序循环$j$,在状态转移时,考虑选取第$i$中物品的个数$cnt$:
$$f[j]=max{f[j-cnt*V_i]+cnt*W_i}(1le cntle C_i)$$
能够转移到状态$j$的决策候选集合${j-cnt*V_i|1le cntle C_i}$如下:
当循环变量$j$减小1时:
可以发现,相邻两个状态$j$和$j-1$对应的决策候选集合没有重叠,很难快速地从$j-1$对应的集合得到$j$对应的集合。
但是,我们试着考虑一下状态$j$和$j-V_i$:
这两者对应的决策候选集合之间的关系,与单调队列十分相似,只有一个新决策加入候选集合、一个已有决策被排除。所以,我们把状态$j$按照除以$V_i$的余数分组,对每一组分别进行计算,不同组之间的状态在阶段$i$不会相互转移:
余数为0——$0,V_i,2V_i,…$
余数为1——$1,V_i+1,2V_i+1,…$
……
余数为$V_i-1$——$V_i-1,(V_i-1)+V_i,(V_i-1)+2V_i,…$
把“倒序循环$j$”的过程,改为对每个余数$uin[0,V_i-1]$,倒序循环$p=lfloor(M-u)/V_i floor~0$,对应的状态就是$j=u+p*V_i$。第$i$种物品只有$C_i$个,故能转移到$j$的决策候选集合就是${u+k*V_i|p-C_ile kle p-1}$。写出新的状态转移方程:
$$f[u+p*V_i]=max{f[u+k*V_i]+(p-k)*W_i}(p-C_ile kle p-1)$$
把外层循环$i$和$u$看作定值,当内层循环变量$p$减小1时,决策$k$的取值范围$[p-C_i,p-1]$的上、下界均单调减小。状态转移方程等号右侧的式子仍然分为两部分,仅包含变量$p$的$p*W_i$和仅包含变量$k$的$f[u+k*V_i]-k*W_i$。综上,我们可以建立一个决策点$k$单调递减,数值$f[u+k*V_i]-k*W_i$单调递减的队列,用于维护候选集合。对于每个$p$,执行单调队列的三个管理操作:
1.检查队头合法性,把大于$p-1$的决策出队
2.取队头为最优决策,更新$f[u+p*V_i]$
3.把新决策$k=p-C_i-1$插入队尾,入队前检查队尾单调性,排除无用决策。
上代码——
1 #include<bits/stdc++.h> 2 #define rg register 3 #define ll long long 4 #define go(i,a,b) for(rg int i=a;i<=b;i++) 5 #define back(i,a,b) for(rg int i=a;i>=b;i--) 6 using namespace std; 7 const N=maxnN+2; 8 int n,m,f[N],V[N],W[N],C[N],q[N]; 9 int calc(int i,int u,int k){ 10 return f[u+k*V[i]]-k*W[i]; 11 } 12 int main(){ 13 cin>>n>>m;//n种物品,背包体积为m 14 memset(f,0xcf,sizeof(f));//-INF 15 f[0]=0; 16 go(i,1,n){ 17 scanf("%d%d%d",&V[i],&W[i],&C[i]); 18 go(u,0,V[i]-1){//除以V[i]的余数 19 int l=1,r=0;//建立单调队列 20 int maxp=(m-u)/V[i]; 21 back(k,maxp-1,max(maxp-C[i],0)){ 22 while(l<=r&&calc(i,u,q[r])<=calc(i,u,k))r--; 23 //检查队尾单调性 24 q[++r]=k; 25 }//把最初的候选集合插入队列 26 back(p,maxp,0){ 27 while(l<=r&&q[l]>p-1)l++;//排除过时决策 28 if(l<=r) f[u+p*V[i]]=max(f[u+p*V[i]],calc(i,u,q[l])+p*W[i]); 29 //取队头状态进行转移 30 if(p-C[i]-1>=0){ 31 while(l<=r&&calc(i,u,q[r])<=calc(i,u,p-C[i]-1)) 32 r--; 33 q[++r]=p-C[i]-1;//插入决策,并维护队尾单调性 34 } 35 } 36 } 37 } 38 int ans=0; 39 go(i,1,m) ans=max(ans,f[m]); 40 cout<<ans<<endl; 41 return 0; 42 }
只关注“状态变量”“决策变量”所在的维度,单调队列优化DP的模型的状态转移方程都可以大致归为如下形式:
$$f[i]=min{f[j]+val(i,j)}(L(i)le ile R(i))$$
上式所代表的问题覆盖广泛,是DP中一类非常基本、非常重要的模型。这种模型也被称为1D/1D的动态规划。它是一个最优化问题,$L(i)$和$R(i)$是关于变量$i$的一次函数,限制了决策$j$的取值范围,并保证起上下界变化有单调性。$val(i,j)$是一个关于变量$i$和$j$的多项式函数,通常是决定我们采用何种优化策略的关键之处。我们把$val(i,j)$分成两部分,第一部分仅与$i$有关,第二部分仅与$j$有关。对于每个$i$,无论采取哪个$j$作为最优决策,第一部分的值都是相等的,可以在选出最优决策更新$f[i]$时再进行计算、累加。而当$i$的值发生变化时,第二部分的值不会发生变化,从而保证原来较优的决策,在$i$改变后仍然较优,不会产生乱序的现象。于是,我们可以在队列中维护第二部分的单调性,及时排除不可能的决策,让DP算法得以高效进行。所以,在上述模型中,多项式$val(i,j)$的每一项仅与$i$和$j$中的一个有关,是使用单调队列进行优化的基本条件。
使用单调队列优化的条件是多项式$val(i,j)$的每一项仅与$i$和$j$中的一个有关,而当多项式$val(i,j)$包含$i,j$的乘积项,即存在一个同时与$i$和$j$有关的部分时,我们可以采用斜率优化。
上例题——
任务安排3
Cats Transport
数位统计DP是与数字相关的一类计数问题。在这类题目中,一般给定一些限制条件,求满足限制条件的第k小的数是多少,或者求在区间[L,R]内有多少个满足限制条件的数。解决方法大致为先用DP进行预处理,然后基于拼凑的思想,用“试填法”求出最终的答案,一些细节部分要根据题目要求来调整。
上例题~QWQ
月之谜