动态规划算法

目录

1. 概述

2. 算法的基本步骤

3. 经典实例分析

3.1 硬币找零

3.2 袋鼠过河问题

3.3 矩阵链乘法问题

3.4 最长公共子序列

3.5 最优二叉查找树


1. 概述

动态规划(dynamic programming)是通过组合子问题的解而解决整个问题。这里要特别注意动态规划算法与分治算法的区别。分治算法是指将问题划分成一些独立的子问题,递归地求解各个子问题,然后合并子问题的解而得到原问题的解(例如:merge sort 算法)。与此不同,动态规划实用与子问题不是独立的情况,也就是各子问题包含公共的子问题。在这种情况下,若采用分治法则会做许多不必要的工作,即重复的求解公共的子问题。动态规划算法对每个子问题只求解一次,将其结果保存在一张表中,从而避免重新求解的问题,即子问题会有重叠的现象。

动态规划常常应用于最优化问题。此类问题可能多种可行解。每个解有一个值,而我们希望找出一个具有最优值得解。

2. 算法的基本步骤

  • 描述最优解的结构
  • 递归定义最优解得值
  • 按自底向上的方式计算最优解的值
  • 由计算出的结果构造一个最优解

简而言之,就是利用递归的方法来解决问题,然而与传统递归不同的是:在计算过程中我们需要存储每一个子问题的最优解,在进行下一步递归是要利用这些解来计算的。

3. 经典实例分析

3.1 硬币找零

题目:假设有几种硬币,如1,3,5,并且数量无限。请找出能够组成某个数目的找零所使用最少的硬币数。

难度系数:⭐️

分析:假设需要组成的数目为n,最简单的思路为穷举所有组合的情况,其算法复杂度为O(3^n)。故而计算成本是非常大的,而我们这里利用动态规划的方法来分析问题。首先通过分析问题的递推公式为:F(i) = \left \{\begin{array}{lr}min\{F(i- X[0])+1,F(i-X[1]) + 1, .........\}, when\ i \notin X\\1,\ when\ i \in X \end{array} \right.;其中F(i)为组成 i 所需要最少的硬币数,X为储存硬币的种类的数组。

PS:简单的理解就是得到 i 的最优的方式是,直接利用最少 i - x(x \in X)的最优组合方式直接加上x即可。

算法(Python 2.7.8):

#!/usr/bin/pyenv python


# X is store the kinds of coins, n is the number which you want get


def findCoin(coins, n):
    # the results is matrix for store the best way to get the number
    results = list()
    temp = list()
    temp.append(0)

    # results[0] means the way to get zero is [0] means no way to get 0
    results.append(temp)
    for i in range(1, n+2):
        minIndex = 0
        min_len = n + 2
        for coin in coins:
            # check the number is in the coins array
            if i == coin:
                # you can get this number directly
                min_len = 1
                print('the number is in the array')
                break
            # This is the recursion function
            # PS: len(results[x]) means the number of coins
            #      and if results[x][0] is 0 means you can not get x using the coins given
            elif i - coin > 0 and results[i - coin][0] != 0 and len(results[i - coin]) < min_len:
                min_len = len(results[i - coin]) + 1
                # Here i = minLastCoin + minIndex
                minLastCoin = coin
                minIndex = i - coin
        temp = list()
        if min_len == 1:
            temp.append(i)
        elif min_len == n + 2:
            temp.append(0)
        else:
            # Should be care you can not using = directly in python you can using copy()
            temp = results[minIndex][:]
            temp.append(minLastCoin)
        print(results)
        results.append(temp)
    return results
if __name__ == '__main__':
    # coins is to store the list of coin; number is the number you want to get
    coins = [2, 4, 8, 10]
    number = 78
    results = findCoin(coins, number)
    # if you only want to get the number of the coin used only print len(results[number])
    print(str(number)+' = '+'+'.join(list(map(str, results[number]))))

3.2 袋鼠过河问题

题目:一只袋鼠要从河这边跳到河对岸,河很宽,但是河中间打了很多桩子,每隔一米就有一个,每个桩子上都有一个弹簧,袋鼠跳到弹簧上就可以跳的更远。每个弹簧力量不同,用一个数字代表它的力量,如果弹簧力量为5,就代表袋鼠下一跳最多能够跳5米。如果为0,就会陷进去无法继续跳跃。河流一共N米宽,袋鼠初始位置就在第一个弹簧上面,要跳到最后一个弹簧之后就算过河,给定每个弹簧的力量,求袋鼠最少需要多少跳能够到达对岸。如果无法到达输出-1。

难度系数:⭐️

分析:利用动态规划的思路,袋鼠到达某个桩子的最优步数的递推公式为:S(i) = min(S(m) +1), when\ X[m] \ge(i - m)\ \&\ m< i, 即到达 i 的最优步数为能够所有通过之前的木桩可以直接跳到 i 的最优步数+1 的最小值。例如:要到达第10个木桩,那需要判断可以通过前面9个木桩能够到达次木桩,然后从中选择步数最小的。

算法(python 2.7.8):

#!/usr/bin/pyenv python

"""
This programe is used to solve the kangaroo cross river by dynamic programing
author: Han  Xia
Time: 2018-11-8
Version 1.0.0
"""

# this is a structure to store the way for kangaroo cross river
class crossRiverWay:
    def __init__(self):
        self.step = 0
        self.ways = list()


def kangarooCrossRiver(springs, river):
    results = list()
    result = crossRiverWay()
    results.append(result)
    print results[0].step
    # TODO can using the max springs to optimize
    # maxSpring  =  max(springs)
    for i in range(1, river + 1):
        print i
        result = crossRiverWay()
        result.step = river + 2
        for j in range(i):
            # justify all the way can arrive the i
            if springs[j] + j >= i and result.step > results[j].step + 1 and results[j].step != -1:
                way = list()
                result.step = results[j].step + 1
                way = results[j].ways[:]
                way.append(i - j)
                for m in range(1, i-j):
                    way.append('*')
        if result.step == river + 2:
            result.step = -1
        else:
            result.ways = way[:]
            print result.ways
        results.append(result)
    return results
if __name__ == '__main__':
    river = 7
    springs = [2, 0, 1, 1, 2, 1, 2]
    results = kangarooCrossRiver(springs, river)
    # results store the minimum step number and the way

3.3 矩阵链乘法问题

题目:给定一个n个矩阵的序列(矩阵链)<A1, A2, .......,An>,我们希望计算它们的乘积 A1*A2*......An,为了计算表达式,我们可以利用括号来明确计算顺序。一组矩阵的乘积是加全部括号的(fully parenthesized),根据矩阵的结合律,无论怎么加括号都可以得到相同的结果。例如<A1, A2, A3, A4>, 共有5种完全括号化的矩阵乘积链的积。

(A1*(A2*(A3*A4))); (A1*((A2*A3)*A4)); ((A1*A2)*(A3*A4)); ((A1*(A2*A3))*A4); (((A1*A2)*A3)*A4)

矩阵链加括号的顺序对求积运算的代价有很大影响。矩阵的计算效率A矩阵为p*q的,B矩阵为q*r的,则A*B的计算效率为p*q*r。例如三个矩阵为10*100, 100*5, 5*50,如果按照((A1*A2)*A3)计算则计算效率为10*100*5+10*5*50,但是按照(A1*(A2*A3) 计算需要100*5*50+10*100*50。所以我们希望利用算法来找到一个最优的求解过程。

难度系数:⭐️⭐️

分析:A_i 矩阵为 p_{i-1} \times p_{i}, 则A_i A_{i+1}.....A_j的最优解

m[i,j] =\left \{\begin{array}{lr} 0,\ i =j \\ \underset{i \le k <j}{min} \{m[i,k]+m[k+1,j]+p_{i-1}p_k p_j\}, i <j \end{array} \right.

PS:第 i 到 j 项乘法,最优解可以分为按照所有的 k 值,的最优值。

算法(python 2.7.8):

#!/usr/bin/pyenv python

"""
This programe is used to solve the best way to computex the matrix chain multiplication
author: Han  Xia
Time: 2018-11-12
Version 1.0.0
"""
class resultsStore:
    def __init__(self):
        self.com = 0
        self.way = ''


def matrixChainMult(matrixNum, matrixChains):

    results = [[0 for x in range(matrixNum + 1)] for y in range(matrixNum + 1)]
    for i in range(1, matrixNum + 1):
        result1 = resultsStore()
        result1.way = 'A'+str(i)
        results[i][i] = result1
        print results[i][i].com
    for delta in range(1, matrixNum):
        print delta
        for i in range(1, matrixNum - delta + 1):
            end = i + delta
            result2 = resultsStore()
            result2.com = float('inf')
            for k in range(i, end):
                complication = results[i][k].com + results[k+1][end].com + matrixChains[i - 1] * matrixChains[k] *matrixChains[end]
                if complication < result2.com:
                    result2.com = complication
                    result2.way = '('+results[i][k].way+' * '+results[k+1][end].way+')'
            results[i][end] = result2
    return results[1][matrixNum]
if __name__ == '__main__':
    # p is a list store the structure for matrix chain
    matrixNum = 6
    matrixChains = [30, 35, 15, 5, 10, 20, 25]
    result = matrixChainMult(matrixNum, matrixChains)
    print result.com

3.4 最长公共子序列

问题:给定两个字符串的最长公共子序列 (long Common Sequence)。比如字符串1:BDCABA; 字符串2:ABCBDAB, 则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA。

难度系数:⭐️⭐️

分析:假设两个子序列分别为X[m], Y[n], 其公共子序列为LCS(X[0 : m], Y[0 : n]), 分情况讨论:

if X[m] == Y[n], LCS(X[0 : m], Y[0 : n]) = LCS(X[0: m -1], Y[0 : n-1])+X[m]

if X[m] != Y[n], LCS(X[0 : m], Y[0 : n]) = max(LCS(X[0: m -1], Y[0 : n]),  LCS(X[0: m], Y[0 : n-1]))

#!/usr/bin/pyenv python

"""
This is a code for solve the long common sequences using the dynamic programming
Author : Han Xia
Time: Nov 21, 2018
Version: 1.0.0
"""

def longCommonSequences(X, Y):
    # compute the length of X and Y
    lenX = len(X)
    lenY = len(Y)
    # create a 2 D array to store the middle result
    lcqs = [[0 for x in range(lenY + 1)] for y in range(lenX + 1)]
    # initial the zero row and columns is null
    # means that the lcq is null when any sequences with another null sequence
    for i in range(lenX + 1):
        lcqs[i][0] = ''
    for j in range(lenY + 1):
        lcqs[0][j] = ''
        
    for i in range(1, lenX + 1):
        for j in range(1, lenY + 1):
            print('i ='+str(i)+'; j = '+str(j))
            # when they are the same
            if X[i - 1] == Y[j - 1]:
                lcqs[i][j] = lcqs[i - 1][j -1]+X[i - 1]
            elif len(lcqs[i - 1][j]) >= len(lcqs[i][j - 1]):
                lcqs[i][j] = lcqs[i - 1][j]
            else:
                lcqs[i][j] = lcqs[i][j - 1]
    return lcqs[lenX][lenY]

if __name__ == '__main__':
    X = 'abcicba'
    Y = 'abdkscab'
    print(list(X))
    print(list(Y))
    # Convert the string to a list
    lcq = longCommonSequences(list(X), list(Y))
    print lcq

3.5 最优二叉查找树

背景介绍:最优二叉树又称哈夫曼树( Huffman), 是指对于一组带有确定权值的叶子节点所构造的具有带权路径长度最短的二叉树。

形式地,给定一个由n个互异的关键字组成的序列K = <k1, k2, ...... kn>, 且关键字是有序的, 即k1<k2...<kn, 设对于每个关键字ki,一次搜索ki的概率为pi。某些搜索的值可能不在K内,因此还有 n+1个“虚拟键” d0, d1, .....dn代表不在K内的值,具体地,d0代表所有小于k1的值,dn代表所有大于kn的值,而对于虚拟键di 代表所有位于ki与ki+1之间的值。对于每一个di的概率是qi。则可知:

\sum_{i=1}^n p_i +\sum_{i=0}^n q_i = 1

定义在一颗二叉树里面的搜索代价为:

\begin{aligned} E[T] & = \sum_{i=1}^n(depth(k_i) +1) * p_i + \sum_{i=0}^n(depth(di)+ 1)*q_i \\ & =1+\sum_{i=1}^n depth(k_i) * p_i + \sum_{i=0}^n depth(di)*q_i \end{aligned}

算法思路:定义e[i, j], 为搜索一颗包含关键字k_i, k_{i+1}.........k_j的最优二叉查找树的期望代价。最终我们要计算的为e[1, n], 当 j = i - 1 时,此时只有虚拟键di-1。其搜索代价为e[i, j-1] = q_{i-1}

j\ge i时,此时e[i, j] = \min \limits_{i \le r \le j}\{e[i, r - 1] +e[r+1, j] +\sum_{x=i}^j p_x + \sum_{x=i-1}^j q_x\},即选择 k_r 作为二叉树的根,k_i ......k_{r -1}作为二叉树的左子树,k_{r+1}.............k_j作为二叉树的右子树。

算法:

#!/usr/bin/pyenv python

import numpy as np
import math
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
"""
This program is for build optimal BST by Dynamic Programming
Author Han XIA, Dec 16, 2018
Version 2.0.0
"""

# This is the class for trees
class TreeNode(object):
    def __init__(self, data=None, left= None, right=None):
        self.height = 0
        self.data = data
        self.left = left
        self.right = right

    def __str__(self):
        return str(self.data)

# Store the weight and tree
class weightBST(object):
    def __init__(self, weight, data=None, left= None, right=None):
        self.weights = weight
        self.bstTree = TreeNode(data, left, right)

# layer traversal for binary search tree
def queue_tarversal(root):
    outList = []
    queue = [root]
    while queue != [] and root:
        outList.append(queue[0].data)
        if queue[0].left != None:
            queue.append(queue[0].left)
        if queue[0].right !=None:
            queue.append(queue[0].right)
        queue.pop(0)
    return outList

class locBSTElements(object):
    def __init__(self, bstTree):
        self.bstTree = bstTree
        self.depth = 0
        self.height = 0
        self.locationX = 0
        self.locationY = 0


def plotBSTTree (bstTree):
    deltaX = 20
    deltaY = 50
    rootLoc = locBSTElements(bstTree)
    rootLoc.depth = 0
    rootLoc.height = bstTree.height
    rootLoc.locationX = 600 / 2
    rootLoc.locationY = 400 -50
    queue = [rootLoc]

    while queue != [] and rootLoc:
        if queue[0].bstTree.left != None:
            locLeft = locBSTElements(queue[0].bstTree.left)
            locLeft.depth = queue[0].depth + 1
            locLeft.height = rootLoc.height
            locLeft.locationX = queue[0].locationX - deltaX * int(math.pow(2, locLeft.height - locLeft.depth))
            locLeft.locationY = queue[0].locationY - deltaY * 1
            queue.append(locLeft)
            plt.plot([locLeft.locationX, queue[0].locationX], [locLeft.locationY, queue[0].locationY], '--b',linewidth = 0.5)
        if queue[0].bstTree.right != None:
            locRight = locBSTElements(queue[0].bstTree.right)
            locRight.depth = queue[0].depth + 1
            locRight.height = rootLoc.height
            locRight.locationX = queue[0].locationX + deltaX * int(math.pow(2, locRight.height - locRight.depth))
            locRight.locationY = queue[0].locationY - deltaY * 1
            plt.plot([locRight.locationX, queue[0].locationX], [locRight.locationY, queue[0].locationY], '--b', linewidth = 0.5)
            queue.append(locRight)
        plotInfo = queue.pop(0)
        plt.text(plotInfo.locationX, plotInfo.locationY, plotInfo.bstTree.data)
        plt.xlim([0, 600])
        plt.ylim([0, 400])
    plt.show()

def buildOptimalBST(elementsLen, elements ,elementsWeights, leafs,leafWeights):
    results = [[0 for x in range(elementsLen + 2)] for y in range(elementsLen+ 2)]

    # init the tree only have leaf
    for startIndex in range(1, elementsLen + 2):
        results[startIndex][startIndex -1] = weightBST(leafWeights[startIndex - 1], leafs[startIndex - 1])
    # delta from 0 to elementsLen - 1
    for delta in range(0, elementsLen):
        for startIndex in range(1, elementsLen - delta + 1):
            endIndex = startIndex +delta
            weight = float('inf')
            weightsTemp = 0
            for i in range(startIndex, endIndex + 1):
                weightsTemp += (elementsWeights[i] + leafWeights[i])
            weightsTemp += leafWeights[startIndex - 1]
            # choose the best root
            for r in range(startIndex, endIndex + 1):
                weightsR = results[startIndex][r - 1].weights + results[r + 1][endIndex].weights + weightsTemp
                if weight > weightsR:
                    weight = weightsR
                    results[startIndex][endIndex] = weightBST(weightsR, elements[r], results[startIndex][r - 1].bstTree,
                                                              results[r + 1][endIndex].bstTree)
                    results[startIndex][endIndex].bstTree.height = max(results[startIndex][r - 1].bstTree.height,
                                                                       results[r + 1][endIndex].bstTree.height) + 1
    return results

if __name__ == '__main__':
    elements = ['', 'A', 'B', 'C', 'D', 'E']
    elementsWeights = [0, 0.15, 0.10, 0.05, 0.10, 0.20]
    leafs = ['0', '1', '2', '3', '4', '5']
    leafWeights = [0.05, 0.10, 0.05, 0.05, 0.05, 0.10]

    weightsBST = buildOptimalBST(5, elements, elementsWeights, leafs, leafWeights)
    print(weightsBST[1][5].weights)
    outList = queue_tarversal(weightsBST[1][5].bstTree)
    print outList
    plotBSTTree(weightsBST[1][5].bstTree)

最终输出的outList 为二叉树的层序遍历。

最后画出我们的二叉树结构, 输出为:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值