动态规划

动态规划

参考网站: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. 递归方法实现

    ##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))
    
  2. 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]:

  1. Delete A[i], A[1...i-1] -> B[1...j]
  2. A[1..i] -> B[1...j-1], Insert B[j]
  3. 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:两个语音信号(长度不一样)的相似度对比

    1. complicate转化为simple:利用z-Normalization

    2. Time Domain转换为Frequency Domain(利用FFT,向量大小-长度相同)

    3. Remain in Time Domain

DTW: A DP Algorithm

对于两个离散的时间序列,\(T_1\)\(T_2\),构建两者之间的\(dp(T_1,T_2)\),这里的p定义为某一特定的wrapping path(对应关系)。

\[DTW = min\quad \{dp(T_1,T_2)\}, p\in\Omega,\Omega =all \quad possible \quad wrapping \quad 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

  1. Global Optimization
  2. Viterbi(经典的DP)
  3. DTW is not a valid metric(Triangle Inequality)不符合三角不等式,即\(DTW(T_1,T_2)+DTW(T_2,T_3)\)不保证大于\(DTW(T_1,T_3)\)
  4. DP 一般用于离散数据场景,一般是可以罗列出所有可能性的情况下,一般用于比较两个序列。并不适用与AI复杂的数据环境中。
  5. Abstract Syntax tree比较两个代码的相似度,转换成树来比较
posted @ 2021-04-18 23:54  MissHsu  阅读(87)  评论(0编辑  收藏  举报