問題描述
假設有 1 元,3 元,5 元的硬幣若干(無限),現在需要湊出 11 元,問如何組合才能使硬幣的數量最少?
問題分析
乍看之下,我們簡單的運用一下心算就能解出需要 2 個 5 元和 1 個 1 元的解。當然這里只是列出了這個問題比較簡單的情況。當硬幣的幣制或者種類變化,並且需要湊出的總價值變大時,就很難靠簡單的計算得出結論了。貪心算法可以在一定的程度上得出較優解,但不是每次都能得出最優解。
這里運用動態規划的思路解決該問題。按照一般思路,我們先從最基本的情況來一步一步地推導。
我們先假設一個函數 d(i) 來表示需要湊出 i 的總價值需要的最少硬幣數量。
- 當 i = 0 時,很顯然我們可以知道 d(0) = 0。因為不要湊錢了嘛,當然也不需要任何硬幣了。注意這是很重要的一步,其后所有的結果都從這一步延伸開來。
- 當 i = 1 時,因為我們有 1 元的硬幣,所以直接在第 1 步的基礎上,加上 1 個 1 元硬幣,得出 d(1) = 1。
- 當 i = 2 時,因為我們並沒有 2 元的硬幣,所以只能拿 1 元的硬幣來湊。在第 2 步的基礎上,加上 1 個 1 元硬幣,得出 d(2) = 2。
- 當 i = 3 時,我們可以在第 3 步的基礎上加上 1 個 1 元硬幣,得到 3 這個結果。但其實我們有 3 元硬幣,所以這一步的最優結果不是建立在第 3 步的結果上得來的,而是應該建立在第 1 步上,加上 1 個 3 元硬幣,得到 d(3) = 1。
- ...
接着就不再舉例了,我們來分析一下。可以看出,除了第 1 步這個看似基本的公理外,其他往后的結果都是建立在它之前得到的某一步的最優解上,加上 1 個硬幣得到。得出:
d(i) = d(j) + 1
這里 j < i。通俗地講,我們需要湊出 i 元,就在湊出 j 的結果上再加上某一個硬幣就行了。
那這里我們加上的是哪個硬幣呢。嗯,其實很簡單,把每個硬幣試一下就行了:
- 假設最后加上的是 1 元硬幣,那 d(i) = d(j) + 1 = d(i - 1) + 1。
- 假設最后加上的是 3 元硬幣,那 d(i) = d(j) + 1 = d(i - 3) + 1。
- 假設最后加上的是 5 元硬幣,那 d(i) = d(j) + 1 = d(i - 5) + 1。
我們分別計算出 d(i - 1) + 1,d(i - 3) + 1,d(i - 5) + 1 的值,取其中的最小值,即為最優解,也就是 d(i)。
最后公式:
代碼示例
這里用 Java 實現了基本的代碼:
public class CoinProblemBasicTest {
private int[] d; // 儲存結果
private int[] coins = {1, 3, 5}; // 硬幣種類
private void d_func(int i, int num) {
if (i == 0) {
d[i] = 0;
d_func(i + 1, num);
}
else {
int min = 9999999; // 初始化一個很大的數值。當最后如果得出的結果是這個數時,說明湊不出來。
for (int coin : coins) {
if (i >= coin && d[i - coin] + 1 < min) {
min = d[i - coin] + 1;
}
}
d[i] = min;
if (i < num) {
d_func(i + 1, num);
}
}
}
@Test
public void test() throws Exception {
int sum = 11; // 需要湊 11 元
d = new int[sum + 1]; // 初始化數組
d_func(0, sum); // 計算需要湊出 0 ~ sum 元需要的硬幣數量
for (int i = 0; i <= sum; i++) {
System.out.println("湊齊 " + i + " 元需要 " + d[i] + " 個硬幣");
}
}
}
結果如下:
湊齊 0 元需要 0 個硬幣
湊齊 1 元需要 1 個硬幣
湊齊 2 元需要 2 個硬幣
湊齊 3 元需要 1 個硬幣
湊齊 4 元需要 2 個硬幣
湊齊 5 元需要 1 個硬幣
湊齊 6 元需要 2 個硬幣
湊齊 7 元需要 3 個硬幣
湊齊 8 元需要 2 個硬幣
湊齊 9 元需要 3 個硬幣
湊齊 10 元需要 2 個硬幣
湊齊 11 元需要 3 個硬幣