1. 程式人生 > >動態規劃之鋼條切割問題

動態規劃之鋼條切割問題

歡迎關注,定期更新演算法問題!

這一篇來討論一下關於動態規劃的一些問題。

動態規劃和分治方法類似,都是組合子問題的解來求解原問題,但是兩者不同的是分治方法將問題化為互不相交的子問題,遞迴的求解子問題,再將子問題的解組合起來求解原問題;而動態規劃應用於子問題重疊的情況,即不同的子問題有公共的子子問題。所以在這種情況下,分治方法會重複計算那些公共子問題,但是動態規劃對公共子問題只求解一次。

動態規劃通常來求解最優化問題,這類問題通常有多個可行解,我們希望尋找最優解,我們稱這樣的解為一個最優解,因為問題可能有多個最優解。我們通常按照下面4個步驟來設計演算法:

(1)、刻畫一個最優解的結構特徵;

(2)、遞迴的定義最優解的值;

(3)、計算最優解的值,通常採用自底向上的方法;

(4)、利用計算出的資訊構造一個最優解。

先來看第一個例子:鋼條切割問題

問題簡述:某公司購買長鋼條,希望將長鋼條切割成短鋼條出售,已知切割工序不耗費費用,下表給出了價格與長度的一個對應關係,現在希望得到一個最優的切割方案。


長度為n的鋼條有2^(n-1)種切割方案,因為在距離鋼條左端距離為i處,我們要麼切割要麼不切割,只有兩種方案。假設最優切割方案為k段,那麼最大的收益就是這k段效益的和。

為了求解原問題,首先求解形式相近,規模更小的子問題,即首次切割將長度為n的鋼條切割成長度為i和n-i的鋼條,再將這兩個鋼條看成相互獨立的問題切割,我們通過組合兩個問題的最優解,選取效益最大的構成原問題的解,這樣可以認為此問題滿足最優子結構性質:即原問題的最優解可以由子問題的最優解組成,而子問題可以獨立求解。

用上述思想求解,其實子問題是劃分為左、右兩部分進行的,當然可以有一種簡化的方法,即子問題只從右部分劃分進行,思路就是:將長度為n的鋼條劃分為長度為i的左部分和長度n-i的右部分,左半部分不在劃分,只是將右半部分繼續劃分。

下邊給出這種樸素遞迴方法的實現:

DataType Cut_Rod(DataType p[],int n)
{
    if(n==0)
        return 0;
    DataType q=0;
    for(int i=0;i<n;i++)
        q=max(q,p[i]+Cut_Rod(p,n-i-1));
    return q;
}

此程式可以正確的求解問題的解,但問題是隨著資料的增大,執行時間會爆炸性的增長,原因就是程式重複的計算相同的子問題,可以看下圖這個樹結構理解:


上述遞迴樹結點的標號表示問題的規模,,根結點到子結點的邊表示切割方案,由於此演算法是由頂到底的遞迴,故此重複的計算了大量相同的子問題,可以證明花費的時間為2的n次方冪,這個執行時間是相當恐怖的,下邊我們考慮更高效的動態規劃演算法。設計演算法時可以考慮合理安排子問題順序,使得子問題只求解一次,並且將子問題的解儲存下來,等到後邊用到時再利用,相比原來演算法只是花了更多的記憶體,其實就是用空間換時間的思想。

有兩種實現方式:

(1)、帶備忘的自頂向下遞迴法:此方法和上述樸素方法遞迴方向是一樣的,但是區別就是會儲存子問題的解,當計運算元問題時首先檢視是否計算過此問題,如果計算過,則直接利用,否則,用常規方法計算。

//帶備忘的自頂向下遞迴
DataType Cut_Rod_upTodown(DataType p[],int n,DataType Subvalue[])
{
    if(n==0)
        return 0;
    DataType q=0;
    for(int i=0;i<n;i++)
    {
        if(Subvalue[n-i-1]==0)
        {
            q=max(q,p[i]+Cut_Rod_upTodown(p,n-i-1,Subvalue));
        }
        else
        {
            q=max(q,p[i]+Subvalue[n-i-1]);
        }
    }
    return q;
}

如果你的編譯器是gcc等,會可以直觀的看到執行時間的差別。

