0x51 线性DP

0x51 线性DP

动态规划算法把原问题视作若干个重叠问题的逐层递进,每个子问题的求解过程都构成一个“阶段”。在完成前一个阶段的计算后,动态规划才会执行下一个阶段的计算。

为了保证这些计算能够按顺序、不重复地进行,动态规划要求已经求解的子问题不受后续阶段的影响。这个条件也被叫做“无后效性”。换言之,动态规划对状态空间的遍历构成一张有向无环图,遍历顺序就是该有向无环图的一个拓扑序。有向无环图中的节点对应问题中的“状态”,图中的边则对应状态之间的“转移”,转移的选取就是动态规划中的“决策”。

在很多情况下,动态规划用于求解最优化问题。此时,下一阶段的最优解应该能够由前面各阶段子问题的最优解导出。这个条件被称为“最优子结构性质”。当然,这是一种比较片面的说法,它其实告诉我们,动态规划在阶段计算完成时,只会在每个状态上保留与最终解集相关的部分代表信息,这些代表信息应该具有可重复的求解过程,并能够导出后续阶段的代表信息。这样一来,动态规划对状态的抽象和子问题的重叠递进才能够起到优化作用。

状态”“阶段”和“决策”是构成动态规划算法的三要素,而“子问题重叠性”“无后效性”和“最优子结构性质”是问题能够使用动态规划求解的三个基本条件。

动态规划算法把相同的计算过程作用于各阶段的同类子问题,就好像把一个固定的公式在格式相同的若干输入数据上运行。因此,我们一般只需要定义出DP的计算过程,就可以编程实现了。这个计算过程被称为“状态转移方程”。

具有线性“阶段”划分的动态规划算法被统称为线性DP。大家对于最长上升子序列(LIS)、最长公共子序列(LCS)、数学三角形等DP的经典入门例题应该并不陌生。我们尝试分析一下DP要素在其中的体现。

1.最长上升子序列(LIS

LIS问题
问题描述 最长上升子序列。给定一个长度为 N N N的数列 A A A,求数值单调递增的子序列的长度是多少。 A A A的任意子序列 B B B可表示为 B = A k 1 , A k 2 , . . . , A k p B={A_{k_1},A_{k_2},...,A_{k_p}} B=Ak1,Ak2,...,Akp,其中 k 1 < k 2 < . . . < k p k_1k1<k2<...<kp
状态表示 F [ i ] F[i] F[i]表示以 A [ i ] A[i] A[i]为结尾的“最长上升子序列”的长度
阶段划分 子序列的结尾位置(数列 A A A中的位置,从前到后)
转移方程 F [ i ] = max ⁡ 0 ≤ j ≤ i , A [ j ] < A [ i ] { F [ j ] + 1 } F[i]=\underset{0\leq j\leq i,A[j]F[i]=0ji,A[j]<A[i]max{F[j]+1}
边界 F [ 0 ] = 0 F[0]=0 F[0]=0
目标 max ⁡ 1 ≤ i ≤ N { F [ i ] } \underset{1\leq i\leq N}{\max} \{ F[i] \} 1iNmax{F[i]}

2.最长公共子序列(LCS

LCS问题
问题描述 最长公共子序列。给定连个长度分别为 N N N M M M的字符串 A A A B B B,求既是 A A A的子序列又是 B B B的子序列的字符串长度最长是多少。
状态表示 F [ i ] [ j ] F[i][j] F[i][j]表示前缀子串 A [ 1 ∼ i ] A[1\sim i] A[1i] B [ 1 ∼ j ] B[1\sim j] B[1j]的“最长公共序列”的长度
阶段划分 已经处理的前缀长度(两个字符串中的位置,即一个二维坐标)
转移方程 F [ i , j ] = max ⁡ { F [ i − 1 , j ] F [ i , j − 1 ] F [ i − 1 , j − 1 ] + 1  if  A [ i ] = B [ j ] F[i, j]=\max \left\{\begin{array}{l}F[i-1, j] \\F[i, j-1] \\F[i-1, j-1]+1 \quad \text { if } A[i]=B[j]\end{array}\right. F[i,j]=max F[i1,j]F[i,j1]F[i1,j1]+1 if A[i]=B[j]
边界 F [ i , 0 ] = F [ 0 , j ] = 0 F[i,0]=F[0,j]=0 F[i,0]=F[0,j]=0
目标 F [ N , M ] F[N,M] F[N,M]

3.数学三角形

数学三角形问题
问题描述 给定一个共有 N N N行的三角矩阵 A A A,其中第 i i i行有 j j j列。从左上角出发,每次可以向下方或右下方走一步,最终到达底部。求把经过的所有位置的数加起来,和最大是多少。
状态表示 F [ i ] [ j ] F[i][j] F[i][j]表示从左上角走到第 i i i行第 j j j列,和最大是多少
阶段划分 路径的结尾位置(矩阵的行、列位置,即一个二维坐标)
转移方程 F [ i , j ] = A [ i , j ] + max ⁡ { F [ i − 1 , j ] F [ i − 1 , j − 1 ]  if  j > 1 F[i, j]=A[i,j]+\max \left\{\begin{array}{l}F[i-1, j] \\F[i-1, j-1] \quad \text{ if } j>1 \end{array}\right. F[i,j]=A[i,j]+max{F[i1,j]F[i1,j1] if j>1
边界 F [ 1 , 1 ] = A [ 1 , 1 ] F[1,1]=A[1,1] F[1,1]=A[1,1]
目标 max ⁡ 1 ≤ j ≤ N { F [ N , j ] } \underset{1\leq j\leq N}{\max} \{ F[N,j]\} 1jNmax{F[N,j]}

容易发现,无论状态表示是一维还是多维,DP算法在这些问题上都体现为“作用在线性空间的递推”——DP的阶段沿着各个维度线性增长,从一个或多个“边界点”开始有方向地向整个状态空间转移、扩展,最后在每个状态上都保留了以自身为“目标”的子问题的最优解。

这几个问题也是线性DP中最简单的一类,在这类问题中,需要计算的对象表现出明显的维度以及有序性,每个状态的求解直接构成一个阶段,这使得DP的状态表示就是阶段的表示。因此,我们只需要在每个维度上各取一个坐标值作为DP的状态,自然就可以描绘出“已求解部分”在状态空间中的轮廓特征,该轮廓的进展就是阶段的推移。另外,每个状态的求解显然只与之前的最优解有关,最优子结构性质也就得以验证。接下来,我们按照顺序依次循环每个维度,根据问题要求递推求解的具体实现过程也就顺理成章了。

你可能感兴趣的:(#,0x50,动态规划,算法,c++)