吃豆子過橋問題
本題來自於百度校招面試題,通過一個簡單的智力問題理解遞歸問題的解法。
一:問題描述
一個人要過一座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中)