一個關於兌換零錢的豆瓣筆試題


  前幾天做了個豆瓣筆試題,時間是90分鍾,共有6題,要做4道,難度如果沒看過類似的着實做起來太慢了。由於豆瓣上面的郵件說不要泄露(還有人會在后期筆試),所以拖到現在才寫博客。我先把題目貼出來:將10000塊錢兌換成由5000塊、2000塊、1000塊、500塊、100塊、50塊、10塊、5塊、1塊的組成的零錢,問有多少種兌換方式?

  這個題,如果朋友們沒做過,或許最開始就跟我一樣,錢有9種,就做個9重循環,各層累加,當總錢等於10000時就計數器加1,這樣是很簡單沒錯,可惜在這數據量的前提下,我用c跑了5、6分鍾也沒跑出來,於是中止了這種天真的想法。但是還是有必要提下完成這種笨方法時需要用到的基礎數據結構,后面我們會對它們進行改進:

  一個用於記錄各種錢(金額)的數據:y[9];

  一個用於統計各種錢(數量)的數組:x[9],初始為0;

  一個用於標示各種錢(數量上限)的數組:z[9];

  一個用於統計從父循環[i]進入子循環[i+1]時(總金額)的數組:sum[9],i= 0 to 8;這里我需要重點說明,這個數組是必然需要的,一定得保存上層循環進入下層循環時的初始總金額數,否則當下層循環[i+1]執行完畢返回[i]時就無法知道[i]循環內正確的總金額初值了(除非你把[ 0 ~ i-1 ]層的都算一次加起來)。

  接下來,我講下想到的幾個優先考慮的改進:

  1、最外層循環應該是最大的數,也就是5000塊,內層的依次遞減。

  這種方式可以使步進更大,而且與后面的第3點配合是非常優秀的省時方法,於是令y[] = {5000,2000,1000,500,100,50,10,5,1},z[] = {3,6,11,21,101,201,1001,2001,10001};

  2、各層循環應該增加臨界條件,當發現總數大於或等於時應該返回上層循環。

  如果只是按照z[]中的極限臨界條件來做循環,明顯會有非常多的無用功,所以應該在各層循環內部,判斷總錢數是否已經大於、等於10000,如果大於就break到上層循環,如果等於就先計數器加1,再返回上層循環。

  3、最底層循環不執行(1塊錢)

  前面i = 0 to 7層循環分別把5000、2000、1000、500、100、50、10、5塊的都算過了,等到了i=8層,也就是算1塊錢的數量時就可以直接跳過了,因為反正會有一個數量能滿足累加和正好等於10000,所以前面實際上的x[]、y[]、z[]的大小都只要8就行了。這一點可以把循環次數上限最大的(10000次)的那重循環給省略掉,能大大加快計算時間,這時循環由9重循環減到8重。

  經過上面3點改進,心想這下應該快了吧,哪知道也花了兩分多鍾才出結果,仔細思考,覺得最大的問題還是在第2點,臨界條件的判斷上,使用2中的方法,會導致各層循環都有判斷操作(大於、等於、小於),會影響執行效率,這是需要改善的問題,目前來說,第2點的實現方式有三種:

 1 // 方式1
 2 ...
 3     for (x[4]=0;x[4]<z[4];++x[4])
 4     {
 5         sum[4]=sum[3]+x[4]*y[4];
 6         if (sum[4]<10000) ;
 7         else if (sum[4]==10000) { ++count; break; }
 8         else break;
 9         for (x[5]=0;x[5]<z[5];++x[5])
10         {
11             ...
12         }
13     }
14 ...
15 
16 // 方式2
17 ...
18     for (x[4]=0;x[4]<z[4];++x[4])
19     {
20         sum[4]=sum[3]+x[4]*y[4];
21         if (sum[4]>10000) break;
22         for (x[5]=0;x[5]<z[5];++x[5])
23         {
24             ...
25         }
26     }
27 ...
28 
29 // 方式3
30 ...
31     for (x[4]=0;x[4]<z[4];++x[4])
32     {
33         sum[4]=sum[3]+x[4]*y[4];
34         if (sum[4]==10000) { ++count; break; }        
35         else if(sum[6]>10000) break;
36         for (x[5]=0;x[5]<z[5];++x[5])
37         {
38             ...
39         }
40     }
41 ...
42 
43 // i=7層代碼
44 ...
45     for (x[7]=0;x[7]<z[7];++x[7])
46     {
47         sum[7]=sum[6]+x[7]*y[7];
48         if (sum[7]>10000) break;
49         ++count;
50     }
51 ...

  這三種都是屬於換湯不換葯的,它們在最后一層循環執行的代碼是一模一樣的,在i=0 to 6層執行的判斷稍微有點區別,可以從下圖看出執行的效率區別:

  它們的結果是一致的,但是雖然方式1代碼量最多,但是速度卻還是快些的(畢竟sum<10000的可能性最大了),不過在這么大的基數前面,這也是浮雲了,都在2分半鍾的時候才計算完。為了更好的優化第2點,提出下面的第4點改進:

  4、改變循環臨界條件,通過當前循環來計算下層循環的結束值

  前面由於是用z[]來保存各層循環的執行上限,然后再在循環內判斷總值是否超過,影響了執行效率,所以考慮:在[i]層循環時,根據當前累積初值sum[i]和下層循環的零錢面值y[i+1]來計算下層循環[i+1]的結束條件z[i+1],這樣就不需要在循環內部不停地進行判斷了,改進方式為:

 1 // 方式4
 2 ...
 3     for (x[4]=0;x[4]<z[4];++x[4])
 4     {
 5          sum[4]=sum[3]+x[4]*y[4];
 6          z[5] = (10000-sum[4])/y[5]+1;
 7          for (x[5]=0;x[5]<z[5];++x[5])
 8          {
 9               ...
10          } 
11     }
12 ...

  一執行,恩,果斷快了不少,只花了一分鍾多一點便得到了結果:

  至此,我能想到的改進就沒有了,因為沒看過類似的題,花了不少時間,如果有朋友有更好的方法,歡迎指教討論(ps:豆瓣招的Python程序員,后來試用Python解這個題目的效率真心比c慢十幾倍,反正大概十分鍾過去了還沒算完,我就手動關了,有耐心的朋友可以去試試,告訴我到底要多久)。

  補充:

  經過園友的貼的代碼,我突然發現我在最后一重循環中做的無用功:

1 ...    
2     for (x[7]=0;x[7]<z[7];++x[7])
3     {
4         ++count;
5     }
6 ...

  實在太2了,這++count不就是count+=z[7]么,於是我改成這樣:

1 ...
2     for (x[6]=0;x[6]<z[6];++x[6])
3     { 
4        sum[6]=sum[5]+x[6]*y[6];
5        z[7] = (10000-sum[6])/y[7]+1;
6        count+=z[7];
7     }
8 ...

  運行一下:

  這才是人生啊,900ms+,看來一個不小心,一個無謂的循環足足多花了一分半鍾!

  最后附上改過的C源代碼

零錢兌換
 1 #include <stdio.h>
 2 #include <time.h>
 3 
 4 int main()
 5 {
 6     clock_t start,end;
 7     unsigned long long count = 0;
 8     int x[8],sum[8],z[8];
 9     int y[8]={5000,2000,1000,500,100,50,10,5};
10     start = clock();
11      for (x[0]=0,z[0]=3;x[0]<z[0];++x[0])
12      {
13          sum[0]=x[0]*y[0];
14          z[1] = (10000-sum[0])/y[1]+1;
15          for (x[1]=0;x[1]<z[1];++x[1])
16          {
17              sum[1]=sum[0]+x[1]*y[1];
18              z[2] = (10000-sum[1])/y[2]+1;
19              for (x[2]=0;x[2]<z[2];++x[2])
20              {
21                  sum[2]=sum[1]+x[2]*y[2];
22                  z[3] = (10000-sum[2])/y[3]+1;
23                  for (x[3]=0;x[3]<z[3];++x[3])
24                  {
25                      sum[3]=sum[2]+x[3]*y[3];
26                      z[4] = (10000-sum[3])/y[4]+1;
27                      for (x[4]=0;x[4]<z[4];++x[4])
28                      {
29                          sum[4]=sum[3]+x[4]*y[4];
30                          z[5] = (10000-sum[4])/y[5]+1;
31                          for (x[5]=0;x[5]<z[5];++x[5])
32                          {
33                              sum[5]=sum[4]+x[5]*y[5];
34                              z[6] = (10000-sum[5])/y[6]+1;
35                              for (x[6]=0;x[6]<z[6];++x[6])
36                              {
37                                  sum[6]=sum[5]+x[6]*y[6];
38                                  z[7] = (10000-sum[6])/y[7]+1;
39                                  count+=z[7];
40                              }
41                          }
42                      }
43                  }
44              }
45          }
46       }
47     end = clock();
48     printf("Count=%lld,Time=%ldms",count,end-start);
49     getchar();
50 }

  轉載請注明原址:http://www.cnblogs.com/lekko/archive/2013/04/05/3000403.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM