《吃豆子过桥问题》——经典智力题、面试题
吃豆子过桥问题
本题来自于百度校招面试题,通过一个简单的智力问题理解递归问题的解法。
一:问题描述
一个人要过一座80米的桥,每走一米需要吃一颗豆子,他最多可以装60颗豆子,问最少需要吃多少颗豆子才能走完桥?
二:初步分析
1.一趟(不折回)最多只能走60米豆子就会被吃完;
2.如果有折回,必须保证能够返回到有豆子的地点,且在折回点放下的豆子尽量多;
3.尽可能少的折回(次数和折回的距离都要少,毕竟一趟折回就要多消耗一个来回的豆子);
4.折回点的豆子数量要求至少能够走完剩余的部分;
三:具体分析
1.由条件1得一趟走不完,由条件3我们可以考虑折回尽量少的次数能否走完。
先考虑只折回一次,那么此时就要求得折回点的位置。
2.由条件3得折回的距离尽量短,那么可以考虑最短的折回距离,很明显应该是20米处;设折回点距离起始位置x米,则此时x=20;那么在x处,需要至少有60颗豆子,一次性拿60颗豆子走完最后一程。
那么问题就来了,怎么在x处囤积60颗豆子呢,假使他第一次信心满满的拿着60颗豆子,走到x处就只剩下40颗了,现在不够走完剩余的全程啊,只能放下一部分折回去再取了,那放多少呢,很明显由条件2得我们需要放下20颗,拿着20颗刚好可以折回到起点,继续拿着60颗豆子第二次来到x处,此时手上还有40颗,再捡起第一次放的20颗,共60颗豆子刚好可以走完全程。
在这种情况下,我们易知,他共走了(20)*3+(60)= 120米,故吃掉120颗豆子。
四:问题拓展
如果桥长81米呢?
此时我们如果还是只折回一次的话,在21米处还是得有60颗豆子,也就是需要搬运60颗豆子到21米处;分析得不可能只折回一次就搬运60颗豆子到21米处,因此需要再设置一个折返点y,此时x=21,0<y<x;
刚才考虑了80米的情况,那么我们可以假设现在在80米的基础上加了1米,因此可以设y=1,即先折回两趟到1米处,这样y处就有60*3-5*(1)=175颗豆子,再从y到x折返1趟,x处就有60*2-3*(20)=60颗豆子,最后从x出发拿着60颗豆子就可以愉快的走过桥了。
因此他共走过5*(1)+3*(20)+(60) = 125米,故吃掉125颗豆子。
五:再次拓展
如果桥长n米,最多装m颗豆子,最少消耗f(n,m)与n和m的关系是什么呢?
此时问题突然就变得很复杂了,别急,我们分析一下刚才的思路,桥长从80米到81米,就是要在1米处放足够多豆子(当然只要不小于桥长80米时的消耗就行),那么这些豆子又需要从起点处运到1米处,那么在这1米内又需要消耗多少豆子呢?
我们可以考虑先把可装的最大豆子数m固定(假设还是60),当桥长0<n<=60米时,f(n,m) = n;
当n=61时,需要在1米处囤积f(n-1,6)即f(60,60)=60颗豆子,囤积过程中需要往返一次,第一次到达1米处留下58颗豆子赶紧回去再取60颗到达1米处还剩59颗,现在有117颗足够走完最后60米。消耗豆子1*2+(1)+60 = 63颗豆子;
当n=62时,需要在1米处囤积f(n-1,m)即f(61,60)=63颗豆子,囤积过程中需要往返一次,第一次到达1米处留下58颗豆子赶紧回去再取60颗到达1米处还剩59颗,现在有117颗足够走完最后61米。消耗豆子1*2+(1)+63 = 66颗豆子;
以此类推...
那么问题来了,细心的你肯定发现了上面那种情况都是往返一次的,但是不是每次都只往返一次,比如当n=81时,该怎么确定往返的次数呢?
分析上面的递推关系我们可以知道,每次往返的趟数与f(n-1,m)的大小有关。分析往返过程易知:最后一次到达1米处剩59颗豆子,之前每次到达1米处可以放下58颗豆子,假设之前到达了1米处T次,则有:
59 + 58 * T >= f(n-1,60) ==> T >= [f(n-1,60)-59]/58
由于T必须是整数,故T = ceil((f(n-1,60)-59) / 58),ceil(x)表示对x向上取整,即不小于x的最小整数
另一种表示方式为T = floor( (f(n-1,60)-2) / 58 ),这种写法是为了方便编程实现,整型数的除法会自动向下取整
之前往返T次消耗掉2T颗豆子,最后一次到达1米处消耗1颗豆子,故总消耗:f(n-1,60) + 2T + 1颗豆子
这时候我们考虑将固定为60的m扩展为任意m,则有:f(n,m) = f(n-1,m) + ceil( f(n-1,m)-(m-1)) / (m-2) ) * 2+ 1 = f(n-1,m) + (f(n-1,m)-2) / (m-2) * 2+ 1 ;
好了到这里,这个问题也彻底解决完了,现在就让我们用简短的代码来实现这个过程吧!
六:编程实现
1 #include<cmath> 2 #include<iostream> 3 using namespace std; 4 typedef unsigned long long int64; 5 6 //参数说明:length为桥的长度,maxNum为最大可带的豆子数 7 //递归实现 8 int64 getMinConsume(int length, int maxNum) { 9 if(length <= maxNum) 10 return length; 11 int64 get_n_1 = getMinConsume(length-1,maxNum); //上一次的豆子消耗 12 return get_n_1 + (get_n_1-2)/(maxNum-2) * 2 + 1; 13 } 14 //第二种公式实现——递归 15 int64 getMinConsume2(int length, int maxNum) { 16 if(length <= maxNum) 17 return length; 18 int64 get_n_1 = getMinConsume2(length-1,maxNum); //上一次的豆子消耗 19 return get_n_1 + ceil((double)(get_n_1-maxNum+1)/(maxNum-2)) * 2 + 1; 20 } 21 //非递归的实现方式 22 int64 getMinConsume_NoRecursive(int length, int maxNum) { 23 if(length <= maxNum) 24 return length; 25 int64 result = maxNum, i; 26 for(i=maxNum; i<length; i++) 27 result += (result-2)/(maxNum-2) * 2 + 1; 28 return result; 29 } 30 31 int main() 32 { 33 int maxNum=60, length; 34 for(length=50; length<201; length++) 35 cout<<length<<"米至少消耗"<<getMinConsume2(length,maxNum)<<"颗豆子!"<<endl; 36 return 0; 37 } 38 39 int main2() 40 { 41 while(1){ 42 cout<<"请输入最大可带的豆子数量和桥的长度: "; 43 int maxNum, length; 44 if(!(cin>>maxNum>>length)) 45 break; 46 cout<<"至少消耗"<<getMinConsume_NoRecursive(length,maxNum)<<"颗豆子!"<<endl; 47 } 48 return 0; 49 }
最后,感谢百度三面面试官耐心的引导我思考这道题,并不断加大难度把这道题理解透彻,还监督我完成代码的实现,在此谢谢温和友善的刘面试官~
更多关于百度校招面试经历请参见:百度校招面试经历及总结(全部通过等offer中)