最近編程題目都是DP,我把今天這個題給大家做個解釋和實現。大家好好體會練習,遇到最優解問題(最小/最大)時,優先DP解法。
概念
一般求最優解通常使用DP(Dynamic Programming)。
DP主要兩個要素,有三種解法。
- 兩要素
- 定義狀態
- 定義狀態轉化方程
狀態和狀態轉化方程最關鍵也比較困難。狀態轉化方程包含邊界。有時稱為三要素,是把邊界單獨分離出來,與狀態和狀態轉化方程并列。
- 三種解法
- 遞歸法
遞歸法是直接使用狀態轉化方程,實際算不上DP,而是減而治之,在重疊子問題時,效率低下(時間復雜度O(2^n)
)。 - 備忘錄法
備忘錄法實際上是對遞歸法優化,通過增加存儲空間保存中間運算值,減少遞歸運算次數。 - 表格法
這是最正統的DP,遞歸法和備忘錄法都是自頂而下的遞歸,表格法是自底而上的迭代。DP本質是把遞歸變為迭代,降低時間復雜度。
- 遞歸法
題目
時間限制:(每個case)2s,空間限制:128MB
小Q從牛博士那里獲得一臺數字轉化機,這臺數字轉化機必須同時輸入兩個整數a
和b
,并且這臺數字轉化機有一個紅色按鈕和一個藍色按鈕。
- 當按下紅色按鈕,兩個數字同時加一。
- 當按下藍色按鈕,兩個數字同時乘二。
小Q現在手中有四個整數,a
、b
、A
和B
,他希望將輸入的兩個整數a
、b
變成A
、B
(a
對應A
,b
對應B
)。因為牛博士允許小Q使用數字轉化機時間有限,所以,小Q希望按動按鈕的次數越少越好,請你幫幫小Q吧。
- 輸入:
輸入包括一行,一行中包含四個整數a
,b
,A
,B
。(1<=a,b,A,B<=10^9) - 輸出:
如果小Q能夠完成轉換,輸出最少需要按動按鈕的次數,否則輸出-1
。 - 樣例輸入:
100 1000 202 2002
- 樣例輸出:
2
分析
- 定義狀態
transfer(first,last)
表示從數字first
轉化到數字last
最少轉化次數 - 定義狀態轉移方程
自頂而下分析,最后一步轉化無非兩種情況,前一步結果*2
或者+1
,那么前一次結果為last/2
或者last-1
。最后一步經歷的轉化次數無非是transfer(first,last/2)
或者transfer(first,last-1)
。顯然可得,取最小的transfer(first,last) = min(transfer(first,last/2),transfer(first,last-1))+1
遞歸邊界最終會有兩種情況,一種是first
剛好與last
相同,說明這是可以轉換。一種是first>last
,說明不能完成轉化。
結合上面兩點狀態轉移方程如下所示:
解決
為了使代碼更加簡潔地表示解決思路,中間未添加參數檢查處理。
測試主函數
本題測試函數非常簡單只是終端輸入輸出操作。
int main(){
int a(0),b(0),A(0),B(0);
cin >> a >> b >> A >> B;
int res1 = transfer(a,A);
int res2 = transfer(b,B);
if(res1 == res2 and -1 != res1){
cout << res1;
}else{
cout << -1;
}
}
實現轉換函數transfer()
- 遞歸法
直接把狀態轉換函數翻譯成代碼即可。
int transfer(int first,int last){
if(first == last) return 0; // 可以完成轉換
if(first > last) return -1; // 不能完成轉換
int res1 = transfer(first,last/2);
int res2 = transfer(first,last-1);
if(-1 == res1 and -1 == res2){
return -1;
}else if(-1 == res1){
return res2+1;
}else if(-1 == res2){
return res1+1;
}else{
return min(res1,res2)+1;
}
}
注意,轉化失敗的分支不能參與最小轉化次數比較的。程序需要增加這些處理。
- 備忘錄法
在遞歸過程中,記錄計算結果,下次出現時不需要重新計算,直接從存儲結果中獲取即可。
int transfer(int first,int last,int buf[]){
if(first == last) return 0; // 可以完成轉換
if(first > last) return -1; // 不能完成轉換
if(-1 != buf[last]){
return buf[last];
}
int res1 = transfer(first,last/2,buf);
int res2 = transfer(first,last-1,buf);
if(-1 == res1 and -1 == res2){
return -1;
}else if(-1 == res1){
buf[last] = res2+1;
}else if(-1 == res2){
buf[last] = res1+1;
}else{
buf[last] = min(res1,res2)+1;
}
return buf[last];
}
int transfer(int first,int last){
int buf[last+1];
fill_n(buf,last+1,-1);
return transfer(first,last,buf);
}
注意這里定義一個數組,數組的下標表示last
,數組值表示從數字first
轉化到數字last
最少轉化次數。數組需要初始化為無效值-1
,用于判斷是否已經計算過。
- 表格法
表格法用自底而上的迭代代替自頂而下的遞歸。
int transfer(int first,int last){
int buf[last+1];
fill_n(buf,last+1,0);
for(int i=first+1;i<=last;i++){
if(i/2 >= first and i-1 >= first){
buf[i] = min(buf[i/2],buf[i-1])+1;
}else if(i/2 >= first){
buf[i] = buf[i/2]+1;
}else if(i-1 >= first){
buf[i] = buf[i-1]+1;
}else{
buf[i] = -1;
}
}
return buf[last];
}
這里數組表示含義與備忘錄法一致的,雖然會有一定空間浪費,時間復雜度,已經從O(2^n)
降低到O(n)
。
通常有時間限制的DP,優先使用表格法,備忘錄法在一些情況會比表格法快,基本不使用遞歸法。
使用回溯法可以把表格法和備忘錄法獲得的存儲中間值,轉化成獲得最優解具體的操作,例如本題中,小Q究竟是依次按了哪些按鈕獲得last
值。有能力的童鞋,可以自己練習解決。