动态规划系列文章~~~
🌹 发现有需要纠正的地方,烦请指正!
🚀 欢迎小伙伴们的三连+关注!
往期系列:
【动态规划】MATLAB和Python实现-Part01
【动态规划】MATLAB和Python实现-Part03
零、回顾
在前两部分,我们由斐波那契数列开始,介绍了递归,又介绍了动态规划的两种思路:
- 带有备忘录的递归算法;
- 自底向上的算法
在该部分,我们将以几个例子来更加深入体会 动态规划。
一、礼物的最大价值
1.1 题目描述
在一个 m × n m\times n m×n ( m m m 和 n n n 都大于 1)的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例输入:
[1, 3, 1;
1, 5, 1;
4, 2, 1]
示例输出:
12
解释:
1+3+5+2+1=12
得到最多价值的礼物
1.2 题目分析
假设棋盘大小为
m
×
n
m\times n
m×n,第
i
i
i 行第
j
j
j 列格子中有价值为
M
i
j
M_{ij}
Mij 的礼物,如图:
因为我们要从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格,直到到达棋盘的右下角。
因此,棋盘中的某一个单元格只可能从它的上边一个单元格或左边一个单元格到达。
接下来我们按照这样的思路对题目进行分析:
- 定义原问题和子问题;
- 定义状态
- 寻找状态转移方程
定义原问题和子问题
原问题:
从棋盘的左上角开始,直到到达棋盘的右下角,计算能获得的最大礼物价值。
子问题:
从棋盘的左上角开始直到到达棋盘的第
i
i
i 行第
j
j
j 列
(
1
≤
i
≤
m
,
1
≤
j
≤
n
)
(1\leq i \leq m,1 \leq j \leq n)
(1≤i≤m,1≤j≤n)的格子所能获得的最大礼物价值。
定义状态
记
f
(
i
,
,
j
)
f(i,,j)
f(i,,j) 为从棋盘左上角走至第
i
i
i 行第
j
j
j 列的格子所能获得的最大礼物价值,这里可以认为
(
i
,
j
)
(i,j)
(i,j) 就是上述子问题的状态;
当
i
=
m
,
j
=
n
i=m,j=n
i=m,j=n 时,该状态对应的
f
(
m
,
n
)
f(m,n)
f(m,n) 就是我们要求解的原问题的答案。
寻找状态转移方程
针对不同的条件,我们可以将其分为 4 种情况讨论:
- 当 i = j = 1 i=j=1 i=j=1 时,这时候在起点,有: f ( 1 , 1 ) = M 11 f(1,1)=M_{11} f(1,1)=M11
- 当 i = 1 , j > 1 i=1,j>1 i=1,j>1 时,这时候在棋盘的第一行,说明此时的路线是从第一格一直向右走,则有: f ( 1 , j ) = f ( 1 , j − 1 ) + M 1 j f(1,j)=f(1,j-1)+M_{1j} f(1,j)=f(1,j−1)+M1j
- 当 i > 1 , j = 1 i>1,j=1 i>1,j=1 时,这时候在棋盘的第一列,说明此时的路线是从第一格一直向下走,则有: f ( i , 1 ) = f ( i − 1 , j ) + M i 1 f(i,1)=f(i-1,j)+M_{i1} f(i,1)=f(i−1,j)+Mi1
- 当 i , j > 1 i,j>1 i,j>1 时,此时可从上面或左面的格子到达,则需要选择较大价值的方案,则有: f ( i , j ) = m a x { f ( i , j − 1 ) , f ( i − 1 , j ) } + M i j f(i,j)=max\{f(i,j-1),f(i-1,j)\}+M_{ij} f(i,j)=max{f(i,j−1),f(i−1,j)}+Mij
综上所述,得到最终的状态转移方程为:
f
(
i
,
j
)
=
{
M
11
i
=
j
=
1
f
(
1
,
j
−
1
)
+
M
1
j
i
=
1
,
j
>
1
f
(
i
−
1
,
j
)
+
M
i
1
i
>
1
,
j
=
1
f
(
i
,
j
)
=
m
a
x
{
f
(
i
,
j
−
1
)
,
f
(
i
−
1
,
j
)
}
+
M
i
j
i
,
j
>
1
f(i,j)=\begin{cases} M_{11} & i=j=1\\ f(1,j-1)+M_{1j} & i=1,j>1\\ f(i-1,j)+M_{i1} & i>1,j=1\\ f(i,j)=max\{f(i,j-1),f(i-1,j)\}+M_{ij} & i,j>1 \end{cases}
f(i,j)=⎩⎪⎪⎪⎨⎪⎪⎪⎧M11f(1,j−1)+M1jf(i−1,j)+Mi1f(i,j)=max{f(i,j−1),f(i−1,j)}+Miji=j=1i=1,j>1i>1,j=1i,j>1
1.3 编程求解
在对题目进行分析后,我们将使用 MATLAB
和 Python
实现编程求解:
MATLAB
函数实现:
function f = gift_value(M)
[m,n] = size(M);
dp = zeros(m,n);
for i = 1:m
for j = 1:n
if i==1 & j==1
dp(i,j) = M(i,j);
elseif i==1 & j>1
dp(i,j) = dp(i,j-1)+M(i,j);
elseif i>1 & j==1
dp(i,j) = dp(i-1,j)+M(i,j);
else
temp1 = dp(i,j-1)+M(i,j);
temp2 = dp(i-1,j)+M(i,j);
dp(i,j) = max(temp1,temp2);
end
end
end
f = dp(m,n);
end
%% =====另一种思路=====
% function f = gift_value(M)
% [m,n] = size(M);
% dp = M;
% dp(:,1) = cumsum(M(:,1));
% dp(1,:) = cumsum(M(1,:));
% for i = 2:m
% for j = 2:n
% temp1 = dp(i,j-1)+M(i,j);
% temp2 = dp(i-1,j)+M(i,j);
% dp(i,j) = max(temp1,temp2);
% end
% end
% f = dp(m,n);
% end
函数调用:
clear;clc;
M = [2, 3, 5, 7;
6, 5, 4, 5;
7, 2, 3, 5;
4, 6, 8, 1;
6, 9, 4, 3];
tic;
gift_value(M)
toc;
结果:
Python
函数实现:
def gift_value(M):
m, n = len(M), len(M[0])
dp = [[-1 for j in range(n)] for i in range(m)]
for i in range(m):
for j in range(n):
if i==0 and j==0:
dp[i][j] = M[i][j]
elif i==0 and j>0:
dp[i][j] = dp[i][j-1]+M[i][j]
elif i>0 and j==0:
dp[i][j] = dp[i-1][j]+M[i][j]
else:
temp1 = dp[i][j-1]
temp2 = dp[i-1][j]
dp[i][j] = max(temp1, temp2)+M[i][j]
return dp[-1][-1]
函数调用:
import time
M = [[2, 3, 5, 7], [6, 5, 4, 5], [7, 2, 3, 5], [4, 6, 8, 1], [6, 9, 4, 3]]
start = time.time()
f = gift_value(M)
print('礼物的最大价值动态规划算法得到的结果为:')
print(f)
end = time.time()
print()
print('礼物的最大价值动态规划算法用时(s):')
print('%.8f' % (end-start))
结果:
1.4 算法深入:得到所走的路线
在得到最大的礼物价值后,想要进一步得到所走的路线,应该如何做?
注意:产生最大礼物价值的路线可能不唯一,我们只需要输出其中一种路线即可。
我们这里从DP数组的最后一个元素出发(将DP数组的元素也对应放在棋盘中),逆着推出整个路线:
因为正常的路线每次只能选择 向下或者向右,所以逆路线只能选择 向上或者向左;
而我们判断的思路就是,比较所处位置 上格子
中的值和 左格子 中的值,选取较大的一个,不断重复直到左上角,这就是最终的路线。
具体代码实现如下:
MATLAB
函数实现:
function [f,path] = gift_value_path(M)
[m,n] = size(M);
dp = zeros(m,n);
for i = 1:m
for j = 1:n
if i==1 & j==1
dp(i,j) = M(i,j);
elseif i==1 & j>1
dp(i,j) = dp(i,j-1)+M(i,j);
elseif i>1 & j==1
dp(i,j) = dp(i-1,j)+M(i,j);
else
temp1 = dp(i,j-1)+M(i,j);
temp2 = dp(i-1,j)+M(i,j);
dp(i,j) = max(temp1,temp2);
end
end
end
f = dp(m,n);
path = zeros(m,n);
i=m;j=n;
while i~=1 || j~=1
path(i,j) = 1;
if i == 1
path(1,1:j) = 1;
break;
end
if j == 1
path(1:i,1) = 1;
break;
end
temp1 = dp(i-1,j);
temp2 = dp(i,j-1);
if temp1>=temp2
i = i-1;
else
j = j-1;
end
end
end
函数调用:
clear;clc;
M = [2, 3, 5, 7;
6, 5, 4, 5;
7, 2, 3, 5;
4, 6, 8, 1;
6, 9, 4, 3];
tic;
[f,path] = gift_value_path(M)
toc;
结果:
其中 path
矩阵中,为 1
则表示路径中存在这个格子,为 0
则表示不存在。
这里我们再给出另一种实现方式:
比上述方法复杂一丢丢,与上述方法不同之处,已在注释中标注。function [f,path] = gift_value_path(M) [m,n] = size(M); dp = zeros(m,n); for i = 1:m for j = 1:n if i==1 & j==1 dp(i,j) = M(i,j); elseif i==1 & j>1 dp(i,j) = dp(i,j-1)+M(i,j); elseif i>1 & j==1 dp(i,j) = dp(i-1,j)+M(i,j); else temp1 = dp(i,j-1)+M(i,j); temp2 = dp(i-1,j)+M(i,j); dp(i,j) = max(temp1,temp2); end end end f = dp(m,n); % 推出路径path path = zeros(m,n); i = m; j = n; while i ~= 1 || j ~= 1 path(i,j) = 1; if i == 1 path(1,1:j) = 1; break; end if j == 1 path(1:i,1) = 1; break; end temp1 = dp(i-1,j); temp2 = dp(i,j-1); %% 接下来稍有不同 ind = find([temp1,temp2]==(dp(i,j)-M(i,j)),1); if ind==1 i = i-1; else j = j-1; end end end
Python
函数实现:
def gift_value_path(M):
m, n = len(M), len(M[0])
dp = [[-1 for j in range(n)] for i in range(m)]
for i in range(m):
for j in range(n):
if i==0 and j==0:
dp[i][j] = M[i][j]
elif i==0 and j>0:
dp[i][j] = dp[i][j-1]+M[i][j]
elif i>0 and j==0:
dp[i][j] = dp[i-1][j]+M[i][j]
else:
temp1 = dp[i][j-1]
temp2 = dp[i-1][j]
dp[i][j] = max(temp1, temp2)+M[i][j]
path = [[0 for j in range(n)] for i in range(m)]
i = m-1
j = n-1
while i!=0 or j!=0:
path[i][j] = 1
if i==0:
for k in range(j):
path[0][k] = 1
break
if j==0:
for k in range(i):
path[k][0] = 1
break
temp1 = dp[i-1][j]
temp2 = dp[i][j-1]
if temp1>=temp2:
i = i-1
else:
j = j-1
return dp[-1][-1], path
函数调用:
import time
M = [[2, 3, 5, 7], [6, 5, 4, 5], [7, 2, 3, 5], [4, 6, 8, 1], [6, 9, 4, 3]]
start = time.time()
f, path = gift_value_path(M)
print('礼物的最大价值动态规划算法得到的结果为:')
print(f)
print('对应的路线为:')
print(path)
end = time.time()
print()
print('礼物的最大价值动态规划算法用时(s):')
print('%.8f' % (end-start))
结果:
二、零钱兑换
2.1 题目描述
给定不同面值的硬币 coins
和一个总金额 S
。编写一个函数来计算可以凑成总金额所需的 最少 的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
。
示例输入:
coins=[1,2,5],S=11
示例输出:
3
解释:
11=5+5+1
共需三枚硬币。
2.2 题目分析
定义原问题和子问题
原问题:
可以凑成总金额
S
S
S 所需的最少硬币个数。
子问题:
可以凑成金额
x
(
x
≤
S
)
x(x\leq S)
x(x≤S) 所需的最少硬币个数。
定义状态
我们记
f
(
x
)
f(x)
f(x) 为凑成目标金额
x
x
x 所需的最少硬币个数,这里的目标金额
x
x
x 就表示子问题所对应的状态。
寻找状态转移方程
这个问题的状态转移方程不容易寻找,我们可以从下面一个具体的例子入手:
假设 coins=[1, 2, 5, 20], S=11
,我们现在要求
f
(
11
)
f(11)
f(11),那么可以知道:状态10,状态9和状态6 可以经过 1 步转移到 状态11,所以我们可以推出:
f
(
11
)
=
m
i
n
{
f
(
10
,
f
(
9
)
,
f
(
6
)
}
+
1
f(11)=min\{f(10,f(9),f(6)\}+1
f(11)=min{f(10,f(9),f(6)}+1.
在此基础上,我们考虑其一般情形,若有
c
o
i
n
s
=
[
c
1
,
c
2
,
.
.
.
,
c
m
]
coins=[c_{1},c_{2},...,c_{m}]
coins=[c1,c2,...,cm],目标金额为
x
x
x,那么有:
f
(
x
)
=
m
i
n
{
f
(
x
−
c
1
)
,
f
(
x
−
c
2
)
,
.
.
.
,
f
(
x
−
c
m
)
}
+
1
f(x)=min\{f(x-c_{1}),f(x-c_{2}),...,f(x-c_{m})\}+1
f(x)=min{f(x−c1),f(x−c2),...,f(x−cm)}+1
但是要注意两种情况:
- 若 x − c i < 0 x-c_{i}<0 x−ci<0,说明这个硬币的面值已经超过我们的目标金额,我们就令 f ( x − c i ) = + ∞ f(x-c_{i})=+\infty f(x−ci)=+∞,这样在取最小值后就排除了这种可能;
- 若 x − c i = 0 x-c_{i}=0 x−ci=0,说明这个硬币的面值刚好等于我们的目标金额,有 f ( x − c i ) = 0 f(x-c_{i})=0 f(x−ci)=0 。
2.3 编程求解
在对题目进行分析后,我们将使用 MATLAB
和 Python
实现编程求解:
MATLAB
函数实现:
function f = coin_change(coins,S)
dp = +inf*ones(1,S+2);
dp(S+2) = 0;
% dp数组中第S+1和S+2项不改变
% 第S+1项始终为 正无穷
% 第S+2项始终为 0
for x = 1:S
ind = x-coins; % 计算前一步的状态
ind(ind<0) = S+1; % 状态小于0,令其为 正无穷对应的下标
ind(ind==0) = S+2; % 状态等于0,令其为 零对应的下标
dp(x) = min(dp(ind))+1;
end
if dp(S)<+inf
f = dp(S);
else
f = -1;
end
end
函数调用:
clear;clc;
coins = [1, 3, 5, 6, 7];
S = 41;
tic;
f = coin_change(coins, S)
toc;
结果:
Python
函数实现:
def coin_change(coins, S):
MAX = 99999999
dp = [MAX for i in range(S+2)]
dp[S+1] = 0
for x in range(1, S+1):
lis = []
for j in coins:
temp = x-j # 计算前一步状态
ind = temp-1 # ind是在dp中的索引,因此要用temp减去1
if temp<0:
ind = S # 状态小于0,令其为 正无穷对应的索引
elif temp==0:
ind = S+1 # 状态等于0,令其为 零对应的索引
lis.append(dp[ind])
dp[x-1] = min(lis)+1
if dp[S-1]<MAX:
return dp[S-1]
else:
return -1
coins = [1, 3, 5, 6, 7]
S = 41
print(coin_change(coins, S))
函数调用:
import time
coins = [1, 3, 5, 6, 7]
S = 41
start = time.time()
print('最终需要的硬币数为:')
print(coin_change(coins, S))
end = time.time()
print('算法用时为(s):')
print('%.8f' % (end-start))
结果:
2.4 算法深入:得到具体的硬币组合
我们已经知道了前面的测试结果为:6,即最少需要 6 枚硬币来达到我们的目标金额。
那我们如何知道这 6 枚硬币是怎么组成的呢?
- 首先我们知道,这 6 枚硬币的总金额为 41,且有
coins=[1, 3, 5, 6, 7]
; - 那么可以知道,如果有 5 枚硬币,可能的组成金额分别是:
41-1=40
,41-3=38
,41-5=36
,41-6=35
,41-7=34
; - 接下来,验证上述 5 枚硬币总金额:
40, 38, 36, 35, 34
的存在性,即在 DP数组 中查找相应位置,判断是否为5
,若判断为真,我们就确定了一枚硬币 (比如,DP数组中位置34
对应的值为5
,那么我们就可确定一枚面值为7
的硬币); - 按照上述步骤,不断重复过程,直至找到所有硬币。
根据上述分析,我们分别用 MATLAB
和 Python
实现代码如下:
MATLAB
函数实现:
function [f,PART] = coin_change_part(coins,S)
dp = +inf*ones(1,S+2);
dp(S+2) = 0;
for x = 1:S
ind = x-coins; % 计算前一步的状态
ind(ind<0) = S+1; % 状态小于0,令其为正无穷
ind(ind==0) = S+2; % 状态等于0,令其为零
dp(x) = min(dp(ind))+1;
end
if dp(S)<+inf
f = dp(S);
p = S; % p先指向最后一个位置
PART = []; % 初始化硬币组合向量
while dp(p)>1
q = p; % 预保存前一个位置
ind = p-coins;
ind(ind<0) = S+1;
ind(ind==0) = S+2;
p = ind(find(dp(ind)==(dp(p)-1),1));
PART = [PART, q-p];
end
PART = [PART,p];
else
f = -1;
end
end
函数调用:
clear;clc;
coins = [1, 3, 5, 6, 7];
S = 41;
tic;
[f,PART] = coin_change_part(coins, S)
toc;
结果:
Python
函数实现:
def coin_change(coins, S):
MAX = 99999999
dp = [MAX for i in range(S+2)]
dp[S+1] = 0
for x in range(1, S+1):
lis = []
for j in coins:
temp = x-j # 计算前一步状态
ind = temp-1 # ind是在dp中的索引,因此要用temp减去1
if temp<0:
ind = S # 状态小于0,令其为 正无穷对应的索引
elif temp==0:
ind = S+1 # 状态等于0,令其为 零对应的索引
lis.append(dp[ind])
dp[x-1] = min(lis)+1
if dp[S-1]<MAX:
p = S-1
PART = []
while dp[p]>1:
q = p # 预保存前一个位置
ind_lis = []
for j in coins:
temp = p - j # 计算前一步状态
ind = temp - 1 # ind是在dp中的索引,因此要用temp减去1
if temp < 0:
ind = S # 状态小于0,令其为 正无穷对应的索引
elif temp == 0:
ind = S + 1 # 状态等于0,令其为 零对应的索引
ind_lis.append(ind)
for k in range(len(ind_lis)):
if dp[ind_lis[k]]==(dp[p]-1):
p = ind_lis[k]
PART.append(q-p)
PART.append(p+1) # 将索引+1,得到对应金额
return dp[S-1], PART
else:
return -1, []
函数调用:
import time
coins = [1, 3, 5, 6, 7]
S = 41
start = time.time()
print('最终需要的硬币数为:')
f, PART = coin_change(coins, S)
print(f)
print('硬币组合为:')
print(PART)
end = time.time()
print('算法用时为(s):')
print('%.8f' % (end-start))
结果: