淺談貪心與動歸

lidasu發表於2019-05-31

淺談貪心與動歸

初學時 想必都會對兩者的認識有一些混淆
概念性質的就不贅述了
來談談我在刷題過程中對兩者的見解(誠心接受各位的指正)
從搜尋到貪心——求解演算法的優化 這篇文章非常值得一看
P1478 陶陶摘蘋果(升級版) 對應的oj題目

對比

貪心像是動歸的一個特例
動歸的核心在於:狀態轉移,找出那個轉移方程
貪心的核心在於:區域性選取最優解,並且選取的貪心策略不會影響到其他的狀態

用01揹包舉個例子

在n件物品取出若干件放在空間為c的揹包裡,每件物品的體積為w1 w2...wn,與之相對應的價值為v1 v2...vn,最終使揹包所裝物品的總價值最高
程式碼如下,基本上大家都會寫

int dp[i][j]; //dp[i][j] 表示取到第i個物品,揹包容量為j
int pack01(int v[],int w[],int n,int c){ //value weight number capacity 
    for(int i=1;i<=n;++i)
        for(int j=1;j<=c;++j){
            if(w[i]>j) dp[i][j]=dp[i-1][j];
            else dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
        }
}
空間優化 一維
for(int i=1;i<=n;i++)
    for(int j=c;j>=1;j--){ //注意從後往前
        if(w[i]<=j){ //二維變一維
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
更簡潔
for(int i=1;i<=n;i++)
    for(int j=c;j>=w[i];j--){
        dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
    }

但如果將v1 v2...vn這些每個物品對應的價值都變成1(v1=v2=...=vn=1)
這樣一來,每個物品的價值都相同。無論你有多重,你的價值和別人都一樣
很顯然,我們只要把物品的重量進行排序,先拿輕的再拿重的,結果必然最優

動歸加上一個特例 就這樣成為了貪心可解決的題目
並且時間複雜度O(n)直接下降到排序的nlogn

再來舉個更接地氣的栗子

找紙幣

基本上每個國家設計的貨幣都是符合貪心原則的
我國的紙幣面額分別為:100元、50元、20元、10元、5元、2元、1元
當需要找錢給別人時,先找大面值的紙幣再找小面值的,最後一定是紙幣數量最少的

但如果面額是1元、5元、11元的紙幣
當你找別人15元時,按照貪心規則,找的是一張11元和4張1元的,一共5張
而正確答案顯而易見是3張5元的,一共3張
這就不符合貪心策略了,這時怎麼來找到最少的?這就需要用到動歸了

這其實是完全揹包(每件物品可以取多次)的模板
必須把揹包裝滿,即正好找出零錢,不多也不少(會影響初始化,文末見揹包九講)
每個物品價值 v[i]=1,並且不是總價值最大,而是紙幣數量最小 max->min

int m[4]={0,1,5,11};
int dp[5][20]; //dp[i][j] i表示紙幣種類,j表示找回的零錢
//轉移方程 dp[i][j]=min(dp[i-1][j],dp[i][j-m[i]]+1);
#define inf 10000 //若揹包則是-∞
//初始化
for(int i=0;i<=3;++i)
    for(int j=0;j<=15;++j){
        if(j==0) dp[i][j]=0;
        else dp[i][j]=inf;
    }

for(int i=1;i<=3;++i){
    for(int j=0;j<=15;++j){
        if(j>=m[i]) dp[i][j]=min(dp[i-1][j],dp[i][j-m[i]]+1);
        else dp[i][j]=dp[i-1][j];
        //請注意轉移方程中dp[i][j-m[i]]+1裡的[i]
        //而01揹包是[i-1],這是物品能否取多次的關鍵所在
    }
}
cout<<dp[3][15];

空間優化

int m[4]={0,1,5,11};
int dp[20];
#define inf 10000
for(int j=0;j<=15;++j){
    if(j==0) dp[j]=0;
    else dp[j]=inf;
}
for(int i=1;i<=3;++i){
    for(int j=m[i];j<=15;++j){
        dp[j]=min(dp[j],dp[j-m[i]]+1);
    }
}
cout<<dp[15];

如果是第一種問法,要求恰好裝滿揹包,那麼在初始化時除了 F[0] 為 0 ,其它
F[1..V ] 均設為 −∞ ,這樣就可以保證最終得到的 F[V ] 是一種恰好裝滿揹包的最優解。
如果並沒有要求必須把揹包裝滿,而是隻希望價格儘量大,初始化時應該將 F[0..V ]
全部設為 0 。
這是為什麼呢?可以這樣理解:初始化的 F 陣列事實上就是在沒有任何物品可以放
入揹包時的合法狀態。如果要求揹包恰好裝滿,那麼此時只有容量為 0 的揹包可以在什
麼也不裝且價值為 0 的情況下被“恰好裝滿”,其它容量的揹包均沒有合法的解,屬於
未定義的狀態,應該被賦值為 -∞ 了。如果揹包並非必須被裝滿,那麼任何容量的揹包
都有一個合法解“什麼都不裝”,這個解的價值為 0 ,所以初始時狀態的值也就全部為 0
了。
取自《揹包問題九講》1.4 初始化的細節問題

相關文章