动态规划
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。
动态规划的特点
Dynamic programming solves problems by combining the solutions to sub-problems
A dynamic-programming algorithm solves every sub-problem just once and then saves its answer in a table, thereby avoiding the work of recomputing the answer every time the sub-problemis encountered.
-
最优子结构:通过子问题的最优解,推导问题最优解
-
无后效性:只关心前一个状态,不关心推导过程
-
重复子问题:不同的决策阶段有重复的状态
经典问题:斐波那契数列,dijkstra最短路径,背包问题,项目规划等
动态规划经典实例
动态规划所解决的经典问题包括:斐波那契数列,dijkstra最短路径,背包问题,项目规划等,下面举几个的例子,以便更好理解其思想。
斐波那契数列
斐波那契数列的实现方法有多种,这里记录几个自己想到的方法。
-
【方法一】直接递归
直接递归法,空间复杂度O(N),时间复杂度太高O(2^N),时间开销太大,一般的电脑只能算出前几十个数,不可行。
-
【方法二】使用历史计算结果的递归
递归调用,但是存储已计算的结果以供使用。时间复杂度O(N),空间复杂度O(N)
fi = {1:1, 2:1} # 存储已计算的结果,{序号:值} def fib(n): if n in fi: # 判断是否已经计算过 value = fi[n] else: value = fib(n-1) + fib(n-2) fi[n] = value return value print(f(100))
-
【方法三】动态规划(使用一个辅助变量c)
def fib(n): a, b = 1, 1 c = 0 for i in range(2, n): c = a + b a = b b = c return c print(fib(100))
-
【方法四】动态规划(不另开辟内存空间)
从第3个数开始依次计算,不另开辟内存空间。时间复杂度O(N),空间复杂度O(1)
num = 0 #循环变量 n = 100 # 计算第几个Fibonanci数 a = 1; b = 1 # 前两个数 while num < n - 2: # 减2是因为前两个数已知,只需循环n-2次 if num % 2 == 0: # 为了节省空间,只使用两个变量,奇偶交替更新 a = a + b # 更新a print(a) else: b = a + b # 更新b print(b) num += 1
连续子序列最大和问题(LeetCode 53)
问题:给定一数组arr,寻找子数组使得它们的之和最大,比如(1,-1,-2,3,5,-1,4,-2)
-
【方法一】暴力解决
找到所有可能的子序列,计算其和并记录下来,排序,返回最大值。
-
【方法二】动态规划
用指针j记录序列的结尾处索引,则结尾元素表示为arr[j],用A[j]表示以j结尾的序列的最大和。
递推公式:
[A[j]=max{A[j-1]+arr[j], arr[j]} ]时间复杂度为O(N),空间复杂度O(1)。
import numpy as np
import sys
def max_subseq_sum(arr):
"""
寻找和最大的子序列,返回起止索引及最大和的值。
最大和序列可能会有多个,这里仅考虑**最长**的子序列。
:param arr:
:return: 和最大的子序列的起止索引及最大和的值(all_idx_start,all_idx_end, max_all)
"""
max_current = 0 # 记录局部的最大和
max_all = -sys.maxsize # 记录全局最大和,初始化为最小的数
cur_idx_start, cur_idx_end = 0, 0 # 局部最大和序列起止索引
all_idx_start, all_idx_end = 0, 0 # 全局最大和序列起止索引
for i in range(len(arr)):
if max_current >= 0: # 即max_current + arr[i] >= arr[i]
cur_idx_end += 1 # 序列向后扩增一位
max_current += arr[i]
if max_current >= max_all: # 【if分支A】
all_idx_start = cur_idx_start
all_idx_end = cur_idx_end
max_all = max_current
else:
cur_idx_start = i
cur_idx_end = i
max_current = arr[i]
# 【if分支A也可以放在此处,但放在上面逻辑更严谨】
return all_idx_start, all_idx_end, max_all
arr1 = [-1, 1, 2, -3, 4, 5, 2, 4]
result = max_subseq_sum(arr1)
print(result)
Coin Change Problem
问题:假如有n种硬币,它们的价格分别是$ 1=v_1<v_2<v_3<v_n $。想把手里价值为C的纸币换成硬币,越少越好,如何设计方案?
进阶:如果是要得到零钱方案呢,怎么得到?
编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成。
进阶:如果需要得到编辑距离的操作步骤,怎么做?
最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000
1 <= text2.length <= 1000
输入的字符串只含有小写英文字符。
最长公共子序列问题在机器翻译的评价中可以用到。