動態規劃(二) 硬幣問題
阿新 • • 發佈:2019-02-07
2.2硬幣問題
2.2.1 問題簡述:有n種硬幣,每種都有無限多,要求最少或最多選用幾個硬幣,能湊出給定整數。
2.2.2 問題分析:與巢狀矩形不同的是,硬幣問題是一個確定終點的最長路和最短路問題。要注意,該問題可能無解。
2.2.3 關鍵程式碼
int dp(int S){ if(vis[S]) return d[S]; vis[S] = 1; int &ans = d[S]; ans = -(1<<30); for(int i = 1; i <= n; i++) if(S > V[i]) ans = max(ans, dp(S - V[i]) + 1); return ans; }
注意與巢狀矩形的對比,硬幣問題有幾個特殊的情況。首先,路徑長度有可能為0,所以,不能再用0表示這個d值還沒有算過;其次,結點S不一定能夠到達結點0,所以,需要用特殊的d[S]來表示無法到達。在這個演算法中,我們用另外一個數組vis[i]表示狀態i是否被訪問過,vis[i]應被初始化0。並且,我們把ans初始化為一個很小的整數,代表“無法到達”的數值。
2.2.4 完整程式碼
#include<iostream> using namespace std; //這裡只列舉最少需要的錢幣數,類推即可 int V[20]; int n, S; int d[200]={0},vis[200] = {0}; int min(int x, int y) { return x < y ? x : y; } int dp(int S) { if(S == 0) return 0; if(vis[S]) return d[S]; int &ans = d[S]; ans = (1<<30); for(int i = 0; i < n; i++) if(S >= V[i]) ans = min(ans, dp(S - V[i]) + 1); return ans; } void print_ans(int *d, int S){ for(int i = 0; i < n; i++) if(S >= V[i] && d[S] == d[S - V[i]] + 1){ cout << V[i] << ' '; print_ans(d, S - V[i]); break; } } int main() { cout << "輸入硬幣種數:"; cin >> n; cout << "依次輸入其面值:" << endl; for(int i = 0; i < n; i++) cin >> V[i]; cout << "輸入要湊的總面值:"; cin >> S; cout << "最少需要的硬幣數為:" << dp(S) << endl << "它們分別是:"; print_ans(d, S); return 0; }
附1:我們也可以不用遞迴,直接遞推即可(如果既要求最大值,也要求最小值,此方法適用)
for(int i = 0; i < S; i++)
for(int j = 0; j < n; j++)
if(i >= V[j]){
minv[i] = min(minv[i], minv[i - V[j]] + 1);
maxv[i] = max(maxv[i], maxv[i - V[j]] + 1);
}
附2:有一種“空間換時間”的列印路徑方法,我們需要將遞推稍微改寫一下。
for(int i = 0; i <= S; i++) for(int j = 0; j <= n; j++) if(i >= V[j]){ if(i >= V[j]){ if(min[i] > min(i - V[j] + 1)){ min[i] = min[i - V[j]] + 1; min_coin[i] = j; } } } //這裡只需呼叫print_ans(min_coin, S)即可 void print_ans(int *d, int S){ while(S){ cout << d[S] << ' '; S -= V[d[S]]; } }
2.2.5 總結
DAG最長路和最短路都可以用記憶化搜尋和遞推兩種方式實現。列印時既可以根據d值重新計算出每一步的最優決策,也可以在動態規劃時“順便”記錄下每一步的最優決策。
這裡有兩種“對稱”的狀態定義方程:
狀態1:設d(i)為從i出發的最長路,則d(i) = max{d(j) + 1 | (i, j)∈E}。
狀態2:設d(i)為以i結束的最長路,則d(i) = max{d(j) + 1 | (i, j)∈E}。
實際上,硬幣問題如果使用狀態2,就和巢狀矩形問題一樣了,但這裡列舉的是狀態1的解答過程,起展示作用。