动态规划——扔鸡蛋问题的递归算法与非递归算法
基础版
有一幢高100层的楼,鸡蛋从 x x x层投下时刚好会碎。现持有2个完全相同的鸡蛋,试设计一个最优方法来找出 x x x,使以此方法投下鸡蛋时,最坏情况下所投掷的总次数 N ( 2 , 100 ) N(2,100) N(2,100)最少。
进阶版
有一幢高 f f f层的楼,鸡蛋从 x x x层投下时刚好会碎。现持有 e e e个完全相同的鸡蛋,试设计一个最优方法来找出 x x x,使以此方法投下鸡蛋时,最坏情况下所投掷的总次数 N ( e , f ) N(e,f) N(e,f)最少。
记
e
e
e为鸡蛋数,
f
f
f为楼层高度,
N
(
e
,
f
)
N(e,f)
N(e,f)为所求最优算法的投掷总次数,
φ
\varphi
φ为某一算法,
ψ
\psi
ψ为全量算法集合,
x
x
x为鸡蛋破碎楼层,
n
e
,
f
(
x
∣
φ
)
n_{e,f}(x|\varphi)
ne,f(x∣φ)为确定
(
e
,
f
,
φ
,
x
)
(e,f,\varphi,x)
(e,f,φ,x)情况下所需投掷的次数。
我们可以得出
N
(
e
,
f
)
N(e,f)
N(e,f)的表达式如下
N
(
e
,
f
)
=
min
φ
∈
ψ
max
0
≤
x
≤
f
n
e
,
f
(
x
∣
φ
)
N(e,f)=\min\limits_{\varphi \in \psi}\max\limits_{0\leq x\leq f}n_{e,f}(x|\varphi)
N(e,f)=φ∈ψmin0≤x≤fmaxne,f(x∣φ)
带着表达式,我们来思考这个问题。
相信凡是有过编程经验的人,都听说过二分法。
介于
<
,
≤
,
=
,
≠
,
≥
,
>
<,\leq ,=,\neq ,\geq ,>
<,≤,=,=,≥,>均是二元关系运算符,二分法成为了最高效的查找算法。其身影不仅出现在查找过程中,在二叉树、归并排序等等算法中均得以一窥。
然而本题并非二分法的领域,以基础版问题为例,若以二分法投鸡蛋,则首次在第50层投掷(记为
T
1
=
50
T_1=50
T1=50)后,若鸡蛋破碎,即
0
<
x
≤
50
0<x\leq 50
0<x≤50时,第二次必须从第1层开始投掷,否则若从第2层开始投掷且鸡蛋破碎,则无法确定
x
=
1
x=1
x=1还是
x
=
2
x=2
x=2。若首次在第50层投掷,最坏情况即
x
=
50
x=50
x=50,此时投掷次数
N
=
50
N=50
N=50,这显然并非最优方法。
但是二分法仍然可以为我们带来启示。根据上述推断,我们可以得出两条规律:
- N ( e , f ) ≥ ⌊ l o g 2 f ⌋ w h e r e e ≥ ⌊ l o g 2 f ⌋ N(e,f)\geq \lfloor log_2 f\rfloor\quad{\rm where}\ e\geq \lfloor log_2 f\rfloor N(e,f)≥⌊log2f⌋where e≥⌊log2f⌋
- N ( 1 , f ) = f N(1,f)=f N(1,f)=f
第一条为二分法所规定的投掷次数下限,即鸡蛋数量充足时至少要投掷的次数,不过事实上这一条的参考意义有限。
第二条为仅有一个鸡蛋时的投掷次数,这一条则是重要的边界条件之一。
依然由
T
1
=
50
T_1=50
T1=50,我们考虑
T
1
=
49
T_1=49
T1=49,最坏情况仍是
x
=
49
x=49
x=49,此时的有
N
=
49
<
50
N=49<50
N=49<50。可以发现
T
1
T_1
T1减少时,
N
N
N同步在减少。那是否
T
1
T_1
T1越小越好呢?并非如此。
我们在
T
1
T_1
T1从50减少到49的过程中,实际默认了“对
x
>
T
1
x>T_1
x>T1的情况下,使用2个鸡蛋总能在
N
−
1
N-1
N−1次内找出
x
x
x”。随着
T
1
T_1
T1越来越小,这一条件的实现越来越困难。但好在对于
x
≤
T
1
x\leq T_1
x≤T1的情况,我们容易确认最坏情况下需要
N
=
T
1
N=T_1
N=T1次来找出
x
x
x。那么当首次投掷鸡蛋不破碎时,我们问题变更为在
T
1
+
1
T_1+1
T1+1至100间找出
T
2
T_2
T2。从而
T
1
+
1
T_1+1
T1+1至100间至多投掷
N
−
1
N-1
N−1次。由
T
1
=
N
T_1=N
T1=N,容易得出
T
2
=
N
−
1
T_2=N-1
T2=N−1。从而有
T
1
+
T
2
+
…
+
T
N
=
N
+
N
−
1
+
…
+
1
=
N
(
N
−
1
)
/
2
≥
100
T_1+T_2+…+T_N=N+N-1+…+1=N(N-1)/2\geq 100
T1+T2+…+TN=N+N−1+…+1=N(N−1)/2≥100
从而
N
≥
14
N\geq 14
N≥14,即最优时
N
=
14
N=14
N=14。此时的投掷方法为:
- 按14,27,39,50,60,69,77,84,90,95,99,100(103)的次序投掷,任一次破碎时,从上一个未破碎节点开始逐个投掷。
\frac{}{}
当问题升级为进阶版时,就不能简单地用
T
1
=
N
T_1=N
T1=N来判断了,但我们仍然可以使用基础版的思想。
以下事实显然:
- N ( 1 , f ) = f N(1,f)=f N(1,f)=f
- N ( e , 0 ) = 0 N(e,0)=0 N(e,0)=0
对任意
e
,
f
e,f
e,f与
T
T
T,
N
(
e
,
f
)
T
N(e,f)_T
N(e,f)T的取值分两种情况,①
T
T
T投掷使鸡蛋破碎,则后续所需投掷次数为
N
(
e
−
1
,
T
)
N(e-1,T)
N(e−1,T);②
T
T
T投掷未使鸡蛋破碎,则后续所需投掷次数为
N
(
e
,
f
−
T
−
1
)
N(e,f-T-1)
N(e,f−T−1)。
从而我们得到
N
(
e
,
f
)
T
=
1
+
max
(
N
(
e
−
1
,
T
)
,
N
(
e
,
f
−
T
−
1
)
N(e,f)_T=1+\max(N(e-1,T),N(e,f-T-1)
N(e,f)T=1+max(N(e−1,T),N(e,f−T−1)
从而有动态规划的边界与最优子结构如下
- N ( 1 , f ) = f N(1,f)=f N(1,f)=f
- N ( e , Z \ N ∗ ) = 0 N(e,Z\backslash N^*)=0 N(e,Z\N∗)=0
- N ( e , f ) = 1 + min 0 ≤ i ≤ f − 1 max ( N ( e − 1 , i ) , N ( e , f − i − 1 ) ) N(e,f)=1+\min\limits_{0\leq i\leq f-1}\max(N(e-1,i),N(e,f-i-1)) N(e,f)=1+0≤i≤f−1minmax(N(e−1,i),N(e,f−i−1))
对此可给出代码实现
public static int dropEgg(int egg, int floor) {
if (egg == 1)
return floor;
if (floor <= 0)
return 0;
int min = floor;
for (int i = 0; i < floor; i++)
min = Math.min(min, 1 + Math.max(dropEgg(egg - 1, i), dropEgg(egg, floor - i - 1)));
return min;
}
这一算法的递归嵌套极为恐怖,甚至不能解决 N ( 2 , 100 ) N(2,100) N(2,100)问题。引入Map来存储已计算的 N ( e , f ) N(e,f) N(e,f)可尽可能规避递归栈溢出与大幅减少运算时间。
private static Map<String, Integer> dropMap = new HashMap<String, Integer>();
public static int dropEgg(int egg, int floor) {
String s = egg + " " + floor;
if (dropMap.containsKey(s))
return dropMap.get(s);
if (egg == 1)
return floor;
if (floor <= 0)
return 0;
int min = floor;
for (int i = 0; i < floor; i++)
min = Math.min(min, 1 + Math.max(dropEgg(egg - 1, i), dropEgg(egg, floor - i - 1)));
dropMap.put(s, min);
return min;
}
递归的算法到此为止,接下来是非递归的算法。
为了得到非递归算法,我们需要进一步的分析问题。除了上文所提及的两个边界与最优子结构,我们还需引入两个显然的关系式:
- N ( e , f ) ≥ N ( e + 1 , f ) N(e,f)\geq N(e+1,f) N(e,f)≥N(e+1,f)
- N ( e , f ) ≤ N ( e , f + 1 ) N(e,f)\leq N(e,f+1) N(e,f)≤N(e,f+1)
从而对 i ≤ ⌊ ( f − 1 ) / 2 ⌋ i\leq \lfloor (f-1)/2\rfloor i≤⌊(f−1)/2⌋,我们有
- N ( e − 1 , i ) ≤ N ( e − 1 , f − i − 1 ) N(e-1,i)\leq N(e-1,f-i-1) N(e−1,i)≤N(e−1,f−i−1)
- N ( e , f − i − 1 ) ≤ N ( e − 1 , f − i − 1 ) N(e,f-i-1)\leq N(e-1,f-i-1) N(e,f−i−1)≤N(e−1,f−i−1)
从而
max
(
N
(
e
−
1
,
i
)
,
N
(
e
,
f
−
i
−
1
)
)
≤
max
(
N
(
e
,
i
)
,
N
(
e
−
1
,
f
−
i
−
1
)
)
\max(N(e-1,i),N(e,f-i-1))\leq \max(N(e,i),N(e-1,f-i-1))
max(N(e−1,i),N(e,f−i−1))≤max(N(e,i),N(e−1,f−i−1))
故最优子结构可收束如下:
N
(
e
,
f
)
=
1
+
min
0
≤
i
≤
⌊
(
f
−
1
)
/
2
⌋
max
(
N
(
e
−
1
,
i
)
,
N
(
e
,
f
−
i
−
1
)
)
N(e,f)=1+\min\limits_{0\leq i\leq \lfloor (f-1)/2\rfloor}\max(N(e-1,i),N(e,f-i-1))
N(e,f)=1+0≤i≤⌊(f−1)/2⌋minmax(N(e−1,i),N(e,f−i−1))
(这一收束同样可优化递归算法,代码参见后附全代码)
在
i
≤
⌊
(
f
−
1
)
/
2
⌋
i\leq \lfloor (f-1)/2\rfloor
i≤⌊(f−1)/2⌋的基础下,考察满足以下两种情况的
i
i
i:
- N ( e − 1 , i ) = N ( e , f − i − 1 ) N(e-1,i)=N(e,f-i-1) N(e−1,i)=N(e,f−i−1)
- { N ( e − 1 , i ) ≠ N ( e , f − i − 1 ) N ( e − 1 , i ) = N ( e , f − i − 2 ) N ( e − 1 , i + 1 ) = N ( e , f − i − 1 ) \left\{\begin{array}{l} N(e-1,i)\neq N(e,f-i-1)\\ N(e-1,i)=N(e,f-i-2)\\ N(e-1,i+1)=N(e,f-i-1) \end{array}\right. ⎩⎨⎧N(e−1,i)=N(e,f−i−1)N(e−1,i)=N(e,f−i−2)N(e−1,i+1)=N(e,f−i−1)
对第一种情况,有
{
max
(
N
(
e
−
1
,
i
+
δ
)
,
N
(
e
,
f
−
i
−
1
−
δ
)
)
≥
N
(
e
−
1
,
i
+
δ
)
≥
N
(
e
−
1
,
i
)
=
max
(
N
(
e
−
1
,
i
)
,
N
(
e
,
f
−
i
−
1
)
)
max
(
N
(
e
−
1
,
i
−
δ
)
,
N
(
e
,
f
−
i
−
1
+
δ
)
)
≥
N
(
e
,
f
−
i
−
1
+
δ
)
≥
N
(
e
,
f
−
i
−
1
)
=
max
(
N
(
e
−
1
,
i
)
,
N
(
e
,
f
−
i
−
1
)
)
\left\{\begin{array}{l} \max(N(e-1,i+\delta),N(e,f-i-1-\delta))\geq N(e-1,i+\delta)\\ \qquad\geq N(e-1,i)= \max(N(e-1,i),N(e,f-i-1))\\ \max(N(e-1,i-\delta),N(e,f-i-1+\delta))\geq N(e,f-i-1+\delta)\\ \qquad\geq N(e,f-i-1)= \max(N(e-1,i),N(e,f-i-1)) \end{array}\right.
⎩⎪⎪⎨⎪⎪⎧max(N(e−1,i+δ),N(e,f−i−1−δ))≥N(e−1,i+δ)≥N(e−1,i)=max(N(e−1,i),N(e,f−i−1))max(N(e−1,i−δ),N(e,f−i−1+δ))≥N(e,f−i−1+δ)≥N(e,f−i−1)=max(N(e−1,i),N(e,f−i−1))
从而
N
(
e
,
f
)
=
1
+
N
(
e
−
1
,
i
)
N(e,f)=1+N(e-1,i)
N(e,f)=1+N(e−1,i)。
对第二种情况,由于
N
(
e
,
f
)
N(e,f)
N(e,f)在
N
∗
N^*
N∗上是连续变换的,从而第二种情况与第一种情况互补,故对任意
N
(
e
,
f
)
N(e,f)
N(e,f),总存在
i
i
i符合两种情况其一。
又有
{
max
(
N
(
e
−
1
,
i
+
1
+
δ
)
,
N
(
e
,
f
−
i
−
2
−
δ
)
)
≥
N
(
e
−
1
,
i
+
1
+
δ
)
≥
N
(
e
−
1
,
i
+
1
)
=
max
(
N
(
e
−
1
,
i
+
1
)
,
N
(
e
,
f
−
i
−
2
)
)
max
(
N
(
e
−
1
,
i
−
δ
)
,
N
(
e
,
f
−
i
−
1
+
δ
)
)
≥
N
(
e
,
f
−
i
−
1
+
δ
)
≥
N
(
e
,
f
−
i
−
1
)
=
max
(
N
(
e
−
1
,
i
)
,
N
(
e
,
f
−
i
−
1
)
)
\left\{\begin{array}{l} \max(N(e-1,i+1+\delta),N(e,f-i-2-\delta))\geq N(e-1,i+1+\delta)\\ \qquad\geq N(e-1,i+1)= \max(N(e-1,i+1),N(e,f-i-2))\\ \max(N(e-1,i-\delta),N(e,f-i-1+\delta))\geq N(e,f-i-1+\delta)\\ \qquad\geq N(e,f-i-1)= \max(N(e-1,i),N(e,f-i-1)) \end{array}\right.
⎩⎪⎪⎨⎪⎪⎧max(N(e−1,i+1+δ),N(e,f−i−2−δ))≥N(e−1,i+1+δ)≥N(e−1,i+1)=max(N(e−1,i+1),N(e,f−i−2))max(N(e−1,i−δ),N(e,f−i−1+δ))≥N(e,f−i−1+δ)≥N(e,f−i−1)=max(N(e−1,i),N(e,f−i−1))
从而
N
(
e
,
f
)
=
1
+
max
(
N
(
e
−
1
,
i
)
,
N
(
e
,
f
−
i
−
1
)
)
N(e,f)=1+\max(N(e-1,i),N(e,f-i-1))
N(e,f)=1+max(N(e−1,i),N(e,f−i−1))。
故对满足以下条件的 i ≤ ⌊ ( f − 1 ) / 2 ⌋ i\leq \lfloor (f-1)/2\rfloor i≤⌊(f−1)/2⌋:
- N ( e − 1 , i ) = N ( e , f − i − 1 ) N(e-1,i)=N(e,f-i-1) N(e−1,i)=N(e,f−i−1)
- { N ( e − 1 , i ) ≠ N ( e , f − i − 1 ) N ( e − 1 , i ) = N ( e , f − i − 2 ) N ( e − 1 , i + 1 ) = N ( e , f − i − 1 ) \left\{\begin{array}{l} N(e-1,i)\neq N(e,f-i-1)\\ N(e-1,i)=N(e,f-i-2)\\ N(e-1,i+1)=N(e,f-i-1) \end{array}\right. ⎩⎨⎧N(e−1,i)=N(e,f−i−1)N(e−1,i)=N(e,f−i−2)N(e−1,i+1)=N(e,f−i−1)
有 N ( e , f ) = 1 + max ( N ( e − 1 , i ) , N ( e , f − i − 1 ) ) N(e,f)=1+\max(N(e-1,i),N(e,f-i-1)) N(e,f)=1+max(N(e−1,i),N(e,f−i−1))。
由此我们得到:
- N ( 1 , f ) = f N(1,f)=f N(1,f)=f
- N ( e , Z \ N ∗ ) = 0 N(e,Z\backslash N^*)=0 N(e,Z\N∗)=0
-
N
(
e
,
f
)
=
1
+
max
(
N
(
e
−
1
,
i
)
,
N
(
e
,
f
−
i
−
1
)
)
N(e,f)=1+\max(N(e-1,i),N(e,f-i-1))
N(e,f)=1+max(N(e−1,i),N(e,f−i−1))
此处 i ≤ ⌊ ( f − 1 ) / 2 ⌋ i\leq \lfloor (f-1)/2\rfloor i≤⌊(f−1)/2⌋满足
N ( e − 1 , i ) = N ( e , f − i − 1 ) N(e-1,i)=N(e,f-i-1) N(e−1,i)=N(e,f−i−1)
或 { N ( e − 1 , i ) ≠ N ( e , f − i − 1 ) N ( e − 1 , i ) = N ( e , f − i − 2 ) N ( e − 1 , i + 1 ) = N ( e , f − i − 1 ) \left\{\begin{array}{l} N(e-1,i)\neq N(e,f-i-1)\\ N(e-1,i)=N(e,f-i-2)\\ N(e-1,i+1)=N(e,f-i-1) \end{array}\right. ⎩⎨⎧N(e−1,i)=N(e,f−i−1)N(e−1,i)=N(e,f−i−2)N(e−1,i+1)=N(e,f−i−1)
由此可得代码实现
public static int[][] dropEggArray(int egg, int floor) {
int[][] eggFloorArray = new int[eggNum][floorNum + 1];
for (int j = 0; j <= floorNum; j++)
eggFloorArray[0][j] = j;
for (int i = 0; i < eggNum; i++)
eggFloorArray[i][0] = 0;
for (int i = 1; i < eggNum; i++)
for (int j = 1; j <= floorNum; j++)
for (int k = j - 1 >> 1; k >= 0; k--)
if (eggFloorArray[i - 1][k] == eggFloorArray[i][j - k - 1]
|| (eggFloorArray[i - 1][k] == eggFloorArray[i][j - k - 2]
&& eggFloorArray[i - 1][k + 1] == eggFloorArray[i][j - k - 1])) {
eggFloorArray[i][j] = 1 + Math.max(eggFloorArray[i - 1][k], eggFloorArray[i][j - k - 1]);
break;
}
return eggFloorArray;
}
全量代码如下
import java.util.HashMap;
import java.util.Map;
public class Egg {
private static final int eggNum = 5;
private static final int floorNum = 2500;
public static void main(String[] args) {
long time = System.currentTimeMillis();
int[][] dropEggArray = dropEggArray(eggNum, floorNum);
System.out.println("N=" + dropEggArray[eggNum - 1][floorNum]);
System.out.println("非递归算法耗时" + (-time + (time = System.currentTimeMillis())) + "ms");
System.out.println("N=" + dropEgg(eggNum, floorNum));
System.out.println("递归算法耗时" + (-time + (time = System.currentTimeMillis())) + "ms");
}
private static Map<String, Integer> dropMap = new HashMap<String, Integer>();
public static int dropEgg(int egg, int floor) {
String s = egg + " " + floor;
if (dropMap.containsKey(s))
return dropMap.get(s);
if (egg == 1)
return floor;
if (floor <= 0)
return 0;
int min = floor;
for (int i = 0; i <= floor >> 1; i++)
min = Math.min(min, 1 + Math.max(dropEgg(egg - 1, i), dropEgg(egg, floor - i - 1)));
dropMap.put(s, min);
return min;
}
public static int[][] dropEggArray(int egg, int floor) {
int[][] eggFloorArray = new int[eggNum][floorNum + 1];
for (int j = 0; j <= floorNum; j++)
eggFloorArray[0][j] = j;
for (int i = 0; i < eggNum; i++)
eggFloorArray[i][0] = 0;
for (int i = 1; i < eggNum; i++)
for (int j = 1; j <= floorNum; j++)
for (int k = j - 1 >> 1; k >= 0; k--)
if (eggFloorArray[i - 1][k] == eggFloorArray[i][j - k - 1]
|| (eggFloorArray[i - 1][k] == eggFloorArray[i][j - k - 2]
&& eggFloorArray[i - 1][k + 1] == eggFloorArray[i][j - k - 1])) {
eggFloorArray[i][j] = 1 + Math.max(eggFloorArray[i - 1][k], eggFloorArray[i][j - k - 1]);
break;
}
return eggFloorArray;
}
}
对测试数据 e = 5 , f = 2500 e=5,f=2500 e=5,f=2500的运行结果如下