1. 程式人生 > >詳解_動態規劃DAG_硬幣找零問題(完全揹包)

詳解_動態規劃DAG_硬幣找零問題(完全揹包)

寫了好多結果一下卡住都沒了。。。(csdn怕是把大部分伺服器資源用在了廣告投放上吧)

參考數目:演算法競賽入門經典(第二版)

NYOJ 995:

描述

在現實生活中,我們經常遇到硬幣找零的問題,例如,在發工資時,財務人員就需要計算最少的找零硬幣數,以便他們能從銀行拿回最少的硬幣數,並保證能用這些硬幣發工資。

我們應該注意到,人民幣的硬幣系統是 100,50,20,10,5,2,1,0.5,0.2,0.1,0.05,

0.02,0.01 元,採用這些硬幣我們可以對任何一個工資數用貪心演算法求出其最少硬幣數。 

但不幸的是: 我們可能沒有這樣一種好的硬幣系統, 因此用貪心演算法不能求出最少的硬幣數,甚至有些金錢總數還不能用這些硬幣找零。例如,如果硬幣系統是 40,30,25 元,那麼 37元就不能用這些硬幣找零;95 元的最少找零硬幣數是 3。又如,硬幣系統是 10,7,5,1元,那麼 12 元用貪心法得到的硬幣數為 3,而最少硬幣數是 2。 

你的任務就是:對於任意的硬幣系統和一個金錢數,請你程式設計求出最少的找零硬幣數;
輸入:

4 12
10 7 5 1

輸出

2

分析 :由描述可知貪心並不一定可以獲得最優解,但是一定是近似最優解的。將一個硬幣面額看作是當前需要湊出的面額(保證可以湊出),則本問題就是一個固定起點終點的DAG最短路徑問題,起點為待湊面額value,終點為0,每使用一枚硬幣 i ,將狀態轉移到 value-v[i](value表示待湊面額,v陣列用於儲存每個硬幣的面額),使用硬幣就是就是在縮小問題規模的過程,也就是當前狀態沿(i,j)的轉移過程,所以可以得到狀態轉移方程:dis[i] = min(dis[i],dis[value-v[i]]+1)

程式碼:

1.記憶化搜尋,程式碼更為清晰,更容易理解。(本質就是縮小問題規模,直到縮小到狀態為0,而此時我們已經將狀態為0時需要的硬幣數算出,即:dis[0] = 0.O(n2))

#include<bits/stdc++.h>

#define min(a,b) a<b?a:b
#define infinite 0x3f3f3f
#define MAX 500
using namespace std;

int n,value;
int dis[MAX];
int v[MAX];

int dp(int s)
{
    int & ans = dis[s];//引用ans來表示dis[s]
    if(ans!=-1)
        return ans;

      ans = infinite;//將其設定為一個大數(試想如果將ans設為一個極小的數則ans將一直很小)
      for(int i = 0 ;i<n ; i++)//對每一個硬幣面額進行匹配
        if(s>=v[i])
        ans = min(ans,dp(s-v[i])+1);//不斷縮小問題規模,找到最短路徑(dp會一直遞迴到s=0,此時dis[0] = 0保證使得ans會等於後一個的值,從而ans將由infinite變為從0開始)
      return ans;

}
int main()
{
    cin>>n>>value;
    for(int i = 0 ; i<n ; i++)
        cin>>v[i];
     memset(dis,-1,sizeof(dis));
     dis[0] = 0;//一定要賦初值(動態規劃的思想是由從小到大,因此必須有至少一個最小的問題的解已經得出
     cout<<dp(value)<<endl;

    return 0;
}

2.遞推法(利用刷表法思想,對每一個硬幣面額所影響的狀態進行維護,d[t] : 表示以t結束的最短路徑長度)

#include<bits/stdc++.h>

#define MAX 500
using namespace std;

int main()
{

    int n,value;
    int v[MAX];
    int d[MAX];
    cin>>n>>value;

    for(int i = 0 ; i<n ; i++)
     cin>>v[i];

     memset(d,0x3f3f3f,sizeof(d));//初始化為一個很大的數
     d[0] = 0;//注意遞推的終點必須事先得到

     for(int i = 0 ; i<n ; i++)//對每個硬幣影響到的狀態進行維護(更新)
        for(int j = value; j>=0 ; j--)//從待湊面值到 0進行遍歷(反過來沒有影響),確保對每一個狀態都進行更新
        d[j] = min(d[j],d[j-v[i]]+1);

         cout<<d[value];
    return 0;
}

5.總結;動態規劃和函式遞迴在解決問題的思路上有異曲同工之妙,它們的聰明之處在於不直接對複雜的問題進行處理,而是在保持問題本質不發生變化的基礎上將問題的規模不斷縮小,到達一個臨界點時,複雜的問題被化為簡單問題解決,這個過程中,複雜問題所依賴的所有問題都已得解,從而自然而然得到最終問題的解。一般動態規劃最小的解是容易得到的,而且一定要保證在將問題規模縮小之前獲得該解。對比函式遞迴,臨界點就是結束遞迴的一個條件,滿足這個條件時,對最小問題求解,然後再回溯。前者先得最小解,後者遇到最小問題再求最小解。如本題所示,最小解為dis[0] = 0,已事先儲存好。