前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >(粗糙的笔记)动态规划

(粗糙的笔记)动态规划

作者头像
WuShF
发布2023-10-05 08:24:04
2530
发布2023-10-05 08:24:04
举报
文章被收录于专栏:笔记分享

动态规划算法框架:

  1. 问题结构分析
  2. 递推关系建立
  3. 自底向上计算
  4. 最优方案追踪

背包问题

输入:

n

个商品组成的集合

O

,每个商品有两个属性

v_i

p_i

,分别表示体积和价格

  • 背包容量
C

输出:

  • 求解一个商品子集
S\subseteq O

直观策略

  • 策略1:按商品价格由高到低排序,优先挑选价格高的商品
  • 策略2:按商品体积由小到大排序,优先挑选体积小的商品
  • 策略3:按商品价值与体积的比由高到低排序,优先挑选比值高的商品

这三种策略都不能保证得到最优解

蛮力枚举

  1. 枚举所有商品组合:
2^n-1

种情况

  1. 检查体积约束

递归函数KnapsackSR(h,i,c)

  • 在第
h

个到第

i

个商品中,容量为

c

时最优解

  • 选择啤酒:
KnapsackSR(1,4,3)+24
  • 不选啤酒:
KnapsackSR(1,4,13)

伪代码:

代码语言:javascript
复制
输入:商品集合{h,...,i},背包容量c
输出:最大总价格P
if c<0 then
| return 0
end
if i <= h-1 then
| return 0
end
P1 <- KnapsackSR(h,i-1,c-vi)
P2 <- KnapsackSR(h,i-1,c)
P <- max(P1+pi,P2)
return P

重复求解大量子问题:

O(2^n)

动态规划

从蛮力枚举到带备忘递归

  • 优化子问题解,避免重复计算

构造备忘录P[i,c]P[i,c]表示在前i个商品中选择,背包容量为c时的最优解

代码语言:javascript
复制
输入:商品集合{h,...,i},背包容量c
输出:最大总价格P
if c<0 then
| return 0
end
if i <= h-1 then
| return 0
end
if P[i,c]!=NULL then
| return P[i,c]
end
P1 <- KnapsackMR(h,i-1,c-vi)
P2 <- KnapsackMR(h,i-1,c)
P[i,c] <- max(P1+pi,P2)
return P[i,c]

递推求解

容量为0时:

P[i,0]=0

没有商品时:

P[0,c]=0
image.png
image.png

确定计算顺序:

  • 按从左往右、从上到下的顺序计算

问题:如何确定选取了哪些商品

  • 记录决策过程:KaTeX parse error: {align} can be used only in display mode.

回溯解决方案:

  • 倒序判断是否选择商品
  • 根据选择结果,确定最优子问题

伪代码:

代码语言:javascript
复制
输入:商品集合{h,...,i},背包容量c
输出:最大总价格P
//初始化,创建二维数组P和Rec
for i <- 0 to C do
| P[0,i] <- 0
end
for i <- 0 to n do
| P[i,0] <- 0
end
//求解表格
for i <- 1 to n do
| for c <- 1 to C do
|  | if v[i]<=c and p[i]+P[i-1,c-v[i]]>P[i-1,c] then
|  | | P[i,c]=p[i]+P[i-1,c-v[i]]
|  | | Rec[i,c] <- 1
|  | end
|  | else
|  | | P[i,c] <- P[i-1,c]
|  | | Rec[i,c] <- 0
|  | end
| end
end

时间复杂度:

O(n\cdot C)

上面带备忘递归和递推求解的方法都属于动态规划:

  • 带备忘递归:自顶向下
  • 递推求解:自底向上

最优子结构性质:

  • 问题的最优解由相关子问题最优解组合而成
  • 子问题可以独立求解

动态规划与分而治之的区别:

  • 动态规划:重叠子问题
  • 分而治之:独立子问题

最大子数组

问题结构分析:

  • 给出问题表示:
D[i]

为以

X[i]

开头的最大子数组和

  • 明确原始问题
S_{max}=max\{D_i\}

递推关系建立:

  • 情况一:
D[i+1]>0

,则

D[i]=X[i]+D[i+1]
  • 情况二:
D[i+1]\leq0

,则

D[i]=X[i]

自底向上计算:

  • 初始化:
D[n]=X[n]
  • 递推公式:KaTeX parse error: {align} can be used only in display mode.

记录决策过程:

  • 构造追踪数组
Rec[1..n]
  • 情况一:结尾相同,则
Rec[i]=Rec[i+1]
  • 情况二:结尾不同,则
Rec[i]=i

最优方案追踪:

  • 从子问题中查找最优解
  • 最大子数组开头位置:
i
  • 最大子数组结尾位置:
Rec[i]

伪代码:

代码语言:javascript
复制
输入:数组X,数组长度n
输出:最大子数组和Smax,子数组起止位置l,r
//初始化
D[n] <- X[n]
Rec[n] <- n
//动态规划
for i <- n-1 to 1 do
| if D[i+1]>0 then
| | D[i] <- X[i]+D[i+1]
| | Rec[i] <- Rec[i+1]
| end
| else
| | D[i] <- X[i]
| | Rec[i] <-i
| end
end
//查找解
Smax <- D[1]
for i <- 2 to n do
| if Smax<D[i] then
| | Smax<-D[i]
| | l <- i
| | r <- Rec[i]
| end
end
return Smax,l,r

最长公共子序列

子序列:将给定序列中零个或多个元素去掉后所得的结果

蛮力枚举

枚举所有子序列 可能存在最优子结构和重叠子问题

动态规划

问题结构分析:

  • 给出问题表示:
C[i,j]

表示

X[1..i]

Y[1..j]

的最长公共子序列长度

递推关系建立:分析最优子结构

  • 考察末尾字符:
  • 情况1:
x_i\neq y_j

时,

C[i,j]=max\{ C[i,j-1],C[i-1,j] \}
  • 情况2:
x_i= y_j

时,

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

自底向上计算:确定计算顺序

  • 初始化:
C[i,0]=C[0.j]=0

//某序列长度为0时,最长公共子序列长度为0

  • 递推公式:KaTeX parse error: {align} can be used only in display mode.

最优方案追踪:记录决策过程

  • 构造追踪数组
rec[1..n]

,记录子问题来源:KaTeX parse error: {align} can be used only in display mode.

伪代码:

代码语言:javascript
复制
输入:两个序列X,Y
输出:X和Y的最长公共子序列
n <- length(X)
m <- length(Y)
//初始化
新建二维数组C[n,m]和rec[n,m]
for i <- 0 to n do
| C[i,0] <-0
end
for j <- 0 to m do
| C[0,j] <- 0
end
//动态规划
for i <- 1 to n do
| for j <- 1 to m do
|  | if Xi=Yj then
|  | | C[i,j] <- C[i-1.j-1]+1
|  | | rec[i,j] <- 'LU'
|  | end
|  | else if C[i-1,j]>=C[i,j-1] then
|  | | C[i,j] <- C[i-1,j]
|  | | rec[i,j] <- 'U'
|  | end
|  | else
|  | | C[i,j] <- C[i,j-1]
|  | | rec[i,j] <- 'L'
|  | end
| end
end
return C,rec

时间复杂度:

O(n\cdot m)

最长公共子串

子串:给定序列中零个或多个连续的元素组成的子序列

蛮力枚举

  1. 序列X和序列Y各选择一个位置
  2. 依次检查元素是否匹配:
    1. 元素相等则继续匹配
    2. 元素不等或某序列已达端点,匹配终止

可能存在最优子结构和重叠子问题。

动态规划

问题结构分析:

  • 给出问题表示:
C[i,j]

表示

X[1..i]

Y[1..j]

中,以

x_i

y_j

结尾的最长公共子串

Z[1..l]

的长度

递推关系建立:分析最优子结构

  • KaTeX parse error: {align} can be used only in display mode.

自底向上计算:确定计算顺序

  • 初始化:
C[i,0]=C[0.j]=0

//某序列长度为0时,最长公共子串长度为0

  • 原始问题:
p_{max}=max\{C[i,j]\}

最优方案追踪:记录决策过程

  • 最长公共子串末尾位置
