Algorithm-DP

0-1背包问题

问题描述:

体积固定的背包,从一组存在两个属性的(体积、价格)物品中挑选,使得总体积不超过背包容量时的最大价值。

选择价格最高的、体积最小的、性价比最高的都无法保证得到最优解。

尝试蛮力枚举法(暴力出奇迹),找出价格最大的方案。

省略掉h得到简化得到的伪代码:

蛮力枚举法的复杂度高,如何优化呢?

其对应的伪代码如下:

\(P[i,c]\)可以解释为一个表格,其实就是空间换时间嘛!

其实就是自顶向下,而又自底向上的递归过程:

是否可以不递归,直接计算出\(P[i,c]\)?

妙妙子,由于\(P[i,c]=max\{P[i-1,c],P[i-1,c-v_i]+p_i\}\).所以为了确定\(P[i,c]\),需要知道其备忘录表格中上方的值和左上方的值。所以这个备忘录应该从左往右,从上往下,一次记录,最后表格右下角的值就是最终求得的值.

但是有一个问题是,最终得到了最优解,但是到底选择了哪些物品呢?

所以引入了一个表格\(R[i,c]=\{1,0\}\)用来记录决策过程。如果是选择了第i个物品,则为1,否则为0.

方法:从右下角往前推。 如果当前为1,则查看\(R[i-1,c-v_i]\)的位置是1还是0;否则查看\(R[i-1,c]\)的位置是1还是0,以此类推。

算法复杂度为\(O(nc)\)

什么是动态规划:就是备忘录法、递归求解。

动态规划的一般步骤

  1. 问题结构分析

  2. 递推关系建立(初始化)

  3. 自底向上计算(做好备忘录)

  4. 最优方案追踪(构建Rec数组)

动态规划vs分而治之

动态规划:存在重叠的子问题

分而治之:各个子问题互相独立


最大子数组问题

之前在分治算法的时候讲过。

蛮力枚举的时间复杂度为\(O(n^3)\)

分而治之的时间复杂度为\(O(nlogn)\)

动态规划算法

定义\(D[i]\)表示以\(X[i]\)开头的最大子数组和。

上面两种情况很显而易见,$ Rec $数组用来记录当前i的最大子数组的结束位置.

对应的伪代码为:

可见动态规划解决最大子数组问题的时间复杂度为\(O(n)\).对于找到起点和终点的方法就是先找到maxD[i],则其对应的i就是起点,Rec[i]的值就是终点.


最长公共子序列

问题描述

子序列: 一个序列去掉零个或多个字符所得的序列.

注意x序列和y序列的长度不一定相等.

一种方法也是蛮力枚举。由于长度长的子序列依赖于长度短的序列,所以一定存在重叠子问题.

考虑末尾字符

定义\(C[m,n]\):X[1..m]和Y[1..n]的最大公共子序列的长度

两种情况:

  1. 末尾字符不等\(x[i-1]\neq y[j-1]\)

\[ C[i,j]=max\{C[i-1,j],C[i,j-1]\} \]

因为末尾字符不等,所以通过减去其中一个序列的末尾字符达到相等的效果,那么相当于不相等的末尾字符在求最长公共子序列时时没有用的,舍弃就好.

那么也有可能减去一个还是不等,但是由于这是一个递归式,所以继续递归下去.

  1. 末尾字符相等\(x[i-1]= y[j-1]\)

\[ C[i,j]=C[i-1,j-1]+1 \]

加的1指的是最末尾的字符已经为最长公共子序列的长度贡献了1.

时间复杂度为\(O(nm)\)


最长公共子串

问题描述

和最长公共子序列的区别就是:子串必须连续

定义

\(C[i,j]\)表示以\(x_i,y_j\)结尾的X[1...i],Y[1...j]的最长公共字串长度

递归公式

两种情况:

  1. $ x_iy_j \(则\)C[i,j]=0$

  2. \(x_i=y_j\)\(C[i,j]=C[i-1,j-1]+1\)

PS:由于\(C[i,j]\)相当于是将所有n*m个最长公共子串长度枚举出来了,所以最终问题的最优解不是\(C[i,j]\)的右下角,而是这个表格中的最大值,应该维护两个值,分别是最优解的位置\(p_{max}\)和长度\(l_{max}\),用于最后最优解追踪,而不至于最后还要遍历表格n*m来找到最大值。其实相当于是利用动态规划的枚举方法。

