DP小整理

DP的整理

DP的闲谈

DP主要的核心就是对于每道题专属的状态转移方程

动态规划过程是:

每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划(DP)。

基本思想与策略

基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

动态规划的算法设计

1:找出最优解的性质,并描述其结构特征

2:递归定义最优值

3:以自底向上的方式计算最优值

4:根据计算最优值时得到的信息构造出最优解

能采用动态规划求解的问题的一般要具有3个性质

1.有最优子结构:

如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,问题的最优解能够由局部子问题的最优解得出。

2.无后效性:

$Yesterday \ \ is\ \ gone ,\ \ now \ \ determines \ \ the \ \ futrue. $
即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关,
如在去买西瓜的时候,今天买多少西瓜会对明天是否买西瓜造成影响,但是不会影响昨天买西瓜(西瓜多了少了,昨天与现在无关,现在只需要考虑的是现在要不要买西瓜,和考虑一下买多少,明天需不需要买,是否够之类的)。

3.有重叠子问题:

即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。

使用动态规划求解问题,最重要的就是确定动态规划三要素

求解动态规划的具体步骤:

(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

普通 DP

简介 :

这个就比较简单了, 准确的来说,主要就是推导出状态转移即可。没有什么特别难的,具体的来说就是,尽量将所有的情况,(或者说,状态的来源) 考虑清楚 。
以一道题为例

例题 P4059 [Code+#1]找爸爸

【description】

见题面

【solution】

首先,分析一下题意,发现,选择的空格只选择一个要比选择2个要优秀,所以我们的状态就设为 \(f_{i,j,k}\) ,状态意为匹配 \(A\) 串的前 \(i\) 个字符, \(B\) 串的前 \(j\) 个字符,最后一个空格的位置在 \(k\) , 其中令 \(k\in[0,2]\) ,分别表示在哪一个串候加空格 的最大相似度

所以我们就有 \(4\) 种的情况

  • \(A,B\) 串后面都不加空格
  • \(A\) 串后面加空格,
  • \(B\) 串后面加空格
  • \(A,B\) 串后面都加上空格

其中由于选择 \(1\) 个空格要比选择两个空格优秀,所以,上述的第四种情况就可以省去了。

同样的状态转移就对应着

\(f_{i,j,0} = max(f_{i - 1 , j - 1 , 0} , f_{i-1,j-1,1} , f_{i - 1 , j -1 , 2 }) + d_{x_i , y_j}\)
表示如果不加空格的话,那就是前面的状态取个 \(max\) 加上当前节点的贡献
\(f_{i,j,1} = max(f_{i,j-1,0} - a , f_{i,j-1,1} - b , f_{i,j-1,2} - a)\)
在取得最大值时,从 \(f_{i,j,k}\) 中选择最大值,同时,当 \(k=1\) 的时候,我们知道是在 \(A\) 串后面加上了一个空格, 所以我们选择减去一个 \(b\) , 否则我们就减去一个 \(a\)
\(f_{i,j,2} = max(f_{i-1,j,0} - b , f_{i-1 ,j,1} - b ,f_{i-1,j,2} - a)\)

这道题的 \(f\) 数组的初始化有点 \(……\) 我是按照题解的写的,我的初始化,死了,只能拿到 \(20pots\) ,题解的 \(f\) 的初始化就可以 \(A\) 掉 。
类似于上述

【Code】

其他题目 :

守望者的逃离 普通的线性 \(DP\) ,分类讨论一下当前的魔力即可。
摆花 计数类DP,直接推出状态转移方程即可
P3842 [TJOI2007]线段,个人认为比上述两个难些,分类讨论一下节点的初始转移即可
P1541 [NOIP2010 提高组] 乌龟棋 暴力 \(DP\) ,很裸的题目

区间DP

区间DP比较模板化,这么说吧,你首先直接定义 \(f_{i,j}\) 表示 \(i\to j\) 这个区间内的贡献,如果你有一个特殊的限制,也就是这个只有 \(i\to j\) 这个区间维护不了了,那么我们就可以直接起一维 \(k\) , 也就是 \(f_{i,j,k}\) 表示 \(i\to j\) 这个区间内,满足 \(k\) 这个特殊限制的贡献,解题的话,一般就是几个思路

  • 可能是一群人围成一个环,这个时候我们的策略是,断环成链 ,我们假设 \(num\) 是维护的是 这个区间的数,那么我们就让 \(num_{n+i} = num_i\) 那么也就是我们直接将这个区间复制一份放到后面即可 。

  • 同时解题的同时,我们一般情况下,第一层枚举 区间的长度 \(len\) , 然后我们枚举左端点 \(l\) , 那同时我们就可以得到右端点 \(r = l + len - 1\) ,最后我们再进行从 \(l \to r\) 中间枚举断点 \(k\) , 我们的状态转移方程一般就是 $f_{i,j} = max(f_{i , k} + f_{k + 1 , j} + num_k) $ 之类的,然后进行统计答案的话,就是按照题目提供的信息,进行区间合并,找到 \(ans\)

例题1 P1880 [NOI1995] 石子合并

【description】 :

区间 \(DP\) 模板题

【solution】:

首先我们很显然的明白,这个是一个环形 \(DP\) , 那么我们首先进行断环成链,同时由于合并 \(i\to j\) 这个区间内的时候,我们需要加上这两堆原来的得分,得到现在的得分,所以我们用一个前缀和统计下 ,从而也能够找到最大值的状态转移方程为

\[f_{l,r} = f_{l , k} + f_{k + 1 , r} + sum_r - sum_{l - 1} \]

最小值同理可得

【Code】

例题2 CF607B Zuma

【description】 :

每次取出一个回文串,问最少几次能够取完。 其中一个字符也算一个回文串,取完之后两边可以连接

【solution】 :

我们仍是按照模板化而来,设 \(f_{i,j}\) 表示合并区间 \(i \to j\) 的贡献,那么我们进行手模一下
\(f_{i , i+1} = 1 | 2\)
,如果 \(num_i == num_{i+1}\) , f_{i , i +1} = 1 , 否则为 $f_{i , i + 1} = 2 $
其他的情况,按照题意,我们需要进行的判断一下两边的端点是否是一致的即可, ,那么也就是得到

\[f_{i,j} = f_{i+1,j-1} | num_i == num_j \]

因为这时候两端的数相等,这时候的贡献是没有的

\[f_{i,j} = f_{i , k} + f_{k + 1,j}|k\in [l , r) \]

合并区间。

【Code】

其他题目

能量项链 环形DP,断环成链的模板题
涂色 模板题
P3146 [USACO16OPEN]248 G 和石子合并类似
UVA12991 Game Rooms 利用区间DP的思想从而引出正解 。

树形DP

树形DP的考虑主要在状态设计和来源上,节点 \(i\) 的节点信息一般来源于 \(i\) 的子节点,状态设计也一般情况下就是以 \(i\) 节点为根的子树的什么什么状态之类的。

例题1 没有上司的舞会

【description】

树形 \(DP\) 模板题 , 同时也是最大权独立集的模板题

【solution】 :

\(f_{i,1|0}\) 表示节点 \(i\) 选择或者不选的这一棵子树的最大欢乐值 。 那么状态来自于它的子节点,同样的也就是 \(f_{i,0}\) 这个节点不选择的话,那么它的状态可以是从 \(f_{son_i , 0}\) 或者 \(f_{son_i , 1}\) ,也就是当前的节点不选择,它的节点可以选择也可以不选择。
同样的 \(f_{i,1}\) 那么就只能够从 \(f_{son_i , 0}\) 中取得,也就是说,节点 \(i\) 选择的话,节点 \(son_i\) 不能够被选择 。

同时,观察题目性质,我们发现是进行求和,那么状态转移方程即为

\[f_{i,0} = \sum_{k=son_i} max(f_{k , 1} , f_{k , 0}) \]

\[f_{i,1} = \sum_{k = son_i} f_{k,0} \]

然后就解决了

【Code】

例题2 三色二叉树

【description】:

见题面

【solution】:

【Code】

其他的题目:

详细的题单

数位DP

如果一个数据的范围是 \(1\)\(10^{18}\) ,或者更高,但是需要你统计这个区间内的数字出现的个数或者一些关于数字的操作,这个时候就用到数位 \(DP\) , 一开始会感觉数位 \(DP\) 的记忆化搜索比较难理解,(个人喜欢写记忆化搜索的数位 \(DP\) ) ,还是 \(YJK\) 强, 笔者的数位 \(DP\) 亦是他教的 。
记忆化搜索还是比较模板化的。

基本上也就是这个模板,这个题是其他的题目,但是由于笔者的惰性,干脆粘了过来。

例题1 数字计数

【description】:

询问在一个区间内,每个数字出现了几次。

【solution】:

首先我们是有一个 \(O(n)\) 的思路的,就是直接枚举一下,然后开个桶,挨着挨着进行统计。显然 \(10^{12}\) 不是那么可做。
我们就可以考虑数位 \(DP\) 。我们从高到低位进行枚举,通过记忆化实现爆搜。
在这个爆搜内,我们需要记录的:

  • 我们需要当前搜索到位数(这是显然的)
  • 当前位置的数(也就是进行枚举的时候,我们枚举不能超过当前数字,举个例子,我们枚举 \(123\) 的时候,我们是不能枚举 \(133\) 的,也就是在 \(2\)那个位置的时候,我们枚举的上界就是 \(2\) ,同样的,没有就是 \(9\))
  • 记录一下是否到了当前位置的最大值的限制(就是如果 \(123\) 其中我枚举了 \(11?\) ,那么这个 \(?\) 是不受限制的,因为你的 \(2\) 的位置没有达到 \(2\) ,所以我们需要记录一下)
  • 前导零,显然有的题目是不能有前导零的出现的,这个题,没有说明非要不出现

然后我们就可以得到记忆化搜索的代码(你问我递推的怎么写,我不会)

int dfs(int id , int sum , int num , int limit , int zero) 
{
	if(!id) return sum ; 
	if(~f[id][sum][limit][zero] && !limit ) return f[id][sum][limit][zero] ; 
	int up = limit ? s[id] : 9 ;//枚举上界
	int ans = 0 ; 
	for(int i = 0 ; i <= up ; i++) 
	{
	    //需要考虑一下是否有前导零的存在。
		if(zero && (i == 0 )) ans += dfs(id - 1, sum , num , 0 , 1) ;
		else ans += dfs(id - 1 , sum + (i == num) , num , limit && (i == up) , 0 ) ;  
	}
	if(!limit) f[id][sum][limit][zero] = ans ; 
	return ans ; 
}

【Code】

例题2 P4124 [CQOI2016]手机号码

【description】

询问 \(l\to r\) 中,不同时存在 \(8,4\) 的数字的个数。
我们发现这个对于模板题,增加了两条限制,分别是 \(8,4\) ,我们就可以通过上述对最当前的位置的数字的处理,从而进行 \(8,4\) 的处理。准确的说,和模板的区别,就是多了俩维 。
不过就是在代码实现的时候,会发现如果不判断一下手机号码是否为 \(11\) 的话,会挂掉 3 个点,很好奇,这竟然不给出手机号码,不符合实际的数据。

【Code】

其他题目

Windy数 数位DP模板题
P2481 [SDOI2010]代码拍卖会有技巧性的数位DP

状态压缩DP

前置位运算,先占个坑,以后补

有的时候我们进行设计状态的时候会不满足无后效性,或者说,我们保存的这个信息不足以进行决策,这个时候,可能需要一个一维的数组来记录一下,才能够进行一下决策,一个决策一个数组,显然,是不行的。同时,状态压缩就是考虑一下,是否能将这个状态进行压缩一下,也就是,将整个状态进行一下打包让他成为一个能进行决策的状态设计,这种情况下,往往我称他们为状态,这个状态不是 \(dp\) 设计状态之中的状态,这个状态是在当前局面的整个状态。还是以一个题目为例吧

例题1 互不侵犯

【description】

给定一个 \(n\times n\) 的方格中选择 \(k\) 个点,保证一个九宫格内只存在一个中心一个点,求解方案数 。

【solution】

首先我们发现,如果不知道状态压缩DP的话,我们很难说明,当 \(i,j\) 这个节点的时候,其实我们只需要考虑一下左上,上方,左边,右上,即可。在进行下一个状态的时候,也就是在下一行的时候,它必然会考虑到我们这个节点的下部分,下一列中放一个,必然会考虑到这个节点的右边,所以是不需要考虑全部的九宫格的。那么我们在考虑的这个节点的 \(DP\) 的时候,我们可以这么做

  • 首先我们在枚举到一个节点的时候,我们分别通过搞一下该点的其他的四个方向,其中,也因为是要搞一下方案数,我们还需要找一下上一次放的,很显然,每一个节点我们甚至可能需要开一个二维数组,显然这是不行的。

  • 我们考虑换一下思路,我们可不可以直接转化成一个局面呢,也就是我们直接将这一行给搞下来是否可行呢。那么这就叫做状态压缩,我们通过二进制数转化,形成一行的局面,也就是我们可以用 \(11\) 来表示 \(1011\) 这个局面,我们如果设 \(1\) 代表这个行有放置的节点,那么其意思为第 \(1,3,4\) 列有节点,这样我们就记录下来一个行的局面了。

那么我们就有状态了 $f_{i,j , k} 表示放了 \(i\) 行$ ,放了 \(j\) 个节点,并且第 \(i\) 行状态为 \(k\)

状态转移为

\[f_{i,j,k} = f_{i-1,j,k-num_j} \]

其中 \(num_j\) 表示的是 \(j\) 状态的 \(1\) 有多少个,放多少个节点,因为总共是要放 \(k\) 个的。

统计答案的时候,就是 \(ans =\sum_{k = 1 } ^{cnt} f_{n , j , k}\)

【Code】

其他题目

邦邦的合唱队
砝码称重 个人认为这一道不错
宝藏
寿司晚宴

期望和概率DP

准确无疑的说,概率 \(DP\) 是相对来说简单点的。

杂言

一开始我是不认为它隶属于 \(DP\) 之中的,因为它缺乏正常 \(DP\) 的决策,而是直接乘上一个概率,所以我并不认为是一个 \(DP\) ,直到我对于 \(DP\) 有了新的理解之后,我才明白,这也是个一个 \(DP\)\(DP\) 满足的是最优子结构和无后效性和重叠子问题,最主要的就是最优子结构和无后效性,很显然是满足的。

  • 还是以一道题目为例吧,这么说,我这个蒟蒻也说不清楚。

例题1 钉子和小球

【description】:

这见题面吧。

【solution】:

状态是很容易得出来的,就是设 \(f_{i,j}\) 表示到达 \((i,j)\) 这个节点的概率,我们这个计算的时候用的是概率乘法原则(是不是无所谓了,理解有这个东西就好,我不尴尬尴尬的就是你,看破不说破),我们在考虑的时候我们只需要考虑一下当前节点的状态(也就是钉子的有无),如果这个节点没有钉子,那么这个球滚下来的时候就可以直接斜着滑下去(不考虑摩擦力和重力对其造成的下降影响) ,同时如果这个节点有钉子的话,那么这个节点跌下去可能会向左飞,也可能会向右飞。其中概率应该是给出的。从而就计算完成了。

对于代码的实现,我们可以直接搞出 \(2 ^{n+1}\) 来。然后在进行一步步的概率推移,就推导出来了。

【Code】

总体的来说,对于一个概率计算,就可以看成一个子结构,同时,一个结构中的概率,就可以看成一个决策,准确的来说,概率DP形成的类似于一个DAG,因为你不可能一个点在同一个问题中是多种概率,否则以后的计算全部都将被打乱。

\(update:\) \(2021.3.7\) 玩我又回来填坑了。

期望DP

首先我们清楚一下什么是期望。
以我浅显的理解 : 所有事件获得的贡献与其概率的乘积之和
也就是我们得到(我们假设 \(ans\) 为我们需要的期望) \(ans = \sum a_i \times p_i\) 我们发现,这个和我们加权平均值貌似是一样的,我们也可以这么理解。就是期望就是在某一事件中的平均贡献

举个例子吧 ,就是我们有一个骰子,但是这个骰子因为某人心怀不轨,给灌了铅,导致转到 \(1\) 的概率是 \(40\text{%}\) ,转到 \(2.3,4,5,6\) 的概率是 \(12\text{%}\)
那么我们的期望就是这么算 \(ans = 1 \times 40\text{%} + 2\times 12\text{%} … + 6\times 12\text{%}\)

那么理解完期望之后我们就可以做一下期望 \(DP\) 的题目了。

例题1 P1365 WJMZBMR打osu! / Easy

【description】 :

给定一个字符串,我们可以根据其中的字符获得一定的贡献,如果有连续的 \(a\)\(o\) 出现,那么就会获得价值 \(a^2\) ,如果一个字符是 \(\text{?}\) ,那么有一半的概率是 \(o\) 或者其他

【solution】:

首先我们且不说关于 \(\text{?}\) 怎么处理,我们先处理一下只有 \(o\)\(x\) 的情况,我们直接设一个 \(len\) 表示当前已经累计了多少个 \(a\) 了,那么我们这个时候 \(f_i\) 就表示为 \(f_i = f_{i-1} + (len + 1)^2 - len^2\)
\(f_i\) 表示前 \(i\) 个字符,得到的贡献是多少。
那么同样的,如果是 \(x\) 的话,那么 , \(f_i = f_{i-1}\) 同时把 \(len\) 清空掉
最后就是处理 \(\text{?}\) 这个事情了,首先我们是有 \(50\text{%}\) 的可能性为 \(o\) 的,至于为什么不考虑 \(x\) ,因为当字符为 \(x\) 的时候,对于答案没有任何的贡献。
那么这个期望就是
\(( (len + 1) \times 50\text{%} + 0 \times \text{50%} )\)
前面的就是为 \(o\) 的贡献 , \(0\) 显然。最后再加上 \(f_{i-1}\) ,状态转移就完成了。
同时对于 \(len\) ,我们让其成为 \(\frac{(len + 1 ) + 0}{2}\)

【Code】

posted @ 2020-12-02 10:44  SkyFairy  阅读(156)  评论(0编辑  收藏  举报