常见DP类型

常见DP类型

第一节:线性DP

思想:DP是作用在线性空间上的递推——DP的阶段按照各个维度线性的增长,从一个或多个边界点开始有方向的向整个状态空间转移扩展,最后在每个状态上保留的以自身为目标问题的最优解

简单的说,DP是不断划分自己的子问题(满足能从小问题推出大问题的答案),从最小的子问题开始一步步逼近目标点得到答案

DP实现的方式:

1.正序计算,

这里适用于最小的子问题可以直接得到解,且终态是一个确定的解。适用于大部分DP情况

2.倒序计算。

即我们知道终态的答案或者是由初始状态可以推出多个最终状态,此时就可以由终态反推回去。常用于数学期望的DP(期望性质:E(ax+by)=aE(x)+bE(y)

3.记忆化搜索 常数略大于一般DP

这里普遍用于DP在状态空间上的遍历很难直接用循环表示出来,或者说,不能用循环表示,此时一般使用记忆化搜索,多数用于图的DP

编写技巧

1.可以思考从当前答案向前推,亦或者用以前的答案求现在的答案两种形式,有些时候两种方式的实现复杂度会相去甚远

2.在DP的状态转移的时候,可以观察多重循环中相对内层循环的不变条件,以及内层循环的变量取值的域,对此可以适当的优化

比如LCIS这道题中,最后一层循环的k可以省去,因为k的取值在j+1时取值范围只会增大一,就可以省略最后一层,避免重复计算,观察决策集合随着外层循环的变化,若值增大不减小且限制条件不变,便可以只用一个变量来维护决策

3.注意挖掘题目中的维度,阶段

4.当DP涉及多个序列或者是连续性,需要序列末尾值的时候,可以将序列末尾的值当作一个状态存下(也可以用其他维度代表)

5.尽量地省略维度,在一个维度可以被其他维度表示出来的时候(这个关系往往需要去发现),就可以省掉这一维度

6.有些时候并不好确定DP的顺序,就可以使用贪心,topsort这些确定DP顺序,一般需要在题目里挖掘条件

7.缩放的艺术,当DP的f值只与前面的元素的相对关系有关的时候,就可以缩放状态,以保持相对关系不变性(比如所有都加一减一啥的)。。。比如ACwing277.饼干

8.当仅仅是题目中的维度不足以表示出整个状态,需要某些变量的时候,就可以找到某些变量将其也作为维度

9.需要注意DP时循环顺序应为:阶段->状态->决策

第二节——背包

01背包:

问题描述:有n个物品,其中第i个物品体积记作Vi,价值记作Wi,要求从中选出若干个,使得总体积不超过M,且价值和最大

模板:

int f[MAX_M];
memset(f,0xcf,sizeof f)l;
for(int i=1;i<=n;i++){
	for(int j=mj>=v[i]--j){//注意是倒序计算
		f[j]=max(f[j],f[j-v[i]]+w[i]);
	}
}
int ans=0xcfcfcfcf;
for(int i=0i<=mi++)ans=max(f[i],ans);
                  

统计方案的时候把max改成加

完全背包

问题描述:同01背包,但有无数个物品
板子:

int f[MAX_M];
memset(f,0xcf,sizeof f);
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=0xcfcfcfcf;
for(int i=0;i<=m;i++)ans=max(f[i],ans);
                  

解释:我们省略了第一维,所以倒序枚举的时候是建立在i1的基础上的,而正序建立在i的基础上,就可以被重复计算
原因:二者二维递推式:

01背包

fi,j=max(fi1,j,fi1,jv[i]+w[i](jv[i]))

完全背包

fi,j=max(fi1,j,fi,jv[i]+w[i](jv[i]))

多重背包

问题描述:有n种物品,每种有Ci个,其中第i种物品体积记作Vi,价值记作Wi,要求从中选出若干个,使得总体积不超过M,且价值和最大

思路

1.暴力拆他死了

2.二进制拆分

先上复杂度 Θ(nMlogE) 其中ECi的值域

思想:因为每一个数都可以表示为若干个二的整次幂(不重复)的和
所以我们可以计算出一个pi满足:

20+21+2pCi

且这个p最大

此时我们就可以将Ci拆分成p+2个部分,即:
20,21,22,,2p,Cii=0p2i,最后那个式子可以由等比数列求和公式改为:Ci2p+1+1

这样我们就可以把一共i=1nCi个物品拆为少于n×logE个物品,成功地掉了复杂度,一般已经够用

3.单调队列优化

见下:单调队列优化DP

分组背包

问题描述:给定N组物品,其中第i组有Ci个物品,其中第j个物品的体积为Vi,j,价值为Wi,j,有一容量为M的背包,要求选择若干个物品,使每组最多选择一个,且物品总体积小于M的情况下价值最大

还是以原始的二维DP,设 fi,j 表示在前i选出总体积为j的物品放入背包的最大价值,有DP式:

fi,j=max(fi1,j,max1jCifi1,jVi,k+Wi,k)

同样的,省略第一维i,可得

memset(f,0xcf,sizeof f);
f[0]=0;
for(int i=1i<=ni++);
    for(int j=mj>=0j--);
	for(int k=1k<=c[i]k++);
     	    if(j>=v[i][k]);
                 f[j]=max(f[j],f[j-v[i][k]]+w[i][k]); 

这里需要注意的是

1.倒序循环j

2.对于每组物品的循环k应该放在j的内层,目的是如果放在外面因为一维数组所以可能变成多重背包

(i是阶段,i,j构成状态,k是决策,应该按照i,j,k的顺序来)

区间DP

概念:关于序列上区间的DP问题

设计技巧:通常有两个维度l,r表示目前DP的区间,需要注意的是,必须如下这样写

 for(int len=2len<=nlen++){
	for(int l=1,r=lenr<=nl++,r++){
		……
    }
}

其目的是为了满足DP原则:先解决所有的小问题

在区间DP中,DP的阶段就是区间,使用两个区间端点l,r描述一个状态。同样是当前区间的答案为一些小区间的答案组合而来,所以我们DP的决策一般就是关于区间的划分。而区间DP的初始化一般就是对长度为1的"元区间"

同样的,在DP时,务必将阶段,状态,决策三者自外向内循环

在区间DP的过程中,决策一般就是区间划分,所以我们一般在关于第三层循环内循环k对区间进行划分统计

区间DP的编写技巧及注意事项:

1.对于带环状的区间,我们可以将1n复制一倍接在后面形成一个2n的链,在其上进行统计,最后枚举所有长度为n的区间[i,i+n1](1in)

2.无论在什么时候,都要牢记动态规划的三原则三要素,时刻关注后效性和最优子结构性质

3.在自己进行区间DP时一定要反复问自己是否决策不完全

4.在两个序列以乘法形式合并的时候,最大值只有可能从1.两个序列最大值相加相乘,2.两个序列最小值相乘(负负得正) 3.一个序列的最大值乘另一个序列的最小值(两个序列都一正一负)。最小值同理

5.有时关于区间DP计数的时候,一定要注意不重不漏,关于DP的状态的设计,很多时候一些状态可以代表多个信息。在ACwing.284金字塔中,就有可能重复这时候我们让区间划分点k的含义[l,k]只能是第一颗子树,就不会重复也不会漏掉。。这里我们附加的这个信息必须具备2个性质:1.对所有的情形下都具有这个性质。2.必须满足不重不漏

6.在DP的方案统计的时候,如果一个状态的多个决策之间满足加法原理,而各个决策的方案又满足乘法原理时,在状态转移的时候各个决策必须具备互斥性才不会重复,对于互斥性的问题,我们可以挖掘题设条件,在DP的维度进行限制,比如改变某个维度的含义或者对这个含义进行缩小范围

7.当一般的f[l,r]并不能够维护好DP的信息的时候,我们观察题目数据范围,可以进行增加维度,如果时间复杂度过高则考虑优化或者换思路

8.在编写DP的时候,一定要注意当各个决策之间具有递归,嵌套性质的时候,在转移的时候就必须考虑之前的那些的嵌套关系,如AcWing.319折叠序列,我们使用KMP求出最小循环元之后,还得考虑这个最小循环元可不可以进行若干个拼凑起来的折叠,故我们得从最小循环元那里转移状态而不是直接用最小循环元算

树形DP

简述:在一颗树(通常是无根树,也就是n1条边的无向图),我们可以任意拉一个节点为根节点,从而定义出节点的深度,根,子树大小等信息。在树上设计动态规划算法时,一般第一维为节点的编号,以节点由深到浅作为DP的阶段,树上的动态规划主要有两种形式:1.自顶向下的记忆化搜索。2.自底向上的topsort,其中绝大多数时候应用记忆化搜索。先递归子节点,回溯的时候进行状态转移

树形DP与背包

即有树形依赖的背包问题,例如AcWing.324

题意简述:给定一棵树,每个节点都有代价Wi,你可以选择节点x并付出Wx的代价得到xx的子树,求得到m个节点的最小代价

我们发现,对于u,它的任意子节点v只能选择一个状态转移到u,这里就是一个分组背包模型
F[u][t]表示在以u为根的子树中,选择t个节点的最小花费

这是一个背包模型:我们有p=|Son(u)|(u的子节点集合)组物品,每组物品有着siz(x)&nbsp&nbsp&nbsp&nbsp&nbsp(xSon(u))&nbsp&nbsp个,每组物品只能最多选择一个(因为只能有一个状态转移到父节点),第i组第j个物品有着体积Vi,j和代价Wi,j,从中选出若干个物品,使得体积之和为m,并且代价最小

这里的状态转移方程就很好写了

F[u][t]=minvSon(u)min0kmin(t,siz(v))F[v][k]+F[u][tk]

当然这个循环顺序还是按照阶段->状态->决策安排,即我们将v放在最外层,将t放在第二层(注意倒序),将k放在最里面

在最后我们还要考虑一下整体买整个子树的代价

即:F[u][j]=min(F[u][j],F[u][max(0,jsiz(u))]+Wu)

最后提一下本题其实是若干棵树,也就是一个森林,遇此情况我们一般都是建立一个虚拟节点0(统计答案时排除),将0作为森林中若干树的根节点的父亲最后在这颗新树上统计即可

void dp(int u){
	siz[u]=1;
	f[u][0]=0;
	for(int i=head[u];i;i=nxt[i]){
		int v=ver[i];
		dp(v);
		siz[u]+=siz[v];
	}
	for(int i=head[u]ii=nxt[i]){
		int v=ver[i];
		for(int t=siz[u]t>=0t--){
			for(int j=0j<=siz[v]++j){
				if(t>=j)f[u][t]=min(f[u][t-j]+f[v][j],f[u][t]);
			}
		}
	}
	if(u!=root){
		for(int i=0i<=siz[u]i++)f[u][i]=min(f[u][i],cost[u]);
	};
};

总结:其实也就是换汤不换药,背包的式子都没变,只是把以前的顺序枚举变成了枚举子树

顺带提一下,有一种方法,是把多叉树转为二叉树,以此更好编码统计,但不提倡,因为会耗费额外空间,并且容易混淆

二次扫描与换根法

适用范围:对于一类在无根树上的统计问题,需要枚举各个点为根节点进行统计的时候复杂度O(n2M),其中M是统计的额外操作时间,用此法可将复杂度消去一个n

思想阐述:第一次dfs,任选一个根节点rt,统计信息,第二次dfs借助第一次dfs的信息,这里假设我们以节点u为根时答案为Fu,则我们对每一个vSon(u),因为u>v之间只有一条边的差异,所以我们可以在一般O(1)的时间内统计出将根节点从u改为v的时候对答案的影响(需要的信息需要在两次DFS中统计出来)从而计算出Fv再继续递归下去:

其本质仍然是充分利用已知信息,避免冗余计算

例如计算机此题

题意简述:给定一颗带权无根树,求出每个节点在树中距离最远的点

现在用二次扫描与换根法的思路来解决这个问题

首先我们先来分析我们从一个节点u转移到v,  (vSon(u))的时候需要哪些信息:

设在树中距离节点i最远的节点为fi

对于一个节点u的最长路有两种情况:在树中(fuu中)和不在树中(此时是ufu的子树中)

而对于v也有两种情况:v在u的最长路上(此时fu必须在u的子树里),v不在u的最长路上

而如果是第一种情况,转移的话需要知道u在树中的次长路,和u在树外的最长路,因为这两个值是有可能更新答案的

综上分析,我们需要维护三个信息:1.u在自家树中的最长路。2.u在自家树中的次长路。3.u在自家树外的最长路

其中信息1.2是可以在第一次dfs中找出来的,信息三的话,我们之前提到过:u在自家树外的最长路,此时u是在这个点的树里的,故我们可以在树自顶向下的时候统计求出

所以我们可以设fu,0/1/2分别表示树内最长路,树内次长路,树外最长路,借此换根统计答案即可。

int dp(int u,int fa){;
    if(f[u][0]>=0)return f[u][0];
    f[u][0]=f[u][1]=f[u][2]=lst[u]=0;
    for(int i=head[u];i;i=nxt[i]){
        int v=ver[i],w=cost[i];
        if(v==fa)continue;
        if(f[u][0]<dp(v,u)+w){
            f[u][1]=f[u][0];
            f[u][0]=dp(v,u)+w;
            lst[u]=v;
        }
        else if(f[u][1]<dp(v,u)+w)
            f[u][1]=dp(v,u)+w;
    }
    return f[u][0];
}
void dfs(int u,int fa){
    for(int i=head[u];i;i=nxt[i]){
        int v=ver[i],w=cost[i];
        if(v==fa) continue;
        if(v==lst[u])f[v][2]=max(f[u][1],f[u][2])+w;
        else f[v][2]=max(f[u][0],f[u][2])+w;
        dfs(v,u);
    }
}

最后再次强调,我们在程序设计时,一定要有充分利用所有信息以及时间,空间

posted @   spdarkle  阅读(168)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示