【动态规划】

动态规划

动态规划方法通常用来求解最优化问题。这里借助运筹学来引入,运筹学解决优化问题的思想就是拉格朗日乘数法和KKT定理,不过其本身具有一定的局限性,比如在列出目标优化函数、资源约束和决策约束后,我们很难用一些线性规划(单纯形表法),或非线性规划的kkt思想去求解这类问题,此时就需要考虑使用动态规划的思想或方法去解决此类问题。



前言

参考《算法导论》的介绍:动态规划与分治方法相似,都是通过组合子问题的解来求解原问题。本文会先从分治思想开始分析二者的相似性和不同点;


一、分治法

许多有用的算法在结构上式“递归的”。

如何理解?
在解决某个问题是重复的使用某一规则去进行评价目标,体现到程序上就是使用while或者for循环去迭代求解。

将这类问题的解法抽象描述为分治法:即将原问题分解为几个规模较小但类似于原问题的子问题,递归的求解这些子问题,然后再合并这些子问题的解来建立原问题的解。

分治模式在每层递归时都有三个步骤:
分解原问题为若干子问题,这些子问题是原问题的规模较小的实例;
解决这些子问题,递归的求解各子问题。然而,若子问题的规模足够小,则直接求解。
合并这些子问题的解成原问题的解。

举例:
EX1:归并排序算法就完全遵循分治模式。

分解待排列的n个元素的序列为成各具n/2个元素的两个子序列;
解决使用归并排序递归的排列两个子序列。
合并两个已排序的子序列以产生已排序的答案。
分治治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。
治

治总结:归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

注:这篇文章对归并排序有详细介绍:归并排序

除此之外,像快速傅里叶变化的蝶形算法也满足分治思想。

二、动态规划

1.动态规划和分治法的异同点

动态规划方法与分治法相似,都是通过组合子问题的解来求解原问题的。分治法将原问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解。与之相反,动态规划应用于子问题重叠的情况,即不同的子问题具有公共子的子子问题。在这种情况下,分治算法会做许多不必要的工作,它会反复地求解那些公共子子问题。而动态规划算法对每个子子问题只求解一次,将其保存在一个表格中,从而无需每次求解一个个子问题时都重新计算,避免这种不必要的计算工作。

代码如下(示例):

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import  ssl
ssl._create_default_https_context = ssl._create_unverified_context

2.动态规划问题详例

2.1 钢条切割:

Serling公司购买长钢条,将其切割成短钢条进行出售。切割工序本身没有成本支出。管理层希望得到最优的切割方法。
现在我们知道Serling公司出售一段长度为 i i i英寸的钢条的价格为 $p_i(i=1,2,3,…,n) $,单位为美元。钢条的长度都为整英寸。

长度i12345678910
价格1589101717202430

钢条切割问题:给定一段长度为n英寸的钢条和一个价格表 p i ( i = 1 , 2 , . . . , n ) p_i(i=1,2,...,n) pi(i=1,2,...,n),求钢条切割方案,使得收益 r n r_n rn最大。注意,如果长度为n英寸的钢条的价格 p n p_n pn足够大,最优解可能就是完全不需要切割。

以4英寸的钢条切割方案为例:

钢条切割从图中可以看出共 2 3 = 8 2^3=8 23=8种切割方案;最优收益为 p 2 + p 2 = 5 + 5 = 10 p_2+p_2=5+5=10 p2+p2=5+5=10

将其抽象为数学模型:
长度为n英寸的钢条共有 2 n − 1 2^{n-1} 2n1种不同切割方案,因为在距离钢条左端 i ( i = 1 , 2 , . . , n − 1 ) i(i=1,2,..,n-1) i(i=1,2,..,n1)英寸处总是选择切割或不切切割。

如果一个最优解将钢条切割为k端(对某个1≤k≤n),那么最优切割方案:
n = i 1 + i 2 + . . . + i k n=i_1+i_2+...+i_k n=i1+i2+...+ik
将钢条切割为长度分别为 i 1 , i 2 , . . . , i k i_1,i_2,...,i_k i1,i2,...,ik的小段得到最大收益:
r n = p i 1 + p i 2 + . . . + p i k r_n=p_{i_1}+p_{i_2}+...+p_{i_k} rn=pi1+pi2+...+pik

结合图片切割方案来看,可以将钢条切割分为多次,即切完一刀后,分别对剩下的两部分进行切割,以此来确保最大收益。

用公式描述:第一次切割:
r n = m a x ( p n , r 1 + r n − 1 , r 2 + r n − 2 , . . r n − 1 + r 1 ) r_n = max(p_n,r_1+r_{n-1},r_2+r_{n-2},..r_{n-1}+r_1) rn=max(pn,r1+rn1,r2+rn2,..rn1+r1)
如何理解上述公式:
第一个参数 p n p_n pn对应不切割,直接出售长度为n英寸的钢条切割方案。其它n-1个参数对应另外n-1个方案:对每个i=1,2,…,n-1,首先将钢条切割为长度为i和n-i的两段,接着求解两段的最优收益 r i 和 r n − i r_i和r_{n-i} rirni(每种方案的最优收益为两段的最优收益之和)。由于无法预知哪种方案会获得最优收益,必须考虑所有的可能的i,选取其中收益最大者。如果直接出售原钢条会获得最大收益,当然不做任何切割。

