1. 問題闡述:現在需要將若干圓錢兌換成1圓,2圓,5圓的零錢,問有多少種不同的兌換方法,以及具體是哪些.
2. 問題分析:假如我們最後兌換到的1圓有one張,2圓的有two張,5圓的five張,那麼我們兌換到的零錢總價值為at = one + 2 * two + 5 * five;所以我們只要判斷最後我們得到的零錢總數是否和原本擁有的錢money是否相等即可;如果相等則是一種正解,如果不等說明這種解不可取.
3. 解決問題:由於本文是基於蠻力法(枚舉)來解決問題的,所以下面的三種方法都是蠻力法及其優化的版本
3.1. 暴力美學: 試想一下小時候偷偷玩爸媽的手機但是不知道鎖屏密碼我們會怎麼辦?第一反應當然就是不管對不對先嘗試一下啦,嘴不對?對於這個問題我們同樣可以採取相同的思路,不管對不對,我就一直嘗試唄.假如我們最後兌換到的1圓有one張,2圓的有two張,5圓的five張,那麼我們兌換到的零錢總價值為at = one + 2 * two + 5 * five,當at=mongey的時候我們便能得到其中的一種解.我們只需要遍歷所有的情況那麼便能得到所有的正解.OK,我自己都覺得這個沒有必要講得這麼麻煩,所以你們可以直接看代碼.
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 int Enum_1(int money) 2 { 3 int max_one = money; // 最大的一圓硬幣數 4 int max_two = money / 2; // 最大的兩圓硬幣數 5 int max_five = money / 5; // 最大的五圓硬幣數 6 int count = 0; // 可行的分配方法數 7 int at; // 當前方案的錢數量 8 int i = 0; 9 for (int one = 0; one <= max_one; one++) 10 { 11 for (int two = 0; two <= max_two; two++) 12 { 13 for (int five = 0; five <= max_five; five++) 14 { 15 at = one + two * 2 + five * 5; 16 if (money == at) 17 { 18 count++; 19 cout << "方法" << count << ": " 20 << one << " * 1 + " << two << " * 2 + " << five << " * 5 ;" << endl; 21 } 22 i++; 23 } 24 } 25 } 26 cout << "循環次數為:" << i << endl; 27 return count; 28 }
3.2. 1圓不配擁有循環: 觀察上面的執行結果我們發現一共只有10中方法(當傳入參數為10),但是循環的次數卻是198次,足足浪費了188次,這些多餘的循環雖然沒有用,但是依舊佔用了CPU的資源.所以這個方法值得優化的地方還有很多.
現在我們依舊設想一個場景:你出去買水,給了對方10圓,買水用了1圓,對方給你找錢的時候先給了你1張5圓,然後又給了你1張2圓,現在你猜他還會給你多少張1圓?2張,這完全就是直接給出的,你沒有沒從0個1圓,1個1圓,2個1圓一個一個開始數吧?這是當然的,因為1是所有整數的因數,所以不管還剩下多少都能用1來"湊數",既然人以這樣直接得出需要的1圓錢的數量那麼CPU是否可以呢?那是當然的,因為需要的1圓的數量永遠為:money - 2 * two + 5 * five.由此我們得出如下的第一個優化版本.
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 int Enum_2(int money) 2 { 3 int max_two = money / 2; // 最大的兩圓硬幣數 4 int max_five = money / 5; // 最大的五圓硬幣數 5 int count = 0; // 可行的分配方法數 6 int at; // 當前方案的錢數量 7 int one; // 需要的一圓硬幣數量 8 int i=0; 9 for (int two = 0; two <= max_two; two++) 10 { 11 for (int five = 0; five <= max_five; five++) 12 { 13 at = two * 2 + five * 5; 14 if (money >= at) // 此處必須要判斷,不然1圓錢會出現負數 15 { 16 count++; 17 one = money - at; 18 cout << "方法" << count << ": " 19 << one << " * 1 + " << two << " * 2 + " << five << " * 5 ;" << endl; 20 } 21 i++; 22 } 23 } 24 cout << "循環次數為:" << i << endl; 25 return count; 26 }
3.3 我們仔細觀察第二個方法的運行結果,雖然已經大大的優化了第一種方案,但是依舊有不少的多餘的循環.這也就很自然的引出了這樣的第三種方法.想辦法避免依舊多餘的循環.
首先我們思考一下這些多餘的循環是怎麼產生的!就這麼直接思考似乎看不出什麼原因,那我們反過來思考一下:有多餘的循環意味著什麼呢?意味著在最內層循環的if判斷不成立,那為什麼不會不成立呢?這個問題就簡單多了,因為2圓和5圓的零錢加起來超過了需要兌換的錢幣總數(如果沒有超過的話直接用1圓錢補齊就可以了),這也就導致了需要的1圓錢的數量為負數,這也就以為這你需要倒貼一些1圓錢,雖然這在日常生活中是很常見的事情,但是對這個問題而言卻不是我們需要的答案.
那為什麼會超過呢?這個問題並不難,因為我們在限定每種錢幣的最高數額的時候是按照單獨兌換當前錢幣計算的(也就是說假設用10圓錢去兌換,最多可以有2個5圓或5個2圓),可是當已經兌換了一定數量的其中一種錢幣之後,另外的一種錢幣就不可能達到單獨兌換這種錢幣的最高數額(10圓兌換了1個5圓后不可能再兌換5個2圓),但是我們依舊把這一部分考慮了進去,這也就是多餘的循環產生的原因,所以我們只需要把這一部分給它消滅便又能得到優化.
既然我們已經知道了多餘的循環是怎麼產生的,只需要稍加思索便可以發現只要把內層循環的循環次數定義成固定的一定會出現多餘的循環或者漏解.所以我們必需要"動態"的改變循環上限.所以在內層的循環開始之前重置一下內層循環的循環上限即可.代碼如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 int Enum_3(int money) 2 { 3 int max_two; // 最大的兩圓硬幣數 4 int max_five = money / 5; // 最大的五圓硬幣數 5 int count = 0; // 可行的分配方法數 6 int at; // 當前方案的錢數量 7 int one; // 需要的一圓硬幣數量 8 int i=0; 9 for (int five = 0; five <= max_five; five++) 10 { 11 max_two = (money - 5 * five) / 2; 12 for (int two = 0; two <= max_two; two++) 13 { 14 at = two * 2 + five * 5; // 每次循環開始之前重置2圓硬幣的最大數量 15 if (money >= at) 16 { 17 count++; 18 one = money - at; 19 cout << "方法" << count << ": " 20 << one << " * 1 + " << two << " * 2 + " << five << " * 5 ;" << endl; 21 } 22 i++; 23 // 我不知道這一行有什麼作用,但是去掉的話會有非常嚴重的後果 24 } 25 } 26 cout << "循環次數為:" << i << endl; 27 return count; 28 }
4. 總結:
4.1 我們不難發現第2種方法與第三種方法其實只有一行代碼的差別,在保證正確的情況下沒有誤解,沒有漏解,沒有多餘的循環,這是目前為止我認為的最優解(如果有更好的解法歡迎留言,望不吝賜教).同樣的這只是一個例子,其實在其他的很多代碼中相同的,有時候只需要一小點的改變便能大大的提高代碼的執行效率
4.2 代碼優化並不是一蹴而就的,更多時候需要一步一步的來,能優化一點先是一點,只需要在上一步的基礎上有所進步,總會達到最後需要的理想情況.
4.3 在方法2中我們省略了1圓的循環,實際上我們也是可以省略5圓或2圓的循環的,進一步說,即使不是1圓哪怕是3圓也是可以省略的(又意願的可以自己下來嘗試一下),因為需要區別兩個人並不需要兩個名字.
4.3 雖然可以省略1圓,2圓,5圓中任何一個循環,但是省略1圓的循環並不僅僅是因為這樣比較簡單,還是因為這樣在相較于省略2圓或5圓的循環一定會擁有更少的循環(請自己思考原因).