动态规划
通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题。
基本思想
若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
重叠子问题:在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解。
分治与动态规划
共同点:二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题。然后将子问题的解合并,形成原问题的解.
不同点:分治法将分解后的子问题看成相互独立的,通过用递归来做。
动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做。
实例
最少找零问题
最少硬币找零问题为:给予不同面值的硬币若干种种(每种硬币个数无限多),用若干种硬币组合为某种面额的钱,使硬币的的个数最少。()
在现实生活中,我们往往使用的是贪心算法,比如找零时需要13元,我们先找10元,再找2元,再找1元。这是因为现实生活中的硬币(纸币)种类特殊。如果我们的零钱可用的有1、2、5、9、10。我们找零18元时,贪心算法的策略是:10+5+2+1,四种,但是明明可以用两个9元的,这就设计动态规划。
class DynamicProgramming: def __init__(self): pass def min_change(self, coin_list, change, min_coins, coins_used): for cents in range(change + 1): # 依次循环从0到所需兑换面值的每一个面值 coin_count = cents # 初始化最优解为当前面值数,即兑换硬币个数,最大为每个都是1元硬币 new_coin = 1 # 初始化找零硬币面值列表中的面值 for j in [c for c in coin_list if c <= cents]: # 在不大于要找零的硬币面值列表中循环 if min_coins[cents - j] + 1 < coin_count: # 寻找最小的min_coins[cents - j]+1,其中j属于coin_list coin_count = min_coins[cents - j] + 1 # 临时保存当前面值的最优解,当前最优硬币个数 new_coin = j # 将当前硬币面值j临时保存为当前找零面值在找零硬币面值列表中的对应值 min_coins[cents] = coin_count # 记录当前找零面值在找零最优解列表中的最优解 coins_used[cents] = new_coin # 记录当前找零面值在找零硬币面值列表中对应的值 return min_coins[change] # 返回待找零数值的最优解 # 获取最终找零的硬币面值 def print_coins(self, coins_used, change): coins = [] while change > 0: this_coin = coins_used[change] # 从找零硬币面值列表中获取对应的硬币面值 coins.append(this_coin) change = change - this_coin # 去除该面值后继续循环获取 return coinsdp = DynamicProgramming()change = 12coin_list = [1, 5, 7, 10]coins_used = [0] * (change + 1)coin_count = [0] * (change + 1)res = dp.min_change(coin_list, change, coin_count, coins_used)coins = dp.print_coins(coins_used, change)print("最少硬币:", res)print("硬币兑换列表:", coins)
最长公共子序列
class DynamicProgramming: def __init__(self): pass def LCS_length(self, seq1, seq2): ''' name: 最长公共子序列 desc: :param seq1: 序列1 :param seq2: 序列2 :return: 最长序列表, 最长个数 ''' m, n = len(seq1) + 1, len(seq2) + 1 Lsc = [] for i in range(m): ''' 注意python列表初始化的问题 ''' elem = [] for j in range(n): if i == 0 or j == 0: elem.append(0) else: elem.append(None) Lsc.append(elem) for i in range(1, m): for j in range(1, n): if seq1[i - 1] == seq2[j - 1]: # -1 保持序列一致 Lsc[i][j] = Lsc[i - 1][j - 1] + 1 else: Lsc[i][j] = max(Lsc[i - 1][j], Lsc[i][j - 1]) return Lsc, Lsc[m-1][n-1] def show_lcs(self, Lcs, seq1, seq2): ''' :param Lcs: 最长序列表 :param seq1: 序列1 :param seq2: 序列2 :return: 公共序列 ''' i, j = len(seq1), len(seq2) show = [] while i != 0 and j != 0: if seq1[i-1] == seq2[j-1]: i -= 1 j -= 1 show.append(seq1[i]) elif Lcs[i-1][j] < Lcs[i][j-1]: j -= 1 elif Lcs[i][j-1] <= Lcs[i-1][j]: i -= 1 return show
0-1背包问题
()
class DynamicProgramming: def __init__(self): pass def knap_sack_01(self, kvs, capacity): m, n = len(kvs) + 1, capacity + 1 M = [] for i in range(m): ''' 注意python列表初始化的问题 ''' elem = [] for j in range(n): if i == 0 or j == 0: elem.append(0) else: elem.append(None) M.append(elem) for i in range(1, m): for j in range(1, n): if j - kvs[i-1][0] >= 0: M[i][j] = max(M[i-1][j], M[i-1][j-kvs[i-1][0]] + kvs[i-1][1]) else: M[i][j] = M[i-1][j] return M, M[m-1][n-1] def show_sack(self, M, kvs): show = [] m, n = len(M) - 1, len(M[0]) - 1 while True: if M[m][n] == M[m-1][n]: m -= 1 else: show.append(kvs[m-1]) n = n - kvs[m - 1][0] m -= 1 if m == 0: break return show
最优二叉搜索树
()
class DynamicProgramming: def __init__(self): pass def optimal_BST(self, p, q): ''' name: 最优二叉搜索树 :param p: :param q: :return: ''' n = len(p) # n = N + 1 root = [[0] * n for i in range(n)] e = [[0] * (n + 1) for i in range(n + 1)] # 当前的搜索期望 w = [[0] * (n + 1) for i in range(n + 1)] # 每个子树上的概率和 for i in range(1, n + 1): # 当j = i-1就是虚拟键的情况 e[i][i-1] = q[i-1] w[i][i-1] = q[i-1] for l in range(1, n): for i in range(1, n - l + 1): j = i + l - 1 e[i][j] = 0x7fffffff # 先赋值无穷 w[i][j] = w[i][j-1] + p[j] + q[j] # w递归 ,注意pq的下标相同,也就是假如当用到k2,d2时他们调用取得是相同的下标,所以p,q长度不同把p前面弄了一个-1 for r in range(i, j + 1): t = e[i][r - 1] + e[r + 1][j] + w[i][j] # e[i][j]的递归 if t < e[i][j]: e[i][j], root[i][j] = t, r # e[i][j]取最小值 return root def construct_optimal_BST(self, root, i, j): n = len(root) - 1 if i == 1 and j == n: print('root is {}'.format(root[1][n])) if i < j: print("k{}是k{}的左孩子".format(root[i][root[i][j]-1], root[i][j])) self.construct_optimal_BST(root, i, root[i][j]-1) if root[i][j] + 1 < j: print("k{}是k{}的右孩子".format(root[root[i][j] + 1][j], root[i][j])) self.construct_optimal_BST(root, root[i][j]+1, j) if i == j: print("d{}是K{}的左孩子".format(i - 1, i)) print("d{}是K{}的右孩子".format(i, i)) if i > j: print("d{}是K{}的右孩子".format(j, j))p = [-1, 0.15, 0.1, 0.05, 0.10, 0.20]q = [0.05, 0.10, 0.05, 0.05, 0.05, 0.10]root = dp.optimal_BST(p, q)print(root)print(len(root))dp.construct_optimal_BST(root, 1, len(root)-1)