问题抽象:为了求解规模为n的原问题,我们先求解形式完全一样,但规模更小的子问题。即当完全首次切割后,我们将两段钢条看成两个独立的钢条切割问题实例。我们通过组合两个相关子问题的最优解,并在所有可能的两段切割方案中选取组合收益最大者,构成原问题的最优解。
钢条切割问题满足最优子结构:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。

除了上述求解方法外,钢条切割问题还存在一种相似的但更为简单的递归求解方法:
将钢条从左边切割下长度为i的一段,只对右边剩下 的长度为n-i的一段进行切割(递归求解),对左边的一段则不进行切割。即问题分解的方式为:将长度为n的钢条分解为左边开始一段,以及剩余部分分解的结果。即:
r n = m a x ( p i + r n − i ) ,其中 1 ≤ i ≤ n r_n = max(p_i+r_{n-i}) ,其中1≤i≤n rn=max(pi+rni),其中1in
上述公式中:第一段的长度为n,收益为 p n p_n pn,剩余部分长度为0,对应的收益为 r 0 = 0 r_0=0 r0=0;原问题的最优解包含一个相关子问题(右端剩余部分)的解,而不是两个。

自顶向下递归实现
算法思路如下:

cut_rod(p,n)
if n==0
	return 0;
q = -无穷
for i = 1 to n
	q = max(q,p[i]+cut_rod(p,n-i))
return q

图示需要指数运算时间 2 n 2^n 2n

使用动态规划方法求解最优钢条切割问题

时间换空间

可能将一个指数时间的解转化为一个多项式时间的解。如果子问题的数量是输入规模的多项式函数,而我们可以在多项式时间内求解出每个子问题,那么动态规划方法的总运行时间就是多项式阶的。

动态规划的两种实现方法

带备忘的自顶向下法此方法仍按照自然的递归形式编写过程,但过程会保存每个子问题的解。当需要一个子问题的解时,过程首先检查是否已经保存过此解。如果是,则直接返回保存的值。从而节省了计算时间。

//伪代码
MEMOIZED-CUT-ROD(p,n)
let r[0...n] be a new array
// 将辅助数组r[0..n]的元素初始化为-无穷
for i=0 to n
    r[i]=-// 调用辅助过程函数
return MEMOIZED-CUT-ROD-AUX(p,n,r)

MEMOIZED-CUT-ROD-AUX(p,n,r)
// 查到所需值,直接返回保存值
if r[n]>=0
    return r[i]
// 计算收益q
if n==0
    q=0
else
    q=-for i=1 to n
        q=max(q,p[i]+MEMOIZED-CUT-ROD-AUX(p,n-i,r))
// 每次存入r[n]        
r[n]=q
return q

自底向上法这种方法一般需要恰当定义子问题“规模”的概念,使得任何子问题的求解都依赖于“更小的”子问题的求解。因此我们可以将子问题按规模排序,按由小到大的顺序进行求解。当求解某个子问题时,它所依赖的那些更小的子问题都已经求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已经求解完成。

//伪代码
BOTTOM-UP-CUT-ROD(p,n)
let r[0..n] be a new array
r[0]=0
for j=1 to n
    q=-// 从最小问题进行求解
    for i=1 to j
        q=max(q,p[i]+r[j-i])
    r[j]=q
return r[n]

重构解问题
(1)问题描述
如何修改动态规划算法,使其不仅输出最优解,还输出最优切割方案?
(2)解题思路
根据简化后的递推式,其中左边部分不再切割,右边部分可以再切割。

对每个子问题,保存切割一次时左边切下的长度,即不再切割的部分,定义为s[j]。

3.动态规划总结

动态规划的核心原理是通过分解问题为重叠子问题,利用最优子结构性质,结合状态转移方程和记忆化存储来提高计算效率。‌ 其核心思想体现在以下关键点:‌

最优子结构‌
动态规划问题的最优解包含其子问题的最优解。例如,在数塔问题中,从顶点到底层的最大路径和,可以通过每一层选择左右子路径的最优解逐步推导得出。类似地,背包问题的最优解依赖于物品选择和剩余容量的子问题最优解。

重叠子问题‌
在递归求解过程中,相同子问题会被多次计算。例如,在计算斐波那契数列时,F(n)=F(n−1)+F(n−2)F(n)=F(n−1)+F(n−2),直接递归会产生指数级重复计算。动态规划通过存储子问题的解(记忆化或填表法)避免重复。

状态定义与转移方程‌
‌状态定义‌:需明确问题的变量和状态表示。
‌状态转移方程‌:描述状态间的递推关系。

实现方法‌
‌自顶向下(记忆化搜索)‌:通过递归+存储子问题结果实现,如数塔问题的递归解法优化。
‌自底向上(迭代填表)‌:从最小子问题开始逐步求解,如背包问题通过二维数组递推。

动态规划的典型应用场景

‌最优化问题‌:如旅行商问题、最短路径问题。
‌组合计数‌:如字符串匹配、括号生成问题。
‌资源分配‌:如任务调度、股票买卖最佳时机。

‌动态规划的关键在于合理定义状态和设计转移方程,同时利用子问题解的存储避免重复计算。‌ 例如,斐波那契数列的递归实现时间复杂度为 O ( 2 n ) O(2^n) O(2n),而动态规划可优化至O(n)。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值