动态规划十大经典案例(Dynamic Programming Practice Problems)
研究方向:NLP
动态规划(Dynamic Programming)是求多阶段决策过程(Multistep Decision Process)最优化的一种数学方法,它将问题的整体按时间或空间的特征分成若干个前后衔接的时空阶段,把多阶段决策问题表示为前后有关的一系列单阶段决策问题,然后逐个求解,从而求出整个问题的最有决策序列。它强调了时间和空间的连续性。
如果没有基础知识,建议参看关文忠教授(退休)的运筹学课程中了解动态规划的建模与求解过程,动态规划的建模过程如图:
本文动态规划10个案例均来自Dynamic Programming Practice Problems
注意:虽然给出了参考链接,但是部分参考题目和本文的题目不同,只是思路相似,而且题目相同的,有的部分代码也做了优化。以防看错 请谨慎 审题 审题
leetcode 53 最大子序列和(Maximum Value Contiguous Subsequence)
题目描述:
给定一个整数数组
nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
解题思路:
这题太简单了,只是简要列出思路。
1、定义两个变量
tempSum:存储之前的累加和
maxSum: 存储当前的最大和。
2、遍历数组,当第i个数时,主要更新两个变量就可以:
判断前面累加和是否为负值,如果为负值,则累加和更新为当前值;否则,继续累加当前值
判断累加和是否大于最大和,如果大于最大和,则更新为累加和;否则,继续保留之前的最大和。
本题代码实际操作验证参考leetcode53
Python:
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
maxSum=None
tempSum=0
for i in nums:
if maxSum ==None:
maxSum=i
tempSum=max(i,tempSum+i)
maxSum=max(maxSum,tempSum)
return maxSum
leetcode 53 零钱兑换(Making Change)
题目描述:
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
解题思路:
主要理解转移方程:用逆推法
定义 d[i]为组合成 i 时需要的最少硬币数,c表示硬币的面值中元素。
向前推d[i]可以由d[i] = d[i - c]需要最少硬币数加1得到, 加1 是表示使用 c 组合了一次。
拿第一个示例举例
假设 i = 1:
当使用1币值组合,既 d[1] = d[0] + 1;假设 i = 2:
当使用1币值组合,既 d[2] = d[1] + 1;
当使用2币值组合,既 d[2] = d[0] + 1;假设 i = 3:
当使用1币值组合,既 d[3] = d[2] + 1;
当使用2币值组合,既 d[3] = d[1] + 1;......
假设 i = 6:
当使用1币值组合,既 d[6] = d[5] + 1;
当使用2币值组合,既 d[6] = d[4] + 1;
当使用5币值组合,既 d[6] = d[1] + 1;最终 d[6] 取值为这 3 种情况的最小值。
动态规划的思路是将大问题化为子问题来解决,然后逐渐往大递推,所以得到最终的动态规划方程式为:d[i] = Math.Min(d[i], d[i - c] + 1),d[i] 的值可能会随着 c不同而改变,所以需要将 d[i] 和 d[i - c] + 1 中较小值重新赋给 d[i]。
解析详细过程参考算法零钱兑换
本题代码实际操作验证参考leetcode322
python:
class Solution(object):
def coinChange(self, coins, amount):
"""
:type coins: List[int]
:type amount: int
:rtype: int
"""
if len(coins) ==0:
return -1
if amount ==0:
return 0
if len(coins) ==1 and coins[0] >amount:
return -1
d=[-1]*(amount+1)
d[0]=0
for i in range(1,amount+1):
cm=amount+1
for c in coins:
if c <= i :
#每一步中直接获取最小的值
cm = d[i-c] if d[i-c]<cm else cm
##判断是否进循环
d[i]=cm+1 if cm<amount+1 else amount+1
if d[-1]==amount+1:
return -1
else:
return d[-1]
LeetCode 300最长上升子序列(Longest Increasing Subsequence)
题目描述:
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例 :
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶:
你能将算法的时间复杂度降低到 O(n log n) 吗?
解题思路:
定义d:记录最长序列的尾元素, dp的下标+1 表示当前长序列的长度
遍历数组如果遍历的元素比dp最后一个元素大 直接拼接遍历的元素
否则直接更新dp中第一个比遍历元素大的值
可以优化的地方是二分法查找第一个比遍历元素大的值
解题详细过程参考最长递增子序列
本题代码实际操作验证参考leetcode300
Python:
class Solution(object):
def lengthOfLIS(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums) ==0 or nums == None :
return 0
if len(nums) ==1:
return 1
d=[]
d.append(nums[0])
for i in nums:
if(i>d[-1]):
d.append(i)
else:
for j in range(len(d)):
if i==d[j]:
break
if i<d[j] :
d[j]=i
break
return len(d)
堆盒子(Box Stacking)
题目描述:
给出了一组n种类型的矩形三维长方体,其中第i个长方体具有高度h(i)、宽度w(i)和深度D(i)。希望创建一个尽可能高的盒子堆,要求下一个盒子的底部的尺寸都严格大于上一个盒子的底部的尺寸。当然,可以旋转一个长方体,使任何边都作为其基部。还允许使用同一类型框的多个实例。
解题思路:
以下是问题陈述中要注意的要点:
只有上框的宽度和深度分别小于下框的宽度和深度时,才可以将一个框放置在另一个框的顶部。
我们可以旋转盒子使其宽度小于深度。例如,如果有一个具有尺寸{1x2x3}的框,其中1是高度,2×3是底部,那么可以有三种可能性,{1x2x3},{2x1x3}和{3x1x2}
我们可以使用多个盒子实例。它的意思是,我们可以有两个不同的旋转框作为我们的最大高度堆栈的一部分。
以下是基于LIS问题DP解的求解方法。
生成所有框的所有3个旋转。旋转数组的大小变为原始数组大小的3倍。为了简单起见,我们认为深度总是小于或等于宽度。
将上述生成的3n个框按基底面积的降序排序。
在对盒子进行排序后,该问题与LIS问题相同,具有以下最优子结构性质。
MSH(i)=堆栈顶部的最大可能堆栈高度
MSH(i)={Max(MSH(j))+height(i)},其中j<i and width(j)>width(i)and depth(j)>depth(i)。
如果没有j,那么MSH(i)=高度(i)
为了得到总体最大高度,我们返回max(MSH(i)),其中0 <i<n
下面是上述解决方案的实现。
解题详细过程参考box-stacking-problem-dp-22
Python:
# Dynamic Programming implementation
# of Box Stacking problem
class Box:
# Representation of a box
def __init__(self, h, w, d):
self.h = h
self.w = w
self.d = d
def __lt__(self, other):
return self.d * self.w < other.d * other.w
def maxStackHeight(arr, n):
# Create an array of all rotations of
# given boxes. For example, for a box {1, 2, 3},
# we consider three instances{{1, 2, 3},
# {2, 1, 3}, {3, 1, 2}}
rot = [Box(0, 0, 0) for _ in range(3 * n)]
index = 0
for i in range(n):
# Copy the original box
rot[index].h = arr[i].h
rot[index].d = max(arr[i].d, arr[i].w)
rot[index].w = min(arr[i].d, arr[i].w)
index += 1
# First rotation of the box
rot[index].h = arr[i].w
rot[index].d = max(arr[i].h, arr[i].d)
rot[index].w = min(arr[i].h, arr[i].d)
index += 1
# Second rotation of the box
rot[index].h = arr[i].d
rot[index].d = max(arr[i].h, arr[i].w)
rot[index].w = min(arr[i].h, arr[i].w)
index += 1
# Now the number of boxes is 3n
n *= 3
# Sort the array 'rot[]' in non-increasing
# order of base area
rot.sort(reverse = True)
# Uncomment following two lines to print
# all rotations
# for i in range(n):
# print(rot[i].h, 'x', rot[i].w, 'x', rot[i].d)
# Initialize msh values for all indexes
# msh[i] --> Maximum possible Stack Height
# with box i on top
msh = [0] * n
for i in range(n):
msh[i] = rot[i].h
# Compute optimized msh values
# in bottom up manner
for i in range(1, n):
for j in range(0, i):
if (rot[i].w < rot[j].w and
rot[i].d < rot[j].d):
if msh[i] < msh[j] + rot[i].h:
msh[i] = msh[j] + rot[i].h
maxm = -1
for i in range(n):
maxm = max(maxm, msh[i])
return maxm
# Driver Code
if __name__ == "__main__":
arr = [Box(4, 6, 7), Box(1, 2, 3),
Box(4, 5, 6), Box(10, 12, 32)]
n = len(arr)
print("The maximum possible height of stack is",
maxStackHeight(arr, n))
# This code is contributed by vibhu4agarwal
航线问题 (Building Bridges)
问题描述:
美丽的莱茵河河畔. 每边都有着N个城市, 并且每个城市都有唯一的对应有好城市. 因为莱茵河经常大雾天气
要制定航线, 每个航线不可以交叉. 现在要你求出最大的航线数目.
输入:有若干组测试数据, 每组测试数据第一行输入n, 接着n行输入a,b表示a城市,b城市联通. (1<= n <= 1000)
输出: 最大的航线数.
示例:
输入:
4
1 2
2 4
3 1
4 3
8
1 3
4 4
3 5
8 6
6 8
7 7
5 2
2 1输出:
2
3
解题思路:
1. 先将全部的a,b城市按照a的大小从小到大排序.
2. 做动态规划题目步骤:
(1).找转移状态: dp[i]: 表示当前i个城市的最大升序的长度.
(2).状态转移方程: 当dp[i] > dp[j] (i >= j)
dp[i] = max(dp[i],dp[j]+1);
这题的代码和最大上升子序列的代码 基本一样即可,没找到检查leetcode lintcode验证题,这里也就没给代码解题详细过程参考variations-of-lis-dp-21
01背包问题 lintcode125(Integer Knapsack Problem (Duplicate Items Forbidden)
题目描述:
有
n
个物品和一个大小为m
的背包. 给定数组A
表示每个物品的大小和数组V
表示每个物品的价值.问最多能装入背包的总价值是多大?
示例1:
输入: m = 10, A = [2, 3, 5, 7], V = [1, 5, 2, 4]
输出: 9
解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9
示例2:
输入: m = 10, A = [2, 3, 8], V = [2, 5, 8]
输出: 10
解释: 装入 A[0] 和 A[2] 可以得到最大价值, V[0] + V[2] = 10
进阶:
O(nm) 空间复杂度可以通过, 不过你可以尝试 O(m) 空间复杂度吗?
注意:
A[i], V[i], n, m
均为整数你不能将物品进行切分
你所挑选的要装入背包的物品的总大小不能超过
m
每个物品只能取一次
解题思路:
dp填表,只不过这里一维数组就可以,差值为a[i]的最大值+v[i] 与截至dp[i-1]对比
思路有些简单,不过多赘述:
对于代码中的逆序遍历参见01逆序遍历
完全背包,不限定次数的就可以正序遍历,解题详细过程参见01背包
本题代码实际操作验证参见lintcode125
Python1:
class Solution:
"""
@param m: An integer m denotes the size of a backpack
@param A: Given n items with size A[i]
@param V: Given n items with value V[i]
@return: The maximum value
"""
def backPackII(self, m, A, V):
# write your code here
n = len(A)
if m <= 0 or n <= 0:
return 0
dp = [0 for _ in range(m + 1)]
for i in range(n):
for j in range(m,A[i]-1,-1):
dp[j] = max(dp[j-A[i]]+V[i], dp[j])
return dp[m]
完全背包:
题目描述:
有
n
个物品和一个大小为m
的背包. 给定数组A
表示每个物品的大小和数组V
表示每个物品的价值.物品是不限使用次数的问最多能装入背包的总价值是多大?
示例:
输入:m=10, a=
[2, 3, 5, 7]
, v=[1, 5, 2, 4]
输出:15
Python:
class Solution:
"""
@param A: an integer array
@param V: an integer array
@param m: An integer
@return: an array
"""
def backPackIII(self, A, V, m):
# write your code here
n = len(A)
if n <= 0 or m <= 0:
return 0
dp = [0 for _ in range(m+1)]
for i in range(n):
for j in range(A[i], m+1):
dp[j] = max(dp[j-A[i]] + V[i], dp[j])
return dp[-1]
数组均衡划分 (Balanced partition)
题目描述:
给定一组整数,任务是将其分为两组S1和S2,使得它们的和之间的绝对差最小。
如果有一个集合S有n个元素,那么如果我们假设Subset1有m个元素,那么Subset2必须有n-m个元素,abs(sum(Subset1)–sum(Subset2))的值应该是最小的。
示例:
输入 arr[] = {1, 6, 11, 5}
输出: 1
解释: s1 = {1, 5, 6}, sum = 12 s2 = {11}, sum = 11
解题思路:
首先计算出数组的总和sum,找出和为sum/2的最大子集,即可
假设A为最大子集
sum-2A即为两个数组的最小差值
解题详细过程参考partition-problem-dp-18和数组切割问题
只是参考过程 题不是一样的 解题思路相同而已,代码本地跑过没问题
Python:
def findPartition(arr, n):
sums = 0
i, j = 0, 0
for i in range(n):
sums += arr[i]
part = [[ True for i in range(n + 1)]
for j in range(sums // 2 + 1)]
for i in range(0, n + 1):
part[0][i] = True
for i in range(1, sums // 2 + 1):
part[i][0] = False
for i in range(1, sums // 2 + 1):
for j in range(1, n + 1):
part[i][j] = part[i][j - 1]
if i >= arr[j - 1]:
part[i][j] = (part[i][j] or
part[i - arr[j - 1]][j - 1])
for i in range(sums // 2,0,-1):
for j in range(1, n + 1):
if (part[i][j]) == 1 :
return sums - 2*i
return -1
LeetCode72 编辑距离(Edit Distance)
题目描述:
给定两个单词 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')
解题思路:
编辑距离比较简单, dp table 减少重复计算提高效率
解题详情参考:编辑距离
本题代码实际操作验证参见leetcode72
Python:
class Solution(object):
def minDistance(self, word1, word2):
"""
:type word1: str
:type word2: str
:rtype: int
"""
m=len(word1)
n=len(word2)
dp=[[0]*(n+1) for _ in range(m+1) ]
for i in range(n+1):
dp[0][i]=i
for i in range(m+1):
dp[i][0]=i
for i in range(1,m+1):
for j in range(1,n+1):
if (word1[i-1]==word2[j-1]):
dp[i][j]=dp[i-1][j-1]
else:
dp[i][j]=min(dp[i-1][j-1],dp[i][j-1],dp[i-1][j])+1
return dp[-1][-1]
逻辑符处理(Counting Boolean Parenthesizations)
题目描述:
给出了一个布尔表达式,该表达式由一个符号字符串“true”、“false”、“and”、“or”和“xor”组成。计算将表达式括起来以使其计算结果为true的方法数。例如,有两种方法将“true和false xor true”括起来,使其计算结果为true。
符号:
'T' ---> true
'F' ---> false
操作符
& ---> boolean AND
| ---> boolean OR
^ ---> boolean XOR
计算将表达式括起来的方法数,以便表达式的值计算为true。
让输入以两个数组的形式出现,一个按顺序包含符号(T和F),另一个包含运算符(&,|和^})
示例:
Input: symbol[] = {T, F, T}
operator[] = {^, &}
Output: 2
The given expression is "T ^ F & T", it evaluates true
in two ways "((T ^ F) & T)" and "(T ^ (F & T))"
Input: symbol[] = {T, F, F}
operator[] = {^, |}
Output: 2
The given expression is "T ^ F | F", it evaluates true
in two ways "( (T ^ F) | F )" and "( T ^ (F | F) )".
Input: symbol[] = {T, T, F, T}
operator[] = {|, &, ^}
Output: 4
The given expression is "T | T & F ^ T", it evaluates true
in 4 ways ((T|T)&(F^T)), (T|(T&(F^T))), (((T|T)&F)^T)
and (T|((T&F)^T)).
解题思路:
如果从开始看到现在,应该有自己思路了,如果卡壳可以参见下面链接
详情参见boolean-parenthesization-problem-dp-37
Python:
def countParenth(symb, oper, n):
F = [[0 for i in range(n + 1)]
for i in range(n + 1)]
T = [[0 for i in range(n + 1)]
for i in range(n + 1)]
for i in range(n):
if symb[i] == 'F':
F[i][i] = 1
else:
F[i][i] = 0
if symb[i] == 'T':
T[i][i] = 1
else:
T[i][i] = 0
for gap in range(1, n):
i = 0
for j in range(gap, n):
T[i][j] = F[i][j] = 0
for g in range(gap):
k = i + g
tik = T[i][k] + F[i][k];
tkj = T[k + 1][j] + F[k + 1][j];
if oper[k] == '&':
T[i][j] += T[i][k] * T[k + 1][j]
F[i][j] += (tik * tkj - T[i][k] *
T[k + 1][j])
if oper[k] == '|':
F[i][j] += F[i][k] * F[k + 1][j]
T[i][j] += (tik * tkj - F[i][k] *
F[k + 1][j])
if oper[k]=='^':
T[i][j] += (F[i][k] * T[k + 1][j] +
T[i][k] * F[k + 1][j])
F[i][j] += (T[i][k] * T[k + 1][j] +
F[i][k] * F[k + 1][j])
i += 1
return T[0][n - 1]
symbols = "TTFT"
operators = "|&^"
n = len(symbols)
print(countParenth(symbols, operators, n))
博弈 (Optimal Strategy for a Game)
题目描述:
考虑一行n个硬币的值v1。vn,其中n为偶数。我们轮流与对手比赛。在每个回合中,玩家从一行中选择第一个或最后一个硬币,将其永久地从一行中移除,并接收硬币的价值。如果我们先走,确定我们能赢得的最大金额。
注:对手和用户一样聪明。
让我们用几个例子来理解这个问题:
5, 3, 7, 10 : The user collects maximum value as 15(10 + 5)
8, 15, 3, 7 : The user collects maximum value as 22(7 + 15)
在每一步中选择最好的是否会给出一个最佳的解决方案
No.在第二个例子中,这是游戏如何完成的 :
1.
…….User chooses 8.
…….Opponent chooses 15.
…….User chooses 7.
…….Opponent chooses 3.
Total value collected by user is 15(8 + 7)
2.
…….User chooses 7.
…….Opponent chooses 8.
…….User chooses 15.
…….Opponent chooses 3.
Total value collected by user is 22(7 + 15)
第二个状态中,虽然第一次选择不是最优值,但是总的取到了max的值
解题思路:
用户选择值为Vi的第i枚硬币:对手选择第(i+1)枚硬币或第j枚硬币。对手打算选择给用户留下最小价值的硬币。i、 e.用户可以采集值Vi+min(F(i+2,j),F(i+1,j-1))
用户选择价值为Vj的第j枚硬币:对手选择第i枚硬币或第(j-1)枚硬币。对手打算选择给用户留下最小价值的硬币。i、 e.用户可以采集值Vj+min(F(i+1,j-1),F(i,j-2)
详情参考:optimal-strategy-for-a-game-dp-31
Python:
def optimalStrategyOfGame(arr, n):
table = [[0 for i in range(n)]
for i in range(n)]
for gap in range(n):
for j in range(gap, n):
i = j - gap
x = 0
+ 2) <= j):
x = table[i + 2][j]
y = 0
+ 1) <= (j - 1)):
y = table[i + 1][j - 1]
z = 0
<= (j - 2)):
z = table[i][j - 2]
max(arr[i] + min(x, y), =
+ min(y, z))
return table[0][n - 1]
arr1 = [ 8, 15, 3, 7 ]
n = len(arr1)
n))
arr2 = [ 2, 2, 2, 2 ]
n = len(arr2)
n))
arr3 = [ 20, 30, 2, 2, 2, 10]
n = len(arr3)
n))
解法2:
def oSRec (arr, i, j, Sum):
if (j == i + 1):
return max(arr[i], arr[j])
return max((Sum - oSRec(arr, i + 1, j, Sum - arr[i])),
- oSRec(arr, i, j - 1, Sum - arr[j])))
def optimalStrategyOfGame(arr, n):
Sum = 0
Sum = sum(arr)
return oSRec(arr, 0, n - 1, Sum)
arr1= [ 8, 15, 3, 7]
n = len(arr1)
n))
arr2= [ 2, 2, 2, 2 ]
n = len(arr2)
n))
arr3= [ 20, 30, 2, 2, 2, 10 ]
n = len(arr3)
n))
以上仅献给努力扣码的同学们,就是写着方便查看的,另外注意的是虽然罗列出了参考链接,但是也仅是参考,有些题目都不一样,只是思路相同。请注意审题。。。。
更多请关注NLP团