Dynamic Programming(动态规划)

本文详细解析了动态规划的两个充分条件及应用,以工厂最快路线问题为例,展示动态规划解决实际问题的方法,并通过矩阵链乘法问题进一步说明动态规划的优势。文章深入分析了动态规划的步骤、递归式及其优化策略,提供了算法伪代码,旨在帮助读者理解动态规划的核心思想和应用。同时,介绍了矩阵链乘法问题的求解方法,通过动态规划实现最优解的计算,降低了时间复杂度。

参考文献:算法导论


可以使用动态规划的两个充分条件:

1.最优子结构(一个问题的最优解中包含了子问题的最优解,也可以适用贪心策略)

2.重叠子问题(一个递归树在不同的分支中可能碰到相同的子问题)


DP步骤:

1.描述最优解的结构

2.递归定义最优解的值

3.按自底向上的方式计算最优解的值

4.由计算出的结果构造一个最优解


通过工厂最快路线问题

题设:工厂有2调装配线,每条装配线都有n个装配步骤,两条装配线在执行同一步骤的时间是不同的,同一装配线由上一步骤转移到下一步骤的时间可以忽略,而有一条装配线转移到另一条需要一定的时间



我们可以令fi[j]是我们执行第i条装配线执行第j步的最短时间。

那么此时针对题设,只有两种情况,要么从装配线2转移到装配线1,要么直接从装配线1的上一步转到下一步。


如果是第1条生产线,那么我们很容易会写出一个递归式:f1[j] = min{ f1[j-1]+a1j, f2[j-1]+t2[j-1]+a1j }(t2[j-1]是从第2条装配线的第j-1步移到第一条装配线的第j步所花费的时间,a1[j]是第1条生产线执行第j步的时间)

f1[j-1]+a1j表示从装配线1的j-1步转到j步,此时花费的总时间

f2[j-1]+a1j+t2[j-1]表示从装配线2的j-1步转移到装配线1执行第j步,此时花费的总时间


如果我们使用递归计算,我们可以令调用fi[j]的次数为ri[j]

那么由递归式子,容易得出r1[j] = r2[j] = r1[j+1] + r2[j+1]

如果一条装配线由n步,那么可以得出最初的j11被调用了2^(n-1)次(我们可以建立一个树形模型来讨论递归问题,递归树在《算法导论》第一章有讲过,不是这篇文章的重点),整个算法的时间复杂度是O(2^n)这是无法忍受的低效率。


我们可以简化理解一下递归式,每次计算当前步骤的时间都会用到上一次所计算的时间,那么我们不用每次都从头开始计算,算法可以并行的解每次每条装配线的最优解,那么可以将算法优化为时间复杂度仅为O(n)

算法伪代码

FASTEST-WAY(a, t, e, x, n)

f1[1] <- e1 + a(1,1)
f2[1] <- e2 + a(2,1)
for j <- 2 to n
    do if f1[j-1] + a(1,j) <= f2[j-1]+t(2,j-1)+a(1,j)
            then f1[j] <- f1[j-1]+a(1,j)
                l1[j] <- 1
            else f1[j] <- f2[j-1]+t(2,j-1)+a(1,j)
                l1[j] <- 2
        if f2[j-1]+a(2,j) <= f1[j-1]+t(1,j-1)+a(2,j)
            then f2[j] <- f2[j-1] + a(2,j)
                l2[j] <- 2
            else f2[j] <- f1[j-1]+t(1,j-1)+a(2,j)
                l2[j] <- 1
if f1[n]+x1 <= f2[n] + x2
    then f* <- f1[n] + x1
        l* <- 1
    else f* <- f2[n] + x2
        l* <- 2


矩阵链乘法

题设:给定由n个要相乘的矩阵构成的链<A1 , A2 , A3 , ... , An>
由于矩阵需要相容相乘,例如A1是个i*j的矩阵,A2是j*q的矩阵,则A1*A2的计算次数为i*j*q,
考虑3个矩阵链乘:A1:10*100 , A2:100*5 , A3:5*50
有2种情况:((A1*A2)*A3),(A1*(A2*A3))
第一种情况:10*100*5+10*5*50 = 7500
第二种情况:100*5*50 + 10*100*50 = 75000
则第一种情况是第二种情况计算次数的1/10;我们以标量运算次数来衡量时间,则可以简单理解为第二种情况要比第一种情况慢10倍
我们希望在计算之前找到一种加括号的方式,使得计算次数最少