p_{max}
  • 最长公共子串长度
l_{max}

伪代码

代码语言:javascript
复制
输入:两个字符串X,Y
输出:X和Y的最长公共子串
//初始化
n <- length(X)
m <- length(Y)
新建二维数组C[n,m]
lmax <- 0
pmax <- 0
for i <- 0 to n do
| C[i,0] <- 0
end
for j <- 0 to n do
| C[0,j] <-0
end
//动态规划
for i <- 1 to n do
| for j <- 1 to m do
|  | if Xi != Yj then
|  | | C[i,j] <- 0
|  | end
|  | else
|  | | C[i,j] <- C[i-1,j-1]+1
|  | | if C[i,j] > lmax then
|  | | | lmax <- C[i,j]
|  | | | pmax <- i
|  | | end
|  | end
| end
end

编辑距离问题

编辑操作:删除、插入、替换 递推关系建立:只操作

s

  • 删除:
D[i,j]=D[i-1,j]+1
  • 插入:
D[i,j]=D[i,j-1]+1
  • 替换:KaTeX parse error: {align} can be used only in display mode.
  • 综合以上三种方式:KaTeX parse error: {align} can be used only in display mode.
  • 最小编辑距离VS最长公共子序列:
    • KaTeX parse error: {align} can be used only in display mode.
    • KaTeX parse error: {align} can be used only in display mode.

自底向上计算:

  • 初始化:
    D[i,0]=i

    //把长度为

    i

    的串变为空串至少需要

    i

    次删除操作

    D[j,0]=j

    //把空串变为长度为

    j

    的串至少需要

    j

    次插入操作

  • 递推公式:
    • KaTeX parse error: {align} can be used only in display mode.

最优方案追踪:

  • 追踪数组
Rec

,记录子问题来源

image.png
image.png
image.png
image.png

伪代码

代码语言:javascript
复制
输入:字符串s和t
输出:s和t的最小编辑距离
n <- length(s)
m <- length(t)
新建D[0..n,0..m],Rec[0..n,0..m]两个数组
//初始化
for i <- 0 to n do
| D[i,0] <- i
| Rec[i,0] <- 'U'
end
for j <- 0 to m do
| D[0,j] <- j
| Rec[0,j] <- 'L'
end
//动态规划
for i <- 1 to n do
| for j <- 1 to m do
|  | c <- 0
|  | if si!=tj then
|  | | c <- 1
|  | end
|  | replace <- D[i-1,j-1]+c
|  | delete <- D[i-1,j]+1
|  | insert <- D[i,j-1]+1
|  | if replace =min{replace,delete,insert} then
|  | | D[i,j] <- D[i-1,j-1]+c
|  | | Rec[i,j] <- 'LU'
|  | end
|  | else if insert = min{replace,delete,insert} then
|  | | D[i,j] <- D[i,j-1]+1
|  | | Rec[i,j] <- 'L'
|  | end
|  | else
|  | | D[i,j] <- D[i-1,j]+1
|  | | Rec[i,j] <- 'U'
|  | end
| end
end

最优方案追踪-伪代码

代码语言:javascript
复制
输入:矩阵Rec,字符串s,t,索引位置i,j
输出:操作序列
if i=0 and j=0 then
| return NULL
end
if Rec[i,j]='LU' then
| Print-MED(Rec,s,t,i-1,j-1)
| if si=tj then
| | print '无需操作'
| end
| else
| | print '用tj代替si'
| end
end
else if Rec[i,j]='U' then
| Print-MED(Rec,s,t,i-1,j)
| print '删除si'
end
else
| Print-MED(Rec,s,t,i,j-1)
| print '插入tj'
end

钢条切割问题

image.png
image.png

形式化定义

输入:

  • 钢条长度
n
  • 价格表
p_l

:表示长度为

l

的钢条价格

输出:

  • 一组切割方案,令收益最大

问题简化

假设至多切割1次,枚举所有可能的切割位置:

  • 不切:
p[10]
  • 切割:
p[i]+p[10-i]

假设至多切割2次:

  • 先将钢条切割一段
  • 在剩余钢条中继续切割,剩余的问题变为至多切一刀的问题

原始问题不限制切割次数

  • 可能存在最优子结构和重叠子问题

动态规划

问题结构分析:

  • 给出问题表示:
C[j]

表示切割长度为

j

的钢条可得的最大收益

递推关系建立:

C[j]=max\{ p[i]+C[j-i],p[j] \}
image.png
image.png

自底向上计算:

  • 初始化:
C[0]=0

//切割长度为0的钢条,总收益为0

  • 递推公式:
C[j]=max\{ p[i]+C[j-i],p[j] \}

最优方案追踪:记录决策过程

  • 构造追踪数组
rec[1..n]
rec[j]

:记录长度为

j

的钢条的最优切割方案

image.png
image.png

伪代码

代码语言:javascript
复制
输入:钢条价格表p[1..n],钢条长度n
输出:最大收益C[n],钢条切割方案
//初始化
新建一维数组C[0..n],rec[0..n]
C[0] <- 0
//动态规划
for j <- 1 to n do
| q <- p[j]
| rec[j] <- j
| for i <- 1 to j-1 do
| | if q<p[i]+C[j-i] then
| | | q <- p[i]+C[j-i]
| | | rec[j] <- i
| | end
| end
| C[j] <- q
end
//输出最优方案
while n>0 do
| print rec[n]
| n <- n-rec[n]
end

时间复杂度为

O(n^2)

矩阵链乘法问题

矩阵乘法时间复杂度:

  • 计算一个数字:
q

次标量乘法

p\times r

个数字:

\Theta(pqr)

三个矩阵相乘:

(UV)W=U(VW)
  • 新问题:矩阵乘法结合的顺序
image.png
image.png
n

个矩阵相乘:

  • 一系列矩阵按顺序排列
  • 每个矩阵的行数=前一个矩阵的列数
n

个矩阵相乘也被称为矩阵链乘法

问题定义

输入:

n

个矩阵组成的矩阵链

U_{1..n}=<U_1,U_2,...,U_n>
  • 矩阵链
U_{1..n}

对应的维度数分别为

p_0,p_1,...,p_n

U_i

的维度是

p_{i-1}\times p_i

输出:

  • 找到一种加括号的方式,使得矩阵链标量乘法的次数最少
image.png
image.png

如何保证不遗漏最优分割位置:

  • 枚举所有可能位置
i..j-1

,共

j-i

image.png
image.png

问题结构分析:

  • 明确原始问题:
D[1,n]

表示计算矩阵链

U_{1..n}

所需标量乘法的最小次数

递推关系建立:

  • 对每个位置
k(i\leq k\leq j)

D[i,j]=D[i,k]+D[k+1,j]+p_{i-1}p_kp_j
  • 枚举所有
k

,得到递推式:

D[i,j]=min(D[i,k]+D[k+1,j]+p_{i-1}p_kp_j)

自底向上计算:

  • 初始化:
i=j

时,矩阵链只有一个矩阵,乘法次数为0

image.png
image.png

最优方案追踪:

  • 构造追踪数组
Rec[1..n,1..n]
Rec[i,j]

:矩阵链

U_{i..j}

的最优分割位置

image.png
image.png

伪代码

代码语言:javascript
复制
输入:矩阵维度数组p,矩阵的个数n
输出:最小标量乘法次数,分割方式追踪数组Rec
新建二维数组D[1..n,1..n],Rec[1..n,1..n]
//初始化
for i <- 1 to n do
| D[i,i] <- 0
end
//动态规划
for l <- 2 to n do
| for i <- 1 to n-l+1 do
| | j <- i+l-1
| | for k <- i to j-1 do
| | | q <- D[i,k]+D[k+1,j]+p[i-1]*p[k]*p[j]
| | | if q<D[i,j] then
| | | | D[i,j] <- q
| | | | Rec[i,j] <- k
| | | end
| | end
| end
end
return D[1,n],Rec

时间复杂度

O(n^3)
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背包问题
    • 动态规划
    • 最大子数组
    • 最长公共子序列
      • 最长公共子串
      • 编辑距离问题
      • 钢条切割问题
      • 矩阵链乘法问题
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档