DP总结

7 篇文章 0 订阅
3 篇文章 0 订阅

一. 本质

递归转递推。

二. 前提

  1. 问题具有最优子结构性质。如果问题的最优解所包含的 子问题的解也是最优的,我们就称该问题具有最优子结 构性质。
  2. 无后效性。当前的若干个状态值一旦确定,则此后过程 的演变就只和这若干个状态的值有关,和之前是采取哪 种手段或经过哪条路径演变到当前的这若干个状态,没有关系。

三.动规解题的一般思路

  1. 将原问题分解为子问题
    即将原问题的规模缩小,得到该问题的子问题,再将子问题进一步细化。
  2. 确定状态
    用几个变量能表示当前的状态,并能通过当前的状态定义推出更大的子问题。
  3. 确定一些初始状态(边界状态)的值
    如dp[1]=1或2等;
  4. 确定状态转移方程
    由子问题转换到当前问题;
    ###四.相关模型
    1.路径模型(E.G.数字三角形)
    特点:能够明确的在一个矩阵或其它图形中体现出来,并且能由其它的点转移过来。
    状态:dp[i][j]表示走到点(i,j)时的最优解。(该题即代表从下到上走到(i,j)时的最大 总值)
    转移:本题:dp[i][j]=max(a[i][j]+dp[i+1][j];a[i][j]+dp[i+1][j+1])
    目标状态:dp[1][1],即从下到上走到顶端时的最优(大)解。
    时间:O(nn)
    时间优化:无
    空间:O(n
    n)
    空间优化:O(n)(滚动数组):
    dp[i]表示如果当前选择了该行的第i个数,那么能够得到的最大的路(和)。直接用已经存在的dp里面的值来自我更新即可。
    实现代码:
int dp[i][j];
for(int i=1;i<=n;i++)					//初始化
	dp[n][i]=a[n][i];
for(int i=n-1;i>=1;i--)
	for(int j=1;j<=I;j++)
dp[i][j]= max(a[i][j]+dp[i+1][j];a[i][j]+dp[i+1][j+1]);

输出方式:
可另外开一个way[n][n],其中way[i][j]存到i+1行时是走左边还是右边。最后即可从第一行到第n行模拟就可以了。
例题:
(1).数字三角形
(2).花店橱窗布置
(3).滑雪
2.最长上升子序列
特点:没有一个固定的起点,需要自己枚举。然后在枚举出来的起点与终点之 间枚举之前算出来的最优值,然后优中取优即可。
状态:dp[i]表示从i到末尾(包括i)的最长上升子序列的长度。
转移:dp[i]=max(dp[k]+1,dp[k])
目标状态:max(dp[all])
时间:O(n*n)
时间优化:无
空间:O(n)
空间优化:无
实现代码:

int dp[MAXN+5],ans;
for(int i=1;i<=n;i++)
	dp[i]=1;						//初始化
for(int i=n-1;i>=1;i--)
{
	for(int j=i+1;j<=n;j++)
		dp[i]=max(dp[j]+1,dp[i]);
	ans=max(dp[i],ans);			//ans即为答案
}

输出方式:
可以将dp[n][n]改为dp[n][n][2],使用类似于father节点的方式来存某个状态中的最优子问题(答案)。
dp[i][j][0]仍为之前的dp数组,dp[i][j][1]是它的最优子问题的位置。
例题:
(1).最长上升子序列
(2).拦截导弹(最长上升子序列+最长下降子序列)
(3).嵌套矩形
(4).渡轮问题
(5).合唱队形
(6).尼克的任务
3.最长公共子序列
特点:
需要多种情况的分类讨论,并结合当前两个串的处理位置的关系(相等或不同),不同时又需讨论如何将指针向前移动。
状态:
dp[i][j]表示A串从i位置、B串从j位置开始,向前找能找到的最长上升子序列。
转移:dp[i][j]=:if(A[i]==B[j]) dp[i][j]= dp[i-1][j-1]+1;
if(A[i]!=B[j]) dp[i][j]=max(dp[i-1][j]+1,dp[i][j-1]+1);
目标状态:dp[n][n]
时间:O(nm) //n代表A串的长度,m代表B串的长度
时间优化:无
空间:O(n
m)
空间优化:无
实现代码:

