往期系列:
【动态规划】MATLAB和Python实现-Part01
【动态规划】MATLAB和Python实现-Part05
零、前言
前面几部分我们通过实例体验了动态规划,本次我们进行动态规划实战,尝试解决问题。
一、爬楼梯
1.1 题目描述
假设你正在爬楼梯,需要
n
n
n 阶你才能到达楼顶,每次你可以爬 1 或 2 个台阶,你有多少种不同的方法可以爬到楼顶呢?
示例输入:
2
示例输出:
2
解释:
有两种方法爬到楼顶:1+1
或 2
1.2 题目分析
定义原问题和子问题:
原问题:
上
n
n
n 阶楼梯的方案数。
子问题:
走
k
(
k
≤
n
)
k(k\leq n)
k(k≤n) 阶楼梯的方案数。
定义状态:
令
f
(
k
)
f(k)
f(k) 表示走
k
k
k 阶楼梯的方案数。
则
k
k
k 就是此时的状态,当
k
=
n
k=n
k=n 时,
f
(
n
)
f(n)
f(n) 就是原问题的解。
寻找状态转移方程:
对该问题,容易知道:
当
k
=
1
k=1
k=1 时,
f
(
k
)
=
1
f(k)=1
f(k)=1 ;
当
k
=
2
k=2
k=2 时,
f
(
k
)
=
2
f(k)=2
f(k)=2 ;
而对于
k
>
2
k>2
k>2 时,容易知道有:
f
(
k
)
=
(
k
−
2
)
+
f
(
k
−
1
)
f(k)=(k-2)+f(k-1)
f(k)=(k−2)+f(k−1)
即可以从状态
k
−
2
k-2
k−2 一步到达,也可以从
k
−
1
k-1
k−1 一步到达。
综上所述,最终的状态转移方程为:
f
(
k
)
=
{
1
,
k
=
1
2
,
k
=
2
f
(
k
−
2
)
+
f
(
k
−
1
)
,
k
>
2
f(k)=\begin{cases} 1, & k=1 \\ 2, & k=2 \\ f(k-2)+f(k-1), & k>2 \end{cases}
f(k)=⎩
⎨
⎧1,2,f(k−2)+f(k−1),k=1k=2k>2
1.3 题目求解
MATLAB
函数实现:
function f = climb_stairs(n)
if n == 1
f = 1;
elseif n == 2
f = 2;
else
dp = ones(1,n);
dp(2) = 2;
for i = 3:n
dp(i) = dp(i-1)+dp(i-2);
end
f = dp(n);
end
end
函数调用:
clear;clc;
n = 10; % 爬 10 阶楼梯
tic;
f = climb_stairs(n)
toc;
结果:
Python
函数实现:
def climb_stairs(n):
if n==1:
f = 1
elif n==2:
f = 2
else:
dp = [1 for i in range(n)]
dp[1] = 2
for i in range(2, n):
dp[i] = dp[i-2]+dp[i-1]
f = dp[-1]
return f
函数调用:
import time
start = time.time()
n = 10
f = climb_stairs(n)
end = time.time()
print('爬楼梯动态规划求解结果为:')
print(f)
print('求解时间为(s):')
print('%.8f' % (start-end))
结果:
二、不同路径
2.1 题目描述
有一个大小为
m
×
n
m\times n
m×n 的网格,一个机器人每次只能向右或向下走一步。现在这个机器人要从左上角走到右下角。计算机器人有多少种走法。
示例输入:
m=3, n=7
示例输出:
28
解释:
机器人共有28种走法。
2.2 题目分析
定义原问题和子问题:
原问题:
若令
f
(
i
,
j
)
f(i,j)
f(i,j) 表示从棋盘左上角走到第
i
i
i 行第
j
j
j 列的格子的方法数,则当
i
=
m
,
j
=
n
i=m, j=n
i=m,j=n 时为原问题的解。
子问题:
当
i
<
m
i<m
i<m 或
j
<
n
j<n
j<n 时为子问题的解。
定义状态:
其中
i
,
j
i,j
i,j 的取值就表示不同的状态。
寻找状态转移方程:
对于二维问题,我们要找边界条件:
当
i
=
1
i=1
i=1 或
j
=
1
j=1
j=1 时,机器人只有沿直线行走的一条线路,即:
f
(
i
,
j
)
=
1
f(i,j)=1
f(i,j)=1;
当
i
>
1
i>1
i>1 且
j
>
1
j>1
j>1 时,机器人可以从上边来,或者左边来,即:
f
(
i
,
j
)
=
f
(
i
,
j
−
1
)
+
f
(
i
−
1
,
j
)
f(i,j)=f(i,j-1)+f(i-1,j)
f(i,j)=f(i,j−1)+f(i−1,j)
综上所述,最终的状态转移方程为:
f
(
i
,
j
)
=
{
1
,
i
=
1
或
j
=
1
f
(
i
,
j
−
1
)
+
f
(
i
−
1
,
j
)
,
i
>
1
且
j
>
1
f(i,j)=\begin{cases} 1, & i=1或j=1 \\ f(i,j-1)+f(i-1,j), & i>1 且 j>1 \end{cases}
f(i,j)={1,f(i,j−1)+f(i−1,j),i=1或j=1i>1且j>1
2.3 题目求解
MATLAB
函数实现:
function f = diff_path(m,n)
dp = ones(m,n);
for i = 2:m
for j = 2:n
dp(i,j) = dp(i,j-1)+dp(i-1,j);
end
end
f = dp(m,n);
end
函数调用:
clear;clc;
m = 6;
n = 7;
tic;
f = diff_path(m,n)
toc;
结果:
Python
函数实现:
def diff_path(m, n):
dp = [[1 for j in range(n)] for i in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i][j-1]+dp[i-1][j]
f = dp[-1][-1]
return f
函数调用:
import time
start = time.time()
m = 6
n = 7
f = diff_path(m, n)
end = time.time()
print('不同路径动态规划求解结果为:')
print(f)
print('求解时间为(s):')
print('%.8f' % (start-end))
结果:
三、不同路径Ⅱ
3.1 题目描述
有一个大小为
m
×
n
m\times n
m×n 的网格,一个机器人每次只能向右或向下走一步。另外,这个网格某些格点存在障碍物,机器人不能通行,现在这个机器人要从左上角走到右下角。计算机器人有多少种走法。
示例输入:
[0 0 0;
0 1 0;
0 0 0]
其中空位置为0,障碍物为1
示例输出:
2
解释:
机器人共有 2 种走法。
3.2 题目分析
定义原问题和子问题:
原问题:
若令
f
(
i
,
j
)
f(i,j)
f(i,j) 表示从棋盘左上角走到第
i
i
i 行第
j
j
j 列的格子的方法数,则当
i
=
m
,
j
=
n
i=m, j=n
i=m,j=n 时为原问题的解。
子问题:
当
i
<
m
i<m
i<m 或
j
<
n
j<n
j<n 时为子问题的解。
定义状态:
其中
i
,
j
i,j
i,j 的取值就表示不同的状态。
寻找状态转移方程:
我们可以将其分为两种情况讨论:
- 当第 i i i 行第 j j j 列的格子上有障碍物,则有 f ( i , j ) = 0 f(i,j)=0 f(i,j)=0
- 当第 i i i 行第 j j j 列的格子上没有障碍物,则有:
-
- 当 i = j = 1 i=j=1 i=j=1 时, f ( i , j ) = 1 f(i,j)=1 f(i,j)=1 ;
-
- 当 i = 1 i=1 i=1 且 j > 1 j>1 j>1 时,因为只能从左边到达,则在遇到障碍物前,有 f ( i , j ) = 1 f(i,j)=1 f(i,j)=1,在遇到障碍物后 f ( i , j ) = 0 f(i,j)=0 f(i,j)=0 ;
-
- 当 i > 1 i>1 i>1 且 j = 1 j=1 j=1 时,因此只能从上边到达,则在遇到障碍物前,有 f ( i , j ) = 1 f(i,j)=1 f(i,j)=1,在遇到障碍物后 f ( i , j ) = 0 f(i,j)=0 f(i,j)=0 ;
-
- 当 i , j > 1 i,j>1 i,j>1 时,则有 f ( i , j ) = f ( i , j − 1 ) + f ( i − 1 , j ) f(i,j)=f(i,j-1)+f(i-1,j) f(i,j)=f(i,j−1)+f(i−1,j)
3.3 题目求解
MATLAB
函数实现:
function f = diff_path02(Mt)
[m,n] = size(Mt);
dp = ones(m,n);
% 第一行
for j = 1:n
if Mt(1,j) == 1
dp(1, j:end) = 0;
break
end
end
% 第一列
for i = 1:m
if Mt(i,1) == 1
dp(i:end,1) = 0;
break
end
end
% 剩余部分
for i = 2:m
for j = 2:n
if Mt(i,j) == 1
dp(i,j) = 0;
else
dp(i,j) = dp(i,j-1)+dp(i-1,j);
end
end
end
f = dp(m,n);
end
函数调用:
clear;clc;
Mt = [0 0 0 0 0;
0 1 0 0 1;
0 0 0 1 0;
0 1 0 0 0;
0 0 0 0 0];
tic;
f = diff_path02(Mt)
toc;
结果:
Python
函数实现:
def diff_path02(Mt):
m = len(Mt)
n = len(Mt[0])
dp = [[1 for j in range(n)] for i in range(m)]
for j in range(n):
if Mt[0][j] == 1:
for k in range(j,n):
dp[0][k] = 0
for i in range(m):
if Mt[i][0] == 1:
for k in range(i, m):
dp[k][0] = 0
for i in range(1, m):
for j in range(1, n):
if Mt[i][j] == 1:
dp[i][j] = 0
else:
dp[i][j] = dp[i][j-1]+dp[i-1][j]
f = dp[-1][-1]
return f
函数调用:
import time
start = time.time()
Mt = [[0, 0, 0, 0, 0], [0, 1, 0, 0, 1], [0, 0, 0, 1, 0], [0, 1, 0, 0, 0], [0, 0, 0, 0, 0]]
f = diff_path02(Mt)
end = time.time()
print('不同路径动态规划求解结果为:')
print(f)
print('求解时间为(s):')
print('%.8f' % (start-end))
结果:
四、掷骰子的N种方法
4.1 题目描述
有
m
m
m 个完全相同的骰子,每个骰子上都有
f
f
f 个面,分别标号为
1
,
2
,
.
.
.
,
f
1,2,...,f
1,2,...,f,我们约定:掷骰子的总点数为各骰子面朝上的数字的总和,且骰子的每个面出现的概率相同。如果需要掷出的总点数为
S
S
S,请你计算出有多少种不同的组合情况。
示例输入:
m=1, f=6, S=3
示例输出:
1
解释:
只有一个骰子,要得到点数 3,只有掷出 3 满足情况。
4.2 题目分析
定义原问题和子问题:
原问题:
用
m
m
m 个骰子,掷出的总点数为
S
S
S,计算出有多少种不同的组合情况。
子问题:
用
i
i
i 个骰子,掷出的总点数为
j
j
j,计算出有多少种不同的组合情况。
定义状态:
令
f
(
i
,
j
)
f(i,j)
f(i,j) 为用
i
i
i 个骰子,掷出的总点数为
j
j
j 的组合数,则
i
,
j
i,j
i,j 就是此时的状态。
寻找状态转移方程:
先考虑边界条件:
当
i
=
1
i=1
i=1 时,说明此时只有一个骰子,则前
f
f
f 个元素为 1,其余为 0;
当
j
=
1
j=1
j=1 时,则第一个元素为
1
1
1,其余为
0
0
0;
容易得到状态转移方程如下:
f
(
i
,
j
)
=
∑
k
=
1
f
f
(
i
−
1
,
j
−
k
)
f(i,j)=\sum_{k=1}^ff(i-1,j-k)
f(i,j)=k=1∑ff(i−1,j−k)
即前
i
i
i 个骰子掷出总点数等于
j
j
j 有以下可能:
前
i
−
1
i-1
i−1 个骰子掷出总点数等于
j
−
1
j-1
j−1,第
i
i
i 个骰子掷出
1
1
1 ;
前
i
−
1
i-1
i−1 个骰子掷出总点数等于
j
−
2
j-2
j−2,第
i
i
i 个骰子掷出
2
2
2 ;
…
前
i
−
1
i-1
i−1 个骰子掷出总点数等于
j
−
f
j-f
j−f,第
i
i
i 个骰子掷出
f
f
f ;
4.3 题目求解
MATLAB
函数实现:
function F = throw_dice(m,f,S)
dp = zeros(m,S);
dp(1,1) = 1;
for j = 1:S
if j<= f
dp(1,j) = 1;
end
end
for i = 2:m
for j = 2:S
for k = 1:f
if j > k
dp(i,j) = dp(i,j)+dp(i-1,j-k);
else
break
end
end
end
end
F = dp(m,S);
end
函数调用:
clear;clc;
m = 5;
f = 8;
S = 27;
tic;
F = throw_dice(m,f,S)
toc;
结果:
Python
函数实现:
def throw_dice(m, f, S):
dp = [[0 for j in range(S)] for i in range(m)]
dp[0][0] = 1
for j in range(S):
if j+1 <= f:
dp[0][j] = 1
for i in range(1, m):
for j in range(1, S):
for k in range(1, f+1):
if j+1 > k:
dp[i][j] += dp[i-1][j-k]
else:
break
F = dp[-1][-1]
return F
函数调用:
import time
start = time.time()
m = 5
f = 8
S = 27
F = throw_dice(m, f, S)
end = time.time()
print('掷骰子的N种方法动态规划求解结果为:')
print(F)
print('求解时间为(s):')
print('%.8f' % (start-end))
结果:
五、编辑距离
5.1 题目描述
给定两个小写的英文单词 w o r d 1 word1 word1 和 w o r d 2 word2 word2,请你计算出将 w o r d 1 word1 word1 转换为 w o r d 2 word2 word2 所使用的最少操作数(定义为 w o r d 1 word1 word1 和 w o r d 2 word2 word2 的编辑距离)。可以对任意单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例输入:
word1='horse', word2='ros'
示例输出:
3
解释:
- 将
'h'
替换为'r'
:horse -> rorse
- 删除
'r'
:rorse -> rose
- 删除
e
:rose->ros
5.2 题目分析
定义原问题和子问题:
原问题:
记 4word1$ 长度为
m
m
m,
w
o
r
d
2
word2
word2 长度为
n
n
n,记
f
(
i
,
j
)
f(i,j)
f(i,j) 表示
w
o
r
d
1
word1
word1 的前
i
i
i 个字母转换为
w
o
r
d
2
word2
word2 的前
j
j
j 个字母所使用的最少操作数,那么
f
(
m
,
n
)
f(m,n)
f(m,n) 就是我们要计算的
w
o
r
d
1
word1
word1 和
w
o
r
d
2
word2
word2 的原问题的解。
子问题:
那么
f
(
i
,
j
)
,
i
<
m
,
j
<
n
f(i,j),i<m,j<n
f(i,j),i<m,j<n 就是子问题的解。
定义状态:
f
(
i
,
j
)
,
i
<
m
,
j
<
n
f(i,j),i<m,j<n
f(i,j),i<m,j<n 中的
i
,
j
i,j
i,j 就表示不同的状态。
寻找状态转移方程:
先考虑边界条件:
- 当 i = 1 i=1 i=1 时, f ( 1 , j ) f(1,j) f(1,j) 表示 w o r d 1 word1 word1 的第 1 个字母转换成 w o r d 2 word2 word2 的前 j j j 个字母所用的最少操作数。则有:
-
- 若 w o r d 1 word1 word1 的第 1 个字母不在 w o r d 2 word2 word2 中,那么需要 j j j 次,即 f ( 1 , j ) = j f(1,j)=j f(1,j)=j
-
- 若 w o r d 1 word1 word1 的第 1 个字母在 w o r d 2 word2 word2 中,找到了其位置:如果这个位置是首位,则有 f ( 1 , j ) = j − 1 f(1,j)=j-1 f(1,j)=j−1;否则,该位置之前有: f ( 1 , j ) = j f(1,j)=j f(1,j)=j,该位置之后有: f ( 1 , j ) = j − 1 f(1,j)=j-1 f(1,j)=j−1
- 当 j = 1 j=1 j=1 时, f ( i , 1 ) f(i,1) f(i,1) 表示 w o r d 1 word1 word1 的前 i i i 个字母转换成 w o r d 2 word2 word2 的第 1 个字母所用的最少操作数。则有(与 i = 1 i=1 i=1 时的情况相似):
-
- 若 w o r d 2 word2 word2 的第 1 个字母不在 w o r d 1 word1 word1 中,那么需要 i i i 次,即 f ( i , 1 ) = i f(i,1)=i f(i,1)=i
-
- 若 w o r d 2 word2 word2 的第 1 个字母在 w o r d 1 word1 word1 中,找到了其位置:如果这个位置是首位,则有 f ( i , 1 ) = i − 1 f(i,1)=i-1 f(i,1)=i−1;否则,该位置之前有: f ( i , 1 ) = i f(i,1)=i f(i,1)=i,该位置之后有: f ( i , 1 ) = i − 1 f(i,1)=i-1 f(i,1)=i−1
接下来,考虑一般情况,即当 i , j > 1 i,j>1 i,j>1 时,有三种情况:
- 当有 w o r d 1 word1 word1 的前 i − 1 i-1 i−1 个变为 w o r d 2 word2 word2 的前 j − 1 j-1 j−1 个,若有 w o r d 1 ( i ) = w o r d 2 ( j ) word1(i)=word2(j) word1(i)=word2(j) 则有: f ( i , j ) = f ( i − 1 , j − 1 ) + 0 f(i,j)=f(i-1,j-1)+0 f(i,j)=f(i−1,j−1)+0,若 w o r d 1 ( i ) ≠ w o r d 2 ( j ) word1(i)\neq word2(j) word1(i)=word2(j) 则有: f ( i , j ) = f ( i − 1 , j − 1 ) + 1 f(i,j)=f(i-1,j-1)+1 f(i,j)=f(i−1,j−1)+1 ;
- 当有 w o r d 1 word1 word1 的前 i − 1 i-1 i−1 个变为 w o r d 2 word2 word2 的前 j j j 个,那么只需要进行一步删除操作,即 f ( i , j ) = f ( i − 1 , j ) + 1 f(i,j)=f(i-1,j)+1 f(i,j)=f(i−1,j)+1 ;
- 当有 w o r d 1 word1 word1 的前 i i i 个变为 w o r d 2 word2 word2 的前 j − 1 j-1 j−1 个,那么只需要进行一步插入操作,即 f ( i , j ) = f ( i , j − 1 ) + 1 f(i,j)=f(i,j-1)+1 f(i,j)=f(i,j−1)+1.
综上所述:
{
t
e
m
p
1
=
f
(
i
−
1
,
j
−
1
)
+
{
0
,
w
o
r
d
1
(
i
)
=
w
o
r
d
2
(
j
)
1
,
w
o
r
d
1
(
i
)
≠
w
o
r
d
2
(
j
)
t
e
m
p
2
=
f
(
i
−
1
,
j
)
+
1
t
e
m
p
3
=
f
(
i
,
j
−
1
)
+
1
f
(
i
,
j
)
=
m
i
n
{
t
e
m
p
1
,
t
e
m
p
2
,
t
e
m
p
3
}
\begin{cases} temp1 = f(i-1,j-1)+\begin{cases} 0, & word1(i)=word2(j) \\ 1, & word1(i)\neq word2(j) \\ \end{cases} \\ temp2 = f(i-1,j)+1 \\ temp3 = f(i,j-1)+1 \\ f(i,j)=min\{temp1,temp2,temp3\} \end{cases}
⎩
⎨
⎧temp1=f(i−1,j−1)+{0,1,word1(i)=word2(j)word1(i)=word2(j)temp2=f(i−1,j)+1temp3=f(i,j−1)+1f(i,j)=min{temp1,temp2,temp3}
5.3 题目求解
MATLAB
函数实现:
function f = edi_dis(word1,word2)
m = length(word1);
n = length(word2);
dp = ones(m,n);
ind = strfind(word2,word1(1));
for j = 1:n
if isempty(ind)
dp(1,j) = j;
elseif ind(1) == 1
dp(1,j) = j-1;
else
if j<ind(1)
dp(1,j) = j;
else
dp(1,j) = j-1;
end
end
end
ind = strfind(word1, word2(1));
for i = 1:m
if isempty(ind)
dp(i,1) = i;
elseif ind(1) == 1
dp(i,1) = i-1;
else
if i < ind(1)
dp(i,1) = i;
else
dp(i,1) = i-1;
end
end
end
for i = 2:m
for j = 2:n
temp1 = dp(i-1,j-1)+(word1(i)~=word2(j))*1;
temp2 = dp(i-1,j)+1;
temp3 = dp(i,j-1)+1;
dp(i,j) = min([temp1, temp2, temp3]);
end
end
f = dp(m,n);
end
函数调用:
%% edi_dis
clear;clc;
word1 = 'horse';
word2 = 'ros';
tic;
f = edi_dis(word1,word2)
toc;
结果:
Python
函数实现:
def edi_dis(word1, word2):
m = len(word1)
n = len(word2)
dp = [[1 for j in range(n)] for i in range(m)]
ind = word2.find(word1[0])
for j in range(n):
if ind == -1:
dp[0][j] = j+1
elif ind == 0:
dp[0][j] = j
else:
if j < ind:
dp[0][j] = j+1
else:
dp[0][j] = j
ind = word1.find(word2[0])
for i in range(m):
if ind == -1:
dp[i][0] = i+1
elif ind == 0:
dp[i][0] = i
else:
if i < ind:
dp[i][0] = i+1
else:
dp[i][0] = i
for i in range(1, m):
for j in range(1, n):
temp1 = dp[i-1][j-1]+(1 if word1[i]!=word2[j] else 0)*1
temp2 = dp[i-1][j]+1
temp3 = dp[i][j-1]+1
dp[i][j] = min(temp1, temp2, temp3)
f =dp[-1][-1]
return f
函数调用:
import time
start = time.time()
word1 = 'horse'
word2 = 'ros'
f = edi_dis(word1, word2)
end = time.time()
print('动态规划求解结果为:')
print(f)
print('求解时间为(s):')
print('%.8f' % (start-end))
结果: