常见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\)个物品体积记作\(V_i\),价值记作\(W_i\),要求从中选出若干个,使得总体积不超过\(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);
                  

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

01背包

\[f_{i,j}=\max(f_{i-1,j},f_{i-1,j-v[i]}+w[i](j \ge v[i])) \]

完全背包

\[f_{i,j}=\max(f_{i-1,j},f_{i,j-v[i]}+w[i](j \ge v[i])) \]

多重背包

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

思路

1.暴力拆他死了

2.二进制拆分

先上复杂度 \(\Theta(nM\log E)\) 其中\(E\)\(C_i\)的值域

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

\[2^0+2^1+…2^p \le C_i \]

且这个\(p\)最大

此时我们就可以将\(C_i\)拆分成\(p+2\)个部分,即:
\(2^0,2^1,2^2,…,2^p,C_i- \sum_{i=0}^p2^i\),最后那个式子可以由等比数列求和公式改为:\(C_i-2^{p+1}+1\)

这样我们就可以把一共\(\sum_{i=1}^nC_i\)个物品拆为少于\(n \times \log E\)个物品,成功地掉了复杂度,一般已经够用

3.单调队列优化

见下:单调队列优化DP

分组背包

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

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

\[f_{i,j}=\max(f_{i-1,j},\max_{1\le j \le C_i}f_{i-1,j-V_{i,k}}+W_{i,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.对于带环状的区间,我们可以将\(1\sim n\)复制一倍接在后面形成一个\(2n\)的链,在其上进行统计,最后枚举所有长度为\(n\)的区间\([i,i+n-1](1\le i\le n)\)

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

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

树形DP与背包

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

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

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

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

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

\[F[u][t]=\min_{v\in Son(u)}\min_{0\le k\le \min(t,siz(v))}{F[v][k]+F[u][t-k]} \]

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

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

即:\(F[u][j]=\min(F[u][j],F[u][\max(0,j-siz(u))]+W_u)\)

最后提一下本题其实是若干棵树,也就是一个森林,遇此情况我们一般都是建立一个虚拟节点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(n^2M)\),其中M是统计的额外操作时间,用此法可将复杂度消去一个\(n\)

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

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

例如计算机此题

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

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

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

设在树中距离节点\(i\)最远的节点为\(f_i\)

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

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

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

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

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

所以我们可以设\(f_{u,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 @ 2022-11-30 22:34  spdarkle  阅读(130)  评论(0编辑  收藏  举报