int dp[MAXN+5][MAXN+5];
dp[0][1]=0,dp[1][0]=0,dp[0][0]=0;
for(int i=1;i<=n;i++)
	for(int j=1;j<=n;j++)
		if(A[i]==B[j])
			dp[i][j]=dp[i-1][j-1]+1;
		else
			dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
	输出方式:
	例题:
		(1).求最长公共子序列

4.0-1背包(E.G.:满足背包容量的最大值)
特点:
当前位置(i)的最优解不一定包括当前所指向的物品(i),但与上一个情况的有关(i-1)。
状态:
dp[i][tot]表示从第一个到第i个满足总体积<=tot的最优解(最大值)。
转移:
if V[i]<=tot
则dp[i][tot]=max{ dp[i-1][tot-V[i]] , dp[i-1][tot] )
else
dp[i][tot]=dp[i-1][tot]
目标状态:dp[n][totV]。
时间:O(ntotV)
时间优化:无
空间:O(n
totV)
空间优化:O(totV)
将dp[n][totV]变为dp[totV],dp[tot]表示当满足总体积<=tot时的最大值。
单独考虑某一个物品,考虑其是否能放进体积为tot的背包中。但要从后往前枚举tot,防止同一种情况下一个物品被用两次(与完全背包正好相反)。
目标状态:dp[totV];
实现代码:
普通实现(二维)

int dp[MAXN+5][MAXV+5];
for(int i=1;i<=n;i++)
	dp[i][0]=0;
for(int i=1;i<=totV;i++)
	dp[0][i]=0;
dp[0][0]=0;
for(int tot=1;tot<=totV;tot++)
	for(int i=1;i<=n;i++)
		if(V[i]<=tot)
			dp[i][tot]=max(dp[i-1][tot-V[i]]+val[i],dp[i-1][tot]);
			//								选它            不选它
		else
			dp[i][tot]=dp[i-1][tot];
			//	由于当前物品不可能放进去,那么就是不选它的情况。

空间优化实现

int dp[MAXV+5];
dp[0]=0;
for(int i=1;i<=n;i++)
	for(int tot=totV;tot-V[i]>=0;tot--)
		dp[tot]=max(dp[tot-V[i]]+val[i],dp[tot]);

5.区间DP
特点:
考虑的是一段区间以内的影响,同时区间的左右端点也需要枚举,在不优化的时候复杂度高达O(nn),但是通常可以套各种优化。
状态:(加法问题)
dp[i][j] //用了i个加号,当前位置在j
转移:
dp[i][j]=max{dp[i-1][k]+num(k,j)}
目标状态:dp[m][n]//有m个加号,string的长度为n
时间:O(n
m)
时间优化:(加法问题好像无法优化掉一样)
通常可以用单调队列or斜率优化(有时可以强行)or高级数据结构,甚至是CDQ分治来优化到O(nlogn)
空间:O(nm)
空间优化:O(m)(与时间一起优化掉)
实现代码:
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=1;k<=j-2;k++)
dp[i][j]=max(dp[i][j],dp[i-1][k]+GetNum(k,j));
例题2:邮局
状态:dp[i][j]表示当前已经处理完了前面的i个村庄,其中有j个有邮局
转移:dp[i][j]=min{dp[k-1][j-1]+sigma{pos[p|p∈[k,j]]-pos[(j+k)/2]}}
目标状态:dp[n][m]//有n个村庄,m个邮局
时间:O(n
nm);
空间:O(n
m)
实现代码:

for(int i=1;i<=p;i++)
    for(int j=i;j<=v-(p-i);j++)
       	for(int k=i;k<=j;k++)
        {
          	int mid=(j+k)/2;
      		int tot=0;
  			for(int m=k;m<=j;m++)
        		tot+=abs(pos[m]-pos[mid]);
        	if(dp[k-1][i-1]+tot<dp[j][i])
              	dp[j][i]=dp[k-1][i-1]+tot;
        }

6.区间DP2.0
特点:
定义的时候往往都要定义为O(nn)的形式,表示某一个区间内的最优答案。通常的形式是O(nn)枚举。且通常是枚举长度->枚举起始位置->枚举中间位置。
状态:(石子合并版本一)dp[i][j]表示将i到j这一段的石子合并的最小代价。
转移:dp[i][j]=min{dp[i][k]+dp[k+1][j]+sum[i][j]}
目标状态:dp[1][n]//总共有n堆石子
时间:O(nnn)
时间优化:无
空间:O(n*n)
空间优化:无
实现代码:

for(int i=1;i<=n;i++)
	dp[i][i]=a[i];
for(int len=2;len<=n;len++)
	for(int i=1;i+len-1<=n;i++)
	{
		j=i+len-1;
		dp[i][j]=INF;
		for(int k=i;k<=j;k++)
			dp[i][j]=min(dp[i][k]+dp[k+1][j],dp[i][j]);
		dp[i][j]+=sum[j]-sum[i-1];
	}

例题2:乘法游戏
状态:dp[i][j]表示从i到j抽空至只剩下i和j两个数时,得到的最大值。
转移:dp[i][j]=max{dp[i][p]+dp[p][k]+a[i]a[p]a[j]}
时间:O(n
n
n)
空间:O(n*n)
实现代码:

for(int i=2;i<=n-1;i++)
    dp[i-1][i+1]=a[i-1]*a[i]*a[i+1];
for(int i=1;i<=n-1;i++)
    dp[i][i+1]=0;
for(int k=3;k<=n-1;k++)
    for(int i=1;i<=n;i++)
    {
        int j=i+k;
        if(j>n)
           break;
        for(int p=i+1;p<=i+k-1;p++)
           dp[i][j]=min(a[i]*a[i+k]*a[p]+dp[i][p]+dp[p][j],dp[i][j]);
   	}

7.单调队列&单调栈优化
虽然有时是单调队列,有时又是单调栈,但是我更倾向于叫它单调队列(想成是deque…)。
单调队列优化首先是最简单的优化,与dp的优化无关。如POJ2559,让你求一个数列里面每个数向两边扩展最多能扩展到哪里(只要大于等于当前的数的数值就可以扩展一格)。
比较经典的思路就是左右各做一遍。举例来说从左往右做的时候,维护一个单调上升的队列,每次从队尾弹出旧的元素,发现最后留下的队尾的元素必定是大于当前枚举到的数的。假设队尾的元素的位置是j,那么从j+1到当前的位置都是合法的(大于等于当前元素)。从右往左扫的时候也是一样的。这样子处理完了之后后,把两边的答案拼接起来就是当前位置的答案了。
这里写图片描述
比如这张图中就是j+1到i的位置对于i来说都是合法的。
然而这只是最简单的单点队列优化…但是大多数题目的第一步优化就是这样子的,所以说是很有用的呢。


之前在学的时候做了一道神奇的题目。按道理来说,通常是要对dp式进行变形后,才能进行单调队列优化。而最常见的做法是转化为前缀和之差的形式。但是这道题,也就是优化多重背包(求最大价值总和),它的变形是将dp式同时减去一个值,使得进行转移的部分变得有规律,然后通过那个有规律的值进行转移。
这是最初始的转移式:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ v [ i ] ) f[i][j]=max(f[i-1][j-k*v[i]]+k*v[i]) f[i][j]=max(f[i1][jkv[i]]+kv[i])
然后我们进行变化之后:
f ′ [ j ] = m a x ( f [ j − k ∗ v [ i ] ] − k ′ ∗ v [ i ] + ( k + k ′ ) ∗ v [ i ] ) f&#x27;[j]=max(f[j-k*v[i]]-k&#x27;*v[i]+(k+k&#x27;)*v[i]) f[j]=max(f[jkv[i]]kv[i]+(k+k)v[i])
其中的k’为之前的已经加进去的当前物品的个数
这样还不是最神奇的部分。然后我们可以将j按照mod v[i]的余数进行分类,将余数相同的部分化成一堆,每一堆中进行转移。
证明如下:

若当前枚举到的状态为j,那么可以对当前造成贡献的状态为j-1v,j-2v,j-3*v…这些数mod v的余数都是j mod v。然后就证到了转移只会发生在同一个余数相同的块内。