令一个包含n个矩阵的链所有加括号的可能方案为Pn,我们可以考虑其实该链可以分裂成两个加括号的子链,其分裂位置为k,k可以在第1~n-1个矩阵之后,所以可以写出一个递归式

Pn = ∑P(k)*P(n-k) (k = 1~n-1)

那么我们用递归树的方式来分解这个加括号方案,可以理解为一个压栈和弹栈的过程,压栈次数必须大于弹栈次数,是一个Catalan数序列(Catalan数,我理解为一个古典概率问题,证明有代数和几何方式,个人感觉几何的折现法比较易懂,大家有兴趣可以看看),其解为C(2n,n)/n+1,解该递归式子的时间复杂度为O(2^n)。


我们按照DP四个步骤来分析:

最优解的结构

我们考虑Ai*A(i+1)*...*Aj的矩阵链乘法(记为Ai...j),如果我们从k位置分裂矩阵链,则Ai..k和Ak+1...j,必须是最优的解,因为如果有更优的解,那么更优解必定会替换当前解。

递归定义

令m[i,j]为Ai..j的最优解,则
如果i = j, m[i,j] = 0;
如果i < j, m[i,j] = min{ m[i,k]+m[k+1,j]+p(i-1)pkpj};(k=i...j-1, pi-1*pi表示第i个矩阵的维度)

计算最优解的值

MATRIX-CHAIN-ORDER(p)

n <- length[p]-1
for i <- 1 to n
    do m[i,i] <- 0
for l <- 2 to n
    do for i <- 1 to n-l+1
        do j <- i+l -1
            m[i,j] <- ∞
            for k <- i to j-1
                do q <- m[i,k]+m[k+1,j]+p(i-1)p(k)p(j)
                    if q<m[i,j]
                        then m[i,j] <- q
                            s[i,j] <- k
return m and s

例如:

计算下属矩阵的最小标量计算次数

A1  30*35

A2  35*15

A3  15*5

A4  5*10

A5  10*20

A6  20*25

第一步:计算1-2,2-3,3-4,4-5,5-6的计算次数

m:

 123456
1030*35*15    
2 035*15*5   
3  015*5*10  
4   05*10*20 
5    010*20*25
6     0

s:

 123456
101    
2 02   
3  03  
4   04 
5    05
6     0

计算过程:

A1*A2 = m[1,1] + m[2,2] + p0p1p2 = 30*35*15 = 15750


第二步:计算1-3,2-4,3-5,4-6

m:

 123456
10157507875   
2 026254375  
3  07502500 
4   010003500
5    05000
6     0

s:

 123456
1011   
2 023  
3  033 
4   045
5    05
6     0

计算过程

例如1-3:

(A1*A2)*A3 = m[1,2] + m[3,3] + p0p2p3 = 30*35*15 + 30*15*5 = 18000

A1*(A2*A3) = m[1,1] + m[2,3] + p0p1p3 = 35*15*5 + 30*35*5 = 7875

7875 < 18000

所以A1...3加括号方式为A1*(A2*A3),计算结果为7875


以此类推,最终计算结果

m:

 123456
1015750787593751187515125
2 026254375712510500
3  075025005375
4   010003500
5    05000
6     0

s:

 123456
1011333
2 02333
3  0333
4   045
5    05
6     0


构造一个最优解

由m表可知:A1...6至少要执行15125次标量计算

由s表得出加括号的结果:

A1...6在3,4之间拆分

A1...3在1,2之间拆分

A4...6在5,6之间拆分

我们可以绘制一颗树来表示括号结构

                  A1...6

                /          \

       A1...3            A4...6

      /        \            /       \

  A1     A2...3    A4        A5...6

           /       \                /        \

        A2      A3          A5        A6

那么可以得出括号结构为:((A1*(A2*A3))*(A4*(A5*A6)))

