动态规划经典问题

动态规划

参考网站: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 ] = m a x { A [ j − 1 ] + a r r [ j ] , a r r [ j ] } A[j] = max\quad \{A[j-1]+arr[j], arr[j]\} A[j]=max{A[j1]+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: array
        输出:
        和最大的子数组的和
        '''
        # 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 1=V_1 < V_2 < V_3 < ... < V_n 1=V1<V2<V3<...<Vn,每种硬币数量充足,张三想要把手里价值C的纸币换成硬币,越少越好,如何设计方案?

分析:
子问题:当纸币金额为j时,最少的硬币兑换个数是 M ( j ) M(j) M(j)

构建dp数列: [ M ( 1 ) , M ( 2 ) , . . . , M ( j ) , . . . , M ( C ) ] [M(1), M(2), ... , M(j), ... , M(C)] [M(1),M(2),...,M(j),...,M(C)]

M ( j ) M(j) M(j)的转化方程: M ( j ) = m i n { M ( j − V 1 ) + 1 , M ( j − V 2 ) + 1 , M ( j − V 3 ) + 1 , . . . , M ( j − V n ) + 1 } M(j)=min\quad \{M(j-V_1)+1, M(j-V_2)+1, M(j-V_3)+1, ..., M(j-V_n)+1\} M(j)=min{M(jV1)+1,M(jV2)+1,M(jV3)+1,...,M(jVn)+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 C_i Ci),删除Delete( C d C_d Cd),替换Replace( C r C_r Cr)

比如,appl -> apple(cost=1, C i C_i 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 ) = m i n { C d + C ( i − 1 , j ) , C ( i , j − 1 ) + C i , C ( i − 1 , j − 1 ) ( i f A [ i ] = B [ j ] ) , C ( i − 1 , j − 1 ) + C r ( o t h e r ) } 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)\} C(i,j)=min{Cd+C(i1,j),C(i,j1)+Ci,C(i1,j1)(ifA[i]=B[j]),C(i1,j1)+Cr(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 1 n } , T 2 = { T 21 , T 22 , T 23 , . . . , T 2 n } T_1=\{T_{11}, T_{12}, T_{13}, ..., T_{1n}\}, T_2=\{T_{21}, T_{22}, T_{23}, ..., T_{2n}\} T1={T11,T12,T13,...,T1n},T2={T21,T22,T23,...,T2n}:

    两个信号的距离: D i s t ( T 1 , T 2 ) = ∑ i = 1 n ( ∣ T 1 i − T 2 i ∣ ) Dist(T_1, T_2)=\sum_{i=1}^{n}(|T_{1i}-T_{2i}|) Dist(T1,T2)=i=1n(T1iT2i)(欧式距离,绝对值距离等)距离越大,相似度越小。

    比如,智能音箱等命令对话的识别:内嵌的语音库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_1 T1 T 2 T_2 T2,构建两者之间的 d p ( T 1 , T 2 ) dp(T_1,T_2) dp(T1,T2),这里的p定义为某一特定的wrapping path(对应关系)。
D T W = m i n { d p ( T 1 , T 2 ) } , p ∈ Ω , Ω = a l l p o s s i b l e w r a p p i n g p a t h DTW = min\quad \{dp(T_1,T_2)\}, p\in\Omega,\Omega =all \quad possible \quad wrapping \quad path DTW=min{dp(T1,T2)}pΩΩ=allpossiblewrappingpath
两个序列里各个离散点之间的对应关系,既可以是一对一的,也可以一对多/多对一。

但是,利用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)不符合三角不等式,即 D T W ( T 1 , T 2 ) + D T W ( T 2 , T 3 ) DTW(T_1,T_2)+DTW(T_2,T_3) DTW(T1,T2)+DTW(T2,T3)不保证大于 D T W ( T 1 , T 3 ) DTW(T_1,T_3) DTW(T1,T3)
  4. DP 一般用于离散数据场景,一般是可以罗列出所有可能性的情况下,一般用于比较两个序列。并不适用与AI复杂的数据环境中。
  5. Abstract Syntax tree比较两个代码的相似度,转换成树来比较

欢迎各位关注我的个人公众号:HsuDan,我将分享更多自己的学习心得、避坑总结、面试经验、AI最新技术资讯。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值