算法:动态规划|动态转移过程与Python实现小样两例(切绳子与跳台阶)

算法:动态规划|动态转移过程与Python实现小样两例(切绳子与跳台阶)

九月 06, 2019
当前设备屏幕尺寸过小,推荐使用PC模式浏览。

写在前面

  • 这是我对动态规划一些入门题的笔记,主要便于随时随地的回顾这些基础内容。
  • 基本都是些简单题目,因为复杂的题目代码太长,不便于作为笔记进行重温。

动态规划要义

在重温动态规划的时候,看到了一句类似GNU's Not Unix的语句:

Those who cannot remember the past are condemned to repeat it.

这句话告诉了我们迭代和回溯在DP算法框架中的重要性,或者说找到核心状态及其转移方程的内涵。

编程领域的动态规划是什么

应当区分在控制领域的动态规划问题,因为控制领域的动态规划确实通过梯度等方式实现了搜索和目标值下降过程。

  • 个人认为基于动态规划的编程是一种高效的枚举方法,通过先前的经验,将子问题递推得到原问题的解。其过程更偏向于随机过程,或者某种意义上的马尔科夫链。

  • 甚至,我不觉得这是一种>传统意义上的数学优化<方法,因为并没有什么目标通过迭代得到了优化和降低。或者说,DP是一种逻辑寻优方法,而不是一种通过大量搜索使得系统目标值降低(升高)的优化方法——当然,二者最终的目的都是得到一个最优解。

  • 尽管在《Dynamic Programming》提到了optimization一词,但是这个词的意义更偏向于"最优解",是基于DP算法框架通过高效的状态转移和枚举得到最优解;从下面这段引用也可以看出它与“目标优化”这个动态过程没有什么太大关系,更多的是在告诉我们DP算法的回溯结构:

In terms of mathematical optimization, dynamic programming usually refers to simplifying a decision by breaking it down into a sequence of decision steps over time. This is done by defining a sequence of value functions V1,V2,...,VnV_1, V_2, ..., V_n taking yy as an argument representing the state of the system at times ii from 11 to nn. The definition of Vn(y)V_n(y) is the value obtained in state yy at the last time nn. The values ViV_i at earlier times i=n1,n2,...,2,1i = n-1, n - 2, ..., 2, 1 can be found by working backwards, using a recursive relationship called the Bellman equation. For i=2,...,n,Vi1i = 2, ..., n, V_{i-1} at any state yy is calculated from ViV_iby maximizing a simple function (usually the sum) of the gain from a decision at time i1i-1 and the function ViV_i at the new state of the system if this decision is made. Since ViV_ihas already been calculated for the needed states, the above operation yields Vi1V_{i-1} for those states. Finally, V1V_1 at the initial state of the system is the value of the optimal solution. The optimal values of the decision variables can be recovered, one by one, by tracking back the calculations already performed.

编程领域的动态规划的核心

  • 因此,我认为,动态规划的核心只有一个:状态转移方程
  • 这也是其DynamicDynamic一词的体现。

样例两道

切绳子问题

问题描述

切绳子问题 给出一段长度为nn的绳子,将其切割为mm段(m=1...nm=1...n),每段的长度为非零自然数;试求能使得各段长度乘积最大的切割方法。

问题求解

  1. 寻求其目标方程f(n)f(n)f(n)f(n)是对长度为nn的绳子进行切割的最优解(各段子绳长度乘积最大值)。
  2. 寻求动态过程:每次切割绳子,都将绳子分成两段,将这一步称为一次切割;随后再对两段子绳进行切割,并依次迭代,直到得到问题的最优解。
  • Tips:得到最优解时,一次切割划分的两个子绳长度分别为(na,nb)(n_a,n_b),要使得这两个子绳使得f(n)f(n)得到最优解,则这两个子绳的一次切割也应该得到最优一次切割解(naa,abb)({n_a}_a,{a_b}_b)(baa,bbb)({b_a}_a,{b_b}_b),并依次递归。
  1. 状态转移方程:

f(n)=max{f(1)f(n1),f(2)f(n2)...f(n/2)f(nn/2)}f(n)=\max\{f(1)*f(n-1),f(2)*f(n-2)...f( \lfloor n/2 \rfloor)*f(n- \lfloor n/2 \rfloor)\}

代码样例

1
2
3
4
5
6
7
8
9
10
11
12
13
# Pyhont 3 代码:
# 定义出基本的数据结构
cut_memory = {
"-1": {
"max": 1, # 最大乘积值
"divide": [] # 求得最大乘积值的切绳方法
}
}
# 两个辅助函数
def get_max(n):
return cut_memory[str(n)]["max"]
def get_divide(n):
return cut_memory[str(n)]["divide"]

自底向上求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def cut(n):
if n <= 0:
return {"max": "0", "divide": []}

# 自底向上求解
for i in range(1, n + 1):
# 初始化数据
# 将当前切绳子的初始化方案设为"一刀不切"
cut_memory[str(i)] = {
"max": i,
"divide": [i]
}
# 寻找 f(left) * f(right) 使得乘积最大的组合
# 初始化为自身与一个超参数的积
find_max_cp = {"left": i, "right": -1}

for j in range(1, i // 2 + 1):
temp_max = get_max(j) * get_max(i - j)
cp_max = get_max(find_max_cp["left"]) * get_max(find_max_cp["right"])
if temp_max >= cp_max :
find_max_cp["left"] = j
find_max_cp["right"] = i - j

# 更新参数
cut_memory[str(i)]["max"] =\
get_max(find_max_cp["left"]) * get_max(find_max_cp["right"])
cut_memory[str(i)]["divide"] =\
get_divide(find_max_cp["left"]) + get_divide(find_max_cp["right"])

return cut_memory[str(n)]

跳台阶问题

问题描述

跳台阶问题 已知小明上楼梯时每次只能跨1~2个阶梯,假定小明从地面(第0阶)开始上台阶,要上到第nn个阶梯共有多少种走法并输出走法(nN+n \in \N^+)?

问题求解

  1. 寻求其目标方程f(n)f(n)f(n)f(n)是上到第nn阶台阶的可能方法数。
  2. 寻求动态过程:每次上台阶,由于只能跨1~2步,因此有下面两个过程:
  • n1n-1阶直接跨1步到第nn阶。
  • n2n-2阶直接跨2步到第nn阶。
  1. 状态转移方程:

f(n)=f(n1)+f(n2)f(n)=f(n-1)+f(n-2)

本质上它就是个斐波那契数列,因此需要直接考虑f(1)=1f(1)=1f(2)=2f(2)=2的初始化情况。

代码样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 定义数据结构
jump_memory = {
"1": {
"count": 1,
"way": [[1]]
},
"2": { # 跳到第2阶的数据记忆
"count": 2, # 跳的方法数
"way": [[1, 1], [2]] # 跳的方法
}
}
# 两个辅助函数
def get_count(n):
return jump_memory[str(n)]["count"]
def get_way(n):
return jump_memory[str(n)]["way"]

自底向上求解:

1
2
3
4
5
6
7
8
9
10
11
12
13
def jump(n):
if n == 1 or n == 2:
return jump_memory[str(n)]
# 从下往上累计
for i in range(3, n+1):
jump_memory[str(i)] = {"count": get_count(i-1)+get_count(i-2), "way": []}
# 只跳一步
for i_1 in get_way(i-1):
get_way(i).append(i_1+[1]) # 在前者的每种基础跳法上跳一步
# 一次性跳两步
for i_2 in get_way(i-2):
get_way(i).append(i_2+[2])
return jump_memory[str(n)]