然后我们就可以根据余数进行分类枚举。首先枚举余数,就相当于枚举了一个块,然后使用一个优先队列,维护一个单减的双向队列,每次先把当前的数值(设余数为d),也就是 d + k ∗ v d+k*v d+kv(k为系数),与队尾进行比较(注意,队列中存储的数值为 f [ i − 1 ] [ d + k ∗ v ] − k ∗ w f[i-1][d+k*v]-k*w f[i1][d+kv]kw)。然后再看队首,因为是多重背包,不是完全背包,所以说它的转移是有范围限制的。那么就需要从队首弹出无法转移到的元素(因为当前都无法从那里转移,那么之后也不可能从那里转移)。这样子处理完了之后,然后就从队首取出元素,自然就是我们需要的最优转移了。(转移的之后要记得加上当前的物品的总个数(其实就是枚举的系数))
优化之后,传统的多重背包问题,变成了 O ( n ∗ t o t V ) O(n*totV) O(ntotV)


然后最经典的当然是斜率优化了,它应该也算是单调队列优化的一种吧。
斜率优化也往往是通过对转移表达式进行转化后,设当前的决策点为i,转移点为j,那么对于i来说,所有的关于i的变量此时都是一个常量,所有的关于j的变量才是真正不定的。
那么我们可以把变量和常量分离开来,变量就可以形成一个表达式。
接着就假设两个转移点j1和j2,假设 j 1 &lt; j 2 j1&lt;j2 j1<j2,然后再假设j1的转移比j2的转移要优,就可以得到一个斜率的不等式。
假设是 Y [ j 1 ] − Y [ j 2 ] X [ j 1 ] − X [ j 2 ] &lt; C o n s t V a l [ i ] \dfrac{Y[j1]-Y[j2]}{X[j1]-X[j2]}&lt;ConstVal[i] X[j1]X[j2]Y[j1]Y[j2]<ConstVal[i],然后就可以写作 T ( j 1 , j 2 ) &lt; C i T(j1,j2)&lt;Ci T(j1,j2)<Ci
然后就可以根据X和ConstVal的单调性进行讨论,自然是两个都具有单调性最好…
(我一般是在队尾加入元素,在队头取出元素,其实可以根据个人喜好而定)
例如最大化平均值这道题,虽然和优化DP没设么关系,却是斜率优化的一道好题。
实际上就是让 p r e s u m [ i ] − p r e s u m [ j ] i − j ( j &lt; i ) 尽 量 大 \dfrac{presum[i]-presum[j]}{i-j}(j&lt;i)尽量大 ijpresum[i]presum[j](j<i)
然后就可以维护一个下凸包,然后我们可以直观地发现它其实就是求与当前曲线的切线即可。
但是又因为这里的presum具有单增的性质,所以我们可以从队首弹出后,就不再需要将其放回去了。也就是说,每一个元素最多会进去一次,出来一次,做到了O(n)。否则的话,我们还需要二分,然后就变成了O(nlogn)。
然而斜率优化并不是如此简单的。当X不具有单调性的时候,在插入的我们也需要二分;如果ConstVal不具有单调性或者说与不等号的方向相反时(也就是要保证随着i往后移,限制条件会越来越严,也是为了保证每个元素只会进出一次),我们就需要在取出的时候进行二分。
对于后者而言,需要二分的证明如下:

假如说当前的更新的点为i,然后我们所维护的图像为斜率单增的一条曲线,那么必定有如下的性质:

T ( j 1 , j 2 ) &lt; T ( j 2 , j 3 ) &lt; T ( j 3 , j 4 ) . . . . . . &lt; C o n s t v a l [ i ] T(j1,j2)&lt;T(j2,j3)&lt;T(j3,j4)......&lt;Constval[i] T(j1,j2)<T(j2,j3)<T(j3,j4)......<Constval[i]
C o n s t v a l [ i ] &lt; T ( j 1 ′ , j 2 ′ ) &lt; T ( j 2 ′ , j 3 ′ ) &lt; T ( j 3 ′ , j 4 ′ ) . . . . . . . Constval[i]&lt;T(j1&#x27;,j2&#x27;)&lt;T(j2&#x27;,j3&#x27;)&lt;T(j3&#x27;,j4&#x27;)....... Constval[i]<T(j1,j2)<T(j2,j3)<T(j3,j4).......

然后我们就发现ConstVal[i]一定是卡在斜率的值的中间的。假如我们的定义是T(j1,j2)大于constval表示j1比j2优,然后就有j4’比j5’优,j3’比j4’优,j2’比j3’优,j1’比j2’优…以此类推,然后我们就发现j1’其实是最优的转移。那么我们就可以二分j1’的位置,从而获得最佳转移点。

这样应该就解决了大多数情况的斜率优化了。
关于斜率优化是维护上凸包还是下凸包的问题:
首先假设当前的转移点是j1,j2,j3,然后有j1<j2<j3。以下讨论的前提都是X()和constval具有单调性的时候。
首先假设j2在某一时刻能够成为最优的转移点,那么就有j2比j1和j3都优,然后就能够得到两个不等式,再和之间的不等式进行比较,若出现矛盾,则不能够是上/下凸包。两个都试一下。
当不具有单调性的时候,往往通过CDQ分治或者是二分/三分查找来解决。
8.矩阵加速优化
首先应该有矩阵乘法的定义。
设相乘的两个矩阵是A和B,得到的目标矩阵是C
那么有:
C [ i ] [ j ] = ∑ ( A [ i ] [ k ] ∗ B [ k ] [ j ] ) C[i][j]=\sum(A[i][k]*B[k][j]) C[i][j]=(A[i][k]B[k][j])
那么这就要求A和B的行和列的数量应该是相等的。实际上“乘法”的定义是可以自己定义的,比如你可以魔改成:
C [ i ] [ j ] = m a x ( A [ i ] [ k ] + B [ k ] [ j ] ) C[i][j]=max(A[i][k]+B[k][j]) C[i][j]=max(A[i][k]+B[k][j])
从而适应各种各样的题目。
至于快速幂的话,就和普通的快速幂是一样的。(所以说我们更愿意将矩阵的乘法用重载运算符重载掉,方便调用)。同时我们需要注意一点的是,矩阵乘法的复杂度通常为 O ( n 3 ∗ l o g n ) O(n^3*logn) O(n3logn)的,谨慎使用。
其实对于矩阵乘法优化来说,最难的是发现可以这样优化和怎样构造矩阵。
首先,发现矩阵的话,我们可以发现,如果把A矩阵看成系数矩阵的话,转移式其实是线性的,也就是说,只有类似于 a n s = A 1 ∗ B 1 + A 2 ∗ B 2 + A 3 ∗ B 3 + A 4 ∗ B 4...... ans=A1*B1+A2*B2+A3*B3+A4*B4...... ans=A1B1+A2B2+A3B3+A4B4......的形式才能够进行矩阵优化。
接着就是如何构造矩阵了。这是难点中的难点。
首先是一个简单的例子,快速求斐波那契数列第i项。(i极大)
构造的矩阵可以是:
这里写图片描述
然后就可以求了。
下面是两种比较特殊的情况。


首先是矩阵套矩阵:
例:给一个n*n的矩阵A,然后求S=A + A^2 + A^3 + …+ A^k.
这道题我们可以构造如下的矩阵:
这里写图片描述
然后就可以套矩阵套矩阵快速幂的板了。
然后是分块矩阵快速幂。最为经典的还是“沼泽鳄鱼”这道题。
题意大概就是一个无向图,一些食人鱼在进行有周期的运动(周期在2到4之间)。你要从s走到t,中途不能在一个点停留,一共走K个单位之间,问有多少种走法。
因为周期只能是2,3,4,所以说每12个单位时间后,所有食人鱼肯定都回到了原来的位置,也就是说整张图的食人鱼的位置分布最多有12种,也就是说12个单位时间是一个周期。然后就根据12对K进行分块,就可以做到 O ( K 12 ∗ n 3 ∗ 12 + n ∗ n ∗ n ∗ ( K m o d 12 ) ) O(\dfrac{K}{12}*n^3*12+n*n*n*(K mod 12)) O(12Kn312+nnn(Kmod12))的时间内解决。
当然我们要处理12个单位时间的总的邻接矩阵,把可以走的邻接矩阵的食人鱼所在的位置变为0再乘到totG里面去就可以了。
至于后面的k%12的部分暴力处理就好了。

差不多就是这样了,如果说还有什么地方博主写错或者写掉了,请帮忙提醒一下哦~
博主后期也会继续补充的呢

  • 1
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值