最优方案追踪

  • 记录最长公共子串末尾的位置

  • 记录最长公共子串的长度

伪代码

初始化n,m,新建数组C[i,j],lm,pm,初始化C[i,j];(看着好像都会忘记哪些要初始化,但是把总的代码写一遍就清楚了)。

两个for循环填C[i,j]表

时间复杂度

\(O(n·m)\)


最小编辑距离

问题描述

输入:长度为n的字符串s和长度为m的字符串t

输出:一组min|编辑操作|使得s=t

定义

\(D[n,m]\):字符串\(s[1..n]\)变为\(t[1...m]\)的最小编辑距离(是一个数).

递归公式

同样考虑末尾元素:

1.删除操作:

将s的末尾删除

$ D[i,j]=D[i-1,j]+1 $(这个1代表删除操作)

2.插入操作:

将s的末尾插入一位t的最后一位,,相当于将s通过D[i,j-1]变成t[1..j-1],最后在后面插入t[j]就变成t了

\(D[i,j]=D[i,j-1]+1\)(这个1 代表最后的插入操作)

3.替换操作:

将s的最后一位替换为t的最后一位

如果\(s[i]=t[j]\)\(D[i,j]=D[i-1][j-1]+0\)

否则,\(D[i,j]=D[i-1,j-1]+1\)(这个1代表替换最后一位)

初始化

初始化\(D[i,0]=i;D[0,j]=j\)分别对应i次删除和j次插入。

最优方案追踪

D表格右下角是最优解。定义一个\(Rec[i,j]\)数组,记录子问题来源:\(L,U,LU\)分别对应删除\(s[i]\),插入\(t[j]\),用\(t[j]\)替换\(s[i]\)(或者无操作)

伪代码

  • 初始化: n,m,新建D数组,Rec数组,初始化D和Rec数组的第一行和第一列

  • 动态规划:

  • 最优方案追踪(打印)(往往这是个递归函数):

那么,递归函数需要有结束条件,具体如下:

可以发现,是先递归调用,再输出。因为这是从后往前推导的。

时间复杂度

\(O(mn)\)


钢条切割问题(区间DP )

问题描述

钢条长度n,价格表\(p_l(1<=l<=n)\).

对于一组切割方案,求解\(max\sum_{l=1}^{m}p_{c_l}\)使得\(\sum_{l=1}^{m}c_l=n\)

定义

\(C[j]\):切割长度为j的钢条可得的最大收益

递归公式

\[ C[j]=max_{1\leq i\leq j-1}\{p[i]+C[j-i],p[j]\} \]

有最优子结构和重叠子问题,为区间动态规划,特点是取最大值max时i和j有范围,这个区间的每一种情况都要枚举,然后找出最大值。

最优方案追踪

构造\(rec[1..n]\):记录长度为j钢条的最优切割方案

  1. 不切:\(rec[j]=j\)

  2. 切割:\(rec[j]=k\)(k表格切割的长度)

从后往前,查找\(rec[j-rec[j]]\)直到为0.

伪代码

  • 初始化:新建一维数组C[0..n],rec[0..n]

  • 动态规划:j从1→n,内层i从1到j-1的一个for循环

  • 最优方案追踪

时间复杂度

\(O(n^2)\)


矩阵链乘法(区间DP)

问题描述

类似于钢条切割问题,给矩阵链加括号(分割)

定义

\(D[i,j]\):对矩阵链\(U_{i..j}\)对应的最小标量乘法次数

递推公式

\[ D[i,j]=min_{i \leq k < j}\{D[i,k]+D[k+1,j]+p_{i-1}p_kp_j\} \]

如果i = j则这个矩阵链只有一个矩阵,不存在乘法,所以D[i,j]=0

所以D表格的对角线为0,且为一个上三角(i<j)。

最优方案追踪

从旋转后的金字塔的尖尖所对应的最优解开始推导:如果\(rec[i,j]=k\)那么相当于是子矩阵链在k的后面切了一刀;然后再分别取查找\(rec[i,k]\)\(rec[k+1,j]\),下图有处纰漏:

算法实例

伪代码

  1. 初始化

  2. 动态规划

最外层为矩阵链的长度,从2到n

内层嵌套的是长度为l的子矩阵链,从1到n-l+1

最内层循环是k在区间上取最小值

时间复杂度

\(O(n)\)