本文爲原創,轉載請說明來源地址:http://blog.csdn.net/ljj583905183/article/details/40937021
問題描述:
一家公司購買長鋼條,將其切割成短鋼條出售,切割本身沒有成本,長度爲i的短鋼條的價格爲Pi。那給定一段長度爲n的鋼條和一個價格表Pi,求鋼條的切割方案使得收益Rn最大。如一個Pi如下:
長度i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
價格Pi | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
在距離鋼條左端i長度處,我們總是可以選擇切割或者不切割,所以長度爲n的鋼條共有2的n-1次方中不同的切割方案.
分析:
假設我們把鋼條分割成k個部分,每個部分的長度分別爲,則我們可以列出其最優化方程:
這自然就想到了運籌學中的動態規劃。說到動態規劃,最經典的莫過於最優路徑問題。最短路徑問題中有一個典型特徵就是從最短路徑中任找一點到終點的路徑也是最短的,這很容易明白,如果還有更短的,那它就不是最短的了。這裏一定是路徑中任何一點到終於,而不是任意兩點。具體描述如下:
假如最短路徑是A-B-C-D-E,那麼D-E,C-D-E,B-C-D-E也是最優的。這樣,這個特徵就給我們一個思路,就是如果倒着尋找最優路徑,每一步都是最優的,那麼最後一定也是最優的。即先找D-E,接着找C-D-E,B-C-D-E,這樣就找到我們想要的最優解了。這裏不要誤會是先D-E,然後找C-D,...,這樣就錯了,這就不符合上面的意思了。或許你還沒有理解是什麼意思,我再舉個例子。
上圖中,要找到從1到10的最短距離,我們倒着找。
第一層:8-10的最短距離是8,9-10的最距離是4
第二層:5到10的最短距離,比較“5-8的最短距離+8-10的最短距離”,和“5-9的最短距離+9-10的最短距離”。由於8-10,9-10的最短距離已經在第一層中求出,所以不用再求了,這樣很快就求出5-10的距離;同樣6-10,7-10的最短距離也就求出來了。
第三層:2-10的最短距離,比較“2-5的最短距離+5-10的最短距離,和"2-6的最短距離+6-10的最短距離"。由於5-10,6-10的最短距離已經在第二層求出,所以也不用再求了。這樣很快求出,2,3,4-10的最短距離。
第四層和上面一樣。
到這裏,大家應該明白我的意思了。同時從計算角度來說,減小了很多計算量,因爲沒有重複計算。上面就是動態規劃的核心思想,大的方面來講就是分治思想,大事化小,小事化了的意思;小的方面來講,就是倒推理,像破案一樣,倒着推理,一個細節也不放過。
如果你還是不明白,覺得應該是每步都是最優的,就應該每一步都是最優的。這麼想也並非不是一個備用方案,貪心法就是這個原理。如果要從數學中找相應的例子,就是最速下降法的每步都是按下降最快的逆梯度方向下降,但整體來看,並非是下降最快的方向。
明白了上面的道理,我們回頭來分析我們的鋼條切割問題。我們也可以大事化小,可以像最短路徑一樣分層分析,如果分爲一段,二段,三段和四段,就達到分層效果了。但我們如果去利用分層去解決我們的問題呢?我們一層一層地慢慢道來。
如果分爲一層,很明顯就是9元,如果分爲二層,則有4種分法(考慮方向),即1-3,2-2,3-1,當兩部分都達到最優時,兩者合起來纔是最優的,這樣我們就把問題轉到每一部分的最優上了,分析方式和上面一樣。如果用公式表示就是:
相信大家,立刻就知道怎麼做的了吧,從編程角度來講,太符合遞歸的原理了,這樣我們就很容易寫程序了。但你真正開始寫程序的時候,發現沒這麼簡單,如果總這麼遞歸下去,能夠用的數就是a1了,其它的都沒用,很顯然這是不對的。解決問題,得要先知道問題出在哪,很顯然問題出在遞歸的終止條件上了,不能總返回a1,應該能夠返回所有的a值。但這樣就麻煩了,只有當分成0-n這個部分是遞歸有返回值,其它的情況就要繼續遞歸,所以這樣就得到其程序代碼了,這裏用C語言描述:
static int k=0;
int steer(int p[],int r[],int n)
//p爲每個長度的價格,n爲長度
//r爲輸出,保存每個長度的最優價格
{
if(n==0)
return 0;
r[n-1]=p[n-1];
for(int i=1;i<n-1;i++)
r[n-1]=MAX(r[n-1],steer(p,r,i)+steer(p,r,n-i));
k++;
cout<<r[n-1]<<" ";//這裏可以看下迭代了多少次
return r[n-1];
}
當n=4時
當n=5時
從上面的結構可以看出,很多長度重複了很多次,浪費了時間
那就繼續改進唄。我們再來分析,我們分析發現,1-3和3-1是一樣的,也就是說對稱的。也就是說a(i)就沒必要了,直接用p[i]代替即可,所以就有如下程序:
static int k=0;
int steer(int p[],int r[],int n)
{
if(n==0)
return 0;
//r[n-1]=p[n-1];
for(int i=0;i<n;i++)
r[n-1]=MAX(r[n-1],p[i]+steer(p,r,n-i-1));
cout<<r[n-1]<<" ";
k++;//記錄迭代次數
return r[n-1];
}
當n=4時
當n=5時
//兩者的比較如下表:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
第一種 | 1 | 1 | 3 | 13 | 45 | 81 | 413 | 865 |
第二種 | 1 | 2 | 4 | 10 | 24 | 42 | 161 | 298 |
很顯示第二種對第一種有了比較大的改進,但仔細看,它們的迭代次數並非算法導論裏寫的,而是更大。當然有可能是程序出了點錯誤,希望讀者指出。算法的複雜度,算法導論內分析的很清楚,通過樹進行分析的,有興趣的同學可以去看看。
現在繼續改進算法。從迭代結果來看,很多時候,我們重複計算了很多次,這就浪費了時間。如果要是不重複計算呢,把每次的結果都保存下來,這樣時間複雜度要少很多。怎麼樣做呢?就像最短路徑分析的那樣,每層計算的時候,利用上層已經計算的最優值,這樣就可以了。所以計算每層時,要保存其值,然後在迭代的時候,判斷其是否已經存在,程序如下:
static int k=0;
int steer(int p[],int r[],int n)
{
if(n==0)
return 0;
if(r[n-1]>0)
return r[n-1];
int q=0;
for(int i=0;i<n;i++)
q=MAX(q,p[i]+steer(p,r,n-i-1));
r[n-1]=q;
cout<<r[n-1]<<" ";
k++;//記錄迭代次數
return r[n-1];
}
當n=4時
當n=5時
很明顯,時間複雜度就就變成n了,效率大大提高了。如果你學習過運籌學裏的動態規劃,你就知道動態規劃算法裏有兩種,一種是逆推解法,另一種是順推解法。上面的最短路徑使用的是逆推解法,而下面的程序採用的是順推解法。仔細思考的話,逆推解法是不需要遞歸的,只需要倒着一步步往上推就可以了。逆推解法的程序如下:
static int k=0;
void steer(int p[],int r[],int n)
{
r[0]=0;
int q;
for(int i=1;i<n+1;i++)
{
q=0;
for(int j=0;j<i;j++)
q=MAX(q,p[j]+r[i-j-1]);
r[i]=q;
cout<<r[i]<<" ";
k++;
}
}
當n=5時
以上皆用到
#define MAX(X,Y) ((X)>(Y))?(X):(Y)
如果要記錄如何切割的方案,那只需要在MAX時記住最大值處的索引即可。相信在這裏,讀者有了很清楚的瞭解了,這裏是從開始到最後的所有思路,告訴你我是怎麼分析的,而不是隻有結論。