动态规划——扔鸡蛋问题的递归算法与非递归算法
基础版
有一幢高100层的楼,鸡蛋从xxx层投下时刚好会碎。现持有2个完全相同的鸡蛋,试设计一个最优方法来找出xxx,使以此方法投下鸡蛋时,最坏情况下所投掷的总次数N(2,100)N(2,100)N(2,100)最少。
进阶版
有一幢高fff层的楼,鸡蛋从xxx层投下时刚好会碎。现持有eee个完全相同的鸡蛋,试设计一个最优方法来找出xxx,使以此方法投下鸡蛋时,最坏情况下所投掷的总次数N(e,f)N(e,f)N(e,f)最少。
记eee为鸡蛋数,fff为楼层高度,N(e,f)N(e,f)N(e,f)为所求最优算法的投掷总次数,φ\varphiφ为某一算法,ψ\psiψ为全量算法集合,xxx为鸡蛋破碎楼层,ne,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φ∈ψmax0≤x≤fne,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层投掷(记为T1=50T_1=50T1=50)后,若鸡蛋破碎,即0<x≤500<x\leq 500<x≤50时,第二次必须从第1层开始投掷,否则若从第2层开始投掷且鸡蛋破碎,则无法确定x=1x=1x=1还是x=2x=2x=2。若首次在第50层投掷,最坏情况即x=50x=50x=50,此时投掷次数N=50N=50N=50,这显然并非最优方法。
但是二分法仍然可以为我们带来启示。根据上述推断,我们可以得出两条规律:
- N(e,f)≥⌊log2f⌋where e≥⌊log2f⌋N(e,f)\geq \lfloor log_2 f\rfloor\quad{\rm where}\ e\geq \lfloor log_2 f\rfloorN(e,f)≥⌊log2f⌋where e≥⌊log2f⌋
- N(1,f)=fN(1,f)=fN(1,f)=f
第一条为二分法所规定的投掷次数下限,即鸡蛋数量充足时至少要投掷的次数,不过事实上这一条的参考意义有限。
第二条为仅有一个鸡蛋时的投掷次数,这一条则是重要的边界条件之一。
依然由T1=50T_1=50T1=50,我们考虑T1=49T_1=49T1=49,最坏情况仍是x=49x=49x=49,此时的有N=49<50N=49<50N=49<50。可以发现T1T_1T1减少时,NNN同步在减少。那是否T1T_1T1越小越好呢?并非如此。
我们在T1T_1T1从50减少到49的过程中,实际默认了“对x>T1x>T_1x>T1的情况下,使用2个鸡蛋总能在N−1N-1N−1次内找出xxx”。随着T1T_1T1越来越小,这一条件的实现越来越困难。但好在对于x≤T1x\leq T_1x≤T1的情况,我们容易确认最坏情况下需要N=T1N=T_1N=T1次来找出xxx。那么当首次投掷鸡蛋不破碎时,我们问题变更为在T1+1T_1+1T1+1至100间找出T2T_2T2。从而T1+1T_1+1T1+1至100间至多投掷N−1N-1N−1次。由T1=NT_1=NT1=N,容易得出T2=N−1T_2=N-1T2=N−1。从而有
T1+T2+…+TN=N+N−1+…+1=N(N−1)/2≥100T_1+T_2+…+T_N=N+N-1+…+1=N(N-1)/2\geq 100T1+T2+…+TN=N+N−1+…+1=N(N−1)/2≥100
从而N≥14N\geq 14N≥14,即最优时N=14N=14N=14。此时的投掷方法为:
- 按14,27,39,50,60,69,77,84,90,95,99,100(103)的次序投掷,任一次破碎时,从上一个未破碎节点开始逐个投掷。
\frac{}{}
当问题升级为进阶版时,就不能简单地用T1=NT_1=NT1=N来判断了,但我们仍然可以使用基础版的思想。
以下事实显然:
- N(1,f)=fN(1,f)=fN(1,f)=f
- N(e,0)=0N(e,0)=0N(e,0)=0
对任意e,fe,fe,f与TTT,N(e,f)TN(e,f)_TN(e,f)T的取值分两种情况,①TTT投掷使鸡蛋破碎,则后续所需投掷次数为N(e−1,T)N(e-1,T)N(e−1,T);②TTT投掷未使鸡蛋破碎,则后续所需投掷次数为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)=fN(1,f)=fN(1,f)=f
- N(e,Z\N∗)=0N(e,Z\backslash N^*)=0N(e,Z\N∗)=0
- N(e,f)=1+min0≤i≤f−1max(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\rfloori≤⌊(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+min0≤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\rfloori≤⌊(f−1)/2⌋的基础下,考察满足以下两种情况的iii:
- 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),总存在iii符合两种情况其一。
又有
{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\rfloori≤⌊(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)=fN(1,f)=fN(1,f)=f
- N(e,Z\N∗)=0N(e,Z\backslash N^*)=0N(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\rfloori≤⌊(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=2500e=5,f=2500e=5,f=2500的运行结果如下