动态规划
剪绳子
题目:给你一根长度为n的绳子,请把绳子剪成m段(m,n都是整数,n > 1 并且 m > 1), 每段绳子的长度记为k[0],k[1],…,k[m]。请问 k[0] ∗ ∗ k[1] ... . . . ∗ ∗ k[m]可能的最大乘积是多少?
例子:当绳子的长度是8时,我们把它剪成长度为2、3 、3的三段,此时的最大乘积是18
最大乘积:
f(n)=max(f(i)∗f(n−i)),0<i<n f ( n ) = m a x ( f ( i ) ∗ f ( n − i ) ) , 0 < i < n
[f(0),f(1),f(2),f(3),...,f(n)] [ f ( 0 ) , f ( 1 ) , f ( 2 ) , f ( 3 ) , . . . , f ( n ) ]
class Solution:
def maxProdectAfterCutting_solution(self,length):
product = []
if length <2:
return 0 # 1
elif length == 2:
return 1
elif length == 3:
return 2
product.extend([0,1,2,3])
max = 0
for i in range(4,length+1):
for j in range(1,i//2+1):
area = product[j] * product[i-j]
if max < area:
max = area
product.append(max)
return max
s = Solution()
s.maxProdectAfterCutting_solution(8)
18
f(n)
f
(
n
)
最值问题
f(n)=max(f(i)∗f(n−i))
f
(
n
)
=
m
a
x
(
f
(
i
)
∗
f
(
n
−
i
)
)
分解成求多个子问题的最优解
f(5)=max(f(1)∗f(4))
f
(
5
)
=
m
a
x
(
f
(
1
)
∗
f
(
4
)
)
f(4)=max(f(1)∗f(3))
f
(
4
)
=
m
a
x
(
f
(
1
)
∗
f
(
3
)
)
问题间存在重叠,可能产生重复计算
f(n)=max(f(i)∗f(n−i))
f
(
n
)
=
m
a
x
(
f
(
i
)
∗
f
(
n
−
i
)
)
从上往下分析
需要
f(1)
f
(
1
)
f(2)
f
(
2
)
求出
f(3)
f
(
3
)
从下往上求解决
第一个特点:求最优解
第二个特点:整体的最优解依赖于子问题的最优解
第三个特点:把大问题分解为小问题,小问题之间相互重叠更小问题
第四个特点:从上往下分析问题,从下往上求解问题
数字三角形
有一个由非负整数组成的三角形,第一行只有一个数,除了最下行之外每个数的左下方和右下方各有一个数。从第一行的数开始,每次可以往左下或右下走一格,直到走到最下行,把沿途经过的数全部加起来。如何走才能使这个和尽量大?
思路:把当前的位置 (i,j) ( i , j ) 看成一个状态,定义状态的指标函数 d(i,j) d ( i , j ) 为从格子 (i,j) ( i , j ) 出发时能得到的最大和(包括 (i,j) ( i , j ) 本身)。 原来问题的解为 d(1,1) d ( 1 , 1 )
动态规划的核心: 状态和状态转移方程
状态转移方程: d(i,j)=a(i,j)+max(d(i+1,j),d(i+1,j+1)) d ( i , j ) = a ( i , j ) + m a x ( d ( i + 1 , j ) , d ( i + 1 , j + 1 ) )
注意:边界处理问题!!!
#方法一:递归计算
def d(i,j,n):
if i <= n-1 :
return a[i][j] + max( d(i+1,j,n),d(i+1,j+1,n) )
else:
return 0
a = [[1],[3,2],[4,10,1],[4,3,2,20]]
print(d(0,0,4))
24
直接用递归计算,产生重复计算
#方法二:递推计算
a = [[1],[3,2],[4,10,1],[4,3,2,20]]
d = [[1],[3,2],[4,10,1],[4,3,2,20]]
n = 4-1
for i in range(n-1,-1,-1):
for j in range(len(d[i])):
d[i][j] = a[i][j] + max(d[i+1][j],d[i+1][j+1])
print(d)
[[24], [16, 23], [8, 13, 21], [4, 3, 2, 20]]
动态规划的应用
1、有向无环图:DAG上最长路、最短路或路径计数问题
2、背包问题
3、递归结构:表达式、凸多边形、树
4、集合上的动态规划
DAG问题
硬币问题
有n中硬币,面值分别为 V1 V 1 , V2 V 2 ,…, Vn V n ,每种都有无限多。给定非负整数 S S ,可以选用多少个硬币,使得面值之和恰好为? 输出硬币数目的最小值或最大值。 1<=n<=100,0<=S<=10000,1<=Vi<=S 1 <= n <= 100 , 0 <= S <= 10000 , 1 <= V i <= S
分析:把每种面值看作一个点,表示“还需要凑足的面值”
初始状态:
S
S
目标状态:
当前状态:
i
i
每使用一个硬币 ,状态转移:
i−Vj
i
−
V
j
状态转移方程 ???
例子:如果我们有面值为1元、3元和5元的硬币若干,如何用最少的硬币凑够11元
假设
d(i)
d
(
i
)
为凑够
i
i
元所需最少硬币数,则
显然
d(1)=1
d
(
1
)
=
1
d(2)=d(2−1)+1=2
d
(
2
)
=
d
(
2
−
1
)
+
1
=
2
d(3)=d(3−1)+1=2+1=3
d
(
3
)
=
d
(
3
−
1
)
+
1
=
2
+
1
=
3
d(3)=d(3−2)+1=1+1=2
d
(
3
)
=
d
(
3
−
2
)
+
1
=
1
+
1
=
2
d(3)=d(3−3)+1=0+1=1
d
(
3
)
=
d
(
3
−
3
)
+
1
=
0
+
1
=
1
最终,
d(3)=1
d
(
3
)
=
1
d(i)=min(d(i−Vj))+1,1<=j<=n d ( i ) = m i n ( d ( i − V j ) ) + 1 , 1 <= j <= n
d(i)=min(d(i−1),d(i−3),d(i−5))+1,1<=j<=n d ( i ) = m i n ( d ( i − 1 ) , d ( i − 3 ) , d ( i − 5 ) ) + 1 , 1 <= j <= n
def select_coin(coin_value,total_value):
min_coin_num = [0]
for i in range(1,total_value + 1):
min_coin_num.append(float('inf'))
for value in coin_value:
if value <= i and min_coin_num[i - value] + 1 < min_coin_num[i]:
min_coin_num[i] = min_coin_num[ i - value] + 1
return min_coin_num
result = select_coin([1,3,5],11)
print("coin number:" + str(result[-1]))
print(result)
coin number:3
[0, 1, 2, 1, 2, 1, 2, 3, 2, 3, 2, 3]
背包问题
物品无限背包问题 —— 完全背包问题
有 n n 种物品,每种均有无穷多个。第种物品的体积为 Vi V i ,重量为 Wi W i 。 选一些物品装到一个容量为 C C 的背包中,使得背包内物品在总体积不超过C的前提下重量尽量大。
硬币问题
有中硬币,面值分别为 V1 V 1 , V2 V 2 ,…, Vn V n ,每种都有无限多。给定非负整数 S S ,可以选用多少个硬币,使得面值之和恰好为? 输出硬币数目的最小值或最大值。 1<=n<=100,0<=S<=10000,1<=Vi<=S 1 <= n <= 100 , 0 <= S <= 10000 , 1 <= V i <= S
硬币面值 ———— 物品体积
面值之和恰好为
S
S
———— 体积之和不超过
求个数最少(多) ———— 求重量最大
差别: 增加重量 (可以看成权重)
完全背包问题的状态转移方程
d(i)=max(d(i−Vj))+Wj,1<=j<=n d ( i ) = m a x ( d ( i − V j ) ) + W j , 1 <= j <= n
完全背包问题的状态转移方程
d(i)=max(d(i−Vj))+Wj,1<=j<=n d ( i ) = m a x ( d ( i − V j ) ) + W j , 1 <= j <= n
例子:有编号 a,b,c,d a , b , c , d 的四件物品,它们的重量分别是 2,3,4,7 2 , 3 , 4 , 7 ,它们的体积分别为 1,3,5,9 1 , 3 , 5 , 9 。容量为 10 10 的背包能装多重的物品?
def knapsack_complete(Volume,Weight,C):
n = len(Volume)
maxWeight = [0]
for i in range(1,C + 1):
maxWeight.append(float('-inf'))
for j in range(n):
if Volume[j] <= i and maxWeight[i - Volume[j]] + Weight[j] > maxWeight[i]:
maxWeight[i] = maxWeight[ i - Volume[j]] + 1
return maxWeight
Volume = [1,3,5,9]
Weight = [2,3,4,7]
C = 10
result = knapsack_complete(Volume,Weight,C)
print(result[-1])
print(result)
10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
0-1背包问题
有 n n 种物品,每种只有1个。第种物品的体积为 Vi V i ,重量为 Wi W i 。 选一些物品装到一个容量为 C C 的背包中,使得背包内物品在总体积不超过C的前提下重量尽量大。
0-1背包问题与完全背包问题区别:0-1背包中每件物品数量只有一个
那么,则需要考虑是否选择把物品加入背包中
需要比较把物品加进去的子问题解与不取该物品的子问题的解进行比较
例子:有编号的四件物品,它们的重量分别是 2,2,6,5,4 2 , 2 , 6 , 5 , 4 ,它们的体积分别为 6,3,5,4,6 6 , 3 , 5 , 4 , 6 ,每件物品数量只有一件。容量为 10 10 的背包能装多重的物品?
分析:
放入一件物品进容量为
10
10
的背包,则考虑分别放入
a,b,c,d,e
a
,
b
,
c
,
d
,
e
时,最大的重量
f(1,10)=6
f
(
1
,
10
)
=
6
加入一件物品时,放入物品c,重量达最大为6
如果某件物品的体积超过背包容量,该物品不用考虑
放入两件物品时,则考虑,比较两种情况,以放入a为例
放入
a
a
:
不放入,仅放入一件物品的最大重量:
f(1,10)=6
f
(
1
,
10
)
=
6
最终,放入
a
a
时,
分别放入 a,b,c,d,e a , b , c , d , e 的情况,最大值 f(2,10) = 11
0-1背包问题的状态转移方程
分析:
f(i,V)
f
(
i
,
V
)
表示一次加入
i
i
件物品,背包中最大容量为 时最重的重量
f(i,V)
f
(
i
,
V
)
= 0 ,
if
i
f
i=0
i
=
0
or
V=0
V
=
0
如果第
i
i
件物品的体积大于容量V,则该物品无法放入背包中
=
f(i−1,V)
f
(
i
−
1
,
V
)
,
if
i
f
Vi>V
V
i
>
V
比较放入与不放入第
i
i
件物品的重量
=
max((Vi+f(i−1,V−Vi),f(i−1,V))
m
a
x
(
(
V
i
+
f
(
i
−
1
,
V
−
V
i
)
,
f
(
i
−
1
,
V
)
)
,
if
i
f
i>0
i
>
0
and
Vi<=V
V
i
<=
V
可以猜测, f f 是一个矩阵 的矩阵
def knapsack_0_1(Volumes,Weights,C):
n = len(Volumes)
maxWeight =[[-1 for j in range(c+1)] for i in range(n+1)] #初始化矩阵 -1
for j in range(C+1): #第一行全为 0
maxWeight[0][j] = 0
for i in range(1,n+1):
for j in range(1,C+1):
maxWeight[i][j] = maxWeight[i-1][j]
if j >= Volumes[i-1] and maxWeight[i][j] < Weights[i-1] + maxWeight[i-1][ j - Volumes[i-1] ]:
maxWeight[i][j] = Weights[i-1] + maxWeight[i-1][ j - Volumes[i-1] ]
return maxWeight
Volumes = [2,2,6,5,4]
Weights = [6,3,5,4,6]
C = 10
result = knapsack_0_1(Volumes,Weights,C)
print(result[-1][-1])
for line in result:
print(line)
15
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[-1, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6]
[-1, 0, 6, 6, 9, 9, 9, 9, 9, 9, 9]
[-1, 0, 6, 6, 9, 9, 9, 9, 11, 11, 14]
[-1, 0, 6, 6, 9, 9, 9, 10, 11, 13, 14]
[-1, 0, 6, 6, 9, 9, 12, 12, 15, 15, 15]
def show(n,c,w,res):
print('最大价值为:',res[n][c])
x=[False for i in range(n)]
j=c
for i in range(1,n+1):
if res[i][j]>res[i-1][j]:
x[i-1]=True
j-=w[i-1]
print('选择的物品为:')
for i in range(n):
if x[i]:
print('第',i,'个',end=' ')
print('')
Volumes = [2,2,6,5,4]
Weights = [6,3,5,4,6]
C = 10
result = knapsack_0_1(Volumes,Weights,C)
print(result[-1][-1])
for line in result:
print(line)
show(n,c,w,result)
15
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[-1, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6]
[-1, 0, 6, 6, 9, 9, 9, 9, 9, 9, 9]
[-1, 0, 6, 6, 9, 9, 9, 9, 11, 11, 14]
[-1, 0, 6, 6, 9, 9, 9, 10, 11, 13, 14]
[-1, 0, 6, 6, 9, 9, 12, 12, 15, 15, 15]
最大价值为: 15
选择的物品为:
第 0 个 第 1 个 第 4 个
多重背包问题
有 n n 种物品,每种分别有个。第 i i 种物品的体积为,重量为 Wi W i 。 选一些物品装到一个容量为 C C 的背包中,使得背包内物品在总体积不超过C的前提下重量尽量大。
多重背包问题与其他背包问题区别:多重背包中每件物品数量有限不止一个
例子:有编号分别为a,b,c的三件物品,它们的重量分别是1,2,2,它们的价值分别是6,10,20,他们的数目分别是10,5,2,现在给你个承重为 8 的背包,如何让背包里装入的物品具有最大的价值总和?
解题思路:
由于每个物品多了数目限制,因此初始化和递推公式都需要更改一下。
初始化时,只考虑一件物品时,
f(1,V)=min(Num[1],V/V1)
f
(
1
,
V
)
=
m
i
n
(
N
u
m
[
1
]
,
V
/
V
1
)
。
计算考虑
i
i
件物品体积限制为时最大价值
f(i,V)
f
(
i
,
V
)
时,
递推公式考虑两种情况:
要么第
i
i
件物品一件也不放,就是,
要么第
i
i
件物品放 件,其中
(0<=k<=min(V/Vi,Num[i]))
(
0
<=
k
<=
m
i
n
(
V
/
V
i
,
N
u
m
[
i
]
)
)
,
考虑这一共
k+1
k
+
1
种情况取其中的最大价值即为
f(i,V)
f
(
i
,
V
)
的值,即
f(i,V)=max(f(i−1,V−k∗Vi)+k∗Wi)
f
(
i
,
V
)
=
m
a
x
(
f
(
i
−
1
,
V
−
k
∗
V
i
)
+
k
∗
W
i
)
。
多重背包问题的状态转移方程
分析:
f(i,V)
f
(
i
,
V
)
表示一次加入
i
i
件物品,背包中最大容量为 时最重的重量
f(i,V)
f
(
i
,
V
)
= 0 ,
if
i
f
i=0
i
=
0
or
V=0
V
=
0
f(1,V)=min(Num[1],V/V1) f ( 1 , V ) = m i n ( N u m [ 1 ] , V / V 1 )
f(i,V)=max(f(i−1,V−k∗Vi)+k∗Wi) f ( i , V ) = m a x ( f ( i − 1 , V − k ∗ V i ) + k ∗ W i ) ,其中 (0<=k<=min(V/Vi,Num[i])) ( 0 <= k <= m i n ( V / V i , N u m [ i ] ) )
可以猜测, f f 是一个矩阵 的矩阵
def knapsack_limitnum(Volumns, Weights,C,Num):
N = len(Volumns)
maxWeight = [[0 for col in range(C + 1)] for row in range(N + 1)]
for i in range(1, N+1):
for j in range(1, C+1):
# 对于物品i最多能取的次数是j // weight[i-1]与num[i-1]中较小者
max_num_i = min(j//Volumns[i-1], Num[i-1])
maxWeight[i][j] = maxWeight[i - 1][j] # 初始取k=0为最大,下面的循环是把取了k个物品i能获得的最大价值赋值给f[i][j]
for k in range(max_num_i+1):
if maxWeight[i][j] < maxWeight[i-1][j-k*Volumns[i-1]]+ k * Weights[i-1]:
maxWeight[i][j] = maxWeight[i-1][j-k*Volumns[i-1]]+ k * Weights[i-1] # 状态方程
max_weight = maxWeight[N][C]
return max_weight
Volumns =[5,4,7,2,6]
Weights=[12,3,10,3,6]
num=[2,4,1,5,3]
result = knapsack_limitnum(Volumns, Weights,V, num)
print(result)
30
集合上的动态规划
连续子数组的最大和
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为 O(n) O ( n )
例子:输入的数组为 [1,−2,3,10,−4,7,2,−5] [ 1 , − 2 , 3 , 10 , − 4 , 7 , 2 , − 5 ] ,和最大的子数组为$[3,10,-4,7,2] ,因此输出该数组的和为18
分析:
f(i)
f
(
i
)
表示以第
i
i
个数字结尾的子数组的最大和
if
i
f
i=0
i
=
0
or
f(i−1)<=0
f
(
i
−
1
)
<=
0
f(i)=f(i−1)+pData[i]
f
(
i
)
=
f
(
i
−
1
)
+
p
D
a
t
a
[
i
]
if
i
f
i!=0
i
!
=
0
and
f(i−1)>0
f
(
i
−
1
)
>
0
最后结果为
max(f(i))
m
a
x
(
f
(
i
)
)
以第
i
i
个数字结尾的子数组中所有数字的和小于0时 【】,把这个负数与第i个数累加【f(i-1) + pData[i]】,得到结果比 pData[i] 还小。该情况下,【
f(i)=pData[i]
f
(
i
)
=
p
D
a
t
a
[
i
]
】
def FindGreatestSumOfSubArray(Array):
n = len(Array)
maxSumArrayOfsubArray = [0] *(n+1)
maxSum = float('-inf')
for i in range(1,n+1):
if maxSumArrayOfsubArray[i-1] <= 0:
maxSumArrayOfsubArray[i] = Array[i-1]
else:
maxSumArrayOfsubArray[i] = maxSumArrayOfsubArray[i-1] + Array[i-1]
if maxSum < maxSumArrayOfsubArray[i]:
maxSum = maxSumArrayOfsubArray[i]
return maxSum
Array = [1,-2,3,10,-4,7,2,-5]
result = FindGreatestSumOfSubArray(Array)
print(result)
18
参考资料:
《算法竞赛入门经典》
《剑指offer》
网站[https://blog.csdn.net/huanghaocs/article/details/77920358]
[https://blog.csdn.net/na_beginning/article/details/62884939]
[https://blog.csdn.net/littlethunder/article/details/26575417]