(2)、自底向上遞迴法:仔細看上邊的樸素演算法遞迴樹,自頂向下的遞迴可以看成樹從左半部分開始遞迴,但是我們考慮讓其從右半部分遞迴,即對子問題的求解依賴於規模更小的子問題的求解,我們可以把問題的規模從小到大排序,這樣程式的求解從更小的子問題開始,然後再去計算較大的子問題,這樣每個子問題只計算一遍。

//自底向上的演算法
DataType Cut_Rod_downToup(DataType p[],int n)
{
    DataType value[5]={0};
    for(int i=1;i<=n;i++)
    {
        DataType q=0;
        for(int j=1;j<=i;j++)
        {
            q=max(q,p[j]+value[i-j]);
        }
        value[i]=q;
    }
    return value[n];
}
上述演算法兩層迴圈,思路就是當計算規模為i的收益時,計算規模比i小的子問題的最優解,即第二層迴圈,可以簡單的理解成從小到大一直都是最優的話,最終就是最優的。

上述三個演算法解決了輸出效益的問題,但是如何輸出劃分方案呢?下邊進行一次重構解,將結果輸出

//重構輸出解
void print_Cut_Rod_downToup(DataType p[],int n,DataType value[],int result[])
{
    for(int i=1;i<=n;i++)
    {
        DataType q=0;
        for(int j=1;j<=i;j++)
        {
            if(q<(p[j]+value[i-j]))
            {
                q=p[j]+value[i-j];
                result[i]=j;
            }
        }
        value[i]=q;
    }
}
結果保留在result中

好了,這篇文章到此結束。

最後附上全部測試程式碼:(注意測試程式碼給出的問題的規模為4)

#include <iostream>
typedef int DataType;
using namespace std;
//樸素遞迴演算法
DataType Cut_Rod(DataType p[],int n)
{
    if(n==0)
        return 0;
    DataType q=0;
    for(int i=0;i<n;i++)
        q=max(q,p[i]+Cut_Rod(p,n-i-1));
    return q;
}
//帶備忘的自頂向下遞迴
DataType Cut_Rod_upTodown(DataType p[],int n,DataType Subvalue[])
{
    if(n==0)
        return 0;
    DataType q=0;
    for(int i=0;i<n;i++)
    {
        if(Subvalue[n-i-1]==0)
        {
            q=max(q,p[i]+Cut_Rod_upTodown(p,n-i-1,Subvalue));
        }
        else
        {
            q=max(q,p[i]+Subvalue[n-i-1]);
        }
    }
    return q;
}
//自底向上的演算法
DataType Cut_Rod_downToup(DataType p[],int n)
{
    DataType value[5]={0};
    for(int i=1;i<=n;i++)
    {
        DataType q=0;
        for(int j=1;j<=i;j++)
        {
            q=max(q,p[j]+value[i-j]);
        }
        value[i]=q;
    }
    return value[n];
}
//重構輸出解
void print_Cut_Rod_downToup(DataType p[],int n,DataType value[],int result[])
{
    for(int i=1;i<=n;i++)
    {
        DataType q=0;
        for(int j=1;j<=i;j++)
        {
            if(q<(p[j]+value[i-j]))
            {
                q=p[j]+value[i-j];
                result[i]=j;
            }
        }
        value[i]=q;
    }
}
int main()
{
    DataType p[10]={1,5,8,9,10,17,17,20,24,30};
    //p1是演算法三的效益測試值,由於演算法的特殊性,故此第一項是0
    DataType p1[11]={0,1,5,8,9,10,17,17,20,24,30};
    DataType Subvalue[4]={0};
    DataType value[5]={0};
    int result[5]={0};
    int n=4;
    DataType Got1=Cut_Rod(p,n);
    DataType Got2=Cut_Rod_upTodown(p,n,Subvalue);
    DataType Got3=Cut_Rod_downToup(p1,n);
    print_Cut_Rod_downToup(p1,n,value,result);
    cout<<"長度為4的鋼條最大收益:"<<"第一種:"<<Got1<<endl<<"第二種:"<<Got2<<endl<<"第三種:"<<Got3<<endl;
    cout<<"解的長度:"<<endl;
    for(int i=0;i<=n;i++)
        cout<<result[i]<<endl;
    return 0;
}