動態規劃---砝碼稱重問題
一、演算法分析
動態規劃(Dynamic Programming)這個詞乍一聽感覺甚是高大上,初次學習或者使用的時候會感覺難以理解,這是正常的,畢竟凡事都是一回生二回熟。其實它也不難的,大家要明白一個道理,能寫到課本上給學生學習的東西必然屬於不難的東西,因為太難的東西寫到課本上讀者接受不了,這本書就沒有出版的意義了。當然我說的不難也僅僅只是說動態規劃的思想不難,因為我們常常面臨著一個棘手的問題---那就是這個理論應用到實踐中的時候,它沒有一個公式或者現成的可以直接用來套用的模型。對於這一點,沒有辦法,只能是通過多種案例,自己多總結,多思考,看看每一種能用動態規劃理論解決的問題,狀態模型究竟是如何建立的,而這就需要讀者多多練習了。
本文通過華為OJ上一個基本題-砝碼稱重問題來讓初學者消化動態規劃。
先來讀一讀題目,題目如下:
現有一組砝碼,重量互不相等,分別為m1、m2……mn;他們可取的最大數量分別為x1、x2……xn。現在要用這些砝碼去稱物體的重量,問能稱出多少種不同的重量。
看完這個題目感覺似乎無從下手,難道要一個個的列舉拼湊嗎?答案是否定的。不繞彎子了,下面直接進入動態規劃的主場。我們假定砝碼的重量單位是克,下文中不新增這個單位,只說數字,比如重量是1,表示重量是1克。
設想第0種情況,假如現在只有0個砝碼,問它能稱出的重量有幾種?是個人都知道只能稱0的重量,並且只有這麼1種情況。
設想第1種情況,現在只有1種規格的砝碼,它的重量是1,數量是1個,問它能稱出的重量有幾種?很明顯,能稱出0和1這2種重量,一共2種情況。
設想第2種情況,現在有2種規格的砝碼,它們的重量分別是1和2,1克的砝碼1個,2克的砝碼1個,問它能稱出的重量有幾種?用手比劃比劃應該能知道,能稱出0,1,2,3這4種重量,4種情況。
。。。
。。。
設想第n種情況,現在有n種規格的砝碼,他們的重量分別為m1,m2。。。mn,問它能稱出的重量是幾種?這個時候貌似不是用手比劃比劃就可以出來結果了。但是這個問題的提出,就相當於是題目最終要解決的問題了。如果能夠利用某種規律回答這第n種情況的提問,那麼砝碼稱重的問題就可以完全解決。問題來了,上面的分析過程是否存在一個規律呢?答案是規律肯定存在,並且分析這種規律的思路就是動態規劃的思路。
現在我們把第n種情況能稱出的重量種數用f(n)來表示,可以試著這樣具體化的描述,第0種情況能稱出的重量有f(0)種,第1種情況能稱出的重量有f(1)種,第2種情況能稱出的重量有f(2)種,。。。第n種情況能稱出的重量有f(n)種。
下面,迴歸到上面的提到的情況分析中去。
第0種情況,f(0)=1;
第1種情況,f(1)=2;如果把這個結果和第0種情況結合起來看,可以認為:f(1)=f(0)+1,此式意義非凡,它的意義為:第1種情況可以建立在第0種情況的基礎上去解決,單單來看情況1的砝碼可以稱1種重量(即用一個1克的砝碼稱1克的物體)不同於情況0中,二者相加即為f(1)的結果。
第2種情況,f(2)=4=f(1)+2,為什麼加2?因為如果僅僅只有1個1克的砝碼和1個2克的砝碼,它只能稱出重量為2克,3克這兩種不同於情況1中的重量(情況1中已經解決了0克和1克的問題)。
現在假定已經解決了第n-1種情況,即f(n-1)的值已經知道,那麼我們如果能計算出單單第n種情況能稱出哪些不同於n-1種情況中的重量,假定為x種,那麼f(n)=f(n-1)+x。最終問題便得到了解決。
請仔細領會上面的這個分析過程,一定要弄明白。
下面附上C++的程式,本程式先計算第0種砝碼能稱出多少種重量,能稱出的重量均在flag[100000]陣列中做好標記,然後計算第0種砝碼最多能稱多大的重量,在這個基礎上加入第1種砝碼,然後計算第2種,直至第n種。程式最關鍵的地方在於29行採用CurrentWeight逐次加1試探的方式得到一個數值,然後在33行驗證這個CurrentWeight是否是前一種情況中的砝碼可以稱出來的重量,如果是,表明新的重量值確實可以稱出來,那麼就把這個重量在flag[100000]陣列中標記。最後統計標誌陣列flag[100000]種1的次數,即為所有砝碼隨意組合可以稱出的重量。
二、參考程式碼#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
int fama(int N, int weight[], int num[])
{
int AllWeight = 0, i, j;
for (i = 0; i < N; i++)
AllWeight = AllWeight + weight[i] * num[i];
int flag[100000] = {0};
/*先計算第0種砝碼能夠得到的稱重數量,並且計算第0種砝碼最大的重量*/
int TempWeight = 0;
for (i = 0; i <=num[0]; i++)
{
flag[weight[0] * i] = 1;
}
TempWeight = weight[0]*num[0];
//從此以後TempWeight將用來表示前一種砝碼的最大重量
i = 1;//從第1種砝碼開始做
int CurrentWeight;
int NewWeight;
while (i < N)
{
for (j = 1; j <=num[i]; j++)//第i種砝碼的個數最多為num[i]
{
for (CurrentWeight = 0; CurrentWeight <=TempWeight; CurrentWeight++)//CurrentWeight採用試探的方式逐次加1它的大小不能大於前一種重量的最大值
{
NewWeight = CurrentWeight + j*weight[i];
if (NewWeight>AllWeight) break;
if (flag[CurrentWeight==1]&&flag[NewWeight]==0)//如果當前的這個重量可以由前一種砝碼和當前砝碼組合而成
{
flag[NewWeight] = 1;
}
}
}
TempWeight = TempWeight + num[i] * weight[i];//更新上一個砝碼的最大重量
i++;
}
/*統計總數量*/
int count = 0;
for (i = 0; i < 1000; i++) { if (flag[i] == 1) count++; }
return count;
}
int main()
{
int n;
cin >> n;
int *w = new int[n];
int *num = new int[n];
for (int i = 0; i < n; i++)
cin >> w[i];
for (int i = 0; i < n; i++)
cin >> num[i];
cout <<fama(n,w,num)<<endl;
return 0;
}