动态规划
动态规划
参考网站:https://people.cs.clemson.edu/~bcdean/dp_practice/
引入:Fibonacci Sequence
对于Fibonacci Sequence:斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N,计算 F(N)。
-
递归方法实现
##1. Fibonacci Sequence - Recursive # 时间复杂度O(2^n),空间复杂度O(n) # f(n) = f(n-1) + f(n-2) def Fibonacci_Recursive(n): # initial f = [0 for i in range(n)] f[0], f[1] = 1, 1 for i in range(2,n): f[i] = f[i-1] + f[i-2] return f[-1] print(Fibonacci_Recursive(80))
-
DP
##2. Fibonacci Sequence - DP def Fibonacci_DP(n): # initial a, b = 1, 1 count = 2 while count < n: c = a + b a = b b = c count += 1 return c print(Fibonacci_DP(80))
P1:Maximum Value Continuous Subsequence
问题:给定一组数组,寻找子数组使得他们的之和最大,比如arr=(-1,-1,-2, 3, 5,-1, 4,-2)
-
暴力法:列出所有子数组,再求和,选择其中最大的那个子数组。
-
DP方法:
A[j]:表示数组arr[1...j]中找到的和最大的子数组的总和。
\(A[j] = max\quad \{A[j-1]+arr[j], arr[j]\}\)
arr[j]能否和前面数组arr[1...j-1]构成连续的子数组,取决于arr[j]的值是否超过A[j-1]+arr[j]。
##3. P1:Maximum Value Continuous Subsequence # 问题:给定一组数组,寻找子数组使得他们的之和最大, import numpy as np def get_maxsum_subseq(arr): ''' 输入: arr: list 输出: 和最大的子数组的和 ''' # case 1:空数组或None if arr is None or len(arr)==0: return 0 # case 2:数组元素全为正数 n = len(arr) pos_num = 0 for i in range(n): if arr[i] >= 0: pos_num += 1 if pos_num == n: return sum(arr) # case 3: # dp[i]: arr[0...i]中以i结尾的子数组中和最大的值 dp = [0 for i in range(n)] dp[0] = arr[0] for i in range(1,n): dp[i] = max(dp[i-1]+arr[i], arr[i]) return max(dp) arr = [1,-1,-2,3,5,-1,4,-2] print(get_maxsum_subseq(np.array(arr)))
P2: Coin Change Problem
问题:假如有n种硬币,它们的价格分别是\(1=V_1 < V_2 < V_3 < ... < V_n\),每种硬币数量充足,张三想要把手里价值C的纸币换成硬币,越少越好,如何设计方案?
分析:
子问题:当纸币金额为j时,最少的硬币兑换个数是\(M(j)\)
构建dp数列:\([M(1), M(2), ... , M(j), ... , M(C)]\)
\(M(j)\)的转化方程:\(M(j)=min\quad \{M(j-V_1)+1, M(j-V_2)+1, M(j-V_3)+1, ..., M(j-V_n)+1\}\)
##4. P2: Coin Change Problem
# 问题:假如有n种硬币,它们的价格分别是1=V1 < V2 < V3 < ... < Vn,
# 每种硬币数量充足,张三想要把手里价值C的纸币换成硬币,越少越好,如何设计方案?
# dp[i]=min{dp[i-V1]+1, dp[i-V2]+1, dp[i-V3]+1, ..., dp[i-Vn]+1}
def min_coin_change(arr, C):
'''
输入:
arr: 各种硬币的面值v1,v2,v3,...,vn
C: 需要被兑换的纸币面值
输出:
最少的硬币个数
'''
if len(arr)==0 or None:
return 0
# dp[i]: minimum number of coin for amount i (i=0,1,2,3,...,C)
dp = [ sys.maxsize for i in range(C+1)]
dp[0] = 0
for i in range(1,C+1):
j = 0
while j < len(arr) and i >= arr[j]:
if dp[i] >= dp[i-arr[j]]+1:
dp[i] = dp[i-arr[j]]+1
j+=1
return dp[-1]
arr = [1,2,3,4,5]
C = 16
print(min_coin_change(arr, C))
进一步需要求出每种硬币的个数,即完整的兑换策略。
def min_coin_change2(arr, C):
'''
输入:
arr: 各种硬币的面值v1,v2,v3,...,vn
C: 需要被兑换的纸币面值
输出:
最少的硬币个数的兑换策略:每种硬币的个数
'''
if len(arr)==0 or None:
return 0
# dp[i]: minimum number of coin for amount i (i=0,1,2,3,...,C)
dp = [ sys.maxsize for i in range(C+1)]
dp[0] = 0
# choice[i]: 保存dp[i]时的硬币面值选择(i =1,2, ... C)
choice = []
for i in range(1,C+1):
j = 0
while j < len(arr) and i >= arr[j]:
if dp[i] >= dp[i-arr[j]]+1:
dp[i] = dp[i-arr[j]]+1
j+=1
choice.append(arr[j-1])
# 回溯找到各硬币的最少个数
coins = {}
while C > 0 :
if choice[C-1] in coins:
coins[choice[C-1]] += 1
else:
coins[choice[C-1]] = 1
C = C - choice[C-1]
return dp[-1], coins
arr = [1,2,3]
C = 16
print(min_coin_change2(arr, C))
P3:Edit Distance
问题:给定两个字符串A[1...n],B[1...m],需要算出将字符串A转换为字符串B的代价(cost)。
应用case:Spell Correction
执行代价包括:插入Insert(\(C_i\)),删除Delete(\(C_d\)),替换Replace(\(C_r\))
比如,appl -> apple(cost=1, Ci)
分析:
子问题:C(i,j): eidt distance between A[1...i] and B[1...j]
A[1...i] -> B[1...j]:
- Delete A[i], A[1...i-1] -> B[1...j]
- A[1..i] -> B[1...j-1], Insert B[j]
- if A[i]=B[j], A[1...j-1] -> B[1...j-1]
otherwise Replace A[i] with B[j], A[1..i-1] -> B[1...j-1]
\(C(i,j) = min\quad \{C_d+C(i-1,j), C(i, j-1)+C_i, C(i-1,j-1) (if A[i]=B[j]), C(i-1,j-1)+C_r (other)\}\)
##5. P3:Edit Distance
# 给定两个字符串A[1...n],B[1...m],需要算出将字符串A转换为字符串B的代价(cost)。
# 计算两个字符串直接的最短距离,涉及3个操作:add, delete, replace,假设每个操作cost=1
def edit_distance(str1, str2):
'''
输入:
str1:字符串1
str2:字符串2
输出:
str1转换为str2的最少代价(进行插入、删除、替换操作次数总和)
'''
m, n = len(str1), len(str2)
if m == 0 or str1 == None:
return n
if n == 0 or str2 == None:
return m
# dp[i][j]:str1[0...i-1](前i个字符)转换为str2[0...j-1](前j个字符)的编辑距离,
# dp[0][0]: str1为空,str2为空
dp = [[0 for i in range(n+1)] for j in range(m+1)]
for i in range(m+1):
for j in range(n+1):
# str1字符串为空,转换为str2的代价为j次插入
if i == 0:
dp[i][j] = j
# str2字符串为空,转换代价为i次删除
elif j == 0:
dp[i][j] = i
# 最后一个字符是否相等,相等则不产生代价
elif str1[i-1] == str2[j-1]:
dp[i][j] = dp[i-1][j-1]
# 最后一个字符是否相等,不相等则考虑多种可能,选择其中代价最小的值
else:
dp[i][j] = 1 + min(dp[i][j-1], # Insert
dp[i-1][j], # Remove
dp[i-1][j-1]) # Replace
return dp[m][n]
str1 = 'apple'
str2 = 'appllication'
print(edit_distance(str1, str2))
P4:Dynamic Time Wrapping
应用Case:计算distance of two time series
-
simple case:两个语音信号(长度一样)的相似度对比
\(T_1=\{T_{11}, T_{12}, T_{13}, ..., T_{1n}\}, T_2=\{T_{21}, T_{22}, T_{23}, ..., T_{2n}\}\):
两个信号的距离:\(Dist(T_1, T_2)=\sum_{i=1}^{n}(|T_{1i}-T_{2i}|)\)(欧式距离,绝对值距离等)距离越大,相似度越小。
比如,智能音箱等命令对话的识别:内嵌的语音库template与用户输入的语音进行比较。
-
complicate case:两个语音信号(长度不一样)的相似度对比
-
complicate转化为simple:利用z-Normalization
-
Time Domain转换为Frequency Domain(利用FFT,向量大小-长度相同)
-
Remain in Time Domain
-
DTW: A DP Algorithm
对于两个离散的时间序列,\(T_1\)和\(T_2\),构建两者之间的\(dp(T_1,T_2)\),这里的p定义为某一特定的wrapping path(对应关系)。
两个序列里各个离散点之间的对应关系,既可以是一对一的,也可以一对多/多对一。
但是,利用DP算法计算,需要保证几个条件(即T1和T2的wrapping path设计):
1)两个序列的开始<-->开始,结尾<-->结尾
2)Non-Overlapping,链接点之间不能有回路/交叉
3) 允许跳跃(可以给不同的rule->权重)
对于T1[1...m]和T2[1...n],构建一个m*n棋盘图形,在上边尝试不同的向上和向下走的路径,目的都是寻找从起点出发到达结尾的最短路径。T1和T2的长度不一样,可以自己设计跳跃方式:序列上的点最多跳两次(三次,四次,或者更复杂)。
dp[m,n]: minimum length of path from T1[1...m] to T2[1...n]
对任意一个对应关系(i,j),可以从5个方向和位置到达它:C1(i-1,j), C2(i-2,j-1), C3(i-1,j-1), C4(i-1, j-2), C5(i,j-1)
dp[i,j] = min{dp[i-1,j]+C1, dp[i-2,j-1]+C2, dp[i-1,j-1]+C3, dp[i-1,j-2]+C4, dp[i,j-1]+C5}
##6. P4:Dynamic Time Wrapping
# 计算两个序列之间的距离。对于两个离散的时间序列,T1[1...m]和T2[1...n],构建两者之间的dp(T1,T2),
# 这里的p定义为某一特定的 wrapping path(对应关系)。
# DTW设计:
# 1)T1和T2起始点(0,0)和终止点(m,n)互相对应
# 2) 构建一个m*n的坐标图
# 3)序列上的点之间的连接设计:最多跳2次
# 4)距离衡量选择:绝对值距离(也可以欧式距离等)
import numpy as np
def DTW(T1, T2):
'''
输入:
离散的序列T1: T1[1...m]
离散的序列T2: T2[1...n]
输出:
T1和T2之间的最短距离dp[m,n]
'''
def compute_dist(t1, t2):
return np.abs(t1-t2)
m, n = len(T1), len(T2)
# dp[i,j]: T1[1..i]到T2[1...j]的wrapping path路径最短距离
dp = np.zeros((m,n))
dp.fill(sys.maxsize)
# 初始化
dp[0, 0] = compute_dist(T1[0],T2[0])
for i in range(1, m):
dp[i, 0] = dp[i-1, 0] + compute_dist(T1[i], T2[0])
for i in range(1, n):
dp[0, i] = dp[0, i-1] + compute_dist(T1[0], T2[i])
# DP:只能向上或向右移动
for i in range(1, m):
for j in range(1,n):
cost = compute_dist(T1[i], T2[j])
ds = []
ds.append(dp[i-1,j] + cost) # 坐标点(i-1, j) -> (i,j)
ds.append(dp[i,j-1] + cost) # 坐标点(i, j-1) -> (i,j)
ds.append(dp[i-1,j-1] + cost) # 坐标点(i-1, j-1) -> (i,j)
ds.append(dp[i-1,j-2] + cost if j > 2 else sys.maxsize)# 坐标点(i-1, j-2) -> (i,j)
ds.append(dp[i-2,j-1] + cost if i > 2 else sys.maxsize)# 坐标点(i-2, j-1) -> (i,j)
dp[i,j] = min(ds)
return dp[m-1, n-1]
T1 = [3,4,6,8,1,5,8]
T2 = [3,4,7,9,2,5]
print(DTW(T1, T2))
Summary
- Global Optimization
- Viterbi(经典的DP)
- DTW is not a valid metric(Triangle Inequality)不符合三角不等式,即\(DTW(T_1,T_2)+DTW(T_2,T_3)\)不保证大于\(DTW(T_1,T_3)\)
- DP 一般用于离散数据场景,一般是可以罗列出所有可能性的情况下,一般用于比较两个序列。并不适用与AI复杂的数据环境中。
- Abstract Syntax tree比较两个代码的相似度,转换成树来比较