由m表可知:A1...6至少要执行15125次标量计算

由s表得出加括号的结果:

A1...6在3,4之间拆分

A1...3在1,2之间拆分

A4...6在5,6之间拆分

我们可以绘制一颗树来表示括号结构

                  A1...6

                /          \

       A1...3            A4...6

      /        \            /       \

  A1     A2...3    A4        A5...6

           /       \                /        \

        A2      A3          A5        A6

那么可以得出括号结构为:((A1*(A2*A3))*(A4*(A5*A6)))


动态规划Dynamic Programming,简称 DP)是一种用于解决**具有重叠子问题**和**最优子结构**性质的复杂问题的算法设计技术。它通过将原问题分解为若干子问题,并保存子问题的解以避免重复计算,从而高效求解。 --- ### 回答问题:什么是动态规划? #### ✅ 定义: 动态规划是一种**分治+记忆化**的算法思想,适用于可以分解为重复出现的子问题、且整体最优解包含局部最优解的问题。 核心思想是: > **“记住已经算过的结果,下次直接用”** 这与单纯的递归不同,因为普通递归会反复计算相同的子问题;而动态规划通过**自底向上**或**带记忆化的自顶向下**方式,确保每个子问题只计算一次。 --- ### 动态规划的两个关键特征: 1. **最优子结构(Optimal Substructure)** - 一个问题的最优解包含其子问题的最优解。 - 例如:从 A 到 C 的最短路径经过 B,则从 A 到 B 的那一段也必须是最短路径。 2. **重叠子问题(Overlapping Subproblems)** - 在递归求解过程中,某些子问题被多次重复计算。 - 动态规划通过**记忆化**(缓存结果)来避免重复计算。 --- ### 典型例子:斐波那契数列 斐波那契定义: $$ F(0) = 0,\quad F(1) = 1,\quad F(n) = F(n-1) + F(n-2) $$ #### 普通递归(效率低): ```c int fib(int n) { if (n <= 1) return n; return fib(n-1) + fib(n-2); } ``` 时间复杂度:$O(2^n)$ —— 大量重复计算(如 `fib(3)` 被算很多次) #### 动态规划优化(记忆化或递推): ##### 方法一:记忆化搜索(自顶向下) ```c #include <stdio.h> int memo[100]; int fib(int n) { if (n <= 1) return n; if (memo[n] != 0) return memo[n]; memo[n] = fib(n-1) + fib(n-2); return memo[n]; } ``` ##### 方法二:递推法(自底向上,标准 DP) ```c int dp[100]; dp[0] = 0; dp[1] = 1; for (int i = 2; i <= n; i++) { dp[i] = dp[i-1] + dp[i-2]; } // dp[n] 即为结果 ``` 时间复杂度降为 $O(n)$,空间 $O(n)$ --- ### 动态规划的基本步骤: 1. **定义状态** 明确 `dp[i]` 或 `dp[i][j]` 表示什么含义(如最大和、最少步数等) 2. **状态转移方程** 找出当前状态如何由之前的状态推导出来 3. **初始化** 设置初始条件(如 `dp[0] = 0`) 4. **遍历顺序** 确保在计算 `dp[i]` 时,所有依赖的状态都已计算 5. **返回结果** 输出最终状态值(如 `dp[n]` 或 `dp[N][N]`) --- ### 常见类型举例: | 类型 | 特点 | 示例 | |------------|------|-------| | 线性DP | 状态在线性序列上转移 | 最长上升子序列、数字三角形 | | 区间DP | 对区间 `[i,j]` 进行操作 | 石子合并 | | 背包DP | 选择物品使价值最大 | 01背包、完全背包 | | 树形DP | 在树结构上做DP | 树的最大独立集 | | 状压DP | 状态用二进制压缩表示 | TSP旅行商问题 | | 双线程DP | 同时处理两条路径 | 方格取数 | --- ### 总结: 动态规划不是一种具体的函数或语法结构,而是一种**解决问题的思想方法**。它的名字听起来很高深,但本质就是: > **把做过的事记下来,下次就不用重做** 就像你走迷宫时画地图,下次就不会再走死路。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值