5 动态规划
5.1
图书馆大门前有 n n n 级台阶, 你每次跨上 1 1 1 级或者 2 2 2 级, 请问等上 n n n 级台阶总共有多少种不同的方法? 设计一个算法求解上述问题, 尝试写出公式, 说明算法设计思想和时间复杂度.
算法设计:核心思路是函数的递归调用,当处理 n n n级台阶时,如果跨上1级则还需要处理 n − 1 n-1 n−1级台阶,如果跨上 2 2 2级则还需要处理 n − 2 n-2 n−2级台阶,容易得到递推表达式 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n-1)+f(n-2) f(n)=f(n−1)+f(n−2),这里要注意递归终止条件的设定,为了避免多统计要在 n < 0 n<0 n<0时返回 0 0 0,而在 n = 0 n=0 n=0时返回 1 1 1,代表 1 1 1种走法。
数据输入:台阶级数 n n n
结果输出:走台阶的不同方法数 s o l v e N u m b e r solveNumber solveNumber
伪码描述:
f i n d s o l v e N u m b e r ( n ) : findsolveNumber\ (n): findsolveNumber (n):
- i f n < 0 if\ \ n<0 if n<0
- $then \ return\ 0 $
- i f n = 0 if\ \ n=0 if n=0
- $then \ return\ 1 $
- r e t u r n f i n d s o l v e N u m b e r ( n − 1 ) + f i n d s o l v e N u m b e r ( n − 2 ) return \ findsolveNumber(n-1)+findsolveNumber(n-2) return findsolveNumber(n−1)+findsolveNumber(n−2)
递归方程:
{
W
(
n
)
=
W
(
n
−
1
)
+
W
(
n
−
2
)
W
(
1
)
=
1
\left\{\begin{array}{l} W(n)=W\left(n-1\right)+W\left(n-2\right) \\ W(1)=1 \end{array}\right.
{W(n)=W(n−1)+W(n−2)W(1)=1
复杂度分析:由递归方程可知,该算法的时间复杂度相当于斐波那契数列的时间复杂度
W
(
n
)
=
5
5
[
(
1
+
5
2
)
n
−
(
1
−
5
2
)
n
]
=
O
(
2
n
)
\begin{aligned} W(n)=\frac{\sqrt{5}}{5}\left[\left(\frac{1+\sqrt{5}}{2}\right)^{n}-\left(\frac{1-\sqrt{5}}{2}\right)^{n}\right]=O(2^n) \end{aligned}
W(n)=55[(21+5)n−(21−5)n]=O(2n)
该算法除了输入和输出外不需要额外的存储空间,因此空间复杂度是
S
(
n
)
=
O
(
1
)
\begin{aligned} S(n)=O(1 ) \end{aligned}
S(n)=O(1)
5.2
n n n 种币值 v 1 , v 2 , . . . , v n v_1, v_2, . . . , v_n v1,v2,...,vn 和总钱数 M M M 都是正整数. 如果每种币值的钱币至多使用 1 1 1 次, 问: 对于 M M M 是否可以有一种找零钱的方法? 设计一个算法求解上述问题. 说明算法设计思想, 分析算法最坏情况下的时间复杂度.
算法设计:核心思路是采用动态规划,不断尝试并缩小问题的规模,同时考虑尽可能多的情况,可以设函数 F ( n , M ) F(n,M) F(n,M),即使用前 n n n种钱币是否可以凑出 M M M币值,其值域为 { 0 , 1 } \{0,1\} {0,1}, 1 1 1代表存在至少一种找零钱的方法, 0 0 0代表不存在任何找零钱的方法。
递推方程:
F
(
n
,
M
)
=
max
{
F
(
n
−
1
,
M
)
,
F
(
n
−
1
,
(
M
−
v
n
)
}
F
(
1
,
M
)
=
{
1
M
=
v
1
0
M
≠
v
1
F
(
n
,
M
)
=
0
M
<
0
\begin{array}{l} F(n,M)=\max \left\{F(n-1,M), F(n-1,\left(M-v_{n}\right)\right\}\\ F(1,M)=\left\{\begin{array}{ll} 1 & M=v_{1} \\ 0 & M≠v_{1} \end{array}\right. \\ F(n,M)=0 \quad M<0 \end{array}
F(n,M)=max{F(n−1,M),F(n−1,(M−vn)}F(1,M)={10M=v1M=v1F(n,M)=0M<0
数据输入:
n
n
n种币值
v
[
1..
n
]
=
v
1
,
v
2
,
.
.
.
,
v
n
v[1..n]=v_1, v_2, . . . , v_n
v[1..n]=v1,v2,...,vn和总钱数
M
M
M
结果输出: 1 1 1或 0 0 0,其中 1 1 1代表存在至少一种找零钱的方法, 0 0 0代表不存在任何找零钱的方法。
伪码描述:
f i n d C h a n g e ( n , M , v ) : findChange\ (n,M,v): findChange (n,M,v):
- i f y < 0 if\ \ y<0 if y<0
- $then \ return\ 0 $
- i f n = 1 a n d y = v 1 if\ \ n=1\ \ and\ \ y=v_{1} if n=1 and y=v1
- $then \ return\ 1 $
- i f n = 1 a n d y ≠ v 1 if\ \ n=1\ \ and\ \ y≠v_{1} if n=1 and y=v1
- $then \ return\ 0 $
- / / 通过二进制或运算实现 M A X 函数 //通过二进制或运算实现MAX函数 //通过二进制或运算实现MAX函数
- r e t u r n f i n d C h a n g e ( n − 1 , M , v ) ∣ f i n d C h a n g e ( n − 1 , M − v [ n ] , v ) return \ findChange(n-1,M,v)|findChange(n-1,M-v[n],v) return findChange(n−1,M,v)∣findChange(n−1,M−v[n],v)
复杂度分析:由递推方程可知,该算法的时间复杂度为
W
(
n
)
=
O
(
n
M
)
\begin{aligned} W(n)=O(nM) \end{aligned}
W(n)=O(nM)
该算法由于通过二进制或运算实现了
M
A
X
MAX
MAX函数,因此除了输入和输出外不需要额外的存储空间,因此空间复杂度是
S
(
n
)
=
O
(
1
)
\begin{aligned} S(n)=O(1 ) \end{aligned}
S(n)=O(1)
5.3
设 P P P 是一台高性能服务器, T = 1 , 2 , . . . , n T = {1, 2, . . . , n} T=1,2,...,n 是 n n n 个计算任务集合, a i a_i ai 表示任务 i i i 所申请的计算资源. 已知服务器的最大计算资源是正整数 K K K. 请确定 T T T 的一个子集 S S S, 使得 ∑ i ∈ S a i ≤ K ∑ _{i∈S} a_i ≤ K ∑i∈Sai≤K, 且 K − ∑ i ∈ S a i K − ∑ _{i∈S} a_i K−∑i∈Sai 的值达到最小. 请设计一个算法求解 S S S, 并分析最坏情况下的时间复杂度.
算法设计:由题目所给的信息可以知道这实质上是一个 0 − 1 0-1 0−1背包问题,物品的价值和重量相等,由于这个背包问题并没有什么特殊已知条件,因此贪心算法并一定可以得到全局最优解,所以最好选用动态规划法来解决。
解决该问题的函数为 F ( n , K ) F(n,K) F(n,K),即只允许前 n n n个任务的申请,总计算资源不超过 K K K时能达到的最大计算资源,使 ∑ i ∈ S a i ∑ _{i∈S} a_i ∑i∈Sai的值达到最大。
递推方程:
F
(
n
,
K
)
=
max
{
F
(
n
−
1
,
K
)
,
F
(
n
−
1
,
K
−
a
n
)
+
a
n
}
F
(
0
,
K
)
=
0
F
(
n
,
0
)
=
0
F
(
1
,
K
)
=
{
a
1
,
a
1
≤
K
0
,
a
1
>
K
\begin{array}{l} F(n,K)=\max \left\{F(n-1,K), F\left(n-1,K-a_{n}\right)+a_{n}\right\} \\ F(0,K)=0 \\ F(n,0)=0 \\ F(1,K)=\left\{\begin{array}{ll} a_{1}, & a_{1} \leq K \\ 0, & a_{1}>K \end{array}\right. \\ \end{array}
F(n,K)=max{F(n−1,K),F(n−1,K−an)+an}F(0,K)=0F(n,0)=0F(1,K)={a1,0,a1≤Ka1>K
数据输入:
n
n
n 个计算任务的集合
T
[
1..
n
]
=
{
a
1
,
a
2
,
.
.
.
,
a
n
}
T[1..n]=\{a_1,a_2,...,a_n\}
T[1..n]={a1,a2,...,an}、服务器最大计算资源K
结果输出:布尔类型数组 S [ 1.. n ] S[1..n] S[1..n],值为 T r u e True True代表相应的任务位于 T T T的子集 S S S中,反之 F a l s e False False为该任务不在 T T T的子集 S S S中。
伪码描述:
f i n d S u b s e t ( n , K ) : findSubset\ (n,K): findSubset (n,K):
- i f n = 0 o r K = 0 if\ \ n=0\ or\ K=0 if n=0 or K=0
- $then \ return \ 0 $
- i f n = 1 a n d a 1 ≤ K if\ \ n=1\ and\ a_{1} \leq K if n=1 and a1≤K
- $then \ return\ a_1 $
- i f n = 1 a n d a 1 > K if\ \ n=1\ and\ a_{1} > K if n=1 and a1>K
- $then \ return \ 0 $
- x ← f i n d S u b s e t ( n − 1 , K ) x\leftarrow findSubset(n-1,K) x←findSubset(n−1,K)
- y ← 0 y\leftarrow0 y←0
- i f y − a n ≥ 0 / / 防止发生 K < 0 的错误 if\ \ y-a_{n}≥0\quad//防止发生K<0的错误 if y−an≥0//防止发生K<0的错误
- t h e n y ← f i n d S u b s e t ( n − 1 , K − a n ) + a n then \ y\leftarrow findSubset(n-1,K-a_{n})+a_{n} then y←findSubset(n−1,K−an)+an
- i f y ≥ x if\ \ y≥x if y≥x
- $then \ S[n]\leftarrow True;\ return \ y $
- e l s e S [ n ] ← F a l s e ; r e t u r n x else\ S[n]\leftarrow False;\ return\ x else S[n]←False; return x
复杂度分析:由递推方程可知,该算法的时间复杂度为
W
(
n
)
=
O
(
n
K
)
\begin{aligned} W(n)=O(nK) \end{aligned}
W(n)=O(nK)
算法需要一个布尔类型数组
S
[
1..
n
]
S[1..n]
S[1..n]来记录相应的任务是否位于
T
T
T的子集
S
S
S中,另外还有两个变量
x
,
y
x,y
x,y用于比较出最优情况,因此空间复杂度是
S
(
n
)
=
O
(
n
+
2
)
=
O
(
n
)
\begin{aligned} S(n)=O(n+2 )=O(n ) \end{aligned}
S(n)=O(n+2)=O(n)
5.4
设 I I I 是一个 n n n 位十进制整数. 如果将 I I I 划分为 k k k 段, 则可得到 k k k 个整数. 这 k k k 个整数的乘积称为 I I I 的一个 k k k 乘积. 试设计一个算法, 对于给定的 I I I 和 k k k, 求出 I I I 的最大 k k k 乘积. 尝试写出公式, 并说明算法设计思想和时间复杂度.
算法设计:核心思路依旧是采用动态规划,不断尝试并缩小问题的规模,同时考虑尽可能多的情况,设 n n n位十进制整数 I I I为 I 1 I 2 I 3 . . . I n I_1I_2I_3...I_n I1I2I3...In,函数 p a r t i t i o n ( I , i , j ) partition(I,i,j) partition(I,i,j)可以将 n n n位十进制整数 I I I中第 i i i位到第 j j j位拆分出来成为一个新的十进制整数。
设求解最大 k k k乘积的函数 F ( n , k ) F(n,k) F(n,k)将第 n n n位之前的部分划分为 k k k段得到的最大 k k k乘积,另外还需要找到这个函数的递推方程。
递推方程:
F
(
n
,
k
)
=
{
0
n
<
k
I
1
I
2
I
3
.
.
.
I
n
k
=
1
max
1
≤
i
<
n
−
1
{
F
(
i
,
k
−
1
)
∗
I
i
+
1
I
i
+
2
.
.
.
I
n
}
k
>
1
a
n
d
k
≤
n
F(n, k)=\left\{\begin{array}{ll} 0 & n<k \\ I_1I_2I_3...I_n & k=1 \\ \max _{1 \leq i<n-1}\{F(i, k-1)*I_{i+1}I_{i+2}...I_n\} & k>1 \ and \ k \leq n \end{array}\right.
F(n,k)=⎩
⎨
⎧0I1I2I3...Inmax1≤i<n−1{F(i,k−1)∗Ii+1Ii+2...In}n<kk=1k>1 and k≤n
数据输入:
n
n
n位十进制整数
I
I
I,划分段数
k
k
k
结果输出: I I I的最大 k k k乘积
伪码描述:
f i n d M a x P r o d u c t ( n , k ) : findMaxProduct\ (n,k): findMaxProduct (n,k):
- i f n < k if\ \ n<k if n<k
- $then \ return \ 0 $
- i f k = 1 if\ \ k=1 if k=1
- $then \ return\ partition(I,1,n) $
- P r o d u c t [ 1.. n − 1 ] ← { 0 , 0...0 } Product[1..n-1]\leftarrow\{0,0...0\} Product[1..n−1]←{0,0...0}
- f o r i ← 1 t o n − 1 d o / / 尝试所有可能 for \ \ i \leftarrow 1 \ \ to \ \ n-1 \ \ do\quad//尝试所有可能 for i←1 to n−1 do//尝试所有可能
- P r o d u c t [ i ] ← f i n d M a x P r o d u c t ( i , k − 1 ) ∗ p a r t i t i o n ( I , i + 1 , n ) Product[i]\leftarrow findMaxProduct(i,k-1)*partition(I,i+1,n) Product[i]←findMaxProduct(i,k−1)∗partition(I,i+1,n)
- r e t u r n M A X ( P r o d u c t ) / / 找到 P r o d u c t 数组中最大的值返回 return \ MAX(Product)\quad //找到Product数组中最大的值返回 return MAX(Product)//找到Product数组中最大的值返回
复杂度分析:由递推方程可知,每层递归的时间复杂度是
O
(
1
+
2
+
.
.
.
+
n
−
1
)
O(1+2+...+n-1)
O(1+2+...+n−1),递归深度为
O
(
k
−
1
)
O(k-1)
O(k−1),因此该算法的时间复杂度为
W
(
n
)
=
O
(
(
k
−
1
)
(
1
+
2
+
.
.
.
+
n
−
1
)
)
=
O
(
k
n
2
)
\begin{aligned} W(n)=O((k-1)(1+2+...+n-1))=O(kn^2) \end{aligned}
W(n)=O((k−1)(1+2+...+n−1))=O(kn2)
该算法每次递归都需要一个额外的
P
r
o
d
u
c
t
Product
Product数组来记录遍历过程中求出的子乘积,以便于最后使用
M
A
X
MAX
MAX函数求出最大
k
k
k乘积,因此空间复杂度是
S
(
n
)
=
O
(
n
)
\begin{aligned} S(n)=O(n) \end{aligned}
S(n)=O(n)